SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
This commit is contained in:
commit
21ebb16fb7
181 files changed
+57207
No files matched your search
@@ -0,0 +1,172 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user