fix: 외부 환경 호환 (OSM 타일, AIS 프록시, Node 20)

- 내부망 타일서버 → OSM + CartoDB Dark Matter 전환
- 내부망 API 프록시 제거, /snp-api 프록시만 유지
- AIS API CORS 해결 (Vite 프록시 경유)
- useFavoriteData 비활성화 (/api/gis 내부망 의존 제거)
- .node-version: 20 (팀 템플릿 기준), .nvmrc 삭제

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
htlee 2026-02-15 07:02:22 +09:00
부모 b9c0b70e06
커밋 3439407b71
7개의 변경된 파일40개의 추가작업 그리고 197개의 파일을 삭제

파일 보기

@ -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

1
.nvmrc
파일 보기

@ -1 +0,0 @@
v22.22.0

파일 보기

@ -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,
},