Files
SPipe/src/BottomPanel.tsx
T
bjkim1 21ebb16fb7 SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음)
- README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리
- 패치내역을 docs/로 이동, _worklog.md 추가
- 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
2026-10-30 11:00:46 +09:00

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>
);
};