Unify popup UI to TradeAlertModal design system.
Add AppPopup shell, shared CSS, MUI theme overrides, and center positioning for consistent modal styling across frontend and frontend_golden. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+355
-100
@@ -6356,116 +6356,405 @@
|
|||||||
.bt-drop-run.running { background: var(--bg4); color: var(--text2); }
|
.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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
background: var(--bg2, #24283b);
|
||||||
min-width: 240px;
|
border: 1px solid var(--lsp-accent, var(--border, rgba(122,162,247,0.15)));
|
||||||
padding: 12px 14px;
|
border-radius: 12px;
|
||||||
background: var(--bg2);
|
box-shadow:
|
||||||
border: 1px solid var(--border);
|
0 0 0 1px var(--lsp-accent-glow, rgba(122,162,247,0.08)),
|
||||||
border-radius: 8px;
|
0 20px 60px rgba(0,0,0,0.65),
|
||||||
font-size: 12px;
|
0 0 40px var(--lsp-accent-glow, rgba(122,162,247,0.06));
|
||||||
color: var(--text);
|
overflow: hidden;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text, #c0caf5);
|
||||||
|
font-family: var(--font, 'Noto Sans KR', sans-serif);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lsp-header {
|
.lsp-header {
|
||||||
display: flex;
|
flex-shrink: 0;
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
}
|
||||||
.lsp-icon { opacity: 0.75; flex-shrink: 0; color: #00BCD4; }
|
.lsp-header-left {
|
||||||
.lsp-title { font-weight: 700; font-size: 12.5px; flex: 1; }
|
|
||||||
.lsp-saving { font-size: 10px; color: var(--text3); }
|
|
||||||
.lsp-close-btn {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 9px;
|
||||||
width: 22px;
|
flex: 1;
|
||||||
height: 22px;
|
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;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
color: var(--text3, #6272a4);
|
||||||
color: var(--text3);
|
font-size: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 2px 6px;
|
||||||
|
line-height: 1;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: color 0.15s, background 0.15s;
|
||||||
flex-shrink: 0;
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
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; }
|
.lsp-info-card {
|
||||||
|
display: flex;
|
||||||
/* 토글 (stg-toggle 재활용 패턴) */
|
align-items: flex-start;
|
||||||
.lsp-toggle { position: relative; display: inline-flex; align-items: center; cursor: pointer; }
|
gap: 12px;
|
||||||
.lsp-toggle input { opacity: 0; width: 0; height: 0; position: absolute; }
|
margin: 12px 14px 0;
|
||||||
.lsp-toggle-slider {
|
padding: 11px 14px;
|
||||||
width: 34px; height: 18px;
|
background: var(--bg3, #1f2335);
|
||||||
background: var(--bg3);
|
border: 1px solid var(--border, rgba(122,162,247,0.12));
|
||||||
border-radius: 9px;
|
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;
|
position: relative;
|
||||||
}
|
}
|
||||||
.lsp-toggle-slider::after {
|
.lsp-toggle-slider::after {
|
||||||
content: ''; position: absolute;
|
content: '';
|
||||||
width: 14px; height: 14px;
|
position: absolute;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
top: 2px; left: 2px;
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
transition: transform 0.2s;
|
transition: transform 0.2s;
|
||||||
}
|
}
|
||||||
.lsp-toggle input:checked + .lsp-toggle-slider { background: #00BCD4; }
|
.lsp-toggle input:checked + .lsp-toggle-slider {
|
||||||
.lsp-toggle input:checked + .lsp-toggle-slider::after { transform: translateX(16px); }
|
background: var(--lsp-accent, #3f7ef5);
|
||||||
|
border-color: var(--lsp-accent, #3f7ef5);
|
||||||
|
}
|
||||||
|
.lsp-toggle input:checked + .lsp-toggle-slider::after {
|
||||||
|
transform: translateX(18px);
|
||||||
|
}
|
||||||
|
|
||||||
.lsp-select {
|
.lsp-select {
|
||||||
flex: 1;
|
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;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
|
||||||
}
|
}
|
||||||
.lsp-radio-group {
|
|
||||||
|
.lsp-option-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
padding-left: 4px;
|
|
||||||
}
|
}
|
||||||
.lsp-radio {
|
.lsp-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
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;
|
cursor: pointer;
|
||||||
padding: 4px 6px;
|
transition: border-color 0.15s, background 0.15s;
|
||||||
border-radius: 5px;
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
}
|
||||||
.lsp-radio:hover { background: var(--bg3); }
|
.lsp-option:hover {
|
||||||
.lsp-radio input { margin-top: 2px; accent-color: #00BCD4; flex-shrink: 0; }
|
border-color: var(--lsp-accent, rgba(63,126,245,0.35));
|
||||||
.lsp-radio-label { display: flex; flex-direction: column; gap: 1px; }
|
background: var(--bg4, #414868);
|
||||||
.lsp-radio-title { font-size: 11.5px; font-weight: 600; }
|
}
|
||||||
.lsp-radio-desc { font-size: 10px; color: var(--text3); }
|
.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 {
|
.lsp-monitor-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -6476,7 +6765,7 @@
|
|||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
.lsp-monitor-sep { opacity: 0.5; }
|
.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 {
|
.lsp-monitor-warn {
|
||||||
font-size: 10.5px;
|
font-size: 10.5px;
|
||||||
color: #ff9800;
|
color: #ff9800;
|
||||||
@@ -6509,14 +6798,6 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
margin-bottom: 8px;
|
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 {
|
.stg-btn-secondary {
|
||||||
background: var(--bg3);
|
background: var(--bg3);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
@@ -6545,32 +6826,6 @@
|
|||||||
padding: 6px 12px;
|
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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ══════════════════════════════════════════════
|
/* ══════════════════════════════════════════════
|
||||||
백테스팅 결과 통계 배지 (차트 위 오버레이)
|
백테스팅 결과 통계 배지 (차트 위 오버레이)
|
||||||
══════════════════════════════════════════════ */
|
══════════════════════════════════════════════ */
|
||||||
|
|||||||
+12
-13
@@ -93,6 +93,7 @@ import type { LoginResponse } from './utils/backendApi';
|
|||||||
import { invalidateAppSettingsCache } from './hooks/useAppSettings';
|
import { invalidateAppSettingsCache } from './hooks/useAppSettings';
|
||||||
import { invalidateIndicatorSettingsCache } from './hooks/useIndicatorSettings';
|
import { invalidateIndicatorSettingsCache } from './hooks/useIndicatorSettings';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
import './styles/appPopup.css';
|
||||||
|
|
||||||
let _indCounter = 0;
|
let _indCounter = 0;
|
||||||
function newIndId() { return `ind_${++_indCounter}_${Date.now()}`; }
|
function newIndId() { return `ind_${++_indCounter}_${Date.now()}`; }
|
||||||
@@ -1866,19 +1867,17 @@ function App() {
|
|||||||
|
|
||||||
{/* 실시간 전략 체크 패널 */}
|
{/* 실시간 전략 체크 패널 */}
|
||||||
{showLivePanel && menuPage === 'chart' && layoutDef.count === 1 && (
|
{showLivePanel && menuPage === 'chart' && layoutDef.count === 1 && (
|
||||||
<div style={{ position: 'absolute', top: 48, right: 14, zIndex: 60 }}>
|
<LiveStrategyPanel
|
||||||
<LiveStrategyPanel
|
theme={theme}
|
||||||
theme={theme}
|
market={symbol}
|
||||||
market={symbol}
|
strategies={liveStrategies}
|
||||||
strategies={liveStrategies}
|
settings={liveStrategySettings}
|
||||||
settings={liveStrategySettings}
|
watchlistCount={watchlist.length}
|
||||||
watchlistCount={watchlist.length}
|
monitoredMarkets={monitoredMarkets}
|
||||||
monitoredMarkets={monitoredMarkets}
|
onClearMarkers={() => { clearLiveMarkers(); managerRef.current?.clearLiveStrategyMarkers(); }}
|
||||||
onClearMarkers={() => { clearLiveMarkers(); managerRef.current?.clearLiveStrategyMarkers(); }}
|
onSettingsChange={handleLiveSettingsChange}
|
||||||
onSettingsChange={handleLiveSettingsChange}
|
onClose={() => setShowLivePanel(false)}
|
||||||
onClose={() => setShowLivePanel(false)}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 백테스팅 설정 모달 */}
|
{/* 백테스팅 설정 모달 */}
|
||||||
|
|||||||
@@ -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<AppPopupProps> = ({
|
||||||
|
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 = (
|
||||||
|
<div
|
||||||
|
className={overlayCls}
|
||||||
|
style={{ zIndex }}
|
||||||
|
onMouseDown={e => {
|
||||||
|
if (closeOnBackdrop && backdrop && e.target === e.currentTarget) onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={panelRef}
|
||||||
|
className={shellCls}
|
||||||
|
style={{
|
||||||
|
...panelStyle,
|
||||||
|
width,
|
||||||
|
maxWidth,
|
||||||
|
cursor: dragging ? 'grabbing' : undefined,
|
||||||
|
'--app-popup-accent': accentColor,
|
||||||
|
'--app-popup-accent-glow': accentGlow,
|
||||||
|
'--tam-accent': accentColor,
|
||||||
|
'--tam-accent-glow': accentGlow,
|
||||||
|
'--lsp-accent': accentColor,
|
||||||
|
'--lsp-accent-glow': accentGlow,
|
||||||
|
} as React.CSSProperties}
|
||||||
|
onMouseDown={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gc-popup-header app-popup-header"
|
||||||
|
onPointerDown={onHeaderPointerDown}
|
||||||
|
style={{ cursor: headerCursor, ...headerTouchStyle }}
|
||||||
|
>
|
||||||
|
<div className="app-popup-header-left">
|
||||||
|
{badge ? (
|
||||||
|
<span className="app-popup-badge" style={{ background: accentColor }}>
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<span className="app-popup-header-title">
|
||||||
|
{title}
|
||||||
|
{titleKo != null && titleKo !== '' ? (
|
||||||
|
<span className="app-popup-header-ko"> ({titleKo})</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{headerExtra}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="app-popup-close gc-popup-close"
|
||||||
|
onMouseDown={e => e.stopPropagation()}
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="닫기"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className={bodyClassName}>{children}</div>
|
||||||
|
{footer}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (portal && typeof document !== 'undefined' && document.body) {
|
||||||
|
return createPortal(content, document.body);
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppPopup;
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* 드래그 가능한 모달 프레임 — 그라데이션 타이틀바(gc-popup-header) + 화면 중앙 초기 배치
|
* 드래그 가능한 모달 프레임 — AppPopup 기반
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
import AppPopup from './AppPopup';
|
||||||
|
|
||||||
export interface DraggableModalFrameProps {
|
export interface DraggableModalFrameProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
|
titleKo?: React.ReactNode;
|
||||||
|
badge?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
overlayClassName?: string;
|
overlayClassName?: string;
|
||||||
dialogClassName?: string;
|
dialogClassName?: string;
|
||||||
@@ -19,60 +21,32 @@ export interface DraggableModalFrameProps {
|
|||||||
export const DraggableModalFrame: React.FC<DraggableModalFrameProps> = ({
|
export const DraggableModalFrame: React.FC<DraggableModalFrameProps> = ({
|
||||||
onClose,
|
onClose,
|
||||||
title,
|
title,
|
||||||
|
titleKo,
|
||||||
|
badge,
|
||||||
children,
|
children,
|
||||||
overlayClassName = 'sp-modal-overlay',
|
overlayClassName = 'sp-modal-overlay',
|
||||||
dialogClassName = 'sp-modal',
|
dialogClassName = 'sp-modal',
|
||||||
bodyClassName = 'sp-modal-body',
|
bodyClassName = 'sp-modal-body app-popup-body',
|
||||||
zIndex = 1000,
|
zIndex = 1000,
|
||||||
width,
|
width,
|
||||||
closeOnBackdrop = true,
|
closeOnBackdrop = true,
|
||||||
}) => {
|
}) => (
|
||||||
const {
|
<AppPopup
|
||||||
panelRef,
|
onClose={onClose}
|
||||||
dragging,
|
title={title}
|
||||||
onHeaderPointerDown, headerTouchStyle,
|
titleKo={titleKo}
|
||||||
panelStyle,
|
badge={badge}
|
||||||
headerCursor,
|
overlayClassName={overlayClassName}
|
||||||
} = useDraggablePanel({ centerOnMount: true });
|
shellClassName={dialogClassName}
|
||||||
|
bodyClassName={bodyClassName}
|
||||||
return (
|
zIndex={zIndex}
|
||||||
<div
|
width={width ?? 420}
|
||||||
className={overlayClassName}
|
backdrop
|
||||||
style={{ zIndex }}
|
closeOnBackdrop={closeOnBackdrop}
|
||||||
onMouseDown={e => {
|
centered
|
||||||
if (closeOnBackdrop && e.target === e.currentTarget) onClose();
|
>
|
||||||
}}
|
{children}
|
||||||
>
|
</AppPopup>
|
||||||
<div
|
);
|
||||||
ref={panelRef}
|
|
||||||
className={dialogClassName}
|
|
||||||
style={{
|
|
||||||
...panelStyle,
|
|
||||||
...(width != null ? { width } : {}),
|
|
||||||
cursor: dragging ? 'grabbing' : undefined,
|
|
||||||
}}
|
|
||||||
onMouseDown={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="gc-popup-header sp-modal-header"
|
|
||||||
onPointerDown={onHeaderPointerDown}
|
|
||||||
style={{ cursor: headerCursor, ...headerTouchStyle }}
|
|
||||||
>
|
|
||||||
<span className="gc-popup-title sp-modal-title">{title}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="gc-popup-close"
|
|
||||||
onMouseDown={e => e.stopPropagation()}
|
|
||||||
onClick={onClose}
|
|
||||||
aria-label="닫기"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className={bodyClassName}>{children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DraggableModalFrame;
|
export default DraggableModalFrame;
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
* 관심종목(★) 등록 종목 전체가 체크 대상이며, 여기서 전략·실행 방식을 지정합니다.
|
* 관심종목(★) 등록 종목 전체가 체크 대상이며, 여기서 전략·실행 방식을 지정합니다.
|
||||||
*/
|
*/
|
||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import type { Theme } from '../types';
|
import type { Theme } from '../types';
|
||||||
|
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
||||||
import {
|
import {
|
||||||
saveLiveStrategySettings,
|
saveLiveStrategySettings,
|
||||||
type LiveStrategySettingsDto,
|
type LiveStrategySettingsDto,
|
||||||
@@ -28,6 +30,20 @@ interface LiveStrategyPanelProps {
|
|||||||
onSettingsChange?: (settings: LiveStrategySettingsDto) => void;
|
onSettingsChange?: (settings: LiveStrategySettingsDto) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ACCENT = '#3f7ef5';
|
||||||
|
const ACCENT_GLOW = 'rgba(63,126,245,0.22)';
|
||||||
|
|
||||||
|
const CandleIcon: React.FC<{ color: string }> = ({ color }) => (
|
||||||
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="6" y="9" width="5" height="12" rx="1" fill={color} opacity="0.9"/>
|
||||||
|
<line x1="8.5" y1="5" x2="8.5" y2="9" stroke={color} strokeWidth="1.5"/>
|
||||||
|
<line x1="8.5" y1="21" x2="8.5" y2="25" stroke={color} strokeWidth="1.5"/>
|
||||||
|
<rect x="17" y="7" width="5" height="9" rx="1" fill="var(--down,#4dabf7)" opacity="0.8"/>
|
||||||
|
<line x1="19.5" y1="4" x2="19.5" y2="7" stroke="var(--down,#4dabf7)" strokeWidth="1.5"/>
|
||||||
|
<line x1="19.5" y1="16" x2="19.5" y2="20" stroke="var(--down,#4dabf7)" strokeWidth="1.5"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const LiveStrategyPanel: React.FC<LiveStrategyPanelProps> = ({
|
const LiveStrategyPanel: React.FC<LiveStrategyPanelProps> = ({
|
||||||
theme, market, strategies, settings,
|
theme, market, strategies, settings,
|
||||||
watchlistCount = 0, monitoredMarkets = [],
|
watchlistCount = 0, monitoredMarkets = [],
|
||||||
@@ -35,6 +51,18 @@ const LiveStrategyPanel: React.FC<LiveStrategyPanelProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [saving, setSaving] = useState(false);
|
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<LiveStrategySettingsDto>) => {
|
const persist = useCallback(async (patch: Partial<LiveStrategySettingsDto>) => {
|
||||||
const next: LiveStrategySettingsDto = { ...settings, ...patch, market };
|
const next: LiveStrategySettingsDto = { ...settings, ...patch, market };
|
||||||
const prev = settings;
|
const prev = settings;
|
||||||
@@ -61,157 +89,209 @@ const LiveStrategyPanel: React.FC<LiveStrategyPanelProps> = ({
|
|||||||
const execType = settings.executionType;
|
const execType = settings.executionType;
|
||||||
const stratId = settings.strategyId;
|
const stratId = settings.strategyId;
|
||||||
const monCount = monitoredMarkets.length;
|
const monCount = monitoredMarkets.length;
|
||||||
|
const selectedStrategy = strategies.find(s => s.id === stratId);
|
||||||
|
|
||||||
return (
|
const execLabel = execType === 'REALTIME_TICK' ? '실시간 틱 (3초)' : '봉 마감 직후';
|
||||||
<div className={`lsp-wrap lsp-wrap--${theme}`}>
|
const posModeLabel = (settings.positionMode ?? 'LONG_ONLY') === 'LONG_ONLY'
|
||||||
<div className="lsp-header">
|
? '보유 자산 기준'
|
||||||
<svg className="lsp-icon" width="13" height="13" viewBox="0 0 14 14"
|
: '순수 지표 기준';
|
||||||
fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<circle cx="7" cy="7" r="5.5"/>
|
|
||||||
<polyline points="5,7 7,9 10,5"/>
|
|
||||||
</svg>
|
|
||||||
<span className="lsp-title">실시간 전략 체크</span>
|
|
||||||
{saving && <span className="lsp-saving">저장 중…</span>}
|
|
||||||
{onClose && (
|
|
||||||
<button className="lsp-close-btn" onClick={onClose} title="닫기">
|
|
||||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none"
|
|
||||||
stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
|
|
||||||
<line x1="1" y1="1" x2="10" y2="10"/>
|
|
||||||
<line x1="10" y1="1" x2="1" y2="10"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="lsp-monitor-stats">
|
const panel = (
|
||||||
<span>관심 {watchlistCount}개</span>
|
<div className={`lsp-overlay lsp-overlay--${theme}`}>
|
||||||
<span className="lsp-monitor-sep">·</span>
|
<div
|
||||||
<span className={monCount > 0 ? 'lsp-monitor-on' : ''}>
|
ref={panelRef}
|
||||||
체크 대상 {monCount}개
|
className="lsp-modal app-popup-shell"
|
||||||
</span>
|
style={{
|
||||||
</div>
|
...panelStyle,
|
||||||
{isOn && stratId && watchlistCount > 0 && (
|
zIndex: 10000,
|
||||||
<p className="lsp-monitor-hint">
|
cursor: dragging ? 'grabbing' : undefined,
|
||||||
★ 관심종목으로 등록한 종목이 자동으로 전략 체크 대상입니다.
|
'--lsp-accent': ACCENT,
|
||||||
</p>
|
'--lsp-accent-glow': ACCENT_GLOW,
|
||||||
)}
|
} as React.CSSProperties}
|
||||||
{monCount > 0 && (
|
onMouseDown={e => e.stopPropagation()}
|
||||||
<div className="lsp-monitor-list" title={monitoredMarkets.join(', ')}>
|
>
|
||||||
{monitoredMarkets.slice(0, 8).map(m => (
|
<div
|
||||||
<span key={m} className="lsp-monitor-chip">{m.replace('KRW-', '')}</span>
|
className="gc-popup-header lsp-header"
|
||||||
))}
|
onPointerDown={onHeaderPointerDown}
|
||||||
{monCount > 8 && <span className="lsp-monitor-chip">+{monCount - 8}</span>}
|
style={{ cursor: headerCursor, ...headerTouchStyle }}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isOn && !stratId && (
|
|
||||||
<p className="lsp-monitor-hint">전략을 선택하면 관심 {watchlistCount}개 종목에 적용됩니다.</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="lsp-row">
|
|
||||||
<span className="lsp-label">실시간 체크</span>
|
|
||||||
<label className="lsp-toggle">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={isOn}
|
|
||||||
onChange={e => persist({ isLiveCheck: e.target.checked })}
|
|
||||||
/>
|
|
||||||
<span className="lsp-toggle-slider" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`lsp-row${!isOn ? ' lsp-row--disabled' : ''}`}>
|
|
||||||
<span className="lsp-label">전략 선택</span>
|
|
||||||
<select
|
|
||||||
className="lsp-select"
|
|
||||||
disabled={!isOn}
|
|
||||||
value={stratId ?? ''}
|
|
||||||
onChange={e => persist({ strategyId: e.target.value ? Number(e.target.value) : null })}
|
|
||||||
>
|
>
|
||||||
<option value="">전략 선택…</option>
|
<div className="lsp-header-left">
|
||||||
{strategies.map(s => (
|
<span className="lsp-badge" style={{ background: ACCENT }}>LIVE</span>
|
||||||
<option key={s.id} value={s.id}>{s.name}</option>
|
<span className="lsp-header-title">
|
||||||
))}
|
STRATEGY CHECK
|
||||||
</select>
|
<span className="lsp-header-ko"> (실시간 전략 체크)</span>
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
{saving && <span className="lsp-saving">저장 중…</span>}
|
||||||
|
{onClose && (
|
||||||
|
<button type="button" className="lsp-close" onClick={onClose} title="닫기">✕</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={`lsp-exec-wrap${!isOn ? ' lsp-row--disabled' : ''}`}>
|
<div className="lsp-summary-section">
|
||||||
<span className="lsp-label">실행 방식</span>
|
<div className="lsp-summary-row">
|
||||||
<div className="lsp-radio-group">
|
<span className="lsp-summary-label">관심 종목</span>
|
||||||
<label className="lsp-radio">
|
<span className="lsp-summary-value">{watchlistCount}개</span>
|
||||||
<input
|
</div>
|
||||||
type="radio"
|
<div className="lsp-summary-row">
|
||||||
name="execType"
|
<span className="lsp-summary-label">체크 대상</span>
|
||||||
value="CANDLE_CLOSE"
|
<span className={`lsp-summary-value${monCount > 0 ? ' lsp-summary-value--active' : ''}`}>
|
||||||
checked={execType === 'CANDLE_CLOSE'}
|
{monCount}개
|
||||||
disabled={!isOn}
|
|
||||||
onChange={() => persist({ executionType: 'CANDLE_CLOSE' })}
|
|
||||||
/>
|
|
||||||
<span className="lsp-radio-label">
|
|
||||||
<span className="lsp-radio-title">봉 마감 직후</span>
|
|
||||||
<span className="lsp-radio-desc">완성된 캔들 기준으로 1회 판정</span>
|
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</div>
|
||||||
<label className="lsp-radio">
|
{isOn && stratId && watchlistCount > 0 && (
|
||||||
<input
|
<p className="lsp-summary-hint">
|
||||||
type="radio"
|
★ 관심종목으로 등록한 종목이 자동으로 전략 체크 대상입니다.
|
||||||
name="execType"
|
</p>
|
||||||
value="REALTIME_TICK"
|
)}
|
||||||
checked={execType === 'REALTIME_TICK'}
|
</div>
|
||||||
|
|
||||||
|
{monCount > 0 && (
|
||||||
|
<div className="lsp-info-card">
|
||||||
|
<div className="lsp-info-icon">
|
||||||
|
<CandleIcon color={ACCENT} />
|
||||||
|
</div>
|
||||||
|
<div className="lsp-info-body">
|
||||||
|
<div className="lsp-info-row">
|
||||||
|
<span className="lsp-info-label">모니터링</span>
|
||||||
|
<span className="lsp-info-val lsp-monitor-chips">
|
||||||
|
{monitoredMarkets.slice(0, 8).map(m => (
|
||||||
|
<span key={m} className="lsp-monitor-chip">{m.replace('KRW-', '')}</span>
|
||||||
|
))}
|
||||||
|
{monCount > 8 && <span className="lsp-monitor-chip">+{monCount - 8}</span>}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{isOn && selectedStrategy && (
|
||||||
|
<div className="lsp-info-row">
|
||||||
|
<span className="lsp-info-label">적용 전략</span>
|
||||||
|
<span className="lsp-info-val">{selectedStrategy.name}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="lsp-body">
|
||||||
|
<div className="lsp-field-row">
|
||||||
|
<span className="lsp-field-label">실시간 체크</span>
|
||||||
|
<label className="lsp-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isOn}
|
||||||
|
onChange={e => persist({ isLiveCheck: e.target.checked })}
|
||||||
|
/>
|
||||||
|
<span className="lsp-toggle-slider" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`lsp-field-row${!isOn ? ' lsp-field-row--disabled' : ''}`}>
|
||||||
|
<span className="lsp-field-label">전략 선택</span>
|
||||||
|
<select
|
||||||
|
className="lsp-select"
|
||||||
disabled={!isOn}
|
disabled={!isOn}
|
||||||
onChange={() => persist({ executionType: 'REALTIME_TICK' })}
|
value={stratId ?? ''}
|
||||||
/>
|
onChange={e => persist({ strategyId: e.target.value ? Number(e.target.value) : null })}
|
||||||
<span className="lsp-radio-label">
|
>
|
||||||
<span className="lsp-radio-title">실시간 틱 (3초)</span>
|
<option value="">전략 선택…</option>
|
||||||
<span className="lsp-radio-desc">진행 중인 캔들 기준, 3초 주기 판정</span>
|
{strategies.map(s => (
|
||||||
</span>
|
<option key={s.id} value={s.id}>{s.name}</option>
|
||||||
</label>
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`lsp-section${!isOn ? ' lsp-section--disabled' : ''}`}>
|
||||||
|
<span className="lsp-section-label">실행 방식</span>
|
||||||
|
<div className="lsp-option-group">
|
||||||
|
<label className={`lsp-option${execType === 'CANDLE_CLOSE' ? ' lsp-option--active' : ''}`}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="execType"
|
||||||
|
value="CANDLE_CLOSE"
|
||||||
|
checked={execType === 'CANDLE_CLOSE'}
|
||||||
|
disabled={!isOn}
|
||||||
|
onChange={() => persist({ executionType: 'CANDLE_CLOSE' })}
|
||||||
|
/>
|
||||||
|
<span className="lsp-option-text">
|
||||||
|
<span className="lsp-option-title">봉 마감 직후</span>
|
||||||
|
<span className="lsp-option-desc">완성된 캔들 기준으로 1회 판정</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label className={`lsp-option${execType === 'REALTIME_TICK' ? ' lsp-option--active' : ''}`}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="execType"
|
||||||
|
value="REALTIME_TICK"
|
||||||
|
checked={execType === 'REALTIME_TICK'}
|
||||||
|
disabled={!isOn}
|
||||||
|
onChange={() => persist({ executionType: 'REALTIME_TICK' })}
|
||||||
|
/>
|
||||||
|
<span className="lsp-option-text">
|
||||||
|
<span className="lsp-option-title">실시간 틱 (3초)</span>
|
||||||
|
<span className="lsp-option-desc">진행 중인 캔들 기준, 3초 주기 판정</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`lsp-section${!isOn ? ' lsp-section--disabled' : ''}`}>
|
||||||
|
<span className="lsp-section-label">시그널 모드</span>
|
||||||
|
<div className="lsp-option-group">
|
||||||
|
<label className={`lsp-option${(settings.positionMode ?? 'LONG_ONLY') === 'LONG_ONLY' ? ' lsp-option--active' : ''}`}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="lsp-posMode"
|
||||||
|
value="LONG_ONLY"
|
||||||
|
checked={(settings.positionMode ?? 'LONG_ONLY') === 'LONG_ONLY'}
|
||||||
|
disabled={!isOn}
|
||||||
|
onChange={() => persist({ positionMode: 'LONG_ONLY' })}
|
||||||
|
/>
|
||||||
|
<span className="lsp-option-text">
|
||||||
|
<span className="lsp-option-title">보유 자산 기준</span>
|
||||||
|
<span className="lsp-option-desc">매수 이력 있을 때만 매도 허용</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label className={`lsp-option${settings.positionMode === 'SIGNAL_ONLY' ? ' lsp-option--active' : ''}`}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="lsp-posMode"
|
||||||
|
value="SIGNAL_ONLY"
|
||||||
|
checked={settings.positionMode === 'SIGNAL_ONLY'}
|
||||||
|
disabled={!isOn}
|
||||||
|
onChange={() => persist({ positionMode: 'SIGNAL_ONLY' })}
|
||||||
|
/>
|
||||||
|
<span className="lsp-option-text">
|
||||||
|
<span className="lsp-option-title">순수 지표 기준</span>
|
||||||
|
<span className="lsp-option-desc">포지션 무관, 지표 충족 시 즉시 매도</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isOn && !stratId && (
|
||||||
|
<p className="lsp-hint">전략을 선택하면 관심 {watchlistCount}개 종목에 적용됩니다.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isOn && (
|
||||||
|
<div className="lsp-status-bar">
|
||||||
|
<span className={`lsp-dot${isOn ? ' lsp-dot--on' : ''}`} />
|
||||||
|
<span className="lsp-status-text">
|
||||||
|
{stratId
|
||||||
|
? `${selectedStrategy?.name ?? '전략'} · ${execLabel} · ${posModeLabel} · ${monCount}종목`
|
||||||
|
: '전략을 선택하세요'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="lsp-disclaimer">
|
||||||
|
※ 실시간 체크 ON 시 관심종목(★) 전체에 선택한 전략이 적용됩니다.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`lsp-exec-wrap${!isOn ? ' lsp-row--disabled' : ''}`}>
|
|
||||||
<span className="lsp-label">시그널 모드</span>
|
|
||||||
<div className="lsp-radio-group">
|
|
||||||
<label className="lsp-radio">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="lsp-posMode"
|
|
||||||
value="LONG_ONLY"
|
|
||||||
checked={(settings.positionMode ?? 'LONG_ONLY') === 'LONG_ONLY'}
|
|
||||||
disabled={!isOn}
|
|
||||||
onChange={() => persist({ positionMode: 'LONG_ONLY' })}
|
|
||||||
/>
|
|
||||||
<span className="lsp-radio-label">
|
|
||||||
<span className="lsp-radio-title">보유 자산 기준</span>
|
|
||||||
<span className="lsp-radio-desc">매수 이력 있을 때만 매도 허용</span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<label className="lsp-radio">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="lsp-posMode"
|
|
||||||
value="SIGNAL_ONLY"
|
|
||||||
checked={settings.positionMode === 'SIGNAL_ONLY'}
|
|
||||||
disabled={!isOn}
|
|
||||||
onChange={() => persist({ positionMode: 'SIGNAL_ONLY' })}
|
|
||||||
/>
|
|
||||||
<span className="lsp-radio-label">
|
|
||||||
<span className="lsp-radio-title">순수 지표 기준</span>
|
|
||||||
<span className="lsp-radio-desc">포지션 무관, 지표 충족 시 즉시 매도</span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isOn && (
|
|
||||||
<div className="lsp-status">
|
|
||||||
<span className={`lsp-dot${isOn ? ' lsp-dot--on' : ''}`} />
|
|
||||||
{stratId
|
|
||||||
? `${strategies.find(s => s.id === stratId)?.name ?? '전략'} · 관심 ${monCount}종목`
|
|
||||||
: '전략을 선택하세요'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (typeof document === 'undefined') return null;
|
||||||
|
return createPortal(panel, document.body);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LiveStrategyPanel;
|
export default LiveStrategyPanel;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* 로그인 모달 — 기본값 admin / admin
|
* 로그인 모달 — AppPopup(tam-) 스타일
|
||||||
*/
|
*/
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { loginUser, type LoginResponse } from '../utils/backendApi';
|
import { loginUser, type LoginResponse } from '../utils/backendApi';
|
||||||
|
import AppPopup from './AppPopup';
|
||||||
|
|
||||||
const DEFAULT_USERNAME = 'admin';
|
const DEFAULT_USERNAME = 'admin';
|
||||||
const DEFAULT_PASSWORD = 'admin';
|
const DEFAULT_PASSWORD = 'admin';
|
||||||
@@ -16,7 +17,7 @@ interface Props {
|
|||||||
const LoginModal: React.FC<Props> = ({ open, onClose, onSuccess }) => {
|
const LoginModal: React.FC<Props> = ({ open, onClose, onSuccess }) => {
|
||||||
const [username, setUsername] = useState(DEFAULT_USERNAME);
|
const [username, setUsername] = useState(DEFAULT_USERNAME);
|
||||||
const [password, setPassword] = useState(DEFAULT_PASSWORD);
|
const [password, setPassword] = useState(DEFAULT_PASSWORD);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -49,54 +50,53 @@ const LoginModal: React.FC<Props> = ({ open, onClose, onSuccess }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-modal-backdrop" onClick={onClose}>
|
<AppPopup
|
||||||
<div
|
onClose={onClose}
|
||||||
className="login-modal"
|
title="LOGIN"
|
||||||
role="dialog"
|
titleKo="로그인"
|
||||||
aria-labelledby="login-modal-title"
|
badge="AUTH"
|
||||||
onClick={e => e.stopPropagation()}
|
width={380}
|
||||||
>
|
backdrop
|
||||||
<div className="login-modal-header">
|
shellClassName="login-modal"
|
||||||
<h2 id="login-modal-title">로그인</h2>
|
bodyClassName="app-popup-body login-modal-body"
|
||||||
<button type="button" className="login-modal-close" onClick={onClose} aria-label="닫기">×</button>
|
>
|
||||||
|
<p className="login-modal-hint app-popup-disclaimer" style={{ textAlign: 'left', marginBottom: 8 }}>
|
||||||
|
로그인 시 동일 계정의 설정이 모든 기기에서 공유됩니다. 로그인하지 않으면 이 브라우저(기기)별로 설정이 저장됩니다.
|
||||||
|
</p>
|
||||||
|
<form className="login-modal-form" onSubmit={submit}>
|
||||||
|
<label className="login-modal-field app-popup-field-row">
|
||||||
|
<span className="app-popup-field-label">아이디</span>
|
||||||
|
<input
|
||||||
|
className="app-popup-input"
|
||||||
|
type="text"
|
||||||
|
autoComplete="username"
|
||||||
|
value={username}
|
||||||
|
onChange={e => setUsername(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="login-modal-field app-popup-field-row" style={{ marginTop: 8 }}>
|
||||||
|
<span className="app-popup-field-label">비밀번호</span>
|
||||||
|
<input
|
||||||
|
className="app-popup-input login-modal-field--visible"
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{error && <p className="login-modal-error">{error}</p>}
|
||||||
|
<div className="login-modal-actions" style={{ marginTop: 12, display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
||||||
|
<button type="button" className="app-popup-btn" onClick={onClose} disabled={loading}>
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="app-popup-btn app-popup-btn--primary" style={{ width: 'auto', padding: '8px 20px' }} disabled={loading}>
|
||||||
|
{loading ? '로그인 중…' : '로그인'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="login-modal-hint">
|
</form>
|
||||||
로그인 시 동일 계정의 설정이 모든 기기에서 공유됩니다. 로그인하지 않으면 이 브라우저(기기)별로 설정이 저장됩니다.
|
</AppPopup>
|
||||||
</p>
|
|
||||||
<form className="login-modal-form" onSubmit={submit}>
|
|
||||||
<label className="login-modal-field">
|
|
||||||
<span>아이디</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
autoComplete="username"
|
|
||||||
value={username}
|
|
||||||
onChange={e => setUsername(e.target.value)}
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="login-modal-field">
|
|
||||||
<span>비밀번호</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
className="login-modal-field--visible"
|
|
||||||
value={password}
|
|
||||||
onChange={e => setPassword(e.target.value)}
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{error && <p className="login-modal-error">{error}</p>}
|
|
||||||
<div className="login-modal-actions">
|
|
||||||
<button type="button" className="login-modal-btn login-modal-btn--ghost" onClick={onClose} disabled={loading}>
|
|
||||||
취소
|
|
||||||
</button>
|
|
||||||
<button type="submit" className="login-modal-btn login-modal-btn--primary" disabled={loading}>
|
|
||||||
{loading ? '로그인 중…' : '로그인'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,81 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* 공통 팝업 셸 — 그라데이션 타이틀바 + 드래그 이동
|
* 공통 팝업 셸 — AppPopup 래퍼 (하위 호환)
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
import AppPopup, { type AppPopupProps } from './AppPopup';
|
||||||
|
|
||||||
export interface PopupShellProps {
|
export type PopupShellProps = Omit<AppPopupProps, 'titleKo' | 'badge'> & {
|
||||||
onClose: () => void;
|
|
||||||
title: React.ReactNode;
|
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<PopupShellProps> = ({
|
|
||||||
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 (
|
|
||||||
<div
|
|
||||||
className={`gc-popup-overlay${className ? ` ${className}` : ''}`}
|
|
||||||
style={{ zIndex }}
|
|
||||||
onMouseDown={e => {
|
|
||||||
if (closeOnBackdrop && e.target === e.currentTarget) onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={panelRef}
|
|
||||||
className={`gc-popup-panel${dialogClassName ? ` ${dialogClassName}` : ''}`}
|
|
||||||
style={{
|
|
||||||
...panelStyle,
|
|
||||||
width,
|
|
||||||
cursor: dragging ? 'grabbing' : undefined,
|
|
||||||
}}
|
|
||||||
onMouseDown={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="gc-popup-header"
|
|
||||||
onPointerDown={onHeaderPointerDown}
|
|
||||||
style={{ cursor: headerCursor, ...headerTouchStyle }}
|
|
||||||
>
|
|
||||||
<span className="gc-popup-title">{title}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="gc-popup-close"
|
|
||||||
onMouseDown={e => e.stopPropagation()}
|
|
||||||
onClick={onClose}
|
|
||||||
aria-label="닫기"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PopupShell: React.FC<PopupShellProps> = (props) => (
|
||||||
|
<AppPopup backdrop={props.backdrop ?? true} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
export default PopupShell;
|
export default PopupShell;
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export const TradeAlertModal: React.FC<Props> = ({
|
|||||||
<div className="tam-overlay">
|
<div className="tam-overlay">
|
||||||
<div
|
<div
|
||||||
ref={panelRef}
|
ref={panelRef}
|
||||||
className="tam-modal"
|
className="tam-modal app-popup-shell"
|
||||||
style={{
|
style={{
|
||||||
...panelStyle,
|
...panelStyle,
|
||||||
zIndex: 10000,
|
zIndex: 10000,
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
App Popup Design System (TradeAlertModal / tam- 기준)
|
||||||
|
신규 팝업: AppPopup 컴포넌트 + app-popup-* 클래스 사용
|
||||||
|
═══════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.app-popup-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.app-popup-overlay--dim {
|
||||||
|
pointer-events: auto;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
.app-popup-overlay--dim > .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; }
|
||||||
@@ -2,19 +2,35 @@ import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|||||||
import { Paper } from '@mui/material';
|
import { Paper } from '@mui/material';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 드래그 가능한 Dialog Paper 컴포넌트
|
* 드래그 가능한 Dialog Paper — tam 스타일 팝업, 마운트 시 화면 중앙 배치
|
||||||
* 모든 Dialog에서 재사용 가능
|
|
||||||
*/
|
*/
|
||||||
const DraggablePaper = (props: any) => {
|
const DraggablePaper = (props: any) => {
|
||||||
const paperRef = useRef<HTMLDivElement>(null);
|
const paperRef = useRef<HTMLDivElement>(null);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
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 [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 handleMouseDown = (e: React.MouseEvent) => {
|
||||||
// 타이틀바 영역에서만 드래그 시작
|
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
// 버튼, 아이콘버튼, 또는 그 내부 요소를 클릭한 경우 드래그 시작 안 함
|
|
||||||
if (target.closest('.MuiDialogTitle-root') &&
|
if (target.closest('.MuiDialogTitle-root') &&
|
||||||
!target.closest('button') &&
|
!target.closest('button') &&
|
||||||
!target.closest('.MuiButton-root') &&
|
!target.closest('.MuiButton-root') &&
|
||||||
@@ -29,17 +45,9 @@ const DraggablePaper = (props: any) => {
|
|||||||
|
|
||||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||||
if (isDragging && paperRef.current) {
|
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({
|
setPosition({
|
||||||
x: newX,
|
x: e.clientX - dragStart.x,
|
||||||
y: newY,
|
y: Math.max(0, e.clientY - dragStart.y),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isDragging, dragStart]);
|
}, [isDragging, dragStart]);
|
||||||
@@ -71,12 +79,29 @@ const DraggablePaper = (props: any) => {
|
|||||||
left: position.x,
|
left: position.x,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
cursor: isDragging ? 'grabbing' : 'default',
|
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': {
|
'& .MuiDialogTitle-root': {
|
||||||
cursor: 'grab',
|
cursor: 'grab',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
'&:active': {
|
background: 'linear-gradient(180deg, rgba(63,126,245,0.14) 0%, rgba(63,126,245,0.04) 28px, #1f2335 100%)',
|
||||||
cursor: 'grabbing',
|
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;
|
export default DraggablePaper;
|
||||||
|
|
||||||
|
|||||||
@@ -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<Props> = ({ badge, title, titleKo, onClose, extra }) => (
|
||||||
|
<DialogTitle sx={goldenPopupDialogTitleSx} component="div">
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1, minWidth: 0 }}>
|
||||||
|
{badge ? <Box component="span" sx={goldenPopupBadgeSx}>{badge}</Box> : null}
|
||||||
|
<Typography component="span" sx={{ fontSize: 'inherit', fontWeight: 'inherit', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||||
|
{title}
|
||||||
|
{titleKo ? (
|
||||||
|
<Typography component="span" sx={{ fontSize: '12px', fontWeight: 400, color: '#9aa5ce', ml: 0.5 }}>
|
||||||
|
({titleKo})
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{extra}
|
||||||
|
{onClose ? (
|
||||||
|
<IconButton size="small" onClick={onClose} sx={{ color: '#6272a4', '&:hover': { color: '#c0caf5', bgcolor: 'rgba(255,255,255,0.07)' } }}>
|
||||||
|
<CloseIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
|
</DialogTitle>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default GoldenPopupDialogTitle;
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
TextField,
|
TextField,
|
||||||
@@ -14,9 +13,10 @@ import {
|
|||||||
InputAdornment,
|
InputAdornment,
|
||||||
Divider,
|
Divider,
|
||||||
} from '@mui/material';
|
} 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 { useAuth } from '../contexts/AuthContext';
|
||||||
import DraggablePaper from './DraggablePaper';
|
import DraggablePaper from './DraggablePaper';
|
||||||
|
import GoldenPopupDialogTitle from './GoldenPopupDialogTitle';
|
||||||
|
|
||||||
interface LoginDialogProps {
|
interface LoginDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -87,32 +87,15 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, onSwitchToSign
|
|||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
'& .MuiDialog-container': {
|
'& .MuiDialog-container': {
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
PaperProps={{
|
PaperProps={{
|
||||||
sx: {
|
sx: { pointerEvents: 'auto' },
|
||||||
pointerEvents: 'auto',
|
|
||||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)',
|
|
||||||
border: '2px solid rgba(80, 100, 160, 0.5)',
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTitle sx={{
|
<GoldenPopupDialogTitle badge="AUTH" title="LOGIN" titleKo="로그인" onClose={handleClose} />
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
bgcolor: '#1976d2',
|
|
||||||
color: '#fff',
|
|
||||||
py: 1.2,
|
|
||||||
}}>
|
|
||||||
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px', color: '#fff' }}>
|
|
||||||
🔐 로그인
|
|
||||||
</Typography>
|
|
||||||
<IconButton size="small" onClick={handleClose} sx={{ color: '#fff' }}>
|
|
||||||
<Close />
|
|
||||||
</IconButton>
|
|
||||||
</DialogTitle>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|||||||
@@ -17,10 +17,8 @@ import {
|
|||||||
useTheme,
|
useTheme,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
|
||||||
import FullscreenIcon from '@mui/icons-material/Fullscreen';
|
import FullscreenIcon from '@mui/icons-material/Fullscreen';
|
||||||
import FullscreenExitIcon from '@mui/icons-material/FullscreenExit';
|
import FullscreenExitIcon from '@mui/icons-material/FullscreenExit';
|
||||||
import ShowChartIcon from '@mui/icons-material/ShowChart';
|
|
||||||
import {
|
import {
|
||||||
evaluateAlertStrategy,
|
evaluateAlertStrategy,
|
||||||
getAllAlerts,
|
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) };
|
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) {
|
function centerPanel(w: number, h: number) {
|
||||||
const r = anchor.getBoundingClientRect();
|
if (typeof window === 'undefined') return { x: 24, y: 80 };
|
||||||
return clamp(r.right - w, r.bottom + 8, w, h);
|
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' {
|
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);
|
const resizeListenersRef = useRef<{ move: (e: MouseEvent) => void; up: () => void } | null>(null);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
setDragPos(nearAnchor(anchorEl, panelW, panelH));
|
setDragPos(centerPanel(panelW, panelH));
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [anchorEl]);
|
}, [anchorEl]);
|
||||||
|
|
||||||
@@ -516,7 +518,7 @@ const AlertRealtimeStrategyPopupShell: React.FC<{
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Paper
|
<Paper
|
||||||
elevation={10}
|
elevation={0}
|
||||||
sx={{
|
sx={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@@ -524,21 +526,22 @@ const AlertRealtimeStrategyPopupShell: React.FC<{
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
border: isDark ? '1px solid rgba(255,255,255,0.12)' : '1px solid rgba(0,0,0,0.12)',
|
borderRadius: '12px',
|
||||||
bgcolor: 'background.paper',
|
border: '1px solid rgba(63, 126, 245, 0.35)',
|
||||||
|
bgcolor: '#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)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 0.5,
|
gap: 1,
|
||||||
px: 1,
|
px: 1.75,
|
||||||
py: 0.75,
|
py: 1.25,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
borderBottom: 1,
|
borderBottom: '1px solid rgba(122,162,247,0.12)',
|
||||||
borderColor: 'divider',
|
background: 'linear-gradient(180deg, rgba(63,126,245,0.14) 0%, rgba(63,126,245,0.04) 28px, #1f2335 100%)',
|
||||||
bgcolor: isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
@@ -546,7 +549,7 @@ const AlertRealtimeStrategyPopupShell: React.FC<{
|
|||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 0.75,
|
gap: 1,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
cursor: isMaximized ? 'default' : 'grab',
|
cursor: isMaximized ? 'default' : 'grab',
|
||||||
@@ -554,10 +557,27 @@ const AlertRealtimeStrategyPopupShell: React.FC<{
|
|||||||
'&:active': { cursor: isMaximized ? 'default' : 'grabbing' },
|
'&:active': { cursor: isMaximized ? 'default' : 'grabbing' },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ShowChartIcon sx={{ fontSize: 20, color: 'primary.main', flexShrink: 0 }} />
|
<Box
|
||||||
<DragIndicatorIcon sx={{ fontSize: 18, color: 'text.secondary', flexShrink: 0, opacity: 0.85 }} />
|
component="span"
|
||||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, flexShrink: 0 }}>
|
sx={{
|
||||||
실시간 전략 데이터
|
px: 1.25,
|
||||||
|
py: 0.35,
|
||||||
|
borderRadius: '20px',
|
||||||
|
fontSize: '11px',
|
||||||
|
fontWeight: 800,
|
||||||
|
color: '#fff',
|
||||||
|
letterSpacing: '1.2px',
|
||||||
|
bgcolor: '#3f7ef5',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
LIVE
|
||||||
|
</Box>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, flexShrink: 0, color: '#c0caf5', fontSize: '14px' }}>
|
||||||
|
STRATEGY DATA
|
||||||
|
<Typography component="span" sx={{ fontSize: '12px', fontWeight: 400, color: '#9aa5ce', ml: 0.5 }}>
|
||||||
|
(실시간 전략 데이터)
|
||||||
|
</Typography>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
@@ -587,10 +607,17 @@ const AlertRealtimeStrategyPopupShell: React.FC<{
|
|||||||
size="small"
|
size="small"
|
||||||
onClick={toggleMaximize}
|
onClick={toggleMaximize}
|
||||||
aria-label={isMaximized ? '이전 크기로' : '최대화'}
|
aria-label={isMaximized ? '이전 크기로' : '최대화'}
|
||||||
|
sx={{ color: '#6272a4', '&:hover': { color: '#c0caf5', bgcolor: 'rgba(255,255,255,0.07)' } }}
|
||||||
>
|
>
|
||||||
{isMaximized ? <FullscreenExitIcon fontSize="small" /> : <FullscreenIcon fontSize="small" />}
|
{isMaximized ? <FullscreenExitIcon fontSize="small" /> : <FullscreenIcon fontSize="small" />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton className="rt-strategy-no-drag" size="small" onClick={onClose} aria-label="닫기">
|
<IconButton
|
||||||
|
className="rt-strategy-no-drag"
|
||||||
|
size="small"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="닫기"
|
||||||
|
sx={{ color: '#6272a4', '&:hover': { color: '#c0caf5', bgcolor: 'rgba(255,255,255,0.07)' } }}
|
||||||
|
>
|
||||||
<CloseIcon fontSize="small" />
|
<CloseIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -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: {
|
MuiInputBase: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user