Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
96 changes: 96 additions & 0 deletions src/hooks/useAuthForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useState, useMemo } from "react";

type Mode = "signup" | "signin";
type UserType = "employee" | "employer";

type FormData = {
email: string;
password: string;
confirmPassword?: string;
userType?: UserType;
};

type FormErrors = {
email?: string;
password?: string;
confirmPassword?: string;
};

export function useAuthForm(mode: Mode) {
const [formData, setFormData] = useState<FormData>({
email: "",
password: "",
...(mode === "signup" && { confirmPassword: "", userType: "employee" }),
});

const [errors, setErrors] = useState<FormErrors>({});

const isFormValid = useMemo(() => {
if (!formData.email || !formData.password) return false;
if (errors.email || errors.password) return false;

if (mode === "signup") {
if (!formData.confirmPassword || !formData.userType) return false;
if (errors.confirmPassword) return false;
}

return true;
}, [formData, errors, mode]);

const handleChange =
(field: keyof FormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setFormData((prev) => ({ ...prev, [field]: value }));

if (field === "email") {
const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
setErrors((prev) => ({
...prev,
email: isValidEmail ? undefined : "올바른 이메일 형식이 아닙니다.",
}));
}

if (field === "password") {
setErrors((prev) => ({
...prev,
password:
value.length >= 8 ? undefined : "비밀번호는 8자 이상이어야 합니다.",
...(mode === "signup" &&
formData.confirmPassword && {
confirmPassword:
value !== formData.confirmPassword
? "비밀번호가 일치하지 않습니다."
: undefined,
}),
}));
}

if (mode === "signup" && field === "confirmPassword") {
setErrors((prev) => ({
...prev,
confirmPassword:
value !== formData.password
? "비밀번호가 일치하지 않습니다."
: undefined,
}));
}
};

const resetForm = () => {
setFormData({
email: "",
password: "",
...(mode === "signup" && { confirmPassword: "", userType: "employee" }),
});
setErrors({});
};

return {
formData,
errors,
isFormValid,
handleChange,
setFormData,
resetForm,
};
}
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,
})),
}));
32 changes: 18 additions & 14 deletions src/layouts/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
import { Link } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";

import ActiveAlarmIcon from "../assets/icon/active.svg";
import InActiveAlarmIcon from "../assets/icon/inactive.svg";
import SearchIcon from "../assets/icon/search.svg";
import Logo from "../assets/logo/thejulge.svg";

import { useUserStore } from "@/hooks/useUserStore";

interface HeaderProps {
isLoggedIn: boolean;
userNavLabel?: "내 가게" | "내 프로필";
hasAlarm?: boolean;
onLogout?: () => void;
onToggleAlarm?: () => void;
}

export default function Header({
isLoggedIn,
userNavLabel,
hasAlarm,
onLogout,
onToggleAlarm,
}: HeaderProps) {
const userPath = userNavLabel === "내 가게" ? "/shop" : "/profile";
export default function Header({ hasAlarm, onToggleAlarm }: HeaderProps) {
const navigate = useNavigate();
const { user, isLoggedIn, clearUser } = useUserStore();

const userNavLabel = user?.type === "employer" ? "내 가게" : "내 프로필";
const userPath = user?.type === "employer" ? "/shop" : "/profile";

const alarmIcon = hasAlarm ? ActiveAlarmIcon : InActiveAlarmIcon;

const handleLogout = () => {
clearUser();
navigate("/");
};

return (
<header className="w-full px-4 py-2">
<div className="hidden md:flex max-w-screen-xl mx-auto w-full items-center justify-between">
Expand All @@ -47,7 +51,7 @@ export default function Header({
{isLoggedIn ? (
<>
<Link to={userPath}>{userNavLabel}</Link>
<button onClick={onLogout} className="cursor-pointer">
<button onClick={handleLogout} className="cursor-pointer">
로그아웃
</button>
<button className="cursor-pointer" onClick={onToggleAlarm}>
Expand All @@ -72,7 +76,7 @@ export default function Header({
{isLoggedIn ? (
<>
<Link to={userPath}>{userNavLabel}</Link>
<button onClick={onLogout} className="cursor-pointer">
<button onClick={handleLogout} className="cursor-pointer">
로그아웃
</button>
<button className="cursor-pointer" onClick={onToggleAlarm}>
Expand Down
11 changes: 1 addition & 10 deletions src/layouts/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,12 @@ interface MainLayoutProps {
}

export default function MainLayout({
isLoggedIn = false,
userNavLabel,
hasAlarm,
onLogout,
onToggleAlarm,
}: MainLayoutProps) {
return (
<div className="w-full min-h-screen flex flex-col">
<Header
isLoggedIn={isLoggedIn}
userNavLabel={userNavLabel}
hasAlarm={hasAlarm}
onLogout={onLogout}
onToggleAlarm={onToggleAlarm}
/>
<Header hasAlarm={hasAlarm} onToggleAlarm={onToggleAlarm} />

<main className="flex flex-col flex-1">
<Outlet />
Expand Down
Loading