Skip to content

Commit

Permalink
회원가입-멀티스탭폼-제작 (#71)
Browse files Browse the repository at this point in the history
* Refactor : 회원가입 실패 이유 표출

* Refactor : 앱바 기본동작 추가

* Minor : Disable 버튼 스타일 추가

* New : FixedBottomCTA 버튼 추가

* Refactor : 회원가입 Multi-step-form 적용
  • Loading branch information
jobkaeHenry authored Dec 4, 2023
1 parent ffa54e6 commit e42380a
Show file tree
Hide file tree
Showing 9 changed files with 389 additions and 128 deletions.
38 changes: 38 additions & 0 deletions client/src/app/(logoutOnly)/auth/signup/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";

import SignupPageContext from "@/store/auth/SignupPageContext";
import { SignupRequirement } from "@/types/auth/signupRequirement";
import { Container, Paper } from "@mui/material";
import { ReactNode, useState } from "react";

type layoutProps = {
children: ReactNode;
};

const layout = ({ children }: layoutProps) => {
const [disableBtn, setDisableBtn] = useState(false);
const [formData, setFormData] = useState<SignupRequirement>({
id: "",
email: "",
password: "",
nickname: "",
});
return (
<SignupPageContext.Provider value={{ formData, setFormData, disableBtn, setDisableBtn }}>
<Container sx={{ px: { xs: 0, sm: 4 } }} maxWidth={"lg"}>
<Paper component={'form'}
sx={{
display: "flex",
flexDirection: "column",
minHeight: "calc(100vh - 56px)",
overflowX:'hidden'
}}
>
{children}
</Paper>
</Container>
</SignupPageContext.Provider>
);
};

export default layout;
289 changes: 163 additions & 126 deletions client/src/app/(logoutOnly)/auth/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,137 +1,174 @@
"use client";
import Avatar from "@mui/material/Avatar";
import Button from "@mui/material/Button";
import CssBaseline from "@mui/material/CssBaseline";
import TextField from "@mui/material/TextField";
import FormControlLabel from "@mui/material/FormControlLabel";
import Checkbox from "@mui/material/Checkbox";

import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import Typography from "@mui/material/Typography";
import Container from "@mui/material/Container";
import { SIGNIN } from "@/const/clientPath";
import Link from "next/link";
import { ChangeEvent, useState } from "react";
import { SignupRequirement } from "@/types/auth/signupRequirement";
import { ChangeEvent, useContext, useCallback, useState } from "react";
import useSignupMutation from "@/queries/auth/useSignupMutation";
import SignupPageContext from "@/store/auth/SignupPageContext";
import useMultistepForm from "@/hooks/useMultistepForm";
import SignupStep from "@/components/auth/signup/SignupStep";
import { TextField, LinearProgress } from "@mui/material";
import CustomAppbar from "@/components/CustomAppbar";
import { useRouter } from "next/navigation";
import HOME from "@/const/clientPath";
import { HomeOutlined } from "@mui/icons-material";
import { SignupRequirement } from "@/types/auth/signupRequirement";
import FixedBottomCTA from "@/components/FixedBottomCTA";

export default function SignUpPage() {
const [formData, setFormData] = useState<SignupRequirement>({
id: "",
email: "",
password: "",
nickname: "",
});
const changeHandler = ({
target,
}: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData((prev) => ({ ...prev, [target.name]: target.value }));
};
const { mutate: submitHandler } = useSignupMutation();
const { formData, setFormData, disableBtn } = useContext(SignupPageContext);
const router = useRouter();

const changeHandler = useCallback(
({ target }: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData((prev) => ({ ...prev, [target.name]: target.value }));
},
[]
);
const [doubleCheckPassword, setDoubleCheckPassword] = useState<string>("");

const { mutateAsync: signupHandler } = useSignupMutation();
const submitHandler = useCallback(async (data: SignupRequirement) => {
try {
await signupHandler(data);
} catch (err) {
goTo(0);
}
}, []);

const {
next,
MultistepForm,
isFirstStep,
isLastStep,
goTo,
totalPageNum,
currentIndex,
} = useMultistepForm(
[
<SignupStep
title={`원활한 환경을 위해\n이메일을 입력해 주세요😃`}
error={
!new RegExp(/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/).test(
formData.email
)
}
>
<TextField
name="email"
label="이메일"
type="email"
value={formData.email}
helperText="이메일을 입력해 주세요."
onChange={changeHandler}
/>
</SignupStep>,

<SignupStep
title={`🔐\n비밀번호를 입력해 주세요`}
error={
formData.password
? !new RegExp(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,20}$/
).test(formData.password)
: undefined
}
>
<TextField
name="password"
autoComplete="new-password"
label="비밀번호"
type="password"
value={formData.password}
placeholder="비밀번호를 입력해주세요"
helperText="8~20자 대소문자, 숫자, 특수기호가 들어가야해요"
onChange={changeHandler}
/>
</SignupStep>,

<SignupStep
title={`🔒🔒\n한번 더 입력해 주세요`}
error={
doubleCheckPassword
? !new RegExp(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,20}$/
).test(doubleCheckPassword) ||
doubleCheckPassword !== formData.password
: undefined
}
>
<TextField
name="passwordcheck"
autoComplete="new-password"
label="비밀번호"
type="password"
value={doubleCheckPassword}
placeholder="비밀번호를 입력해주세요"
helperText="8~20자 대소문자, 숫자, 특수기호가 들어가야해요"
onChange={({ target }) => setDoubleCheckPassword(target.value)}
/>
</SignupStep>,

<SignupStep
title={`거의 다 끝나가요!\n아이디와 닉네임을 설정해주세요🤓`}
error={
formData.id ? !!formData.id && !(formData.id.length > 2) : undefined
}
>
<TextField
name="id"
autoComplete="username"
label="아이디"
type="id"
placeholder="아이디를 입력해주세요"
helperText="이메일 노출없이 아이디로 대체해요"
value={formData.id}
onChange={changeHandler}
/>
</SignupStep>,

<SignupStep
title={`거의 다 끝나가요!\n아이디와 닉네임을 설정해주세요🤓`}
error={
formData.nickname
? !!formData.nickname && !(formData.nickname.length > 2)
: undefined
}
>
<TextField
name="nickname"
label="닉네임"
placeholder="닉네임을 입력해주세요"
value={formData.nickname}
autoComplete="off"
helperText="사용할 닉네임을 입력해 주세요."
onChange={changeHandler}
/>
</SignupStep>,
],
0
);

return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<Box
sx={{
marginTop: 8,
display: "flex",
flexDirection: "column",
alignItems: "center",
<>
<CustomAppbar
appendButton={"취소"}
prependButton={isFirstStep ? <HomeOutlined sx={{ p: 0 }} /> : undefined}
onClickPrepend={() => (isFirstStep ? router.push(HOME) : router.back())}
onClickAppend={() => router.push(HOME)}
/>
<LinearProgress
variant="determinate"
value={(currentIndex / (totalPageNum - 1)) * 100}
/>
{MultistepForm}
<FixedBottomCTA
onClick={() => {
!isLastStep ? next() : submitHandler(formData);
}}
size="large"
disabled={disableBtn}
>
<Avatar sx={{ m: 1, bgcolor: "secondary.main" }}>
<LockOutlinedIcon />
</Avatar>
<Typography variant="h1">회원가입</Typography>
<Box
component="form"
onSubmit={(e) => {
e.preventDefault();
submitHandler(formData);
}}
noValidate
sx={{ mt: 3 }}
>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
required
autoFocus
fullWidth
id="email"
label="이메일"
name="email"
autoComplete="email"
onChange={(e) => changeHandler(e)}
/>
</Grid>
<Grid item xs={12}>
<TextField
required
fullWidth
name="password"
label="비밀번호"
type="password"
id="password"
onChange={(e) => changeHandler(e)}
autoComplete="new-password"
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
name="id"
required
fullWidth
id="id"
onChange={(e) => changeHandler(e)}
label="유저 아이디"
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
required
fullWidth
id="nickname"
label="닉네임"
onChange={(e) => changeHandler(e)}
name="nickname"
/>
</Grid>
<Grid item xs={12}>
<FormControlLabel
control={<Checkbox value="allowExtraEmails" color="primary" />}
label="유용한 정보를 받아볼게요."
/>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2 }}
>
회원가입
</Button>
<Grid container justifyContent="flex-end">
<Grid item>
<Link href={SIGNIN}>
<Typography variant="label">
Already have an account?{" "}
<Typography
variant="label"
sx={{ color: "primary", fontWeight: "bold" }}
>
로그인 하러가기
</Typography>
</Typography>
</Link>
</Grid>
</Grid>
</Box>
</Box>
</Container>
{!isLastStep ? "다음" : "투파이아 시작하기"}
</FixedBottomCTA>
</>
);
}
8 changes: 6 additions & 2 deletions client/src/components/CustomAppbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ const CustomAppbar = ({
{prependButton}
</AppbarButton>
) : (
<IconButton onClick={() => router.back()}>
<IconButton
onClick={(e) =>
onClickPrepend ? onClickPrepend(e) : router.back()
}
>
<GoBackIcon />
</IconButton>
)}
Expand All @@ -67,7 +71,7 @@ const CustomAppbar = ({
const AppbarButton = styled(Button)(() => ({
minWidth: 40,
fontWeight: "medium",
fontSize:'18px'
fontSize: "18px",
}));

export default memo(CustomAppbar);
33 changes: 33 additions & 0 deletions client/src/components/FixedBottomCTA.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Button, ButtonProps, Paper } from "@mui/material";

const FixedBottomCTA = ({ ...props }: ButtonProps) => {
return (
<>
<Button
sx={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: 56,
borderRadius: 0,
fontSize: "1rem",
zIndex: 2,
}}
{...props}
/>
<Paper
sx={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: 56,
zIndex: 1,
}}
></Paper>
</>
);
};

export default FixedBottomCTA;
Loading

0 comments on commit e42380a

Please sign in to comment.