-
Notifications
You must be signed in to change notification settings - Fork 4
[feat] 회원가입 페이지 구현 (/signup) #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
4545378
03d5ebc
e9095d4
fc0e687
a3772bf
7155ef8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import { create } from "zustand"; | ||
|
|
||
| type User = { | ||
| id: string; | ||
| email: string; | ||
| type: "employer" | "employee"; | ||
| name?: string; | ||
| phone?: string; | ||
| address?: string; | ||
| bio?: string; | ||
| shopId?: string; | ||
| }; | ||
|
|
||
| interface UserState { | ||
| user: User | null; | ||
| token: string | null; | ||
| isLoggedIn: boolean; | ||
| setUserAndToken: (user: User, token: string) => void; | ||
| updateShopId: (shopId: string) => void; | ||
| clearUser: () => void; | ||
| } | ||
|
|
||
| export const useUserStore = create<UserState>((set, get) => ({ | ||
| user: null, | ||
| token: null, | ||
| isLoggedIn: false, | ||
|
|
||
| setUserAndToken: (user, token) => | ||
| set(() => ({ | ||
| user, | ||
| token, | ||
| isLoggedIn: true, | ||
| })), | ||
|
|
||
| updateShopId: (shopId) => { | ||
| const current = get(); | ||
| if (!current.user || !current.token) return; | ||
|
|
||
| const updatedUser = { | ||
| ...current.user, | ||
| shopId, | ||
| }; | ||
|
|
||
| set({ | ||
| user: updatedUser, | ||
| }); | ||
| }, | ||
|
|
||
| clearUser: () => | ||
| set(() => ({ | ||
| user: null, | ||
| token: null, | ||
| isLoggedIn: false, | ||
| })), | ||
| })); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,278 @@ | ||
| import { useState, useMemo } from "react"; | ||
|
|
||
| import { AxiosError } from "axios"; | ||
| import clsx from "clsx"; | ||
| import { useNavigate, Link } from "react-router-dom"; | ||
|
|
||
| import IconCheck from "../assets/icon/check.svg?react"; | ||
| import Logo from "../assets/logo/thejulge.svg?react"; | ||
|
|
||
| import { postAuthentication } from "@/apis/services/authenticationService"; | ||
| import { postUser } from "@/apis/services/userService"; | ||
| import Button from "@/components/Button"; | ||
| import TextField from "@/components/TextField"; | ||
| import { ROUTES } from "@/constants/router"; | ||
| import { useUserStore } from "@/hooks/useUserStore"; | ||
|
|
||
| type UserType = "employee" | "employer"; | ||
|
|
||
| type FormData = { | ||
| email: string; | ||
| password: string; | ||
| confirmPassword: string; | ||
| userType: UserType; | ||
| }; | ||
|
|
||
| type FormErrors = { | ||
| email?: string; | ||
| password?: string; | ||
| confirmPassword?: string; | ||
| }; | ||
|
|
||
| export default function SignupPage() { | ||
| return <div>SignupPage</div>; | ||
| const navigate = useNavigate(); | ||
| const { setUserAndToken } = useUserStore(); | ||
|
|
||
| const [formData, setFormData] = useState<FormData>({ | ||
| email: "", | ||
| password: "", | ||
| confirmPassword: "", | ||
| userType: "employee", | ||
| }); | ||
|
|
||
| const [errors, setErrors] = useState<FormErrors>({}); | ||
| //Alert 사용시 | ||
| // const [alertMessage, setAlertMessage] = useState(""); | ||
| // const [nextRoute, setNextRoute] = useState<string | null>(null); | ||
|
|
||
| const isFormValid = useMemo(() => { | ||
| return ( | ||
| formData.email && | ||
| formData.password && | ||
| formData.confirmPassword && | ||
| !errors.email && | ||
| !errors.password && | ||
| !errors.confirmPassword | ||
| ); | ||
| }, [formData, errors]); | ||
|
|
||
| const handleChange = | ||
| (field: keyof FormData) => (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| setFormData((prev) => ({ ...prev, [field]: e.target.value })); | ||
|
|
||
| if (field === "email") { | ||
| setErrors((prev) => ({ | ||
| ...prev, | ||
| email: e.target.value.includes("@") | ||
|
||
| ? undefined | ||
| : "올바른 이메일 형식이 아닙니다.", | ||
| })); | ||
| } | ||
| if (field === "password") { | ||
| setErrors((prev) => ({ | ||
| ...prev, | ||
| password: | ||
| e.target.value.length >= 8 | ||
| ? undefined | ||
| : "비밀번호는 8자 이상이어야 합니다.", | ||
| confirmPassword: | ||
| formData.confirmPassword && | ||
| e.target.value !== formData.confirmPassword | ||
| ? "비밀번호가 일치하지 않습니다." | ||
| : undefined, | ||
| })); | ||
| } | ||
| if (field === "confirmPassword") { | ||
| setErrors((prev) => ({ | ||
| ...prev, | ||
| confirmPassword: | ||
| formData.password && e.target.value !== formData.password | ||
| ? "비밀번호가 일치하지 않습니다." | ||
| : undefined, | ||
| })); | ||
| } | ||
| }; | ||
|
|
||
| const handleSubmit = async () => { | ||
| try { | ||
| const response = await postUser({ | ||
| email: formData.email, | ||
| password: formData.password, | ||
| type: formData.userType, | ||
| }); | ||
|
|
||
| if (response.status === 201) { | ||
| const loginRes = await postAuthentication({ | ||
| email: formData.email, | ||
| password: formData.password, | ||
| }); | ||
|
|
||
| const token = loginRes.data.item.token; | ||
| const user = loginRes.data.item.user.item; | ||
|
|
||
| setUserAndToken(user, token); | ||
|
|
||
| navigate( | ||
| user.type === "employer" ? ROUTES.SHOP.ROOT : ROUTES.PROFILE.ROOT, | ||
| ); | ||
| //Alert 사용 | ||
| // setAlertMessage("가입이 완료되었습니다!"); | ||
| // setNextRoute( | ||
| // user.type === "employer" ? ROUTES.SHOP.ROOT : ROUTES.PROFILE.ROOT, | ||
| // ); | ||
| } | ||
| } catch (error: unknown) { | ||
| const axiosError = error as AxiosError<{ message: string }>; | ||
| const message = | ||
| axiosError.response?.data?.message ?? "알 수 없는 에러 발생"; | ||
|
|
||
| console.log(message); | ||
| //Alert 사용시 | ||
| // setAlertMessage(message ?? "회원가입 실패! 다시 시도해주세요."); | ||
| // setNextRoute(null); | ||
| } finally { | ||
| setFormData({ | ||
| email: "", | ||
| password: "", | ||
| confirmPassword: "", | ||
| userType: "employee", | ||
| }); | ||
|
||
| setErrors({}); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="w-full"> | ||
| <Link to={ROUTES.NOTICE.ROOT}> | ||
| <Logo className="mx-auto mb-2 h-[45px] w-[248px]" /> | ||
| </Link> | ||
|
|
||
| <form className="mt-[40px] mx-auto flex max-w-sm flex-col gap-[28px]"> | ||
| <TextField.Input | ||
| id="email" | ||
| label="이메일" | ||
| type="email" | ||
| placeholder="입력" | ||
| value={formData.email} | ||
| onChange={handleChange("email")} | ||
| fullWidth | ||
| validateMessage={errors.email} | ||
| /> | ||
|
|
||
| <TextField.Input | ||
| id="password" | ||
| label="비밀번호" | ||
| type="password" | ||
| placeholder="입력" | ||
| value={formData.password} | ||
| onChange={handleChange("password")} | ||
| fullWidth | ||
| validateMessage={errors.password} | ||
| /> | ||
|
|
||
| <TextField.Input | ||
| id="confirm-password" | ||
| label="비밀번호 확인" | ||
| type="password" | ||
| placeholder="입력" | ||
| value={formData.confirmPassword} | ||
| onChange={handleChange("confirmPassword")} | ||
| fullWidth | ||
| validateMessage={errors.confirmPassword} | ||
| /> | ||
|
|
||
| <fieldset className="space-y-2"> | ||
| <legend className="text-base font-normal text-black"> | ||
| 회원 유형 | ||
| </legend> | ||
| <div className="flex gap-4"> | ||
| <Button | ||
| type="button" | ||
| variant="white" | ||
| fullWidth | ||
| onClick={() => | ||
| setFormData((prev) => ({ ...prev, userType: "employee" })) | ||
| } | ||
| className={clsx( | ||
| "flex items-center justify-center gap-[9px] rounded-[30px] border px-[41px] py-[13px]", | ||
| formData.userType === "employee" | ||
| ? "border-primary" | ||
| : "border-gray-30", | ||
| )} | ||
| > | ||
| <div | ||
| className={clsx( | ||
| "h-5 w-5 rounded-full", | ||
| formData.userType !== "employee" && "border border-gray-30", | ||
| )} | ||
| > | ||
| {formData.userType === "employee" && ( | ||
| <IconCheck className="h-5 w-5" /> | ||
| )} | ||
| </div> | ||
| <span className="text-sm font-normal text-black">알바님</span> | ||
| </Button> | ||
|
|
||
| <Button | ||
| type="button" | ||
| variant="white" | ||
| fullWidth | ||
| onClick={() => | ||
| setFormData((prev) => ({ ...prev, userType: "employer" })) | ||
| } | ||
| className={clsx( | ||
| "flex items-center justify-center gap-[9px] rounded-[30px] border px-[41px] py-[13px]", | ||
| formData.userType === "employer" | ||
| ? "border-primary" | ||
| : "border-gray-30", | ||
| )} | ||
| > | ||
| <div | ||
| className={clsx( | ||
| "h-5 w-5 rounded-full", | ||
| formData.userType !== "employer" && "border border-gray-30", | ||
| )} | ||
| > | ||
| {formData.userType === "employer" && ( | ||
| <IconCheck className="h-5 w-5" /> | ||
| )} | ||
| </div> | ||
| <span className="text-sm font-normal text-black">사장님</span> | ||
| </Button> | ||
| </div> | ||
| </fieldset> | ||
|
|
||
| <Button | ||
| type="button" | ||
| fullWidth | ||
| className="py-[14px]" | ||
| onClick={handleSubmit} | ||
| disabled={!isFormValid} | ||
| > | ||
| 가입하기 | ||
| </Button> | ||
| </form> | ||
|
|
||
| <p className="mt-[16px] text-center text-sm"> | ||
| 이미 가입하셨나요?{" "} | ||
| <Link to={ROUTES.AUTH.SIGNIN} className="text-[#5534DA] underline"> | ||
| 로그인하기 | ||
| </Link> | ||
| </p> | ||
|
|
||
| {/* 임시 Alert */} | ||
| {/* {alertMessage && ( | ||
| <AlertModal | ||
| message={alertMessage} | ||
| onClose={() => { | ||
| setAlertMessage(""); | ||
| if (nextRoute) { | ||
| navigate(nextRoute); | ||
| setNextRoute(null); | ||
| } | ||
| }} | ||
| /> | ||
| )} */} | ||
| </div> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
유효성 검사랑 상태 업데이트 로직이 너무 뭉쳐 있어서 따로 훅으로 추출하는 것도 좋을 것 같습니다. 이것은 그냥 저의 의견 ㅎㅎ입니당
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
리뷰 주신 부분들 반영하여 코드 수정하도록 하겠습니다😊