diff --git a/pages/_app.tsx b/pages/_app.tsx index 92be579be..2cc46ae9a 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -3,14 +3,20 @@ import Footer from "@/src/layouts/Footer"; import "@/styles/Home.css"; import "@/styles/Footer.css"; import "@/styles/globals.css"; +import "@/styles/Login.css"; import type { AppProps } from "next/app"; +import { useRouter } from "next/router"; export default function App({ Component, pageProps }: AppProps) { + const { pathname } = useRouter(); + const login = pathname !== "/login"; + const signup = pathname !== "/signup"; + return ( <> - + {login && signup && } - + {login && signup && } > ); } diff --git a/pages/login/index.tsx b/pages/login/index.tsx index 9fd8785a7..072216b83 100644 --- a/pages/login/index.tsx +++ b/pages/login/index.tsx @@ -1,5 +1,183 @@ -import StyledInner from "@/src/layouts/StyledInner.style"; +import Image from "next/image"; +import Link from "next/link"; +import Logo from "@/src/assets/login/logo.svg"; +import GoogleLogin from "@/src/assets/login/easyLogin_google.svg"; +import KakaoLogin from "@/src/assets/login/easyLogin_kakao.svg"; +import EyeIcon from "@/src/assets/login/eye_icon.svg"; +import EyeShowIcon from "@/src/assets/login/eye_show_icon.svg"; +import { ChangeEvent, FocusEvent, FormEvent, useState } from "react"; +import { useRouter } from "next/router"; + +type ValuesProps = { + email: string; + password: string; +}; export default function Login() { - return Login; + const [loginInfo, setLoginInfo] = useState({ + email: "", + password: "", + }); + const [isPasswordHidden, setIsPasswordHidden] = useState(false); + const [emailErrorMessage, setEmailErrorMessage] = useState(""); + const [passwordErrorMessage, setPasswordErrorMessage] = useState(""); + const router = useRouter(); + + const pattern = /^[A-Za-z0-9]+@[A-Za-z0-9]+\.[A-Za-z0-9]+/; + + const loginValid = + pattern.test(loginInfo.email.trim()) && loginInfo.password.length >= 8; + + const handleInputBlur = (e: ChangeEvent) => { + if (!e.target.value.trim()) { + setEmailErrorMessage("이메일을 입력해 주세요."); + } else if (!pattern.test(e.target.value.trim())) { + setEmailErrorMessage("잘못된 이메일 형식입니다."); + } else { + setEmailErrorMessage(""); + } + }; + + const handleChange = (e: ChangeEvent) => { + const { name, value } = e.target; + + setLoginInfo((prevLoginInfo) => ({ + ...prevLoginInfo, + [name]: value, + })); + + if (name === "password") { + if (loginInfo.password.length < 7) { + setPasswordErrorMessage("비밀번호를 8자 이상 입력해 주세요."); + } else { + setPasswordErrorMessage(""); + } + } + }; + + const handlePasswordBlur = (e: FocusEvent) => { + if (e.target.value.trim() === "") { + setPasswordErrorMessage("비밀번호를 입력해 주세요."); + } + }; + + const handleClick = () => { + setIsPasswordHidden(!isPasswordHidden); + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + + const res = await fetch("https://panda-market-api.vercel.app/auth/signIn", { + method: "POST", + headers: { + "Content-type": "application/json", + }, + body: JSON.stringify(loginInfo), + }); + + const data = await res.json(); + + if (res.ok) { + localStorage.setItem("accessToken", data.accessToken); + router.push("/"); + } + }; + + return ( + + + + + + + + + + + + 이메일 + + + {emailErrorMessage} + + + + 비밀번호 + + + + + {isPasswordHidden ? ( + + ) : ( + + )} + + + + {passwordErrorMessage} + + + + 로그인 + + + + + + 간편 로그인하기 + + + + + + + + + + + + + + + + + + 판다마켓이 처음이신가요? 회원가입 + + + + ); } diff --git a/pages/signup/index.tsx b/pages/signup/index.tsx new file mode 100644 index 000000000..39ef47e00 --- /dev/null +++ b/pages/signup/index.tsx @@ -0,0 +1,262 @@ +import Link from "next/link"; +import Logo from "@/src/assets/login/logo.svg"; +import Image from "next/image"; +import GoogleLogin from "@/src/assets/login/easyLogin_google.svg"; +import KakaoLogin from "@/src/assets/login/easyLogin_kakao.svg"; +import EyeIcon from "@/src/assets/login/eye_icon.svg"; +import EyeShowIcon from "@/src/assets/login/eye_show_icon.svg"; +import { ChangeEvent, FocusEvent, FormEvent, useEffect, useState } from "react"; +import { useRouter } from "next/router"; + +type ValuesProps = { + email: string; + nickname: string; + password: string; + passwordConfirmation: string; +}; + +export default function SignUp() { + const [signUpInfo, setSignUpInfo] = useState({ + email: "", + nickname: "", + password: "", + passwordConfirmation: "", + }); + const [isPasswordHidden, setIsPasswordHidden] = useState(false); + const [isPasswordConfirmHidden, setIsPasswordConfirmHidden] = useState(false); + const [emailErrorMessage, setEmailErrorMessage] = useState(""); + const [passwordErrorMessage, setPasswordErrorMessage] = useState(""); + const [nickNameErrorMessage, setNickNameErrorMessage] = useState(""); + const [confirmErrorMessage, setConfirmErrorMessage] = useState(""); + const router = useRouter(); + + const pattern = /^[A-Za-z0-9]+@[A-Za-z0-9]+\.[A-Za-z0-9]+/; + + const signUpValid = + pattern.test(signUpInfo.email.trim()) && + signUpInfo.password.length >= 8 && + signUpInfo.nickname && + signUpInfo.password === signUpInfo.passwordConfirmation; + + useEffect(() => { + if (localStorage.getItem("accessToken")) { + router.push("/"); + } + }, []); + + const handleInputBlur = (e: ChangeEvent) => { + if (!e.target.value.trim()) { + setEmailErrorMessage("이메일을 입력해 주세요."); + } else if (!pattern.test(e.target.value.trim())) { + setEmailErrorMessage("잘못된 이메일 형식입니다."); + } else { + setEmailErrorMessage(""); + } + }; + + const handleChange = (e: ChangeEvent) => { + const { name, value } = e.target; + + setSignUpInfo((prevLoginInfo) => ({ + ...prevLoginInfo, + [name]: value, + })); + + if (name === "password") { + if (signUpInfo.password.length < 7) { + setPasswordErrorMessage("비밀번호를 8자 이상 입력해 주세요."); + } else { + setPasswordErrorMessage(""); + } + } + }; + + const handlePasswordBlur = (e: FocusEvent) => { + if (e.target.value.trim() === "") { + setPasswordErrorMessage("비밀번호를 입력해 주세요."); + } + }; + + const handlePasswordToggleClick = () => { + setIsPasswordHidden(!isPasswordHidden); + }; + + const handlePasswordConfirmToggleClick = () => { + setIsPasswordConfirmHidden(!isPasswordConfirmHidden); + }; + + const handleNickNameBlur = () => { + if (signUpInfo.nickname.trim() === "") { + setNickNameErrorMessage("닉네임을 입력해 주세요."); + } else { + setNickNameErrorMessage(""); + } + }; + + const handleConfirmBlur = (e: FocusEvent) => { + if (signUpInfo.password !== signUpInfo.passwordConfirmation) { + setConfirmErrorMessage("비밀번호가 일치하지 않습니다."); + } else { + setConfirmErrorMessage(""); + } + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + + const res = await fetch("https://panda-market-api.vercel.app/auth/signUp", { + method: "POST", + headers: { + "Content-type": "application/json", + }, + body: JSON.stringify(signUpInfo), + }); + + if (res.ok) { + router.push("/login"); + } + }; + + return ( + + + + + + + + + + + + 이메일 + + + {emailErrorMessage} + + + + 닉네임 + + + {nickNameErrorMessage} + + + + 비밀번호 + + + + + {isPasswordHidden ? ( + + ) : ( + + )} + + + + {passwordErrorMessage} + + + + 비밀번호 확인 + + + + + {isPasswordHidden ? ( + + ) : ( + + )} + + + + {confirmErrorMessage} + + + + 회원가입 + + + + + + 간편 로그인하기 + + + + + + + + + + + + + + + + + + 이미 회원이신가요? 로그인 + + + + ); +} diff --git a/response.http b/response.http index 5a0c92a1f..606c0d539 100644 --- a/response.http +++ b/response.http @@ -16,4 +16,14 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NDgxLCJzY29wZ { "content": "댓글 테스트" +} + + +### 로그인 +POST https://panda-market-api.vercel.app/auth/signIn +Content-Type: application/json + +{ + "email": "shinhwiiron@gmail.com", + "password": "shc0918*" } \ No newline at end of file diff --git a/src/assets/login/easyLogin_google.svg b/src/assets/login/easyLogin_google.svg new file mode 100644 index 000000000..39de6337b --- /dev/null +++ b/src/assets/login/easyLogin_google.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/login/easyLogin_kakao.svg b/src/assets/login/easyLogin_kakao.svg new file mode 100644 index 000000000..15dc4d0ff --- /dev/null +++ b/src/assets/login/easyLogin_kakao.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/login/eye_icon.svg b/src/assets/login/eye_icon.svg new file mode 100644 index 000000000..195b77b1f --- /dev/null +++ b/src/assets/login/eye_icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/login/eye_show_icon.svg b/src/assets/login/eye_show_icon.svg new file mode 100644 index 000000000..35a75305e --- /dev/null +++ b/src/assets/login/eye_show_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/login/logo.svg b/src/assets/login/logo.svg new file mode 100644 index 000000000..8100d2f6a --- /dev/null +++ b/src/assets/login/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/layouts/Header.style.ts b/src/layouts/Header.style.ts index c04fec5fb..074431a15 100644 --- a/src/layouts/Header.style.ts +++ b/src/layouts/Header.style.ts @@ -14,7 +14,8 @@ const StyledHeaderInner = styled.div` margin: 0 auto; button, - a { + a, + > div { margin-left: auto; } diff --git a/src/layouts/Header.tsx b/src/layouts/Header.tsx index da2be82ba..06d2d2834 100644 --- a/src/layouts/Header.tsx +++ b/src/layouts/Header.tsx @@ -3,9 +3,17 @@ import Logo from "./Logo"; import Navigation from "./Navigation"; import Button from "./Button"; import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import ProfileImg from "./ProfileImg"; const Header = () => { const router = useRouter(); + const [login, setLogin] = useState(false); + useEffect(() => { + if (localStorage.getItem("accessToken")) { + setLogin(true); + } + }, []); return ( @@ -14,7 +22,11 @@ const Header = () => { {router.pathname !== "/" && } - + {!login ? ( + + ) : ( + + )} ); diff --git a/styles/Login.css b/styles/Login.css new file mode 100644 index 000000000..f26c40aec --- /dev/null +++ b/styles/Login.css @@ -0,0 +1,187 @@ +/* 로그인, 회원가입 */ +.wrapper.login { + padding: 231px 0; +} + +.login .inner { + width: 640px; +} + +.login .logo { + display: block; + width: 396px; + height: 132px; + margin: 0 auto; +} + +.login .loginWrap { + margin-top: 40px; +} + +.login .loginWrap .input { + display: flex; + flex-direction: column; +} + +.login .loginWrap .input + .input { + margin-top: 24px; +} + +.login .loginWrap .input label { + font-size: 18px; + font-weight: 700; + color: var(--gray800); + margin-bottom: 16px; +} + +.login .loginWrap .loginBtn, +.login .loginWrap .signupBtn { + border-radius: 40px; + background: var(--blue); + width: 100%; + height: 56px; + font-size: 20px; + font-weight: 600; + color: #fff; + margin-top: 24px; + transition: 0.3s; +} + +.login .loginWrap .loginBtn:disabled, +.login .loginWrap .signupBtn:disabled { + cursor: auto; + background: var(--gray400); +} + +.login .easyLogin { + border-radius: 8px; + background: #e6f2ff; + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 23px; + margin-top: 24px; +} + +.login .easyLogin p { + font-weight: 500; + color: var(--gray800); +} + +.login .easyLogin ul { + display: flex; + gap: 16px; +} + +.login .easyLogin ul li a { + display: block; + width: 42px; + height: 42px; + position: relative; +} + +.login .signup { + font-size: 14px; + font-weight: 500; + color: var(--gray800); + text-align: center; + margin-top: 24px; +} + +.login .signup a { + font-size: 14px; + font-weight: 500; + color: var(--blue); + text-decoration: underline; +} + +input { + border: 0; + border-radius: 12px; + background: var(--gray100); + height: 56px; + color: var(--gray800); + padding: 0 24px; +} + +input::placeholder { + color: var(--gray400); +} + +input:focus { + outline-color: var(--blue); +} + +input.error { + border: 1px solid #f74747; +} + +.errorMessage { + font-size: 14px; + font-weight: 600; + color: #f74747; + margin: 8px 0 0 16px; +} + +.password { + position: relative; +} + +.password input { + width: 100%; + padding-right: 56px; +} + +.password .passwordToggleBtn, +.password .passwordCheckToggleBtn { + width: 24px; + height: 24px; + position: absolute; + top: 50%; + right: 16px; + transform: translateY(-50%); +} + +@media (max-width: 1200px) { + .wrapper.login { + padding: 190px 0; + } + + .login .inner { + width: 86%; + } +} + +@media (max-width: 744px) { + .wrapper.login { + padding: 80px 0; + } + + .login .inner { + width: 91.5%; + max-width: 400px; + } + + .login .logo { + width: 198px; + height: 66px; + } + + .login .loginWrap { + margin-top: 24px; + } + + .login .loginWrap .input label { + font-size: 14px; + margin-bottom: 8px; + } + + .login .loginWrap .input + .input { + margin-top: 16px; + } + + .login .loginWrap .loginBtn, + .login .loginWrap .signupBtn { + margin-top: 16px; + } +}
간편 로그인하기
+ 판다마켓이 처음이신가요? 회원가입 +
+ 이미 회원이신가요? 로그인 +