Compare commits
No commits in common. "main" and "fix/ci-workflow-and-lint" have entirely different histories.
main
...
fix/ci-wor
@ -1,25 +0,0 @@
|
|||||||
# Release Notes
|
|
||||||
|
|
||||||
이 문서는 [Keep a Changelog](https://keepachangelog.com/ko/1.0.0/) 형식을 따릅니다.
|
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
## [2026-03-31.2]
|
|
||||||
|
|
||||||
### 추가
|
|
||||||
- 선박 신호 수신 중 지도 오버레이 + 스피너 로딩 인디케이터 표시
|
|
||||||
|
|
||||||
### 변경
|
|
||||||
- 초기 뷰포트를 대한민국 전역 중심(zoom 7)으로 변경
|
|
||||||
- deck.gl 선박 툴팁 배경색을 흰색 계통으로 변경하여 가독성 개선
|
|
||||||
|
|
||||||
## [2026-03-31]
|
|
||||||
|
|
||||||
### 수정
|
|
||||||
- CI/CD 배포 워크플로우 개선: 배포 디렉토리 자동 생성(mkdir -p), lint 검증 단계 추가
|
|
||||||
- ESLint 에러 수정: useEffect 내 직접 setState 호출 제거, 렌더 중 ref 업데이트를 useEffect로 이동
|
|
||||||
|
|
||||||
### 기타
|
|
||||||
- 팀 워크플로우 초기 구성: CLAUDE.md, .sdkmanrc(Java 21), .node-version(24) 추가
|
|
||||||
- settings.json hooks 섹션 추가, workflow-version.json gitea_url 필드 추가
|
|
||||||
- NEXUS_NPM_AUTH 시크릿 등록
|
|
||||||
@ -63,11 +63,11 @@ body {
|
|||||||
|
|
||||||
/* deck.gl tooltip */
|
/* deck.gl tooltip */
|
||||||
.deck-tooltip{
|
.deck-tooltip{
|
||||||
background:rgba(255,255,255,.97)!important;
|
background:rgba(26,58,74,.96)!important;
|
||||||
color:#1a2e38!important;
|
color:#fff!important;
|
||||||
border:1px solid #d4dde2!important;
|
border:1px solid rgba(0,147,178,.35)!important;
|
||||||
border-radius:6px!important;
|
border-radius:6px!important;
|
||||||
box-shadow:0 4px 16px rgba(0,0,0,.15)!important;
|
box-shadow:0 6px 20px rgba(0,0,0,.35)!important;
|
||||||
padding:7px 9px!important;
|
padding:7px 9px!important;
|
||||||
font-size:12px!important;
|
font-size:12px!important;
|
||||||
line-height:1.35!important;
|
line-height:1.35!important;
|
||||||
|
|||||||
@ -30,8 +30,6 @@ const MapViewML: React.FC<MapViewMLProps> = ({ children }) => {
|
|||||||
const [scaleText, setScaleText] = React.useState('1:50,000');
|
const [scaleText, setScaleText] = React.useState('1:50,000');
|
||||||
const [zoom, setZoom] = React.useState(10);
|
const [zoom, setZoom] = React.useState(10);
|
||||||
|
|
||||||
const aisLoading = useStore((s) => s.aisLoading);
|
|
||||||
const aisEmpty = useStore((s) => s.aisTargets.size === 0);
|
|
||||||
const photoModal = useStore((s) => s.photoModal);
|
const photoModal = useStore((s) => s.photoModal);
|
||||||
const openPhotoModal = useStore((s) => s.openPhotoModal);
|
const openPhotoModal = useStore((s) => s.openPhotoModal);
|
||||||
const closePhotoModal = useStore((s) => s.closePhotoModal);
|
const closePhotoModal = useStore((s) => s.closePhotoModal);
|
||||||
@ -99,20 +97,6 @@ const MapViewML: React.FC<MapViewMLProps> = ({ children }) => {
|
|||||||
<ScaleBar text={`${scaleText} ━━━ 5 km`} />
|
<ScaleBar text={`${scaleText} ━━━ 5 km`} />
|
||||||
<MapLegend />
|
<MapLegend />
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{(aisLoading || aisEmpty) && (
|
|
||||||
<>
|
|
||||||
<div style={{ position: 'absolute', inset: 0, background: 'rgba(255,255,255,.45)', zIndex: 15, pointerEvents: 'none' }} />
|
|
||||||
<div style={{ position: 'absolute', top: 12, left: '50%', transform: 'translateX(-50%)', background: 'rgba(26,58,74,.88)', color: '#fff', padding: '8px 20px', borderRadius: 6, fontSize: 13, fontWeight: 500, display: 'flex', alignItems: 'center', gap: 10, zIndex: 20, boxShadow: '0 2px 8px rgba(0,0,0,.25)', pointerEvents: 'none' }}>
|
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" style={{ animation: 'spin 1s linear infinite' }}>
|
|
||||||
<circle cx="12" cy="12" r="10" fill="none" stroke="rgba(255,255,255,.3)" strokeWidth="3" />
|
|
||||||
<path d="M12 2a10 10 0 0 1 10 10" fill="none" stroke="#fff" strokeWidth="3" strokeLinecap="round" />
|
|
||||||
</svg>
|
|
||||||
선박 신호 수신 중...
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CoordStatusBar coord={coord} zoom={zoom} scale={scaleText} />
|
<CoordStatusBar coord={coord} zoom={zoom} scale={scaleText} />
|
||||||
|
|
||||||
{photoModal && (
|
{photoModal && (
|
||||||
|
|||||||
@ -10,7 +10,6 @@ const RETENTION_MS = 120 * 60 * 1000;
|
|||||||
|
|
||||||
export function useAisPolling() {
|
export function useAisPolling() {
|
||||||
const setAisTargets = useStore((s) => s.setAisTargets);
|
const setAisTargets = useStore((s) => s.setAisTargets);
|
||||||
const setAisLoading = useStore((s) => s.setAisLoading);
|
|
||||||
const storeRef = useRef(useStore.getState);
|
const storeRef = useRef(useStore.getState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -20,7 +19,6 @@ export function useAisPolling() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ac = new AbortController();
|
const ac = new AbortController();
|
||||||
let timer: ReturnType<typeof setInterval> | null = null;
|
let timer: ReturnType<typeof setInterval> | null = null;
|
||||||
let isInitial = true;
|
|
||||||
|
|
||||||
function mergeAndPrune(incoming: AisTarget[]) {
|
function mergeAndPrune(incoming: AisTarget[]) {
|
||||||
const prev = storeRef.current().aisTargets;
|
const prev = storeRef.current().aisTargets;
|
||||||
@ -45,18 +43,12 @@ export function useAisPolling() {
|
|||||||
|
|
||||||
async function poll(minutes: number) {
|
async function poll(minutes: number) {
|
||||||
try {
|
try {
|
||||||
if (isInitial) setAisLoading(true);
|
|
||||||
const targets = await fetchRecentPositions(minutes, ac.signal);
|
const targets = await fetchRecentPositions(minutes, ac.signal);
|
||||||
mergeAndPrune(targets);
|
mergeAndPrune(targets);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if ((err as Error).name !== 'AbortError') {
|
if ((err as Error).name !== 'AbortError') {
|
||||||
console.warn('[AIS] Poll failed:', err);
|
console.warn('[AIS] Poll failed:', err);
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
if (isInitial) {
|
|
||||||
setAisLoading(false);
|
|
||||||
isInitial = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,5 +61,5 @@ export function useAisPolling() {
|
|||||||
ac.abort();
|
ac.abort();
|
||||||
if (timer) clearInterval(timer);
|
if (timer) clearInterval(timer);
|
||||||
};
|
};
|
||||||
}, [setAisTargets, setAisLoading]);
|
}, [setAisTargets]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,8 +28,6 @@ interface AppState {
|
|||||||
// AIS 실시간 데이터
|
// AIS 실시간 데이터
|
||||||
aisTargets: Map<number, AisTarget>;
|
aisTargets: Map<number, AisTarget>;
|
||||||
setAisTargets: (targets: Map<number, AisTarget>) => void;
|
setAisTargets: (targets: Map<number, AisTarget>) => void;
|
||||||
aisLoading: boolean;
|
|
||||||
setAisLoading: (loading: boolean) => void;
|
|
||||||
|
|
||||||
// 선박 사진 모달
|
// 선박 사진 모달
|
||||||
photoModal: { imo: number; name: string; imagePath: string; imageCount: number } | null;
|
photoModal: { imo: number; name: string; imagePath: string; imageCount: number } | null;
|
||||||
@ -74,8 +72,6 @@ export const useStore = create<AppState>((set) => ({
|
|||||||
|
|
||||||
aisTargets: new Map(),
|
aisTargets: new Map(),
|
||||||
setAisTargets: (targets) => set({ aisTargets: targets }),
|
setAisTargets: (targets) => set({ aisTargets: targets }),
|
||||||
aisLoading: true,
|
|
||||||
setAisLoading: (loading) => set({ aisLoading: loading }),
|
|
||||||
|
|
||||||
photoModal: null,
|
photoModal: null,
|
||||||
openPhotoModal: (info) => set({ photoModal: info }),
|
openPhotoModal: (info) => set({ photoModal: info }),
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import type maplibregl from 'maplibre-gl';
|
import type maplibregl from 'maplibre-gl';
|
||||||
|
|
||||||
/** 대한민국 전역 중심 좌표 [lng, lat] */
|
/** 인천 해역 기본 중심 좌표 [lng, lat] */
|
||||||
export const DEFAULT_CENTER: [number, number] = [127.8, 35.5];
|
export const DEFAULT_CENTER: [number, number] = [126.7052, 37.4563];
|
||||||
|
|
||||||
/** 기본 줌 레벨 (대한민국 전역 표시) */
|
/** 기본 줌 레벨 */
|
||||||
export const DEFAULT_ZOOM = 7;
|
export const DEFAULT_ZOOM = 10;
|
||||||
|
|
||||||
/** Martin ENC 타일 서버 프록시 경로 */
|
/** Martin ENC 타일 서버 프록시 경로 */
|
||||||
export const MARTIN_BASE_PATH = '/martin';
|
export const MARTIN_BASE_PATH = '/martin';
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user