release: Phase 3 완료 (React 19 + MapLibre GL JS 전환) #2
@ -6,8 +6,9 @@
|
||||
# 배포 경로
|
||||
VITE_BASE_URL=/
|
||||
|
||||
# API 서버 (SNP-Batch API)
|
||||
VITE_API_URL=http://211.208.115.83:8041/snp-api
|
||||
# API 서버 — 로컬 개발은 Vite 프록시 사용 (/snp-api → 211.208.115.83:8041)
|
||||
# 빈 값으로 설정하여 .env의 절대 URL을 override → aisTargetApi 기본값 /snp-api 사용
|
||||
VITE_API_URL=
|
||||
|
||||
# 선박 데이터 쓰로틀링 (ms, 0=무제한)
|
||||
VITE_SHIP_THROTTLE=0
|
||||
|
||||
@ -1 +1 @@
|
||||
24
|
||||
20
|
||||
|
||||
@ -4,7 +4,9 @@
|
||||
*/
|
||||
import axios from 'axios';
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL || '';
|
||||
// dev: Vite 프록시 (/snp-api → 211.208.115.83:8041)
|
||||
// prod: 환경변수로 직접 지정
|
||||
const BASE_URL = import.meta.env.VITE_API_URL || '/snp-api';
|
||||
|
||||
/**
|
||||
* AIS 타겟 검색 (최근 N분 데이터)
|
||||
|
||||
@ -1,40 +1,10 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { fetchFavoriteShips, fetchRealms } from '../api/favoriteApi';
|
||||
import useFavoriteStore from '../stores/favoriteStore';
|
||||
|
||||
/**
|
||||
* 관심선박 + 관심구역 데이터 로딩 훅
|
||||
* MapContainer에서 1회 호출
|
||||
*
|
||||
* 비활성화: 내부망 API(/api/gis) 접근 불가 (민간화)
|
||||
* TODO: 외부 API 연동 시 복원
|
||||
*/
|
||||
export default function useFavoriteData() {
|
||||
const loaded = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded.current) return;
|
||||
loaded.current = true;
|
||||
|
||||
const load = async () => {
|
||||
const [ships, realms] = await Promise.allSettled([
|
||||
fetchFavoriteShips(),
|
||||
fetchRealms(),
|
||||
]);
|
||||
|
||||
const shipList = ships.status === 'fulfilled' ? ships.value : [];
|
||||
const realmList = realms.status === 'fulfilled' ? realms.value : [];
|
||||
|
||||
if (ships.status === 'rejected') {
|
||||
console.warn('[useFavoriteData] 관심선박 로드 실패:', ships.reason);
|
||||
}
|
||||
if (realms.status === 'rejected') {
|
||||
console.warn('[useFavoriteData] 관심구역 로드 실패:', realms.reason);
|
||||
}
|
||||
|
||||
useFavoriteStore.getState().setFavoriteList(shipList);
|
||||
useFavoriteStore.getState().setRealmList(realmList);
|
||||
|
||||
console.log(`[useFavoriteData] 관심선박 ${shipList.length}건, 관심구역 ${realmList.length}건 로드`);
|
||||
};
|
||||
|
||||
load();
|
||||
}, []);
|
||||
// noop — 내부망 /api/gis 의존 제거
|
||||
}
|
||||
|
||||
@ -1,106 +1,22 @@
|
||||
/**
|
||||
* 베이스맵 레이어 설정
|
||||
* - 메인 프로젝트(mda-react-front)의 mapLayer.ts 참조
|
||||
* - 민간화: 내부망 타일서버 → OSM 타일로 임시 전환
|
||||
* - Phase 3에서 MapLibre GL JS 벡터맵으로 최종 전환 예정
|
||||
*/
|
||||
import { XYZ } from 'ol/source';
|
||||
import { transformExtent } from 'ol/proj';
|
||||
import WebGLTileLayer from 'ol/layer/WebGLTile';
|
||||
import { XYZ, OSM } from 'ol/source';
|
||||
import TileLayer from 'ol/layer/Tile';
|
||||
|
||||
// 좌표계 상수
|
||||
const EPSG_3857 = 'EPSG:3857';
|
||||
const EPSG_4326 = 'EPSG:4326';
|
||||
|
||||
// 1x1 투명 PNG (타일 로드 실패 시 대체용)
|
||||
const TRANSPARENT_PIXEL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
||||
const DARK_TILE_URL = 'https://{a-d}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png';
|
||||
|
||||
/**
|
||||
* 타일 로드 함수 (fetch 사용으로 콘솔 에러 완전 방지)
|
||||
* - 404/네트워크 에러 시 투명 이미지로 대체
|
||||
* - 브라우저 네트워크 탭에는 표시되지만 콘솔 에러는 없음
|
||||
*/
|
||||
function silentTileLoadFunction(imageTile, src) {
|
||||
const img = imageTile.getImage();
|
||||
|
||||
fetch(src)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Tile not found');
|
||||
}
|
||||
return response.blob();
|
||||
})
|
||||
.then(blob => {
|
||||
img.src = URL.createObjectURL(blob);
|
||||
})
|
||||
.catch(() => {
|
||||
img.src = TRANSPARENT_PIXEL;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이어 설정
|
||||
* 레이어 설정 (하위 모듈 호환용 export)
|
||||
*/
|
||||
export const mapLayerConfig = {
|
||||
// 일반지도 (줌 0-11)
|
||||
worldLayer: {
|
||||
source: new XYZ({
|
||||
url: '/MAPS/WORLD_webp/{z}/{x}/{y}.webp',
|
||||
minZoom: 0,
|
||||
maxZoom: 11,
|
||||
attributions: 'ⓒ OpenStreetMap',
|
||||
tileLoadFunction: silentTileLoadFunction,
|
||||
}),
|
||||
preload: Infinity,
|
||||
},
|
||||
|
||||
// 전자해도 (줌 0-11) - URL은 임시로 worldLayer와 동일
|
||||
encLayer: {
|
||||
source: new XYZ({
|
||||
url: '/MAPS/WORLD_webp/{z}/{x}/{y}.webp',
|
||||
minZoom: 0,
|
||||
maxZoom: 11,
|
||||
attributions: 'ⓒ OpenStreetMap',
|
||||
tileLoadFunction: silentTileLoadFunction,
|
||||
}),
|
||||
preload: Infinity,
|
||||
},
|
||||
|
||||
// 야간지도 (줌 5-15, 타일은 6-11레벨까지만 로드 → 12+ 확대 표시)
|
||||
darkLayer: {
|
||||
source: new XYZ({
|
||||
url: '/MAPS/SIMPLE_B_webp/{z}/{x}/{y}.webp',
|
||||
minZoom: 6,
|
||||
maxZoom: 11, // 타일은 11레벨까지만 로드 (12+ 는 11레벨 타일 확대)
|
||||
tileLoadFunction: silentTileLoadFunction,
|
||||
url: DARK_TILE_URL,
|
||||
maxZoom: 19,
|
||||
}),
|
||||
preload: Infinity,
|
||||
},
|
||||
|
||||
// 동아시아 상세 (줌 12-15, 타일은 12레벨까지만 로드 → 13+ 확대 표시)
|
||||
eastAsiaLayer: {
|
||||
source: new XYZ({
|
||||
url: '/MAPS/EAST_ASIA_webp/{z}/{x}/{y}.webp',
|
||||
minZoom: 12,
|
||||
maxZoom: 12, // 타일은 12레벨까지만 로드 (13+ 는 12레벨 타일 확대)
|
||||
tileLoadFunction: silentTileLoadFunction,
|
||||
}),
|
||||
preload: 0,
|
||||
minZoom: 12,
|
||||
zIndex: 1,
|
||||
extent: transformExtent([110, 20, 140, 45], EPSG_4326, EPSG_3857),
|
||||
},
|
||||
|
||||
// 한국 상세 (줌 16-17)
|
||||
korLayer: {
|
||||
source: new XYZ({
|
||||
url: '/MAPS/KOR_webp/{z}/{x}/{y}.webp',
|
||||
minZoom: 16,
|
||||
maxZoom: 17,
|
||||
tileLoadFunction: silentTileLoadFunction,
|
||||
}),
|
||||
preload: Infinity,
|
||||
minZoom: 16,
|
||||
zIndex: 1,
|
||||
extent: transformExtent([124, 32, 133, 39], EPSG_4326, EPSG_3857),
|
||||
},
|
||||
};
|
||||
|
||||
@ -109,30 +25,31 @@ export const mapLayerConfig = {
|
||||
* @param {string} baseMapType - 배경지도 타입 ('normal' | 'enc' | 'dark')
|
||||
*/
|
||||
export const createBaseLayers = (baseMapType = 'dark') => {
|
||||
// 3가지 배경지도 레이어 생성
|
||||
const worldMap = new WebGLTileLayer({
|
||||
...mapLayerConfig.worldLayer,
|
||||
// OSM 기반 일반지도
|
||||
const worldMap = new TileLayer({
|
||||
source: new OSM(),
|
||||
visible: baseMapType === 'normal',
|
||||
});
|
||||
const encMap = new WebGLTileLayer({
|
||||
...mapLayerConfig.encLayer,
|
||||
|
||||
// ENC (일반지도와 동일 — 임시)
|
||||
const encMap = new TileLayer({
|
||||
source: new OSM(),
|
||||
visible: baseMapType === 'enc',
|
||||
});
|
||||
const darkMap = new WebGLTileLayer({
|
||||
...mapLayerConfig.darkLayer,
|
||||
|
||||
// 야간 모드 (CartoDB Dark Matter)
|
||||
const darkMap = new TileLayer({
|
||||
source: new XYZ({
|
||||
url: DARK_TILE_URL,
|
||||
attributions: '© OpenStreetMap contributors, © CARTO',
|
||||
maxZoom: 19,
|
||||
}),
|
||||
visible: baseMapType === 'dark',
|
||||
});
|
||||
|
||||
// 상세 지도 레이어 (일반지도/전자해도 전용, 야간지도에서는 숨김)
|
||||
const isDarkMode = baseMapType === 'dark';
|
||||
const eastAsiaMap = new WebGLTileLayer({
|
||||
...mapLayerConfig.eastAsiaLayer,
|
||||
visible: !isDarkMode,
|
||||
});
|
||||
const korMap = new WebGLTileLayer({
|
||||
...mapLayerConfig.korLayer,
|
||||
visible: !isDarkMode,
|
||||
});
|
||||
// 상세 레이어는 OSM이 자체 커버하므로 빈 레이어로 대체
|
||||
const eastAsiaMap = new TileLayer({ visible: false });
|
||||
const korMap = new TileLayer({ visible: false });
|
||||
|
||||
return {
|
||||
worldMap,
|
||||
|
||||
@ -42,55 +42,9 @@ export default ({ mode, command }) => {
|
||||
host: true,
|
||||
port: 3000,
|
||||
proxy: {
|
||||
// 지도 타일 서버
|
||||
'/MAPS': {
|
||||
target: env.VITE_MAP_TILE_URL || 'http://10.26.252.39:9090',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
// GeoJSON 데이터
|
||||
'/geo': {
|
||||
target: env.VITE_MAP_TILE_URL || 'http://10.26.252.39:9090',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
// 선박 신호 API (signal-api)
|
||||
// 참조: mda-react-front/vite.config.ts
|
||||
'/signal-api': {
|
||||
target: env.VITE_SIGNAL_API || 'http://10.26.252.39:9090/signal-api',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: (path) => path.replace(/^\/signal-api/, ''),
|
||||
},
|
||||
// 선박 이미지 (국기, 선종 아이콘)
|
||||
// 참조: mda-react-front/vite.config.ts - /ship/image 프록시
|
||||
'/ship/image': {
|
||||
target: env.VITE_API_URL || 'http://10.26.252.39:9090',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
// 항적 조회 API (별도 서버)
|
||||
// 참조: mda-react-front/vite.config.ts - /api/v2/tracks 프록시
|
||||
'/api/v2/tracks': {
|
||||
target: env.VITE_TRACK_API || 'http://10.26.252.51:8090',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
// 공통 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',
|
||||
// SNP-Batch AIS API (선박 위치 데이터)
|
||||
'/snp-api': {
|
||||
target: env.VITE_SNP_API_TARGET || 'http://211.208.115.83:8041',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user