From 4cc8f9c7da3b21efde902756b3c667aa56a4f753 Mon Sep 17 00:00:00 2001 From: leesangdal Date: Mon, 7 Jul 2025 17:36:30 +0900 Subject: [PATCH 01/16] =?UTF-8?q?style:=20Footer=20=EB=B0=98=EC=9D=91?= =?UTF-8?q?=ED=98=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/Footer/FooterStyle.js | 2 + src/pages/main/MainPage.tsx | 202 ++++++++++++------------ src/styles/variables.css | 6 +- 3 files changed, 105 insertions(+), 105 deletions(-) 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/pages/main/MainPage.tsx b/src/pages/main/MainPage.tsx index f789aeba..0ab3df4a 100644 --- a/src/pages/main/MainPage.tsx +++ b/src/pages/main/MainPage.tsx @@ -17,121 +17,115 @@ const MainPage = () => { const navigate = useNavigate(); return ( - <> -
-
-
-
-

일상의 모든 물건을 거래해 보세요

- -
- 팬더가 파란 장바구니를 메고 마을 가운데에 서있는 일러스트 +
+
+
+
+

일상의 모든 물건을 거래해 보세요

+
+ 팬더가 파란 장바구니를 메고 마을 가운데에 서있는 일러스트
-
-
-
- 두 마리의 팬더가 인기 상품인 초록색 티셔츠를 보고 있는 일러스트 -
-
Hot item
-

인기 상품을 확인해 보세요

-

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

-
-
-
-
-
- 돋보기로 상품들 중 가운데 상품을 확대해서 보는 일러스트 -
-
Search
-

- 구매를 원하는 상품을 검색하세요 -

-

- 구매하고 싶은 물품은 검색해서 -
- 쉽게 찾아보세요 -

-
-
-
-
-
- 아래에 폴더들이 있고, 그 위에 마법봉으로 가운데의 연필꽂이, 공책, 하트 프레임 안경을 가리키는 일러스트 -
-
Register
-

- 판매를 원하는 상품을 등록하세요 -

-

- 어떤 물건이든 판매하고 싶은 상품을 -
- 쉽게 등록하세요 -

-
+
+
+
+
+ 두 마리의 팬더가 인기 상품인 초록색 티셔츠를 보고 있는 일러스트 +
+
Hot item
+

인기 상품을 확인해 보세요

+

+ 가장 HOT한 중고거래 물품을 +
+ 판다 마켓에서 확인해 보세요 +

-
-
-
-
-
-

- 믿을 수 있는 +

+
+
+
+ 돋보기로 상품들 중 가운데 상품을 확대해서 보는 일러스트 +
+
Search
+

구매를 원하는 상품을 검색하세요

+

+ 구매하고 싶은 물품은 검색해서
- 판다마켓 중고 거래 - + 쉽게 찾아보세요 +

+
+
+
+
팬더 두 마리가 파란 장바구니를 메고 서로 상품 후기를 주고받는 일러스트 +
+
Register
+

판매를 원하는 상품을 등록하세요

+

+ 어떤 물건이든 판매하고 싶은 상품을 +
+ 쉽게 등록하세요 +

+
+
+
+
+
+
+
+

+ 믿을 수 있는 +
+ 판다마켓 중고 거래 +

+ 팬더 두 마리가 파란 장바구니를 메고 서로 상품 후기를 주고받는 일러스트
-
+
- +
); }; 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; } } From 496a761fb4c8f6e6864ad5cf2f7f54321a3f0b4a Mon Sep 17 00:00:00 2001 From: leesangdal Date: Mon, 7 Jul 2025 18:14:19 +0900 Subject: [PATCH 02/16] =?UTF-8?q?refactor:=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B0=B0=EB=84=88=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Banner/Banner.tsx | 38 +++++++++++++++++ src/pages/main/MainPage.tsx | 70 +++++++++++++++----------------- 2 files changed, 70 insertions(+), 38 deletions(-) create mode 100644 src/components/Banner/Banner.tsx diff --git a/src/components/Banner/Banner.tsx b/src/components/Banner/Banner.tsx new file mode 100644 index 00000000..b85a59f7 --- /dev/null +++ b/src/components/Banner/Banner.tsx @@ -0,0 +1,38 @@ +import React, { ReactNode } from "react"; + +interface BannerProps { + title: string | ReactNode; + imgSrc: string; + imgAlt: string; + linkBtn?: ReactNode; + ariaLabel?: string; + lazyLoading?: boolean; +} + +const Banner = ({ + title, + linkBtn, + imgSrc, + imgAlt, + ariaLabel, + lazyLoading, +}: BannerProps) => { + return ( +
+
+
+

{title}

+ {linkBtn} +
+ {imgAlt} +
+
+ ); +}; + +export default Banner; diff --git a/src/pages/main/MainPage.tsx b/src/pages/main/MainPage.tsx index 0ab3df4a..852848a8 100644 --- a/src/pages/main/MainPage.tsx +++ b/src/pages/main/MainPage.tsx @@ -12,33 +12,30 @@ 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"; const MainPage = () => { const navigate = useNavigate(); return (
-
-
-
-

일상의 모든 물건을 거래해 보세요

- -
- 팬더가 파란 장바구니를 메고 마을 가운데에 서있는 일러스트 -
-
+ navigate("/products")} + aria-label="상품 페이지로 이동" + variant="bannerPrimary" + size="lg" + > + 구경하러 가기 + + } + ariaLabel="상단 배너" + /> +
{
-
-
-
-

- 믿을 수 있는 -
- 판다마켓 중고 거래 -

-
- 팬더 두 마리가 파란 장바구니를 메고 서로 상품 후기를 주고받는 일러스트 -
-
+ + + 믿을 수 있는 +
+ 판다마켓 중고 거래 + + } + imgSrc={HomeBottomImg} + imgAlt="팬더 두 마리가 파란 장바구니를 메고 서로 상품 후기를 주고받는 일러스트" + ariaLabel="하단 배너" + lazyLoading={true} + />
); From a35a5da887b19208c0bdbd166e33d709d22deb7d Mon Sep 17 00:00:00 2001 From: leesangdal Date: Mon, 7 Jul 2025 19:55:00 +0900 Subject: [PATCH 03/16] =?UTF-8?q?refactor:=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=84=B9=EC=85=98=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Section/MainSection.tsx | 46 +++++ src/components/Section/MainSectionStyle.ts | 103 ++++++++++ src/pages/main/MainPage.tsx | 128 +++++------- src/pages/main/MainPageStyle.ts | 219 --------------------- 4 files changed, 196 insertions(+), 300 deletions(-) create mode 100644 src/components/Section/MainSection.tsx create mode 100644 src/components/Section/MainSectionStyle.ts 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/pages/main/MainPage.tsx b/src/pages/main/MainPage.tsx index 852848a8..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,98 +9,66 @@ 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 (
navigate("/products")} - aria-label="상품 페이지로 이동" - variant="bannerPrimary" - size="lg" - > - 구경하러 가기 - - } ariaLabel="상단 배너" + linkTo="/products" />
-
-
- 두 마리의 팬더가 인기 상품인 초록색 티셔츠를 보고 있는 일러스트 -
-
Hot item
-

인기 상품을 확인해 보세요

-

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

-
-
-
-
-
- 돋보기로 상품들 중 가운데 상품을 확대해서 보는 일러스트 -
-
Search
-

구매를 원하는 상품을 검색하세요

-

- 구매하고 싶은 물품은 검색해서 -
- 쉽게 찾아보세요 -

-
-
-
-
-
- 아래에 폴더들이 있고, 그 위에 마법봉으로 가운데의 연필꽂이, 공책, 하트 프레임 안경을 가리키는 일러스트 -
-
Register
-

판매를 원하는 상품을 등록하세요

-

- 어떤 물건이든 판매하고 싶은 상품을 -
- 쉽게 등록하세요 -

-
-
-
+ + 가장 HOT한 중고거래 물품을 +
+ 판다 마켓에서 확인해 보세요 + + } + imgSrc={HomeImg1} + imgMobileSrc={HomeImg1Small} + /> + + + 구매하고 싶은 물품은 검색해서 +
+ 쉽게 찾아보세요 + + } + imgSrc={HomeImg2} + imgMobileSrc={HomeImg2Small} + /> + + + 어떤 물건이든 판매하고 싶은 상품을 +
+ 쉽게 등록하세요 + + } + imgSrc={HomeImg3} + imgMobileSrc={HomeImg3Small} + />
Date: Mon, 7 Jul 2025 19:55:36 +0900 Subject: [PATCH 04/16] =?UTF-8?q?refactor:=20=EB=B0=B0=EB=84=88=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20linkBtn=20->=20linkTo?= =?UTF-8?q?=EB=A1=9C=20=EC=86=8D=EC=84=B1=20=EB=B0=8F=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Banner/Banner.tsx | 150 +++++++++++++++++++++++++++++-- 1 file changed, 144 insertions(+), 6 deletions(-) diff --git a/src/components/Banner/Banner.tsx b/src/components/Banner/Banner.tsx index b85a59f7..69bc3ff4 100644 --- a/src/components/Banner/Banner.tsx +++ b/src/components/Banner/Banner.tsx @@ -1,28 +1,43 @@ -import React, { ReactNode } from "react"; - +/** @jsxImportSource @emotion/react */ +import { css } from "@emotion/react"; +import { ReactNode } from "react"; +import { useNavigate } from "react-router-dom"; +import { BREAKPOINTS } from "@/constants/responsive"; +import Button from "@/components/ui/Button"; interface BannerProps { title: string | ReactNode; imgSrc: string; imgAlt: string; - linkBtn?: ReactNode; + linkTo?: string; ariaLabel?: string; lazyLoading?: boolean; } const Banner = ({ title, - linkBtn, + linkTo, imgSrc, imgAlt, ariaLabel, lazyLoading, }: BannerProps) => { + const navigate = useNavigate(); + return ( -
+

{title}

- {linkBtn} + {linkTo && ( + + )}
Date: Mon, 7 Jul 2025 23:00:07 +0900 Subject: [PATCH 05/16] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8,?= =?UTF-8?q?=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20-=20=EA=B0=84?= =?UTF-8?q?=ED=8E=B8=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SocialLogin/SocialLogin.tsx | 52 +++++++++++++++++++ .../SocialLogin/SocialLoginButton.tsx | 35 +++++++++++++ src/pages/auth/AuthPageStyle.ts | 22 -------- src/pages/auth/LoginPage.tsx | 35 ++----------- src/pages/auth/SignUpPage.tsx | 35 ++----------- 5 files changed, 95 insertions(+), 84 deletions(-) create mode 100644 src/components/SocialLogin/SocialLogin.tsx create mode 100644 src/components/SocialLogin/SocialLoginButton.tsx 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 ( + + {imgAlt} + + ); +}; + +export default SocialLoginButton; + +const SocialLoginButtonStyle = css` + border-radius: 50%; +`; diff --git a/src/pages/auth/AuthPageStyle.ts b/src/pages/auth/AuthPageStyle.ts index 37608e1c..69016f40 100644 --- a/src/pages/auth/AuthPageStyle.ts +++ b/src/pages/auth/AuthPageStyle.ts @@ -43,24 +43,6 @@ const AuthPageStyle = css` 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) { @@ -71,10 +53,6 @@ const AuthPageStyle = css` .form-logo { width: 396px; } - - .easy-login { - margin: 0; - } } @media (min-width: ${BREAKPOINTS.tablet}px) { diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index add81370..e7c60a70 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -4,8 +4,7 @@ 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 SocialLogin from "@/components/SocialLogin/SocialLogin"; import logoImg from "@/assets/images/logo.svg"; import useForm from "@/hooks/useForm"; import Input from "@/components/ui/Input"; @@ -142,35 +141,9 @@ const LoginPage = () => { {isSigningIn ? "로그인중..." : "로그인"} {isSignInError &&

{`${isSignInError}`}

} -
- 간편 로그인하기 - -
+ + +
판다마켓이 처음이신가요? diff --git a/src/pages/auth/SignUpPage.tsx b/src/pages/auth/SignUpPage.tsx index 36f18f4e..c2848d60 100644 --- a/src/pages/auth/SignUpPage.tsx +++ b/src/pages/auth/SignUpPage.tsx @@ -8,14 +8,13 @@ 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 SocialLogin from "@/components/SocialLogin/SocialLogin"; const SignUpPage = () => { const [isVisible, setIsVisible] = useState(false); @@ -213,35 +212,9 @@ const SignUpPage = () => { {isSigningUp ? "회원가입중..." : "회원가입"} {isSignUpError &&

{`${isSignUpError}`}

} -
- 간편 로그인하기 - -
+ + +
이미 회원이신가요? From b75e80041e3d4d9e177b50a401ca0a1b6a20d273 Mon Sep 17 00:00:00 2001 From: "asksa1256@gmail.com" Date: Tue, 8 Jul 2025 14:05:42 +0900 Subject: [PATCH 06/16] =?UTF-8?q?refactor:=20text,=20password=20input=20fi?= =?UTF-8?q?eld=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=97=90=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Form/LoginForm.tsx | 110 +++++++++++++++++ src/components/ui/Form/InputField.tsx | 42 +++++++ src/components/ui/Form/PasswordField.tsx | 60 +++++++++ src/pages/auth/LoginPage.tsx | 147 +---------------------- 4 files changed, 214 insertions(+), 145 deletions(-) create mode 100644 src/components/Form/LoginForm.tsx create mode 100644 src/components/ui/Form/InputField.tsx create mode 100644 src/components/ui/Form/PasswordField.tsx diff --git a/src/components/Form/LoginForm.tsx b/src/components/Form/LoginForm.tsx new file mode 100644 index 00000000..4dacabc0 --- /dev/null +++ b/src/components/Form/LoginForm.tsx @@ -0,0 +1,110 @@ +/** @jsxImportSource @emotion/react */ +import FormStyle from "./FormStyle"; +import { useState, useRef } from "react"; +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 useForm from "@/hooks/useForm"; +import { ReqData } from "@/types/form"; +import loginUser from "@/services/post/loginUser"; +import useSignIn from "@/hooks/useSignIn"; +import InputField from "../ui/Form/InputField"; +import PasswordField from "../ui/Form/PasswordField"; + +const LoginForm = () => { + const [isSigningIn, setIsSigningIn] = useState(false); + const [isSignInError, setIsSignInError] = useState(null); + const formRef = useRef(null); + + const signIn = useSignIn(); + + const { handleBlur, validateForm, isFormValid } = 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 ( +
+
+ + 판다마켓 로고 + +
+ +
+ + + + + {isSignInError &&

{`${isSignInError}`}

} + + + +
+ 판다마켓이 처음이신가요? + + 회원가입 + +
+
+
+ ); +}; + +export default LoginForm; diff --git a/src/components/ui/Form/InputField.tsx b/src/components/ui/Form/InputField.tsx new file mode 100644 index 00000000..34388002 --- /dev/null +++ b/src/components/ui/Form/InputField.tsx @@ -0,0 +1,42 @@ +/** @jsxImportSource @emotion/react */ +import useForm from "@/hooks/useForm"; +import Input from "@/components/ui/Input"; +import FormControl from "./FormControl"; +import { FormField } from "@/types/form"; + +const InputField = ({ + label, + inputId, + type, + name, + placeholder, + required, + form, + onBlur, +}: FormField) => { + const { fieldErrors } = useForm(form); + + return ( + + +
+ + {fieldErrors[name]} +
+
+ ); +}; + +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..d0748f57 --- /dev/null +++ b/src/components/ui/Form/PasswordField.tsx @@ -0,0 +1,60 @@ +/** @jsxImportSource @emotion/react */ +import { useState } from "react"; +import useForm from "@/hooks/useForm"; +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, + form, + onBlur, +}: FormField) => { + const [isVisible, setIsVisible] = useState(false); + const { fieldErrors } = useForm(form); + + return ( + + +
+
+ + setIsVisible((prev) => !prev)} + /> +
+ {fieldErrors.password} +
+
+ ); +}; + +export default PasswordField; diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index e7c60a70..008736af 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -1,157 +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 SocialLogin from "@/components/SocialLogin/SocialLogin"; -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 (
-
-
- - 판다마켓 로고 - -
- -
- - -
- - {fieldErrors.email} -
-
- - - -
-
- - setIsVisible((prev) => !prev)} - /> -
- - {fieldErrors.password} - -
-
- - - {isSignInError &&

{`${isSignInError}`}

} - - - -
- 판다마켓이 처음이신가요? - - 회원가입 - -
-
-
+
From 684affa5b29afe7068e0d0e0152426e028cc84ca Mon Sep 17 00:00:00 2001 From: "asksa1256@gmail.com" Date: Tue, 8 Jul 2025 14:05:58 +0900 Subject: [PATCH 07/16] =?UTF-8?q?refactor:=20=ED=8F=BC=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=ED=83=80=EC=9E=85=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/types/form.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/types/form.ts b/src/types/form.ts index ac96d1c1..7003bb21 100644 --- a/src/types/form.ts +++ b/src/types/form.ts @@ -1,3 +1,16 @@ -export type ReqData = { +import { ChangeEvent, RefObject } from "react"; + +export interface FormField { + label: string; + inputId?: string; + type?: string; + name: string; + placeholder: string; + required?: boolean; + form: RefObject; + onBlur?: (e: ChangeEvent) => void; +} + +export interface ReqData { [key: string]: FormDataEntryValue; -}; +} From b2961cf6ae3e73cbe1b878d707c9856d57f08fd3 Mon Sep 17 00:00:00 2001 From: "asksa1256@gmail.com" Date: Tue, 8 Jul 2025 14:27:57 +0900 Subject: [PATCH 08/16] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=8E=98=EC=9D=B4=EC=A7=80=20InputField,=20passwor?= =?UTF-8?q?dField=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Form/SignUpForm.tsx | 130 +++++++++++++++++ src/pages/auth/SignUpPage.tsx | 218 +---------------------------- 2 files changed, 132 insertions(+), 216 deletions(-) create mode 100644 src/components/Form/SignUpForm.tsx diff --git a/src/components/Form/SignUpForm.tsx b/src/components/Form/SignUpForm.tsx new file mode 100644 index 00000000..5a16a639 --- /dev/null +++ b/src/components/Form/SignUpForm.tsx @@ -0,0 +1,130 @@ +/** @jsxImportSource @emotion/react */ +import { useRef, useState } from "react"; +import { Link } from "react-router-dom"; +import useForm from "@/hooks/useForm"; +import logoImg from "@/assets/images/logo.svg"; +import Button from "@/components/ui/Button"; +import createUser from "@/services/post/createUser"; +import { ReqData } from "@/types/form"; +import useSignIn from "@/hooks/useSignIn"; +import SocialLogin from "@/components/SocialLogin/SocialLogin"; +import InputField from "@/components/ui/Form/InputField"; +import PasswordField from "@/components/ui/Form/PasswordField"; +import FormStyle from "./FormStyle"; + +const SignUpForm = () => { + const [isSigningUp, setIsSigningUp] = useState(false); + const [isSignUpError, setIsSignUpError] = useState(null); + + const formRef = useRef(null); + const { handleBlur, isFormValid, validateForm, fieldErrors } = + 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 ( +
+
+ + 판다마켓 로고 + +
+ +
+ + + + + + + {isSignUpError &&

{`${isSignUpError}`}

} + + + +
+ 이미 회원이신가요? + + 로그인 + +
+
+
+ ); +}; + +export default SignUpForm; diff --git a/src/pages/auth/SignUpPage.tsx b/src/pages/auth/SignUpPage.tsx index c2848d60..599111f0 100644 --- a/src/pages/auth/SignUpPage.tsx +++ b/src/pages/auth/SignUpPage.tsx @@ -1,228 +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 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 SocialLogin from "@/components/SocialLogin/SocialLogin"; +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 (
-
-
- - 판다마켓 로고 - -
- -
- - -
- - {fieldErrors.email} -
-
- - - -
- - - {fieldErrors.nickname} - -
-
- - - -
-
- - setIsVisible((prev) => !prev)} - /> -
- - {fieldErrors.password} - -
-
- - - -
-
- - setIsVisible((prev) => !prev)} - /> -
- - {fieldErrors.passwordCheck} - -
-
- - - {isSignUpError &&

{`${isSignUpError}`}

} - - - -
- 이미 회원이신가요? - - 로그인 - -
-
-
+
From 724eb362e18f739409ee7770f9819df54df3f40d Mon Sep 17 00:00:00 2001 From: "asksa1256@gmail.com" Date: Tue, 8 Jul 2025 14:28:57 +0900 Subject: [PATCH 09/16] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=95=88=20=EB=9C=A8=EB=8A=94=20?= =?UTF-8?q?=ED=98=84=EC=83=81=20=EC=88=98=EC=A0=95=20(formRef=EB=A5=BC=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=EA=B0=80=20=EA=B0=81=EC=9E=90=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=ED=95=B4=EC=84=9C=20=EB=B0=9C=EC=83=9D=ED=95=9C=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Form/LoginForm.tsx | 7 ++++--- src/components/ui/Form/InputField.tsx | 9 +++------ src/components/ui/Form/PasswordField.tsx | 8 +++----- src/types/form.ts | 4 ++-- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/components/Form/LoginForm.tsx b/src/components/Form/LoginForm.tsx index 4dacabc0..807c86d0 100644 --- a/src/components/Form/LoginForm.tsx +++ b/src/components/Form/LoginForm.tsx @@ -19,7 +19,8 @@ const LoginForm = () => { const signIn = useSignIn(); - const { handleBlur, validateForm, isFormValid } = useForm(formRef); + const { handleBlur, validateForm, isFormValid, fieldErrors } = + useForm(formRef); const handleLogin = async () => { const form = formRef.current; @@ -69,16 +70,16 @@ const LoginForm = () => { name="email" placeholder="이메일" required - form={formRef} onBlur={handleBlur} + fieldError={fieldErrors.email} />
); diff --git a/src/components/ui/Form/PasswordField.tsx b/src/components/ui/Form/PasswordField.tsx index d0748f57..6c01a42d 100644 --- a/src/components/ui/Form/PasswordField.tsx +++ b/src/components/ui/Form/PasswordField.tsx @@ -1,6 +1,5 @@ /** @jsxImportSource @emotion/react */ import { useState } from "react"; -import useForm from "@/hooks/useForm"; import Input from "@/components/ui/Input"; import FormControl from "./FormControl"; import { FormField } from "@/types/form"; @@ -13,11 +12,10 @@ const PasswordField = ({ inputId, name, placeholder, - form, onBlur, + fieldError, }: FormField) => { const [isVisible, setIsVisible] = useState(false); - const { fieldErrors } = useForm(form); return ( @@ -35,7 +33,7 @@ const PasswordField = ({ placeholder={placeholder} required onBlur={onBlur} - isError={fieldErrors.password} + isError={fieldError} /> setIsVisible((prev) => !prev)} />
- {fieldErrors.password} + {fieldError}
); diff --git a/src/types/form.ts b/src/types/form.ts index 7003bb21..3d9e553e 100644 --- a/src/types/form.ts +++ b/src/types/form.ts @@ -1,4 +1,4 @@ -import { ChangeEvent, RefObject } from "react"; +import { ChangeEvent } from "react"; export interface FormField { label: string; @@ -7,8 +7,8 @@ export interface FormField { name: string; placeholder: string; required?: boolean; - form: RefObject; onBlur?: (e: ChangeEvent) => void; + fieldError?: string; } export interface ReqData { From 6331a419d80e1bf92021071ca8d49f19b92c969a Mon Sep 17 00:00:00 2001 From: "asksa1256@gmail.com" Date: Tue, 8 Jul 2025 16:15:53 +0900 Subject: [PATCH 10/16] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8,?= =?UTF-8?q?=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85(us?= =?UTF-8?q?eAuthForm)=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Form/LoginForm.tsx | 59 ++++++++------------------ src/components/Form/SignUpForm.tsx | 62 +++++++++------------------ src/hooks/useAuthForm.ts | 67 ++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 84 deletions(-) create mode 100644 src/hooks/useAuthForm.ts diff --git a/src/components/Form/LoginForm.tsx b/src/components/Form/LoginForm.tsx index 807c86d0..d6b034fb 100644 --- a/src/components/Form/LoginForm.tsx +++ b/src/components/Form/LoginForm.tsx @@ -1,53 +1,29 @@ /** @jsxImportSource @emotion/react */ import FormStyle from "./FormStyle"; -import { useState, useRef } from "react"; 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 useForm from "@/hooks/useForm"; -import { ReqData } from "@/types/form"; import loginUser from "@/services/post/loginUser"; -import useSignIn from "@/hooks/useSignIn"; import InputField from "../ui/Form/InputField"; import PasswordField from "../ui/Form/PasswordField"; +import useAuthForm from "@/hooks/useAuthForm"; const LoginForm = () => { - 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); + const { + formRef, + isSubmitting, + submitError, + handleBlur, + validateForm, + isFormValid, + fieldErrors, + handleSubmit, + } = useAuthForm({ + onSubmit: async (userData) => { await loginUser(userData); - signIn(); // 로그인 컨텍스트 처리 -> 상품 목록 페이지로 이동 - } catch (err) { - if (err instanceof Error) { - setIsSignInError(err); - } else { - setIsSignInError(new Error("알 수 없는 오류가 발생했습니다.")); - } - } finally { - setIsSigningIn(false); - } - }; + }, + }); return (
{ disabled={!isFormValid} variant="primary" size="lg" - onClick={handleLogin} + onClick={handleSubmit} > - {isSigningIn ? "로그인중..." : "로그인"} + {isSubmitting ? "로그인중..." : "로그인"} - {isSignInError &&

{`${isSignInError}`}

} + + {submitError &&

{`${submitError}`}

} diff --git a/src/components/Form/SignUpForm.tsx b/src/components/Form/SignUpForm.tsx index 5a16a639..d8280980 100644 --- a/src/components/Form/SignUpForm.tsx +++ b/src/components/Form/SignUpForm.tsx @@ -1,54 +1,29 @@ /** @jsxImportSource @emotion/react */ -import { useRef, useState } from "react"; import { Link } from "react-router-dom"; -import useForm from "@/hooks/useForm"; import logoImg from "@/assets/images/logo.svg"; import Button from "@/components/ui/Button"; -import createUser from "@/services/post/createUser"; -import { ReqData } from "@/types/form"; -import useSignIn from "@/hooks/useSignIn"; 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"; const SignUpForm = () => { - const [isSigningUp, setIsSigningUp] = useState(false); - const [isSignUpError, setIsSignUpError] = useState(null); - - const formRef = useRef(null); - const { handleBlur, isFormValid, validateForm, fieldErrors } = - 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); + const { + formRef, + isSubmitting, + submitError, + handleBlur, + validateForm, + isFormValid, + fieldErrors, + handleSubmit, + } = useAuthForm({ + onSubmit: async (userData) => { await createUser(userData); - signIn(); // 로그인 컨텍스트 처리 -> 상품 목록 페이지로 이동 - } catch (err) { - if (err instanceof Error) { - setIsSignUpError(err); - } else { - setIsSignUpError(new Error("알 수 없는 오류가 발생했습니다.")); - } - } finally { - setIsSigningUp(false); - } - }; + }, + }); return ( { size="lg" id="signupBtn" disabled={!isFormValid} - onClick={handleSignUp} + onClick={handleSubmit} > - {isSigningUp ? "회원가입중..." : "회원가입"} + {isSubmitting ? "회원가입중..." : "회원가입"} - {isSignUpError &&

{`${isSignUpError}`}

} + + {submitError &&

{`${submitError}`}

} 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; From f7cb57de8323fecc84e8d96ed1d38574442023fd Mon Sep 17 00:00:00 2001 From: "asksa1256@gmail.com" Date: Tue, 8 Jul 2025 16:26:24 +0900 Subject: [PATCH 11/16] =?UTF-8?q?style:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=ED=8F=BC=20=EB=A1=9C?= =?UTF-8?q?=EA=B3=A0=20=EB=B0=98=EC=9D=91=ED=98=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/auth/AuthPageStyle.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pages/auth/AuthPageStyle.ts b/src/pages/auth/AuthPageStyle.ts index 69016f40..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,26 +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; } - /*================ 반응형 ================*/ - /* Tablet */ @media (min-width: 640px) { .form { max-width: 640px; } .form-logo { - width: 396px; + img { + max-width: 396px; + } } } From 783916826e86e55c5665d9e99132102f048238d5 Mon Sep 17 00:00:00 2001 From: "asksa1256@gmail.com" Date: Tue, 8 Jul 2025 16:28:36 +0900 Subject: [PATCH 12/16] =?UTF-8?q?refactor:=20BannerStyle=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Banner/Banner.tsx | 127 +------------------------- src/components/Banner/BannerStyle.ts | 128 +++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 125 deletions(-) create mode 100644 src/components/Banner/BannerStyle.ts diff --git a/src/components/Banner/Banner.tsx b/src/components/Banner/Banner.tsx index 69bc3ff4..f9501d0d 100644 --- a/src/components/Banner/Banner.tsx +++ b/src/components/Banner/Banner.tsx @@ -1,9 +1,9 @@ /** @jsxImportSource @emotion/react */ -import { css } from "@emotion/react"; import { ReactNode } from "react"; import { useNavigate } from "react-router-dom"; -import { BREAKPOINTS } from "@/constants/responsive"; import Button from "@/components/ui/Button"; +import BannerStyle from "./BannerStyle"; + interface BannerProps { title: string | ReactNode; imgSrc: string; @@ -51,126 +51,3 @@ const Banner = ({ }; export default Banner; - -const BannerStyle = css` - 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; - } - - @media (min-width: 640px) { - .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; - } - } - - @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)) { - :root { - --banner-font-size: clamp(32px, 5vw, 40px); - } - } -`; diff --git a/src/components/Banner/BannerStyle.ts b/src/components/Banner/BannerStyle.ts new file mode 100644 index 00000000..13d839dd --- /dev/null +++ b/src/components/Banner/BannerStyle.ts @@ -0,0 +1,128 @@ +/** @jsxImportSource @emotion/react */ +import { css } from "@emotion/react"; +import { BREAKPOINTS } from "@/constants/responsive"; + +const BannerStyle = css` + 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; + } + + @media (min-width: 640px) { + .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; + } + } + + @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)) { + :root { + --banner-font-size: clamp(32px, 5vw, 40px); + } + } +`; + +export default BannerStyle; From 04d105e00957c74843af4403f3deafa3ca6da5ad Mon Sep 17 00:00:00 2001 From: "asksa1256@gmail.com" Date: Thu, 10 Jul 2025 17:53:57 +0900 Subject: [PATCH 13/16] =?UTF-8?q?refactor(BannerStyle):=20css=20vars=20->?= =?UTF-8?q?=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=A1=9C=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=A0=95=EC=9D=98=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Banner/BannerStyle.ts | 21 ++++++++------- src/styles/colors.ts | 38 ++++++++++++++++++++++++++++ src/styles/fontSizes.ts | 7 +++++ 3 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 src/styles/colors.ts create mode 100644 src/styles/fontSizes.ts diff --git a/src/components/Banner/BannerStyle.ts b/src/components/Banner/BannerStyle.ts index 13d839dd..2b3a86a5 100644 --- a/src/components/Banner/BannerStyle.ts +++ b/src/components/Banner/BannerStyle.ts @@ -1,11 +1,13 @@ /** @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: var(--background-blue); - color: var(--gray700); + background: ${COLORS.background.blue}; + color: ${COLORS.gray[700]}; text-align: center; .banner-container { @@ -17,11 +19,11 @@ const BannerStyle = css` } .banner-hero { - background: var(--background-blue-light); + background: ${COLORS.background.lightBlue}; } .banner-title { - font-size: var(--banner-font-size); + /* font-size: var(--banner-font-size); */ margin-bottom: 18px; word-break: keep-all; @@ -50,7 +52,7 @@ const BannerStyle = css` .banner-info .btn-lg { display: block; width: 100%; - font-size: var(--banner-btn-font-size); + font-size: ${FONT_SIZES[18]}; line-height: 24px; max-width: 356px; } @@ -69,10 +71,6 @@ const BannerStyle = css` } @media (min-width: ${BREAKPOINTS.tablet}px) { - :root { - --banner-btn-font-size: 20px; - } - .banner { height: 926px; } @@ -82,6 +80,7 @@ const BannerStyle = css` .banner-info .btn-lg { line-height: 32px; + font-size: ${FONT_SIZES[20]}; } } @@ -119,8 +118,8 @@ const BannerStyle = css` } @supports (font-size: clamp(1rem, 2vw, 3rem)) { - :root { - --banner-font-size: clamp(32px, 5vw, 40px); + .banner-title { + font-size: clamp(32px, 5vw, 40px); } } `; 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", +}; From 15b385baddfa95c6ccbacb4afd7379d600a855a8 Mon Sep 17 00:00:00 2001 From: "asksa1256@gmail.com" Date: Thu, 10 Jul 2025 18:47:09 +0900 Subject: [PATCH 14/16] =?UTF-8?q?refactor:=20Banner=20=ED=95=A9=EC=84=B1?= =?UTF-8?q?=20=EA=B8=B0=EB=B0=98=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Banner/Banner.tsx | 92 +++++++++++++++++++++++----- src/components/Banner/BannerStyle.ts | 1 - 2 files changed, 75 insertions(+), 18 deletions(-) diff --git a/src/components/Banner/Banner.tsx b/src/components/Banner/Banner.tsx index f9501d0d..f6d2f221 100644 --- a/src/components/Banner/Banner.tsx +++ b/src/components/Banner/Banner.tsx @@ -4,6 +4,63 @@ 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 ( + {imgAlt} + ); +}; + interface BannerProps { title: string | ReactNode; imgSrc: string; @@ -11,6 +68,8 @@ interface BannerProps { linkTo?: string; ariaLabel?: string; lazyLoading?: boolean; + infoAlign?: string; + imgAlign?: string; } const Banner = ({ @@ -20,34 +79,33 @@ const Banner = ({ imgAlt, ariaLabel, lazyLoading, + infoAlign = "left", + imgAlign = "right", }: BannerProps) => { - const navigate = useNavigate(); - return ( -
+
-
+

{title}

{linkTo && ( - + )} -
- {imgAlt} +
); }; +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 index 2b3a86a5..580d2a5e 100644 --- a/src/components/Banner/BannerStyle.ts +++ b/src/components/Banner/BannerStyle.ts @@ -23,7 +23,6 @@ const BannerStyle = css` } .banner-title { - /* font-size: var(--banner-font-size); */ margin-bottom: 18px; word-break: keep-all; From cdbb48e4bc11b0e16075ca13c7a4b858a6b9b12f Mon Sep 17 00:00:00 2001 From: "asksa1256@gmail.com" Date: Thu, 10 Jul 2025 18:48:11 +0900 Subject: [PATCH 15/16] =?UTF-8?q?fix:=20=EB=B0=B0=EB=84=88=20ariaLabel=20?= =?UTF-8?q?=EC=86=8D=EC=84=B1=20=EC=A0=81=EC=9A=A9=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Banner/Banner.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/Banner/Banner.tsx b/src/components/Banner/Banner.tsx index f6d2f221..b3a0020b 100644 --- a/src/components/Banner/Banner.tsx +++ b/src/components/Banner/Banner.tsx @@ -83,15 +83,11 @@ const Banner = ({ imgAlign = "right", }: BannerProps) => { return ( -
+

{title}

- {linkTo && ( - - 구경하러 가기 - - )} + {linkTo && 구경하러 가기}
Date: Sat, 12 Jul 2025 17:04:40 +0900 Subject: [PATCH 16/16] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8,?= =?UTF-8?q?=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=EC=9D=98=20isSubmitting=20=EC=A1=B0=EA=B1=B4=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Form/LoginForm.tsx | 3 ++- src/components/Form/SignUpForm.tsx | 3 ++- src/utils/renderButtonText.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 src/utils/renderButtonText.ts diff --git a/src/components/Form/LoginForm.tsx b/src/components/Form/LoginForm.tsx index d6b034fb..9b2943f6 100644 --- a/src/components/Form/LoginForm.tsx +++ b/src/components/Form/LoginForm.tsx @@ -8,6 +8,7 @@ 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 { @@ -67,7 +68,7 @@ const LoginForm = () => { size="lg" onClick={handleSubmit} > - {isSubmitting ? "로그인중..." : "로그인"} + {renderButtonTextByState(isSubmitting, "로그인")} {submitError &&

{`${submitError}`}

} diff --git a/src/components/Form/SignUpForm.tsx b/src/components/Form/SignUpForm.tsx index d8280980..e9c8cfc4 100644 --- a/src/components/Form/SignUpForm.tsx +++ b/src/components/Form/SignUpForm.tsx @@ -8,6 +8,7 @@ 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 { @@ -85,7 +86,7 @@ const SignUpForm = () => { disabled={!isFormValid} onClick={handleSubmit} > - {isSubmitting ? "회원가입중..." : "회원가입"} + {renderButtonTextByState(isSubmitting, "회원가입")} {submitError &&

{`${submitError}`}

} 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, + }); +};