feat: 로그인 세션 통합 (인증 가드, fetchWithAuth, 환경변수)

- authStore: 메인 프로젝트 세션 쿠키 기반 인증 상태 관리
- fetchWithAuth: 401 응답 시 메인 프로젝트 로그인 페이지 리다이렉트
- SessionGuard: 앱 진입 시 세션 유효성 검증 래퍼 컴포넌트
- 기존 API 모듈 fetch → fetchWithAuth 전환
- 환경변수에 VITE_MAIN_APP_URL, VITE_DEV_SKIP_AUTH 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
LHT 2026-02-12 13:54:21 +09:00
부모 34d5f6ef9e
커밋 de2cd907f1
17개의 변경된 파일273개의 추가작업 그리고 65개의 파일을 삭제

3
.env
파일 보기

@ -28,3 +28,6 @@ VITE_TRACKING_WS=wss://mda.kcg.go.kr/ws-tracks/websocket
# 선박 데이터 쓰로틀링 (ms, 위성망 대역폭 절약)
VITE_SHIP_THROTTLE=30
# 메인 프로젝트 URL (세션 만료 시 리다이렉트)
VITE_MAIN_APP_URL=https://mda.kcg.go.kr

파일 보기

@ -27,3 +27,6 @@ VITE_TRACKING_WS=ws://10.26.252.51:8090/ws-tracks/websocket
# 선박 데이터 쓰로틀링 (ms)
VITE_SHIP_THROTTLE=30
# 메인 프로젝트 URL (세션 만료 시 리다이렉트)
VITE_MAIN_APP_URL=http://10.26.252.39:9090

파일 보기

@ -4,8 +4,8 @@
# - 로컬 개발 전용
# ============================================
# 배포 경로 (로컬 개발은 루트)
VITE_BASE_URL=/
# 배포 경로 (프록시 모드: localhost:9090/kcgnv/ → localhost:3000/kcgnv/)
VITE_BASE_URL=/kcgnv/
# API 서버 (프록시 타겟)
VITE_API_URL=http://10.26.252.39:9090
@ -27,3 +27,10 @@ VITE_TRACKING_WS=ws://10.26.252.51:8090/ws-tracks/websocket
# 선박 데이터 쓰로틀링 (ms, 0=무제한)
VITE_SHIP_THROTTLE=0
# 메인 프로젝트 URL (세션 만료 시 리다이렉트)
VITE_MAIN_APP_URL=http://localhost:9090
# 로컬 개발 인증 우회 (포트가 달라 localStorage 공유 불가)
# true면 세션 없을 때 모의 사용자로 자동 로그인
VITE_DEV_SKIP_AUTH=false

파일 보기

@ -27,3 +27,6 @@ VITE_TRACKING_WS=ws://10.188.141.123:9090/ws-tracks/websocket
# 선박 데이터 쓰로틀링 (ms)
VITE_SHIP_THROTTLE=30
# 메인 프로젝트 URL (세션 만료 시 리다이렉트)
VITE_MAIN_APP_URL=http://10.188.141.123:9090

파일 보기

@ -3,6 +3,7 @@ import { lazy, Suspense } from 'react';
// -
import MainLayout from './components/layout/MainLayout';
import SessionGuard from './components/auth/SessionGuard';
import { ToastContainer } from './components/common/Toast';
import { AlertModalContainer } from './components/common/AlertModal';
@ -22,7 +23,7 @@ const PublishRouter = import.meta.env.DEV
export default function App() {
return (
<>
<SessionGuard>
<ToastContainer />
<AlertModalContainer />
<Routes>
@ -48,6 +49,6 @@ export default function App() {
{/* />*/}
{/*)}*/}
</Routes>
</>
</SessionGuard>
);
}

파일 보기

@ -1,6 +1,7 @@
/**
* 공통코드 API
*/
import { fetchWithAuth } from './fetchWithAuth';
const COMMON_CODE_LIST_ENDPOINT = '/api/cmn/code/list/search';
@ -12,10 +13,9 @@ const COMMON_CODE_LIST_ENDPOINT = '/api/cmn/code/list/search';
*/
export async function fetchCommonCodeList(commonCodeTypeNumber) {
try {
const response = await fetch(COMMON_CODE_LIST_ENDPOINT, {
const response = await fetchWithAuth(COMMON_CODE_LIST_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ commonCodeTypeNumber }),
});

41
src/api/fetchWithAuth.js Normal file
파일 보기

@ -0,0 +1,41 @@
import { useAuthStore } from '../stores/authStore';
import { SESSION_TIMEOUT_MS } from '../types/constants';
/**
* 인증 래퍼 fetch
* - 사전 체크: loginDate 기반 타임아웃
* - 사후 체크: 4011 응답 감지 (세션 만료)
* - credentials: 'include' 자동 설정
*/
export async function fetchWithAuth(url, options = {}) {
// 로컬 개발: 세션 타임아웃 체크 우회
if (import.meta.env.VITE_DEV_SKIP_AUTH !== 'true') {
const loginDate = localStorage.getItem('loginDate');
if (!loginDate || Date.now() - Number(loginDate) > SESSION_TIMEOUT_MS) {
useAuthStore.getState().handleSessionExpired();
throw new Error('Session expired');
}
localStorage.setItem('loginDate', String(Date.now()));
}
const response = await fetch(url, { ...options, credentials: 'include' });
// JSON 응답에서 4011 체크
if (response.ok) {
const ct = response.headers.get('content-type');
if (ct && ct.includes('application/json')) {
const cloned = response.clone();
try {
const data = await cloned.json();
if (data?.code === 4011) {
useAuthStore.getState().handleSessionExpired();
throw new Error('Session expired (4011)');
}
} catch (e) {
if (e.message.includes('Session expired')) throw e;
}
}
}
return response;
}

파일 보기

@ -1,6 +1,7 @@
/**
* 위성 API
*/
import { fetchWithAuth } from './fetchWithAuth';
const SATELLITE_VIDEO_SEARCH_ENDPOINT = '/api/gis/satlit/search';
const SATELLITE_CSV_ENDPOINT = '/api/gis/satlit/excelToJson';
@ -43,10 +44,10 @@ export async function fetchSatelliteVideoList({
satelliteVideoOrigin,
}) {
try {
const response = await fetch(SATELLITE_VIDEO_SEARCH_ENDPOINT, {
const response = await fetchWithAuth(SATELLITE_VIDEO_SEARCH_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
page,
startDate,
@ -83,10 +84,10 @@ export async function fetchSatelliteVideoList({
*/
export async function fetchSatelliteCsvFeatures(csvFileName) {
try {
const response = await fetch(SATELLITE_CSV_ENDPOINT, {
const response = await fetchWithAuth(SATELLITE_CSV_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ csvFileName }),
});
@ -116,10 +117,10 @@ export async function fetchSatelliteCsvFeatures(csvFileName) {
*/
export async function fetchSatelliteVideoDetail(satelliteId) {
try {
const response = await fetch(SATELLITE_DETAIL_ENDPOINT, {
const response = await fetchWithAuth(SATELLITE_DETAIL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ satelliteId }),
});
@ -154,10 +155,10 @@ export async function fetchSatelliteVideoDetail(satelliteId) {
*/
export async function updateSatelliteVideo(params) {
try {
const response = await fetch(SATELLITE_UPDATE_ENDPOINT, {
const response = await fetchWithAuth(SATELLITE_UPDATE_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(params),
});
@ -177,10 +178,10 @@ export async function updateSatelliteVideo(params) {
*/
export async function fetchSatelliteCompanyList() {
try {
const response = await fetch(SATELLITE_COMPANY_LIST_ENDPOINT, {
const response = await fetchWithAuth(SATELLITE_COMPANY_LIST_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({}),
});
@ -204,10 +205,10 @@ export async function fetchSatelliteCompanyList() {
*/
export async function fetchSatelliteManageList(companyNo) {
try {
const response = await fetch(SATELLITE_MANAGE_LIST_ENDPOINT, {
const response = await fetchWithAuth(SATELLITE_MANAGE_LIST_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ companyNo }),
});
@ -230,9 +231,9 @@ export async function fetchSatelliteManageList(companyNo) {
*/
export async function saveSatelliteVideo(formData) {
try {
const response = await fetch(SATELLITE_SAVE_ENDPOINT, {
const response = await fetchWithAuth(SATELLITE_SAVE_ENDPOINT, {
method: 'POST',
credentials: 'include',
body: formData,
});
@ -255,10 +256,10 @@ export async function saveSatelliteVideo(formData) {
*/
export async function searchSatelliteCompany({ companyTypeCode, companyName, page, limit }) {
try {
const response = await fetch(SATELLITE_COMPANY_SEARCH_ENDPOINT, {
const response = await fetchWithAuth(SATELLITE_COMPANY_SEARCH_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ companyTypeCode, companyName, page, limit }),
});
@ -289,10 +290,10 @@ export async function searchSatelliteCompany({ companyTypeCode, companyName, pag
*/
export async function saveSatelliteCompany(params) {
try {
const response = await fetch(SATELLITE_COMPANY_SAVE_ENDPOINT, {
const response = await fetchWithAuth(SATELLITE_COMPANY_SAVE_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(params),
});
@ -313,10 +314,10 @@ export async function saveSatelliteCompany(params) {
*/
export async function fetchSatelliteCompanyDetail(companyNo) {
try {
const response = await fetch(SATELLITE_COMPANY_DETAIL_ENDPOINT, {
const response = await fetchWithAuth(SATELLITE_COMPANY_DETAIL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ companyNo }),
});
@ -345,10 +346,10 @@ export async function fetchSatelliteCompanyDetail(companyNo) {
*/
export async function updateSatelliteCompany(params) {
try {
const response = await fetch(SATELLITE_COMPANY_UPDATE_ENDPOINT, {
const response = await fetchWithAuth(SATELLITE_COMPANY_UPDATE_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(params),
});
@ -372,10 +373,10 @@ export async function updateSatelliteCompany(params) {
*/
export async function searchSatelliteManage({ companyNo, satelliteName, sensorType, page, limit }) {
try {
const response = await fetch(SATELLITE_MANAGE_SEARCH_ENDPOINT, {
const response = await fetchWithAuth(SATELLITE_MANAGE_SEARCH_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ companyNo, satelliteName, sensorType, page, limit }),
});
@ -407,10 +408,10 @@ export async function searchSatelliteManage({ companyNo, satelliteName, sensorTy
*/
export async function saveSatelliteManage(params) {
try {
const response = await fetch(SATELLITE_MANAGE_SAVE_ENDPOINT, {
const response = await fetchWithAuth(SATELLITE_MANAGE_SAVE_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(params),
});
@ -431,10 +432,10 @@ export async function saveSatelliteManage(params) {
*/
export async function fetchSatelliteManageDetail(satelliteManageId) {
try {
const response = await fetch(SATELLITE_MANAGE_DETAIL_ENDPOINT, {
const response = await fetchWithAuth(SATELLITE_MANAGE_DETAIL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ satelliteManageId }),
});
@ -464,10 +465,10 @@ export async function fetchSatelliteManageDetail(satelliteManageId) {
*/
export async function updateSatelliteManage(params) {
try {
const response = await fetch(SATELLITE_MANAGE_UPDATE_ENDPOINT, {
const response = await fetchWithAuth(SATELLITE_MANAGE_UPDATE_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(params),
});

파일 보기

@ -8,6 +8,7 @@
* - 응답 데이터 가공 (ProcessedTrack 형태로 변환)
*/
import useShipStore from '../stores/shipStore';
import { fetchWithAuth } from './fetchWithAuth';
/** API 엔드포인트 (메인 프로젝트와 동일) */
const API_ENDPOINT = '/api/v2/tracks/vessels';
@ -31,10 +32,9 @@ export async function fetchTrackQuery({ startTime, endTime, vessels, isIntegrati
isIntegration: isIntegration ? '1' : '0',
};
const response = await fetch(API_ENDPOINT, {
const response = await fetchWithAuth(API_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(body),
});

파일 보기

@ -1,6 +1,7 @@
/**
* 기상해양 API
*/
import { fetchWithAuth } from './fetchWithAuth';
const SPECIAL_NEWS_ENDPOINT = '/api/gis/weather/special-news/search';
@ -16,10 +17,10 @@ const SPECIAL_NEWS_ENDPOINT = '/api/gis/weather/special-news/search';
*/
export async function fetchWeatherAlerts({ startPresentationDate, endPresentationDate, page, limit }) {
try {
const response = await fetch(SPECIAL_NEWS_ENDPOINT, {
const response = await fetchWithAuth(SPECIAL_NEWS_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
startPresentationDate,
endPresentationDate,
@ -59,10 +60,10 @@ const TYPHOON_DETAIL_ENDPOINT = '/api/gis/weather/typhoon/search';
*/
export async function fetchTyphoonList({ typhoonBeginningYear, typhoonBeginningMonth, page, limit }) {
try {
const response = await fetch(TYPHOON_LIST_ENDPOINT, {
const response = await fetchWithAuth(TYPHOON_LIST_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
typhoonBeginningYear,
typhoonBeginningMonth,
@ -100,10 +101,10 @@ export async function fetchTyphoonList({ typhoonBeginningYear, typhoonBeginningM
*/
export async function fetchTyphoonDetail({ typhoonSequence, year }) {
try {
const response = await fetch(TYPHOON_DETAIL_ENDPOINT, {
const response = await fetchWithAuth(TYPHOON_DETAIL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
typhoonSequence,
year,
@ -135,10 +136,10 @@ const SUNRISE_SUNSET_DETAIL_ENDPOINT = '/api/gis/weather/tide-information/observ
*/
export async function fetchTideInformation() {
try {
const response = await fetch(TIDE_INFORMATION_ENDPOINT, {
const response = await fetchWithAuth(TIDE_INFORMATION_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({}),
});
@ -171,10 +172,10 @@ export async function fetchTideInformation() {
*/
export async function fetchSunriseSunsetDetail(params) {
try {
const response = await fetch(SUNRISE_SUNSET_DETAIL_ENDPOINT, {
const response = await fetchWithAuth(SUNRISE_SUNSET_DETAIL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(params),
});
@ -201,10 +202,10 @@ const OBSERVATORY_DETAIL_ENDPOINT = '/api/gis/weather/observatory/select/detail/
*/
export async function fetchObservatoryList() {
try {
const response = await fetch(OBSERVATORY_ENDPOINT, {
const response = await fetchWithAuth(OBSERVATORY_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({}),
});
@ -239,10 +240,10 @@ const AIRPORT_DETAIL_ENDPOINT = '/api/gis/weather/airport/select';
*/
export async function fetchAirportList() {
try {
const response = await fetch(AIRPORT_ENDPOINT, {
const response = await fetchWithAuth(AIRPORT_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({}),
});
@ -267,10 +268,10 @@ export async function fetchAirportList() {
*/
export async function fetchAirportDetail({ airportId }) {
try {
const response = await fetch(AIRPORT_DETAIL_ENDPOINT, {
const response = await fetchWithAuth(AIRPORT_DETAIL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ airportId }),
});
@ -288,10 +289,10 @@ export async function fetchAirportDetail({ airportId }) {
export async function fetchObservatoryDetail({ observatoryId, toDate }) {
try {
const response = await fetch(OBSERVATORY_DETAIL_ENDPOINT, {
const response = await fetchWithAuth(OBSERVATORY_DETAIL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ observatoryId, toDate }),
});

파일 보기

@ -5,6 +5,7 @@
* 응답 변환: trackQueryApi.convertToProcessedTracks() 재사용
*/
import { convertToProcessedTracks } from '../../tracking/services/trackQueryApi';
import { fetchWithAuth } from '../../api/fetchWithAuth';
const API_ENDPOINT = '/api/v2/tracks/area-search';
@ -62,10 +63,9 @@ export async function fetchAreaSearch(params) {
polygons: params.polygons,
};
const response = await fetch(API_ENDPOINT, {
const response = await fetchWithAuth(API_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(request),
});

파일 보기

@ -5,6 +5,7 @@
* 응답 변환: trackQueryApi.convertToProcessedTracks() 재사용
*/
import { convertToProcessedTracks } from '../../tracking/services/trackQueryApi';
import { fetchWithAuth } from '../../api/fetchWithAuth';
const API_ENDPOINT = '/api/v2/tracks/vessel-contacts';
@ -37,10 +38,9 @@ export async function fetchVesselContacts(params) {
maxContactDistanceMeters: params.maxContactDistanceMeters,
};
const response = await fetch(API_ENDPOINT, {
const response = await fetchWithAuth(API_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(request),
});

파일 보기

@ -0,0 +1,36 @@
import { useEffect } from 'react';
import { useAuthStore } from '../../stores/authStore';
import useShipStore from '../../stores/shipStore';
import { fetchUserFilter } from '../../api/userSettingApi';
import './SessionGuard.scss';
export default function SessionGuard({ children }) {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const isChecking = useAuthStore((s) => s.isChecking);
useEffect(() => {
const result = useAuthStore.getState().checkSession();
if (!result.valid) {
window.location.href = import.meta.env.VITE_MAIN_APP_URL || '/';
return;
}
//
fetchUserFilter()
.then((filterArray) => {
if (filterArray) {
useShipStore.getState().applyFilterSettings(filterArray);
}
})
.catch((err) => console.warn('[SessionGuard] 필터 로드 실패, 기본값 사용:', err));
}, []);
if (isChecking || !isAuthenticated) {
return (
<div className="session-guard-loading">
{!isChecking && <p>로그인 페이지로 이동합니다...</p>}
</div>
);
}
return children;
}

파일 보기

@ -0,0 +1,9 @@
.session-guard-loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #1a1a2e;
color: #fff;
font-size: 14px;
}

파일 보기

@ -1,6 +1,7 @@
import { useRef } from 'react';
import { Link } from 'react-router-dom';
import { showAlert } from '../common/AlertModal';
import { useAuthStore } from '../../stores/authStore';
const SAMPLE_ALERTS = [
{ message: '서버 응답 시간이 초과되었습니다.\n잠시 후 다시 시도해 주세요.', errorCode: 'ERR_TIMEOUT_504' },
@ -14,6 +15,7 @@ const SAMPLE_ALERTS = [
* - 로고, 알람, 설정(드롭다운), 마이페이지
*/
export default function Header() {
const user = useAuthStore((s) => s.user);
const alertIndexRef = useRef(0);
const handleAlarmClick = (e) => {
@ -33,6 +35,11 @@ export default function Header() {
</div>
<aside>
<ul>
{user?.shipName && (
<li className="ship-info">
<span>{user.shipName}</span>
</li>
)}
<li>
<a href="#" className="alram" title="알람" onClick={handleAlarmClick}>
<i className="badge"></i>

85
src/stores/authStore.js Normal file
파일 보기

@ -0,0 +1,85 @@
import { create } from 'zustand';
import { SESSION_TIMEOUT_MS, KCGV_GROUP_IDS } from '../types/constants';
/** 로컬 개발 모의 사용자 (포트가 달라 localStorage 공유 불가) */
const DEV_MOCK_USER = {
userName: 'DevUser',
userId: 'dev',
groupId: '2',
loginDate: Date.now(),
accountNo: '0',
accountRoll: 'USER',
lat: '34.75',
lon: '128.03',
shipName: '개발함정',
targetId: 'DEV001',
};
/**
* localStorage에서 사용자 정보 읽기
* 메인 프로젝트(mda-react-front) 로그인 저장된
*/
function readUserFromStorage() {
const userName = localStorage.getItem('userName');
const userId = localStorage.getItem('userId');
const groupId = localStorage.getItem('groupId');
const loginDate = localStorage.getItem('loginDate');
if (!userName || !userId || !groupId || !loginDate) {
// 로컬 개발: 세션 없으면 모의 사용자 자동 주입
if (import.meta.env.VITE_DEV_SKIP_AUTH === 'true') {
console.warn('[AuthStore] 개발 모드: 모의 사용자로 자동 로그인');
return DEV_MOCK_USER;
}
return null;
}
return {
userName,
userId,
groupId,
loginDate: Number(loginDate),
accountNo: localStorage.getItem('accountNo'),
accountRoll: localStorage.getItem('accountRoll'),
lat: localStorage.getItem('lat'),
lon: localStorage.getItem('lon'),
shipName: localStorage.getItem('shipName'),
targetId: localStorage.getItem('targetId'),
};
}
export const useAuthStore = create((set) => ({
user: null,
isAuthenticated: false,
isChecking: true,
checkSession: () => {
const user = readUserFromStorage();
if (!user) {
set({ isChecking: false });
return { valid: false, reason: 'NO_USER' };
}
if (Date.now() - user.loginDate > SESSION_TIMEOUT_MS) {
set({ isChecking: false });
return { valid: false, reason: 'TIMEOUT' };
}
if (!KCGV_GROUP_IDS.includes(user.groupId)) {
set({ isChecking: false });
return { valid: false, reason: 'NOT_KCGV' };
}
// 세션 갱신 (슬라이딩 윈도우)
localStorage.setItem('loginDate', String(Date.now()));
set({ user, isAuthenticated: true, isChecking: false });
return { valid: true };
},
handleSessionExpired: () => {
localStorage.clear();
set({ user: null, isAuthenticated: false });
window.location.href = import.meta.env.VITE_MAIN_APP_URL || '/';
},
}));

파일 보기

@ -16,10 +16,9 @@ export default ({ mode, command }) => {
const isProd = !isLocalDev && !isDev && !isQA; // production 또는 기타 모드
const isBuild = command === 'build';
// 배포 경로 설정 (예: '/kcgv/', '/' 등)
// 로컬 개발 모드(development)에서는 항상 '/' 사용
// 빌드 모드(dev, qa, prod)에서는 VITE_BASE_URL 사용
const base = isLocalDev ? '/' : (env.VITE_BASE_URL || '/');
// 배포 경로 설정 (예: '/kcgnv/', '/' 등)
// 모든 모드에서 VITE_BASE_URL 사용 (로컬 개발 프록시 모드 지원)
const base = env.VITE_BASE_URL || '/';
console.log(`[Vite] Mode: ${mode}, Command: ${command}, Base: ${base}, isLocalDev: ${isLocalDev}`);
@ -77,7 +76,19 @@ export default ({ mode, command }) => {
changeOrigin: true,
secure: false,
},
// API 서버
// 공통 API (개인설정, 공통코드 등) — 메인 API 서버로 라우팅
'/api/cmn': {
target: env.VITE_API_URL || 'http://10.26.252.39:9090',
changeOrigin: true,
secure: false,
},
// 기상/위성 등 GIS API — 메인 API 서버로 라우팅
'/api/gis': {
target: env.VITE_API_URL || 'http://10.26.252.39:9090',
changeOrigin: true,
secure: false,
},
// API 서버 (기타)
'/api': {
target: env.VITE_TRACK_API || 'http://localhost:8090',
changeOrigin: true,