483 lines
19 KiB
TypeScript
483 lines
19 KiB
TypeScript
/**
|
|
* 최상단 글로벌 메뉴바
|
|
*
|
|
* 레이아웃:
|
|
* [로고] | [그룹메뉴 ▾] … | [테마토글] [로그인]
|
|
*/
|
|
import React, { memo, useEffect, useState } from 'react';
|
|
import type { Theme } from '../types';
|
|
import type { AuthSession } from '../utils/auth';
|
|
import type { TradeAlertPopupLayout, TradeAlertPopupPosition } from '../utils/tradeAlertPopupLayout';
|
|
import { canAccessMenu } from '../utils/permissions';
|
|
import { isDesktop } from '../utils/platform';
|
|
import { useAppTitleBarVersion } from '../hooks/useDesktopAppTitle';
|
|
import DesktopUpdateModal from './DesktopUpdateModal';
|
|
import TopMenuBarNavGroups from './TopMenuBarNavGroups';
|
|
|
|
export type MenuPage = 'dashboard' | 'chart' | 'paper' | 'virtual' | 'trend-search' | 'verification-board' | 'strategy' | 'strategy-editor' | 'strategy-evaluation' | 'backtest' | 'analysis-report' | 'notifications' | 'settings' | 'widget-dashboard';
|
|
|
|
interface TopMenuBarProps {
|
|
activePage: MenuPage;
|
|
theme: Theme;
|
|
onPage: (page: MenuPage) => void;
|
|
onTheme: () => void;
|
|
/** 미확인 매매 시그널 알림 수 — 알림목록 메뉴 뱃지로 표시 */
|
|
tradeNotifyUnread?: number;
|
|
/** @deprecated 알림 아이콘 버튼 제거됨, 하위 호환 유지 */
|
|
onOpenNotifications?: () => void;
|
|
/** 모바일 앱 다운로드 모달 */
|
|
onOpenAppDownload?: () => 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;
|
|
/** 브라우저 전체화면 (모든 화면에서 사용) */
|
|
onFullscreen?: () => void;
|
|
/** 플로팅 위젯 레이아웃 선택 열기 */
|
|
onOpenFloatingWidgets?: () => void;
|
|
/** 실행 중인 플로팅 위젯 창 수 */
|
|
floatingWidgetCount?: number;
|
|
/** 데스크톱 — 현재 화면 데이터 새로고침 */
|
|
onRefresh?: () => 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 IcAnalysisReport = () => (
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M5 2h8a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/>
|
|
<path d="M3 5H2a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h8"/>
|
|
<line x1="7" y1="6" x2="11" y2="6"/>
|
|
<line x1="7" y1="9" x2="11" y2="9"/>
|
|
<line x1="7" y1="12" x2="9" y2="12"/>
|
|
</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 IcAppDownload = () => (
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<rect x="4.5" y="1.5" width="7" height="13" rx="1.5"/>
|
|
<line x1="8" y1="9" x2="8" y2="12.5"/>
|
|
<polyline points="6,10.5 8,12.5 10,10.5"/>
|
|
</svg>
|
|
);
|
|
|
|
const IcDesktopUpdate = () => (
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M13.5 8a5.5 5.5 0 1 1-1.6-3.9"/>
|
|
<polyline points="13.5,2.5 13.5,5.5 10.5,5.5"/>
|
|
</svg>
|
|
);
|
|
|
|
const IcRefresh = () => (
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M2 8a6 6 0 0 1 10.2-4.2"/>
|
|
<polyline points="12,2 12,5.5 8.5,5.5"/>
|
|
<path d="M14 8a6 6 0 0 1-10.2 4.2"/>
|
|
<polyline points="4,14 4,10.5 7.5,10.5"/>
|
|
</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 IcFullscreen = () => (
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<polyline points="1,6 1,1 6,1"/>
|
|
<polyline points="10,1 15,1 15,6"/>
|
|
<polyline points="15,10 15,15 10,15"/>
|
|
<polyline points="6,15 1,15 1,10"/>
|
|
</svg>
|
|
);
|
|
|
|
const IcFullscreenExit = () => (
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<polyline points="6,6 1,1"/>
|
|
<polyline points="10,6 15,1"/>
|
|
<polyline points="6,10 1,15"/>
|
|
<polyline points="10,10 15,15"/>
|
|
</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 IcStrategyEditor = () => (
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<rect x="1.5" y="2" width="13" height="12" rx="1.5"/>
|
|
<circle cx="4.5" cy="6" r="1.2" fill="currentColor" stroke="none"/>
|
|
<circle cx="8" cy="8" r="1.2" fill="currentColor" stroke="none"/>
|
|
<circle cx="11.5" cy="10" r="1.2" fill="currentColor" stroke="none"/>
|
|
<path d="M5.5 6 L7 8 L10.5 10"/>
|
|
</svg>
|
|
);
|
|
|
|
const IcStrategyEvaluation = () => (
|
|
<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="1.5" width="5" height="13" rx="1"/>
|
|
<line x1="3.5" y1="13" x2="3.5" y2="7"/>
|
|
<line x1="5.5" y1="13" x2="5.5" y2="5"/>
|
|
<rect x="8.5" y="3" width="5.5" height="3" rx="0.8"/>
|
|
<rect x="8.5" y="7.5" width="5.5" height="3" rx="0.8"/>
|
|
<rect x="8.5" y="12" width="5.5" height="2.5" rx="0.8"/>
|
|
<polyline points="13.5,4.5 12.5,5.5 11,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="1.5" y="4" width="13" height="9" rx="1.5"/>
|
|
<path d="M1.5 6.5h13"/>
|
|
<circle cx="8" cy="10" r="1.5"/>
|
|
</svg>
|
|
);
|
|
|
|
const IcVirtual = () => (
|
|
<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="2" width="12" height="12" rx="1.5"/>
|
|
<path d="M5 8h6M8 5v6"/>
|
|
<circle cx="11.5" cy="4.5" r="1.2" fill="currentColor" stroke="none"/>
|
|
</svg>
|
|
);
|
|
|
|
const IcTrendSearch = () => (
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<circle cx="7" cy="7" r="4.5"/>
|
|
<path d="M10.5 10.5L14 14"/>
|
|
<path d="M5 7h4M7 5v4"/>
|
|
</svg>
|
|
);
|
|
|
|
const IcVerificationBoard = () => (
|
|
<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="1.5" width="12" height="13" rx="1.5"/>
|
|
<path d="M5 5h6M5 8h6M5 11h4"/>
|
|
<path d="M11.5 10.5l1 1 2-2" strokeWidth="1.3"/>
|
|
</svg>
|
|
);
|
|
|
|
const IcWidget = () => (
|
|
<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="6" height="6" rx="1"/>
|
|
<rect x="9" y="1" width="6" height="4" rx="1"/>
|
|
<rect x="1" y="9" width="6" height="6" rx="1"/>
|
|
<rect x="9" y="7" width="6" height="8" rx="1"/>
|
|
</svg>
|
|
);
|
|
|
|
const IcFloatingWidgetAdd = () => (
|
|
<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="6" height="6" rx="1"/>
|
|
<rect x="9" y="1" width="6" height="6" rx="1"/>
|
|
<rect x="1" y="9" width="6" height="6" rx="1"/>
|
|
<line x1="12" y1="12" x2="12" y2="15"/>
|
|
<line x1="10.5" y1="13.5" x2="13.5" y2="13.5"/>
|
|
</svg>
|
|
);
|
|
|
|
const MENU_ITEMS: { page: MenuPage; label: string; icon: React.ReactNode }[] = [
|
|
{ page: 'dashboard', label: '대시보드', icon: <IcDashboard /> },
|
|
{ page: 'widget-dashboard', label: '위젯', icon: <IcWidget /> },
|
|
{ page: 'chart', label: '실시간차트', icon: <IcChart /> },
|
|
{ page: 'paper', label: '투자관리', icon: <IcPaper /> },
|
|
{ page: 'virtual', label: '가상매매', icon: <IcVirtual /> },
|
|
{ page: 'trend-search', label: '추세검색', icon: <IcTrendSearch /> },
|
|
{ page: 'strategy-editor', label: '전략편집기', icon: <IcStrategyEditor /> },
|
|
{ page: 'strategy-evaluation', label: '전략 평가', icon: <IcStrategyEvaluation /> },
|
|
{ page: 'backtest', label: '백테스팅', icon: <IcBacktest /> },
|
|
{ page: 'analysis-report', label: '분석레포트', icon: <IcAnalysisReport /> },
|
|
{ page: 'notifications', label: '알림목록', icon: <IcNotify /> },
|
|
{ page: 'settings', label: '설정', icon: <IcSettings /> },
|
|
{ page: 'verification-board', label: '검증게시판', icon: <IcVerificationBoard /> },
|
|
];
|
|
|
|
/** 기능별 그룹 — 클릭 시 하단 드롭다운 */
|
|
const MENU_GROUPS = [
|
|
{
|
|
id: 'home',
|
|
label: '홈·분석',
|
|
icon: <IcDashboard />,
|
|
pages: ['dashboard', 'widget-dashboard', 'analysis-report'] as MenuPage[],
|
|
},
|
|
{
|
|
id: 'trading',
|
|
label: '차트·매매',
|
|
icon: <IcChart />,
|
|
pages: ['chart', 'paper', 'virtual', 'trend-search'] as MenuPage[],
|
|
},
|
|
{
|
|
id: 'strategy',
|
|
label: '전략',
|
|
icon: <IcStrategyEditor />,
|
|
pages: ['strategy-editor', 'strategy-evaluation', 'backtest'] as MenuPage[],
|
|
},
|
|
{
|
|
id: 'system',
|
|
label: '알림·설정',
|
|
icon: <IcSettings />,
|
|
pages: ['notifications', 'verification-board', 'settings'] as MenuPage[],
|
|
},
|
|
];
|
|
|
|
export const TopMenuBar = memo(function TopMenuBar({
|
|
activePage, theme, onPage, onTheme,
|
|
tradeNotifyUnread = 0,
|
|
onOpenAppDownload,
|
|
tradeNotifyToastCount = 0,
|
|
onDismissAllNotifyPopups,
|
|
tradeAlertPopupPosition = 'right',
|
|
tradeAlertPopupLayout = 'stack',
|
|
tradeAlertPopupGridCols = 2,
|
|
onTradeAlertPopupPosition,
|
|
onTradeAlertPopupLayout,
|
|
onTradeAlertPopupGridCols,
|
|
authUser = null,
|
|
guestMode = false,
|
|
onLoginClick,
|
|
onLogout,
|
|
onFullscreen,
|
|
onOpenFloatingWidgets,
|
|
floatingWidgetCount = 0,
|
|
onRefresh,
|
|
menuPermissions,
|
|
}: TopMenuBarProps) {
|
|
const [isFullscreen, setIsFullscreen] = useState(() => !!document.fullscreenElement);
|
|
const [desktopUpdateOpen, setDesktopUpdateOpen] = useState(false);
|
|
const [refreshBusy, setRefreshBusy] = useState(false);
|
|
const desktopClient = isDesktop();
|
|
const appVersion = useAppTitleBarVersion();
|
|
|
|
useEffect(() => {
|
|
const sync = () => setIsFullscreen(!!document.fullscreenElement);
|
|
document.addEventListener('fullscreenchange', sync);
|
|
return () => document.removeEventListener('fullscreenchange', sync);
|
|
}, []);
|
|
|
|
const tc = THEME_CONFIG[theme];
|
|
const visibleItems = menuPermissions
|
|
? MENU_ITEMS.filter(({ page }) => canAccessMenu(menuPermissions, page))
|
|
: MENU_ITEMS;
|
|
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>
|
|
{appVersion ? (
|
|
<span className="tmb-logo-version">(v{appVersion})</span>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="tmb-divider" />
|
|
|
|
{/* 기능별 그룹 메뉴 */}
|
|
<TopMenuBarNavGroups
|
|
activePage={activePage}
|
|
items={visibleItems}
|
|
groups={MENU_GROUPS}
|
|
onPage={onPage}
|
|
tradeNotifyUnread={tradeNotifyUnread}
|
|
/>
|
|
|
|
<span className="tmb-divider" aria-hidden="true" />
|
|
|
|
{/* 우측 영역 */}
|
|
<div className="tmb-right">
|
|
{desktopClient && onRefresh && (
|
|
<button
|
|
type="button"
|
|
className={`tmb-refresh-btn${refreshBusy ? ' busy' : ''}`}
|
|
onClick={() => {
|
|
if (refreshBusy) return;
|
|
setRefreshBusy(true);
|
|
try {
|
|
onRefresh();
|
|
} finally {
|
|
window.setTimeout(() => setRefreshBusy(false), 600);
|
|
}
|
|
}}
|
|
title="현재 화면 새로고침"
|
|
aria-label="현재 화면 새로고침"
|
|
>
|
|
<IcRefresh />
|
|
</button>
|
|
)}
|
|
|
|
{onOpenFloatingWidgets && (
|
|
<div className="tmb-floating-widget-wrap">
|
|
<button
|
|
type="button"
|
|
className="tmb-floating-widget-btn"
|
|
onClick={onOpenFloatingWidgets}
|
|
title="플로팅 위젯 추가"
|
|
aria-label="플로팅 위젯 추가"
|
|
>
|
|
<IcFloatingWidgetAdd />
|
|
</button>
|
|
{floatingWidgetCount > 0 && (
|
|
<span className="tmb-floating-widget-count" aria-hidden>
|
|
{floatingWidgetCount > 9 ? '9+' : floatingWidgetCount}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{desktopClient ? (
|
|
<button
|
|
type="button"
|
|
className="tmb-app-download-btn tmb-desktop-update-btn"
|
|
onClick={() => setDesktopUpdateOpen(true)}
|
|
title="PC 프로그램 업데이트"
|
|
aria-label="PC 프로그램 업데이트"
|
|
>
|
|
<IcDesktopUpdate />
|
|
</button>
|
|
) : onOpenAppDownload ? (
|
|
<button
|
|
type="button"
|
|
className="tmb-app-download-btn"
|
|
onClick={onOpenAppDownload}
|
|
title="GoldenChart 앱·PC 프로그램 다운로드"
|
|
aria-label="앱·PC 프로그램 다운로드"
|
|
>
|
|
<IcAppDownload />
|
|
</button>
|
|
) : null}
|
|
|
|
{onFullscreen && (
|
|
<>
|
|
<button
|
|
type="button"
|
|
className="tmb-fullscreen-btn"
|
|
onClick={onFullscreen}
|
|
title={isFullscreen ? '전체화면 종료 (Esc)' : '전체화면'}
|
|
aria-label={isFullscreen ? '전체화면 종료' : '전체화면'}
|
|
>
|
|
{isFullscreen ? <IcFullscreenExit /> : <IcFullscreen />}
|
|
</button>
|
|
<span className="tmb-divider" aria-hidden="true" />
|
|
</>
|
|
)}
|
|
|
|
{/* 테마 토글 버튼 */}
|
|
<button
|
|
type="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>
|
|
|
|
<span className="tmb-divider" aria-hidden="true" />
|
|
|
|
{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>
|
|
{desktopClient && (
|
|
<DesktopUpdateModal
|
|
open={desktopUpdateOpen}
|
|
onClose={() => setDesktopUpdateOpen(false)}
|
|
autoStart
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
});
|
|
|
|
export default TopMenuBar;
|