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:
sb
2026-07-11 08:35:00 +09:00
parent f089ba2dfe
commit 0c538e0658
10 changed files with 352 additions and 9 deletions
+29
View File
@@ -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
View File
@@ -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