diff --git a/frontend/src/App.css b/frontend/src/App.css index 237fd92..147ff4f 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -6356,116 +6356,405 @@ .bt-drop-run.running { background: var(--bg4); color: var(--text2); } /* ══════════════════════════════════════════════════════════════════ - 실시간 전략 체크 패널 (LiveStrategyPanel) + 실시간 전략 체크 패널 (LiveStrategyPanel) — TradeAlertModal 스타일 ══════════════════════════════════════════════════════════════════ */ -.lsp-wrap { +.lsp-overlay { + position: fixed; + inset: 0; + z-index: 9998; + pointer-events: none; +} +.lsp-overlay > .lsp-modal { + pointer-events: auto; +} + +.lsp-modal { + position: fixed; + width: 400px; + max-width: calc(100vw - 24px); + max-height: calc(100vh - 24px); display: flex; flex-direction: column; - gap: 8px; - min-width: 240px; - padding: 12px 14px; - background: var(--bg2); - border: 1px solid var(--border); - border-radius: 8px; - font-size: 12px; - color: var(--text); + background: var(--bg2, #24283b); + border: 1px solid var(--lsp-accent, var(--border, rgba(122,162,247,0.15))); + border-radius: 12px; + box-shadow: + 0 0 0 1px var(--lsp-accent-glow, rgba(122,162,247,0.08)), + 0 20px 60px rgba(0,0,0,0.65), + 0 0 40px var(--lsp-accent-glow, rgba(122,162,247,0.06)); + overflow: hidden; + font-size: 13px; + color: var(--text, #c0caf5); + font-family: var(--font, 'Noto Sans KR', sans-serif); } .lsp-header { - display: flex; - align-items: center; - gap: 6px; - padding-bottom: 8px; - border-bottom: 1px solid var(--border); + flex-shrink: 0; } -.lsp-icon { opacity: 0.75; flex-shrink: 0; color: #00BCD4; } -.lsp-title { font-weight: 700; font-size: 12.5px; flex: 1; } -.lsp-saving { font-size: 10px; color: var(--text3); } -.lsp-close-btn { +.lsp-header-left { display: flex; align-items: center; - justify-content: center; - width: 22px; - height: 22px; + gap: 9px; + flex: 1; + min-width: 0; +} +.lsp-badge { + display: inline-block; + padding: 3px 10px; + border-radius: 20px; + font-size: 11px; + font-weight: 800; + color: #fff; + letter-spacing: 1.2px; + flex-shrink: 0; +} +.lsp-header-title { + font-size: 14px; + font-weight: 700; + color: var(--text, #c0caf5); + letter-spacing: 0.4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.lsp-header-ko { + font-size: 12px; + font-weight: 400; + color: var(--text2, #9aa5ce); +} +.lsp-saving { + font-size: 10px; + color: var(--text3, #6272a4); + flex-shrink: 0; +} +.lsp-close { background: none; border: none; - border-radius: 4px; - color: var(--text3); + color: var(--text3, #6272a4); + font-size: 16px; cursor: pointer; - padding: 0; + padding: 2px 6px; + line-height: 1; + border-radius: 4px; + transition: color 0.15s, background 0.15s; flex-shrink: 0; - transition: background 0.15s, color 0.15s; } -.lsp-close-btn:hover { background: var(--bg3); color: var(--text); } +.lsp-close:hover { + color: var(--text, #c0caf5); + background: var(--bg4, rgba(255,255,255,0.07)); +} -.lsp-row { +.lsp-summary-section { + padding: 12px 16px 10px; + background: linear-gradient( + 135deg, + var(--lsp-accent-glow, rgba(122,162,247,0.08)) 0%, + transparent 60% + ); + border-bottom: 1px solid var(--border, rgba(122,162,247,0.1)); + flex-shrink: 0; +} +.lsp-summary-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; + margin-bottom: 4px; +} +.lsp-summary-label { + font-size: 11.5px; + color: var(--text2, #9aa5ce); +} +.lsp-summary-value { + font-size: 15px; + font-weight: 800; + color: var(--text, #c0caf5); + letter-spacing: -0.3px; +} +.lsp-summary-value--active { + color: var(--lsp-accent, #3f7ef5); +} +.lsp-summary-hint { + margin: 6px 0 0; + font-size: 10.5px; + color: var(--text3, #6272a4); + line-height: 1.45; } -.lsp-row--disabled { opacity: 0.45; pointer-events: none; } -.lsp-label { color: var(--text2); white-space: nowrap; } - -/* 토글 (stg-toggle 재활용 패턴) */ -.lsp-toggle { position: relative; display: inline-flex; align-items: center; cursor: pointer; } -.lsp-toggle input { opacity: 0; width: 0; height: 0; position: absolute; } -.lsp-toggle-slider { - width: 34px; height: 18px; - background: var(--bg3); +.lsp-info-card { + display: flex; + align-items: flex-start; + gap: 12px; + margin: 12px 14px 0; + padding: 11px 14px; + background: var(--bg3, #1f2335); + border: 1px solid var(--border, rgba(122,162,247,0.12)); border-radius: 9px; - transition: background 0.2s; + flex-shrink: 0; +} +.lsp-info-icon { + flex-shrink: 0; + display: flex; + align-items: center; +} +.lsp-info-body { + display: flex; + flex-direction: column; + gap: 6px; + flex: 1; + min-width: 0; +} +.lsp-info-row { + display: flex; + flex-direction: column; + gap: 4px; +} +.lsp-info-label { + font-size: 11px; + color: var(--text3, #6272a4); +} +.lsp-info-val { + font-size: 12.5px; + font-weight: 600; + color: var(--text, #c0caf5); +} + +.lsp-body { + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px 14px 14px; + overflow-y: auto; + flex: 1; + min-height: 0; +} + +.lsp-field-row { + display: flex; + align-items: center; + gap: 8px; +} +.lsp-field-row--disabled, +.lsp-section--disabled { + opacity: 0.45; + pointer-events: none; +} +.lsp-field-label, +.lsp-section-label { + font-size: 11.5px; + color: var(--text2, #9aa5ce); + flex-shrink: 0; + min-width: 62px; +} +.lsp-section-label { + display: block; + margin-bottom: 6px; +} + +.lsp-toggle { + position: relative; + display: inline-flex; + align-items: center; + cursor: pointer; + margin-left: auto; +} +.lsp-toggle input { + opacity: 0; + width: 0; + height: 0; + position: absolute; +} +.lsp-toggle-slider { + width: 38px; + height: 20px; + background: var(--bg3, #1f2335); + border: 1px solid var(--border, rgba(122,162,247,0.18)); + border-radius: 10px; + transition: background 0.2s, border-color 0.2s; position: relative; } .lsp-toggle-slider::after { - content: ''; position: absolute; - width: 14px; height: 14px; + content: ''; + position: absolute; + width: 14px; + height: 14px; border-radius: 50%; background: #fff; - top: 2px; left: 2px; + top: 2px; + left: 2px; transition: transform 0.2s; } -.lsp-toggle input:checked + .lsp-toggle-slider { background: #00BCD4; } -.lsp-toggle input:checked + .lsp-toggle-slider::after { transform: translateX(16px); } +.lsp-toggle input:checked + .lsp-toggle-slider { + background: var(--lsp-accent, #3f7ef5); + border-color: var(--lsp-accent, #3f7ef5); +} +.lsp-toggle input:checked + .lsp-toggle-slider::after { + transform: translateX(18px); +} .lsp-select { flex: 1; - padding: 3px 6px; - background: var(--bg3); - border: 1px solid var(--border); - border-radius: 4px; - color: var(--text); - font-size: 11.5px; - cursor: pointer; min-width: 0; + padding: 7px 10px; + background: var(--bg3, #1f2335); + border: 1px solid var(--border, rgba(122,162,247,0.18)); + border-radius: 6px; + color: var(--text, #c0caf5); + font-size: 12.5px; + font-weight: 600; + cursor: pointer; + outline: none; + transition: border-color 0.15s; +} +.lsp-select:focus { + border-color: var(--lsp-accent, #3f7ef5); +} +.lsp-select:disabled { + cursor: not-allowed; } -.lsp-exec-wrap { +.lsp-section { display: flex; flex-direction: column; - gap: 4px; } -.lsp-radio-group { + +.lsp-option-group { display: flex; flex-direction: column; - gap: 4px; - padding-left: 4px; + gap: 6px; } -.lsp-radio { +.lsp-option { display: flex; align-items: flex-start; - gap: 6px; + gap: 8px; + padding: 9px 11px; + border: 1px solid var(--border, rgba(122,162,247,0.12)); + border-radius: 8px; + background: var(--bg3, #1f2335); cursor: pointer; - padding: 4px 6px; - border-radius: 5px; - transition: background 0.15s; + transition: border-color 0.15s, background 0.15s; } -.lsp-radio:hover { background: var(--bg3); } -.lsp-radio input { margin-top: 2px; accent-color: #00BCD4; flex-shrink: 0; } -.lsp-radio-label { display: flex; flex-direction: column; gap: 1px; } -.lsp-radio-title { font-size: 11.5px; font-weight: 600; } -.lsp-radio-desc { font-size: 10px; color: var(--text3); } +.lsp-option:hover { + border-color: var(--lsp-accent, rgba(63,126,245,0.35)); + background: var(--bg4, #414868); +} +.lsp-option--active { + border-color: var(--lsp-accent, #3f7ef5); + background: rgba(63, 126, 245, 0.1); + box-shadow: inset 0 0 0 1px rgba(63, 126, 245, 0.15); +} +.lsp-option input { + margin-top: 3px; + accent-color: var(--lsp-accent, #3f7ef5); + flex-shrink: 0; +} +.lsp-option-text { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} +.lsp-option-title { + font-size: 12px; + font-weight: 700; + color: var(--text, #c0caf5); +} +.lsp-option-desc { + font-size: 10.5px; + color: var(--text3, #6272a4); + line-height: 1.4; +} + +.lsp-hint { + font-size: 10.5px; + color: var(--text3, #6272a4); + margin: 0; + line-height: 1.45; +} + +.lsp-monitor-chips { + display: flex; + flex-wrap: wrap; + gap: 4px; + font-weight: 400; +} +.lsp-monitor-chip { + font-size: 10px; + padding: 2px 7px; + border-radius: 4px; + background: rgba(63, 126, 245, 0.12); + color: var(--lsp-accent, #3f7ef5); + border: 1px solid rgba(63, 126, 245, 0.25); + font-weight: 600; +} + +.lsp-status-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 9px 11px; + background: var(--bg3, #1f2335); + border: 1px solid var(--border, rgba(122,162,247,0.12)); + border-radius: 8px; +} +.lsp-status-text { + font-size: 11px; + color: var(--text2, #9aa5ce); + line-height: 1.4; + min-width: 0; +} +.lsp-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--text3, #6272a4); + flex-shrink: 0; +} +.lsp-dot--on { + background: var(--lsp-accent, #3f7ef5); + box-shadow: 0 0 6px var(--lsp-accent-glow, rgba(63,126,245,0.5)); + animation: lsp-pulse 1.5s ease-in-out infinite; +} +@keyframes lsp-pulse { + 0%,100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +.lsp-disclaimer { + font-size: 10.5px; + color: var(--text3, #6272a4); + text-align: center; + margin: 2px 0 0; + line-height: 1.5; +} + +/* 라이트 테마 */ +.app.light .lsp-modal { + background: #ffffff; + border-color: rgba(25,118,210,0.35); + color: #212121; + box-shadow: 0 20px 60px rgba(0,0,0,0.18), 0 0 40px rgba(25,118,210,0.08); +} +.app.light .lsp-header-title { color: #1a237e; } +.app.light .lsp-header-ko { color: #546e7a; } +.app.light .lsp-close { color: #78909c; } +.app.light .lsp-close:hover { background: rgba(0,0,0,0.06); color: #37474f; } +.app.light .lsp-summary-section { border-color: rgba(0,0,0,0.08); } +.app.light .lsp-summary-label { color: #546e7a; } +.app.light .lsp-summary-value { color: #212121; } +.app.light .lsp-info-card { background: #f5f7fa; border-color: rgba(0,0,0,0.1); } +.app.light .lsp-info-label { color: #78909c; } +.app.light .lsp-info-val { color: #212121; } +.app.light .lsp-field-label, +.app.light .lsp-section-label { color: #546e7a; } +.app.light .lsp-select { background: #f0f3f8; border-color: rgba(0,0,0,0.12); color: #212121; } +.app.light .lsp-option { background: #f5f7fa; border-color: rgba(0,0,0,0.1); } +.app.light .lsp-option:hover { background: #eef2f7; } +.app.light .lsp-option--active { background: rgba(25,118,210,0.08); border-color: rgba(25,118,210,0.45); } +.app.light .lsp-option-title { color: #212121; } +.app.light .lsp-status-bar { background: #f5f7fa; border-color: rgba(0,0,0,0.1); } +.app.light .lsp-status-text { color: #546e7a; } +.app.light .lsp-monitor-chip { background: rgba(25,118,210,0.1); color: #1976d2; border-color: rgba(25,118,210,0.25); } .lsp-monitor-stats { display: flex; @@ -6476,7 +6765,7 @@ margin-bottom: 6px; } .lsp-monitor-sep { opacity: 0.5; } -.lsp-monitor-on { color: #00BCD4; font-weight: 600; } +.lsp-monitor-on { color: #3f7ef5; font-weight: 600; } .lsp-monitor-warn { font-size: 10.5px; color: #ff9800; @@ -6509,14 +6798,6 @@ gap: 4px; margin-bottom: 8px; } -.lsp-monitor-chip { - font-size: 9.5px; - padding: 2px 6px; - border-radius: 3px; - background: rgba(0, 188, 212, 0.12); - color: #00BCD4; - border: 1px solid rgba(0, 188, 212, 0.25); -} .stg-btn-secondary { background: var(--bg3); color: var(--text); @@ -6545,32 +6826,6 @@ padding: 6px 12px; } -.lsp-status { - display: flex; - align-items: center; - gap: 6px; - font-size: 10.5px; - color: var(--text3); - padding: 4px 6px; - background: var(--bg3); - border-radius: 4px; -} -.lsp-dot { - width: 6px; height: 6px; - border-radius: 50%; - background: var(--text3); - flex-shrink: 0; -} -.lsp-dot--on { - background: #00BCD4; - box-shadow: 0 0 5px rgba(0,188,212,0.6); - animation: lsp-pulse 1.5s ease-in-out infinite; -} -@keyframes lsp-pulse { - 0%,100% { opacity: 1; } - 50% { opacity: 0.4; } -} - /* ══════════════════════════════════════════════ 백테스팅 결과 통계 배지 (차트 위 오버레이) ══════════════════════════════════════════════ */ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8e15c12..343b33e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -93,6 +93,7 @@ import type { LoginResponse } from './utils/backendApi'; import { invalidateAppSettingsCache } from './hooks/useAppSettings'; import { invalidateIndicatorSettingsCache } from './hooks/useIndicatorSettings'; import './App.css'; +import './styles/appPopup.css'; let _indCounter = 0; function newIndId() { return `ind_${++_indCounter}_${Date.now()}`; } @@ -1866,19 +1867,17 @@ function App() { {/* 실시간 전략 체크 패널 */} {showLivePanel && menuPage === 'chart' && layoutDef.count === 1 && ( -
- { clearLiveMarkers(); managerRef.current?.clearLiveStrategyMarkers(); }} - onSettingsChange={handleLiveSettingsChange} - onClose={() => setShowLivePanel(false)} - /> -
+ { clearLiveMarkers(); managerRef.current?.clearLiveStrategyMarkers(); }} + onSettingsChange={handleLiveSettingsChange} + onClose={() => setShowLivePanel(false)} + /> )} {/* 백테스팅 설정 모달 */} diff --git a/frontend/src/components/AppPopup.tsx b/frontend/src/components/AppPopup.tsx new file mode 100644 index 0000000..a401c4d --- /dev/null +++ b/frontend/src/components/AppPopup.tsx @@ -0,0 +1,145 @@ +/** + * AppPopup — TradeAlertModal(tam-) 기준 공통 팝업 셸 + * 신규 팝업은 이 컴포넌트를 사용하세요. + */ +import React from 'react'; +import { createPortal } from 'react-dom'; +import { useDraggablePanel } from '../hooks/useDraggablePanel'; + +export const APP_POPUP_ACCENT = '#3f7ef5'; +export const APP_POPUP_ACCENT_GLOW = 'rgba(63,126,245,0.22)'; + +export interface AppPopupProps { + onClose: () => void; + title: React.ReactNode; + titleKo?: React.ReactNode; + badge?: string; + accentColor?: string; + children: React.ReactNode; + width?: number | string; + maxWidth?: number | string; + className?: string; + shellClassName?: string; + overlayClassName?: string; + bodyClassName?: string; + centered?: boolean; + zIndex?: number; + closeOnBackdrop?: boolean; + backdrop?: boolean; + portal?: boolean; + headerExtra?: React.ReactNode; + footer?: React.ReactNode; + initialPosition?: { x: number; y: number }; +} + +export const AppPopup: React.FC = ({ + onClose, + title, + titleKo, + badge, + accentColor = APP_POPUP_ACCENT, + children, + width = 400, + maxWidth = '96vw', + className, + shellClassName, + overlayClassName, + bodyClassName = 'app-popup-body', + centered = true, + zIndex = 9999, + closeOnBackdrop = true, + backdrop = false, + portal = true, + headerExtra, + footer, + initialPosition, +}) => { + const accentGlow = accentColor === APP_POPUP_ACCENT + ? APP_POPUP_ACCENT_GLOW + : `${accentColor}38`; + + const { + panelRef, + dragging, + onHeaderPointerDown, + headerTouchStyle, + panelStyle, + headerCursor, + } = useDraggablePanel({ centerOnMount: centered, initialPosition }); + + const overlayCls = [ + 'app-popup-overlay', + backdrop ? 'app-popup-overlay--dim' : '', + overlayClassName ?? '', + className ?? '', + ].filter(Boolean).join(' '); + + const shellCls = ['app-popup-shell', shellClassName ?? ''].filter(Boolean).join(' '); + + const content = ( +
{ + if (closeOnBackdrop && backdrop && e.target === e.currentTarget) onClose(); + }} + > +
e.stopPropagation()} + > +
+
+ {badge ? ( + + {badge} + + ) : null} + + {title} + {titleKo != null && titleKo !== '' ? ( + ({titleKo}) + ) : null} + +
+ {headerExtra} + +
+
{children}
+ {footer} +
+
+ ); + + if (portal && typeof document !== 'undefined' && document.body) { + return createPortal(content, document.body); + } + return content; +}; + +export default AppPopup; diff --git a/frontend/src/components/DraggableModalFrame.tsx b/frontend/src/components/DraggableModalFrame.tsx index 49e4fc5..4ce08b0 100644 --- a/frontend/src/components/DraggableModalFrame.tsx +++ b/frontend/src/components/DraggableModalFrame.tsx @@ -1,12 +1,14 @@ /** - * 드래그 가능한 모달 프레임 — 그라데이션 타이틀바(gc-popup-header) + 화면 중앙 초기 배치 + * 드래그 가능한 모달 프레임 — AppPopup 기반 */ import React from 'react'; -import { useDraggablePanel } from '../hooks/useDraggablePanel'; +import AppPopup from './AppPopup'; export interface DraggableModalFrameProps { onClose: () => void; title: React.ReactNode; + titleKo?: React.ReactNode; + badge?: string; children: React.ReactNode; overlayClassName?: string; dialogClassName?: string; @@ -19,60 +21,32 @@ export interface DraggableModalFrameProps { export const DraggableModalFrame: React.FC = ({ onClose, title, + titleKo, + badge, children, overlayClassName = 'sp-modal-overlay', dialogClassName = 'sp-modal', - bodyClassName = 'sp-modal-body', + bodyClassName = 'sp-modal-body app-popup-body', zIndex = 1000, width, closeOnBackdrop = true, -}) => { - const { - panelRef, - dragging, - onHeaderPointerDown, headerTouchStyle, - panelStyle, - headerCursor, - } = useDraggablePanel({ centerOnMount: true }); - - return ( -
{ - if (closeOnBackdrop && e.target === e.currentTarget) onClose(); - }} - > -
e.stopPropagation()} - > -
- {title} - -
-
{children}
-
-
- ); -}; +}) => ( + + {children} + +); export default DraggableModalFrame; diff --git a/frontend/src/components/LiveStrategyPanel.tsx b/frontend/src/components/LiveStrategyPanel.tsx index 9c60452..eace040 100644 --- a/frontend/src/components/LiveStrategyPanel.tsx +++ b/frontend/src/components/LiveStrategyPanel.tsx @@ -5,7 +5,9 @@ * 관심종목(★) 등록 종목 전체가 체크 대상이며, 여기서 전략·실행 방식을 지정합니다. */ import React, { useState, useCallback } from 'react'; +import { createPortal } from 'react-dom'; import type { Theme } from '../types'; +import { useDraggablePanel } from '../hooks/useDraggablePanel'; import { saveLiveStrategySettings, type LiveStrategySettingsDto, @@ -28,6 +30,20 @@ interface LiveStrategyPanelProps { onSettingsChange?: (settings: LiveStrategySettingsDto) => void; } +const ACCENT = '#3f7ef5'; +const ACCENT_GLOW = 'rgba(63,126,245,0.22)'; + +const CandleIcon: React.FC<{ color: string }> = ({ color }) => ( + + + + + + + + +); + const LiveStrategyPanel: React.FC = ({ theme, market, strategies, settings, watchlistCount = 0, monitoredMarkets = [], @@ -35,6 +51,18 @@ const LiveStrategyPanel: React.FC = ({ }) => { const [saving, setSaving] = useState(false); + const { + panelRef, + dragging, + onHeaderPointerDown, + headerTouchStyle, + panelStyle, + headerCursor, + } = useDraggablePanel({ + centerOnMount: true, + initialPosition: { x: 40, y: 40 }, + }); + const persist = useCallback(async (patch: Partial) => { const next: LiveStrategySettingsDto = { ...settings, ...patch, market }; const prev = settings; @@ -61,157 +89,209 @@ const LiveStrategyPanel: React.FC = ({ const execType = settings.executionType; const stratId = settings.strategyId; const monCount = monitoredMarkets.length; + const selectedStrategy = strategies.find(s => s.id === stratId); - return ( -
-
- - - - - 실시간 전략 체크 - {saving && 저장 중…} - {onClose && ( - - )} -
+ const execLabel = execType === 'REALTIME_TICK' ? '실시간 틱 (3초)' : '봉 마감 직후'; + const posModeLabel = (settings.positionMode ?? 'LONG_ONLY') === 'LONG_ONLY' + ? '보유 자산 기준' + : '순수 지표 기준'; -
- 관심 {watchlistCount}개 - · - 0 ? 'lsp-monitor-on' : ''}> - 체크 대상 {monCount}개 - -
- {isOn && stratId && watchlistCount > 0 && ( -

- ★ 관심종목으로 등록한 종목이 자동으로 전략 체크 대상입니다. -

- )} - {monCount > 0 && ( -
- {monitoredMarkets.slice(0, 8).map(m => ( - {m.replace('KRW-', '')} - ))} - {monCount > 8 && +{monCount - 8}} -
- )} - {isOn && !stratId && ( -

전략을 선택하면 관심 {watchlistCount}개 종목에 적용됩니다.

- )} - -
- 실시간 체크 - -
- -
- 전략 선택 - -
+
+ LIVE + + STRATEGY CHECK + (실시간 전략 체크) + +
+ {saving && 저장 중…} + {onClose && ( + + )} +
-
- 실행 방식 -
-
); + + if (typeof document === 'undefined') return null; + return createPortal(panel, document.body); }; export default LiveStrategyPanel; diff --git a/frontend/src/components/LoginModal.tsx b/frontend/src/components/LoginModal.tsx index af0ce1c..a9c6cd3 100644 --- a/frontend/src/components/LoginModal.tsx +++ b/frontend/src/components/LoginModal.tsx @@ -1,8 +1,9 @@ /** - * 로그인 모달 — 기본값 admin / admin + * 로그인 모달 — AppPopup(tam-) 스타일 */ import React, { useEffect, useState } from 'react'; import { loginUser, type LoginResponse } from '../utils/backendApi'; +import AppPopup from './AppPopup'; const DEFAULT_USERNAME = 'admin'; const DEFAULT_PASSWORD = 'admin'; @@ -16,7 +17,7 @@ interface Props { const LoginModal: React.FC = ({ open, onClose, onSuccess }) => { const [username, setUsername] = useState(DEFAULT_USERNAME); const [password, setPassword] = useState(DEFAULT_PASSWORD); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const [loading, setLoading] = useState(false); useEffect(() => { @@ -49,54 +50,53 @@ const LoginModal: React.FC = ({ open, onClose, onSuccess }) => { }; return ( -
-
e.stopPropagation()} - > -
-

로그인

- + +

+ 로그인 시 동일 계정의 설정이 모든 기기에서 공유됩니다. 로그인하지 않으면 이 브라우저(기기)별로 설정이 저장됩니다. +

+
+ + + {error &&

{error}

} +
+ +
-

- 로그인 시 동일 계정의 설정이 모든 기기에서 공유됩니다. 로그인하지 않으면 이 브라우저(기기)별로 설정이 저장됩니다. -

- - - - {error &&

{error}

} -
- - -
-
-
-
+ + ); }; diff --git a/frontend/src/components/PopupShell.tsx b/frontend/src/components/PopupShell.tsx index 2b8a9b3..4c969d1 100644 --- a/frontend/src/components/PopupShell.tsx +++ b/frontend/src/components/PopupShell.tsx @@ -1,81 +1,15 @@ /** - * 공통 팝업 셸 — 그라데이션 타이틀바 + 드래그 이동 + * 공통 팝업 셸 — AppPopup 래퍼 (하위 호환) */ import React from 'react'; -import { useDraggablePanel } from '../hooks/useDraggablePanel'; +import AppPopup, { type AppPopupProps } from './AppPopup'; -export interface PopupShellProps { - onClose: () => void; +export type PopupShellProps = Omit & { title: React.ReactNode; - children: React.ReactNode; - /** 패널 너비 (px 또는 CSS 값) */ - width?: number | string; - className?: string; - dialogClassName?: string; - centered?: boolean; - zIndex?: number; - closeOnBackdrop?: boolean; - initialPosition?: { x: number; y: number }; -} - -export const PopupShell: React.FC = ({ - onClose, - title, - children, - width, - className, - dialogClassName, - centered = true, - zIndex = 5000, - closeOnBackdrop = true, - initialPosition, -}) => { - const { - panelRef, - dragging, - onHeaderPointerDown, headerTouchStyle, - panelStyle, - headerCursor, - } = useDraggablePanel({ centerOnMount: centered, initialPosition }); - - return ( -
{ - if (closeOnBackdrop && e.target === e.currentTarget) onClose(); - }} - > -
e.stopPropagation()} - > -
- {title} - -
- {children} -
-
- ); }; +export const PopupShell: React.FC = (props) => ( + +); + export default PopupShell; diff --git a/frontend/src/components/TradeAlertModal.tsx b/frontend/src/components/TradeAlertModal.tsx index 4fd7da9..da4298f 100644 --- a/frontend/src/components/TradeAlertModal.tsx +++ b/frontend/src/components/TradeAlertModal.tsx @@ -159,7 +159,7 @@ export const TradeAlertModal: React.FC = ({
.app-popup-shell, +.app-popup-overlay--dim > .tam-modal, +.app-popup-overlay--dim > .lsp-modal, +.app-popup-overlay--dim > .gc-popup-panel, +.brm-overlay > .brm-modal, +.ism-overlay > .ism-dialog, +.ibsm-overlay > .ibsm-dialog, +.mcs-overlay > .mcs-dialog, +.dsm-overlay > .dsm-dialog, +.sp-modal-overlay > .sp-modal, +.obj-tree-overlay > .obj-tree-panel, +.ind-overlay > .ind-panel, +.plsp-overlay > .plsp-popup, +.login-modal-backdrop > .login-modal { + pointer-events: auto; +} + +.app-popup-overlay > .app-popup-shell, +.tam-overlay > .tam-modal, +.lsp-overlay > .lsp-modal { + pointer-events: auto; +} + +/* ── 셸 (모달 컨테이너) ── */ +.app-popup-shell, +.tam-modal, +.lsp-modal, +.gc-popup-panel, +.bts-modal, +.brm-modal, +.ism-dialog, +.ibsm-dialog, +.mcs-dialog, +.dsm-dialog, +.sp-modal, +.obj-tree-panel, +.ind-panel, +.plsp-popup, +.indicator-modal, +.login-modal { + position: fixed; + display: flex; + flex-direction: column; + background: var(--bg2, #1e222d); + border: 1px solid var(--app-popup-accent, var(--tam-accent, rgba(63, 126, 245, 0.35))); + border-radius: 12px; + box-shadow: + 0 0 0 1px var(--app-popup-accent-glow, var(--tam-accent-glow, rgba(63, 126, 245, 0.08))), + 0 20px 60px rgba(0, 0, 0, 0.65), + 0 0 40px var(--app-popup-accent-glow, rgba(63, 126, 245, 0.06)); + overflow: hidden; + font-size: 13px; + color: var(--text, #c0caf5); + font-family: var(--font, 'Noto Sans KR', sans-serif); + max-width: 96vw; + max-height: 90vh; +} + +/* ── 헤더 ── */ +.app-popup-header, +.gc-popup-header.tam-header, +.gc-popup-header.lsp-header, +.gc-popup-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 10px 14px; + flex-shrink: 0; + cursor: grab; + user-select: none; + touch-action: none; + border-bottom: 1px solid var(--border, rgba(122, 162, 247, 0.12)); + background: linear-gradient( + 180deg, + rgba(63, 126, 245, 0.14) 0%, + rgba(63, 126, 245, 0.04) 28px, + var(--bg3, #1f2335) 100% + ); +} +.app-popup-header:active, +.gc-popup-header:active { cursor: grabbing; } + +.app-popup-header-left, +.tam-header-left, +.lsp-header-left { + display: flex; + align-items: center; + gap: 9px; + flex: 1; + min-width: 0; +} + +.app-popup-badge, +.tam-badge, +.lsp-badge { + display: inline-block; + padding: 3px 10px; + border-radius: 20px; + font-size: 11px; + font-weight: 800; + color: #fff; + letter-spacing: 1.2px; + flex-shrink: 0; +} + +.app-popup-header-title, +.tam-header-title, +.lsp-header-title, +.gc-popup-title { + font-size: 14px; + font-weight: 700; + color: var(--text, #c0caf5); + letter-spacing: 0.4px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.app-popup-header-ko, +.tam-header-ko, +.lsp-header-ko { + font-size: 12px; + font-weight: 400; + color: var(--text2, #9aa5ce); +} + +.app-popup-close, +.tam-close, +.lsp-close, +.gc-popup-close, +.bts-close, +.brm-close, +.ism-close, +.ibsm-close, +.login-modal-close { + background: none; + border: none; + color: var(--text3, #6272a4); + font-size: 16px; + cursor: pointer; + padding: 2px 6px; + line-height: 1; + border-radius: 4px; + transition: color 0.15s, background 0.15s; + flex-shrink: 0; +} +.app-popup-close:hover, +.tam-close:hover, +.lsp-close:hover, +.gc-popup-close:hover, +.bts-close:hover, +.brm-close:hover { color: var(--text, #c0caf5); background: var(--bg4, rgba(255, 255, 255, 0.07)); } + +/* ── 본문 ── */ +.app-popup-body { + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px 14px 14px; + overflow-y: auto; + flex: 1; + min-height: 0; +} + +.app-popup-section { + padding: 12px 16px; + border-bottom: 1px solid var(--border, rgba(122, 162, 247, 0.1)); +} +.app-popup-section--gradient { + background: linear-gradient( + 135deg, + var(--app-popup-accent-glow, rgba(63, 126, 245, 0.08)) 0%, + transparent 60% + ); +} + +.app-popup-card { + display: flex; + align-items: flex-start; + gap: 12px; + margin: 0 14px; + padding: 11px 14px; + background: var(--bg3, #1f2335); + border: 1px solid var(--border, rgba(122, 162, 247, 0.12)); + border-radius: 9px; +} + +.app-popup-field-row { + display: flex; + align-items: center; + gap: 8px; +} +.app-popup-field-label { + font-size: 11.5px; + color: var(--text2, #9aa5ce); + flex-shrink: 0; + min-width: 42px; +} + +.app-popup-input, +.app-popup-select { + flex: 1; + min-width: 0; + box-sizing: border-box; + background: var(--bg3, #1f2335); + border: 1px solid var(--border, rgba(122, 162, 247, 0.18)); + border-radius: 6px; + outline: none; + color: var(--text, #c0caf5); + font-size: 12.5px; + padding: 7px 10px; + transition: border-color 0.15s; +} +.app-popup-input:focus, +.app-popup-select:focus { + border-color: var(--app-popup-accent, #3f7ef5); +} + +.app-popup-btn { + padding: 7px 11px; + border: 1px solid var(--border, rgba(122, 162, 247, 0.18)); + border-radius: 6px; + background: var(--bg3, #1f2335); + color: var(--text2, #9aa5ce); + font-size: 11.5px; + font-weight: 600; + cursor: pointer; + transition: border-color 0.15s, color 0.15s, background 0.15s; +} +.app-popup-btn:hover { + border-color: var(--app-popup-accent, #3f7ef5); + color: var(--text, #c0caf5); + background: var(--bg4, #414868); +} +.app-popup-btn--primary { + width: 100%; + padding: 12px; + border: none; + border-radius: 8px; + background: var(--app-popup-accent, #3f7ef5); + color: #fff; + font-size: 14px; + font-weight: 800; + box-shadow: 0 4px 16px var(--app-popup-accent-glow, rgba(63, 126, 245, 0.25)); +} +.app-popup-btn--primary:hover { opacity: 0.92; } + +.app-popup-disclaimer { + font-size: 10.5px; + color: var(--text3, #6272a4); + text-align: center; + margin: 0; + line-height: 1.5; +} + +/* 레거시 오버레이 통일 */ +.brm-overlay, +.ism-overlay, +.ibsm-overlay, +.mcs-overlay, +.dsm-overlay, +.sp-modal-overlay, +.obj-tree-overlay, +.ind-overlay, +.plsp-overlay, +.login-modal-backdrop { + position: fixed; + inset: 0; + z-index: 5000; + background: rgba(0, 0, 0, 0.45); +} + +/* 라이트 테마 */ +.app.light .app-popup-shell, +.app.light .tam-modal, +.app.light .lsp-modal, +.app.light .gc-popup-panel, +.app.light .bts-modal, +.app.light .brm-modal, +.app.light .ism-dialog, +.app.light .login-modal { + background: #ffffff; + border-color: rgba(25, 118, 210, 0.35); + color: #212121; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.18), 0 0 40px rgba(25, 118, 210, 0.08); +} +.app.light .app-popup-header, +.app.light .gc-popup-header { + background: linear-gradient(180deg, rgba(25, 118, 210, 0.1) 0%, #f5f7fa 100%); + border-color: rgba(0, 0, 0, 0.1); +} +.app.light .app-popup-header-title, +.app.light .gc-popup-title { color: #1a237e; } +.app.light .app-popup-header-ko { color: #546e7a; } +.app.light .app-popup-card { background: #f5f7fa; border-color: rgba(0, 0, 0, 0.1); } +.app.light .app-popup-input, +.app.light .app-popup-select { background: #f0f3f8; border-color: rgba(0, 0, 0, 0.12); color: #212121; } diff --git a/frontend_golden/src/components/DraggablePaper.tsx b/frontend_golden/src/components/DraggablePaper.tsx index 003c41f..8706925 100644 --- a/frontend_golden/src/components/DraggablePaper.tsx +++ b/frontend_golden/src/components/DraggablePaper.tsx @@ -2,22 +2,38 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Paper } from '@mui/material'; /** - * 드래그 가능한 Dialog Paper 컴포넌트 - * 모든 Dialog에서 재사용 가능 + * 드래그 가능한 Dialog Paper — tam 스타일 팝업, 마운트 시 화면 중앙 배치 */ const DraggablePaper = (props: any) => { const paperRef = useRef(null); const [isDragging, setIsDragging] = useState(false); - const [position, setPosition] = useState({ x: 0, y: 60 }); // 초기 위치를 더 아래로 (60px) + const [position, setPosition] = useState({ x: 0, y: 0 }); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); + const centeredOnce = useRef(false); + + useEffect(() => { + if (centeredOnce.current) return; + const el = paperRef.current; + if (!el) return; + const placeCenter = () => { + const w = el.offsetWidth || 400; + const h = el.offsetHeight || 320; + setPosition({ + x: Math.max(8, (window.innerWidth - w) / 2), + y: Math.max(8, (window.innerHeight - h) / 2), + }); + centeredOnce.current = true; + }; + placeCenter(); + const id = requestAnimationFrame(placeCenter); + return () => cancelAnimationFrame(id); + }, []); const handleMouseDown = (e: React.MouseEvent) => { - // 타이틀바 영역에서만 드래그 시작 const target = e.target as HTMLElement; - // 버튼, 아이콘버튼, 또는 그 내부 요소를 클릭한 경우 드래그 시작 안 함 - if (target.closest('.MuiDialogTitle-root') && - !target.closest('button') && - !target.closest('.MuiButton-root') && + if (target.closest('.MuiDialogTitle-root') && + !target.closest('button') && + !target.closest('.MuiButton-root') && !target.closest('.MuiIconButton-root')) { setIsDragging(true); setDragStart({ @@ -29,17 +45,9 @@ const DraggablePaper = (props: any) => { const handleMouseMove = useCallback((e: MouseEvent) => { if (isDragging && paperRef.current) { - // 새 위치 계산 - let newX = e.clientX - dragStart.x; - let newY = e.clientY - dragStart.y; - - // 상단만 제한 (0보다 작아지지 않도록) - const minY = 0; - newY = Math.max(minY, newY); - setPosition({ - x: newX, - y: newY, + x: e.clientX - dragStart.x, + y: Math.max(0, e.clientY - dragStart.y), }); } }, [isDragging, dragStart]); @@ -71,12 +79,29 @@ const DraggablePaper = (props: any) => { left: position.x, margin: 0, cursor: isDragging ? 'grabbing' : 'default', + borderRadius: '12px', + border: '1px solid rgba(63, 126, 245, 0.35)', + backgroundImage: 'none', + backgroundColor: '#1e222d', + boxShadow: '0 0 0 1px rgba(63,126,245,0.08), 0 20px 60px rgba(0,0,0,0.65), 0 0 40px rgba(63,126,245,0.06)', + overflow: 'hidden', '& .MuiDialogTitle-root': { cursor: 'grab', userSelect: 'none', - '&:active': { - cursor: 'grabbing', - }, + background: 'linear-gradient(180deg, rgba(63,126,245,0.14) 0%, rgba(63,126,245,0.04) 28px, #1f2335 100%)', + borderBottom: '1px solid rgba(122,162,247,0.12)', + color: '#c0caf5', + fontSize: '14px', + fontWeight: 700, + '&:active': { cursor: 'grabbing' }, + }, + '& .MuiDialogContent-root': { + bgcolor: '#1e222d', + color: '#c0caf5', + }, + '& .MuiDialogActions-root': { + bgcolor: '#1e222d', + borderTop: '1px solid rgba(122,162,247,0.1)', }, }} /> @@ -84,4 +109,3 @@ const DraggablePaper = (props: any) => { }; export default DraggablePaper; - diff --git a/frontend_golden/src/components/GoldenPopupDialogTitle.tsx b/frontend_golden/src/components/GoldenPopupDialogTitle.tsx new file mode 100644 index 0000000..3a0c81f --- /dev/null +++ b/frontend_golden/src/components/GoldenPopupDialogTitle.tsx @@ -0,0 +1,39 @@ +/** + * TradeAlertModal 스타일 MUI Dialog 타이틀 (배지 + EN/KO 제목) + */ +import React from 'react'; +import { Box, DialogTitle, IconButton, Typography } from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import { goldenPopupBadgeSx, goldenPopupDialogTitleSx } from '../utils/goldenPopupStyles'; + +interface Props { + badge?: string; + title: React.ReactNode; + titleKo?: React.ReactNode; + onClose?: () => void; + extra?: React.ReactNode; +} + +const GoldenPopupDialogTitle: React.FC = ({ badge, title, titleKo, onClose, extra }) => ( + + + {badge ? {badge} : null} + + {title} + {titleKo ? ( + + ({titleKo}) + + ) : null} + + + {extra} + {onClose ? ( + + + + ) : null} + +); + +export default GoldenPopupDialogTitle; diff --git a/frontend_golden/src/components/LoginDialog.tsx b/frontend_golden/src/components/LoginDialog.tsx index bc3f6ee..08c973c 100644 --- a/frontend_golden/src/components/LoginDialog.tsx +++ b/frontend_golden/src/components/LoginDialog.tsx @@ -1,7 +1,6 @@ import React, { useState } from 'react'; import { Dialog, - DialogTitle, DialogContent, DialogActions, TextField, @@ -14,9 +13,10 @@ import { InputAdornment, Divider, } from '@mui/material'; -import { Visibility, VisibilityOff, Close, AccountCircle } from '@mui/icons-material'; +import { Visibility, VisibilityOff, AccountCircle } from '@mui/icons-material'; import { useAuth } from '../contexts/AuthContext'; import DraggablePaper from './DraggablePaper'; +import GoldenPopupDialogTitle from './GoldenPopupDialogTitle'; interface LoginDialogProps { open: boolean; @@ -87,32 +87,15 @@ const LoginDialog: React.FC = ({ open, onClose, onSwitchToSign pointerEvents: 'none', '& .MuiDialog-container': { pointerEvents: 'none', - alignItems: 'flex-start', + alignItems: 'center', + justifyContent: 'center', }, }} PaperProps={{ - sx: { - pointerEvents: 'auto', - boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)', - border: '2px solid rgba(80, 100, 160, 0.5)', - } + sx: { pointerEvents: 'auto' }, }} > - - - 🔐 로그인 - - - - - +
diff --git a/frontend_golden/src/components/alert/AlertRealtimeStrategyPopup.tsx b/frontend_golden/src/components/alert/AlertRealtimeStrategyPopup.tsx index e5e5471..8a27f38 100644 --- a/frontend_golden/src/components/alert/AlertRealtimeStrategyPopup.tsx +++ b/frontend_golden/src/components/alert/AlertRealtimeStrategyPopup.tsx @@ -17,10 +17,8 @@ import { useTheme, } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; -import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; import FullscreenIcon from '@mui/icons-material/Fullscreen'; import FullscreenExitIcon from '@mui/icons-material/FullscreenExit'; -import ShowChartIcon from '@mui/icons-material/ShowChart'; import { evaluateAlertStrategy, getAllAlerts, @@ -104,9 +102,13 @@ function clamp(x: number, y: number, w: number, h: number) { return { x: Math.min(Math.max(m, x), maxX), y: Math.min(Math.max(m, y), maxY) }; } -function nearAnchor(anchor: HTMLElement, w: number, h: number) { - const r = anchor.getBoundingClientRect(); - return clamp(r.right - w, r.bottom + 8, w, h); +function centerPanel(w: number, h: number) { + if (typeof window === 'undefined') return { x: 24, y: 80 }; + const m = 8; + return { + x: Math.max(m, (window.innerWidth - w) / 2), + y: Math.max(m, (window.innerHeight - h) / 2), + }; } function statusColor(s: ConnectionStatus): 'default' | 'primary' | 'success' | 'error' | 'warning' { @@ -431,7 +433,7 @@ const AlertRealtimeStrategyPopupShell: React.FC<{ const resizeListenersRef = useRef<{ move: (e: MouseEvent) => void; up: () => void } | null>(null); useLayoutEffect(() => { - setDragPos(nearAnchor(anchorEl, panelW, panelH)); + setDragPos(centerPanel(panelW, panelH)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [anchorEl]); @@ -516,7 +518,7 @@ const AlertRealtimeStrategyPopupShell: React.FC<{ }} > - - - - 실시간 전략 데이터 + + LIVE + + + STRATEGY DATA + + (실시간 전략 데이터) + {isMaximized ? : } - + diff --git a/frontend_golden/src/contexts/ThemeContext.tsx b/frontend_golden/src/contexts/ThemeContext.tsx index 1b742cb..84fcbe9 100644 --- a/frontend_golden/src/contexts/ThemeContext.tsx +++ b/frontend_golden/src/contexts/ThemeContext.tsx @@ -230,6 +230,48 @@ const darkTheme = createTheme({ }, }, }, + MuiDialog: { + styleOverrides: { + paper: { + borderRadius: 12, + border: '1px solid rgba(63, 126, 245, 0.35)', + backgroundImage: 'none', + backgroundColor: '#1e222d', + boxShadow: '0 0 0 1px rgba(63,126,245,0.08), 0 20px 60px rgba(0,0,0,0.65), 0 0 40px rgba(63,126,245,0.06)', + overflow: 'hidden', + }, + }, + }, + MuiDialogTitle: { + styleOverrides: { + root: { + background: 'linear-gradient(180deg, rgba(63,126,245,0.14) 0%, rgba(63,126,245,0.04) 28px, #1f2335 100%)', + borderBottom: '1px solid rgba(122,162,247,0.12)', + color: '#c0caf5', + fontSize: '14px', + fontWeight: 700, + padding: '10px 14px', + }, + }, + }, + MuiDialogContent: { + styleOverrides: { + root: { + backgroundColor: '#1e222d', + color: '#c0caf5', + padding: '12px 14px', + }, + }, + }, + MuiDialogActions: { + styleOverrides: { + root: { + backgroundColor: '#1e222d', + borderTop: '1px solid rgba(122,162,247,0.1)', + padding: '10px 14px', + }, + }, + }, MuiInputBase: { styleOverrides: { root: { diff --git a/frontend_golden/src/utils/goldenPopupStyles.ts b/frontend_golden/src/utils/goldenPopupStyles.ts new file mode 100644 index 0000000..c6165db --- /dev/null +++ b/frontend_golden/src/utils/goldenPopupStyles.ts @@ -0,0 +1,73 @@ +/** TradeAlertModal(tam-) 기준 MUI Dialog 공통 스타일 — frontend_golden */ +export const GOLDEN_POPUP_ACCENT = '#3f7ef5'; +export const GOLDEN_POPUP_ACCENT_GLOW = 'rgba(63,126,245,0.22)'; + +export const goldenPopupDialogPaperSx = { + borderRadius: '12px', + border: `1px solid ${GOLDEN_POPUP_ACCENT}40`, + background: '#1e222d', + backgroundImage: 'none', + boxShadow: `0 0 0 1px ${GOLDEN_POPUP_ACCENT_GLOW}, 0 20px 60px rgba(0,0,0,0.65), 0 0 40px ${GOLDEN_POPUP_ACCENT_GLOW}`, + overflow: 'hidden', + maxWidth: '96vw', + maxHeight: '90vh', +} as const; + +export const goldenPopupDialogTitleSx = { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 1, + py: 1.25, + px: 1.75, + cursor: 'grab', + userSelect: 'none', + background: `linear-gradient(180deg, rgba(63,126,245,0.14) 0%, rgba(63,126,245,0.04) 28px, #1f2335 100%)`, + borderBottom: '1px solid rgba(122,162,247,0.12)', + color: '#c0caf5', + fontSize: '14px', + fontWeight: 700, + letterSpacing: '0.4px', + '&:active': { cursor: 'grabbing' }, +} as const; + +export const goldenPopupDialogContentSx = { + px: 1.75, + py: 1.5, + bgcolor: '#1e222d', + color: '#c0caf5', +} as const; + +export const goldenPopupDialogActionsSx = { + px: 1.75, + py: 1.25, + bgcolor: '#1e222d', + borderTop: '1px solid rgba(122,162,247,0.1)', +} as const; + +export const goldenPopupBadgeSx = { + display: 'inline-block', + px: 1.25, + py: 0.35, + borderRadius: '20px', + fontSize: '11px', + fontWeight: 800, + color: '#fff', + letterSpacing: '1.2px', + bgcolor: GOLDEN_POPUP_ACCENT, + flexShrink: 0, +} as const; + +/** Dialog 공통 props (드래그 Paper + tam 스타일) */ +export function goldenPopupDialogProps(width = 400) { + return { + hideBackdrop: true, + PaperProps: { + sx: { + ...goldenPopupDialogPaperSx, + width, + m: 0, + }, + }, + } as const; +}