[feat]: 보호된 대시보드 및 API 라우트 추가
- 인증된 사용자용 대시보드 페이지 구현 ((protected) 라우트 그룹) - API 엔드포인트 추가 (인증, 사용자 관리) - 커스텀 훅 추가 (useAuth) - 미들웨어 인증 로직 강화 - 환경변수 예제 업데이트 - 기존 dashboard 페이지 제거 후 보호된 라우트로 이동 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
56
src/lib/api/auth/auth-config.ts
Normal file
56
src/lib/api/auth/auth-config.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// lib/api/auth/auth-config.ts
|
||||
|
||||
export const AUTH_CONFIG = {
|
||||
// API Base URL
|
||||
apiUrl: process.env.NEXT_PUBLIC_API_URL || 'https://api.5130.co.kr',
|
||||
|
||||
// Frontend URL
|
||||
frontendUrl: process.env.NEXT_PUBLIC_FRONTEND_URL || 'http://localhost:3000',
|
||||
|
||||
// 인증 모드 (환경에 따라 선택)
|
||||
defaultAuthMode: (process.env.NEXT_PUBLIC_AUTH_MODE || 'sanctum') as 'sanctum' | 'bearer',
|
||||
|
||||
// 🔓 공개 라우트 (인증 불필요)
|
||||
// 명시적으로 여기에 추가된 경로만 비로그인 접근 가능
|
||||
// 기본 정책: 모든 페이지는 인증 필요
|
||||
publicRoutes: [
|
||||
// 비어있음 - 필요시 추가 (예: '/about', '/terms', '/privacy')
|
||||
],
|
||||
|
||||
// 🔒 보호된 라우트 (참고용, 실제로는 기본 정책으로 보호됨)
|
||||
// publicRoutes와 guestOnlyRoutes가 아닌 모든 경로는 자동으로 보호됨
|
||||
protectedRoutes: [
|
||||
'/dashboard',
|
||||
'/profile',
|
||||
'/settings',
|
||||
'/admin',
|
||||
'/tenant',
|
||||
'/users',
|
||||
'/reports',
|
||||
'/analytics',
|
||||
'/inventory',
|
||||
'/finance',
|
||||
'/hr',
|
||||
'/crm',
|
||||
'/employee',
|
||||
'/customer',
|
||||
'/supplier',
|
||||
'/orders',
|
||||
'/invoices',
|
||||
'/payroll',
|
||||
],
|
||||
|
||||
// 게스트 전용 라우트 (로그인 후 접근 불가)
|
||||
guestOnlyRoutes: [
|
||||
'/login',
|
||||
'/signup',
|
||||
'/forgot-password',
|
||||
],
|
||||
|
||||
// 리다이렉트 설정
|
||||
redirects: {
|
||||
afterLogin: '/dashboard',
|
||||
afterLogout: '/login',
|
||||
unauthorized: '/login',
|
||||
},
|
||||
} as const;
|
||||
87
src/lib/api/auth/types.ts
Normal file
87
src/lib/api/auth/types.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
// lib/api/auth/types.ts
|
||||
|
||||
/**
|
||||
* 인증 모드 타입
|
||||
*/
|
||||
export type AuthMode = 'sanctum' | 'api-key' | 'bearer';
|
||||
|
||||
/**
|
||||
* 사용자 정보
|
||||
*/
|
||||
export interface User {
|
||||
id: number;
|
||||
user_id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
role?: string;
|
||||
permissions?: string[];
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 정보 (추후 사용)
|
||||
*/
|
||||
export interface Tenant {
|
||||
id: number;
|
||||
name: string;
|
||||
// 추가 필드
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 정보 (추후 사용)
|
||||
*/
|
||||
export interface Menu {
|
||||
id: number;
|
||||
name: string;
|
||||
// 추가 필드
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인 요청 (PHP API 형식)
|
||||
*/
|
||||
export interface LoginCredentials {
|
||||
user_id: string; // PHP API: user_id
|
||||
user_pwd: string; // PHP API: user_pwd
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인 응답 (PHP API 형식)
|
||||
*/
|
||||
export interface LoginResponse {
|
||||
message: string;
|
||||
user_token: string; // Bearer 토큰
|
||||
user: User;
|
||||
tenant: Tenant | null;
|
||||
menus: Menu[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원가입 요청 (추후 구현)
|
||||
*/
|
||||
export interface RegisterData {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
password_confirmation: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 에러 응답
|
||||
*/
|
||||
export interface ApiError {
|
||||
message: string;
|
||||
errors?: Record<string, string[]>;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bearer Token 응답 (모바일/SPA용)
|
||||
*/
|
||||
export interface BearerTokenResponse {
|
||||
access_token: string;
|
||||
token_type: 'Bearer';
|
||||
expires_in: number;
|
||||
user: User;
|
||||
}
|
||||
153
src/lib/api/client.ts
Normal file
153
src/lib/api/client.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
// lib/api/client.ts
|
||||
import { AUTH_CONFIG } from './auth/auth-config';
|
||||
import type { AuthMode } from './auth/types';
|
||||
|
||||
interface ClientConfig {
|
||||
mode: AuthMode;
|
||||
apiKey?: string; // API Key 모드용
|
||||
getToken?: () => string | null; // Bearer 모드용
|
||||
}
|
||||
|
||||
interface ApiErrorResponse {
|
||||
message: string;
|
||||
errors?: Record<string, string[]>;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export class ApiClient {
|
||||
private baseURL: string;
|
||||
private mode: AuthMode;
|
||||
private apiKey?: string;
|
||||
private getToken?: () => string | null;
|
||||
|
||||
constructor(config: ClientConfig) {
|
||||
this.baseURL = AUTH_CONFIG.apiUrl;
|
||||
this.mode = config.mode;
|
||||
this.apiKey = config.apiKey;
|
||||
this.getToken = config.getToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증 헤더 생성
|
||||
*/
|
||||
private getAuthHeaders(): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
// API Key는 모든 모드에서 기본으로 포함 (PHP API 요구사항)
|
||||
if (this.apiKey) {
|
||||
headers['X-API-KEY'] = this.apiKey;
|
||||
}
|
||||
|
||||
switch (this.mode) {
|
||||
case 'api-key':
|
||||
// API Key만 사용 (이미 위에서 추가됨)
|
||||
break;
|
||||
|
||||
case 'bearer':
|
||||
const token = this.getToken?.();
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
// API Key도 함께 전송 (이미 위에서 추가됨)
|
||||
break;
|
||||
|
||||
case 'sanctum':
|
||||
// 쿠키 기반 - 별도 헤더 불필요
|
||||
break;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 요청 실행
|
||||
*/
|
||||
async request<T>(
|
||||
endpoint: string,
|
||||
options?: RequestInit
|
||||
): Promise<T> {
|
||||
const url = `${this.baseURL}${endpoint}`;
|
||||
const headers = {
|
||||
...this.getAuthHeaders(),
|
||||
...options?.headers,
|
||||
};
|
||||
|
||||
const config: RequestInit = {
|
||||
...options,
|
||||
headers,
|
||||
};
|
||||
|
||||
// Sanctum 모드는 쿠키 포함
|
||||
if (this.mode === 'sanctum') {
|
||||
config.credentials = 'include';
|
||||
}
|
||||
|
||||
const response = await fetch(url, config);
|
||||
|
||||
if (!response.ok) {
|
||||
await this.handleError(response);
|
||||
}
|
||||
|
||||
// 204 No Content 처리
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 요청
|
||||
*/
|
||||
async get<T>(endpoint: string): Promise<T> {
|
||||
return this.request<T>(endpoint, { method: 'GET' });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 요청
|
||||
*/
|
||||
async post<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'POST',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT 요청
|
||||
*/
|
||||
async put<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'PUT',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 요청
|
||||
*/
|
||||
async delete<T>(endpoint: string): Promise<T> {
|
||||
return this.request<T>(endpoint, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 처리
|
||||
*/
|
||||
private async handleError(response: Response): Promise<never> {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
|
||||
const error: ApiErrorResponse = {
|
||||
message: data.message || 'An error occurred',
|
||||
errors: data.errors,
|
||||
code: data.code,
|
||||
};
|
||||
|
||||
throw {
|
||||
status: response.status,
|
||||
...error,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user