432 lines
17 KiB
TypeScript
432 lines
17 KiB
TypeScript
// FloatOverlayContent.tsx — Map Overlay + Map Popup 카탈로그
|
|
|
|
import type { DesignTheme } from '../designTheme';
|
|
|
|
interface FloatOverlayContentProps {
|
|
theme: DesignTheme;
|
|
}
|
|
|
|
const OVERLAY_CASES = [
|
|
{
|
|
component: 'BacktrackReplayBar',
|
|
position: '하단 중앙',
|
|
zIndex: 'z-40',
|
|
pointerEvents: 'auto',
|
|
desc: '역추적 재생 컨트롤 바. 재생/일시정지/슬라이더.',
|
|
source: 'common/components/map/BacktrackReplayBar.tsx',
|
|
},
|
|
{
|
|
component: 'MeasureOverlay',
|
|
position: '마커 위치',
|
|
zIndex: 'z-40',
|
|
pointerEvents: 'auto',
|
|
desc: '거리 측정 마커 "지우기" 버튼. MapLibre Marker 컴포넌트 활용.',
|
|
source: 'common/components/map/MeasureOverlay.tsx',
|
|
},
|
|
{
|
|
component: 'OilDetectionOverlay',
|
|
position: 'inset-0 + 우하단 정보',
|
|
zIndex: 'z-[15]',
|
|
pointerEvents: 'none',
|
|
desc: '유류 탐지 결과 마스크 렌더링. OffscreenCanvas 기반. 정보 패널만 클릭 가능.',
|
|
source: 'tabs/aerial/components/OilDetectionOverlay.tsx',
|
|
},
|
|
{
|
|
component: 'WeatherMapOverlay',
|
|
position: 'absolute inset-0',
|
|
zIndex: 'map layer',
|
|
pointerEvents: 'none',
|
|
desc: '기상 데이터 레이어 오버레이.',
|
|
source: 'tabs/weather/components/WeatherMapOverlay.tsx',
|
|
},
|
|
{
|
|
component: 'OceanForecastOverlay',
|
|
position: 'absolute inset-0',
|
|
zIndex: 'map layer',
|
|
pointerEvents: 'none',
|
|
desc: '해양 예측 레이어 오버레이.',
|
|
source: 'tabs/weather/components/OceanForecastOverlay.tsx',
|
|
},
|
|
];
|
|
|
|
export const FloatOverlayContent = ({ theme }: FloatOverlayContentProps) => {
|
|
const t = theme;
|
|
const isDark = t.mode === 'dark';
|
|
|
|
const mapMockBg = isDark ? '#0f1a2e' : '#c8d8e8';
|
|
const mapGridColor = isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.06)';
|
|
|
|
return (
|
|
<div className="px-8 py-10 flex flex-col gap-12 max-w-[1200px]">
|
|
{/* ── 개요 ── */}
|
|
<div className="flex flex-col gap-3">
|
|
<h2 className="font-sans text-xl font-bold" style={{ color: t.textPrimary }}>
|
|
Overlay
|
|
</h2>
|
|
<p className="font-korean text-body-2 leading-6" style={{ color: t.textSecondary }}>
|
|
지도 컨테이너 위에
|
|
<code
|
|
className="font-mono text-caption px-1.5 py-0.5 mx-1 rounded"
|
|
style={{
|
|
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
|
|
color: t.textAccent,
|
|
}}
|
|
>
|
|
position: absolute
|
|
</code>
|
|
로 레이어되는 UI. 백드롭 없이 지도 위에 기능 UI를 표시한다. Modal과 달리 화면 상호작용을
|
|
차단하지 않으며, 지도 컨테이너의 크기 변화에 반응한다.
|
|
</p>
|
|
</div>
|
|
|
|
{/* ── Overlay vs Modal 비교 ── */}
|
|
<div className="flex flex-col gap-4">
|
|
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
|
|
Overlay vs Modal
|
|
</h3>
|
|
<div
|
|
className="rounded-lg border border-solid overflow-hidden"
|
|
style={{ backgroundColor: t.tableContainerBg, borderColor: t.cardBorder }}
|
|
>
|
|
<div
|
|
className="grid"
|
|
style={{
|
|
gridTemplateColumns: '160px 1fr 1fr',
|
|
backgroundColor: t.tableHeaderBg,
|
|
borderBottom: `1px solid ${t.tableRowBorder}`,
|
|
}}
|
|
>
|
|
{['속성', 'Overlay', 'Modal'].map((col) => (
|
|
<div key={col} className="py-2.5 px-4">
|
|
<span
|
|
className="font-mono text-caption uppercase"
|
|
style={{ letterSpacing: '1px', color: t.textMuted }}
|
|
>
|
|
{col}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
{[
|
|
{ attr: 'position', overlay: 'absolute (지도 기준)', modal: 'fixed (뷰포트 기준)' },
|
|
{ attr: '백드롭', overlay: '없음', modal: 'rgba(0,0,0,0.65) + blur' },
|
|
{ attr: '클릭 차단', overlay: 'pointer-events: none (일반)', modal: '전체 화면 차단' },
|
|
{ attr: 'z-index', overlay: 'z-40 (지도 UI 위)', modal: 'z-[9999] (최상위)' },
|
|
{ attr: '크기 기준', overlay: '지도 컨테이너 = 100%', modal: '고정 너비 (380~720px)' },
|
|
{
|
|
attr: '닫기 방식',
|
|
overlay: '기능 비활성화 시 사라짐',
|
|
modal: '닫기 버튼 / 백드롭 클릭',
|
|
},
|
|
].map((row, idx) => (
|
|
<div
|
|
key={row.attr}
|
|
className="grid items-center"
|
|
style={{
|
|
gridTemplateColumns: '160px 1fr 1fr',
|
|
borderTop: idx === 0 ? 'none' : `1px solid ${t.tableRowBorder}`,
|
|
}}
|
|
>
|
|
<div className="py-2.5 px-4">
|
|
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
|
|
{row.attr}
|
|
</span>
|
|
</div>
|
|
<div className="py-2.5 px-4">
|
|
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
|
|
{row.overlay}
|
|
</span>
|
|
</div>
|
|
<div className="py-2.5 px-4">
|
|
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
|
|
{row.modal}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── 지도 목업 다이어그램 ── */}
|
|
<div className="flex flex-col gap-4">
|
|
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
|
|
Overlay 배치 다이어그램
|
|
</h3>
|
|
<div
|
|
className="rounded-lg border border-solid p-4 flex flex-col gap-3"
|
|
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
|
|
>
|
|
<span
|
|
className="font-mono text-caption uppercase"
|
|
style={{ letterSpacing: '1px', color: t.textMuted }}
|
|
>
|
|
지도 컨테이너 기준 절대 위치
|
|
</span>
|
|
|
|
{/* 지도 목업 */}
|
|
<div
|
|
className="relative rounded overflow-hidden"
|
|
style={{ backgroundColor: mapMockBg, minHeight: '280px' }}
|
|
>
|
|
{/* 격자 배경 (지도 모사) */}
|
|
<div
|
|
className="absolute inset-0"
|
|
style={{
|
|
backgroundImage: `linear-gradient(${mapGridColor} 1px, transparent 1px), linear-gradient(90deg, ${mapGridColor} 1px, transparent 1px)`,
|
|
backgroundSize: '40px 40px',
|
|
}}
|
|
/>
|
|
|
|
{/* 지도 레이블 */}
|
|
<div className="absolute top-3 left-3">
|
|
<span
|
|
className="font-mono text-caption"
|
|
style={{ color: isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)' }}
|
|
>
|
|
MapView (position: relative)
|
|
</span>
|
|
</div>
|
|
|
|
{/* OilDetectionOverlay — 전체 영역 */}
|
|
<div
|
|
className="absolute inset-0 pointer-events-none"
|
|
style={{ border: `1.5px dashed rgba(6,182,212,0.35)`, borderRadius: '4px' }}
|
|
>
|
|
<div className="absolute top-10 left-3">
|
|
<span
|
|
className="font-mono text-caption rounded px-1.5 py-0.5"
|
|
style={{ backgroundColor: 'rgba(6,182,212,0.15)', color: t.textAccent }}
|
|
>
|
|
OilDetectionOverlay — inset-0, z-[15], pointer-events:none
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* MeasureOverlay 마커 */}
|
|
<div className="absolute" style={{ top: '80px', left: '120px' }}>
|
|
<div
|
|
className="rounded-full w-3 h-3 border-2 border-solid"
|
|
style={{ backgroundColor: '#ef4444', borderColor: '#ffffff' }}
|
|
/>
|
|
<div
|
|
className="rounded border border-solid px-2 py-0.5 mt-1"
|
|
style={{
|
|
backgroundColor: isDark ? 'rgba(239,68,68,0.85)' : 'rgba(239,68,68,0.90)',
|
|
borderColor: 'transparent',
|
|
}}
|
|
>
|
|
<span className="font-korean text-caption" style={{ color: '#ffffff' }}>
|
|
지우기
|
|
</span>
|
|
</div>
|
|
<div className="absolute -top-4 left-8">
|
|
<span
|
|
className="font-mono text-caption rounded px-1 py-0.5"
|
|
style={{
|
|
backgroundColor: 'rgba(239,68,68,0.15)',
|
|
color: '#ef4444',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
MeasureOverlay — Marker 위치
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* BacktrackReplayBar — 하단 중앙 */}
|
|
<div
|
|
className="absolute bottom-3 left-1/2 rounded border border-solid px-4 py-2 flex items-center gap-3"
|
|
style={{
|
|
transform: 'translateX(-50%)',
|
|
backgroundColor: isDark ? 'rgba(23,27,40,0.92)' : 'rgba(255,255,255,0.92)',
|
|
borderColor: isDark ? 'rgba(66,71,84,0.40)' : '#e2e8f0',
|
|
backdropFilter: 'blur(6px)',
|
|
}}
|
|
>
|
|
<span className="font-mono text-caption" style={{ color: t.textAccent }}>
|
|
◀◀
|
|
</span>
|
|
<span className="font-mono text-caption" style={{ color: t.textAccent }}>
|
|
▶
|
|
</span>
|
|
<div
|
|
className="w-24 h-1 rounded"
|
|
style={{ backgroundColor: isDark ? 'rgba(66,71,84,0.40)' : '#e2e8f0' }}
|
|
>
|
|
<div className="h-1 w-10 rounded" style={{ backgroundColor: t.textAccent }} />
|
|
</div>
|
|
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
|
|
▶▶
|
|
</span>
|
|
</div>
|
|
<div className="absolute bottom-14 left-1/2" style={{ transform: 'translateX(-50%)' }}>
|
|
<span
|
|
className="font-mono text-caption rounded px-1.5 py-0.5"
|
|
style={{
|
|
backgroundColor: isDark ? 'rgba(76,215,246,0.12)' : 'rgba(6,182,212,0.10)',
|
|
color: t.textAccent,
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
BacktrackReplayBar — bottom-3 center, z-40
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── Map Popup 서브패턴 ── */}
|
|
<div className="flex flex-col gap-4">
|
|
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
|
|
Map Popup — 위치 앵커드 패턴
|
|
</h3>
|
|
<div
|
|
className="rounded-lg border border-solid p-5 flex flex-col gap-4"
|
|
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
|
|
>
|
|
<p className="font-korean text-body-2 leading-6" style={{ color: t.textSecondary }}>
|
|
<strong>ScatPopup</strong>은 지도 마커에 앵커된 컨텍스트 팝업이다. Modal(fixed 뷰포트
|
|
중앙)과 달리 마커 위치에서 동적으로 좌표를 계산하며, 지도 패닝·줌 시 위치가 함께
|
|
업데이트된다.
|
|
</p>
|
|
<div className="grid grid-cols-3 gap-3">
|
|
{[
|
|
{
|
|
label: '위치 계산',
|
|
value: 'map.project(lngLat)',
|
|
desc: '지도 좌표 → 픽셀 좌표 변환',
|
|
},
|
|
{ label: '위치 업데이트', value: 'map.on("move")', desc: '패닝/줌 시 재계산' },
|
|
{ label: 'z-index', value: 'z-[9999]', desc: '다른 오버레이 위' },
|
|
].map((item) => (
|
|
<div
|
|
key={item.label}
|
|
className="rounded border border-solid px-3 py-2.5 flex flex-col gap-1"
|
|
style={{ borderColor: t.cardBorder }}
|
|
>
|
|
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
|
|
{item.label}
|
|
</span>
|
|
<span className="font-mono text-caption" style={{ color: t.textAccent }}>
|
|
{item.value}
|
|
</span>
|
|
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
|
|
{item.desc}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div
|
|
className="rounded border border-solid px-3 py-2"
|
|
style={{
|
|
backgroundColor: isDark ? 'rgba(234,179,8,0.06)' : 'rgba(234,179,8,0.04)',
|
|
borderColor: 'rgba(234,179,8,0.25)',
|
|
}}
|
|
>
|
|
<span className="font-korean text-caption" style={{ color: '#eab308' }}>
|
|
주의: ScatPopup은 MapLibre GL JS의 Popup/Marker 컴포넌트가 아닌 React DOM으로 구현됨.
|
|
지도 컨테이너 내부에 position: absolute로 렌더링된다.
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
|
|
Source:
|
|
</span>
|
|
<span className="font-mono text-caption" style={{ color: t.textAccent }}>
|
|
tabs/scat/components/ScatPopup.tsx
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── 사용 사례 목록 ── */}
|
|
<div className="flex flex-col gap-4">
|
|
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
|
|
현재 사용 사례
|
|
</h3>
|
|
<div
|
|
className="rounded-lg border border-solid overflow-hidden"
|
|
style={{ backgroundColor: t.tableContainerBg, borderColor: t.cardBorder }}
|
|
>
|
|
<div
|
|
className="grid"
|
|
style={{
|
|
gridTemplateColumns: '200px 120px 80px 100px 1fr',
|
|
backgroundColor: t.tableHeaderBg,
|
|
borderBottom: `1px solid ${t.tableRowBorder}`,
|
|
}}
|
|
>
|
|
{['Component', 'Position', 'Z-Index', 'Events', 'Description'].map((col) => (
|
|
<div key={col} className="py-2.5 px-3">
|
|
<span
|
|
className="font-mono text-caption uppercase"
|
|
style={{ letterSpacing: '1px', color: t.textMuted }}
|
|
>
|
|
{col}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
{OVERLAY_CASES.map((item, idx) => (
|
|
<div
|
|
key={item.component}
|
|
className="grid items-start"
|
|
style={{
|
|
gridTemplateColumns: '200px 120px 80px 100px 1fr',
|
|
borderTop: idx === 0 ? 'none' : `1px solid ${t.tableRowBorder}`,
|
|
}}
|
|
>
|
|
<div className="py-2.5 px-3">
|
|
<span className="font-mono text-caption" style={{ color: t.textPrimary }}>
|
|
{item.component}
|
|
</span>
|
|
</div>
|
|
<div className="py-2.5 px-3">
|
|
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
|
|
{item.position}
|
|
</span>
|
|
</div>
|
|
<div className="py-2.5 px-3">
|
|
<span
|
|
className="font-mono text-caption rounded border border-solid px-1.5 py-0.5"
|
|
style={{ color: t.textAccent, borderColor: t.cardBorder }}
|
|
>
|
|
{item.zIndex}
|
|
</span>
|
|
</div>
|
|
<div className="py-2.5 px-3">
|
|
<span
|
|
className="font-mono text-caption rounded px-1.5 py-0.5"
|
|
style={{
|
|
color: item.pointerEvents === 'none' ? t.textMuted : '#22c55e',
|
|
backgroundColor:
|
|
item.pointerEvents === 'none'
|
|
? isDark
|
|
? 'rgba(140,144,159,0.10)'
|
|
: 'rgba(148,163,184,0.10)'
|
|
: isDark
|
|
? 'rgba(34,197,94,0.10)'
|
|
: 'rgba(34,197,94,0.08)',
|
|
}}
|
|
>
|
|
{item.pointerEvents}
|
|
</span>
|
|
</div>
|
|
<div className="py-2.5 px-3">
|
|
<span
|
|
className="font-korean text-caption leading-5"
|
|
style={{ color: t.textSecondary }}
|
|
>
|
|
{item.desc}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FloatOverlayContent;
|