-
-

-
-
- 로그인
-
-
+
+
+ 로그인
+
+
+
+
+
+
+ >
);
};
diff --git a/src/main.tsx b/src/main.tsx
index 47e3869b..b5ec4d28 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,5 +1,4 @@
import { createRoot } from "react-dom/client";
-import "../public/reset.css";
import "../public/common.css";
import "./index.css";
import App from "./App.jsx";
diff --git a/src/pages/Login_SignupPage/LoginPage.tsx b/src/pages/Login_SignupPage/LoginPage.tsx
new file mode 100644
index 00000000..8edbe62c
--- /dev/null
+++ b/src/pages/Login_SignupPage/LoginPage.tsx
@@ -0,0 +1,115 @@
+import logo from "@/assets/logo/logo.svg";
+import { Link, useNavigate } from "react-router-dom";
+import FieldUncontrolled from "./components/FieldUncontrolled";
+import SocialLogin from "./components/SocialLogin";
+import { useEffect, useRef, useState } from "react";
+import clsx from "clsx";
+import {
+ validateEmail,
+ validatePassword,
+ validatePasswordCheck,
+} from "@/utils/validations";
+
+export interface ILoginData {
+ email: string;
+ password: string;
+}
+
+export const fieldMap = {
+ email: {
+ id: "email",
+ label: "이메일",
+ type: "email",
+ placeholder: "이메일을 입력해주세요",
+ validator: validateEmail,
+ },
+ nickname: {
+ id: "nickname",
+ label: "닉네임",
+ type: "text",
+ placeholder: "닉네임을 입력해주세요",
+ },
+ password: {
+ id: "password",
+ label: "비밀번호",
+ type: "password",
+ placeholder: "비밀번호를 입력해주세요",
+ validator: validatePassword,
+ },
+ passwordCheck: {
+ id: "passwordCheck",
+ label: "비밀번호 확인",
+ type: "password",
+ placeholder: "비밀번호를 다시 입력해주세요",
+ validator: validatePasswordCheck,
+ },
+} as const;
+
+const LoginPage = () => {
+ const [isValidating, setIsValidating] = useState(false);
+ const [hasError, setHasError] = useState
(true);
+ const loginData = useRef({ email: "", password: "" });
+ const isMount = useRef(true);
+ const navigate = useNavigate();
+ const commonFieldProps = {
+ formData: loginData.current,
+ validationTrigger: isValidating,
+ onCheckValidating: setIsValidating,
+ errorSetter: setHasError,
+ };
+
+ const handleStartValidation = () => {
+ //검증 시작할 때 hasError->false, 각 필드에서 하나라도 에러있으면 true
+ setIsValidating(true);
+ setHasError(false);
+ };
+
+ useEffect(() => {
+ if (isMount.current) {
+ isMount.current = false;
+ return;
+ }
+
+ if (isValidating || hasError === true) {
+ return;
+ }
+
+ navigate("/");
+ }, [isValidating, hasError]);
+
+ return (
+
+
+
+
+
+
+
+ 로그인
+
+
+
+ 판다마켓이 처음이신가요?
+
+ 회원가입
+
+
+
+ );
+};
+
+export default LoginPage;
diff --git a/src/pages/Login_SignupPage/SignupPage.tsx b/src/pages/Login_SignupPage/SignupPage.tsx
new file mode 100644
index 00000000..c9157972
--- /dev/null
+++ b/src/pages/Login_SignupPage/SignupPage.tsx
@@ -0,0 +1,93 @@
+import logo from "@/assets/logo/logo.svg";
+import { Link, useNavigate } from "react-router-dom";
+import FieldUncontrolled from "./components/FieldUncontrolled";
+import SocialLogin from "./components/SocialLogin";
+import { fieldMap } from "./LoginPage";
+import { useEffect, useRef, useState } from "react";
+import { type ILoginData } from "./LoginPage";
+import clsx from "clsx";
+
+interface ISignupData extends ILoginData {
+ nickname: string;
+ passwordCheck: string;
+}
+
+const SignupPage = () => {
+ const [isValidating, setIsValidating] = useState(false);
+ const [hasError, setHasError] = useState(true);
+ const signupData = useRef({
+ email: "",
+ nickname: "",
+ password: "",
+ passwordCheck: "",
+ });
+ const isMount = useRef(true);
+ const navigate = useNavigate();
+ const commonFieldProps = {
+ formData: signupData.current,
+ validationTrigger: isValidating,
+ onCheckValidating: setIsValidating,
+ errorSetter: setHasError,
+ };
+
+ const handleStartValidation = () => {
+ //검증 시작할 때 hasError->false, 각 필드에서 하나라도 에러있으면 true
+ setIsValidating(true);
+ setHasError(false);
+ };
+
+ useEffect(() => {
+ if (isMount.current) {
+ isMount.current = false;
+ return;
+ }
+
+ if (isValidating || hasError === true) {
+ return;
+ }
+
+ navigate("/login");
+ }, [isValidating, hasError]);
+
+ return (
+
+
+
+
+
+
+
+
+
+ 회원가입
+
+
+
+ 이미 회원이신가요?
+
+ 로그인
+
+
+
+ );
+};
+
+export default SignupPage;
diff --git a/src/pages/Login_SignupPage/components/FieldUncontrolled.tsx b/src/pages/Login_SignupPage/components/FieldUncontrolled.tsx
new file mode 100644
index 00000000..f060036b
--- /dev/null
+++ b/src/pages/Login_SignupPage/components/FieldUncontrolled.tsx
@@ -0,0 +1,155 @@
+import visibilityOn from "@/assets/icons/ic_visibility_on.svg";
+import visibilityOff from "@/assets/icons/ic_visibility_off.svg";
+import isEmpty from "@/utils/isEmpty";
+import { type ILoginData } from "../LoginPage";
+import React, { useEffect, useRef, useState } from "react";
+import clsx from "clsx";
+
+type SingleParamValidator = (value: string) => {
+ isValid: boolean;
+ errorMsg: string | null;
+};
+type TwoParamValidator = (
+ value: string,
+ comparisonValue: string | undefined
+) => { isValid: boolean; errorMsg: string | null };
+
+interface Props {
+ id: string;
+ label: string;
+ type: "email" | "text" | "password";
+ placeholder: string;
+ validator?: SingleParamValidator | TwoParamValidator;
+ formData: ILoginData;
+ validationTrigger: boolean;
+ onCheckValidating: React.Dispatch>;
+ errorSetter: React.Dispatch>;
+ //비밀번호 확인을 위한 비밀번호 값
+ comparisonValue?: string;
+}
+
+const FieldUncontrolled = ({
+ id,
+ label,
+ type,
+ placeholder,
+ validator,
+ formData,
+ validationTrigger,
+ onCheckValidating,
+ errorSetter,
+ comparisonValue,
+}: Props) => {
+ const [errorMsg, setErrorMsg] = useState(null);
+ const [isVisibilityOn, setIsVisibilityOn] = useState(false);
+ const inputRef = useRef(null);
+ const isMount = useRef(true);
+ const isPassword = type === "password";
+
+ const handleChangeValue = () => {
+ if (id in formData && inputRef.current) {
+ formData[id as keyof ILoginData] = inputRef.current?.value;
+ }
+ };
+
+ useEffect(() => {
+ if (isMount.current) {
+ isMount.current = false;
+ return;
+ }
+
+ //의존성 배열에 다른 것을 포함시키면서 trigger가 true일 때에만 실행하도록
+ if (!validationTrigger) {
+ return;
+ }
+
+ const value = inputRef.current!.value;
+
+ if (isEmpty(value)) {
+ errorSetter(true);
+ setErrorMsg(placeholder);
+ onCheckValidating(false);
+ return;
+ }
+
+ if (validator && id === "passwordCheck") {
+ const { isValid, errorMsg } = validator(value, comparisonValue);
+
+ if (!isValid) {
+ errorSetter(true);
+ setErrorMsg(errorMsg);
+ onCheckValidating(false);
+ return;
+ }
+ } else if (validator) {
+ const { isValid, errorMsg } = (validator as SingleParamValidator)(value);
+
+ if (!isValid) {
+ errorSetter(true);
+ setErrorMsg(errorMsg);
+ onCheckValidating(false);
+ return;
+ }
+ }
+
+ setErrorMsg(null);
+ onCheckValidating(false);
+ }, [validationTrigger, isEmpty, validator]);
+
+ return (
+
+
+
+
{
+ setErrorMsg(null);
+ }}
+ />
+ {isPassword ? (
+
{
+ setIsVisibilityOn((prev) => !prev);
+ }}
+ >
+
+
+
+ ) : undefined}
+
+ {errorMsg ? (
+
+ {errorMsg}
+
+ ) : undefined}
+
+ );
+};
+
+export default FieldUncontrolled;
diff --git a/src/pages/Login_SignupPage/components/SocialLogin.tsx b/src/pages/Login_SignupPage/components/SocialLogin.tsx
new file mode 100644
index 00000000..69d788c4
--- /dev/null
+++ b/src/pages/Login_SignupPage/components/SocialLogin.tsx
@@ -0,0 +1,16 @@
+import googleIcon from "@/assets/icons/ic_google.svg";
+import kakaoIcon from "@/assets/icons/ic_kakaotalk.svg";
+
+const SocialLogin = () => {
+ return (
+
+ 간편 로그인하기
+
+

+

+
+
+ );
+};
+
+export default SocialLogin;
diff --git a/src/pages/MainPage/MainPage.tsx b/src/pages/MainPage/MainPage.tsx
new file mode 100644
index 00000000..629e0fb9
--- /dev/null
+++ b/src/pages/MainPage/MainPage.tsx
@@ -0,0 +1,70 @@
+import Hero from "./components/Hero";
+import Introduction from "./components/Introduction";
+import introImg1 from "@/assets/img/img_home_01.png";
+import introImg2 from "@/assets/img/img_home_02.png";
+import introImg3 from "@/assets/img/img_home_03.png";
+import Closing from "./components/Closing";
+import Footer from "./components/Footer";
+
+const INTRODUCTION_MAP = {
+ section1: {
+ className: "items-start text-left",
+ imgSrc: introImg1,
+ keyword: "Hot item",
+ title: "인기 상품을\n확인해보세요",
+ description: "가장 HOT한 중고거래 물품을\n판다 마켓에서 확인해보세요",
+ },
+ section2: {
+ className: "items-end text-right xl:flex-row-reverse",
+ imgSrc: introImg2,
+ keyword: "Search",
+ title: "구매를 원하는\n상품을 검색하세요",
+ description: "구매하고 싶은 물품은 검색해서\n쉽게 찾아보세요",
+ },
+ section3: {
+ className: "items-start text-left",
+ imgSrc: introImg3,
+ keyword: "Register",
+ title: "판매를 원하는\n상품을 등록하세요",
+ description: "어떤 물건이든 판매하고 싶은 상품을\n쉽게 등록하세요",
+ },
+};
+
+const MainPage = () => {
+ return (
+
+ );
+};
+
+export default MainPage;
diff --git a/src/pages/MainPage/components/Closing.tsx b/src/pages/MainPage/components/Closing.tsx
new file mode 100644
index 00000000..cd1a3a64
--- /dev/null
+++ b/src/pages/MainPage/components/Closing.tsx
@@ -0,0 +1,28 @@
+import closingImg from "@/assets/img/img_home_bottom.png";
+
+const Closing = () => {
+ return (
+
+
+
+ 믿을 수 있는
+
+ 판다마켓 중고 거래
+
+

+
+
+ );
+};
+
+export default Closing;
diff --git a/src/pages/MainPage/components/Footer.tsx b/src/pages/MainPage/components/Footer.tsx
new file mode 100644
index 00000000..e2388f3c
--- /dev/null
+++ b/src/pages/MainPage/components/Footer.tsx
@@ -0,0 +1,46 @@
+import facebookIcon from "@/assets/icons/ic_facebook.svg";
+import twitterIcon from "@/assets/icons/ic_twitter.svg";
+import youtubeIcon from "@/assets/icons/ic_youtube.svg";
+import instagramIcon from "@/assets/icons/ic_instagram.svg";
+
+const Footer = () => {
+ return (
+
+ );
+};
+
+export default Footer;
diff --git a/src/pages/MainPage/components/Hero.tsx b/src/pages/MainPage/components/Hero.tsx
new file mode 100644
index 00000000..1ad32803
--- /dev/null
+++ b/src/pages/MainPage/components/Hero.tsx
@@ -0,0 +1,47 @@
+import heroImg from "@/assets/img/img_home_top.png";
+import { Link } from "react-router-dom";
+
+const Hero = () => {
+ return (
+
+
+
+
+ 일상의 모든 물건을
+ 거래해보세요
+
+
+
+ 구경하러 가기
+
+
+
+

+
+
+ );
+};
+
+export default Hero;
diff --git a/src/pages/MainPage/components/Introduction.tsx b/src/pages/MainPage/components/Introduction.tsx
new file mode 100644
index 00000000..aa0461fd
--- /dev/null
+++ b/src/pages/MainPage/components/Introduction.tsx
@@ -0,0 +1,54 @@
+interface Props {
+ className: string;
+ imgSrc: string;
+ keyword: string;
+ title: string;
+ description: string;
+}
+
+const Introduction = ({
+ className,
+ imgSrc,
+ keyword,
+ title,
+ description,
+}: Props) => {
+ return (
+
+
+
+
+ {keyword}
+
+
+ {title}
+
+
+ {description}
+
+
+
+ );
+};
+
+export default Introduction;
diff --git a/src/utils/validations.ts b/src/utils/validations.ts
new file mode 100644
index 00000000..1f5c8a96
--- /dev/null
+++ b/src/utils/validations.ts
@@ -0,0 +1,29 @@
+const EMAIL_REG = /^[A-Za-z0-9_\.\-]+@[A-Za-z0-9\-]+\.[A-Za-z0-9\-]+/;
+
+export const validateEmail = (emailValue: string) => {
+ if (EMAIL_REG.test(emailValue)) {
+ return { isValid: true, errorMsg: null };
+ } else {
+ return { isValid: false, errorMsg: "잘못된 이메일 형식입니다" };
+ }
+};
+
+export const validatePassword = (passwordValue: string) => {
+ if (passwordValue.length < 8) {
+ return { isValid: false, errorMsg: "비밀번호를 8자 이상 입력해주세요" };
+ } else {
+ return { isValid: true, errorMsg: null };
+ }
+};
+
+//passwordcheckvalue는 isEmpty로 undefined값이 사전에 필터링
+export const validatePasswordCheck = (
+ passwordCheckValue: string,
+ comparisonValue: string | undefined
+) => {
+ if (comparisonValue !== passwordCheckValue) {
+ return { isValid: false, errorMsg: "비밀번호가 일치하지 않습니다" };
+ } else {
+ return { isValid: true, errorMsg: null };
+ }
+};
diff --git a/vite.config.js b/vite.config.js
index 461d5972..48aa681d 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,9 +1,10 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
+import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/
export default defineConfig({
- plugins: [react()],
+ plugins: [react(), tailwindcss()],
assetsInclude: ["**/*.ttf"],
resolve: {
alias: {