release: Phase 3 완료 (React 19 + MapLibre GL JS 전환) #2

병합
htlee develop 에서 main 로 11 commits 를 머지했습니다 2026-02-15 17:53:44 +09:00
3개의 변경된 파일129개의 추가작업 그리고 2개의 파일을 삭제
Showing only changes of commit 2d871306ed - Show all commits

파일 보기

@ -35,7 +35,8 @@ export async function searchAisTargets(minutes = 60) {
*/
export function aisTargetToFeature(aisTarget) {
const mmsi = String(aisTarget.mmsi || '');
const signalKindCode = mapVesselTypeToKindCode(aisTarget.vesselType);
// 백엔드에서 signalKindCode를 직접 제공, 없으면 vesselType 기반 fallback
const signalKindCode = aisTarget.signalKindCode || mapVesselTypeToKindCode(aisTarget.vesselType);
return {
// 고유 식별자 (AIS 신호원 코드 + MMSI)

파일 보기

@ -3,6 +3,7 @@ import { useNavigate, useLocation } from 'react-router-dom';
import SideNav, { keyToPath, pathToKey } from './SideNav';
//
import ShipFilterPanel from '../ship/ShipFilterPanel';
import ReplayPage from '../../pages/ReplayPage';
import AreaSearchPage from '../../areaSearch/components/AreaSearchPage';
@ -49,7 +50,7 @@ export default function Sidebar() {
//
const renderPanel = () => {
const panelMap = {
gnb1: null, // TODO: /
gnb1: <ShipFilterPanel {...panelProps} />,
gnb4: null, // TODO:
gnb5: null, // TODO:
gnb7: <ReplayPage {...panelProps} />,

파일 보기

@ -0,0 +1,125 @@
/**
* 선박 필터 패널
* - 선종별 표시/숨김 토글
* - 기존 DisplayComponent의 필터 탭을 민간화 버전으로 재구현
*/
import { useState, memo, useCallback } from 'react';
import useShipStore from '../../stores/shipStore';
import { SHIP_KIND_LIST } from '../../types/constants';
/**
* 스위치 그룹 헤더 + 접이식 본문
*/
const SwitchGroup = memo(({ title, children, defaultOpen = true }) => {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">{title}</div>
<button
type="button"
className={`toggleBtn ${isOpen ? 'is-open' : ''}`}
onClick={() => setIsOpen((v) => !v)}
aria-label="접기/펼치기"
/>
</div>
<div className={`switchBox ${isOpen ? 'is-open' : ''}`}>
{children}
</div>
</div>
);
});
/**
* 개별 토글 스위치 (CSS 토글은 기존 common.css의 .switch 클래스 사용)
*/
const ToggleSwitch = memo(({ label, checked, onChange }) => (
<li>
<span>{label}</span>
<label className="switch">
<input type="checkbox" checked={checked} onChange={onChange} />
<span className="slider" />
</label>
</li>
));
/**
* 전체 ON/OFF 토글
*/
const AllToggle = memo(({ label, allChecked, onToggleAll }) => (
<li>
<span style={{ fontWeight: 'bold' }}>{label}</span>
<label className="switch">
<input type="checkbox" checked={allChecked} onChange={onToggleAll} />
<span className="slider" />
</label>
</li>
));
/**
* 선박 필터 패널 메인 컴포넌트
*/
export default function ShipFilterPanel({ isOpen, onToggle }) {
const kindVisibility = useShipStore((s) => s.kindVisibility);
const kindCounts = useShipStore((s) => s.kindCounts);
const toggleKindVisibility = useShipStore((s) => s.toggleKindVisibility);
//
const allKindVisible = Object.values(kindVisibility).every(Boolean);
const handleToggleAllKind = useCallback(() => {
const nextValue = !allKindVisible;
SHIP_KIND_LIST.forEach(({ code }) => {
if (kindVisibility[code] !== nextValue) {
toggleKindVisibility(code);
}
});
}, [allKindVisible, kindVisibility, toggleKindVisibility]);
//
const totalCount = Object.values(kindCounts).reduce((sum, v) => sum + v, 0);
return (
<div className={`slidePanel ${!isOpen ? 'is-closed' : ''}`}>
{/* 탭 헤더 */}
<div className="tabBox">
<ul className="tabMenu">
<li className="is-active">
<button type="button">필터</button>
</li>
</ul>
</div>
{/* 필터 본문 */}
<div className="tabWrap is-active">
<div className="tabWrapInner">
<div className="tabWrapCnt">
{/* 선종 필터 */}
<SwitchGroup title="선종">
<ul className="switchList">
<AllToggle
label={`전체 (${totalCount.toLocaleString()})`}
allChecked={allKindVisible}
onToggleAll={handleToggleAllKind}
/>
{SHIP_KIND_LIST.map(({ code, label }) => (
<ToggleSwitch
key={code}
label={`${label} (${(kindCounts[code] || 0).toLocaleString()})`}
checked={!!kindVisibility[code]}
onChange={() => toggleKindVisibility(code)}
/>
))}
</ul>
</SwitchGroup>
</div>
</div>
</div>
{/* 패널 토글 버튼 */}
<button type="button" className="toogle" onClick={onToggle} aria-label="패널 접기/펼치기" />
</div>
);
}