goldenChat base source add
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* 최상단 글로벌 메뉴바
|
||||
*
|
||||
* 레이아웃:
|
||||
* [로고] | [대시보드] [실시간차트] [투자전략] | [테마토글] [로그인]
|
||||
*/
|
||||
import React, { memo } from 'react';
|
||||
import type { Theme } from '../types';
|
||||
import type { AuthSession } from '../utils/auth';
|
||||
import TradeAlertPopupMenubarSelect from './TradeAlertPopupMenubarSelect';
|
||||
import type { TradeAlertPopupLayout, TradeAlertPopupPosition } from '../utils/tradeAlertPopupLayout';
|
||||
|
||||
export type MenuPage = 'dashboard' | 'chart' | 'paper' | 'strategy' | 'backtest' | 'notifications' | 'settings';
|
||||
|
||||
interface TopMenuBarProps {
|
||||
activePage: MenuPage;
|
||||
theme: Theme;
|
||||
onPage: (page: MenuPage) => void;
|
||||
onTheme: () => void;
|
||||
/** 미확인 매매 시그널 알림 수 */
|
||||
tradeNotifyUnread?: number;
|
||||
onOpenNotifications?: () => void;
|
||||
/** 우측에 떠 있는 알림 팝업(토스트) 개수 */
|
||||
tradeNotifyToastCount?: number;
|
||||
/** 모든 알림 팝업 닫기 */
|
||||
onDismissAllNotifyPopups?: () => void;
|
||||
/** 알림 팝업 표시 위치·방식 */
|
||||
tradeAlertPopupPosition?: string;
|
||||
tradeAlertPopupLayout?: string;
|
||||
tradeAlertPopupGridCols?: number;
|
||||
onTradeAlertPopupPosition?: (v: TradeAlertPopupPosition) => void;
|
||||
onTradeAlertPopupLayout?: (v: TradeAlertPopupLayout) => void;
|
||||
onTradeAlertPopupGridCols?: (v: number) => void;
|
||||
/** 로그인 사용자 (null = 비로그인·기기별 설정) */
|
||||
authUser?: AuthSession | null;
|
||||
/** 게스트 모드(스플래시에서 게스트 진입) */
|
||||
guestMode?: boolean;
|
||||
onLoginClick?: () => void;
|
||||
onLogout?: () => void;
|
||||
/** 메뉴별 접근 허용 (없으면 전체 표시) */
|
||||
menuPermissions?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
// ── SVG 아이콘 ────────────────────────────────────────────────────────────
|
||||
const IcDashboard = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="1" y="1" width="5.5" height="5.5" rx="1"/>
|
||||
<rect x="9.5" y="1" width="5.5" height="5.5" rx="1"/>
|
||||
<rect x="1" y="9.5" width="5.5" height="5.5" rx="1"/>
|
||||
<rect x="9.5" y="9.5" width="5.5" height="5.5" rx="1"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IcChart = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="1,12 5,7 8,9 11,4 15,4"/>
|
||||
<line x1="3" y1="14" x2="3" y2="10"/>
|
||||
<line x1="6.5" y1="14" x2="6.5" y2="11.5"/>
|
||||
<line x1="10" y1="14" x2="10" y2="7"/>
|
||||
<line x1="13.5" y1="14" x2="13.5" y2="4"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IcPaper = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="2" y="3" width="12" height="10" rx="1.5"/>
|
||||
<line x1="5" y1="6" x2="11" y2="6"/>
|
||||
<line x1="5" y1="9" x2="9" y2="9"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IcStrategy = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="8" cy="8" r="6.5"/>
|
||||
<circle cx="8" cy="8" r="2.5"/>
|
||||
<line x1="8" y1="1.5" x2="8" y2="5.5"/>
|
||||
<line x1="8" y1="10.5" x2="8" y2="14.5"/>
|
||||
<line x1="1.5" y1="8" x2="5.5" y2="8"/>
|
||||
<line x1="10.5" y1="8" x2="14.5" y2="8"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IcBacktest = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="1" y="9" width="2.5" height="5" rx="0.5"/>
|
||||
<rect x="4.5" y="6" width="2.5" height="8" rx="0.5"/>
|
||||
<rect x="8" y="3.5" width="2.5" height="10.5" rx="0.5"/>
|
||||
<rect x="11.5" y="1" width="2.5" height="13" rx="0.5"/>
|
||||
<polyline points="2.25,8 5.75,5 9.25,3 12.75,0.5" strokeDasharray="2 1.5"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IcSettings = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="8" cy="8" r="2.5"/>
|
||||
<path d="M8 1.5v1.3M8 13.2v1.3M1.5 8h1.3M13.2 8h1.3M3.4 3.4l.9.9M11.7 11.7l.9.9M3.4 12.6l.9-.9M11.7 4.3l.9-.9"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IcNotify = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M8 1.5a4.5 4.5 0 0 0-4.5 4.5c0 3.5-1 4.5-1.5 4.5h12c-.5 0-1.5-1-1.5-4.5A4.5 4.5 0 0 0 8 1.5z"/>
|
||||
<path d="M6.5 13.5a1.5 1.5 0 0 0 3 0"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
/** 알림 팝업 전체 닫기 */
|
||||
const IcDismissPopups = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="2" y="3" width="9" height="10" rx="1"/>
|
||||
<path d="M11 5.5 14 2.5M14 5.5 11 2.5"/>
|
||||
<line x1="4.5" y1="6.5" x2="8.5" y2="10.5"/>
|
||||
<line x1="8.5" y1="6.5" x2="4.5" y2="10.5"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IcLogin = () => (
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M9 2h3a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H9"/>
|
||||
<polyline points="6,10.5 9,7.5 6,4.5"/>
|
||||
<line x1="1.5" y1="7.5" x2="9" y2="7.5"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// ── 테마 아이콘 ───────────────────────────────────────────────────────────
|
||||
const THEME_CONFIG: Record<Theme, { icon: string; label: string; next: Theme }> = {
|
||||
dark: { icon: '🌙', label: '다크', next: 'blue' },
|
||||
blue: { icon: '🌊', label: '블루', next: 'light' },
|
||||
light: { icon: '☀️', label: '라이트', next: 'dark' },
|
||||
};
|
||||
|
||||
const MENU_ITEMS: { page: MenuPage; label: string; icon: React.ReactNode }[] = [
|
||||
{ page: 'dashboard', label: '대시보드', icon: <IcDashboard /> },
|
||||
{ page: 'chart', label: '실시간차트', icon: <IcChart /> },
|
||||
{ page: 'paper', label: '모의투자', icon: <IcPaper /> },
|
||||
{ page: 'strategy', label: '투자전략', icon: <IcStrategy /> },
|
||||
{ page: 'backtest', label: '백테스팅', icon: <IcBacktest /> },
|
||||
{ page: 'settings', label: '설정', icon: <IcSettings /> },
|
||||
];
|
||||
|
||||
export const TopMenuBar = memo(function TopMenuBar({
|
||||
activePage, theme, onPage, onTheme,
|
||||
tradeNotifyUnread = 0,
|
||||
onOpenNotifications,
|
||||
tradeNotifyToastCount = 0,
|
||||
onDismissAllNotifyPopups,
|
||||
tradeAlertPopupPosition = 'right',
|
||||
tradeAlertPopupLayout = 'stack',
|
||||
tradeAlertPopupGridCols = 2,
|
||||
onTradeAlertPopupPosition,
|
||||
onTradeAlertPopupLayout,
|
||||
onTradeAlertPopupGridCols,
|
||||
authUser = null,
|
||||
guestMode = false,
|
||||
onLoginClick,
|
||||
onLogout,
|
||||
menuPermissions,
|
||||
}: TopMenuBarProps) {
|
||||
const tc = THEME_CONFIG[theme];
|
||||
const visibleItems = menuPermissions
|
||||
? MENU_ITEMS.filter(({ page }) => menuPermissions[page] === true)
|
||||
: MENU_ITEMS;
|
||||
const showNotifications = menuPermissions == null || menuPermissions.notifications === true;
|
||||
|
||||
return (
|
||||
<header className="top-menubar">
|
||||
{/* 로고 */}
|
||||
<div className="tmb-logo">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<rect width="20" height="20" rx="4" fill="#2196f3"/>
|
||||
<polyline points="3,14 7,9 10,12 14,6 17,6" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
|
||||
</svg>
|
||||
<span className="tmb-logo-text">GoldenChart</span>
|
||||
</div>
|
||||
|
||||
<div className="tmb-divider" />
|
||||
|
||||
{/* 내비게이션 메뉴 */}
|
||||
<nav className="tmb-nav">
|
||||
{visibleItems.map(({ page, label, icon }) => (
|
||||
<button
|
||||
key={page}
|
||||
className={`tmb-nav-btn ${activePage === page ? 'tmb-nav-btn--active' : ''}`}
|
||||
onClick={() => onPage(page)}
|
||||
>
|
||||
<span className="tmb-nav-icon">{icon}</span>
|
||||
<span className="tmb-nav-label">{label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* 우측 영역 */}
|
||||
<div className="tmb-right">
|
||||
{onOpenNotifications && showNotifications && (
|
||||
<div className="tmb-notify-group">
|
||||
<button
|
||||
type="button"
|
||||
className={`tmb-notify-btn ${activePage === 'notifications' ? 'tmb-nav-btn--active' : ''}`}
|
||||
onClick={onOpenNotifications}
|
||||
title="매매 시그널 알림"
|
||||
aria-label="매매 시그널 알림"
|
||||
>
|
||||
<IcNotify />
|
||||
{tradeNotifyUnread > 0 && (
|
||||
<span className="tmb-notify-badge">
|
||||
{tradeNotifyUnread > 99 ? '99+' : tradeNotifyUnread}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<TradeAlertPopupMenubarSelect
|
||||
position={tradeAlertPopupPosition}
|
||||
layout={tradeAlertPopupLayout}
|
||||
gridCols={tradeAlertPopupGridCols}
|
||||
onPositionChange={onTradeAlertPopupPosition}
|
||||
onLayoutChange={onTradeAlertPopupLayout}
|
||||
onGridColsChange={onTradeAlertPopupGridCols}
|
||||
/>
|
||||
{onDismissAllNotifyPopups && (
|
||||
<button
|
||||
type="button"
|
||||
className="tmb-notify-dismiss-btn"
|
||||
onClick={onDismissAllNotifyPopups}
|
||||
disabled={tradeNotifyToastCount <= 0}
|
||||
title={
|
||||
tradeNotifyToastCount > 0
|
||||
? `알림 팝업 ${tradeNotifyToastCount}건 전체 닫기`
|
||||
: '닫을 알림 팝업 없음'
|
||||
}
|
||||
aria-label="알림 팝업 전체 닫기"
|
||||
>
|
||||
<IcDismissPopups />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 테마 토글 버튼 */}
|
||||
<button
|
||||
className={`tmb-theme-btn tmb-theme-btn--${theme}`}
|
||||
onClick={onTheme}
|
||||
title={`테마 전환 (현재: ${tc.label}) → ${THEME_CONFIG[tc.next].label}`}
|
||||
>
|
||||
<span className="tmb-theme-icon">{tc.icon}</span>
|
||||
<span className="tmb-theme-label">{tc.label}</span>
|
||||
</button>
|
||||
|
||||
<div className="tmb-sep" />
|
||||
|
||||
{authUser ? (
|
||||
<>
|
||||
<span className="tmb-user-badge" title={`${authUser.displayName} (${authUser.username}) · ${authUser.role}`}>
|
||||
{authUser.displayName}
|
||||
<span className="tmb-user-role">{authUser.role}</span>
|
||||
</span>
|
||||
<button type="button" className="tmb-login-btn tmb-login-btn--out" onClick={onLogout}>
|
||||
로그아웃
|
||||
</button>
|
||||
</>
|
||||
) : guestMode ? (
|
||||
<>
|
||||
<span className="tmb-guest-badge" title="게스트 권한으로 실행 중">게스트</span>
|
||||
<button type="button" className="tmb-login-btn" onClick={onLoginClick}>
|
||||
<IcLogin />
|
||||
<span>로그인</span>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button type="button" className="tmb-login-btn" onClick={onLoginClick}>
|
||||
<IcLogin />
|
||||
<span>로그인</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
});
|
||||
|
||||
export default TopMenuBar;
|
||||
Reference in New Issue
Block a user