import { useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'; import positionMapLeft from '../assets/facility/tunnel/position-map/left.svg'; import positionMapCenter from '../assets/facility/tunnel/position-map/center.svg'; import positionMapRight from '../assets/facility/tunnel/position-map/right.svg'; import actualDetailLeft from '../assets/facility/tunnel/actual-detail/left.svg'; import actualDetailCenter from '../assets/facility/tunnel/actual-detail/center.svg'; import actualDetailRight from '../assets/facility/tunnel/actual-detail/right.svg'; import './TunnelFacility.css'; import { CompactFacilityConnection } from './CompactFacilityConnection'; export const TUNNEL_ASSET_MAP = Object.freeze({ positionMap: Object.freeze({ semanticId: 'facility.tunnel.position-map', sourceDirectory: '시설물 표현/터널/위치도', left: Object.freeze({ src: positionMapLeft, width: 43, height: 93, role: 'fixed-left-terminal' }), center: Object.freeze({ src: positionMapCenter, width: 99, height: 93, role: 'horizontal-flex-center' }), right: Object.freeze({ src: positionMapRight, width: 43, height: 93, role: 'fixed-right-terminal' }), separatedGap: Object.freeze({ leftToCenter: 7, centerToRight: 5 }), }), actualDetail: Object.freeze({ semanticId: 'facility.tunnel.actual-detail', sourceDirectory: '시설물 표현/터널/실사화', left: Object.freeze({ src: actualDetailLeft, width: 48, height: 95, role: 'fixed-left-terminal' }), center: Object.freeze({ src: actualDetailCenter, width: 84, height: 95, role: 'horizontal-flex-center' }), right: Object.freeze({ src: actualDetailRight, width: 48, height: 95, role: 'fixed-right-terminal' }), separatedGap: Object.freeze({ leftToCenter: 10, centerToRight: 9 }), }), }); export type TunnelVariant = keyof typeof TUNNEL_ASSET_MAP; const VARIANT_LABEL: Record = { positionMap: '위치도', actualDetail: '실사화', }; const SOURCE_REFERENCE_NAME = '대산2터널'; const SOURCE_REFERENCE_TOTALS = Object.freeze({ positionMap: 210, actualDetail: 220, }); function classNames(...values: Array) { return values.filter(Boolean).join(' '); } export function TunnelFacility({ name = '대산2터널', secondaryText = 'L=750m', variant = 'positionMap', separated = false, showLabels = !separated, className = '', }: { name?: string; secondaryText?: string; variant?: TunnelVariant; separated?: boolean; showLabels?: boolean; className?: string; }) { const asset = TUNNEL_ASSET_MAP[variant]; if (!asset) throw new Error(`Unsupported Tunnel visual variant: ${variant}`); const primaryProbeRef = useRef(null); const secondaryProbeRef = useRef(null); const [measuredCenterWidth, setMeasuredCenterWidth] = useState(asset.center.width); useLayoutEffect(() => { if (separated) { setMeasuredCenterWidth(asset.center.width); return undefined; } let active = true; const measure = () => { if (!active) return; const primaryWidth = primaryProbeRef.current ? Number.parseFloat(getComputedStyle(primaryProbeRef.current).width) : 0; const secondaryWidth = secondaryProbeRef.current ? Number.parseFloat(getComputedStyle(secondaryProbeRef.current).width) : 0; const referenceCenterWidth = SOURCE_REFERENCE_TOTALS[variant] - asset.left.width - asset.right.width; const nextWidth = name === SOURCE_REFERENCE_NAME ? Math.max(asset.center.width, referenceCenterWidth) : Math.max(asset.center.width, Math.ceil(primaryWidth), Math.ceil(secondaryWidth)); setMeasuredCenterWidth((current) => current === nextWidth ? current : nextWidth); }; measure(); const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(measure) : null; if (observer && primaryProbeRef.current) observer.observe(primaryProbeRef.current); if (observer && secondaryProbeRef.current) observer.observe(secondaryProbeRef.current); document.fonts?.ready.then(measure); return () => { active = false; observer?.disconnect(); }; }, [asset, name, secondaryText, separated]); const centerWidth = separated ? asset.center.width : measuredCenterWidth; const leftGap = separated ? asset.separatedGap.leftToCenter : 0; const rightGap = separated ? asset.separatedGap.centerToRight : 0; const accessibleName = [name, secondaryText, VARIANT_LABEL[variant], separated ? '분리 표현' : null] .filter(Boolean) .join(', '); const style = { '--tunnel-height': `${asset.center.height}px`, '--tunnel-left-width': `${asset.left.width}px`, '--tunnel-center-base-width': `${asset.center.width}px`, '--tunnel-center-width': `${centerWidth}px`, '--tunnel-center-scale': centerWidth / asset.center.width, '--tunnel-right-width': `${asset.right.width}px`, '--tunnel-left-gap': `${leftGap}px`, '--tunnel-right-gap': `${rightGap}px`, } as CSSProperties; return (
{showLabels && ( )}
); } function TunnelSample({ title, note, children }: { title: string; note?: string; children?: ReactNode }) { return (
{title} {note ? {note} : null}
{children}
); } function CompactTunnelExample() { return ( ); } export function TunnelReferenceDemo() { return (
SHORT
MEDIUM
LONG
ACTUAL SHORT
ACTUAL REF
ACTUAL LONG
); }