From 05d2de5234d21bbb0697663b3883d4c1701485a0 Mon Sep 17 00:00:00 2001 From: Macbook Date: Sun, 31 May 2026 04:34:32 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AA=A8=EC=9D=98=ED=88=AC=EC=9E=90=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/PaperTradingPage.tsx | 9 +- frontend/src/components/TradeOrderPanel.tsx | 3 - .../components/layout/BuilderPageShell.tsx | 36 +- .../components/paper/PaperAllocationTable.tsx | 120 ++++--- .../components/paper/PaperLeftSettingsTab.tsx | 174 +++++----- .../components/paper/PaperLeftStrategyTab.tsx | 109 ++++-- .../src/components/paper/PaperSplitPanel.tsx | 27 +- .../components/trade/TradeSplitOrderPanel.tsx | 9 + frontend/src/styles/builderPageShell.css | 36 ++ frontend/src/styles/paperDashboard.css | 309 +++++++++++++++++- frontend/src/styles/tradeRightPanel.css | 24 ++ .../src/styles/virtualTradingDashboard.css | 51 ++- 12 files changed, 717 insertions(+), 190 deletions(-) diff --git a/frontend/src/components/PaperTradingPage.tsx b/frontend/src/components/PaperTradingPage.tsx index 3c81d3f..9bca230 100644 --- a/frontend/src/components/PaperTradingPage.tsx +++ b/frontend/src/components/PaperTradingPage.tsx @@ -250,15 +250,18 @@ const PaperTradingPage: React.FC = ({ rightStorageKey="ptd-right-width" rightDefaultWidth={440} rightCollapsedStorageKey="ptd-right-open" + headerCenter={!s?.enabled ? ( + + 모의투자가 꺼져 있습니다. 설정에서 활성화하세요. + + ) : undefined} + headerCenterMode="between" headerActions={( <> )} - banner={!s?.enabled ? ( -
모의투자가 꺼져 있습니다. 설정에서 활성화하세요.
- ) : undefined} leftTabs={leftTabs} left={leftTab === 'strategy' ? ( = ({ return (
- {paperTradingEnabled && !paperAutoTradeEnabled && showSymbolField && ( -

자동매매 OFF · 수동 주문만 모의 계좌에 체결됩니다.

- )} {showSymbolField && (
diff --git a/frontend/src/components/layout/BuilderPageShell.tsx b/frontend/src/components/layout/BuilderPageShell.tsx index c35c0a7..34490c6 100644 --- a/frontend/src/components/layout/BuilderPageShell.tsx +++ b/frontend/src/components/layout/BuilderPageShell.tsx @@ -27,6 +27,8 @@ export interface BuilderPageShellProps { headerActions?: React.ReactNode; /** 타이틀 우측·중앙 그리드 시작선 정렬 (가상투자 등) */ headerCenter?: React.ReactNode; + /** grid: 좌패널폭 기준 3열 / between: 타이틀·버튼 사이 중앙 (모의투자 안내 등) */ + headerCenterMode?: 'grid' | 'between'; pageClassName?: string; banner?: React.ReactNode; leftTitle?: string; @@ -103,6 +105,7 @@ export default function BuilderPageShell({ subtitle, headerActions, headerCenter, + headerCenterMode = 'grid', pageClassName, banner, leftTitle, @@ -252,21 +255,34 @@ export default function BuilderPageShell({ className={`bps-page se-page se-page--${theme}${pageClassName ? ` ${pageClassName}` : ''}`} style={pageStyle} > -
+

{title}

{subtitle && {subtitle}}
{headerCenter ? ( - <> -
{headerCenter}
- {headerActions && ( - <> -
); }; diff --git a/frontend/src/components/paper/PaperLeftSettingsTab.tsx b/frontend/src/components/paper/PaperLeftSettingsTab.tsx index 182f094..dca49f7 100644 --- a/frontend/src/components/paper/PaperLeftSettingsTab.tsx +++ b/frontend/src/components/paper/PaperLeftSettingsTab.tsx @@ -9,6 +9,13 @@ interface Props { onOpenSettings?: () => void; } +const SettingsFieldRow: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => ( +
+ {label} + {children} +
+); + const PaperLeftSettingsTab: React.FC = ({ onSettingsSaved, onAccountReset, onOpenSettings }) => { const { defaults, save, isLoaded } = useAppSettings(); @@ -28,101 +35,100 @@ const PaperLeftSettingsTab: React.FC = ({ onSettingsSaved, onAccountReset }; const top = ( - <> +
{onOpenSettings && ( - )} -
- +
+
+ + + + + + + + patch({ paperAutoTradeBudgetPct: Number(e.target.value) })} + /> + +
-
- - -
- +
); const bottom = ( - <> -
- - - - +
+
+
+ + patch({ paperInitialCapital: Number(e.target.value) })} + /> + + + patch({ paperFeeRatePct: Number(e.target.value) })} + /> + + + patch({ paperSlippagePct: Number(e.target.value) })} + /> + + + patch({ paperMinOrderKrw: Number(e.target.value) })} + /> + +
- - +
); return ( diff --git a/frontend/src/components/paper/PaperLeftStrategyTab.tsx b/frontend/src/components/paper/PaperLeftStrategyTab.tsx index 2271f05..4f8bc0f 100644 --- a/frontend/src/components/paper/PaperLeftStrategyTab.tsx +++ b/frontend/src/components/paper/PaperLeftStrategyTab.tsx @@ -91,33 +91,90 @@ const PaperLeftStrategyTab: React.FC = ({ ); + const positions = summary?.positions ?? []; + const bottom = ( - - - - - - {(summary?.positions ?? []).length === 0 ? ( - - ) : summary!.positions.map(p => { - const code = p.symbol.replace(/^KRW-/, ''); - const up = (p.profitLoss ?? 0) >= 0; - return ( - onMarketSelect?.(p.symbol)} - > - - - - - ); - })} - -
자산수량손익
보유 없음
{code}{p.quantity.toFixed(4)} - {p.profitLoss != null ? `${up ? '+' : ''}${fmtKrw(p.profitLoss)}` : '—'} -
+
+
+ 보유 목록 + {positions.length}종목 +
+ {positions.length === 0 ? ( +

보유 종목이 없습니다.

+ ) : ( +
+
+ {positions.map(p => { + const code = p.symbol.replace(/^KRW-/, ''); + const name = p.koreanName || code; + const up = (p.profitLoss ?? 0) >= 0; + const selected = market === p.symbol; + const retPct = p.profitLossPct; + + return ( +
onMarketSelect?.(p.symbol)} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onMarketSelect?.(p.symbol); + } + }} + > +
+
+ {name} + {code}/KRW +
+ {p.profitLoss != null && ( +
+ + {up ? '+' : ''}{fmtKrw(p.profitLoss)} + +
+ )} +
+ +
+
+ 보유수량 + {p.quantity.toFixed(4)} +
+
+ 평단가 + {fmtKrw(p.avgPrice)} +
+ {p.evalAmount != null && ( +
+ 평가금액 + {fmtKrw(p.evalAmount)} +
+ )} + {retPct != null && ( +
+ 수익률 + = 0 ? 'vtd-target-up' : 'vtd-target-dn'}`}> + {retPct >= 0 ? '+' : ''}{retPct.toFixed(2)}% + +
+ )} +
+
+ ); + })} +
+
+ )} +
); return ( diff --git a/frontend/src/components/paper/PaperSplitPanel.tsx b/frontend/src/components/paper/PaperSplitPanel.tsx index 6a95aac..426ff13 100644 --- a/frontend/src/components/paper/PaperSplitPanel.tsx +++ b/frontend/src/components/paper/PaperSplitPanel.tsx @@ -2,21 +2,42 @@ import React from 'react'; interface Props { topTitle?: string; + /** 타이틀바 우측 (매수·매도 안내 등) */ + topTitleAside?: React.ReactNode; bottomTitle?: string; + bottomTitleAside?: React.ReactNode; top: React.ReactNode; bottom: React.ReactNode; className?: string; } /** 좌·우 패널 상·하단 카드 분리 */ -const PaperSplitPanel: React.FC = ({ topTitle, bottomTitle, top, bottom, className = '' }) => ( +const PaperSplitPanel: React.FC = ({ + topTitle, + topTitleAside, + bottomTitle, + bottomTitleAside, + top, + bottom, + className = '', +}) => (
- {topTitle &&
{topTitle}
} + {topTitle && ( +
+ {topTitle} + {topTitleAside} +
+ )}
{top}
- {bottomTitle &&
{bottomTitle}
} + {bottomTitle && ( +
+ {bottomTitle} + {bottomTitleAside} +
+ )}
{bottom}
diff --git a/frontend/src/components/trade/TradeSplitOrderPanel.tsx b/frontend/src/components/trade/TradeSplitOrderPanel.tsx index 93197db..6d342f9 100644 --- a/frontend/src/components/trade/TradeSplitOrderPanel.tsx +++ b/frontend/src/components/trade/TradeSplitOrderPanel.tsx @@ -45,11 +45,20 @@ const TradeSplitOrderPanel: React.FC = ({ const resolvedBuyFill = fillBuy ?? (fillRequest?.side === 'buy' ? fillRequest : null); const resolvedSellFill = fillSell ?? (fillRequest?.side === 'sell' ? fillRequest : null); + const paperAutoOffHint = + paperTradingEnabled && !paperAutoTradeEnabled ? ( + + 자동매매 OFF · 수동 주문만 모의 계좌에 체결됩니다. + + ) : null; + return ( .ptd-table { + font-size: 12px; +} + +.bps-page--ptd .ptd-split-panel--left .ptd-split-card-body > .ptd-table th, +.bps-page--ptd .ptd-split-panel--left .ptd-split-card-body > .ptd-table td { + padding: 10px 12px; +} + +.bps-page--ptd .ptd-item-card-section .vtd-target-list-head { + font-size: 14px; +} + +.bps-page--ptd .ptd-item-card-section .vtd-target-count { + font-size: 12px; +} + +.bps-page--ptd .ptd-item-card-list.vtd-target-list { + gap: 10px; +} + +.bps-page--ptd .ptd-item-card-list .vtd-target-item { + padding: 12px 14px; +} + +.bps-page--ptd .ptd-item-card-list .vtd-target-ko { + font-size: 15px; +} + +.bps-page--ptd .ptd-item-card-list .vtd-target-en { + font-size: 11px; +} + +.bps-page--ptd .ptd-item-card-metrics { + gap: 8px; + margin-top: 10px; +} + +.bps-page--ptd .ptd-item-card-metrics .vtd-target-strategy-field { + gap: 10px; +} + +.bps-page--ptd .ptd-item-card-metrics .vtd-target-strategy-label, +.bps-page--ptd .ptd-item-card-metrics .vtd-trade-meta-value { + font-size: 13px; +} + +.bps-page--ptd .ptd-item-card-metrics .vtd-trade-meta-value { + padding: 6px 10px; +} + +.bps-page--ptd .ptd-holding-card .vtd-target-item-actions .vtd-target-price { + font-size: 14px; +} + +.bps-page--ptd .ptd-alloc-toggle { + font-size: 12px; + padding: 4px 10px; +} + +.bps-page--ptd .ptd-item-card-empty { + font-size: 12px; + padding: 14px 6px; +} + +.bps-page--ptd .ptd-invest-card .ptd-alloc-card-section { + flex: 1 1 auto; + min-height: 200px; + max-height: none; +} + .ptd-left-reset { width: 100%; margin-top: 4px; } +/* 설정 탭 — 보유 목록 카드와 동일 라벨·값 박스 행 */ +.ptd-settings-panel { + display: flex; + flex-direction: column; + gap: 10px; + flex: 1; + min-height: 0; + height: 100%; +} + +.ptd-settings-panel--account { + gap: 12px; +} + +.ptd-settings-link { + width: 100%; + justify-content: center; + font-size: 12px; + padding: 8px 10px; + border-radius: 8px; + flex-shrink: 0; +} + +.ptd-settings-card { + flex: 1; + min-height: 0; + cursor: default; +} + +.ptd-settings-card .ptd-item-card-metrics { + margin-top: 0; +} + +.ptd-settings-card .vtd-target-strategy-field { + margin-top: 0; +} + +.ptd-settings-input { + flex: 1; + min-width: 0; + width: 100%; + box-sizing: border-box; + margin: 0; + text-align: right; + font-variant-numeric: tabular-nums; + cursor: text; +} + +.ptd-settings-input:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.ptd-settings-value-box--check { + display: flex; + align-items: center; + justify-content: flex-end; + padding: 4px 10px; + cursor: pointer; +} + +.ptd-settings-value-box--check input[type='checkbox'] { + width: 16px; + height: 16px; + margin: 0; + cursor: pointer; + accent-color: var(--accent, #3f7ef5); +} + +.ptd-settings-reset { + width: 100%; + flex-shrink: 0; +} + +.ptd-split-panel--left .ptd-split-card-body:has(.ptd-settings-panel) { + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; + padding: 8px 10px 10px; +} + +.bps-page--ptd .ptd-settings-card .vtd-target-strategy-label, +.bps-page--ptd .ptd-settings-card .ptd-settings-input { + font-size: 13px; +} + .bps-right-body .ptd-right-body, .trade-right-panel .bps-right-body .ptd-right-body { flex: 1 1 0; diff --git a/frontend/src/styles/tradeRightPanel.css b/frontend/src/styles/tradeRightPanel.css index 2944008..4c344d1 100644 --- a/frontend/src/styles/tradeRightPanel.css +++ b/frontend/src/styles/tradeRightPanel.css @@ -95,6 +95,8 @@ .trade-right-panel .ptd-split-panel--right .ptd-split-card-head { display: flex; align-items: center; + justify-content: space-between; + gap: 10px; min-height: 42px; padding: 12px 14px 11px; font-size: 13px; @@ -103,6 +105,28 @@ box-sizing: border-box; } +.trade-right-panel .ptd-split-panel--right .ptd-split-card-head-label { + flex-shrink: 0; +} + +.trade-right-panel .ptd-split-panel--right .ptd-split-card-head-hint { + flex: 1; + min-width: 0; + margin: 0; + padding: 0; + font-size: 10px; + font-weight: 500; + line-height: 1.2; + color: var(--text3); + text-align: right; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + background: none; + border: none; + border-radius: 0; +} + .trade-right-panel .ptd-split-panel--right .ptd-split-card--top .ptd-split-card-head { color: var(--gc-trade-buy); border-bottom-color: color-mix(in srgb, var(--gc-trade-buy) 30%, var(--border)); diff --git a/frontend/src/styles/virtualTradingDashboard.css b/frontend/src/styles/virtualTradingDashboard.css index 56ce146..dfb19d0 100644 --- a/frontend/src/styles/virtualTradingDashboard.css +++ b/frontend/src/styles/virtualTradingDashboard.css @@ -2807,7 +2807,17 @@ } .ptd-tabs--center { flex-shrink: 0; + border-bottom: none; } + +.ptd-tabs--center .ptd-tab { + flex: 0 0 auto; + min-width: 0; + padding: 8px 18px; + font-size: 11px; + white-space: nowrap; +} + .ptd-invest-card { display: flex; flex-direction: column; @@ -2860,40 +2870,49 @@ gap: 8px; } .ptd-invest-kpi-cell { - padding: 8px 10px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 10px 12px; border-radius: 6px; background: var(--gc-surface-2, rgba(255,255,255,0.04)); + min-width: 0; } .ptd-invest-kpi-cell--up .ptd-invest-kpi-value { color: var(--gc-trade-buy, #ef5350); } .ptd-invest-kpi-cell--down .ptd-invest-kpi-value { color: var(--gc-trade-sell, #4dabf7); } .ptd-invest-kpi-label { - display: block; + flex-shrink: 0; font-size: 11px; - opacity: 0.7; + font-weight: 600; + opacity: 0.75; + white-space: nowrap; } .ptd-invest-kpi-value { - font-size: 15px; - font-weight: 600; -} -.ptd-alloc-table-wrap { - overflow: auto; flex: 1; - min-height: 0; -} -.ptd-alloc-row--selected { - background: rgba(255, 255, 255, 0.06); + min-width: 0; + font-size: 14px; + font-weight: 600; + text-align: right; + font-variant-numeric: tabular-nums; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } +/* 투자금 관리: PaperAllocationTable 카드 목록 — paperDashboard.css .ptd-alloc-card-section */ .ptd-alloc-toggle { font-size: 11px; padding: 2px 8px; border-radius: 4px; - border: 1px solid rgba(255,255,255,0.15); - background: transparent; + border: 1px solid color-mix(in srgb, var(--gc-trade-buy, #ef5350) 50%, transparent); + background: color-mix(in srgb, var(--gc-trade-buy, #ef5350) 12%, transparent); + color: var(--gc-trade-buy, #ef5350); cursor: pointer; } .ptd-alloc-toggle--on { - border-color: var(--gc-trade-buy, #ef5350); - color: var(--gc-trade-buy, #ef5350); + border-color: color-mix(in srgb, #b8f397 65%, var(--se-border, var(--border))); + color: #b8f397; + background: color-mix(in srgb, #a3e635 20%, transparent); } .ptd-footer-stack { display: flex;