- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
173 lines
5.4 KiB
TypeScript
173 lines
5.4 KiB
TypeScript
import React, { useEffect, useRef } from 'react';
|
|
import { Activity, Table, X } from 'lucide-react';
|
|
|
|
export type BottomPanelTab = 'table' | 'eps';
|
|
|
|
interface BottomPanelProps {
|
|
activeTab: BottomPanelTab | null;
|
|
height: number;
|
|
epsAvailable: boolean;
|
|
onToggleTab: (tab: BottomPanelTab) => void;
|
|
onHeightChange: (height: number) => void;
|
|
tableContent: React.ReactNode;
|
|
epsContent: React.ReactNode;
|
|
}
|
|
|
|
export const BottomPanel: React.FC<BottomPanelProps> = ({
|
|
activeTab,
|
|
height,
|
|
epsAvailable,
|
|
onToggleTab,
|
|
onHeightChange,
|
|
tableContent,
|
|
epsContent,
|
|
}) => {
|
|
const dragRef = useRef<{ startY: number; startHeight: number } | null>(null);
|
|
|
|
useEffect(() => {
|
|
const handleMove = (event: PointerEvent) => {
|
|
const drag = dragRef.current;
|
|
if (!drag) return;
|
|
const next = Math.min(
|
|
Math.max(drag.startHeight + drag.startY - event.clientY, 160),
|
|
Math.floor(window.innerHeight * 0.7)
|
|
);
|
|
onHeightChange(next);
|
|
};
|
|
const handleUp = () => {
|
|
dragRef.current = null;
|
|
document.body.style.cursor = '';
|
|
document.body.style.userSelect = '';
|
|
};
|
|
window.addEventListener('pointermove', handleMove);
|
|
window.addEventListener('pointerup', handleUp);
|
|
return () => {
|
|
window.removeEventListener('pointermove', handleMove);
|
|
window.removeEventListener('pointerup', handleUp);
|
|
};
|
|
}, [onHeightChange]);
|
|
|
|
if (!activeTab) return null;
|
|
|
|
return (
|
|
<section
|
|
aria-label="하단 패널"
|
|
style={{
|
|
position: 'absolute',
|
|
left: 44,
|
|
right: 0,
|
|
bottom: 0,
|
|
height,
|
|
minHeight: 160,
|
|
maxHeight: '70vh',
|
|
zIndex: 300,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
background: 'var(--bg-panel)',
|
|
borderTop: '1px solid var(--border-strong)',
|
|
boxShadow: '0 -4px 16px rgba(15, 23, 42, 0.12)',
|
|
}}
|
|
>
|
|
<div
|
|
role="separator"
|
|
aria-orientation="horizontal"
|
|
aria-label="하단 패널 높이 조절"
|
|
onPointerDown={(event) => {
|
|
dragRef.current = { startY: event.clientY, startHeight: height };
|
|
document.body.style.cursor = 'ns-resize';
|
|
document.body.style.userSelect = 'none';
|
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
}}
|
|
style={{
|
|
position: 'absolute',
|
|
top: -4,
|
|
left: 0,
|
|
right: 0,
|
|
height: 8,
|
|
cursor: 'ns-resize',
|
|
zIndex: 2,
|
|
}}
|
|
/>
|
|
|
|
<header
|
|
style={{
|
|
flex: '0 0 34px',
|
|
display: 'flex',
|
|
alignItems: 'stretch',
|
|
justifyContent: 'space-between',
|
|
background: 'var(--bg-subtle)',
|
|
borderBottom: '1px solid var(--border)',
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'stretch' }}>
|
|
<button
|
|
onClick={() => onToggleTab('table')}
|
|
style={{
|
|
minWidth: 116,
|
|
border: 'none',
|
|
borderBottom: activeTab === 'table' ? '2px solid var(--accent)' : '2px solid transparent',
|
|
background: 'transparent',
|
|
color: activeTab === 'table' ? 'var(--text)' : 'var(--text-secondary)',
|
|
fontSize: 'var(--fs-xs)',
|
|
fontWeight: activeTab === 'table' ? 600 : 500,
|
|
cursor: 'pointer',
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 'var(--s-2)',
|
|
}}
|
|
title="요소 테이블 열기/닫기 (Ctrl+T)"
|
|
>
|
|
<Table size={13} />
|
|
요소 테이블
|
|
</button>
|
|
{epsAvailable && (
|
|
<button
|
|
onClick={() => onToggleTab('eps')}
|
|
style={{
|
|
minWidth: 116,
|
|
border: 'none',
|
|
borderBottom: activeTab === 'eps' ? '2px solid var(--accent)' : '2px solid transparent',
|
|
background: 'transparent',
|
|
color: activeTab === 'eps' ? 'var(--text)' : 'var(--text-secondary)',
|
|
fontSize: 'var(--fs-xs)',
|
|
fontWeight: activeTab === 'eps' ? 600 : 500,
|
|
cursor: 'pointer',
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 'var(--s-2)',
|
|
}}
|
|
title="EPS 시간 컨트롤러 열기/닫기"
|
|
>
|
|
<Activity size={13} />
|
|
EPS 시간
|
|
</button>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={() => onToggleTab(activeTab)}
|
|
style={{
|
|
width: 34,
|
|
border: 'none',
|
|
background: 'transparent',
|
|
color: 'var(--text-muted)',
|
|
cursor: 'pointer',
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
}}
|
|
title="하단 패널 닫기"
|
|
aria-label="하단 패널 닫기"
|
|
>
|
|
<X size={14} />
|
|
</button>
|
|
</header>
|
|
|
|
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden', padding: activeTab === 'table' ? 0 : 'var(--s-4)' }}>
|
|
{activeTab === 'table' ? tableContent : epsContent}
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|