diff --git a/public/Icons/google.svg b/public/Icons/google.svg new file mode 100644 index 0000000000..7ae503d30e --- /dev/null +++ b/public/Icons/google.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/Icons/kakaolg.svg b/public/Icons/kakaolg.svg new file mode 100644 index 0000000000..b5d0a6169a --- /dev/null +++ b/public/Icons/kakaolg.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/components/signinPage/ErrorMesage.tsx b/src/components/signinPage/ErrorMesage.tsx new file mode 100644 index 0000000000..a7f97075ab --- /dev/null +++ b/src/components/signinPage/ErrorMesage.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import styled from "styled-components"; + +const Error = styled.p` + color: #ff5b56; + font-size: 14px; + font-weight: 400; +`; + +function ErrorMesage({ text }: { text: string | undefined }) { + return {text}; +} + +export default ErrorMesage; diff --git a/src/components/signinPage/Input.tsx b/src/components/signinPage/Input.tsx index 5970ec99d4..f2961800b5 100644 --- a/src/components/signinPage/Input.tsx +++ b/src/components/signinPage/Input.tsx @@ -3,18 +3,20 @@ import React, { InputHTMLAttributes, RefObject, useRef, useState } from "react"; import styled from "styled-components"; import setPwOff from "@/public/Icons/eye-off.svg"; import setPwOn from "@/public/Icons/eye-on.svg"; -import { useForm } from "react-hook-form"; +import { FieldErrors, UseFormRegisterReturn, useForm } from "react-hook-form"; +import ErrorMesage from "./ErrorMesage"; type InputStyledProps = { $error?: boolean; }; const Layout = styled.div` - width: 400px; + width: 100%; display: flex; flex-direction: column; gap: 12px; position: relative; + margin-bottom: 6px; `; const Label = styled.label` @@ -45,38 +47,27 @@ const ImgPosition = styled.div` right: 18px; `; -const ErrorMesage = styled.p` - color: #ff5b56; - font-size: 14px; - font-weight: 400; -`; - interface InputProps extends InputHTMLAttributes { id: string; placeholder: string; - error?: boolean; - label: string; + errors: FieldErrors; + label?: string; + validation?: UseFormRegisterReturn; } -function Input({ id, type = "text", placeholder, error, label }: InputProps) { +function Input({ + id, + type = "text", + placeholder, + errors, + label, + validation, +}: InputProps) { const [pwState, setPwState] = useState(false); - const passwordRef = useRef(null); - - const { - register, - formState: { errors }, - } = useForm(); + const error = errors[id]; const toggleEyesButton = () => { - if (passwordRef.current) { - if (pwState) { - passwordRef.current.type = "text"; - setPwState(false); - } else { - passwordRef.current.type = "password"; - setPwState(true); - } - } + setPwState((prev) => !prev); }; return ( @@ -84,25 +75,23 @@ function Input({ id, type = "text", placeholder, error, label }: InputProps) { {type === "password" && ( {pwState ? ( - Pw-off - ) : ( Pw-on + ) : ( + Pw-off )} )} - {errors.category?.type === "required" && ( - 필수 입력사항입니다. - )} + {errors && } ); } diff --git a/src/components/signinPage/LogoBox.tsx b/src/components/signinPage/LogoBox.tsx index 859e00d61e..c1b46027ce 100644 --- a/src/components/signinPage/LogoBox.tsx +++ b/src/components/signinPage/LogoBox.tsx @@ -17,7 +17,6 @@ const Text = styled.p` font-size: 16px; font-weight: 400; line-height: 24px; /* 150% */ - margin-bottom: 30px; `; const LinkStyle = { @@ -26,16 +25,21 @@ const LinkStyle = { fontWeight: 600, }; -function LogoBox() { +interface LogoBox { + text: string; + linkText: string; +} + +function LogoBox({ text, linkText }: LogoBox) { return ( logo - 회원이 아니신가요?{" "} + {text}{" "} - 회원 가입하기 + {linkText} diff --git a/src/components/signinPage/SocialBox.tsx b/src/components/signinPage/SocialBox.tsx new file mode 100644 index 0000000000..86bdebc3bb --- /dev/null +++ b/src/components/signinPage/SocialBox.tsx @@ -0,0 +1,47 @@ +import Image from "next/image"; +import React from "react"; +import googleIcon from "@/public/Icons/google.svg"; +import kakao from "@/public/Icons/kakaolg.svg"; +import styled from "styled-components"; +import Link from "next/link"; + +const Layout = styled.div` + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 24px; + margin: 0 auto; + border-radius: 8px; + border: 1px solid var(--Linkbrary-gray20, #ccd5e3); + background: var(--Linkbrary-gray10, #e7effb); +`; + +const Text = styled.p` + color: var(--Linkbrary-gray100, #373740); + font-size: 14px; + font-weight: 400; +`; + +const SocialLinks = styled.div` + display: flex; + gap: 16px; +`; + +function SocialBox({ text }: { text: string }) { + return ( + + {text} + + + google + + + kakao + + + + ); +} + +export default SocialBox; diff --git a/src/pages/signin/index.tsx b/src/pages/signin/index.tsx index 2f21d8843c..b3f82640db 100644 --- a/src/pages/signin/index.tsx +++ b/src/pages/signin/index.tsx @@ -4,10 +4,14 @@ import styled from "styled-components"; import SubmitBtn from "@/src/components/signinPage/SubmitBtn"; import LogoBox from "@/src/components/signinPage/LogoBox"; import { useForm } from "react-hook-form"; +import SocialBox from "@/src/components/signinPage/SocialBox"; +import ErrorMesage from "@/src/components/signinPage/ErrorMesage"; +import { loginRequest } from "@/src/utils/Api"; +import { useRouter } from "next/router"; const BackGround = styled.div` width: 100%; - padding: 150px 0px 253px 0px; + padding: 100px 0px 200px 0px; margin: 0 auto; background: var(--Linkbrary-bg, #f0f6ff); `; @@ -18,33 +22,90 @@ const Layout = styled.div` flex-direction: column; align-items: center; margin: 0 auto; + gap: 30px; + + @media (max-width: 767px) { + width: 325px; + } `; const Form = styled.form` + width: 100%; display: flex; flex-direction: column; gap: 30px; `; + function SigninPage() { + const router = useRouter(); + + const { + register, + formState: { errors }, + handleSubmit, + setError, + } = useForm({ mode: "onBlur" }); + + const emailValidation = register("userEmail", { + required: { + value: true, + message: "이메일을 입력해 주세요", + }, + pattern: { + value: /^\S+@\S+$/i, + message: "올바른 이메일 주소가 아닙니다.", + }, + }); + + const passwordValidation = register("userPw", { + required: { + value: true, + message: "비밀번호를 입력해 주세요", + }, + }); + + const onSubmit = async (data: any) => { + const res = await loginRequest(data); + + if (res) { + const { data } = res; + window.localStorage.setItem("accessToken", data.accessToken); + router.push("/folder"); + } else { + setError("userEmail", { message: "이메일을 확인해주세요" }); + setError("userPw", { message: "비밀번호를 확인해주세요" }); + } + }; + return ( - -
- - + + +
+ +
+ +
+ +
로그인 +
); diff --git a/src/pages/signup/index.tsx b/src/pages/signup/index.tsx new file mode 100644 index 0000000000..96202fe452 --- /dev/null +++ b/src/pages/signup/index.tsx @@ -0,0 +1,131 @@ +import React from "react"; +import Input from "@/src/components/signinPage/Input"; +import styled from "styled-components"; +import SubmitBtn from "@/src/components/signinPage/SubmitBtn"; +import LogoBox from "@/src/components/signinPage/LogoBox"; +import { useForm } from "react-hook-form"; +import SocialBox from "@/src/components/signinPage/SocialBox"; +import { chechEmail, signupRequest } from "@/src/utils/Api"; +import { useRouter } from "next/router"; + +const BackGround = styled.div` + width: 100%; + padding: 80px 0px 180px 0px; + margin: 0 auto; + background: var(--Linkbrary-bg, #f0f6ff); +`; + +const Layout = styled.div` + width: 400px; + display: flex; + flex-direction: column; + align-items: center; + margin: 0 auto; + gap: 30px; + + @media (max-width: 767px) { + width: 325px; + } +`; + +const Form = styled.form` + width: 100%; + display: flex; + flex-direction: column; + gap: 20px; +`; + +function SignupPage() { + const router = useRouter(); + + const { + register, + formState: { errors }, + handleSubmit, + watch, + } = useForm({ mode: "onBlur" }); + + const emailValidation = register("userEmail", { + required: { + value: true, + message: "이메일을 입력해 주세요", + }, + pattern: { + value: /^\S+@\S+$/i, + message: "올바른 이메일 주소가 아닙니다.", + }, + validate: async (value) => { + const res = await chechEmail(value); + if (!res) return "이미 존재하는 이메일입니다."; + }, + }); + + const passwordValidation = register("userPw", { + required: { + value: true, + message: "비밀번호를 입력해 주세요", + }, + pattern: { + value: /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/, + message: "비밀번호는 영문, 숫자 조합 8자 이상 입력해 주세요.", + }, + }); + + const passwordCheckValidation = register("userPwCh", { + required: { + value: true, + message: "비밀번호를 입력해 주세요", + }, + validate: (value) => { + if (watch("userPw") !== value) return "비밀번호가 일치하지 않아요."; + }, + }); + + const onSubmit = async (data: any) => { + const res = await signupRequest(data); + + if (res) { + const { data } = res; + window.localStorage.setItem("accessToken", data.accessToken); + router.push("/signin"); + } + }; + + return ( + + + +
+ + + + 회원가입 +
+ +
+
+ ); +} + +export default SignupPage; diff --git a/src/utils/Api.ts b/src/utils/Api.ts index e30ea60cbe..ca93d93dea 100644 --- a/src/utils/Api.ts +++ b/src/utils/Api.ts @@ -1,3 +1,4 @@ +import axios from "axios"; import { LinkType } from "../interface/types"; const BASE_URL = "https://bootcamp-api.codeit.kr/api/"; @@ -98,3 +99,75 @@ export async function searchFolderLink( console.log(e); } } + +interface FormdataType { + userEmail: string; + userPw: string; +} + +export async function loginRequest(formdata: FormdataType) { + try { + const response = await axios.post(`${BASE_URL}sign-in`, { + email: formdata.userEmail, + password: formdata.userPw, + }); + + if (response.status === 200) { + const res = response.data; + return res; + } else if (response.status === 400) { + console.log("없는 유저 정보"); + } else if (response.status === 500) { + console.log("서버에러"); + } + } catch (error) { + // 네트워크 연결 오류 처리 + console.error("Network error:", error); + return null; + } +} + +export async function signupRequest(formdata: FormdataType) { + try { + const response = await axios.post(`${BASE_URL}sign-in`, { + email: formdata.userEmail, + password: formdata.userPw, + }); + + if (response.status === 200) { + const res = response.data; + return res; + } else if (response.status === 400) { + console.log("잘 못 된 정보를 입력했습니다."); + } else if (response.status === 500) { + console.log("서버에러"); + } + } catch (error) { + // 네트워크 연결 오류 처리 + console.error("Network error:", error); + return null; + } +} + +export async function chechEmail(email: string) { + try { + const response = await axios.post(`${BASE_URL}check-email`, { + email: email, + }); + + if (response.status === 200) { + const res = response.data; + return res; + } else if (response.status === 400) { + console.log("잘 못 된 정보를 입력했습니다."); + } else if (response.status === 409) { + console.log("이메일이 중복되었습니다."); + } else if (response.status === 500) { + console.log("서버에러"); + } + } catch (error) { + // 네트워크 연결 오류 처리 + console.error("Network error:", error); + return null; + } +}