diff --git a/flutter/lib/core/api/dio_provider.dart b/flutter/lib/core/api/dio_provider.dart new file mode 100644 index 0000000..1771e8b --- /dev/null +++ b/flutter/lib/core/api/dio_provider.dart @@ -0,0 +1,9 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'dio_client.dart'; + +// 전역 Dio Provider — 리포지토리/데이터소스에서 ref.watch(dioProvider) 로 주입받는다. +// 인터셉터(토큰 첨부·응답 언래핑·에러 매핑)가 적용된 DioClient.dio 를 노출한다. +final dioProvider = Provider((ref) { + return DioClient().dio; +}); diff --git a/flutter/lib/features/user/presentation/providers/user_provider.dart b/flutter/lib/features/user/presentation/providers/user_provider.dart new file mode 100644 index 0000000..7ffd7c2 --- /dev/null +++ b/flutter/lib/features/user/presentation/providers/user_provider.dart @@ -0,0 +1,15 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../core/api/dio_provider.dart'; +import '../../data/models/user_model.dart'; +import '../../data/repositories/user_repository.dart'; + +// 리포지토리 Provider — dioProvider 를 주입받아 생성 +final userRepositoryProvider = Provider((ref) { + return UserRepository(ref.watch(dioProvider)); +}); + +// 특정 사용자 조회 Provider (family) +// 위젯에서: ref.watch(userProvider(id)).when(data:..., loading:..., error:...) +final userProvider = FutureProvider.family((ref, id) { + return ref.watch(userRepositoryProvider).getUser(id); +}); diff --git a/frontend/src/components/BaseButton.vue b/frontend/src/components/BaseButton.vue new file mode 100644 index 0000000..15a31cb --- /dev/null +++ b/frontend/src/components/BaseButton.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/frontend/src/composables/useAuth.ts b/frontend/src/composables/useAuth.ts new file mode 100644 index 0000000..6fd9700 --- /dev/null +++ b/frontend/src/composables/useAuth.ts @@ -0,0 +1,43 @@ +import { ref } from 'vue' +import { useApi } from './useApi' +import { useAuthStore } from '@/stores/auth.store' + +// 인증 도메인 API Composable 샘플 — 로그인/로그아웃 흐름의 시작점 +// 실제 인증 엔드포인트가 추가되면 이 패턴을 그대로 사용/확장한다. +export interface LoginPayload { + email: string + password: string +} + +export function useAuth() { + const api = useApi() + const authStore = useAuthStore() + const loading = ref(false) + + async function login(payload: LoginPayload): Promise { + loading.value = true + try { + // 서버는 인증 성공 시 HttpOnly 쿠키로 토큰을 내려준다 (응답 바디에 토큰 없음) + await api.post('/auth/login', payload) + authStore.markAuthenticated() + return true + } catch { + // 에러 메시지/알림은 useApi 인터셉터에서 전역 처리됨 + return false + } finally { + loading.value = false + } + } + + async function logout(): Promise { + loading.value = true + try { + await api.post('/auth/logout') + } finally { + authStore.clearAuth() + loading.value = false + } + } + + return { loading, login, logout } +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 0f8fab3..d99f8c9 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,4 +1,5 @@ import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' +import { useAuthStore } from '@/stores/auth.store' // 코드 스플리팅 — 모든 페이지 컴포넌트는 lazy import 처리 const routes: RouteRecordRaw[] = [ @@ -20,10 +21,13 @@ const router = createRouter({ routes, }) -// 네비게이션 가드 — 인증 분기 (스토어 도입 후 useAuthStore로 교체) +// 네비게이션 가드 — 보호 라우트(requiresAuth) 접근 시 인증 여부 검사 router.beforeEach((to) => { - if (to.meta.requiresAuth) { - // TODO: useAuthStore().isLoggedIn 검사 후 '/login' 으로 리다이렉트 + const authStore = useAuthStore() + if (to.meta.requiresAuth && !authStore.isLoggedIn) { + // 미인증 시 홈으로 리다이렉트 + // (실제 로그인 페이지 추가 시 { name: 'login' } 으로 변경) + return { name: 'home' } } return true }) diff --git a/frontend/src/stores/auth.store.ts b/frontend/src/stores/auth.store.ts new file mode 100644 index 0000000..c827cd1 --- /dev/null +++ b/frontend/src/stores/auth.store.ts @@ -0,0 +1,24 @@ +import { computed, ref } from 'vue' +import { defineStore } from 'pinia' + +// 인증 세션 상태 스토어 +// 웹은 JWT 를 HttpOnly·Secure 쿠키로 보관하므로(보안 규칙), JS 에서 토큰을 직접 +// 저장/조회하지 않는다. 여기서는 "현재 인증된 세션인지" 여부만 관리한다. +// (사용자 프로필 데이터는 user.store.ts 에서 별도 관리) +export const useAuthStore = defineStore('auth', () => { + // 로그인 성공 또는 세션 확인 API 응답으로 갱신되는 인증 여부 + const authenticated = ref(false) + const isLoggedIn = computed(() => authenticated.value) + + // 로그인 성공 시 호출 (실제 로그인 API 연동 시 useAuth 에서 사용) + function markAuthenticated() { + authenticated.value = true + } + + // 로그아웃/세션 만료 시 호출 + function clearAuth() { + authenticated.value = false + } + + return { isLoggedIn, markAuthenticated, clearAuth } +})