Merge main into lhj: 그룹별 카드 + 일반 지표 독립 데이터 소스 기능 + getApiUrl 통합
This commit is contained in:
commit
4f8d6fe875
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { DashboardElement } from "@/components/admin/dashboard/types";
|
||||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||||
|
||||
interface CustomMetricWidgetProps {
|
||||
element?: DashboardElement;
|
||||
|
|
@ -91,7 +92,7 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps)
|
|||
if (!groupByDS.query) return;
|
||||
|
||||
const token = localStorage.getItem("authToken");
|
||||
const response = await fetch("/api/dashboards/execute-query", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -129,7 +130,7 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps)
|
|||
if (!groupByDS.endpoint) return;
|
||||
|
||||
const token = localStorage.getItem("authToken");
|
||||
const response = await fetch("/api/dashboards/fetch-external-api", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -191,7 +192,7 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps)
|
|||
}
|
||||
|
||||
const token = localStorage.getItem("authToken");
|
||||
const response = await fetch("/api/dashboards/execute-query", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -232,7 +233,7 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps)
|
|||
}
|
||||
|
||||
const token = localStorage.getItem("authToken");
|
||||
const response = await fetch("/api/dashboards/fetch-external-api", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -457,7 +457,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
</aside>
|
||||
|
||||
{/* 가운데 컨텐츠 영역 - 스크롤 가능 */}
|
||||
<main className="h-[calc(100vh-3.5rem)] min-w-0 flex-1 overflow-auto bg-white p-4">{children}</main>
|
||||
<main className="h-[calc(100vh-3.5rem)] min-w-0 flex-1 overflow-auto bg-white">{children}</main>
|
||||
</div>
|
||||
|
||||
{/* 프로필 수정 모달 */}
|
||||
|
|
|
|||
|
|
@ -63,13 +63,19 @@ const TokenManager = {
|
|||
|
||||
setToken: (token: string): void => {
|
||||
if (typeof window !== "undefined") {
|
||||
// localStorage에 저장
|
||||
localStorage.setItem("authToken", token);
|
||||
// 쿠키에도 저장 (미들웨어에서 사용)
|
||||
document.cookie = `authToken=${token}; path=/; max-age=86400; SameSite=Lax`;
|
||||
}
|
||||
},
|
||||
|
||||
removeToken: (): void => {
|
||||
if (typeof window !== "undefined") {
|
||||
// localStorage에서 제거
|
||||
localStorage.removeItem("authToken");
|
||||
// 쿠키에서도 제거
|
||||
document.cookie = "authToken=; path=/; max-age=0; SameSite=Lax";
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -61,11 +61,15 @@ export const useLogin = () => {
|
|||
* API 호출 공통 함수
|
||||
*/
|
||||
const apiCall = useCallback(async (endpoint: string, options: RequestInit = {}): Promise<LoginResponse> => {
|
||||
// 로컬 스토리지에서 토큰 가져오기
|
||||
const token = localStorage.getItem("authToken");
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
...options.headers,
|
||||
},
|
||||
...options,
|
||||
|
|
@ -90,16 +94,19 @@ export const useLogin = () => {
|
|||
// 토큰이 있으면 API 호출로 유효성 확인
|
||||
const result = await apiCall(AUTH_CONFIG.ENDPOINTS.STATUS);
|
||||
|
||||
if (result.success && result.data?.isLoggedIn) {
|
||||
// 백엔드가 isAuthenticated 필드를 반환함
|
||||
if (result.success && result.data?.isAuthenticated) {
|
||||
// 이미 로그인된 경우 메인으로 리다이렉트
|
||||
router.push(AUTH_CONFIG.ROUTES.MAIN);
|
||||
} else {
|
||||
// 토큰이 유효하지 않으면 제거
|
||||
localStorage.removeItem("authToken");
|
||||
document.cookie = "authToken=; path=/; max-age=0; SameSite=Lax";
|
||||
}
|
||||
} catch (error) {
|
||||
// 에러가 발생하면 토큰 제거
|
||||
localStorage.removeItem("authToken");
|
||||
document.cookie = "authToken=; path=/; max-age=0; SameSite=Lax";
|
||||
console.debug("기존 인증 체크 중 오류 (정상):", error);
|
||||
}
|
||||
}, [apiCall, router]);
|
||||
|
|
@ -128,9 +135,12 @@ export const useLogin = () => {
|
|||
});
|
||||
|
||||
if (result.success && result.data?.token) {
|
||||
// JWT 토큰 저장
|
||||
// JWT 토큰 저장 (localStorage와 쿠키 모두에 저장)
|
||||
localStorage.setItem("authToken", result.data.token);
|
||||
|
||||
// 쿠키에도 저장 (미들웨어에서 사용)
|
||||
document.cookie = `authToken=${result.data.token}; path=/; max-age=86400; SameSite=Lax`;
|
||||
|
||||
// 로그인 성공
|
||||
router.push(AUTH_CONFIG.ROUTES.MAIN);
|
||||
} else {
|
||||
|
|
@ -148,13 +158,10 @@ export const useLogin = () => {
|
|||
[formData, validateForm, apiCall, router],
|
||||
);
|
||||
|
||||
// 컴포넌트 마운트 시 기존 인증 상태 확인 (한 번만 실행)
|
||||
// 컴포넌트 마운트 시 기존 인증 상태 확인
|
||||
useEffect(() => {
|
||||
// 로그인 페이지에서만 실행
|
||||
if (window.location.pathname === "/login") {
|
||||
checkExistingAuth();
|
||||
}
|
||||
}, []); // 의존성 배열을 비워서 한 번만 실행
|
||||
checkExistingAuth();
|
||||
}, [checkExistingAuth]);
|
||||
|
||||
return {
|
||||
// 상태
|
||||
|
|
|
|||
|
|
@ -3,7 +3,27 @@
|
|||
*/
|
||||
|
||||
import { DashboardElement } from "@/components/admin/dashboard/types";
|
||||
import { API_BASE_URL } from "./client";
|
||||
|
||||
// API URL 동적 설정
|
||||
function getApiBaseUrl(): string {
|
||||
// 클라이언트 사이드에서만 실행
|
||||
if (typeof window !== "undefined") {
|
||||
const hostname = window.location.hostname;
|
||||
|
||||
// 프로덕션: v1.vexplor.com → https://api.vexplor.com/api
|
||||
if (hostname === "v1.vexplor.com") {
|
||||
return "https://api.vexplor.com/api";
|
||||
}
|
||||
|
||||
// 로컬 개발: localhost → http://localhost:8080/api
|
||||
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
||||
return "http://localhost:8080/api";
|
||||
}
|
||||
}
|
||||
|
||||
// 서버 사이드 렌더링 시 기본값
|
||||
return "/api";
|
||||
}
|
||||
|
||||
// 토큰 가져오기 (실제 인증 시스템에 맞게 수정)
|
||||
function getAuthToken(): string | null {
|
||||
|
|
@ -17,6 +37,7 @@ async function apiRequest<T>(
|
|||
options: RequestInit = {},
|
||||
): Promise<{ success: boolean; data?: T; message?: string; pagination?: any }> {
|
||||
const token = getAuthToken();
|
||||
const API_BASE_URL = getApiBaseUrl();
|
||||
|
||||
const config: RequestInit = {
|
||||
headers: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* API URL 유틸리티
|
||||
* 프로덕션/개발 환경에 따라 올바른 API URL을 반환
|
||||
*/
|
||||
|
||||
export function getApiUrl(endpoint: string): string {
|
||||
// 클라이언트 사이드에서만 실행
|
||||
if (typeof window !== "undefined") {
|
||||
const hostname = window.location.hostname;
|
||||
|
||||
// 프로덕션: v1.vexplor.com → https://api.vexplor.com
|
||||
if (hostname === "v1.vexplor.com") {
|
||||
return `https://api.vexplor.com${endpoint}`;
|
||||
}
|
||||
|
||||
// 로컬 개발: localhost → http://localhost:8080
|
||||
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
||||
return `http://localhost:8080${endpoint}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 기본값: 상대 경로
|
||||
return endpoint;
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
/**
|
||||
* Next.js 미들웨어
|
||||
* 페이지 렌더링 전에 실행되어 인증 상태를 확인하고 리다이렉트 처리
|
||||
*/
|
||||
export function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
// 인증 토큰 확인
|
||||
const token = request.cookies.get("authToken")?.value || request.headers.get("authorization")?.replace("Bearer ", "");
|
||||
|
||||
// /login 페이지 접근 시
|
||||
if (pathname === "/login") {
|
||||
// 토큰이 있으면 메인 페이지로 리다이렉트
|
||||
if (token) {
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/main";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
// 토큰이 없으면 로그인 페이지 표시
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// 인증이 필요한 페이지들
|
||||
const protectedPaths = ["/main", "/admin", "/dashboard", "/settings"];
|
||||
const isProtectedPath = protectedPaths.some((path) => pathname.startsWith(path));
|
||||
|
||||
if (isProtectedPath && !token) {
|
||||
// 인증되지 않은 사용자는 로그인 페이지로 리다이렉트
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/login";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* 미들웨어가 실행될 경로 설정
|
||||
*/
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* 다음 경로를 제외한 모든 요청에 대해 실행:
|
||||
* - api (API routes)
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
* - public 폴더의 파일들
|
||||
*/
|
||||
"/((?!api|_next/static|_next/image|favicon.ico|.*\\.png$|.*\\.jpg$|.*\\.jpeg$|.*\\.svg$).*)",
|
||||
],
|
||||
};
|
||||
Loading…
Reference in New Issue