From 99cfb07556c96bc99c0608299cc2250b3d06b5db Mon Sep 17 00:00:00 2001 From: Dew Date: Mon, 2 Feb 2026 21:57:15 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20Role=20=EC=84=A0=ED=83=9D=20UI=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?,=20=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/auth/SignupDialog.tsx | 60 ++++++++++++++++++++++++---- src/components/auth/signup.schema.ts | 4 +- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/components/auth/SignupDialog.tsx b/src/components/auth/SignupDialog.tsx index f382b29..f632a4f 100644 --- a/src/components/auth/SignupDialog.tsx +++ b/src/components/auth/SignupDialog.tsx @@ -14,6 +14,7 @@ import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { signupSchema, type SignupFormValues } from "./signup.schema"; import { useEffect } from "react"; +import { phoneNumber } from "@/utils/phoneNumber"; interface SignupDialogProps { isOpen: boolean; @@ -21,7 +22,8 @@ interface SignupDialogProps { onSwitchToLogin: () => void; } -const defaultValues = { +const defaultValues: SignupFormValues = { + role: "customer", name: "", email: "", phone: "", @@ -88,7 +90,38 @@ export function SignupDialog({ -
+
+ ( +
+ + +
+ )} + /> + {/* 소셜 회원가입 */}
+ +

+ 소셜 계정으로 가입 시 이용약관,{" "} + 개인정보처리방침 에 동의하는 + 것으로 간주합니다. +

@@ -374,10 +363,10 @@ export function SignupDialog({
diff --git a/src/hooks/queries/useAuth.ts b/src/hooks/queries/useAuth.ts new file mode 100644 index 0000000..f9ee9f5 --- /dev/null +++ b/src/hooks/queries/useAuth.ts @@ -0,0 +1,61 @@ +import { postLogin, postSignup, postSocialLogin } from "@/api/auth"; +import type { LoginFormValues } from "@/components/auth/login.schema"; +import type { SignupFormValues } from "@/components/auth/signup.schema"; +import { useAuthActions } from "@/stores/useAuthStore"; +import type { ApiError } from "@/types/api"; +import { useMutation } from "@tanstack/react-query"; + +// 이메일 회원가입 훅 +export const useEmailSignup = () => { + return useMutation({ + mutationFn: (data: SignupFormValues) => { + const requestBody = { + role: data.role!, + name: data.name, + email: data.email, + phone: data.phone, + password: data.password, + }; + return postSignup(requestBody); + }, + onError: (error: ApiError) => { + console.error("회원가입 실패:", error); + }, + }); +}; + +// 이메일 로그인 훅 +export const useEmailLogin = () => { + const { login } = useAuthActions(); + + return useMutation({ + mutationFn: (data: LoginFormValues) => postLogin(data), + onSuccess: (response) => { + login(response.data.accessToken); + }, + onError: (error: ApiError) => { + console.error("이메일 로그인 실패:", error); + }, + }); +}; + +// 소셜 로그인 훅 +export const useSocialLogin = () => { + const { login } = useAuthActions(); + + return useMutation({ + mutationFn: ({ + provider, + token, + }: { + provider: "google" | "kakao"; + token: string; + }) => postSocialLogin(provider, { accessToken: token }), + onSuccess: (response) => { + login(response.data.accessToken); + }, + onError: (error: ApiError) => { + console.error("소셜 로그인 실패:", error); + }, + }); +}; diff --git a/src/stores/useAuthStore.ts b/src/stores/useAuthStore.ts new file mode 100644 index 0000000..399e1ae --- /dev/null +++ b/src/stores/useAuthStore.ts @@ -0,0 +1,50 @@ +import { create } from "zustand"; +import { persist, createJSONStorage } from "zustand/middleware"; +import { immer } from "zustand/middleware/immer"; + +interface AuthState { + accessToken: string | null; + isAuthenticated: boolean; + + actions: { + login: (token: string) => void; + logout: () => void; + }; +} + +export const useAuthStore = create()( + // persist: 내용물이 바뀌면 무조건 저장소에 기록 + persist( + immer((set) => ({ + accessToken: null, + isAuthenticated: false, + + actions: { + login: (token) => + set((state) => { + state.accessToken = token; + state.isAuthenticated = true; + }), + logout: () => + set((state) => { + state.accessToken = null; + state.isAuthenticated = false; + }), + }, + })), + { + name: "auth-storage", + storage: createJSONStorage(() => localStorage), + // persist가 저장할 때 state만 저장하도록 설정 + partialize: (state) => ({ + accessToken: state.accessToken, + isAuthenticated: state.isAuthenticated, + }), + }, + ), +); + +export const useAuthActions = () => useAuthStore((state) => state.actions); +export const useAuthToken = () => useAuthStore((state) => state.accessToken); +export const useIsAuthenticated = () => + useAuthStore((state) => state.isAuthenticated); From b5944e00a02a69e4a0918226b8abb1b806994f2b Mon Sep 17 00:00:00 2001 From: Dew Date: Thu, 5 Feb 2026 05:07:39 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=20UI=20=EB=B6=84=EA=B8=B0=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 3 +- src/components/main/Header.tsx | 160 +++++++++++++++++++-------------- src/layouts/PublicLayout.tsx | 35 +++++--- 3 files changed, 119 insertions(+), 79 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 5149fd4..0141ad9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,8 +23,7 @@ import { useEffect } from "react"; const routes: RouteObject[] = [ { - //TODO: 로그아웃처리 필요 - element: {}} />, + element: , errorElement: , children: [ { path: "/search", element: }, diff --git a/src/components/main/Header.tsx b/src/components/main/Header.tsx index 8e2cdbe..c9eb67d 100644 --- a/src/components/main/Header.tsx +++ b/src/components/main/Header.tsx @@ -5,6 +5,7 @@ import { Button } from "../ui/button"; import { Link, useNavigate } from "react-router-dom"; import { Menu, X } from "lucide-react"; import { cn } from "@/lib/utils"; +import { useAuthActions, useIsAuthenticated } from "@/stores/useAuthStore"; type NavItem = { label: string; @@ -18,6 +19,10 @@ export default function Header() { const [loginOpen, setLoginOpen] = useState(false); const [signupOpen, setSignupOpen] = useState(false); + const isAuthenticated = useIsAuthenticated(); + + const { logout } = useAuthActions(); + const nav = useNavigate(); const navItems: NavItem[] = useMemo( @@ -57,6 +62,16 @@ export default function Header() { }; }, [mobileOpen]); + const handleLogout = () => { + if (!confirm("로그아웃 하시겠습니까?")) return; + + logout(); + setMobileOpen(false); + + alert("로그아웃 되었습니다."); + nav("/", { replace: true }); + }; + const go = (to: string) => { setMobileOpen(false); nav(to); @@ -90,24 +105,13 @@ export default function Header() { >
- setMobileOpen(false)} - aria-label="홈으로 이동" +
- -
- - -
+ {isAuthenticated ? ( +
+ + +
+ ) : ( +
+ + +
+ )}
diff --git a/src/layouts/PublicLayout.tsx b/src/layouts/PublicLayout.tsx index 760df30..9ac6223 100644 --- a/src/layouts/PublicLayout.tsx +++ b/src/layouts/PublicLayout.tsx @@ -1,10 +1,19 @@ -import { Link, Outlet } from "react-router-dom"; +import { useAuthActions, useIsAuthenticated } from "@/stores/useAuthStore"; +import { Link, Outlet, useNavigate } from "react-router-dom"; -type PublicLayoutProps = { - onLogOut?: () => void; -}; +export default function PublicLayout() { + const nav = useNavigate(); + const { logout } = useAuthActions(); + const isAuthenticated = useIsAuthenticated(); + + const handleLogout = () => { + if (!confirm("로그아웃 하시겠습니까?")) return; + + logout(); + alert("로그아웃 되었습니다."); + nav("/", { replace: true }); + }; -export default function PublicLayout({ onLogOut }: PublicLayoutProps) { return (
@@ -20,13 +29,15 @@ export default function PublicLayout({ onLogOut }: PublicLayoutProps) {

원하는 자리를 선택하는 스마트 식당 예약

- + {isAuthenticated && ( + + )}