feat: 프론트 인증 연동 (소셜 로그인 + Bearer 세션)
- 로그인 화면: 구글(GIS) 버튼 + 애플 버튼 + 개발용 로그인(local) - 인증 스토어(Pinia): 토큰 복구/로그인/로그아웃, 회원 티어를 광고·매칭 제어에 반영 - HTTP 클라이언트: Authorization Bearer 자동 첨부, 401 시 토큰 폐기 후 로그인 이동 - 라우터 가드: 미로그인 시 보호 경로 → /login (소셜 로그인 전용 앱) - 헤더 회원 메뉴/로그아웃, 체크인 주체를 로그인 회원으로 전환 - 토큰 localStorage 저장(웹/Capacitor 공용) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { http } from './http'
|
||||
|
||||
export interface Member {
|
||||
id: number
|
||||
email: string
|
||||
nickname: string
|
||||
role: string
|
||||
provider: string
|
||||
subscriptionTier: 'FREE' | 'AD_FREE' | 'PREMIUM'
|
||||
status: string
|
||||
profileImage: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string
|
||||
expiresInSeconds: number
|
||||
member: Member
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
googleClientId: () => http.get<{ clientId: string }>('/api/auth/google-client-id'),
|
||||
google: (idToken: string, rememberMe = true) =>
|
||||
http.post<LoginResponse>('/api/auth/google', { idToken, rememberMe }),
|
||||
apple: (identityToken: string, name: string | null, rememberMe = true) =>
|
||||
http.post<LoginResponse>('/api/auth/apple', { identityToken, name, rememberMe }),
|
||||
me: () => http.get<Member>('/api/auth/me'),
|
||||
logout: () => http.post<void>('/api/auth/logout'),
|
||||
}
|
||||
+19
-2
@@ -1,4 +1,6 @@
|
||||
// 공통 HTTP 클라이언트 — VITE_API_BASE_URL 기준
|
||||
import { tokenStore } from '@/lib/token'
|
||||
|
||||
// 공통 HTTP 클라이언트 — VITE_API_BASE_URL 기준, Bearer 토큰 자동 첨부
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8080'
|
||||
|
||||
export class ApiError extends Error {
|
||||
@@ -12,13 +14,28 @@ export class ApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
// 401 발생 시 호출될 핸들러(로그인 화면 이동 등). 순환참조 방지용 주입.
|
||||
let onUnauthorized: (() => void) | null = null
|
||||
export function setUnauthorizedHandler(fn: () => void): void {
|
||||
onUnauthorized = fn
|
||||
}
|
||||
|
||||
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
const token = tokenStore.get()
|
||||
if (token) headers.Authorization = `Bearer ${token}`
|
||||
|
||||
const res = await fetch(`${BASE_URL}${path}`, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
})
|
||||
|
||||
if (res.status === 401) {
|
||||
tokenStore.clear()
|
||||
onUnauthorized?.()
|
||||
}
|
||||
|
||||
const text = await res.text()
|
||||
const data = text ? JSON.parse(text) : null
|
||||
|
||||
|
||||
Reference in New Issue
Block a user