feat: 선종 필터 패널 재구현 + signalKindCode 백엔드 응답 우선
- ShipFilterPanel: 선종별 표시/숨김 토글 (전체 ON/OFF 포함) - Sidebar gnb1에 ShipFilterPanel 연결 - aisTargetApi: 백엔드 signalKindCode 필드 우선, 없으면 vesselType fallback Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
부모
3439407b71
커밋
2d871306ed
@ -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} />,
|
||||
|
||||
125
src/components/ship/ShipFilterPanel.jsx
Normal file
125
src/components/ship/ShipFilterPanel.jsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
||||
불러오는 중...
Reference in New Issue
Block a user