diff --git a/package-lock.json b/package-lock.json
index e453b60..ba326b6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,8 @@
"lucide-react": "^0.525.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
- "react-router-dom": "^7.7.0"
+ "react-router-dom": "^7.7.0",
+ "zustand": "^5.0.8"
},
"devDependencies": {
"@biomejs/biome": "^2.1.2",
@@ -3669,6 +3670,35 @@
"optional": true
}
}
+ },
+ "node_modules/zustand": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
+ "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index 25079a4..de51955 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,8 @@
"lucide-react": "^0.525.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
- "react-router-dom": "^7.7.0"
+ "react-router-dom": "^7.7.0",
+ "zustand": "^5.0.8"
},
"devDependencies": {
"@biomejs/biome": "^2.1.2",
diff --git a/src/App.tsx b/src/App.tsx
index 0b8e786..1d04db1 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -3,7 +3,6 @@ import { Route, Routes } from "react-router-dom";
import styles from "./App.module.css";
import { ChatWidget } from "./components/organisms";
import { MainTemplate } from "./components/templates";
-import { AuthProvider } from "./contexts/AuthContext";
import { CategoryProvider } from "./contexts/CategoryContext";
import { ModalProvider } from "./contexts/ModalContext";
import { ToastProvider } from "./contexts/ToastContext";
@@ -55,39 +54,37 @@ export default function App() {
return (
-
-
-
-
-
-
- } />
- } />
- } />
- } />
- } />
- } />
- }
- />
- } />
- } />
- } />
-
-
- setIsChatOpen(!isChatOpen)}
- onChange={setNewMessage}
- onSend={handleSendMessage}
+
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ }
/>
-
-
-
-
+ } />
+ } />
+ } />
+
+
+ setIsChatOpen(!isChatOpen)}
+ onChange={setNewMessage}
+ onSend={handleSendMessage}
+ />
+
+
+
);
diff --git a/src/api/authApi.ts b/src/api/authApi.ts
index 276cc83..c259fe0 100644
--- a/src/api/authApi.ts
+++ b/src/api/authApi.ts
@@ -88,3 +88,9 @@ export async function resetPassword(
const res = await api.post("/auth/password/reset", body);
return { message: res.data.message };
}
+
+// 로그아웃 API 함수
+export async function logout(): Promise {
+ const res = await api.post("/auth/logout");
+ return { message: res.data.message };
+}
diff --git a/src/components/molecules/UserNav/UserNav.module.css b/src/components/molecules/UserNav/UserNav.module.css
new file mode 100644
index 0000000..5b4d7ed
--- /dev/null
+++ b/src/components/molecules/UserNav/UserNav.module.css
@@ -0,0 +1,30 @@
+.container {
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+}
+
+.navLink {
+ text-decoration: none;
+ color: var(--color-text);
+ font-size: 0.9rem;
+ font-weight: 500;
+}
+
+.navLink:hover {
+ text-decoration: underline;
+}
+
+.logoutButton {
+ border: none;
+ background-color: transparent;
+ color: var(--color-text);
+ font-size: 0.9rem;
+ font-weight: 500;
+ cursor: pointer;
+ padding: 0;
+}
+
+.logoutButton:hover {
+ text-decoration: underline;
+}
diff --git a/src/components/molecules/UserNav/UserNav.tsx b/src/components/molecules/UserNav/UserNav.tsx
new file mode 100644
index 0000000..c53df62
--- /dev/null
+++ b/src/components/molecules/UserNav/UserNav.tsx
@@ -0,0 +1,21 @@
+import { useAuthStore } from "../../../store/authStore";
+import { Button } from "../../atoms";
+import styles from "./UserNav.module.css";
+
+export default function UserNav() {
+ const { logout } = useAuthStore();
+
+ return (
+
+ );
+}
diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts
index 63abc1b..6af68dd 100644
--- a/src/components/molecules/index.ts
+++ b/src/components/molecules/index.ts
@@ -23,3 +23,4 @@ export { default as SearchBar } from "./SearchBar/SearchBar";
export { default as SidebarMenu } from "./SidebarMenu/SidebarMenu";
export { default as Table } from "./Table/Table";
export { default as UserMenu } from "./UserMenu/UserMenu";
+export { default as UserNav } from "./UserNav/UserNav";
diff --git a/src/components/organisms/Header/Header.module.css b/src/components/organisms/Header/Header.module.css
index d68e48d..22aa145 100644
--- a/src/components/organisms/Header/Header.module.css
+++ b/src/components/organisms/Header/Header.module.css
@@ -18,3 +18,28 @@
align-items: center;
gap: 1.5rem;
}
+
+.navLink {
+ text-decoration: none;
+ color: var(--color-text);
+ font-size: 0.9rem;
+ font-weight: 500;
+}
+
+.navLink:hover {
+ text-decoration: underline;
+}
+
+.logoutButton {
+ border: none;
+ background-color: transparent;
+ color: var(--color-text);
+ font-size: 0.9rem;
+ font-weight: 500;
+ cursor: pointer;
+ padding: 0;
+}
+
+.logoutButton:hover {
+ text-decoration: underline;
+}
diff --git a/src/components/organisms/Header/Header.tsx b/src/components/organisms/Header/Header.tsx
index ac82f82..d54a15e 100644
--- a/src/components/organisms/Header/Header.tsx
+++ b/src/components/organisms/Header/Header.tsx
@@ -1,14 +1,7 @@
-import { Heart, ShoppingCart } from "lucide-react";
import { CATEGORIES } from "../../../constants/categories";
-import { useAuth } from "../../../contexts/AuthContext";
+import { useAuthStore } from "../../../store/authStore";
import { Logo } from "../../atoms";
-import {
- AuthNav,
- IconButton,
- NavMenu,
- SearchBar,
- UserMenu,
-} from "../../molecules";
+import { AuthNav, NavMenu, SearchBar, UserNav } from "../../molecules";
import styles from "./Header.module.css";
type HeaderProps = {
@@ -24,7 +17,8 @@ export default function Header({
selectedCategory,
setSelectedCategory,
}: HeaderProps) {
- const { isLoggedIn, user, wishlistCount, cartCount } = useAuth();
+ const { accessToken } = useAuthStore();
+ const isLoggedIn = !!accessToken;
return (
@@ -38,25 +32,9 @@ export default function Header({
}
/>
- {isLoggedIn ? (
-
-
-
-
-
- ) : (
-
- )}
+
void;
- logout: () => void;
- // 필요하면 카운트 업데이트 함수도 추가
-};
-
-const AuthContext = createContext(undefined);
-
-export function AuthProvider({ children }: { children: ReactNode }) {
- const [isLoggedIn, setIsLoggedIn] = useState(false);
- const [user, setUser] = useState();
- const [wishlistCount, setWishlistCount] = useState(0);
- const [cartCount, setCartCount] = useState(0);
-
- const login = (userData: User) => {
- setIsLoggedIn(true);
- setUser(userData);
- // 예: 서버에서 counts 받아오기
- // setWishlistCount(fetchedWishlist);
- // setCartCount(fetchedCart);
- };
-
- const logout = () => {
- setIsLoggedIn(false);
- setUser(undefined);
- setWishlistCount(0);
- setCartCount(0);
- };
-
- return (
-
- {children}
-
- );
-}
-
-export function useAuth() {
- const ctx = useContext(AuthContext);
- if (!ctx) throw new Error("useAuth must be inside AuthProvider");
- return ctx;
-}
diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx
index 8e89bef..cd09c78 100644
--- a/src/pages/Login/Login.tsx
+++ b/src/pages/Login/Login.tsx
@@ -3,10 +3,12 @@ import { Eye, EyeClosed } from "lucide-react";
import { type FormEvent, useState } from "react";
import { useNavigate } from "react-router-dom";
import { login } from "../../api/authApi";
+import { useAuthStore } from "../../store/authStore";
import styles from "./Login.module.css";
const Login = () => {
const navigate = useNavigate();
+ const { setAuth } = useAuthStore();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
@@ -21,14 +23,13 @@ const Login = () => {
setIsLoading(true);
try {
- // 1. API를 호출하여 응답 데이터를 받습니다.
const data = await login({ email, password });
console.log("로그인 성공:", data);
- // 2. 받은 accessToken을 localStorage에 저장합니다.
- localStorage.setItem("accessToken", data.accessToken);
+ // localStorage와 메모리 상태(zustand)를 모두 업데이트해줍니다.
+ setAuth(data.accessToken, data.userId);
- // 3. alert 대신 메인 페이지('/')로 즉시 리다이렉트(이동)합니다.
+ // 메인 페이지('/')로 즉시 리다이렉트(이동)합니다.
navigate("/");
} catch (err) {
const error = err as AxiosError<{ code: string; message: string }>;
@@ -86,7 +87,7 @@ const Login = () => {
className={styles.input}
placeholder="이메일 또는 아이디를 입력하세요"
required
- disabled={isLoading} // 로딩 중 비활성화
+ disabled={isLoading}
/>
@@ -104,7 +105,7 @@ const Login = () => {
className={`${styles.input} ${styles.passwordInput}`}
placeholder="비밀번호를 입력하세요"
required
- disabled={isLoading} // 로딩 중 비활성화
+ disabled={isLoading}
/>