diff --git a/src/components/layout/SideNav.jsx b/src/components/layout/SideNav.jsx
index 2c46f1e7..e5953b4a 100644
--- a/src/components/layout/SideNav.jsx
+++ b/src/components/layout/SideNav.jsx
@@ -1,25 +1,15 @@
/**
* 사이드 네비게이션 메뉴
- * - 퍼블리시 NavComponent 구조와 동일하게 맞춤
*/
const gnbList = [
{ key: 'gnb1', className: 'gnb1', label: '선박', path: 'ship' },
- { key: 'gnb2', className: 'gnb2', label: '위성', path: 'satellite' },
- { key: 'gnb3', className: 'gnb3', label: '기상', path: 'weather' },
{ key: 'gnb4', className: 'gnb4', label: '분석', path: 'analysis' },
{ key: 'gnb5', className: 'gnb5', label: '타임라인', path: 'timeline' },
- // { key: 'gnb6', className: 'gnb6', label: 'AI모드', path: 'ai' },
{ key: 'gnb7', className: 'gnb7', label: '리플레이', path: 'replay' },
{ key: 'gnb8', className: 'gnb8', label: '항적분석', path: 'area-search' },
];
-// 필터/레이어 버튼 비활성화 — 선박(gnb1) 버튼에서 DisplayComponent로 통합
-const sideList = [
- // { key: 'filter', className: 'filter', label: '필터', path: 'filter' },
- // { key: 'layer', className: 'layer', label: '레이어', path: 'layer' },
-];
-
export default function SideNav({ activeKey, onChange }) {
return (
);
}
@@ -61,15 +35,10 @@ export default function SideNav({ activeKey, onChange }) {
// 키-경로 매핑 export (Sidebar에서 사용)
export const keyToPath = {
gnb1: 'ship',
- gnb2: 'satellite',
- gnb3: 'weather',
gnb4: 'analysis',
gnb5: 'timeline',
- gnb6: 'ai',
gnb7: 'replay',
gnb8: 'area-search',
- filter: 'filter',
- layer: 'layer',
};
export const pathToKey = Object.fromEntries(
diff --git a/src/components/layout/Sidebar.jsx b/src/components/layout/Sidebar.jsx
index d4b44506..c099caad 100644
--- a/src/components/layout/Sidebar.jsx
+++ b/src/components/layout/Sidebar.jsx
@@ -2,25 +2,9 @@ import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import SideNav, { keyToPath, pathToKey } from './SideNav';
-// 퍼블리시 패널 컴포넌트 (폴더 없어도 빌드 가능 — import.meta.glob은 매칭 파일 없으면 빈 객체 반환)
-const publishPanels = import.meta.glob('../../publish/pages/Panel*Component.jsx', { eager: true });
-const getPanel = (name) => publishPanels[`../../publish/pages/${name}.jsx`]?.default || null;
-
-const Panel1Component = getPanel('Panel1Component');
-const Panel2Component = getPanel('Panel2Component');
-const Panel3Component = getPanel('Panel3Component');
-const Panel4Component = getPanel('Panel4Component');
-const Panel5Component = getPanel('Panel5Component');
-const Panel6Component = getPanel('Panel6Component');
-const Panel8Component = getPanel('Panel8Component');
-
-// DisplayComponent는 스토어 연결된 버전 사용
-import DisplayComponent from '../../component/wrap/side/DisplayComponent';
// 구현된 페이지
import ReplayPage from '../../pages/ReplayPage';
import AreaSearchPage from '../../areaSearch/components/AreaSearchPage';
-import WeatherPage from '../../pages/WeatherPage';
-import SatellitePage from '../../pages/SatellitePage';
/**
* 사이드바 컴포넌트
@@ -62,19 +46,14 @@ export default function Sidebar() {
onToggle: handleTogglePanel,
};
- // 활성 키에 따른 패널 컴포넌트 렌더링 (퍼블리시 패널이 없으면 null 반환)
+ // 활성 키에 따른 패널 컴포넌트 렌더링
const renderPanel = () => {
const panelMap = {
- gnb1: DisplayComponent ?
: null,
- gnb2:
,
- gnb3:
,
- gnb4: Panel4Component ?
: null,
- gnb5: Panel5Component ?
: null,
- gnb6: Panel6Component ?
: null,
+ gnb1: null, // TODO: 필터/디스플레이 패널 재구현
+ gnb4: null, // TODO: 분석 패널
+ gnb5: null, // TODO: 타임라인 패널
gnb7:
,
gnb8:
,
- filter: DisplayComponent ?
: null,
- layer: DisplayComponent ?
: null,
};
return panelMap[activeKey] || null;
};
diff --git a/src/components/map/PatrolShipSelector.jsx b/src/components/map/PatrolShipSelector.jsx
deleted file mode 100644
index ec804bb5..00000000
--- a/src/components/map/PatrolShipSelector.jsx
+++ /dev/null
@@ -1,203 +0,0 @@
-/**
- * 경비함정 선택 드롭다운
- * 선박 모드에서 추적할 함정을 선택
- * - 검색 기능 (like 검색)
- * - 반경 설정
- */
-import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
-import useShipStore from '../../stores/shipStore';
-import useTrackingModeStore, { isPatrolShip, RADIUS_OPTIONS } from '../../stores/trackingModeStore';
-import './PatrolShipSelector.scss';
-
-/**
- * 검색어 정규화 (공백/특수문자 제거, 소문자 변환)
- */
-function normalizeText(text) {
- if (!text) return '';
- return text.toLowerCase().replace(/[\s\-_.,:;!@#$%^&*()+=\[\]{}|\\/<>?'"]/g, '');
-}
-
-export default function PatrolShipSelector() {
- const features = useShipStore((s) => s.features);
- const showShipSelector = useTrackingModeStore((s) => s.showShipSelector);
- const closeShipSelector = useTrackingModeStore((s) => s.closeShipSelector);
- const selectTrackedShip = useTrackingModeStore((s) => s.selectTrackedShip);
- const setMapMode = useTrackingModeStore((s) => s.setMapMode);
- const setRadius = useTrackingModeStore((s) => s.setRadius);
- const currentRadius = useTrackingModeStore((s) => s.radiusNM);
-
- const [searchValue, setSearchValue] = useState('');
- const [selectedRadius, setSelectedRadius] = useState(currentRadius);
- const containerRef = useRef(null);
- const searchInputRef = useRef(null);
-
- // 패널 열릴 때 검색창 포커스
- useEffect(() => {
- if (showShipSelector && searchInputRef.current) {
- searchInputRef.current.focus();
- }
- }, [showShipSelector]);
-
- // 외부 클릭 시 닫기
- useEffect(() => {
- if (!showShipSelector) return;
-
- const handleClickOutside = (e) => {
- if (containerRef.current && !containerRef.current.contains(e.target)) {
- // 선박 버튼 클릭은 제외 (TopBar에서 처리)
- if (e.target.closest('.ship')) return;
- closeShipSelector();
- }
- };
-
- document.addEventListener('mousedown', handleClickOutside);
- return () => document.removeEventListener('mousedown', handleClickOutside);
- }, [showShipSelector, closeShipSelector]);
-
- // 패널 닫힐 때 검색어 초기화
- useEffect(() => {
- if (!showShipSelector) {
- setSearchValue('');
- }
- }, [showShipSelector]);
-
- // 경비함정 목록 필터링
- const patrolShips = useMemo(() => {
- const ships = [];
- features.forEach((ship, featureId) => {
- if (isPatrolShip(ship.originalTargetId)) {
- ships.push({
- featureId,
- ship,
- shipName: ship.shipName || ship.originalTargetId || '-',
- originalTargetId: ship.originalTargetId,
- });
- }
- });
-
- // 선박명 정렬
- ships.sort((a, b) => a.shipName.localeCompare(b.shipName, 'ko'));
- return ships;
- }, [features]);
-
- // 검색 필터링된 목록
- const filteredShips = useMemo(() => {
- const normalizedSearch = normalizeText(searchValue);
- if (!normalizedSearch) return patrolShips;
-
- return patrolShips.filter((item) => {
- const normalizedName = normalizeText(item.shipName);
- const normalizedId = normalizeText(item.originalTargetId);
- return normalizedName.includes(normalizedSearch) || normalizedId.includes(normalizedSearch);
- });
- }, [patrolShips, searchValue]);
-
- // 함정 선택 핸들러
- const handleSelectShip = useCallback((item) => {
- setRadius(selectedRadius);
- selectTrackedShip(item.featureId, item.ship);
- setSearchValue('');
- }, [selectTrackedShip, setRadius, selectedRadius]);
-
- // 취소 (지도 모드로 복귀)
- const handleCancel = useCallback(() => {
- setMapMode();
- setSearchValue('');
- }, [setMapMode]);
-
- // 검색어 변경
- const handleSearchChange = useCallback((e) => {
- setSearchValue(e.target.value);
- }, []);
-
- // 검색어 초기화
- const handleClearSearch = useCallback(() => {
- setSearchValue('');
- searchInputRef.current?.focus();
- }, []);
-
- // 반경 선택
- const handleRadiusChange = useCallback((radius) => {
- setSelectedRadius(radius);
- }, []);
-
- if (!showShipSelector) return null;
-
- return (
-
- {/* 헤더 */}
-
- 경비함정 선택
-
-
-
- {/* 검색 영역 */}
-
-
-
- {searchValue && (
-
- )}
-
-
-
- {/* 반경 설정 */}
-
-
반경 설정
-
- {RADIUS_OPTIONS.map((radius) => (
-
- ))}
- NM
-
-
-
- {/* 함정 목록 */}
-
- {filteredShips.length === 0 ? (
-
- {searchValue ? '검색 결과가 없습니다' : '활성화된 경비함정이 없습니다'}
-
- ) : (
-
- {filteredShips.map((item) => (
- - handleSelectShip(item)}
- >
- {item.shipName}
- {item.originalTargetId}
-
- ))}
-
- )}
-
-
- {/* 푸터 */}
-
-
- {searchValue ? `${filteredShips.length} / ${patrolShips.length}척` : `${patrolShips.length}척`}
-
-
-
- );
-}
diff --git a/src/components/map/PatrolShipSelector.scss b/src/components/map/PatrolShipSelector.scss
deleted file mode 100644
index b0e0ccee..00000000
--- a/src/components/map/PatrolShipSelector.scss
+++ /dev/null
@@ -1,258 +0,0 @@
-/**
- * 경비함정 선택 드롭다운 스타일
- */
-.patrol-ship-selector {
- position: absolute;
- top: calc(100% + 0.5rem);
- right: 0;
- width: 32rem;
- max-height: 50rem;
- background-color: rgba(var(--secondary6-rgb), 0.95);
- border-radius: 0.8rem;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
- z-index: 200;
- display: flex;
- flex-direction: column;
-
- // 헤더
- .selector-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 1rem 1.2rem;
- border-bottom: 1px solid rgba(var(--white-rgb), 0.1);
- flex-shrink: 0;
-
- .selector-title {
- color: var(--white);
- font-size: 1.4rem;
- font-weight: var(--fw-bold);
- }
-
- .close-btn {
- width: 2.4rem;
- height: 2.4rem;
- display: flex;
- align-items: center;
- justify-content: center;
- border: none;
- background: transparent;
- color: rgba(var(--white-rgb), 0.6);
- font-size: 2rem;
- cursor: pointer;
- border-radius: 0.4rem;
- transition: all 0.15s ease;
-
- &:hover {
- color: var(--white);
- background-color: rgba(var(--white-rgb), 0.1);
- }
- }
- }
-
- // 검색 영역
- .selector-search {
- padding: 0.8rem 1.2rem;
- border-bottom: 1px solid rgba(var(--white-rgb), 0.1);
- flex-shrink: 0;
-
- .search-input-wrapper {
- position: relative;
- width: 100%;
- }
-
- .search-input {
- width: 100%;
- height: 3.2rem;
- padding: 0 3rem 0 1rem;
- border: 1px solid rgba(var(--white-rgb), 0.2);
- border-radius: 0.4rem;
- background-color: rgba(var(--white-rgb), 0.05);
- color: var(--white);
- font-size: 1.3rem;
- outline: none;
- transition: all 0.15s ease;
-
- &::placeholder {
- color: rgba(var(--white-rgb), 0.4);
- }
-
- &:focus {
- border-color: var(--primary1);
- background-color: rgba(var(--white-rgb), 0.1);
- }
- }
-
- .search-clear-btn {
- position: absolute;
- top: 50%;
- right: 0.6rem;
- transform: translateY(-50%);
- width: 2rem;
- height: 2rem;
- display: flex;
- align-items: center;
- justify-content: center;
- border: none;
- background: transparent;
- color: rgba(var(--white-rgb), 0.5);
- font-size: 1.6rem;
- cursor: pointer;
- border-radius: 0.3rem;
-
- &:hover {
- color: var(--white);
- background-color: rgba(var(--white-rgb), 0.1);
- }
- }
- }
-
- // 반경 설정
- .selector-radius {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0.8rem 1.2rem;
- border-bottom: 1px solid rgba(var(--white-rgb), 0.1);
- flex-shrink: 0;
-
- .radius-label {
- color: rgba(var(--white-rgb), 0.7);
- font-size: 1.2rem;
- flex-shrink: 0;
- }
-
- .radius-options {
- display: flex;
- align-items: center;
- gap: 0.4rem;
- }
-
- .radius-btn {
- min-width: 3.6rem;
- height: 2.6rem;
- padding: 0 0.6rem;
- border: 1px solid rgba(var(--white-rgb), 0.2);
- border-radius: 0.4rem;
- background-color: transparent;
- color: rgba(var(--white-rgb), 0.7);
- font-size: 1.2rem;
- cursor: pointer;
- transition: all 0.15s ease;
-
- &:hover {
- border-color: rgba(var(--white-rgb), 0.4);
- color: var(--white);
- }
-
- &.active {
- border-color: var(--primary1);
- background-color: var(--primary1);
- color: var(--white);
- }
- }
-
- .radius-unit {
- color: rgba(var(--white-rgb), 0.5);
- font-size: 1.1rem;
- margin-left: 0.3rem;
- }
- }
-
- // 함정 목록 영역
- .selector-content {
- flex: 1;
- overflow-x: hidden;
- overflow-y: auto;
- min-height: 10rem;
- max-height: 28rem;
-
- // 스크롤바 스타일
- &::-webkit-scrollbar {
- width: 6px;
- }
-
- &::-webkit-scrollbar-track {
- background: transparent;
- }
-
- &::-webkit-scrollbar-thumb {
- background: rgba(var(--white-rgb), 0.3);
- border-radius: 3px;
-
- &:hover {
- background: rgba(var(--white-rgb), 0.5);
- }
- }
- }
-
- .no-ships {
- padding: 3rem 1.2rem;
- text-align: center;
- color: rgba(var(--white-rgb), 0.5);
- font-size: 1.3rem;
- }
-
- .ship-list {
- list-style: none;
- padding: 0.5rem 0;
- margin: 0;
- display: flex;
- flex-direction: column;
-
- // TopBar li 스타일 상속 초기화
- li {
- height: auto;
- padding: 0;
- }
- }
-
- .ship-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0.9rem 1.2rem !important; // TopBar li:first-child, li:last-child 오버라이드
- cursor: pointer;
- transition: background-color 0.15s ease;
- width: 100%;
- box-sizing: border-box;
- height: auto !important; // TopBar li height: 100% 오버라이드
-
- &:hover {
- background-color: rgba(var(--primary1-rgb), 0.3);
- }
-
- .ship-name {
- color: var(--white);
- font-size: 1.3rem;
- font-weight: var(--fw-bold);
- flex: 1;
- min-width: 0;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .ship-id {
- color: rgba(var(--white-rgb), 0.5);
- font-size: 1.1rem;
- margin-left: 1rem;
- flex-shrink: 0;
- }
- }
-
- // 푸터
- .selector-footer {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- padding: 0.8rem 1.2rem;
- border-top: 1px solid rgba(var(--white-rgb), 0.1);
- flex-shrink: 0;
-
- .ship-count {
- color: rgba(var(--white-rgb), 0.6);
- font-size: 1.2rem;
- }
- }
-}
diff --git a/src/components/map/TopBar.jsx b/src/components/map/TopBar.jsx
index 81147b94..c0e1689b 100644
--- a/src/components/map/TopBar.jsx
+++ b/src/components/map/TopBar.jsx
@@ -10,7 +10,6 @@ import { toLonLat } from 'ol/proj';
import { useMapStore } from '../../stores/mapStore';
import useTrackingModeStore from '../../stores/trackingModeStore';
import useShipSearch from '../../hooks/useShipSearch';
-import PatrolShipSelector from './PatrolShipSelector';
import './TopBar.scss';
/**
@@ -353,8 +352,6 @@ export default function TopBar() {
)}
- {/* 함정 선택 드롭다운 */}
-