UI 수정
기획안 반영 및 보완
This commit is contained in:
94
client/src/components/overlay/RouteInfoOverlay.tsx
Normal file
94
client/src/components/overlay/RouteInfoOverlay.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useGeoStore } from '../../store/geoStore';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
import styles from './RouteInfo.module.css';
|
||||
|
||||
/** 정적 에셋 경로 (Vite base 반영). */
|
||||
const bgUrl = `${import.meta.env.BASE_URL}assets/title-panel-bg@2x.png`;
|
||||
|
||||
/** 원본 디자인 무대 가로폭(px). 배너는 이 기준으로 만들어졌다. */
|
||||
const STAGE_WIDTH = 1920;
|
||||
|
||||
/** 초 → "M분 S초". */
|
||||
function formatDuration(sec?: number | null): string {
|
||||
if (sec == null || !isFinite(sec) || sec <= 0) return '';
|
||||
const m = Math.floor(sec / 60);
|
||||
const s = Math.round(sec % 60);
|
||||
return s > 0 ? `${m}분 ${s}초` : `${m}분`;
|
||||
}
|
||||
|
||||
/** "158k700" → 158700 (m). 매칭 실패 시 -1. */
|
||||
function stationKm(title: string): number {
|
||||
const m = title.match(/(\d+)[Kk](\d+)/);
|
||||
return m ? parseInt(m[1], 10) * 1000 + parseInt(m[2], 10) : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 영상 좌상단 노선 정보 배너 — videoplayer 의 RouteInfo 디자인 이식.
|
||||
* 값은 모두 선택한 폴더에서 가져온다(하드코딩 없음).
|
||||
* - 연장(lengthKm): route.json 우선 → 측점 CSV 구간(min~max km) 계산 폴백.
|
||||
* - 소요(durationSec): route.json 우선 → 실제 영상 길이 폴백.
|
||||
* - 방향/노선명: CSV에 없는 정보 → route.json(routeInfo).
|
||||
* 표출할 값이 하나도 없으면 렌더하지 않는다.
|
||||
*/
|
||||
export default function RouteInfoOverlay() {
|
||||
const routeInfo = useGeoStore((s) => s.routeMeta?.routeInfo);
|
||||
const stations = useGeoStore((s) => s.stations);
|
||||
const videoDuration = usePlayerStore((s) => s.duration);
|
||||
|
||||
// 원본처럼 영상 폭/1920 비율로 배너를 스케일 (부모=영상 영역 폭 관측).
|
||||
const [scale, setScale] = useState(1);
|
||||
const roRef = useRef<ResizeObserver | null>(null);
|
||||
const setPanelRef = useCallback((el: HTMLDivElement | null) => {
|
||||
roRef.current?.disconnect();
|
||||
const parent = el?.parentElement;
|
||||
if (!parent) return;
|
||||
const update = () => setScale(parent.clientWidth / STAGE_WIDTH);
|
||||
update();
|
||||
const ro = new ResizeObserver(update);
|
||||
ro.observe(parent);
|
||||
roRef.current = ro;
|
||||
}, []);
|
||||
|
||||
const direction = routeInfo?.direction;
|
||||
const name = routeInfo?.name;
|
||||
|
||||
// 연장: route.json 우선 → 측점 구간 계산 폴백
|
||||
let lengthKm = routeInfo?.lengthKm ?? null;
|
||||
if (lengthKm == null && stations.length) {
|
||||
const kms = stations.map((s) => stationKm(s.title)).filter((k) => k >= 0);
|
||||
if (kms.length >= 2) {
|
||||
lengthKm = Math.round((Math.max(...kms) - Math.min(...kms)) / 10) / 100; // m→km, 소수2
|
||||
}
|
||||
}
|
||||
|
||||
// 소요시간: route.json 우선 → 실제 영상 길이 폴백
|
||||
const dur = formatDuration(routeInfo?.durationSec ?? videoDuration);
|
||||
|
||||
if (!direction && !name && lengthKm == null && !dur) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setPanelRef}
|
||||
className={styles.panel}
|
||||
style={{ transform: `scale(${scale})`, transformOrigin: 'top left' }}
|
||||
>
|
||||
<img className={styles.bg} src={bgUrl} alt="" />
|
||||
{direction && <p className={styles.direction}>{direction}</p>}
|
||||
{name && <p className={styles.routeName}>{name}</p>}
|
||||
{lengthKm != null && (
|
||||
<>
|
||||
<p className={styles.lengthLabel}>연장</p>
|
||||
<p className={styles.lengthValue}>{lengthKm}</p>
|
||||
<p className={styles.lengthUnit}>km</p>
|
||||
</>
|
||||
)}
|
||||
{dur && (
|
||||
<>
|
||||
<p className={styles.durationValue}>{dur}</p>
|
||||
<p className={styles.durationLabel}>소요</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user