feat: 해경관할구역 FGB 레이어 + 필터 개인설정 영속화 (AI모드/위험물)

- useCoastGuardLayer: flatgeobuf 해경관할구역 레이어 (테마별 스타일)
- userSettingApi: 필터 개인설정 저장/불러오기 API
- applyFilterSettings/buildFilterSettings에 AI모드(6개 서브) + 위험물 추가
- AI모드 전체 토글: 선종/국적/신호와 동일 every 패턴으로 통일
- DisplayComponent: AI모드/위험물/해경관할구역/관심구역 토글 바인딩
- 해경관할구역 기본값 ON

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
LHT 2026-02-12 13:54:46 +09:00
부모 059b0670fc
커밋 8ccb261d65
29개의 변경된 파일663개의 추가작업 그리고 76개의 파일을 삭제

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

파일 보기

@ -23,6 +23,7 @@
"@stomp/stompjs": "^7.2.1",
"axios": "^1.4.0",
"dayjs": "^1.11.11",
"flatgeobuf": "^4.4.0",
"html2canvas": "^1.4.1",
"ol": "^9.2.4",
"ol-ext": "^4.0.10",

Binary file not shown.

32
src/api/userSettingApi.js Normal file
파일 보기

@ -0,0 +1,32 @@
import { fetchWithAuth } from './fetchWithAuth';
import { USER_SETTING_FILTER } from '../types/constants';
const SEARCH_ENDPOINT = '/api/cmn/personal/settings/search';
const SAVE_ENDPOINT = '/api/cmn/personal/settings/save';
/**
* 필터 설정 조회
* @returns {Promise<Array|null>} 설정 배열 또는 null (저장된 설정 없음)
*/
export async function fetchUserFilter() {
const url = `${SEARCH_ENDPOINT}?type=${USER_SETTING_FILTER}`;
const response = await fetchWithAuth(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
if (result?.code === 4006) return null;
return result?.data?.[USER_SETTING_FILTER] || null;
}
/**
* 필터 설정 저장
* @param {Array<{code: string, value: string}>} settings
*/
export async function saveUserFilter(settings) {
const response = await fetchWithAuth(SAVE_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ settings }),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}

파일 보기

@ -3,6 +3,9 @@ import { Link, useNavigate } from "react-router-dom";
import Slider from '../../common/Slider';
import useShipStore from '../../../stores/shipStore';
import { useMapStore, BASE_MAP_TYPES } from '../../../stores/mapStore';
import { saveUserFilter } from '../../../api/userSettingApi';
import { showToast } from '../../../components/common/Toast';
import useFavoriteStore from '../../../stores/favoriteStore';
import {
SIGNAL_SOURCE_CODE_AIS,
SIGNAL_SOURCE_CODE_ENAV,
@ -25,17 +28,17 @@ import {
NATIONAL_CODE_OTHER,
} from '../../../types/constants';
//
// ( )
const SIGNAL_FILTERS = [
{ code: SIGNAL_SOURCE_CODE_AIS, label: 'AIS' },
{ code: SIGNAL_SOURCE_CODE_ENAV, label: 'E-NAV' },
{ code: SIGNAL_SOURCE_CODE_VPASS, label: 'V-PASS' },
{ code: SIGNAL_SOURCE_CODE_ENAV, label: 'E-NAV' },
{ code: SIGNAL_SOURCE_CODE_VTS_AIS, label: 'VTS_AIS' },
{ code: SIGNAL_SOURCE_CODE_D_MF_HF, label: 'D_MF_HF' },
{ code: SIGNAL_SOURCE_CODE_RADAR, label: 'VTS_RADAR' },
];
//
// ( )
const KIND_FILTERS = [
{ code: SIGNAL_KIND_CODE_FISHING, label: '어선' },
{ code: SIGNAL_KIND_CODE_PASSENGER, label: '여객선' },
@ -43,8 +46,8 @@ const KIND_FILTERS = [
{ code: SIGNAL_KIND_CODE_TANKER, label: '유조선' },
{ code: SIGNAL_KIND_CODE_GOV, label: '관공선' },
{ code: SIGNAL_KIND_CODE_KCGV, label: '함정' },
{ code: SIGNAL_KIND_CODE_NORMAL, label: '기타' },
{ code: SIGNAL_KIND_CODE_BUOY, label: '어망/부이' },
{ code: SIGNAL_KIND_CODE_NORMAL, label: '기타' },
];
//
@ -70,20 +73,35 @@ export default function DisplayComponent({ isOpen, onToggle, initialTab = 'filte
nationalVisibility,
darkSignalVisible,
darkSignalCount,
aiModeVisibility,
hazardVisible,
toggleSourceVisibility,
toggleKindVisibility,
toggleNationalVisibility,
toggleDarkSignalVisible,
toggleAiModeEnabled,
toggleAiModeVisibility,
toggleHazardVisible,
clearDarkSignals,
} = useShipStore();
// /
const isFavoriteEnabled = useFavoriteStore((s) => s.isFavoriteEnabled);
const toggleFavoriteEnabled = useFavoriteStore((s) => s.toggleFavoriteEnabled);
const isRealmVisible = useFavoriteStore((s) => s.isRealmVisible);
const toggleRealmVisible = useFavoriteStore((s) => s.toggleRealmVisible);
//
const isCoastGuardVisible = useMapStore((s) => s.isCoastGuardVisible);
const toggleCoastGuard = useMapStore((s) => s.toggleCoastGuard);
//
const [opacity, setOpacity] = useState(70);
//
const [isAccordionOpen1, setIsAccordionOpen1] = useState(true); //
const [isAccordionOpen2, setIsAccordionOpen2] = useState(true); //
const [isAccordionOpen3, setIsAccordionOpen3] = useState(true); //
const [isAccordionOpen1, setIsAccordionOpen1] = useState(true); //
const [isAccordionOpen2, setIsAccordionOpen2] = useState(true); //
const [isAccordionOpen3, setIsAccordionOpen3] = useState(true); //
const [isAccordionOpen4, setIsAccordionOpen4] = useState(false); // AI
const toggleAccordion1 = () => setIsAccordionOpen1(prev => !prev);
@ -129,6 +147,21 @@ export default function DisplayComponent({ isOpen, onToggle, initialTab = 'filte
});
}, [isAllNationalsOn, nationalVisibility, toggleNationalVisibility]);
// AI On/Off (// )
const isAllAiModeOn = Object.values(aiModeVisibility).every(v => v);
//
const handleSaveFilter = useCallback(async () => {
try {
const settings = useShipStore.getState().buildFilterSettings();
await saveUserFilter(settings);
showToast('필터 설정이 저장되었습니다.');
} catch (err) {
console.error('[Filter] 저장 실패:', err);
showToast('필터 저장에 실패했습니다.');
}
}, []);
// ( )
const [activeTab, setActiveTab] = useState(initialTab);
@ -165,51 +198,7 @@ export default function DisplayComponent({ isOpen, onToggle, initialTab = 'filte
<div className="tabWrapInner">
<div className="tabWrapCnt">
{/* 스위치그룹 01 - 신호 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<span>신호</span>
<label className="switch">
<input
type="checkbox"
aria-label="신호"
checked={isAllSignalsOn}
onChange={toggleAllSignals}
/>
<span></span>
</label>
</div>
<button
type="button"
className={`toggleBtn ${isAccordionOpen1 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen1}
onClick={toggleAccordion1}
></button>
</div>
{/* 여기서부터 토글 */}
<div className={`switchBox ${isAccordionOpen1 ? 'is-open' : ''}`}>
<ul className="switchList">
{SIGNAL_FILTERS.map(({ code, label }) => (
<li key={code}>
<span>{label}</span>
<label className="switch sm">
<input
type="checkbox"
aria-label={label}
checked={sourceVisibility[code] || false}
onChange={() => toggleSourceVisibility(code)}
/>
<span></span>
</label>
</li>
))}
</ul>
</div>
{/* 여기까지 */}
</div>
{/* 스위치그룹 02 - 선종 */}
{/* 스위치그룹 01 - 선종 (메인 프로젝트와 동일 순서) */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
@ -226,13 +215,13 @@ export default function DisplayComponent({ isOpen, onToggle, initialTab = 'filte
</div>
<button
type="button"
className={`toggleBtn ${isAccordionOpen2 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen2}
onClick={toggleAccordion2}
className={`toggleBtn ${isAccordionOpen1 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen1}
onClick={toggleAccordion1}
></button>
</div>
{/* 여기서부터 토글 */}
<div className={`switchBox ${isAccordionOpen2 ? 'is-open' : ''}`}>
<div className={`switchBox ${isAccordionOpen1 ? 'is-open' : ''}`}>
<ul className="switchList">
{KIND_FILTERS.map(({ code, label }) => (
<li key={code}>
@ -253,7 +242,7 @@ export default function DisplayComponent({ isOpen, onToggle, initialTab = 'filte
{/* 여기까지 */}
</div>
{/* 스위치그룹 03 - 국적 */}
{/* 스위치그룹 02 - 국적 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
@ -270,13 +259,13 @@ export default function DisplayComponent({ isOpen, onToggle, initialTab = 'filte
</div>
<button
type="button"
className={`toggleBtn ${isAccordionOpen3 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen3}
onClick={toggleAccordion3}
className={`toggleBtn ${isAccordionOpen2 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen2}
onClick={toggleAccordion2}
></button>
</div>
{/* 여기서부터 토글 */}
<div className={`switchBox ${isAccordionOpen3 ? 'is-open' : ''}`}>
<div className={`switchBox ${isAccordionOpen2 ? 'is-open' : ''}`}>
<ul className="switchList">
{NATIONAL_FILTERS.map(({ code, label }) => (
<li key={code}>
@ -296,12 +285,59 @@ export default function DisplayComponent({ isOpen, onToggle, initialTab = 'filte
</div>
{/* 여기까지 */}
</div>
{/* 스위치그룹 04 */}
{/* 스위치그룹 03 - 신호종류 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<span>신호</span>
<label className="switch">
<input
type="checkbox"
aria-label="신호"
checked={isAllSignalsOn}
onChange={toggleAllSignals}
/>
<span></span>
</label>
</div>
<button
type="button"
className={`toggleBtn ${isAccordionOpen3 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen3}
onClick={toggleAccordion3}
></button>
</div>
{/* 여기서부터 토글 */}
<div className={`switchBox ${isAccordionOpen3 ? 'is-open' : ''}`}>
<ul className="switchList">
{SIGNAL_FILTERS.map(({ code, label }) => (
<li key={code}>
<span>{label}</span>
<label className="switch sm">
<input
type="checkbox"
aria-label={label}
checked={sourceVisibility[code] || false}
onChange={() => toggleSourceVisibility(code)}
/>
<span></span>
</label>
</li>
))}
</ul>
</div>
{/* 여기까지 */}
</div>
{/* 스위치그룹 04 - AI 모드 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<span>AI 모드</span>
<label className="switch"> <input type="checkbox" aria-label="AI 모드" /> <span></span></label>
<label className="switch">
<input type="checkbox" aria-label="AI 모드" checked={isAllAiModeOn} onChange={toggleAiModeEnabled} />
<span></span>
</label>
</div>
<button
type="button"
@ -315,27 +351,45 @@ export default function DisplayComponent({ isOpen, onToggle, initialTab = 'filte
<ul className="switchList">
<li>
<span>MMSI 변조</span>
<label className="switch sm"> <input type="checkbox" aria-label="MMSI 변조" /> <span></span></label>
<label className="switch sm">
<input type="checkbox" aria-label="MMSI 변조" checked={aiModeVisibility.mmsiChange} onChange={() => toggleAiModeVisibility('mmsiChange')} />
<span></span>
</label>
</li>
<li>
<span>중국 허가선박</span>
<label className="switch sm"> <input type="checkbox" aria-label="중국 허가선박" /> <span></span></label>
<label className="switch sm">
<input type="checkbox" aria-label="중국 허가선박" checked={aiModeVisibility.chinaPermission} onChange={() => toggleAiModeVisibility('chinaPermission')} />
<span></span>
</label>
</li>
<li>
<span>관공선</span>
<label className="switch sm"> <input type="checkbox" aria-label="관공선" /> <span></span></label>
<label className="switch sm">
<input type="checkbox" aria-label="관공선" checked={aiModeVisibility.govShip} onChange={() => toggleAiModeVisibility('govShip')} />
<span></span>
</label>
</li>
<li>
<span>비정상 접촉</span>
<label className="switch sm"> <input type="checkbox" aria-label="비정상 접촉" /> <span></span></label>
<label className="switch sm">
<input type="checkbox" aria-label="비정상 접촉" checked={aiModeVisibility.sseZoneContact} onChange={() => toggleAiModeVisibility('sseZoneContact')} />
<span></span>
</label>
</li>
<li>
<span>비정상 선박</span>
<label className="switch sm"> <input type="checkbox" aria-label="비정상 선박" /> <span></span></label>
<label className="switch sm">
<input type="checkbox" aria-label="비정상 선박" checked={aiModeVisibility.nonPermission} onChange={() => toggleAiModeVisibility('nonPermission')} />
<span></span>
</label>
</li>
<li>
<span>북한선박</span>
<label className="switch sm"> <input type="checkbox" aria-label="북한선박" /> <span></span></label>
<label className="switch sm">
<input type="checkbox" aria-label="북한선박" checked={aiModeVisibility.northKoreaAi} onChange={() => toggleAiModeVisibility('northKoreaAi')} />
<span></span>
</label>
</li>
</ul>
</div>
@ -371,13 +425,16 @@ export default function DisplayComponent({ isOpen, onToggle, initialTab = 'filte
</div>
</div>
{/* 스위치그룹 06 */}
{/* 스위치그룹 06 - 위험물 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<span>위험물</span>
</div>
<label className="switch"> <input type="checkbox" aria-label="위험물" /> <span></span></label>
<label className="switch">
<input type="checkbox" aria-label="위험물" checked={hazardVisible} onChange={toggleHazardVisible} />
<span></span>
</label>
</div>
</div>
@ -388,14 +445,14 @@ export default function DisplayComponent({ isOpen, onToggle, initialTab = 'filte
<i className="favship"></i>
<span>관심선박</span>
</div>
<label className="switch"> <input type="checkbox" aria-label="관심선박" /> <span></span></label>
<label className="switch"> <input type="checkbox" aria-label="관심선박" checked={isFavoriteEnabled} onChange={toggleFavoriteEnabled} /> <span></span></label>
</div>
</div>
</div>
{/* 버튼영역 */}
<div className="btnBox">
<button type="button" className="btn btnLine">저장</button>
<button type="button" className="btn btnLine" onClick={handleSaveFilter}>저장</button>
</div>
</div>
</div>
@ -466,7 +523,13 @@ export default function DisplayComponent({ isOpen, onToggle, initialTab = 'filte
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<input type="checkbox" checked={isRealmVisible} onChange={toggleRealmVisible} />
<span>관심구역</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" checked={isCoastGuardVisible} onChange={toggleCoastGuard} />
<span>해경관할구역</span>
</label>
</li>

파일 보기

@ -0,0 +1,160 @@
import { useEffect, useRef } from 'react';
import VectorImageLayer from 'ol/layer/VectorImage';
import VectorSource from 'ol/source/Vector';
import GeoJSON from 'ol/format/GeoJSON';
import { Style, Fill, Stroke, Text } from 'ol/style';
import { deserialize } from 'flatgeobuf/lib/mjs/geojson.js';
import { useMapStore, THEME_TYPES } from '../stores/mapStore';
const BASE_URL = import.meta.env.BASE_URL || '/';
const FGB_URL = `${BASE_URL}fgb/해경관할구역.fgb`;
/** 테마별 색상 정의 */
const THEME_STYLE = {
[THEME_TYPES.DARK]: {
lineColor: 'rgba(100, 200, 255, 0.8)',
textColor: 'rgba(100, 200, 255, 0.9)',
textStrokeColor: 'rgba(0, 0, 0, 0.6)',
textStrokeWidth: 1,
font: 'bold 1.1rem "NanumSquare", sans-serif',
},
[THEME_TYPES.LIGHT]: {
lineColor: 'rgba(20, 60, 100, 0.7)',
textColor: 'rgba(20, 60, 100, 0.8)',
textStrokeColor: 'rgba(255, 255, 255, 0.7)',
textStrokeWidth: 1,
font: 'bold 1.1rem "NanumSquare", sans-serif',
},
};
/**
* 해경관할구역 스타일 팩토리
* 테마에 따라 스타일 함수를 생성
*/
function createKcgAreaStyle(theme) {
const ts = THEME_STYLE[theme] || THEME_STYLE[THEME_TYPES.DARK];
return (feature) => {
const areaName = feature.get('해역명');
const isSpecial = areaName != null && areaName.includes('특별');
if (isSpecial) {
return [
new Style({
stroke: new Stroke({
color: 'rgba(255, 80, 80, 0.8)',
lineDash: [5, 5],
width: 2,
}),
fill: new Fill({ color: 'rgba(255,255,255,0)' }),
text: new Text({
offsetY: -15,
text: areaName || '',
font: ts.font,
fill: new Fill({ color: 'rgba(255, 80, 80, 0.9)' }),
stroke: new Stroke({ color: ts.textStrokeColor, width: ts.textStrokeWidth }),
}),
zIndex: 999,
}),
];
}
return [
new Style({
stroke: new Stroke({ color: ts.lineColor, width: 2 }),
fill: new Fill({ color: 'rgba(255,255,255,0)' }),
text: new Text({
offsetY: -15,
text: areaName || '',
font: ts.font,
fill: new Fill({ color: ts.textColor }),
stroke: new Stroke({ color: ts.textStrokeColor, width: ts.textStrokeWidth }),
}),
}),
];
};
}
/**
* 해경관할구역 FGB 레이어 관리
* 참조: mda-react-front/src/common/targetLayer.ts - kcgWatchZoneLayer, setFGBFeatures
*/
export default function useCoastGuardLayer() {
const map = useMapStore((s) => s.map);
const layerRef = useRef(null);
const loadedRef = useRef(false);
useEffect(() => {
if (!map) return;
const currentTheme = useMapStore.getState().getTheme();
const source = new VectorSource();
const layer = new VectorImageLayer({
source,
zIndex: 45,
style: createKcgAreaStyle(currentTheme),
declutter: true,
visible: useMapStore.getState().isCoastGuardVisible,
});
map.addLayer(layer);
layerRef.current = layer;
// FGB 파일 로드 (1회)
if (!loadedRef.current) {
loadedRef.current = true;
loadFgb(source);
}
// visible 토글 구독
const unsubVisible = useMapStore.subscribe(
(state) => state.isCoastGuardVisible,
(isVisible) => {
if (layerRef.current) {
layerRef.current.setVisible(isVisible);
}
},
);
// 배경지도(테마) 변경 구독 → 스타일 재적용
const unsubTheme = useMapStore.subscribe(
(state) => state.baseMapType,
() => {
if (layerRef.current) {
const theme = useMapStore.getState().getTheme();
layerRef.current.setStyle(createKcgAreaStyle(theme));
}
},
);
return () => {
unsubVisible();
unsubTheme();
if (map && layerRef.current) {
map.removeLayer(layerRef.current);
}
layerRef.current = null;
};
}, [map]);
}
/**
* FlatGeobuf 파일 로드 VectorSource에 Feature 추가
*/
async function loadFgb(source) {
try {
const response = await fetch(FGB_URL);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const format = new GeoJSON();
for await (const geojsonFeature of deserialize(response.body)) {
const feature = format.readFeature(geojsonFeature);
source.addFeature(feature);
}
console.log(`[useCoastGuardLayer] 해경관할구역 ${source.getFeatures().length}건 로드 완료`);
} catch (err) {
console.warn('[useCoastGuardLayer] FGB 로드 실패:', err);
}
}

파일 보기

@ -44,6 +44,9 @@ import AreaSearchTimeline from '../areaSearch/components/AreaSearchTimeline';
import AreaSearchTooltip from '../areaSearch/components/AreaSearchTooltip';
import useMeasure from './measure/useMeasure';
import useTrackingMode from '../hooks/useTrackingMode';
import useFavoriteData from '../hooks/useFavoriteData';
import useRealmLayer from '../hooks/useRealmLayer';
import useCoastGuardLayer from '../hooks/useCoastGuardLayer';
import './measure/measure.scss';
import './MapContainer.scss';
@ -71,6 +74,15 @@ export default function MapContainer() {
// STOMP
useShipData({ autoConnect: true });
// +
useFavoriteData();
// OL
useRealmLayer();
// FGB
useCoastGuardLayer();
// Deck.gl
const { deckRef } = useShipLayer(map);

파일 보기

@ -119,4 +119,8 @@ export const useMapStore = create(subscribeWithSelector((set, get) => ({
[layerName]: !state.layerVisibility[layerName],
},
})),
// 해경관할구역 레이어
isCoastGuardVisible: true,
toggleCoastGuard: () => set((s) => ({ isCoastGuardVisible: !s.isCoastGuardVisible })),
})));

파일 보기

@ -27,6 +27,7 @@ import {
NATIONAL_CODE_OTHER,
SOURCE_PRIORITY_RANK,
SOURCE_TO_ACTIVE_KEY,
USER_SETTING_CODES,
} from '../types/constants';
// =====================
@ -238,6 +239,19 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
/** 마지막 모달 위치 (새 모달 초기 위치 계산용) */
lastModalPos: null,
/** AI 모드 서브 토글 (메인 토글은 컴포넌트에서 every()로 파생 — 선종/국적/신호와 동일 패턴) */
aiModeVisibility: {
mmsiChange: false, // MMSI 변조
chinaPermission: false, // 중국 허가선박
govShip: false, // 관공선
sseZoneContact: false, // 비정상 접촉
nonPermission: false, // 비정상 선박
northKoreaAi: false, // 북한선박
},
/** 위험물 표시 여부 */
hazardVisible: false,
/** 다크시그널(소실신호) 표시 여부 */
darkSignalVisible: false,
@ -456,6 +470,33 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
/**
* 다크시그널 표시 토글
*/
toggleAiModeEnabled: () => {
set((state) => {
const allOn = Object.values(state.aiModeVisibility).every(v => v);
const next = !allOn;
return {
aiModeVisibility: {
mmsiChange: next,
chinaPermission: next,
govShip: next,
sseZoneContact: next,
nonPermission: next,
northKoreaAi: next,
},
};
});
},
toggleAiModeVisibility: (key) => {
set((state) => ({
aiModeVisibility: { ...state.aiModeVisibility, [key]: !state.aiModeVisibility[key] },
}));
},
toggleHazardVisible: () => {
set((state) => ({ hazardVisible: !state.hazardVisible }));
},
toggleDarkSignalVisible: () => {
set((state) => ({
darkSignalVisible: !state.darkSignalVisible,
@ -744,6 +785,103 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
set((state) => ({ showLegend: !state.showLegend }));
},
/**
* 서버에서 불러온 필터 설정 배열을 스토어에 적용
* 참조: mda-react-front/src/common/userSetting.ts
* @param {Array<{settingCode: string, settingValue: string}>} filterArray
*/
applyFilterSettings: (filterArray) => {
if (!Array.isArray(filterArray) || filterArray.length === 0) return;
const toBoolean = (item) => item?.settingValue === 'true';
// settingCode → settingValue 맵 생성
const map = {};
filterArray.forEach((item) => {
if (item?.settingCode) map[item.settingCode] = item;
});
set({
kindVisibility: {
[SIGNAL_KIND_CODE_FISHING]: toBoolean(map[USER_SETTING_CODES.FISHING]),
[SIGNAL_KIND_CODE_PASSENGER]: toBoolean(map[USER_SETTING_CODES.PASS]),
[SIGNAL_KIND_CODE_CARGO]: toBoolean(map[USER_SETTING_CODES.CARGO]),
[SIGNAL_KIND_CODE_TANKER]: toBoolean(map[USER_SETTING_CODES.TANKER]),
[SIGNAL_KIND_CODE_GOV]: toBoolean(map[USER_SETTING_CODES.GOV]),
[SIGNAL_KIND_CODE_KCGV]: toBoolean(map[USER_SETTING_CODES.KCGV]),
[SIGNAL_KIND_CODE_NORMAL]: toBoolean(map[USER_SETTING_CODES.NORMAL]),
[SIGNAL_KIND_CODE_BUOY]: toBoolean(map[USER_SETTING_CODES.BUOY]),
},
nationalVisibility: {
[NATIONAL_CODE_KR]: toBoolean(map[USER_SETTING_CODES.KOREA]),
[NATIONAL_CODE_CN]: toBoolean(map[USER_SETTING_CODES.CHINA]),
[NATIONAL_CODE_JP]: toBoolean(map[USER_SETTING_CODES.JAPAN]),
[NATIONAL_CODE_KP]: toBoolean(map[USER_SETTING_CODES.NORTH_KOREA]),
[NATIONAL_CODE_OTHER]: toBoolean(map[USER_SETTING_CODES.ETC_NATION]),
},
sourceVisibility: {
[SIGNAL_SOURCE_CODE_AIS]: toBoolean(map[USER_SETTING_CODES.AIS]),
[SIGNAL_SOURCE_CODE_VPASS]: toBoolean(map[USER_SETTING_CODES.VPASS]),
[SIGNAL_SOURCE_CODE_ENAV]: toBoolean(map[USER_SETTING_CODES.ENAV]),
[SIGNAL_SOURCE_CODE_VTS_AIS]: toBoolean(map[USER_SETTING_CODES.VTS_AIS]),
[SIGNAL_SOURCE_CODE_D_MF_HF]: toBoolean(map[USER_SETTING_CODES.D_MF_HF]),
[SIGNAL_SOURCE_CODE_RADAR]: toBoolean(map[USER_SETTING_CODES.RADAR]),
},
darkSignalVisible: toBoolean(map[USER_SETTING_CODES.LOST_SIGNAL]),
// AI 모드 (메인 토글은 하위 토글에서 파생 — 서버에 000039가 없을 수 있음)
aiModeVisibility: {
mmsiChange: toBoolean(map[USER_SETTING_CODES.MMSI_CHANGE]),
chinaPermission: toBoolean(map[USER_SETTING_CODES.CHINA_PERMISSION]),
govShip: toBoolean(map[USER_SETTING_CODES.GOV_SHIP]),
sseZoneContact: toBoolean(map[USER_SETTING_CODES.SSE_ZONE_CONTACT]),
nonPermission: toBoolean(map[USER_SETTING_CODES.NON_PERMISSION]),
northKoreaAi: toBoolean(map[USER_SETTING_CODES.NORTH_KOREA_AI]),
},
// 위험물
hazardVisible: toBoolean(map[USER_SETTING_CODES.HAZARD]),
});
},
/**
* 현재 필터 상태를 서버 저장 형식으로 직렬화
* @returns {Array<{code: string, value: string}>}
*/
buildFilterSettings: () => {
const { kindVisibility, sourceVisibility, nationalVisibility, darkSignalVisible, aiModeVisibility, hazardVisible } = get();
return [
{ code: USER_SETTING_CODES.FISHING, value: String(!!kindVisibility[SIGNAL_KIND_CODE_FISHING]) },
{ code: USER_SETTING_CODES.PASS, value: String(!!kindVisibility[SIGNAL_KIND_CODE_PASSENGER]) },
{ code: USER_SETTING_CODES.CARGO, value: String(!!kindVisibility[SIGNAL_KIND_CODE_CARGO]) },
{ code: USER_SETTING_CODES.TANKER, value: String(!!kindVisibility[SIGNAL_KIND_CODE_TANKER]) },
{ code: USER_SETTING_CODES.GOV, value: String(!!kindVisibility[SIGNAL_KIND_CODE_GOV]) },
{ code: USER_SETTING_CODES.KCGV, value: String(!!kindVisibility[SIGNAL_KIND_CODE_KCGV]) },
{ code: USER_SETTING_CODES.NORMAL, value: String(!!kindVisibility[SIGNAL_KIND_CODE_NORMAL]) },
{ code: USER_SETTING_CODES.BUOY, value: String(!!kindVisibility[SIGNAL_KIND_CODE_BUOY]) },
{ code: USER_SETTING_CODES.KOREA, value: String(!!nationalVisibility[NATIONAL_CODE_KR]) },
{ code: USER_SETTING_CODES.CHINA, value: String(!!nationalVisibility[NATIONAL_CODE_CN]) },
{ code: USER_SETTING_CODES.JAPAN, value: String(!!nationalVisibility[NATIONAL_CODE_JP]) },
{ code: USER_SETTING_CODES.NORTH_KOREA, value: String(!!nationalVisibility[NATIONAL_CODE_KP]) },
{ code: USER_SETTING_CODES.ETC_NATION, value: String(!!nationalVisibility[NATIONAL_CODE_OTHER]) },
{ code: USER_SETTING_CODES.AIS, value: String(!!sourceVisibility[SIGNAL_SOURCE_CODE_AIS]) },
{ code: USER_SETTING_CODES.VPASS, value: String(!!sourceVisibility[SIGNAL_SOURCE_CODE_VPASS]) },
{ code: USER_SETTING_CODES.ENAV, value: String(!!sourceVisibility[SIGNAL_SOURCE_CODE_ENAV]) },
{ code: USER_SETTING_CODES.VTS_AIS, value: String(!!sourceVisibility[SIGNAL_SOURCE_CODE_VTS_AIS]) },
{ code: USER_SETTING_CODES.D_MF_HF, value: String(!!sourceVisibility[SIGNAL_SOURCE_CODE_D_MF_HF]) },
{ code: USER_SETTING_CODES.RADAR, value: String(!!sourceVisibility[SIGNAL_SOURCE_CODE_RADAR]) },
{ code: USER_SETTING_CODES.LOST_SIGNAL, value: String(!!darkSignalVisible) },
// AI 모드
{ code: USER_SETTING_CODES.AI, value: String(Object.values(aiModeVisibility).every(v => v)) },
{ code: USER_SETTING_CODES.MMSI_CHANGE, value: String(!!aiModeVisibility.mmsiChange) },
{ code: USER_SETTING_CODES.CHINA_PERMISSION, value: String(!!aiModeVisibility.chinaPermission) },
{ code: USER_SETTING_CODES.GOV_SHIP, value: String(!!aiModeVisibility.govShip) },
{ code: USER_SETTING_CODES.SSE_ZONE_CONTACT, value: String(!!aiModeVisibility.sseZoneContact) },
{ code: USER_SETTING_CODES.NON_PERMISSION, value: String(!!aiModeVisibility.nonPermission) },
{ code: USER_SETTING_CODES.NORTH_KOREA_AI, value: String(!!aiModeVisibility.northKoreaAi) },
// 위험물
{ code: USER_SETTING_CODES.HAZARD, value: String(!!hazardVisible) },
];
},
/**
* 모든 선박 데이터 초기화
*/

파일 보기

@ -331,3 +331,49 @@ export const SIGNAL_SOURCE_LIST = [
// =====================
export const TRACK_QUERY_MAX_DAYS = 7; // 최대 조회기간 (일)
export const TRACK_QUERY_DEFAULT_DAYS = 3; // 기본 조회기간 (일)
// =====================
// 세션 관리 (메인 프로젝트 동일)
// =====================
export const SESSION_TIMEOUT_MS = 14400000; // 4시간
export const KCGV_GROUP_IDS = ['2', '18']; // 함정용 사용자 그룹
// =====================
// 개인설정 API 타입 코드
// 참조: mda-react-front/src/types/constants.ts (648-695)
// =====================
export const USER_SETTING_FILTER = '000001';
// 필터 개별 설정 코드 (저장용 배열 인덱스 → 코드)
export const USER_SETTING_CODES = {
FISHING: '000001',
PASS: '000002',
CARGO: '000003',
TANKER: '000004',
GOV: '000005',
KCGV: '000006',
NORMAL: '000008',
KOREA: '000009',
CHINA: '000010',
JAPAN: '000011',
NORTH_KOREA: '000012',
ETC_NATION: '000013',
AIS: '000014',
VPASS: '000015',
ENAV: '000016',
VTS_AIS: '000017',
D_MF_HF: '000018',
RADAR: '000019',
LOST_SIGNAL: '000026',
BUOY: '000028',
// AI 모드 설정 코드 (참조: mda-react-front/src/types/constants.ts)
AI: '000039', // AI 모드 전체 토글
MMSI_CHANGE: '000020', // MMSI 변조
CHINA_PERMISSION: '000021', // 중국 허가선박
GOV_SHIP: '000022', // 관공선
SSE_ZONE_CONTACT: '000023', // 비정상 접촉
NON_PERMISSION: '000024', // 비정상 선박
NORTH_KOREA_AI: '000077', // 북한선박
// 위험물
HAZARD: '000027',
};

135
yarn.lock
파일 보기

@ -908,6 +908,11 @@
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.23.2.tgz#156c4b481c0bee22a19f7924728a67120de06971"
integrity sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==
"@repeaterjs/repeater@3.0.6":
version "3.0.6"
resolved "https://registry.yarnpkg.com/@repeaterjs/repeater/-/repeater-3.0.6.tgz#be23df0143ceec3c69f8b6c2517971a5578fdaa2"
integrity sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==
"@rolldown/pluginutils@1.0.0-beta.27":
version "1.0.0-beta.27"
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz#47d2bf4cef6d470b22f5831b420f8964e0bf755f"
@ -1160,6 +1165,11 @@
resolved "https://registry.yarnpkg.com/@types/pako/-/pako-1.0.7.tgz#aa0e4af9855d81153a29ff84cc44cce25298eda9"
integrity sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A==
"@types/rbush@4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/rbush/-/rbush-4.0.0.tgz#b327bf54952e9c924ea6702c36904c2ce1d47f35"
integrity sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==
"@ungap/structured-clone@^1.2.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8"
@ -1177,6 +1187,14 @@
"@types/babel__core" "^7.20.5"
react-refresh "^0.17.0"
"@zarrita/storage@^0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@zarrita/storage/-/storage-0.1.4.tgz#05d7d1d43fc0163d22a17356b619ffeb1ed223d7"
integrity sha512-qURfJAQcQGRfDQ4J9HaCjGaj3jlJKc66bnRk6G/IeLUsM7WKyG7Bzsuf1EZurSXyc0I4LVcu6HaeQQ4d3kZ16g==
dependencies:
reference-spec-reader "^0.2.0"
unzipit "1.4.3"
a5-js@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/a5-js/-/a5-js-0.5.0.tgz#b0241651efdf573229d6f8e25243be31cd0b9451"
@ -1646,6 +1664,11 @@ earcut@^2.2.3, earcut@^2.2.4:
resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a"
integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==
earcut@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/earcut/-/earcut-3.0.2.tgz#d478a29aaf99acf418151493048aa197d0512248"
integrity sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==
electron-to-chromium@^1.5.263:
version "1.5.286"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz#142be1ab5e1cd5044954db0e5898f60a4960384e"
@ -1985,6 +2008,11 @@ fflate@0.7.4:
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.7.4.tgz#61587e5d958fdabb5a9368a302c25363f4f69f50"
integrity sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==
fflate@^0.8.0:
version "0.8.2"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea"
integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@ -2009,6 +2037,22 @@ flat-cache@^3.0.4:
keyv "^4.5.3"
rimraf "^3.0.2"
flatbuffers@25.9.23:
version "25.9.23"
resolved "https://registry.yarnpkg.com/flatbuffers/-/flatbuffers-25.9.23.tgz#346811557fe9312ab5647535e793c761e9c81eb1"
integrity sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==
flatgeobuf@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/flatgeobuf/-/flatgeobuf-4.4.0.tgz#f2067f359ed35a5bd6a19e0cde1a3cd2d16d6c37"
integrity sha512-uUt1xxywP+q8K73MmyKtapF4++dMCzvoqH+ojBTsCtZBbnQEg5qy0Ujze61Rwmpmt6Ra526jpRFHtEkFun5YVw==
dependencies:
"@repeaterjs/repeater" "3.0.6"
flatbuffers "25.9.23"
slice-source "0.4.1"
optionalDependencies:
ol ">=10"
flatted@^3.2.9:
version "3.3.3"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
@ -2093,6 +2137,20 @@ geotiff@^2.0.7:
xml-utils "^1.0.2"
zstddec "^0.1.0"
geotiff@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/geotiff/-/geotiff-3.0.2.tgz#f3ce7d9a6c9278b8b4e1638ffe10a08d507c8255"
integrity sha512-KZ+0YK8gW9HWitovPhfHvkyd1gsyXtY9oOrS/OSX+12M8ojAm+NJ6Vl3tUjp7ZMcPO7e7pJoqpWoMdzO0rF8IQ==
dependencies:
"@petamoriken/float16" "^3.4.7"
lerc "^3.0.0"
pako "^2.0.4"
parse-headers "^2.0.2"
quick-lru "^6.1.1"
web-worker "^1.5.0"
xml-utils "^1.10.2"
zstddec "^0.2.0"
get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
@ -2714,6 +2772,13 @@ node-releases@^2.0.27:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e"
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
numcodecs@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/numcodecs/-/numcodecs-0.3.2.tgz#09887cfc2a3ae1c59a495c01a7f0528118d85dcd"
integrity sha512-6YSPnmZgg0P87jnNhi3s+FVLOcIn3y+1CTIgUulA3IdASzK9fJM87sUFkpyA+be9GibGRaST2wCgkD+6U+fWKw==
dependencies:
fflate "^0.8.0"
object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@ -2776,6 +2841,18 @@ ol-ext@^4.0.10:
resolved "https://registry.yarnpkg.com/ol-ext/-/ol-ext-4.0.37.tgz#8da5c4097322e56f99b45537ca353c242d1c9b88"
integrity sha512-RxzdgMWnNBDP9VZCza3oS3rl1+OCl+1SJLMjt7ATyDDLZl/zzrsQELfJ25WAL6HIWgjkQ2vYDh3nnHFupxOH4w==
ol@>=10:
version "10.8.0"
resolved "https://registry.yarnpkg.com/ol/-/ol-10.8.0.tgz#fe528cd93f13e673e309435f577076e644653aa3"
integrity sha512-kLk7jIlJvKyhVMAjORTXKjzlM6YIByZ1H/d0DBx3oq8nSPCG6/gbLr5RxukzPgwbhnAqh+xHNCmrvmFKhVMvoQ==
dependencies:
"@types/rbush" "4.0.0"
earcut "^3.0.0"
geotiff "^3.0.2"
pbf "4.0.1"
rbush "^4.0.0"
zarrita "^0.6.0"
ol@^9.2.4:
version "9.2.4"
resolved "https://registry.yarnpkg.com/ol/-/ol-9.2.4.tgz#07dcefdceb66ddbde13089bca136f4d4852b772b"
@ -2880,6 +2957,13 @@ pbf@3.2.1:
ieee754 "^1.1.12"
resolve-protobuf-schema "^2.1.0"
pbf@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/pbf/-/pbf-4.0.1.tgz#ad9015e022b235dcdbe05fc468a9acadf483f0d4"
integrity sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==
dependencies:
resolve-protobuf-schema "^2.1.0"
pbf@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.3.0.tgz#1790f3d99118333cc7f498de816028a346ef367f"
@ -2966,6 +3050,11 @@ quickselect@^2.0.0:
resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018"
integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==
quickselect@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-3.0.0.tgz#a37fc953867d56f095a20ac71c6d27063d2de603"
integrity sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==
rbush@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/rbush/-/rbush-3.0.1.tgz#5fafa8a79b3b9afdfe5008403a720cc1de882ecf"
@ -2973,6 +3062,13 @@ rbush@^3.0.1:
dependencies:
quickselect "^2.0.0"
rbush@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/rbush/-/rbush-4.0.1.tgz#1f55afa64a978f71bf9e9a99bc14ff84f3cb0d6d"
integrity sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==
dependencies:
quickselect "^3.0.0"
react-dom@^18.2.0:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
@ -3031,6 +3127,11 @@ readdirp@^4.0.1:
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
reference-spec-reader@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/reference-spec-reader/-/reference-spec-reader-0.2.0.tgz#52bd79614dde68e68f05c97a05ae04ff20acd7ec"
integrity sha512-q0mfCi5yZSSHXpCyxjgQeaORq3tvDsxDyzaadA/5+AbAUwRyRuuTh0aRQuE/vAOt/qzzxidJ5iDeu1cLHaNBlQ==
reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9:
version "1.0.10"
resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9"
@ -3285,6 +3386,11 @@ side-channel@^1.1.0:
side-channel-map "^1.0.1"
side-channel-weakmap "^1.0.2"
slice-source@0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/slice-source/-/slice-source-0.4.1.tgz#40a57ac03c6668b5da200e05378e000bf2a61d79"
integrity sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg==
snappyjs@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/snappyjs/-/snappyjs-0.6.1.tgz#9bca9ff8c54b133a9cc84a71d22779e97fc51878"
@ -3506,6 +3612,13 @@ undici-types@~7.16.0:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46"
integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==
unzipit@1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/unzipit/-/unzipit-1.4.3.tgz#738298a6b235892bf7ce7db82cff813d4ca664ac"
integrity sha512-gsq2PdJIWWGhx5kcdWStvNWit9FVdTewm4SEG7gFskWs+XCVaULt9+BwuoBtJiRE8eo3L1IPAOrbByNLtLtIlg==
dependencies:
uzip-module "^1.0.2"
update-browserslist-db@^1.2.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d"
@ -3546,6 +3659,11 @@ utrie@^1.0.2:
dependencies:
base64-arraybuffer "^1.0.2"
uzip-module@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/uzip-module/-/uzip-module-1.0.3.tgz#6bbabe2a3efea5d5a4a47479f523a571de3427ce"
integrity sha512-AMqwWZaknLM77G+VPYNZLEruMGWGzyigPK3/Whg99B3S6vGHuqsyl5ZrOv1UUF3paGK1U6PM0cnayioaryg/fA==
vite@^5.2.10:
version "5.4.21"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.21.tgz#84a4f7c5d860b071676d39ba513c0d598fdc7027"
@ -3557,7 +3675,7 @@ vite@^5.2.10:
optionalDependencies:
fsevents "~2.3.3"
web-worker@^1.2.0:
web-worker@^1.2.0, web-worker@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.5.0.tgz#71b2b0fbcc4293e8f0aa4f6b8a3ffebff733dcc5"
integrity sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==
@ -3651,7 +3769,7 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
xml-utils@^1.0.2:
xml-utils@^1.0.2, xml-utils@^1.10.2:
version "1.10.2"
resolved "https://registry.yarnpkg.com/xml-utils/-/xml-utils-1.10.2.tgz#436b39ccc25a663ce367ea21abb717afdea5d6b1"
integrity sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==
@ -3666,6 +3784,14 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zarrita@^0.6.0:
version "0.6.1"
resolved "https://registry.yarnpkg.com/zarrita/-/zarrita-0.6.1.tgz#4e0d13e14c0ebcf70e08ed3bafd1d9a290651445"
integrity sha512-YOMTW8FT55Rz+vadTIZeOFZ/F2h4svKizyldvPtMYSxPgSNcRkOzkxCsWpIWlWzB1I/LmISmi0bEekOhLlI+Zw==
dependencies:
"@zarrita/storage" "^0.1.4"
numcodecs "^0.3.2"
zstd-codec@^0.1:
version "0.1.5"
resolved "https://registry.yarnpkg.com/zstd-codec/-/zstd-codec-0.1.5.tgz#c180193e4603ef74ddf704bcc835397d30a60e42"
@ -3676,6 +3802,11 @@ zstddec@^0.1.0:
resolved "https://registry.yarnpkg.com/zstddec/-/zstddec-0.1.0.tgz#7050f3f0e0c3978562d0c566b3e5a427d2bad7ec"
integrity sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==
zstddec@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/zstddec/-/zstddec-0.2.0.tgz#91c8cde8f351ef5fe0bdfca66bb14a5fa0d16d71"
integrity sha512-oyPnDa1X5c13+Y7mA/FDMNJrn4S8UNBe0KCqtDmor40Re7ALrPN6npFwyYVRRh+PqozZQdeg23QtbcamZnG5rA==
zustand@^4.5.2:
version "4.5.7"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.7.tgz#7d6bb2026a142415dd8be8891d7870e6dbe65f55"