ID | PW |
---|---|
test@a.com | asdzxc123! |
๐ ๋ฐฐํฌ URL : ๐ด TasteMap
TasteMap์ ๋ง์ง์ ๊ณต์ ํ๊ณ ์์ ์ ๋ง์ง ์ง๋๋ฅผ ์์ฑํ๋ SNS ํ๋ซํผ์ ๋๋ค.
- ๋๋ง ์๊ณ ์๋ ์จ๊ฒจ์ง ๋ง์ง ์ ๋ณด๋ฅผ ๊ณต์ ํ๊ณ , ์ํ๋ ๋ง์ง์ ๋์ ๋ง์ง ์ง๋์ ์ถ๊ฐํ์ฌ ๋๋ง์ ๋ง์ง ์ง๋๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
- ๋ด๊ฐ ๋ง๋ ๋ง์ง ์ง๋๋ฅผ ๋ณ๋์ URL ๋งํฌ๋ฅผ ํตํด ๊ณต์ ํ ์ ์์ต๋๋ค.
- ์ง๋์ ๋ก๋ ๋ทฐ ๊ธฐ๋ฅ์ ํตํด ํด๋น ๋ง์ง ์์น๋ฅผ ์ฝ๊ฒ ํ์ ํ ์ ์์ต๋๋ค.
- ๋๊ธ๊ณผ ๋ต๊ธ ์์ฑ์ ํตํด ์ฌ๋ฌ ์ฌ์ฉ์๋ค๊ณผ ๋ง์ง์ ๋ํด ์ํตํ ์ ์์ต๋๋ค.
- ํ๋ก์ฐํ ์ฌ์ฉ์์ ๊ฒ์๋ฌผ์ ํผ๋ ํ์ด์ง์์ ๋ณผ ์ ์์ต๋๋ค.
๊ฐ๋ฐ ์๋
- ๋๋ง ์๊ณ ์๋ ์จ์ ๋ง์ง์ ๊ณต์ ํ๊ณ , ์ฌ์ฉ์๋ค์ด ๋ง์ง ์ ๋ณด๋ฅผ ์์๊ฐ๋ฉฐ ๋๋ง์ ๋ง์ง ์ง๋๋ฅผ ์์ฑํด๊ฐ๋ SNS ํ๋ซํผ์ ๊ตฌํํ๊ณ ์ ๊ฐ๋ฐํ๊ฒ ๋์์ต๋๋ค.
- ๊ฐ๊ฒ ํ๋ณด ๋ฐ ์ง์ญ ํน์ ๋จน๊ฑฐ๋ฆฌ๋ค์ ์๋ ค ์ง์ญ ๊ฒฝ์ ํ์ฑํ์ ๋์์ ์ฃผ๊ณ ์ ๊ฐ๋ฐํ๊ฒ ๋์์ต๋๋ค.
๊ฐ๋ฐ ์์ : 2023. 09. 08
๊ฐ๋ฐ ์๋ฃ : 2023. 10. 08
Refactoring
- react-query ๋์ : 2023.11.19 ~ 2023.11.27
- customhook ๋์์ธ ํจํด ์ ์ฉ: 2023.12.01 ~ 2023.12.03
- clean code : 2023.12.04 ~ 2023.12.19
- react-hook-form : 2023.12.12 ~ 2023.12.18
๋์ ์ด์
- ๊ธฐ์กด์๋ redux-toolkit thunk๋ฅผ ์ด์ฉํ์ฌ api ์ฒ๋ฆฌ ๋ฐ api ์ํ๊ด๋ฆฌ๊ฐ ์ฝ๋ ์์ด ๋ง์์ง๊ณ , ๋ณต์กํ๋ค๋ ๋จ์ ์ด ์์ด react-query๋ฅผ ๋์ ํ์์ต๋๋ค.
๋์ ๋ฐฉ์
- ๊ธฐ์กด redux-toolkit์ global state ๊ด๋ฆฌ๋ฅผ ์ํด ์ฌ์ฉํ๊ณ , react-query๋ api ์ฒ๋ฆฌ ๋ฐ api ์ํ๊ด๋ฆฌ์ ์ฌ์ฉํ์์ต๋๋ค.
๋์ ์ผ๋ก ์ป์ ์ด์
- react-query ๋์ ์ผ๋ก ์๋ฒ api ์ฒ๋ฆฌ๊ฐ ๋งค์ฐ ๊ฐ๊ฒฐํด ์ก์ผ๋ฉด ์ํ๊ด๋ฆฌ ์ฝ๋๋ฅผ ์ง์ ๊ตฌ์ฑํ์ง ์์๋ react-query ์์ฒด ๋ด์ฅ๋ ์ํ๊ด๋ฆฌ ์์ฑ์ ํตํด ์ํ๊ด๋ฆฌ๋ฅผ ํ ์ ์์์ต๋๋ค.
- react-query๋ ์บ์ฑ๋ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ์๋ ํฅ์์ ๋์์ ์ค ์ ์์์ต๋๋ค.
- ๋์ผํ ๋ฐ์ดํฐ ์์ฒญ์ ๊ฒฝ์ฐ ์๋์ผ๋ก ์ ๊ฑฐํ๊ธฐ ๋๋ฌธ์ ์ค๋ณต ์์ฒญ์ ์ ๊ฒฝ์ฐ์ง ์์๋ ๋์ด ํธ๋ฆฌํ๊ฒ ์ฌ์ฉํ ์ ์์์ต๋๋ค.
์ ์ฉ ์ด์
- UI์ logic๋ฅผ ๊ตฌ๋ถํ ์ ์์ผ๋ฉด UI์ ๊ธฐ๋ฅ์๋ง ์ด์ ์ ๋ ์ ์์ด ๊ฐ๋ฐ ๋ฐ ์ ์ง ๋ณด์๊ฐ ์ฉ์ดํ๊ธฐ ๋๋ฌธ์ ๋๋ค.
- customhook ํจํด์ ์ฌ์ฉํ๋ฉด ๋ฐ๋ณต๋๋ logic์ ์ฌ์ฌ์ฉ์ฑ์ ๋์ผ ์ ์๊ธฐ๋๋ฌธ์ ๋๋ค.
์ ์ฉ ๋ฐฉ์
- customhook์ผ๋ก ์ปดํฌ๋ํธ์ ํ์ํ ๋ก์ง๋ค์ ๊ตฌํํ๊ณ ๊ธฐ์กด ์ปดํฌ๋ํธ์๋ UI ์ฝ๋๋ง ๋จ๊ธฐ๋๋ก ๋ฆฌํฉํ ๋ง ํ์์ผ๋ฉฐ, ํ์ํ ๋ก์ง์ ์ปค์คํ ํ ์ผ๋ก ๋ถ๋ฌ์ ์ฌ์ฉํ์์ต๋๋ค.
์ ์ฉ์ผ๋ก ์ป์ ์ด์
- ๊ธฐ์กด container, presenter ํจํด์ props๋ก presenter์ ํ์ํ ๊ฐ๋ค์ ๋๊ฒจ์ฃผ์ด์ผ ํ์ต๋๋ค. props๊ฐ ๋ง์ ์ง์๋ก ์ฝ๋๊ฐ ๋ณต์กํด์ง๋ฉฐ, ์ ์ง๋ณด์๊ฐ ์์ข์์ง๋ค๋ ๋จ์ ์ด ์กด์ฌํ์์ต๋๋ค. customhook ํจํด์ ํตํด ์ด๋ฅผ ํด๊ฒฐํ์ฌ ์ฝ๋๊ฐ ๋ ๊ฐ๊ฒฐํด์ง ์ ์์์ต๋๋ค.
- customhook์ผ๋ก ๊ตฌํํ์๊ธฐ ๋๋ฌธ์ ์ฌ์ฌ์ฉ์ฑ์ด ๋์์ก์ต๋๋ค.
- UI์ ๊ธฐ๋ฅ์ ๊ตฌ๋ถํ์๊ธฐ ๋จ๋ฌธ์ ๊ฐ๊ฐ์ ๊ธฐ๋ฅ์ ์ง์คํ ์ ์์ผ๋ฉฐ ์ ์ง๋ณด์์ฑ ๋ํ ํฅ์๋์์ต๋๋ค.
์ฝ๋ ๋น๊ต
์ฝ๋ ๋ณด๊ธฐ
์ด์ ์ฝ๋
import React, { useEffect, useRef, useState } from "react";
import {
LoginBtn,
LoginForm,
import {
InputWrapper,
SocialLoginItem
} from "./login.styels";
import { useValidationInput } from "../../hook/useValidationInput";
import Loading from "../../component/commons/loading/Loading";
import ErrorMsg from "../../component/commons/errorMsg/ErrorMsg";
import UserInput from "../../component/commons/userInput/UserInput";
import { useLoginMutation } from "../../hook/query/auth/useLoginMutation";
import { useSocialLoginMutation } from "../../hook/query/auth/useSocialLoginMutation";
import { useSupportedWebp } from '../../hook/useSupportedWebp';
export default function Login() {
const { isWebpSupported, resolveWebp } = useSupportedWebp();
const [disabled, setDisabled] = useState(true);
const emailRef = useRef<HTMLInputElement>(null);
const [emailValue, emailValid, onChangeEmail, setEmailValue] =
useValidationInput("", "email", false);
const [passwordValue, passwordValid, onChangePassword, setPasswordValue] =
useValidationInput("", "password", false);
const { mutate: loginMutate, isPending: loginIsPending } = useLoginMutation();
const { mutate: socialLoginMutate, isPending: socialLoginIsPending } =
useSocialLoginMutation();
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (emailValid.valid && passwordValid.valid) {
loginMutate({ email: emailValue, password: passwordValue });
setEmailValue("");
setPasswordValue("");
setDisabled(true);
}
};
const socialLoginHandler = (type: "google" | "github") => {
socialLoginMutate(type);
};
useEffect(() => {
emailRef.current && emailRef.current.focus();
}, []);
useEffect(() => {
if (emailValid.valid && passwordValid.valid) {
setDisabled(false);
} else {
setDisabled(true);
}
}, [emailValid, passwordValid]);
return (
<>
<Title className='a11y-hidden'>๋ก๊ทธ์ธ ํ์ด์ง</Title>
<Wrapper>
<LoginForm onSubmit={handleSubmit}>
<LoginFormTitle>
<img src={resolveWebp("/assets/webp/icon-loginLogo.webp", "svg")} />
</LoginFormTitle>
<InputWrapper>
<UserInput
label_hidden={true}
label={"์ด๋ฉ์ผ"}
id={"input-email"}
placeholder={"Email"}
type={"text"}
value={emailValue}
onChange={onChangeEmail}
InputRef={emailRef}
/>
{emailValid.errorMsg && <ErrorMsg message={emailValid.errorMsg} />}
</InputWrapper>
<InputWrapper>
<UserInput
label_hidden={true}
label={"๋น๋ฐ๋ฒํธ"}
id={"input-password"}
placeholder={"Password"}
type={"password"}
onChange={onChangePassword}
value={passwordValue}
/>
{passwordValid.errorMsg && (
<ErrorMsg message={passwordValid.errorMsg} />
)}
</InputWrapper>
<FindAccountLink to={"/findAccount"}>
์ด๋ฉ์ผ{" "}
<span style={{ fontSize: "10px", verticalAlign: "top" }}>|</span>{" "}
๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ
</FindAccountLink>
<LoginBtn type='submit' disabled={disabled}>
๋ก๊ทธ์ธ
</LoginBtn>
<SignupText>
์์ง ํ์์ด ์๋๊ฐ์?
<SignupLink to={"/signup"}>ํ์๊ฐ์
</SignupLink>
</SignupText>
<SocialLoginWrapper>
<SocialLoginItem>
<SocialLoginBtn
className='google'
type='button'
onClick={() => socialLoginHandler("google")}
$isWebpSupported={isWebpSupported}
>
๊ตฌ๊ธ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ
</SocialLoginBtn>
</SocialLoginItem>
<SocialLoginItem>
<SocialLoginBtn
className='github'
type='button'
onClick={() => socialLoginHandler("github")}
$isWebpSupported={isWebpSupported}
>
๊น ํ๋ธ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ
</SocialLoginBtn>
</SocialLoginItem>
</SocialLoginWrapper>
</LoginForm>
<Wrapper />
{(loginIsPending || socialLoginIsPending) && <Loading />}
</>
customhook ํจํด ์ ์ฉ ํ ์ฝ๋
import React from "react";
import Loading from "../../component/commons/loading/Loading";
import ErrorMsg from "../../component/commons/errorMsg/ErrorMsg";
import UserInput from "../../component/commons/userInput/UserInput";
import { useSupportedWebp } from "../../hook/useSupportedWebp";
import {
LoginBtn,
LoginForm,
LoginFormTitle,
Title,
Wrapper,
SignupLink,
FindAccountLink,
SignupText,
SocialLoginWrapper,
SocialLoginBtn,
InputWrapper,
SocialLoginItem
} from "./login.styels";
import { useLogin } from "../../hook/logic/login/useLogin";
export default function Login() {
const { isWebpSupported, resolveWebp } = useSupportedWebp();
const {
loginHandler,
socialLoginHandler,
disabled,
onChangeEmail,
onChangePassword,
loginIsPending,
socialLoginIsPending,
emailValue,
emailRef,
emailValid,
passwordValue,
passwordValid
} = useLogin();
return (
<>
<Title className='a11y-hidden'>๋ก๊ทธ์ธ ํ์ด์ง</Title>
<Wrapper>
<LoginForm onSubmit={loginHandler}>
<LoginFormTitle>
<img src={resolveWebp("/assets/webp/icon-loginLogo.webp", "svg")} />
</LoginFormTitle>
<InputWrapper>
<UserInput
label_hidden={true}
label={"์ด๋ฉ์ผ"}
id={"input-email"}
placeholder={"Email"}
type={"text"}
value={emailValue}
onChange={onChangeEmail}
InputRef={emailRef}
/>
{emailValid.errorMsg && <ErrorMsg message={emailValid.errorMsg} />}
</InputWrapper>
<InputWrapper>
<UserInput
label_hidden={true}
label={"๋น๋ฐ๋ฒํธ"}
id={"input-password"}
placeholder={"Password"}
type={"password"}
onChange={onChangePassword}
value={passwordValue}
/>
{passwordValid.errorMsg && (
<ErrorMsg message={passwordValid.errorMsg} />
)}
</InputWrapper>
<FindAccountLink to={"/findAccount"}>
์ด๋ฉ์ผ{" "}
<span style={{ fontSize: "10px", verticalAlign: "top" }}>|</span>{" "}
๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ
</FindAccountLink>
<LoginBtn type='submit' disabled={disabled}>
๋ก๊ทธ์ธ
</LoginBtn>
<SignupText>
์์ง ํ์์ด ์๋๊ฐ์?
<SignupLink to={"/signup"}>ํ์๊ฐ์
</SignupLink>
</SignupText>
<SocialLoginWrapper>
<SocialLoginItem>
<SocialLoginBtn
className='google'
type='button'
onClick={() => socialLoginHandler("google")}
$isWebpSupported={isWebpSupported}
>
๊ตฌ๊ธ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ
</SocialLoginBtn>
</SocialLoginItem>
<SocialLoginItem>
<SocialLoginBtn
className='github'
type='button'
onClick={() => socialLoginHandler("github")}
$isWebpSupported={isWebpSupported}
>
๊น ํ๋ธ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ
</SocialLoginBtn>
</SocialLoginItem>
</SocialLoginWrapper>
</LoginForm>
</Wrapper>
{(loginIsPending || socialLoginIsPending) && <Loading />}
</>
);
}
ํด๋ฆฐ์ฝ๋๋, ๋ฌด์กฐ๊ฑด ์งง์ ์ฝ๋๊ฐ ์๋ ์ฝ๊ธฐ ์ข์ ์ฝ๋, ํ๋ฆ ํ์ ์ด ์ฝ๊ณ , ์ ์ง ๋ณด์๊ฐ ์ฉ์ดํ ์ฝ๋๋ฅผ ์๋ฏธํฉ๋๋ค.
์๋ ์กฐ๊ฑด์ ๋ง์กฑํ๋ clean code๋ก ๋ฆฌํฉํ ๋งํ์์ต๋๋ค.
- ์์ง๋ : ๊ฐ์ ๋ชฉ์ ์ ์ฝ๋๋ ๋ญ์ณ๋ก๋๋ค.
- ๋จ์ผ์ฑ ์ : ํ๋์ ์ผ์ ํ๋ ๋๋ ทํ ์ด๋ฆ์ ํจ์๋ฅผ ๋ง๋ญ๋๋ค. ํ๋์ ์ปดํฌ๋ํธ์์ ํ๋์ ์ฑ ์์ ๊ฐ์ง๋๋ก ํฉ๋๋ค.
- ์ถ์ํ : ํต์ฌ ๊ฐ๋ ์ ํ์ํ ๋งํผ๋ง ๋ ธ์ถ์ํต๋๋ค.
์ ์ฉ ์ด์
- ๊ธฐ์กด ์ฝ๋๋ ๋๋ฌด ๊ธธ๊ณ ๋ณต์กํ๋ฉฐ, ์ ์ง๋ณด์์ ์ฝ๋์ ํ์ ์ด ์ด๋ ค์ ์ต๋๋ค.
- ์ฝ๋๋ฅผ ๋ณด๋ ์ฌ๋์ด ์ดํดํ๊ธฐ ์ฝ๋๋ก, ์ ์ง๋ณด์ ๋ฐ ๊ฐ๋ ์ฑ ํฅ์์ ์ํด ์ ์ฉํ์์ต๋๋ค.
์ ์ฉ ๋ฐฉ์
- ์์ง๋, ๋จ์ผ์ฑ ์, ์ถ์ํ 3๊ฐ์ง ์์น์ ๋ง์กฑ์ํค๋ clean code๋ก ์ฝ๋๋ฅผ ๋ณ๊ฒฝํ์์ต๋๋ค.
์ฝ๋ ๋น๊ต
์ฝ๋ ๋ณด๊ธฐ
์ด์ ์ฝ๋
import React from "react";
import Loading from "../../component/commons/loading/Loading";
import ErrorMsg from "../../component/commons/errorMsg/ErrorMsg";
import UserInput from "../../component/commons/userInput/UserInput";
import {
LoginBtn,
LoginForm,
LoginFormTitle,
Title,
Wrapper,
SignupLink,
FindAccountLink,
SignupText,
SocialLoginWrapper,
SocialLoginBtn,
InputWrapper,
SocialLoginItem
} from "./login.styels";
import { useLogin } from "../../hook/logic/login/useLogin";
import { useSelector } from 'react-redux';
import { RootState } from '../../store/store';
import { resolveWebp } from '../../library/resolveWebp';
export default function Login() {
const isWebpSupported = useSelector((state: RootState) => state.setting.isWebpSupported);
const {
loginHandler,
socialLoginHandler,
disabled,
onChangeEmail,
onChangePassword,
loginIsPending,
socialLoginIsPending,
emailValue,
emailRef,
emailValid,
passwordValue,
passwordValid
} = useLogin();
return (
<>
<Title className='a11y-hidden'>๋ก๊ทธ์ธ ํ์ด์ง</Title>
<Wrapper>
<LoginForm onSubmit={loginHandler}>
<LoginFormTitle>
<img src={resolveWebp("/assets/webp/icon-loginLogo.webp", "svg")} />
</LoginFormTitle>
<InputWrapper>
<UserInput
label_hidden={true}
label={"์ด๋ฉ์ผ"}
id={"input-email"}
placeholder={"Email"}
type={"text"}
value={emailValue}
onChange={onChangeEmail}
InputRef={emailRef}
/>
{emailValid.errorMsg && <ErrorMsg message={emailValid.errorMsg} />}
</InputWrapper>
<InputWrapper>
<UserInput
label_hidden={true}
label={"๋น๋ฐ๋ฒํธ"}
id={"input-password"}
placeholder={"Password"}
type={"password"}
onChange={onChangePassword}
value={passwordValue}
/>
{passwordValid.errorMsg && (
<ErrorMsg message={passwordValid.errorMsg} />
)}
</InputWrapper>
<FindAccountLink to={"/findAccount"}>
์ด๋ฉ์ผ{" "}
<span style={{ fontSize: "10px", verticalAlign: "top" }}>|</span>{" "}
๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ
</FindAccountLink>
<LoginBtn type='submit' disabled={disabled}>
๋ก๊ทธ์ธ
</LoginBtn>
<SignupText>
์์ง ํ์์ด ์๋๊ฐ์?
<SignupLink to={"/signup"}>ํ์๊ฐ์
</SignupLink>
</SignupText>
<SocialLoginWrapper>
<SocialLoginItem>
<SocialLoginBtn
className='google'
type='button'
onClick={() => socialLoginHandler("google")}
$isWebpSupported={isWebpSupported}
>
๊ตฌ๊ธ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ
</SocialLoginBtn>
</SocialLoginItem>
<SocialLoginItem>
<SocialLoginBtn
className='github'
type='button'
onClick={() => socialLoginHandler("github")}
$isWebpSupported={isWebpSupported}
>
๊น ํ๋ธ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ
</SocialLoginBtn>
</SocialLoginItem>
</SocialLoginWrapper>
</LoginForm>
</Wrapper>
{(loginIsPending || socialLoginIsPending) && <Loading />}
</>
);
}
claean code ๋ณ๊ฒฝ ํ
์ปดํฌ๋ํธ๋ฅผ ๊ธฐ๋ฅ๋ณ๋ก ๋ถ๋ฆฌ => LoginForm ์์ฑ
๊ธฐ์กด useLogin customhook๋ฅผ ๊ธฐ๋ฅ๋ณ๋ก ๋ถ๋ฆฌ
import React from "react";
import styled from "styled-components";
import LoginForm from "./LoginForm";
export const Title = styled.h1``;
const Wrapper = styled.main`
display: flex;
justify-content: center;
align-items: center;
background-color: #f5f5f5;
height: 100vh;
overflow: auto;
`;
export default function Login() {
return (
<>
<Title className='a11y-hidden'>๋ก๊ทธ์ธ ํ์ด์ง</Title>
<Wrapper>
<LoginForm />
</Wrapper>
</>
);
}
LoginForm ์ปดํฌ๋ํธ ๊ธฐ๋ฅ๋ณ ์ธ๋ถํ => LoginFormTitle, InputField, FindAccountLink, LoginBtn, SignupLink, SocialLoginBtns
// LoginForm.tsx
import React from "react";
import { InputField } from "../../component/commons/UI/InputField";
import LoginFormTitle from "./LoginFormTitle";
import FindAccountLink from "./FindAccountLink";
import SignupLink from "./SignupLink";
import { SocialLoginBtns } from "./SocialLoginBtns";
import styled from "styled-components";
import { useLoginDataFetch } from "../../hook/logic/login/useLoginDataFetch";
import { useSocialLoginDataFetch } from "../../hook/logic/login/useSocialLoginDataFetch";
import { useLoginEmailInput } from "../../hook/logic/login/useLoginEmailInput";
import Loading from "../../component/commons/loading/Loading";
import { useLoginPasswordInput } from "../../hook/logic/login/useLoginPasswordInput";
const Form = styled.form`
display: flex;
flex-direction: column;
height: 100vh;
gap: 20px;
max-width: 400px;
width: calc(100% - 60px);
padding: 100px 40px 0 40px;
@media screen and (max-width: 431px) {
width: calc(100% - 40px);
padding: 30px 20px;
}
`;
const InputWrapper = styled.div`
& > p {
margin-top: 10px;
}
`;
export const LoginBtn = styled.button`
width: 100%;
background-color: ${(props) => (props.disabled ? "#cbcbcb" : "gold")};
padding: 14px 0;
border-radius: 4px;
font-size: 18px;
font-weight: 500;
margin-top: 10px;
transition: all 0.5s;
`;
export default function LoginForm() {
const { emailValue, emailValid, onChangeEmail, emailRef } =
useLoginEmailInput();
const { passwordValue, passwordValid, onChangePassword } =
useLoginPasswordInput();
const { loginIsPending, loginHandler } = useLoginDataFetch();
const { socialLoginHandler, socialLoginIsPending } =
useSocialLoginDataFetch();
if (loginIsPending || socialLoginIsPending) {
return <Loading />;
}
return (
<Form onSubmit={loginHandler}>
<LoginFormTitle />
<InputField
label_hidden={true}
label={"์ด๋ฉ์ผ"}
name={"email"}
id={"input-email"}
placeholder={"Email"}
type={"email"}
onChange={onChangeEmail}
value={emailValue}
InputRef={emailRef}
errorMsg={emailValid.errorMsg}
/>
<InputField
label_hidden={true}
label={"๋น๋ฐ๋ฒํธ"}
name={"password"}
id={"input-password"}
placeholder={"Password"}
type={"password"}
onChange={onChangePassword}
value={passwordValue}
errorMsg={passwordValid.errorMsg}
/>
<FindAccountLink />
<LoginBtn
type='submit'
disabled={!(emailValid.valid && passwordValid.valid)}
>
๋ก๊ทธ์ธ
</LoginBtn>
<SignupLink />
<SocialLoginBtns
buttonTypeArr={["google", "github"]}
textArr={["๊ตฌ๊ธ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ", "๊น ํ๋ธ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ"]}
onClickArr={[
() => socialLoginHandler("google"),
() => socialLoginHandler("github")
]}
/>
</Form>
);
}
// LoginFormTitle
import React from "react";
import styled from "styled-components";
import { resolveWebp } from "../../library/resolveWebp";
export const Title = styled.h2`
text-align: center;
font-weight: 500;
`;
export default function LoginFormTitle() {
return (
<Title>
<img src={resolveWebp("/assets/webp/icon-loginLogo.webp", "svg")} />
</Title>
);
}
// SignupLink.tsx
import React from "react";
import styled from "styled-components";
import { Link } from "react-router-dom";
const SignupLinkWrapper = styled.div`
display: inline-block;
font-size: 12px;
color: #111;
text-align: center;
`;
const StyledSignupLink = styled(Link)`
font-size: 12px;
margin-left: 5px;
font-weight: 500;
`;
export default function SignupLink() {
return (
<SignupLinkWrapper>
์์ง ํ์์ด ์๋๊ฐ์?
<StyledSignupLink to={"/signup"}>ํ์๊ฐ์
</StyledSignupLink>
</SignupLinkWrapper>
);
}
// SocialLoginBtns.tsx
import styled from "styled-components";
import { useSupportedWebp } from "../../hook/useSupportedWebp";
import { isMobile } from "react-device-detect";
const SocialLoginWrapper = styled.ul`
position: relative;
@@ -114,3 +54,35 @@ export const SocialLoginBtn = styled.button`
background-color: ${isMobile ? "" : "#ddd"};
}
`;
interface IPrpos {
buttonTypeArr: string[];
textArr: string[];
onClickArr: React.MouseEventHandler<HTMLButtonElement>[];
}
export const SocialLoginBtns = ({
buttonTypeArr,
textArr,
onClickArr
}: IPrpos) => {
const { isWebpSupported } = useSupportedWebp();
return (
<SocialLoginWrapper>
{textArr.map((text: string, i: number) => {
return (
<SocialLoginItem key={text + i}>
<SocialLoginBtn
className={buttonTypeArr[i]}
type='button'
onClick={onClickArr[i]}
$isWebpSupported={isWebpSupported}
>
{text}
</SocialLoginBtn>
</SocialLoginItem>
);
})}
</SocialLoginWrapper>
);
};
์ ์ฉ ์ด์
- ๊ธฐ์กด form์ ๋ํ ๋ก์ง๊ณผ ๊ด๋ จ ์ฝ๋๋ค์ด ๋ณต์กํ๊ธฐ ๋๋ฌธ์ react-hook-form๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ๋ ์ฑ ๋ฐ ์ ์ง๋ณด์์ฑ ํฅ์์ ์ํด ๋์ ํ์์ต๋๋ค.
- formProvider๋ฅผ ํตํด form ํ์ input๋ค์ ๊ฐ๋ค์ ์ฌ์ฉํ ์ ์์ด ์ ์ด ์ปดํฌ๋ํธ์ ์์กด์ฑ์ ๋ถ๋ฆฌ์ํฌ ์ ์๊ธฐ ๋๋ฌธ์ ๋์ ํ์์ต๋๋ค.
์ฌ์ฉ ๋ฐฉ์
- formProvider๋ฅผ ์ ์ฉ์ํจ MyForm customhook๋ฅผ ๋ง๋ค์ด์ ์ฌ์ฉํ์์ต๋๋ค.
- ๊ธฐ์กด InputField ์ปดํฌ๋ํธ์ react-hook-form ์์ฑ์ ์ ์ฉ์์ผฐ์ต๋๋ค.
MyForm ์ฝ๋
import React from "react";
import {
useForm,
FormProvider,
SubmitHandler,
UseFormProps,
FieldValues
} from "react-hook-form";
import { DevTool } from "@hookform/devtools";
import { Form } from "./myForm.styles";
// ์ ๋ค๋ฆญ ํ์
์ ์ฌ์ฉํ ํผ interface ์ ์
interface GenericFormInterface<TFormData extends FieldValues> {
children: React.ReactNode;
onSubmit: SubmitHandler<TFormData>;
formOptions?: UseFormProps<TFormData>;
}
export const MyForm = <TFormData extends FieldValues>({
children,
onSubmit,
formOptions
}: GenericFormInterface<TFormData>) => {
const methods = useForm<TFormData>(formOptions);
return (
// form provider๋ฅผ ํตํด useForm์์ ๊ฐ์ ธ์จ methods๋ฅผ children (ํ์ ์ปดํฌ๋ํธ)์ ์ ๋ฌ
<>
<FormProvider {...methods}>
<Form onSubmit={methods.handleSubmit(onSubmit)} noValidate>
{children}
</Form>
<DevTool control={methods.control} />
</FormProvider>
</>
);
};
์ฝ๋ ๋น๊ต
์ฝ๋ ๋ณด๊ธฐ
์ด์ ์ฝ๋
import React from "react";
import { InputField } from "../../../component/commons/UI/InputField";
import LoginFormTitle from "./LoginFormTitle/LoginFormTitle";
import FindAccountLink from "./FindAccountLink/FindAccountLink";
import SignupLink from "./SignupLink/SignupLink";
import { SocialLoginBtns } from "./socialLoginBtns/SocialLoginBtns";
import { useLoginDataFetch } from "../../../hook/logic/login/useLoginDataFetch";
import { useSocialLoginDataFetch } from "../../../hook/logic/login/useSocialLoginDataFetch";
import { useLoginEmailInput } from "../../../hook/logic/login/useLoginEmailInput";
import Loading from "../../../component/commons/loading/Loading";
import { useLoginPasswordInput } from "../../../hook/logic/login/useLoginPasswordInput";
import { Form, LoginBtn } from '../login.styles';
export default function LoginForm() {
const { emailValue, emailValid, onChangeEmail, emailRef } =
useLoginEmailInput();
const { passwordValue, passwordValid, onChangePassword } =
useLoginPasswordInput();
const { loginIsPending, loginHandler } = useLoginDataFetch();
const { socialLoginHandler, socialLoginIsPending } =
useSocialLoginDataFetch();
if (loginIsPending || socialLoginIsPending) {
return <Loading />;
}
return (
<Form onSubmit={loginHandler}>
<LoginFormTitle />
<InputField
label_hidden={true}
label={"์ด๋ฉ์ผ"}
name={"email"}
id={"input-email"}
placeholder={"Email"}
type={"email"}
onChange={onChangeEmail}
value={emailValue}
InputRef={emailRef}
errorMsg={emailValid.errorMsg}
/>
<InputField
label_hidden={true}
label={"๋น๋ฐ๋ฒํธ"}
name={"password"}
id={"input-password"}
placeholder={"Password"}
type={"password"}
onChange={onChangePassword}
value={passwordValue}
errorMsg={passwordValid.errorMsg}
/>
<FindAccountLink />
<LoginBtn
type='submit'
disabled={!(emailValid.valid && passwordValid.valid)}
>
๋ก๊ทธ์ธ
</LoginBtn>
<SignupLink />
<SocialLoginBtns
buttonTypeArr={["google", "github"]}
textArr={["๊ตฌ๊ธ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ", "๊น ํ๋ธ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ"]}
onClickArr={[
() => socialLoginHandler("google"),
() => socialLoginHandler("github")
]}
/>
</Form>
);
}
react-hook-form ์ ์ฉ ํ ์ฝ๋
formProvider ์ฌ์ฉ์ผ๋ก ๋ฒํผ ์ ์ด๋ฅผ ์ํ input ๊ฐ๋ค์ ์์กด์ฑ ๋ถ๋ฆฌ ๊ฐ๋ฅ
=> input๋ณ๋ก ์ปดํฌ๋ํธ ์ธ๋ถํ (email, password) ๊ฐ๋ฅ
// LoginForm.tsx
import React from "react";
import { useLoginDataFetch } from "../../../hook/logic/login/useLoginDataFetch";
import { MyForm } from "../../../component/commons/UI/myForm/MyForm";
import LoginFormContent from "./LoginFormContent/LoginFormContent";
export default function LoginForm() {
const { loginIsPending, loginHandler, loginError } = useLoginDataFetch();
return (
<MyForm
onSubmit={loginHandler}
formOptions={{
mode: "onChange",
defaultValues: { email: "", password: "" }
}}
>
<LoginFormContent
loginError={loginError}
loginIsPending={loginIsPending}
/>
</MyForm>
);
}
// LoginFormContent.tsx
import React from "react";
import { FormContentWrapper } from "../../login.styles";
import LoginFormTitle from "./LoginFormTitle/LoginFormTitle";
import LoginEmail from "./LoginEmailField/LoginEmail";
import LoginPassword from "./LoginPasswordField/LoginPassword";
import FindAccountLink from "./FindAccountLink/FindAccountLink";
import LoginError from "./LoginError/LoginError";
import LoginBtn from "./LoginBtn/LoginBtn";
import SignupLink from "./SignupLink/SignupLink";
import { SocialLogin } from "./socialLogin/SocialLogin";
interface IProps {
loginError: Error | null;
loginIsPending: boolean;
}
export default function LoginFormContent({
loginError,
loginIsPending
}: IProps) {
return (
<FormContentWrapper>
<LoginFormTitle />
<LoginEmail />
<LoginPassword />
<FindAccountLink />
{<LoginError isError={loginError} />}
<LoginBtn loginIsPending={loginIsPending} />
<SignupLink />
<SocialLogin />
</FormContentWrapper>
);
}
// LoginFormTitle.tsx
import React from "react";
import { resolveWebp } from "../../../../../library/resolveWebp";
import { FormTitle } from "../../../login.styles";
export default function LoginFormTitle() {
return (
<FormTitle>
<img src={resolveWebp("/assets/webp/icon-loginLogo.webp", "svg")} />
</FormTitle>
);
}
// LoginEmail.tsx
import React from "react";
import { InputField } from "../../../../../component/commons/UI/InputField/InputField";
import {
emailRegex,
emailRegexErrorMsg
} from "../../../../../library/validationRegex";
export default function LoginEmail() {
return (
<InputField
label_hidden={true}
label={"์ด๋ฉ์ผ"}
name={"email"}
id={"input-email"}
placeholder={"Email"}
type={"email"}
pattern={{
value: emailRegex,
message: emailRegexErrorMsg
}}
duplicationErrorMsg={"์ค๋ณต๋ ์ด๋ฉ์ผ ์
๋๋ค."}
required={true}
/>
);
}
// LoginPassword.tsx
import { InputField } from "../../../../../component/commons/UI/InputField/InputField";
import {
passwordRegex,
passwordRegexErrorMsg
} from "../../../../../library/validationRegex";
export default function LoginPassword() {
return (
<InputField
label_hidden={true}
label={"๋น๋ฐ๋ฒํธ"}
name={"password"}
id={"input-password"}
placeholder={"Password"}
type={"password"}
pattern={{
value: passwordRegex,
message: passwordRegexErrorMsg
}}
required={true}
/>
);
}
// FindAccountLink.tsx
import React from "react";
import { Line, StyledFindAccountLink } from "../../../login.styles";
export default function FindAccountLink() {
return (
<StyledFindAccountLink to={"/findAccount"}>
์ด๋ฉ์ผ <Line /> ๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ
</StyledFindAccountLink>
);
}
// LoginError.tsx
import React from "react";
import ErrorMsg from "../../../../../component/commons/errorMsg/ErrorMsg";
import { useLoginError } from "../../../../../hook/logic/login/useLoginError";
import { useFormContext } from "react-hook-form";
interface IProps {
isError: Error | null;
}
export default function LoginError({ isError }: IProps) {
const { getValues, reset } = useFormContext();
const email = getValues("email");
const password = getValues("password");
const { error } = useLoginError({ email, password, reset, isError });
return error ? <ErrorMsg message={error} /> : null;
}
// LoginBtn.tsx
import React from "react";
import { StyledLoginBtn } from "../../../login.styles";
import { useFormContext } from "react-hook-form";
interface IProps {
loginIsPending: boolean;
}
export default function LoginBtn({ loginIsPending }: IProps) {
const { formState } = useFormContext();
return (
<StyledLoginBtn
type='submit'
disabled={!formState.isValid || loginIsPending}
>
{loginIsPending ? "๋ก๊ทธ์ธ์ค..." : "๋ก๊ทธ์ธ"}
</StyledLoginBtn>
);
}
// SignupLink.tsx
import React from "react";
import { SignupLinkWrapper, StyledSignupLink } from "../../../login.styles";
export default function SignupLink() {
return (
<SignupLinkWrapper>
์์ง ํ์์ด ์๋๊ฐ์?
<StyledSignupLink to={"/signup"}>ํ์๊ฐ์
</StyledSignupLink>
</SignupLinkWrapper>
);
}
// SocialLoginBtns.tsx
import Loading from "../../../../../component/commons/loading/Loading";
import { useSocialLoginDataFetch } from "../../../../../hook/logic/login/useSocialLoginDataFetch";
import { SocialLoginWrapper } from "../../../login.styles";
import SocialLoginBtn from "./SocialLoginBtn/SocialLoginBtn";
export const SocialLogin = () => {
const { socialLoginHandler, socialLoginIsPending } =
useSocialLoginDataFetch();
if (socialLoginIsPending) {
return <Loading />;
}
return (
<SocialLoginWrapper>
<SocialLoginBtn
loginType='google'
socialLoginHandler={socialLoginHandler}
btnText='๊ตฌ๊ธ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ'
/>
<SocialLoginBtn
loginType='github'
socialLoginHandler={socialLoginHandler}
btnText='๊น ํ๋ธ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ'
/>
</SocialLoginWrapper>
);
};
useFunnel์์ฌ๋ฌ ๋จ๊ณ๋ก ์ด๋ฃจ์ด์ง ์ปดํฌ๋ํธ๋ฅผ ์ํ์ ํ๋ฆ์ ํ๋ฒ์ ๊ด๋ฆฌํ๊ธฐ ์ํด toss์์ ๊ฐ๋ฐํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค.
(๐ ๊ณต์์ฌ์ดํธ: https://slash.page/ko/libraries/react/use-funnel/readme.i18n/)
์ ์ฉ ์ด์
- ์ฌ๋ฌ ๋จ๊ณ๊ฐ ์กด์ฌํ๋ ํ์๊ฐ์ ํ์ด์ง๋ฅผ ํ๋ฆ ํ์ ์ด ์ฉ์ดํ๊ณ , ๊ฐ๋ ์ฑ ์ข์ ํด๋ฆฐ ์ฝ๋๋ก ๋ง๋ค๊ธฐ ์ํด ์ ์ฉํ์์ต๋๋ค.
์ ์ฉ ๋ฐฉ์
- useFunnel customhook๋ฅผ ์์ฑํ์ฌ ํ์๊ฐ์ ์ปดํฌ๋ํธ์ ํ์๊ฐ์ ๋จ๊ณ๋ณ ์ปดํฌ๋ํธ๋ฅผ ๊ด๋ฆฌํ๋ ๋ฐฉ์์ผ๋ก ์ ์ฉํ์์ต๋๋ค.
- react-hook-form์ formProvider๋ฅผ ์ด์ฉํ์ฌ ๊ฐ ๋จ๊ณ๋ณ input ๊ฐ๋ค์ ๊ณต์ ํ ์ ์๋๋ก ํ์์ต๋๋ค.
- ๊ธฐ์กด useFunnel customhook์ Step๋ฅผ ๊ด๋ฆฌํ๋๋ก setpIndex ์ํ์ prevStepHandler, nextStepHandler ํจ์๋ฅผ ์ถ๊ฐํ์์ต๋๋ค.
useFunnel ์ฝ๋
import React, { ReactElement, ReactNode, useState } from "react";
export interface StepProps {
name: string;
children: ReactNode;
}
export interface FunnelProps {
children: Array<ReactElement<StepProps>>;
}
export const useFunnel = (steps: string[]) => {
// state๋ฅผ ํตํด ํ์ฌ ์คํ
์ ๊ด๋ฆฌ
// setStep ํจ์๋ฅผ ํตํด ํ์ฌ ์คํ
์ ๋ณ๊ฒฝ
const [step, setStep] = useState(steps[0]);
// step index๋ฅผ ๊ด๋ฆฌํ๋ค.
const [setpIndex, setStepIndex] = useState(0);
// ์ด์ ์คํ
์ผ๋ก ๋์๊ฐ๋ค.
const prevStepHandler = () => {
setStepIndex((prev) => prev - 1);
setStep(steps[setpIndex - 1]);
};
// ๋ค์ ์คํ
์ผ๋ก ๋์ด๊ฐ๋ค.
const nextStepHandler = () => {
setStepIndex((prev) => prev + 1);
setStep(steps[setpIndex + 1]);
};
// ๊ฐ ๋จ๊ณ๋ฅผ ๋ํ๋ด๋ Step ์ปดํฌ๋ํธ
// children์ ํตํด ๊ฐ ์คํ
์ ์ปจํ
์ธ ๋ฅผ ๋ ๋๋ง
const Step = (props: StepProps): ReactElement => {
return <>{props.children}</>;
};
// ์ฌ๋ฌ ๋จ๊ณ์ Step ์ปดํฌ๋ํธ ์ค ํ์ฌ ํ์ฑํ๋ ์คํ
์ ๋ ๋๋งํ๋ Funnel
// find๋ฅผ ํตํด Step ์ค ํ์ฌ Step์ ์ฐพ์ ๋ ๋๋ง
const Funnel = ({ children }: FunnelProps) => {
const targetStep = children.find(
(childStep) => childStep.props.name === step
);
return <>{targetStep}</>;
};
return {
Funnel,
Step,
setStep,
currentStep: step,
nextStepHandler,
prevStepHandler
} as const;
};
์ ์ฉํ ์ป๋ ์ด์
- ์ด์ ์ฝ๋์ ๋นํด ์ฝ๋์ ๊ฐ๋ ์ฑ์ด ์ข์์ก์ผ๋ฉฐ, ํ์๊ฐ์ ๋จ๊ณ๋ณ ํ๋ฆ ํ์ ์ด ์ฉ์ดํด์ก์ต๋๋ค.
- input๋ค์ ์ํ๋ฅผ ํ๋์ form์์ ๊ด๋ฆฌํ๊ธฐ ๋๋ฌธ์ ์ํ ๊ด๋ฆฌ๊ฐ ํธ๋ฆฌํด์ก์ต๋๋ค.
์ฝ๋ ๋น๊ต
์ฝ๋ ๋ณด๊ธฐ
์ด์ ์ฝ๋
import React from "react";
import UserInfoSetting from "./userInfoSetting/UserInfoSetting";
import ProfileSetting from "./profileSetting/ProfileSetting";
import Loading from "../../component/commons/loading/Loading";
import { useUserInfoSettingEmailInput } from "../../hook/logic/signup/useUserInfoSettingEmailInput";
import { useUserInfoSettingPwInput } from "../../hook/logic/signup/useUserInfoSettingPwInput";
import { useUserInfoSettingPwChkInput } from "../../hook/logic/signup/useUserInfoSettingPwChkInput";
import { useUserInfoSettingPhoneInput } from "../../hook/logic/signup/useUserInfoSettingPhoneInput";
import { useSignupStepController } from "../../hook/logic/signup/useSignupStepController";
import { useProfileSettingDisplayNameInput } from "../../hook/logic/signup/useProfileSettingDisplayNameInput";
import { useProfileSettingImg } from "../../hook/logic/signup/useProfileSettingImg";
import { useSignupDataFetch } from "../../hook/logic/signup/useSignupDataFetch";
import { useProfileSettingIntroduceInput } from "../../hook/logic/signup/useProfileSettingIntroduceInput";
import { useSingupSetScreenSize } from "../../hook/logic/signup/useSignupSetScreenSize";
import ProgressBar from "./progressBar/ProgressBar";
import { FormWrapper, Title, Wrapper } from './signup.styles';
export default function Signup() {
const { emailValue, emailValid, onChangeEmail } =
useUserInfoSettingEmailInput();
const {
passwordValue,
passwordValid,
onChangePassword,
checkPwMatchValidation
} = useUserInfoSettingPwInput();
const {
passwordChkValue,
passwordChkValid,
onChangePasswordChk,
checkPwChkMatchValidation
} = useUserInfoSettingPwChkInput();
const { phoneValue, phoneValid, onChangePhone } =
useUserInfoSettingPhoneInput();
const { displayNameValue, displayNameValid, onChangeDislayName } =
useProfileSettingDisplayNameInput();
const {
imgInputRef,
previewImg,
uploadImg,
isImgLoading,
changeImgHandler,
imgResetHandler
} = useProfileSettingImg();
const { introduceValue, onChangeIntroduce, preventKeydownEnter } =
useProfileSettingIntroduceInput();
const { signupHandler, signupLoading } = useSignupDataFetch({
displayNameValue,
uploadImg,
emailValue,
passwordValue,
phoneValue,
introduceValue
});
const {
next,
percentage,
setPercentage,
nextStepHandler,
prevStepHandler,
cancelHandler,
completedUserInfoSetting,
completedProfileSetting
} = useSignupStepController({
emailValid: emailValid.valid,
passwordValid: passwordValid.valid,
passwordChkValid: passwordChkValid.valid,
phoneValid: phoneValid.valid,
displayNameValid: displayNameValid.valid
});
if (signupLoading) {
return <Loading />;
}
// next(๊ธฐ๋ณธ ์ ๋ณด ์
๋ ฅ ํ ๋ค์ ๋ฒํผ์ ๋๋ฅธ ๊ฒฝ์ฐ)
const SignupForm = !next ? (
<UserInfoSetting
emailValue={emailValue}
onChangeEmail={onChangeEmail}
emailValid={emailValid}
passwordValue={passwordValue}
onChangePassword={onChangePassword}
checkPwMatchValidation={checkPwMatchValidation}
passwordValid={passwordValid}
passwordChkValue={passwordChkValue}
onChangePasswordChk={onChangePasswordChk}
checkPwChkMatchValidation={checkPwChkMatchValidation}
passwordChkValid={passwordChkValid}
phoneValue={phoneValue}
onChangePhone={onChangePhone}
phoneValid={phoneValid}
nextDisabled={
!(
emailValid.valid &&
passwordChkValid.valid &&
passwordValid.valid &&
phoneValid.valid
)
}
nextStepHandler={nextStepHandler}
cancelHandler={cancelHandler}
/>
) : (
<ProfileSetting
setPercentage={setPercentage}
prevStepHandler={prevStepHandler}
signupHandler={signupHandler}
imgInputRef={imgInputRef}
changeImgHandler={changeImgHandler}
previewImg={previewImg}
imgResetHandler={imgResetHandler}
displayNameValue={displayNameValue}
onChangeDislayName={onChangeDislayName}
introduce={introduceValue}
onChangeIntroduce={onChangeIntroduce}
displayNameValid={displayNameValid}
signupDisabled={
!(
emailValid.valid &&
passwordChkValid.valid &&
passwordValid.valid &&
phoneValid.valid &&
displayNameValid.valid
)
}
isImgLoading={isImgLoading}
preventKeydownEnter={preventKeydownEnter}
/>
);
// ๋ชจ๋ฐ์ผ ํ๋ฉด 100vh ๋์ด ์ค์ ์ ํ๋ฉด ์คํฌ๋กค ๋ฌธ์ ํด๊ฒฐ
useSingupSetScreenSize();
return (
<Wrapper>
<Title>ํ์๊ฐ์
</Title>
<ProgressBar
percentage={percentage}
completedUserInfoSetting={completedUserInfoSetting}
completedProfileSetting={completedProfileSetting}
/>
<FormWrapper>{SignupForm}</FormWrapper>
</Wrapper>
);
}
// UserInfoSetting.tsx
import React from "react";
import { InputField } from "../../../component/commons/UI/InputField";
import { CancelBtn, SignupBtn, SignupForm } from '../signup.styles';
interface IProps {
emailValue: string;
onChangeEmail: (e: React.ChangeEvent<HTMLInputElement>) => void;
emailValid: {
errorMsg: string;
valid: boolean;
};
passwordValue: string;
onChangePassword: (e: React.ChangeEvent<HTMLInputElement>) => void;
passwordValid: {
errorMsg: string;
valid: boolean;
};
checkPwMatchValidation: (
e: React.ChangeEvent<HTMLInputElement>,
passwordChkValue: string
) => void;
passwordChkValue: string;
onChangePasswordChk: (e: React.ChangeEvent<HTMLInputElement>) => void;
passwordChkValid: {
errorMsg: string;
valid: boolean;
};
checkPwChkMatchValidation: (
e: React.ChangeEvent<HTMLInputElement>,
passwordValue: string
) => void;
phoneValue: string;
onChangePhone: (e: React.ChangeEvent<HTMLInputElement>) => void;
phoneValid: {
errorMsg: string;
valid: boolean;
};
nextStepHandler: (e: React.FormEvent<HTMLFormElement>) => void;
nextDisabled: boolean;
cancelHandler: () => void;
}
export default function UserInfoSetting({
emailValue,
onChangeEmail,
emailValid,
passwordValue,
onChangePassword,
checkPwMatchValidation,
passwordValid,
passwordChkValue,
onChangePasswordChk,
checkPwChkMatchValidation,
passwordChkValid,
phoneValue,
onChangePhone,
phoneValid,
nextStepHandler,
nextDisabled,
cancelHandler
}: IProps) {
return (
<SignupForm onSubmit={nextStepHandler}>
<InputField
type='text'
label={"์ด๋ฉ์ผ (ํ์)"}
name={"email"}
id={"input-email"}
placeholder={"์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์
๋ ฅํด์ฃผ์ธ์."}
value={emailValue}
onChange={onChangeEmail}
errorMsg={emailValid.errorMsg}
/>
<InputField
type='password'
label={"๋น๋ฐ๋ฒํธ (ํ์)"}
name={"password"}
id={"input-password"}
placeholder={"8-16์ ํน์๋ฌธ์, ์ซ์, ์๋ฌธ ํฌํจ"}
value={passwordValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onChangePassword(e);
checkPwMatchValidation(e, passwordChkValue);
}}
minLength={8}
maxLength={16}
errorMsg={passwordValid.errorMsg}
/>
<InputField
type='password'
label={"๋น๋ฐ๋ฒํธ ํ์ธ (ํ์)"}
name={"password"}
id={"input-passwordChk"}
placeholder={"๋น๋ฐ๋ฒํธ ํ์ธ์ ์
๋ ฅํด์ฃผ์ธ์."}
value={passwordChkValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onChangePasswordChk(e);
checkPwChkMatchValidation(e, passwordValue);
}}
minLength={8}
maxLength={16}
errorMsg={passwordChkValid.errorMsg}
/>
<InputField
type='text'
label={"ํด๋ํฐ (ํ์)"}
name={"phone"}
id={"input-phone"}
placeholder={"ํด๋ํฐ ๋ฒํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์. ( - ์ ์ธ )"}
value={phoneValue
.replace(/[^0-9]/g, "")
.replace(/^(\d{2,3})(\d{3,4})(\d{4})$/, `$1-$2-$3`)}
onChange={onChangePhone}
maxLength={13}
errorMsg={phoneValid.errorMsg}
/>
<SignupBtn type='submit' disabled={nextDisabled}>
๋ค์
</SignupBtn>
<CancelBtn type='button' onClick={cancelHandler}>
์ทจ์
</CancelBtn>
</SignupForm>
);
}
// ProfileSetting.tsx
import React from "react";
import TextAreaField from "../../../component/commons/UI/TextAreaField";
import ProfileSettingImg from "./profileSettingImg/ProfileSettingImg";
import { InputField } from "../../../component/commons/UI/InputField";
import styled from "styled-components";
const SignupForm = styled.form`
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
`;
const SignupBtn = styled.button`
width: 100%;
background-color: ${(props) => (props.disabled ? "#cbcbcb" : "gold")};
cursor: ${(props) => (props.disabled ? "default" : "cursor")};
padding: 14px 0;
border-radius: 4px;
font-size: 16px;
font-weight: 500;
margin-top: 10px;
transition: all 0.5s;
`;
const PrevBtn = styled.button`
width: 100%;
background-color: #eee;
padding: 14px 0;
border-radius: 4px;
font-size: 16px;
font-weight: 500;
color: #111;
`;
interface IProps {
setPercentage: React.Dispatch<React.SetStateAction<string>>;
prevStepHandler: () => void;
signupHandler: (e: React.FormEvent<HTMLFormElement>) => Promise<void>;
imgInputRef: React.RefObject<HTMLInputElement>;
changeImgHandler: (e: React.ChangeEvent<HTMLInputElement>) => Promise<void>;
previewImg: string;
imgResetHandler: () => void;
displayNameValue: string;
onChangeDislayName: (e: React.ChangeEvent<HTMLInputElement>) => void;
introduce: string;
onChangeIntroduce: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
displayNameValid: {
errorMsg: string;
valid: boolean;
};
signupDisabled: boolean;
isImgLoading: boolean;
preventKeydownEnter: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
}
export default function ProfileSetting({
prevStepHandler,
signupHandler,
imgInputRef,
changeImgHandler,
previewImg,
imgResetHandler,
displayNameValue,
onChangeDislayName,
introduce,
onChangeIntroduce,
displayNameValid,
signupDisabled,
isImgLoading,
preventKeydownEnter
}: IProps) {
return (
<SignupForm onSubmit={signupHandler}>
<ProfileSettingImg
imgInputRef={imgInputRef}
changeImgHandler={changeImgHandler}
previewImg={previewImg}
imgResetHandler={imgResetHandler}
isImgLoading={isImgLoading}
/>
<InputField
type='text'
label={"๋๋ค์ (ํ์)"}
name={"nickname"}
id={"input-nickname"}
placeholder={"4-10์ ์๋ฌธ, ์๋ฌธ + ์ซ์"}
value={displayNameValue}
onChange={onChangeDislayName}
minLength={4}
maxLength={10}
errorMsg={displayNameValid.errorMsg}
/>
<TextAreaField
label={"์๊ธฐ์๊ฐ"}
label_hidden={true}
name={"introduce"}
id={"input-nickname"}
placeholder={"์ต๋ 100์๊น์ง ์์ฑ ๊ฐ๋ฅํฉ๋๋ค."}
value={introduce}
onChange={onChangeIntroduce}
onKeyDown={preventKeydownEnter}
maxLength={100}
/>
<SignupBtn type='submit' disabled={signupDisabled}>
ํ์๊ฐ์
</SignupBtn>
<PrevBtn
className='prev'
type='button'
onClick={() => {
prevStepHandler();
}}
>
์ด์
</PrevBtn>
</SignupForm>
);
}
useFuneel ์ ์ฉ ์ฝ๋
// Signup.tsx
import React from "react";
import { Wrapper, Title } from "./signup.styles";
import { MyForm } from "../../component/commons/UI/myForm/MyForm";
import FormContent from "./FormContent/FormContent";
import { useSignupDataFetch } from "../../hook/logic/signup/useSignupDataFetch";
import Loading from "../../component/commons/loading/Loading";
import { useSingupSetScreenSize } from "../../hook/logic/signup/useSignupSetScreenSize";
export default function Signup() {
const { signupHandler, signupLoading } = useSignupDataFetch();
// ๋ชจ๋ฐ์ผ ํ๋ฉด 100vh ๋์ด ์ค์ ์ ํ๋ฉด ์คํฌ๋กค ๋ฌธ์ ํด๊ฒฐ
useSingupSetScreenSize();
if (signupLoading) {
return <Loading />;
}
return (
<Wrapper>
<Title>ํ์๊ฐ์
</Title>
<MyForm
onSubmit={signupHandler}
formOptions={{
mode: "onChange",
defaultValues: {
email: "",
password: "",
passwordChk: "",
phone: "",
nickname: "",
img: process.env.REACT_APP_DEFAULT_PROFILE_IMG,
introduce: ""
}
}}
>
<FormContent />
</MyForm>
</Wrapper>
);
}
// FormContent.tsx
import React from "react";
import { FormContentWrapper } from "../signup.styles";
import { useFunnel } from "../../../hook/useFunnel";
import UserInfoSetting from "./userInfoSetting/UserInfoSetting";
import ProfileSetting from "./profileSetting/ProfileSetting";
import ProgressBar from "../progressBar/ProgressBar";
export default function FormContent() {
const steps = ["userInfoSetting", "profileSetting"];
const { Funnel, Step, currentStep, prevStepHandler, nextStepHandler } = useFunnel(steps);
return (
<FormContentWrapper>
<ProgressBar currentStep={currentStep} steps={steps}
/>
<Funnel>
<Step name='userInfoSetting'>
<UserInfoSetting nextStepHandler={nextStepHandler} />
</Step>
<Step name='profileSetting'>
<ProfileSetting prevStepHandler={prevStepHandler} />
</Step>
</Funnel>
</FormContentWrapper>
);
}
// ProfileSetting.tsx
import React from "react";
import { FieldWrapper, PrevBtn } from "../../signup.styles";
import IntroduceField from "./intorduceField/IntroduceField";
import DisplayNameField from "./displayNameField/DisplayNameField";
import ProfileImgField from "./profileImgField/ProfileImgField";
import SignupBtn from './signupBtn/SignupBtn';
import { useDispatch } from "react-redux";
import { AppDispatch } from "../../../../store/store";
import { signupSlice } from "../../../../slice/signupSlice";
interface IProps {
prevStepHandler: () => void;
}
export default function ProfileSetting({ prevStepHandler }: IProps) {
const dispatch = useDispatch<AppDispatch>();
const minusPercentageHandler = () => {
dispatch(signupSlice.actions.minusPercentage(50));
};
return (
<>
<FieldWrapper>
<ProfileImgField />
<DisplayNameField />
<IntroduceField />
<PrevBtn
onClick={() => {
prevStepHandler();
minusPercentageHandler();
}}
>
์ด์
</PrevBtn>
<SignupBtn />
</FieldWrapper>
</>
);
}
// UserInfoSetting.tsx
import React from "react";
import { FieldWrapper } from "../../signup.styles";
import EmailField from "./emailField/EmailField";
import PasswordField from "./passwordField/PasswordField";
import PasswordChkField from "./passwordChkField/PasswordChkField";
import PhoneField from "./phoneField/PhoneField";
import NextBtn from "./nextBtn/NextBtn";
interface IProps {
nextStepHandler: () => void;
}
export default function UserInfoSetting({ nextStepHandler }: IProps) {
return (
<FieldWrapper>
<EmailField />
<PasswordField />
<PasswordChkField />
<PhoneField />
<NextBtn
nextStepHandler={nextStepHandler}
/>
</FieldWrapper>
);
}
ํ๋ก ํธ์๋ | ๋ฒก์๋ | ๋์์ธ | ๋ฐฐํฌ, ๊ด๋ฆฌ |
---|---|---|---|
- ๋ค์ด๋ฒ ๊ฒ์ API๋ฅผ ํตํด ๋ง์ง ๊ฒ์ ๊ธฐ๋ฅ์ ๊ตฌํํ์์ต๋๋ค.
- ๋ค์ด๋ฒ ๊ฒ์ API๋ก ์ป์ ๋ง์ง ์ ๋ณด์ ์ขํ๋ฅผ KakaMapAPI์ ์ ๋ฌํ์ฌ ์ง๋๋ฅผ ๊ทธ๋ฆฌ๊ณ , ๋ง์ปค๋ก ํด๋น ๋ง์ง์ ํ์ํ๋๋ก ๊ตฌํํ์์ต๋๋ค.
- ํ์ด์ด๋ฒ ์ด์ค๋ฅผ ์ด์ฉํ์ฌ db๋ฅผ ๊ตฌ์ฑํ๊ณ , ๋ก๊ทธ์ธ, ๋ก๊ทธ์์, ๊ฒ์๋ฌผ, ๋๊ธ, ๋ต๊ธ, ํ๋กํ ๋ฑ ์ฃผ์ ๊ธฐ๋ฅ API๋ฅผ ๊ตฌํํ์์ต๋๋ค.
- GitHub Issue
- ๋น ๋ฅธ issue ์์ฑ์ ์ํด issue ํ ํ๋ฆฟ์ ๋ง๋ค์ด ์ฌ์ฉํ์์ต๋๋ค.
- issue label์ ์์ฑํ์ฌ ์ด๋ค ์์ ์ ํ๋์ง ๊ตฌ๋ถํ์์ต๋๋ค.
- issue๋ฅผ ํตํด ๊ตฌํํ ๋ด์ฉ๊ณผ ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ๋ง๋ค์ด ์ด๋ค ์์ ์ ํ ์ง ๋ฆฌ์คํธ ๋ง๋ค์ด ๊ด๋ฆฌํ์์ต๋๋ค.
- GitHub Project
- ํ๋ก์ ํธ ๋ณด๋์ ์ด์ ๋ชฉ๋ก์ ํตํด ๊ฐ๋ฐ ๊ณผ์ ๊ณผ ์งํ ์ํฉ์ ํ ๋์ ์์ ๋ณผ ์ ์์ต๋๋ค.
์ด๋ค ์์ ์ ํ๋์ง ํ์ ํ๊ธฐ ์ํด ์ปจ๋ฒค์ ์ ์ ํ์ฌ commit๊ณผ isuue๋ฅผ ๊ด๋ฆฌํ์์ต๋๋ค.
Fix
: ์์ ์ฌํญ๋ง ์์ ๊ฒฝ์ฐ
Feat
: ์๋ก์ด ๊ธฐ๋ฅ์ด ์ถ๊ฐ ๋๊ฑฐ๋ ์ฌ๋ฌ ๋ณ๊ฒฝ ์ฌํญ๋ค์ด ์์ ๊ฒฝ์ฐ
Style
: ์คํ์ผ๋ง ๋ณ๊ฒฝ๋์์ ๊ฒฝ์ฐ
Docs
: ๋ฌธ์๋ฅผ ์์ ํ ๊ฒฝ์ฐ
Refactor
: ์ฝ๋ ๋ฆฌํฉํ ๋ง์ ํ๋ ๊ฒฝ์ฐ
Remove
: ํ์ผ์ ์ญ์ ํ๋ ์์
๋ง ์ํํ ๊ฒฝ์ฐ
Rename
: ํ์ผ ํน์ ํด๋๋ช
์ ์์ ํ๊ฑฐ๋ ์ฎ๊ธฐ๋ ์์
๋ง์ธ ๊ฒฝ์ฐ
Relese
: ๋ฐฐํฌ ๊ด๋ จ ์์
์ธ ๊ฒฝ์ฐ
Chore
: ๊ทธ ์ธ ๊ธฐํ ์ฌํญ์ด ์์ ๊ฒฝ์ฐ ์ฌ์ฉํฉ๋๋ค.
๐ ๊ตฌํ ๊ธฐ๋ฅ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ( ์ ๋ชฉ ํด๋ฆญ ์ ํด๋น ๊ธฐ๋ฅ ์์ธ์ค๋ช ์ผ๋ก ์ด๋๋ฉ๋๋ค. )
๐ ๋ก๊ทธ์ธ | ๐ ํ์๊ฐ์ | ๐ ์์ด๋/๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ |
---|---|---|
![]() |
![]() |
![]() |
๐ ๊ฒ์๋ฌผ ์กฐํ | ๐ ๊ฒ์๋ฌผ ์ ๋ก๋ | ๐ ๊ฒ์๋ฌผ ์์ |
---|---|---|
![]() |
![]() |
![]() |
๐ ๊ฒ์๋ฌผ ์ญ์ | ๐ ๊ฒ์๋ฌผ ์ ๊ณ | ๐ ๋ง์ง์ถ๊ฐ, ์ข์์ |
---|---|---|
![]() |
![]() |
![]() |
๐ ๋๊ธ, ๋ต๊ธ | ๐ ํ๋กํ ํ์ด์ง | ๐ ํ๋ก์ฐ, ํ๋ก์ |
---|---|---|
![]() |
![]() |
![]() |