Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/hooks/useUserStore.tsx
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,
})),
}));
277 changes: 276 additions & 1 deletion src/pages/SignupPage.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

유효성 검사랑 상태 업데이트 로직이 너무 뭉쳐 있어서 따로 훅으로 추출하는 것도 좋을 것 같습니다. 이것은 그냥 저의 의견 ㅎㅎ입니당

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 주신 부분들 반영하여 코드 수정하도록 하겠습니다😊

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("@")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

includes("@")만으로는 잘못된 형태가 많이 통과될 것 같습니다. (도메인이 없는 경우, @가 두 번 사용된 경우 등) 간단한 정규식을 사용해서 이메일 포맷을 좀 더 안정적으로 검증하는 게 어떨까요?

일례로 아래와 같이 작성할 수 있다고 합니당

const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e.target.value);

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분도 더 안전하게 검증하는 것이 좋을 것 같다고 생각합니다👍

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@만 확인하는 것은 이메일 형식의 최소 조건을 충족하지만, 실제 이메일 주소를 충족하지 않을 수 있을 것 같습니다.!
예를 들어, "abx@"나 "@naver.com" 같은 경우는 유효하지 않지만 @가 포함되어 있어 에러를 발생시키지 않을 것 같습니다.
정규식을 사용해서 이메일 형식을 검증해도 좋을 것 같습니다 ~!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 해당 부분 수정하도록 하겠습니다!👍

? 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",
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

finally에서 setFormData()를 호출해 초기화를 하고 있는데 이 경우라면 서버 요청이 실패해도 폼이 초기화되어 사용자 입장에서는 에러가 났는데 입력값이 사라져서 재입력을 해야 하는 불편한 UX가 될 것 같습니다.

성공했을 경우에만 초기화를 시켜주는 건 어떨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 성공했을 때만 초기화를 시켜주는 것이 좋을 것 같습니다👍

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>
);
}