diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx
new file mode 100644
index 0000000..ef88708
--- /dev/null
+++ b/src/app/(app)/dashboard/page.tsx
@@ -0,0 +1,53 @@
+"use client";
+
+import { useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { useAuthStore } from "@/store/useAuthStore";
+import { UserProfileCard, useUserProfile } from "@/features/user";
+
+export default function DashboardPage() {
+ const router = useRouter();
+ const { logout, isAuthenticated } = useAuthStore();
+
+ // Feature 모듈에서 로직과 데이터를 가져옴
+ const { user, isLoading, error } = useUserProfile();
+
+ useEffect(() => {
+ if (!isAuthenticated) {
+ router.push("/login");
+ }
+ }, [isAuthenticated, router]);
+
+ const handleLogout = () => {
+ logout();
+ router.push("/login");
+ };
+
+ return (
+
+
+ VibeRoom Dashboard
+
+
+
+
+ {/* Feature 컴포넌트에 데이터와 상태만 전달 */}
+
+
+
+
+
+ );
+}
diff --git a/src/features/auth/components/SocialLoginGroup.tsx b/src/features/auth/components/SocialLoginGroup.tsx
index a693688..b9ede44 100644
--- a/src/features/auth/components/SocialLoginGroup.tsx
+++ b/src/features/auth/components/SocialLoginGroup.tsx
@@ -92,10 +92,6 @@ const SocialLoginButtons = () => {
);
};
-/**
- * 로그인 화면 UI (View)
- * GoogleOAuthProvider로 감싸서 내부에서 useGoogleLogin 훅을 사용할 수 있게 합니다.
- */
export const SocialLoginGroup = () => {
return (
diff --git a/src/features/auth/hooks/useSocialLogin.ts b/src/features/auth/hooks/useSocialLogin.ts
index 547806c..2af0f7e 100644
--- a/src/features/auth/hooks/useSocialLogin.ts
+++ b/src/features/auth/hooks/useSocialLogin.ts
@@ -21,7 +21,7 @@ export const useSocialLogin = () => {
setIsLoading(true);
setError(null);
- console.log("token:" + socialToken);
+ console.log(`[${provider}] token:`, socialToken);
try {
// 1. 백엔드로 소셜 토큰 전송 (토큰 교환)
const response = await authApi.loginWithSocial({
@@ -50,6 +50,7 @@ export const useSocialLogin = () => {
*/
const loginWithGoogle = useGoogleLogin({
onSuccess: (tokenResponse) => {
+ console.log(tokenResponse);
handleSocialLogin("google", tokenResponse.access_token);
},
onError: () => {
diff --git a/src/features/auth/types/index.ts b/src/features/auth/types/index.ts
index 1ec81ef..d2a1c32 100644
--- a/src/features/auth/types/index.ts
+++ b/src/features/auth/types/index.ts
@@ -1,9 +1,17 @@
export interface SocialLoginRequest {
- provider: "GOOGLE" | "apple" | "FACEBOOK";
+ provider: "google" | "apple" | "facebook";
token: string; // 소셜 프로바이더로부터 발급받은 id_token 또는 access_token
}
export interface AuthResponse {
accessToken: string; // VibeRoom 전용 JWT (API 요청 시 사용)
refreshToken: string; // 토큰 갱신용
+ user?: UserMeResponse; // 선택적으로 유저 정보를 포함할 수 있음
+}
+
+export interface UserMeResponse {
+ id: number;
+ name: string;
+ email: string;
+ grade: string;
}
diff --git a/src/features/user/api/userApi.ts b/src/features/user/api/userApi.ts
new file mode 100644
index 0000000..6389797
--- /dev/null
+++ b/src/features/user/api/userApi.ts
@@ -0,0 +1,11 @@
+import { apiClient } from "@/shared/lib/apiClient";
+import { UserProfile } from "../types";
+
+export const userApi = {
+ /**
+ * 내 정보 조회
+ */
+ getMe: async (): Promise => {
+ return apiClient("api/v1/user/me");
+ },
+};
diff --git a/src/features/user/components/UserProfileCard.tsx b/src/features/user/components/UserProfileCard.tsx
new file mode 100644
index 0000000..f234f6d
--- /dev/null
+++ b/src/features/user/components/UserProfileCard.tsx
@@ -0,0 +1,60 @@
+import { UserProfile } from "../types";
+
+interface UserProfileCardProps {
+ user: UserProfile | null;
+ isLoading: boolean;
+ error: string | null;
+}
+
+export const UserProfileCard = ({
+ user,
+ isLoading,
+ error,
+}: UserProfileCardProps) => {
+ if (isLoading) {
+ return ;
+ }
+
+ if (error) {
+ return (
+
+ {error}
+
+ );
+ }
+
+ if (!user) return null;
+
+ return (
+
+
+
+ {user.name[0]}
+
+
+
{user.name}
+
{user.email}
+
+
+
+
+
+
+ User ID
+
+
{user.id}
+
+
+
+ Grade
+
+
+
+ {user.grade}
+
+
+
+
+
+ );
+};
diff --git a/src/features/user/hooks/useUserProfile.ts b/src/features/user/hooks/useUserProfile.ts
new file mode 100644
index 0000000..7c1fbf3
--- /dev/null
+++ b/src/features/user/hooks/useUserProfile.ts
@@ -0,0 +1,32 @@
+import { useAuthStore } from "@/store/useAuthStore";
+import { useEffect, useState } from "react";
+import { userApi } from "../api/userApi";
+import { UserProfile } from "../types";
+
+export const useUserProfile = () => {
+ const [user, setUser] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const { isAuthenticated } = useAuthStore();
+
+ useEffect(() => {
+ if (!isAuthenticated) return;
+
+ const loadProfile = async () => {
+ try {
+ setIsLoading(true);
+ const data = await userApi.getMe();
+ console.log("data: " + data);
+ setUser(data);
+ } catch (err) {
+ setError("프로필을 불러오는 데 실패했습니다.");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ loadProfile();
+ }, [isAuthenticated]);
+
+ return { user, isLoading, error };
+};
diff --git a/src/features/user/index.ts b/src/features/user/index.ts
new file mode 100644
index 0000000..fd0b66a
--- /dev/null
+++ b/src/features/user/index.ts
@@ -0,0 +1,4 @@
+export * from "./types";
+export * from "./hooks/useUserProfile";
+export * from "./components/UserProfileCard";
+export * from "./api/userApi";
diff --git a/src/features/user/types/index.ts b/src/features/user/types/index.ts
new file mode 100644
index 0000000..8280527
--- /dev/null
+++ b/src/features/user/types/index.ts
@@ -0,0 +1,6 @@
+export interface UserProfile {
+ id: number;
+ name: string;
+ email: string;
+ grade: string;
+}
diff --git a/src/shared/lib/apiClient.ts b/src/shared/lib/apiClient.ts
index 664398e..f11c68d 100644
--- a/src/shared/lib/apiClient.ts
+++ b/src/shared/lib/apiClient.ts
@@ -6,20 +6,31 @@
* - 빌드 후 운영 환경: https://api.viberoom.io (from .env.production)
*/
+import Cookies from "js-cookie";
+
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8080";
+const TOKEN_COOKIE_KEY = "vr_access_token";
+
export const apiClient = async (
endpoint: string,
options: RequestInit = {},
): Promise => {
- // 엔드포인트 앞의 슬래시(/) 중복 방지
const url = `${API_BASE_URL}${endpoint.startsWith("/") ? endpoint : `/${endpoint}`}`;
- const defaultHeaders = {
+ // 쿠키에서 토큰 가져오기
+ const token = Cookies.get(TOKEN_COOKIE_KEY);
+
+ const defaultHeaders: Record = {
"Content-Type": "application/json",
};
+ // 토큰이 있으면 Authorization 헤더 추가
+ if (token) {
+ defaultHeaders["Authorization"] = `Bearer ${token}`;
+ }
+
const response = await fetch(url, {
...options,
headers: {
@@ -34,5 +45,6 @@ export const apiClient = async (
throw new Error(`API 요청 실패: ${response.status}`);
}
- return response.json() as Promise;
+ const result = await response.json();
+ return result.data as T;
};
diff --git a/src/store/useAuthStore.ts b/src/store/useAuthStore.ts
index 8d1393a..7801c06 100644
--- a/src/store/useAuthStore.ts
+++ b/src/store/useAuthStore.ts
@@ -12,34 +12,43 @@ interface AuthState {
logout: () => void;
}
-// 쿠키 키 상수 정의 (Access Token 보관용)
+// 쿠키 키 상수 정의
const TOKEN_COOKIE_KEY = 'vr_access_token';
+const REFRESH_TOKEN_COOKIE_KEY = 'vr_refresh_token';
/**
* VibeRoom 전역 인증(Auth) 상태 저장소
- *
- * Zustand를 활용하여 메모리에 상태를 쥐고 있으면서,
- * 쿠키(js-cookie)를 통해 브라우저 종료 후에도 세션이 유지되도록 동기화합니다.
*/
export const useAuthStore = create((set) => {
- // 브라우저 환경에서만 쿠키 접근
const isClient = typeof window !== 'undefined';
const savedToken = isClient ? Cookies.get(TOKEN_COOKIE_KEY) : null;
return {
accessToken: savedToken || null,
- user: null, // 새로고침 시 이 토큰을 기반으로 유저 정보(me API)를 다시 가져와야 합니다(Hydrate).
+ user: null,
isAuthenticated: !!savedToken,
setAuth: (data: AuthResponse) => {
- // 1. 브라우저 쿠키에 저장 (보안 옵션 설정)
+ const cookieOptions = {
+ secure: process.env.NODE_ENV === 'production',
+ sameSite: 'strict' as const
+ };
+
+ // 1. Access Token 저장 (7일)
Cookies.set(TOKEN_COOKIE_KEY, data.accessToken, {
- expires: 7, // 7일 후 만료
- secure: process.env.NODE_ENV === 'production', // HTTPS 환경에서만 전송
- sameSite: 'strict' // CSRF 공격 방어
+ ...cookieOptions,
+ expires: 7
});
- // 2. Zustand 메모리 상태 업데이트 (연결된 UI 전체 리렌더링)
+ // 2. Refresh Token 저장 (30일)
+ if (data.refreshToken) {
+ Cookies.set(REFRESH_TOKEN_COOKIE_KEY, data.refreshToken, {
+ ...cookieOptions,
+ expires: 30
+ });
+ }
+
+ // 3. 상태 업데이트
set({
accessToken: data.accessToken,
user: data.user,
@@ -48,10 +57,9 @@ export const useAuthStore = create((set) => {
},
logout: () => {
- // 1. 쿠키에서 토큰 삭제
Cookies.remove(TOKEN_COOKIE_KEY);
+ Cookies.remove(REFRESH_TOKEN_COOKIE_KEY);
- // 2. Zustand 상태 초기화
set({
accessToken: null,
user: null,