본문 바로가기

Web_Project

로그인모달창, 회원가입, 로그인

import React from "react";
import { IoClose } from "react-icons/io5";
import { Link } from "react-router-dom";
import { useDispatch } from "react-redux";
import { googleLogin, kakaoLogin } from "../../../redux/authSlice";
import {
  ModalContainer,
  Background,
  IoCloseDiv,
  LoginModalImg,
  P,
  GoogleBtn,
  KakaoBtn,
  LoginModalBtn,
} from "./LoginModalStyles";

const LoginModal = ({ closeModal }) => {
  const dispatch = useDispatch();

  const GoogleLogin = () => {
    window.location.href = process.env.REACT_APP_GOOGLE_URL;
    dispatch(googleLogin());
  };

  const KakaoLogin = () => {
    window.location.href = process.env.REACT_APP_KAKAO_URL;
    dispatch(kakaoLogin());
  };

  return (
    <>
      <Background onClick={closeModal} />
      <ModalContainer>
        <IoCloseDiv onClick={closeModal}>
          <IoClose />
        </IoCloseDiv>
        <LoginModalImg
          src="/imgs/Character/giftipie-01.png"
          alt="pie"
          w="100px"
          h="100px"
        />
        <P fs="20px" fw="700" mt="20px" color="white">
          Giftipie에서
        </P>
        <P fs="20px" fw="700" mt="4px" color="white">
          정말 원하는 선물을
        </P>
        <P fs="20px" fw="700" mt="4px" mb="20px" color="white">
          주고받아요
        </P>
        <Link to="/signup">
          <LoginModalBtn>
            가입하기
          </LoginModalBtn>
        </Link>
        <Link to="/login">
          <LoginModalBtn>
            로그인하기
          </LoginModalBtn>
        </Link>
        <GoogleBtn onClick={GoogleLogin}>
          <LoginModalImg
            src="/imgs/Login/google.png"
            alt="google"
            w="65%"
            mt="10px"
          />
        </GoogleBtn>
        <KakaoBtn onClick={KakaoLogin}>
          <LoginModalImg
            src="/imgs/Login/kakao.png"
            alt="kakao"
            w="65%"
            mt="10px"
          />
        </KakaoBtn>
      </ModalContainer>
    </>
  );
};

export default LoginModal;
import styled from "styled-components";
import theme from "../../../styles/theme";

export const ModalContainer = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 100%;
  max-width: 350px;
  height: 550px;
  background-color: ${theme.gray1};
  position: fixed;
  bottom: 50%;
  right: 50%;
  transform: translate(50%, 50%);
  border-radius: 10px;
  z-index: 10;
  padding-bottom: 30px;
`;

export const Background = styled.div`
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: #00000080;
  z-index: 9;
`;

export const IoCloseDiv = styled.div`
  display: flex;
  justify-content: flex-end;
  align-items: center;
  width: 100%;
  max-width: 442px;
  font-size: 30px;
  color: lightgray;
  padding: 8px;

  &:hover {
    color: darkgray;
    cursor: pointer;
  }
`;

export const LoginModalImg = styled.img`
  width: ${(props) => props.w};
  height: ${(props) => props.h};
  margin-top: ${(props) => props.mt};
  margin-bottom: ${(props) => props.mb};
`;

export const P = styled.p`
  padding-top: ${(props) => props.pt};
  padding-bottom: ${(props) => props.pb};
  padding-left: ${(props) => props.pl};
  padding-right: ${(props) => props.pr};
  margin-top: ${(props) => props.mt};
  margin-bottom: ${(props) => props.mb};
  margin-left: ${(props) => props.ml};
  margin-right: ${(props) => props.mr};
  font-size: ${(props) => props.fs};
  font-weight: ${(props) => props.fw};
  color: ${(props) => props.color};
  display: flex;
  align-items: center;
`;

export const GoogleBtn = styled.button``;

export const KakaoBtn = styled.button``;

export const LoginModalBtn = styled.button`
  background-color: ${(props) => props.color};
  padding: 12px 12px;
  font-size: 18px;
  font-weight: 500;
  color: white;
  border-radius: 8px;
  width: 220px;
  margin: 6px;
  border: 1px solid lightgray;
`;
import React, { useState } from "react";
import { FaAngleLeft } from "react-icons/fa6";
import { useNavigate } from "react-router-dom";
import { signup } from "../../../apis/auth";
import {
  MainContainer,
  LeftContainer,
  Logo,
  P,
  RightContainer,
  SignupFieldContainer,
  Body,
  SignupIconDiv,
  SignupImg,
  SignupInput,
  SignupBtn,
  SignupEmailHelpDiv,
  SignupNicknameHelpDiv,
  SignupPasswordHelpDiv,
  SignupPhoneNumberHelpDiv,
  BlankLine,
} from "./SignupStyles";

// InputField 컴포넌트
const InputField = ({ onChange, onKeyDown, title, type, placeholder }) => (
  <div>
    <P fs="20px" pb="10px" color="#FFFFFF">
      {title}
    </P>
    <SignupInput
      type={type}
      placeholder={`${placeholder}`}
      onChange={onChange}
      onKeyDown={onKeyDown}
    />
  </div>
);

const Signup = () => {
  const navigate = useNavigate();
  const [email, setEmail] = useState("");
  const [nickname, setNickname] = useState("");
  const [password, setPassword] = useState("");
  const [phoneNumber, setPhoneNumber] = useState("");
  const [showEmailHelp, setShowEmailHelp] = useState(false);
  const [showNicknameHelp, setShowNicknameHelp] = useState(false);
  const [showPasswordHelp, setShowPasswordHelp] = useState(false);
  const [showPhoneNumberHelp, setShowPhoneNumberHelp] = useState(false);

  const handleBackClick = () => {
    navigate("/");
  };

  // Enter키가 눌렸을 때 로그인 처리
  const handleKeyDown = (e) => {
    if (e.key === "Enter") {
      handleSignupClick();
    }
  };

  // 알파벳 대소문자, 숫자, 특수문자, @기호, 도메인 부분은 2자 이상
  const isValidEmailFormat = (email) => {
    const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
    return emailRegex.test(email);
  };

  // 한 글자 이상
  const isValidNicknameFormat = (nickname) => {
    return nickname.length > 0;
  };

  // 알파벳 대소문자, 숫자, 특수문자를 조합하여 8자에서 15자 사이의 비밀번호
  const isValidPasswordFormat = (password) => {
    const passwordRegex =
      /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,15}$/;
    return passwordRegex.test(password);
  };

  // 10자에서 11자 사이의 숫자
  const isValidPhoneNumberFormat = (phoneNumber) => {
    const phoneNumberRegex = /^[0-9]{10,11}$/;
    return phoneNumberRegex.test(phoneNumber);
  };

  // 이메일이 비어있을 때 help 보여주기
  const handleEmailChange = (e) => {
    setEmail(e.target.value);
    setShowEmailHelp(
      e.target.value.trim() === "" || !isValidEmailFormat(e.target.value)
    );
  };

  // 닉네임이 비어있을 때 help 보여주기
  const handleNicknameChange = (e) => {
    setNickname(e.target.value);
    setShowNicknameHelp(
      e.target.value.trim() === "" || !isValidNicknameFormat(e.target.value)
    );
  };

  // 비밀번호가 비어있을 때 help 보여주기
  const handlePasswordChange = (e) => {
    setPassword(e.target.value);
    setShowPasswordHelp(
      e.target.value.trim() === "" || !isValidPasswordFormat(e.target.value)
    );
  };

  // 휴대폰 번호가 비어있을 때 help 보여주기
  const handlePhoneNumberChange = (e) => {
    setPhoneNumber(e.target.value);
    setShowPhoneNumberHelp(
      e.target.value.trim() === "" || !isValidPhoneNumberFormat(e.target.value)
    );
  };

  const handleSignupClick = async () => {
    try {
      await signup({ email, nickname, password, phoneNumber });
      navigate("/login");
    } catch (error) {
      console.error("가입 오류:", error);
    }
  };

  return (
    <MainContainer>
      <LeftContainer>
        <Logo>Giftipie</Logo>
        <P pt="25px" fs="16px" fw="800" pb="5px">
          기프티파이에서
        </P>
        <P fs="16px" fw="800" pb="5px">
          정말 원하는 선물을
        </P>
        <P fs="16px" fw="800">
          주고 받아요
        </P>
      </LeftContainer>
      <RightContainer>
        <Body>
          <SignupIconDiv>
            <FaAngleLeft onClick={handleBackClick} />
          </SignupIconDiv>
          <SignupFieldContainer>
            <SignupImg src="/imgs/Character/giftipie-02.png" alt="pie" />
            <InputField
              value={email}
              onChange={handleEmailChange}
              onKeyDown={handleKeyDown}
              title="이메일"
              type="email"
              placeholder="Email"
            />
            {showEmailHelp && email.trim() === "" && (
              <SignupEmailHelpDiv>이메일을 입력해 주세요.</SignupEmailHelpDiv>
            )}
            {showEmailHelp &&
              !isValidEmailFormat(email) &&
              email.trim() !== "" && (
                <SignupEmailHelpDiv>
                  유효한 이메일 형식이어야 합니다.
                </SignupEmailHelpDiv>
              )}
            <BlankLine h="30px" />
            <InputField
              value={nickname}
              onChange={handleNicknameChange}
              onKeyDown={handleKeyDown}
              title="닉네임"
              type="string"
              placeholder="Nickname"
            />
            {showNicknameHelp && nickname.trim() === "" && (
              <SignupNicknameHelpDiv>
                닉네임을 입력해 주세요.
              </SignupNicknameHelpDiv>
            )}
            <BlankLine h="30px" />
            <InputField
              value={password}
              onChange={handlePasswordChange}
              onKeyDown={handleKeyDown}
              title="비밀번호"
              type="password"
              placeholder="Password"
            />
            {showPasswordHelp && (
              <SignupPasswordHelpDiv>
                {password.trim() === ""
                  ? "비밀번호를 입력해 주세요."
                  : "비밀번호는 8자에서 15자 사이의 알파벳 대소문자, 숫자, 특수문자로 구성되어야 합니다."}
              </SignupPasswordHelpDiv>
            )}
            <BlankLine h="30px" />
            <InputField
              value={phoneNumber}
              onChange={handlePhoneNumberChange}
              onKeyDown={handleKeyDown}
              title="휴대폰 번호"
              type="text"
              placeholder="Phone Number"
            />
            {showPhoneNumberHelp && (
              <SignupPhoneNumberHelpDiv>
                {phoneNumber.trim() === ""
                  ? "휴대폰 번호를 입력해 주세요."
                  : !isValidPhoneNumberFormat(phoneNumber)
                  ? "휴대전화 번호는 10자에서 11자 사이의 숫자로 구성되어야 합니다."
                  : null}
              </SignupPhoneNumberHelpDiv>
            )}
          </SignupFieldContainer>
          <SignupBtn onClick={handleSignupClick}>회원가입하기</SignupBtn>
        </Body>
      </RightContainer>
    </MainContainer>
  );
};

export default Signup;
import styled from "styled-components";
import theme from "../../../styles/theme";

// 전체 컨테이너
export const MainContainer = styled.div`
  display: flex;
  justify-content: center;
  max-width: 1200px;
  min-height: 100vh;
  margin: 0 auto;
  flex-wrap: wrap;
`;

// 왼쪽 컨테이너
export const LeftContainer = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 500px;
  height: 100vh;
  padding: 20px;
  /* border: 1px solid lightgray; */
  margin-right: 100px;

  @media (max-width: 1024px) {
    display: none;
  }
`;

export const Logo = styled.h1`
  font-size: 24px;
  font-weight: 800;
`;

export const P = styled.p`
  padding-top: ${(props) => props.pt};
  padding-bottom: ${(props) => props.pb};
  padding-left: ${(props) => props.pl};
  padding-right: ${(props) => props.pr};
  font-size: ${(props) => props.fs};
  font-weight: ${(props) => props.fw};
  color: ${(props) => props.color};
  display: flex;
  align-items: center;
`;

// 오른쪽 컨테이너
export const RightContainer = styled.div`
  position: relative;
  width: 442px;
  height: 100vh;
  /* border: 1px solid lightgray; */
  overflow-y: scroll;

  &::-webkit-scrollbar {
    display: none;
  }

  @media (max-width: 442px) {
    width: 100%;
  }
`;

// 바디 영역
export const Body = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 100%;
  max-width: 442px;
  height: 100vh;
  margin: 0 auto;
`;

export const SignupFieldContainer = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: auto;
  width: 100%;
  max-width: 442px;
`;

export const BlankLine = styled.div`
  width: ${(props) => props.w};
  height: ${(props) => props.h};
`;

export const SignupIconDiv = styled.div`
  display: flex;
  justify-content: start;
  align-items: center;
  font-size: 30px;
  color: darkgray;
  width: 100%;
  max-width: 442px;
  padding: 20px;

  &:hover {
    color: gray;
    cursor: pointer;
  }
`;

export const SignupImg = styled.img`
  width: 150px;
  height: 150px;
  margin-bottom: 50px;
`;

export const SignupInput = styled.input`
  width: 300px;
  height: 40px;
  font-size: 16px;
  padding-left: 10px;
  border-radius: 5px;
`;

export const SignupBtn = styled.button`
  width: 100%;
  max-width: 442px;
  height: 50px;
  font-size: 20px;
  margin-top: auto;
  margin-bottom: auto;
  background-color: ${theme.primary};
  transition: all 300ms ease-in-out;
  border-radius: 7px;
  
  &:hover {
    background-color: ${theme.secondary};
  }
`;

export const SignupEmailHelpDiv = styled.div`
  width: 300px;
  height: 20px;
  font-size: 14px;
  margin-top: 10px;
  color: #f45757;
`;

export const SignupNicknameHelpDiv = styled.div`
  width: 300px;
  height: 20px;
  font-size: 14px;
  margin-top: 10px;
  color: #f45757;
`;

export const SignupPasswordHelpDiv = styled.div`
  width: 300px;
  height: 20px;
  font-size: 14px;
  margin-top: 10px;
  color: #f45757;
`;

export const SignupPhoneNumberHelpDiv = styled.div`
  width: 300px;
  height: 20px;
  font-size: 14px;
  margin-top: 10px;
  color: #f45757;
`;
import React, { useState } from "react";
import { FaAngleLeft } from "react-icons/fa6";
import { useNavigate } from "react-router-dom";
import InputField from "../../../components/LoginInput";
import { login } from "../../../apis/auth";
import { useDispatch } from "react-redux";
import { userLogin } from "../../../redux/authSlice";
import {
  MainContainer,
  LeftContainer,
  Logo,
  P,
  RightContainer,
  InputFieldContainer,
  Body,
  LoginIconDiv,
  LoginImg,
  LoginBtn,
  BlankLine,
} from "./LoginStyles";

const Login = () => {
  const dispatch = useDispatch();
  const navigate = useNavigate();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [showEmailHelp, setShowEmailHelp] = useState(false);
  const [showPasswordHelp, setShowPasswordHelp] = useState(false);

  const handleBackClick = () => navigate("/");

  // Enter키가 눌렸을 때 로그인 처리
  const handleKeyDown = (e) => {
    if (e.key === "Enter") {
      handleLoginClick();
    }
  };

  const isValidEmailFormat = (email) => {
    const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
    return emailRegex.test(email);
  };

  const isValidPasswordFormat = (password) => {
    const passwordRegex =
      /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,15}$/;
    return passwordRegex.test(password);
  };

  // 이메일이 비어있을 때 help 보여주기
  const handleEmailChange = (e) => {
    setEmail(e.target.value);
    setShowEmailHelp(
      e.target.value.trim() === "" || !isValidEmailFormat(e.target.value)
    );
  };

  // 비밀번호가 비어있을 때 help 보여주기
  const handlePasswordChange = (e) => {
    setPassword(e.target.value);
    setShowPasswordHelp(
      e.target.value.trim() === "" || !isValidPasswordFormat(e.target.value)
    );
  };

  // 빈 칸인 상태에서 로그인을 했을 때 help 보여주기
  const handleLoginClick = async () => {
    if (email.trim() === "" || !isValidEmailFormat(email)) {
      setShowEmailHelp(true);
      return;
    }

    if (password.trim() === "" || !isValidPasswordFormat(password)) {
      setShowPasswordHelp(true);
      return;
    }

    // API 호출을 통한 로그인 처리
    try {
      await login({ email, password });
      dispatch(userLogin()); // 로그인 액션 디스패치
      navigate("/");
    } catch (error) {
      console.error("로그인 에러:", error);
    }
  };

  return (
    <MainContainer>
      <LeftContainer>
        <Logo>Giftipie</Logo>
        <P pt="25px" fs="16px" fw="800" pb="5px">
          기프티파이에서
        </P>
        <P fs="16px" fw="800" pb="5px">
          정말 원하는 선물을
        </P>
        <P fs="16px" fw="800">
          주고 받아요
        </P>
      </LeftContainer>
      <RightContainer>
        <Body>
          <LoginIconDiv>
            <FaAngleLeft onClick={handleBackClick} />
          </LoginIconDiv>
          <InputFieldContainer>
            <LoginImg src="/imgs/Character/giftipie-02.png" alt="pie" />
            <InputField
              value={email}
              onChange={handleEmailChange}
              onKeyDown={handleKeyDown}
              title="이메일"
              type="email"
              placeholder="Email"
              showHelp={showEmailHelp}
              helpMessage={
                showEmailHelp && !isValidEmailFormat(email)
                  ? "올바른 이메일 주소 형식으로 입력해주세요."
                  : ""
              }
            />
            <BlankLine h="50px" />
            <InputField
              value={password}
              onChange={handlePasswordChange}
              onKeyDown={handleKeyDown}
              title="비밀번호"
              type="password"
              placeholder="Password"
              showHelp={showPasswordHelp}
              helpMessage={
                showPasswordHelp && !isValidPasswordFormat(password)
                  ? "비밀번호는 8자에서 15자 사이이어야 하며, 영문, 숫자, 특수문자(@$!%*?&)를 포함해야 합니다."
                  : ""
              }
            />
          </InputFieldContainer>
          <LoginBtn onClick={handleLoginClick}>로그인하기</LoginBtn>
        </Body>
      </RightContainer>
    </MainContainer>
  );
};

export default Login;
import styled from "styled-components";
import theme from "../../../styles/theme";

// 전체 컨테이너
export const MainContainer = styled.div`
  display: flex;
  justify-content: center;
  max-width: 1200px;
  min-height: 100vh;
  margin: 0 auto;
  flex-wrap: wrap;
`;

// 왼쪽 컨테이너
export const LeftContainer = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 500px;
  height: 100vh;
  padding: 20px;
  /* border: 1px solid lightgray; */
  margin-right: 100px;

  @media (max-width: 1024px) {
    display: none;
  }
`;

export const Logo = styled.h1`
  font-size: 24px;
  font-weight: 800;
`;

export const P = styled.p`
  padding-top: ${(props) => props.pt};
  padding-bottom: ${(props) => props.pb};
  padding-left: ${(props) => props.pl};
  padding-right: ${(props) => props.pr};
  font-size: ${(props) => props.fs};
  font-weight: ${(props) => props.fw};
  color: ${(props) => props.color};
  display: flex;
  align-items: center;
`;

// 오른쪽 컨테이너
export const RightContainer = styled.div`
  position: relative;
  width: 442px;
  height: 100vh;
  /* border: 1px solid lightgray; */
  overflow-y: scroll;

  &::-webkit-scrollbar {
    display: none;
  }

  @media (max-width: 442px) {
    width: 100%;
  }
`;

// 바디 영역
export const Body = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 100%;
  max-width: 442px;
  height: 100vh;
  margin: 0 auto;
`;

export const InputFieldContainer = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: auto;
  width: 100%;
  max-width: 390px;
  color: white;
`;

export const BlankLine = styled.div`
  width: ${(props) => props.w};
  height: ${(props) => props.h};
`;

export const LoginIconDiv = styled.div`
  display: flex;
  justify-content: start;
  align-items: center;
  font-size: 30px;
  color: darkgray;
  width: 100%;
  max-width: 442px;
  padding: 20px;

  &:hover {
    color: gray;
    cursor: pointer;
  }
`;

export const LoginImg = styled.img`
  width: 150px;
  height: 150px;
  margin-bottom: 50px;
`;

export const LoginInput = styled.input`
  width: 300px;
  height: 40px;
  font-size: 16px;
  padding-left: 10px;
  border-radius: 5px;
`;

export const LoginBtn = styled.button`
  width: 100%;
  max-width: 442px;
  height: 50px;
  font-size: 20px;
  margin-top: auto;
  margin-bottom: auto;
  background-color: ${theme.primary};
  transition: all 300ms ease-in-out;
  border-radius: 7px;

  &:hover {
    background-color: ${theme.secondary};
  }
`;

export const LoginHelpDiv = styled.div`
  width: 300px;
  height: 20px;
  font-size: 14px;
  margin-top: 10px;
  color: #f45757;
`;
const theme = {
  primary: "#FF7C7C",
  primaryFont: "#FF5959",
  white: "#FFFFFF",
  black: "#2C2C2C",
  gray1: "#3F3F3F",
  gray2: "#5F5F5F",
  gray3: "#868686",
  gray4: "#B6B6B6",
  gray5: "#E4E4E4",
  gray6: "#ECECEC",
  secondary: "#FFE6C1",
};

export default theme;

/* 예시 */
// background-color: ${theme.primary};
// import theme from "../../styles/theme";

'Web_Project' 카테고리의 다른 글

데스크탑뷰  (0) 2024.02.18
생성-수정-결제페이지  (0) 2024.02.16
메인페이지 webkit-scrollbar 적용  (0) 2024.02.16
카카오페이 API 연동  (0) 2024.02.14
API 연결 성공  (2) 2024.02.07