모의투자 화면수정

This commit is contained in:
Macbook
2026-05-31 04:34:32 +09:00
parent 16d0e2c226
commit 05d2de5234
12 changed files with 717 additions and 190 deletions
+6 -3
View File
@@ -250,15 +250,18 @@ const PaperTradingPage: React.FC<Props> = ({
rightStorageKey="ptd-right-width" rightStorageKey="ptd-right-width"
rightDefaultWidth={440} rightDefaultWidth={440}
rightCollapsedStorageKey="ptd-right-open" rightCollapsedStorageKey="ptd-right-open"
headerCenter={!s?.enabled ? (
<span className="bps-header-status-text" role="status">
. .
</span>
) : undefined}
headerCenterMode="between"
headerActions={( headerActions={(
<> <>
<button type="button" className="bps-btn bps-btn--ghost" onClick={() => void loadData()}></button> <button type="button" className="bps-btn bps-btn--ghost" onClick={() => void loadData()}></button>
<button type="button" className="bps-btn bps-btn--danger" onClick={() => void handleReset()}> </button> <button type="button" className="bps-btn bps-btn--danger" onClick={() => void handleReset()}> </button>
</> </>
)} )}
banner={!s?.enabled ? (
<div className="bps-banner bps-banner--warn"> . .</div>
) : undefined}
leftTabs={leftTabs} leftTabs={leftTabs}
left={leftTab === 'strategy' ? ( left={leftTab === 'strategy' ? (
<PaperLeftStrategyTab <PaperLeftStrategyTab
@@ -268,9 +268,6 @@ const TradeOrderPanel: React.FC<TradeOrderPanelProps> = ({
return ( return (
<div className={`top-panel top-panel--upbit top-panel--${side}`}> <div className={`top-panel top-panel--upbit top-panel--${side}`}>
<div className="top-panel-fields"> <div className="top-panel-fields">
{paperTradingEnabled && !paperAutoTradeEnabled && showSymbolField && (
<p className="top-paper-hint"> OFF · .</p>
)}
{showSymbolField && ( {showSymbolField && (
<div className="top-field"> <div className="top-field">
<label className="top-label"></label> <label className="top-label"></label>
@@ -27,6 +27,8 @@ export interface BuilderPageShellProps {
headerActions?: React.ReactNode; headerActions?: React.ReactNode;
/** 타이틀 우측·중앙 그리드 시작선 정렬 (가상투자 등) */ /** 타이틀 우측·중앙 그리드 시작선 정렬 (가상투자 등) */
headerCenter?: React.ReactNode; headerCenter?: React.ReactNode;
/** grid: 좌패널폭 기준 3열 / between: 타이틀·버튼 사이 중앙 (모의투자 안내 등) */
headerCenterMode?: 'grid' | 'between';
pageClassName?: string; pageClassName?: string;
banner?: React.ReactNode; banner?: React.ReactNode;
leftTitle?: string; leftTitle?: string;
@@ -103,6 +105,7 @@ export default function BuilderPageShell({
subtitle, subtitle,
headerActions, headerActions,
headerCenter, headerCenter,
headerCenterMode = 'grid',
pageClassName, pageClassName,
banner, banner,
leftTitle, leftTitle,
@@ -252,21 +255,34 @@ export default function BuilderPageShell({
className={`bps-page se-page se-page--${theme}${pageClassName ? ` ${pageClassName}` : ''}`} className={`bps-page se-page se-page--${theme}${pageClassName ? ` ${pageClassName}` : ''}`}
style={pageStyle} style={pageStyle}
> >
<header className={`bps-header${headerCenter ? ' bps-header--vtd' : ''}`}> <header
className={[
'bps-header',
headerCenter && headerCenterMode === 'between' ? 'bps-header--between' : '',
headerCenter && headerCenterMode === 'grid' ? 'bps-header--vtd' : '',
].filter(Boolean).join(' ')}
>
<div className="bps-header-left"> <div className="bps-header-left">
<h1 className="bps-title">{title}</h1> <h1 className="bps-title">{title}</h1>
{subtitle && <span className="bps-subtitle">{subtitle}</span>} {subtitle && <span className="bps-subtitle">{subtitle}</span>}
</div> </div>
{headerCenter ? ( {headerCenter ? (
<> headerCenterMode === 'between' ? (
<div className="bps-header-center">{headerCenter}</div> <>
{headerActions && ( <div className="bps-header-center">{headerCenter}</div>
<> {headerActions && <div className="bps-header-actions">{headerActions}</div>}
<span className="header-toolbar-divider" aria-hidden="true" /> </>
<div className="bps-header-actions">{headerActions}</div> ) : (
</> <>
)} <div className="bps-header-center">{headerCenter}</div>
</> {headerActions && (
<>
<span className="header-toolbar-divider" aria-hidden="true" />
<div className="bps-header-actions">{headerActions}</div>
</>
)}
</>
)
) : ( ) : (
headerActions && <div className="bps-header-actions">{headerActions}</div> headerActions && <div className="bps-header-actions">{headerActions}</div>
)} )}
@@ -26,57 +26,101 @@ const PaperAllocationTable: React.FC<Props> = ({
}, [onChanged]); }, [onChanged]);
if (!allocations.length) { if (!allocations.length) {
return <p className="ptd-muted"> . .</p>; return (
<div className="ptd-item-card-section ptd-alloc-card-section">
<div className="vtd-target-list-head">
<span> </span>
<span className="vtd-target-count">0</span>
</div>
<p className="ptd-muted ptd-item-card-empty"> . .</p>
</div>
);
} }
return ( return (
<div className="ptd-alloc-table-wrap"> <div className="ptd-item-card-section ptd-alloc-card-section">
<table className="ptd-table ptd-alloc-table"> <div className="vtd-target-list-head">
<thead> <span> </span>
<tr> <span className="vtd-target-count">{allocations.length}</span>
<th></th> </div>
<th></th> <div className="ptd-item-card-scroll">
<th></th> <div className="vtd-target-list ptd-item-card-list">
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{allocations.map(row => { {allocations.map(row => {
const ret = row.symbolReturnPct ?? 0; const ret = row.symbolReturnPct ?? 0;
const selected = row.symbol === selectedMarket; const selected = row.symbol === selectedMarket;
const name = row.koreanName || coinCode(row.symbol);
const code = coinCode(row.symbol);
const up = ret >= 0;
return ( return (
<tr <div
key={row.symbol} key={row.symbol}
className={selected ? 'ptd-alloc-row--selected' : ''} className={[
onClick={() => onSelectMarket?.(row.symbol)} 'vtd-target-item',
'ptd-item-card',
'ptd-alloc-card',
selected ? 'vtd-target-item--active' : '',
row.isActive === false ? 'ptd-alloc-card--inactive' : '',
].filter(Boolean).join(' ')}
role="button" role="button"
tabIndex={0} tabIndex={0}
onKeyDown={e => { if (e.key === 'Enter') onSelectMarket?.(row.symbol); }} onClick={() => onSelectMarket?.(row.symbol)}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onSelectMarket?.(row.symbol);
}
}}
> >
<td>{row.koreanName || coinCode(row.symbol)}</td> <div className="vtd-target-item-main">
<td>{(row.weightPct ?? 0).toFixed(1)}%</td> <div className="vtd-target-names">
<td>{fmtKrw(row.maxInvestKrw)}</td> <span className="vtd-target-ko">{name}</span>
<td>{fmtKrw(row.investedKrw ?? 0)}</td> <span className="vtd-target-en">{code}/KRW</span>
<td>{fmtKrw(row.evalAmount ?? 0)}</td> </div>
<td className={ret >= 0 ? 'up' : 'down'}>{ret >= 0 ? '+' : ''}{ret.toFixed(2)}%</td> <div className="vtd-target-item-actions">
<td onClick={e => e.stopPropagation()}> <button
<button type="button"
type="button" className={`ptd-alloc-toggle${row.isActive !== false ? ' ptd-alloc-toggle--on' : ''}`}
className={`ptd-alloc-toggle${row.isActive !== false ? ' ptd-alloc-toggle--on' : ''}`} onClick={e => {
onClick={() => void toggleActive(row)} e.stopPropagation();
title={row.isActive !== false ? '신규 매수 허용' : '신규 매수 차단'} void toggleActive(row);
> }}
{row.isActive !== false ? 'ON' : 'OFF'} title={row.isActive !== false ? '신규 매수 허용' : '신규 매수 차단'}
</button> >
</td> {row.isActive !== false ? 'ON' : 'OFF'}
</tr> </button>
</div>
</div>
<div className="ptd-item-card-metrics">
<div className="vtd-target-strategy-field">
<span className="vtd-target-strategy-label"></span>
<span className="vtd-trade-meta-value">{(row.weightPct ?? 0).toFixed(1)}%</span>
</div>
<div className="vtd-target-strategy-field">
<span className="vtd-target-strategy-label"></span>
<span className="vtd-trade-meta-value">{fmtKrw(row.maxInvestKrw)}</span>
</div>
<div className="vtd-target-strategy-field">
<span className="vtd-target-strategy-label"></span>
<span className="vtd-trade-meta-value">{fmtKrw(row.investedKrw ?? 0)}</span>
</div>
<div className="vtd-target-strategy-field">
<span className="vtd-target-strategy-label"></span>
<span className="vtd-trade-meta-value">{fmtKrw(row.evalAmount ?? 0)}</span>
</div>
<div className="vtd-target-strategy-field">
<span className="vtd-target-strategy-label"></span>
<span className={`vtd-trade-meta-value ${up ? 'vtd-target-up' : 'vtd-target-dn'}`}>
{ret >= 0 ? '+' : ''}{ret.toFixed(2)}%
</span>
</div>
</div>
</div>
); );
})} })}
</tbody> </div>
</table> </div>
</div> </div>
); );
}; };
@@ -9,6 +9,13 @@ interface Props {
onOpenSettings?: () => void; onOpenSettings?: () => void;
} }
const SettingsFieldRow: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => (
<div className="vtd-target-strategy-field">
<span className="vtd-target-strategy-label">{label}</span>
{children}
</div>
);
const PaperLeftSettingsTab: React.FC<Props> = ({ onSettingsSaved, onAccountReset, onOpenSettings }) => { const PaperLeftSettingsTab: React.FC<Props> = ({ onSettingsSaved, onAccountReset, onOpenSettings }) => {
const { defaults, save, isLoaded } = useAppSettings(); const { defaults, save, isLoaded } = useAppSettings();
@@ -28,101 +35,100 @@ const PaperLeftSettingsTab: React.FC<Props> = ({ onSettingsSaved, onAccountReset
}; };
const top = ( const top = (
<> <div className="ptd-settings-panel">
{onOpenSettings && ( {onOpenSettings && (
<button type="button" className="bps-btn bps-btn--ghost ptd-left-settings-link" onClick={onOpenSettings}> <button type="button" className="bps-btn bps-btn--ghost ptd-settings-link" onClick={onOpenSettings}>
</button> </button>
)} )}
<div className="ptd-left-section"> <div className="vtd-target-item ptd-item-card ptd-settings-card">
<label className="ptd-left-row"> <div className="ptd-item-card-metrics">
<span> </span> <SettingsFieldRow label="모의투자 활성화">
<input <label className="vtd-trade-meta-value ptd-settings-value-box ptd-settings-value-box--check">
type="checkbox" <input
checked={defaults.paperTradingEnabled} type="checkbox"
onChange={e => patch({ paperTradingEnabled: e.target.checked })} checked={defaults.paperTradingEnabled}
/> onChange={e => patch({ paperTradingEnabled: e.target.checked })}
</label> />
</label>
</SettingsFieldRow>
<SettingsFieldRow label="자동매매">
<label className="vtd-trade-meta-value ptd-settings-value-box ptd-settings-value-box--check">
<input
type="checkbox"
checked={defaults.paperAutoTradeEnabled}
onChange={e => patch({ paperAutoTradeEnabled: e.target.checked })}
/>
</label>
</SettingsFieldRow>
<SettingsFieldRow label="매수 예산 (%)">
<input
type="number"
className="vtd-trade-meta-value ptd-settings-input"
min={1}
max={100}
value={defaults.paperAutoTradeBudgetPct}
disabled={!defaults.paperAutoTradeEnabled}
onChange={e => patch({ paperAutoTradeBudgetPct: Number(e.target.value) })}
/>
</SettingsFieldRow>
</div>
</div> </div>
<div className="ptd-left-section"> </div>
<label className="ptd-left-row">
<span></span>
<input
type="checkbox"
checked={defaults.paperAutoTradeEnabled}
onChange={e => patch({ paperAutoTradeEnabled: e.target.checked })}
/>
</label>
<label className="ptd-left-field">
<span> (%)</span>
<input
type="number"
className="ptd-left-input"
min={1}
max={100}
value={defaults.paperAutoTradeBudgetPct}
disabled={!defaults.paperAutoTradeEnabled}
onChange={e => patch({ paperAutoTradeBudgetPct: Number(e.target.value) })}
/>
</label>
</div>
</>
); );
const bottom = ( const bottom = (
<> <div className="ptd-settings-panel ptd-settings-panel--account">
<div className="ptd-left-section"> <div className="vtd-target-item ptd-item-card ptd-settings-card">
<label className="ptd-left-field"> <div className="ptd-item-card-metrics">
<span> (KRW)</span> <SettingsFieldRow label="초기 자본 (KRW)">
<input <input
type="number" type="number"
className="ptd-left-input" className="vtd-trade-meta-value ptd-settings-input"
min={100000} min={100000}
step={100000} step={100000}
value={defaults.paperInitialCapital} value={defaults.paperInitialCapital}
onChange={e => patch({ paperInitialCapital: Number(e.target.value) })} onChange={e => patch({ paperInitialCapital: Number(e.target.value) })}
/> />
</label> </SettingsFieldRow>
<label className="ptd-left-field"> <SettingsFieldRow label="수수료율 (%)">
<span> (%)</span> <input
<input type="number"
type="number" className="vtd-trade-meta-value ptd-settings-input"
className="ptd-left-input" min={0}
min={0} max={1}
max={1} step={0.01}
step={0.01} value={defaults.paperFeeRatePct}
value={defaults.paperFeeRatePct} onChange={e => patch({ paperFeeRatePct: Number(e.target.value) })}
onChange={e => patch({ paperFeeRatePct: Number(e.target.value) })} />
/> </SettingsFieldRow>
</label> <SettingsFieldRow label="슬리피지 (%)">
<label className="ptd-left-field"> <input
<span> (%)</span> type="number"
<input className="vtd-trade-meta-value ptd-settings-input"
type="number" min={0}
className="ptd-left-input" max={5}
min={0} step={0.01}
max={5} value={defaults.paperSlippagePct}
step={0.01} onChange={e => patch({ paperSlippagePct: Number(e.target.value) })}
value={defaults.paperSlippagePct} />
onChange={e => patch({ paperSlippagePct: Number(e.target.value) })} </SettingsFieldRow>
/> <SettingsFieldRow label="최소 주문 (KRW)">
</label> <input
<label className="ptd-left-field"> type="number"
<span> (KRW)</span> className="vtd-trade-meta-value ptd-settings-input"
<input min={1000}
type="number" step={1000}
className="ptd-left-input" value={defaults.paperMinOrderKrw}
min={1000} onChange={e => patch({ paperMinOrderKrw: Number(e.target.value) })}
step={1000} />
value={defaults.paperMinOrderKrw} </SettingsFieldRow>
onChange={e => patch({ paperMinOrderKrw: Number(e.target.value) })} </div>
/>
</label>
</div> </div>
<button type="button" className="bps-btn bps-btn--danger ptd-left-reset" onClick={() => void handleReset()}> <button type="button" className="bps-btn bps-btn--danger ptd-settings-reset" onClick={() => void handleReset()}>
</button> </button>
</> </div>
); );
return ( return (
@@ -91,33 +91,90 @@ const PaperLeftStrategyTab: React.FC<Props> = ({
</> </>
); );
const positions = summary?.positions ?? [];
const bottom = ( const bottom = (
<table className="ptd-table ptd-table--compact"> <div className="ptd-item-card-section ptd-holdings-card-section">
<thead> <div className="vtd-target-list-head">
<tr><th></th><th></th><th></th></tr> <span> </span>
</thead> <span className="vtd-target-count">{positions.length}</span>
<tbody> </div>
{(summary?.positions ?? []).length === 0 ? ( {positions.length === 0 ? (
<tr><td colSpan={3} className="ptd-muted"> </td></tr> <p className="ptd-muted ptd-item-card-empty"> .</p>
) : summary!.positions.map(p => { ) : (
const code = p.symbol.replace(/^KRW-/, ''); <div className="ptd-item-card-scroll">
const up = (p.profitLoss ?? 0) >= 0; <div className="vtd-target-list ptd-item-card-list">
return ( {positions.map(p => {
<tr const code = p.symbol.replace(/^KRW-/, '');
key={p.symbol} const name = p.koreanName || code;
className={market === p.symbol ? 'ptd-row--sel' : ''} const up = (p.profitLoss ?? 0) >= 0;
onClick={() => onMarketSelect?.(p.symbol)} const selected = market === p.symbol;
> const retPct = p.profitLossPct;
<td>{code}</td>
<td>{p.quantity.toFixed(4)}</td> return (
<td className={up ? 'up' : 'down'}> <div
{p.profitLoss != null ? `${up ? '+' : ''}${fmtKrw(p.profitLoss)}` : '—'} key={p.symbol}
</td> className={[
</tr> 'vtd-target-item',
); 'ptd-item-card',
})} 'ptd-holding-card',
</tbody> selected ? 'vtd-target-item--active' : '',
</table> ].filter(Boolean).join(' ')}
role="button"
tabIndex={0}
onClick={() => onMarketSelect?.(p.symbol)}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onMarketSelect?.(p.symbol);
}
}}
>
<div className="vtd-target-item-main">
<div className="vtd-target-names">
<span className="vtd-target-ko">{name}</span>
<span className="vtd-target-en">{code}/KRW</span>
</div>
{p.profitLoss != null && (
<div className="vtd-target-item-actions">
<span className={`vtd-target-price ${up ? 'vtd-target-up' : 'vtd-target-dn'}`}>
{up ? '+' : ''}{fmtKrw(p.profitLoss)}
</span>
</div>
)}
</div>
<div className="ptd-item-card-metrics">
<div className="vtd-target-strategy-field">
<span className="vtd-target-strategy-label"></span>
<span className="vtd-trade-meta-value">{p.quantity.toFixed(4)}</span>
</div>
<div className="vtd-target-strategy-field">
<span className="vtd-target-strategy-label"></span>
<span className="vtd-trade-meta-value">{fmtKrw(p.avgPrice)}</span>
</div>
{p.evalAmount != null && (
<div className="vtd-target-strategy-field">
<span className="vtd-target-strategy-label"></span>
<span className="vtd-trade-meta-value">{fmtKrw(p.evalAmount)}</span>
</div>
)}
{retPct != null && (
<div className="vtd-target-strategy-field">
<span className="vtd-target-strategy-label"></span>
<span className={`vtd-trade-meta-value ${retPct >= 0 ? 'vtd-target-up' : 'vtd-target-dn'}`}>
{retPct >= 0 ? '+' : ''}{retPct.toFixed(2)}%
</span>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
</div>
); );
return ( return (
@@ -2,21 +2,42 @@ import React from 'react';
interface Props { interface Props {
topTitle?: string; topTitle?: string;
/** 타이틀바 우측 (매수·매도 안내 등) */
topTitleAside?: React.ReactNode;
bottomTitle?: string; bottomTitle?: string;
bottomTitleAside?: React.ReactNode;
top: React.ReactNode; top: React.ReactNode;
bottom: React.ReactNode; bottom: React.ReactNode;
className?: string; className?: string;
} }
/** 좌·우 패널 상·하단 카드 분리 */ /** 좌·우 패널 상·하단 카드 분리 */
const PaperSplitPanel: React.FC<Props> = ({ topTitle, bottomTitle, top, bottom, className = '' }) => ( const PaperSplitPanel: React.FC<Props> = ({
topTitle,
topTitleAside,
bottomTitle,
bottomTitleAside,
top,
bottom,
className = '',
}) => (
<div className={`ptd-split-panel${className ? ` ${className}` : ''}`}> <div className={`ptd-split-panel${className ? ` ${className}` : ''}`}>
<section className="ptd-split-card ptd-split-card--top"> <section className="ptd-split-card ptd-split-card--top">
{topTitle && <div className="ptd-split-card-head">{topTitle}</div>} {topTitle && (
<div className="ptd-split-card-head">
<span className="ptd-split-card-head-label">{topTitle}</span>
{topTitleAside}
</div>
)}
<div className="ptd-split-card-body">{top}</div> <div className="ptd-split-card-body">{top}</div>
</section> </section>
<section className="ptd-split-card ptd-split-card--bottom"> <section className="ptd-split-card ptd-split-card--bottom">
{bottomTitle && <div className="ptd-split-card-head">{bottomTitle}</div>} {bottomTitle && (
<div className="ptd-split-card-head">
<span className="ptd-split-card-head-label">{bottomTitle}</span>
{bottomTitleAside}
</div>
)}
<div className="ptd-split-card-body">{bottom}</div> <div className="ptd-split-card-body">{bottom}</div>
</section> </section>
</div> </div>
@@ -45,11 +45,20 @@ const TradeSplitOrderPanel: React.FC<TradeSplitOrderPanelProps> = ({
const resolvedBuyFill = fillBuy ?? (fillRequest?.side === 'buy' ? fillRequest : null); const resolvedBuyFill = fillBuy ?? (fillRequest?.side === 'buy' ? fillRequest : null);
const resolvedSellFill = fillSell ?? (fillRequest?.side === 'sell' ? fillRequest : null); const resolvedSellFill = fillSell ?? (fillRequest?.side === 'sell' ? fillRequest : null);
const paperAutoOffHint =
paperTradingEnabled && !paperAutoTradeEnabled ? (
<span className="ptd-split-card-head-hint" title="자동매매 OFF · 수동 주문만 모의 계좌에 체결됩니다.">
OFF · .
</span>
) : null;
return ( return (
<PaperSplitPanel <PaperSplitPanel
className={`ptd-split-panel--right${className ? ` ${className}` : ''}`} className={`ptd-split-panel--right${className ? ` ${className}` : ''}`}
topTitle="매수" topTitle="매수"
topTitleAside={paperAutoOffHint}
bottomTitle="매도" bottomTitle="매도"
bottomTitleAside={paperAutoOffHint}
top={( top={(
<TradeBuyOrderPanel <TradeBuyOrderPanel
market={market} market={market}
+36
View File
@@ -113,6 +113,42 @@
color: #fbbf24; color: #fbbf24;
} }
/* 타이틀·우측 버튼 사이 중앙 안내 (텍스트만, 1줄) */
.bps-header--between {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 18px;
min-height: 48px;
}
.bps-header--between .bps-header-left {
flex-shrink: 0;
}
.bps-header--between .bps-header-center {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
min-width: 0;
}
.bps-header--between .bps-header-actions {
flex-shrink: 0;
}
.bps-header-status-text {
font-size: 0.75rem;
font-weight: 500;
line-height: 1.2;
color: #fbbf24;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.bps-body { .bps-body {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
+302 -7
View File
@@ -138,6 +138,48 @@
color: var(--text2, var(--se-text-muted)); color: var(--text2, var(--se-text-muted));
border-bottom: 1px solid var(--sep, var(--se-border)); border-bottom: 1px solid var(--sep, var(--se-border));
} }
/* 모의투자 좌측(전략·자산 / 보유 종목 등): 우측 매수·매도 타이틀바와 동일 높이·연한 그라데이션 */
.ptd-split-panel--left .ptd-split-card--top {
border-color: color-mix(in srgb, var(--gc-trade-buy) 35%, var(--border));
}
.ptd-split-panel--left .ptd-split-card--bottom {
border-color: color-mix(in srgb, var(--gc-trade-sell) 35%, var(--border));
}
.ptd-split-panel--left .ptd-split-card-head {
display: flex;
align-items: center;
min-height: 42px;
padding: 12px 14px 11px;
font-size: 13px;
font-weight: 700;
border-bottom: 1px solid var(--border);
box-sizing: border-box;
}
.ptd-split-panel--left .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));
background: linear-gradient(
180deg,
color-mix(in srgb, var(--gc-trade-buy) 24%, var(--bg2)) 0%,
color-mix(in srgb, var(--gc-trade-buy) 10%, var(--bg2)) 50%,
var(--bg2) 100%
);
}
.ptd-split-panel--left .ptd-split-card--bottom .ptd-split-card-head {
color: var(--gc-trade-sell);
border-bottom-color: color-mix(in srgb, var(--gc-trade-sell) 30%, var(--border));
background: linear-gradient(
180deg,
color-mix(in srgb, var(--gc-trade-sell) 24%, var(--bg2)) 0%,
color-mix(in srgb, var(--gc-trade-sell) 10%, var(--bg2)) 50%,
var(--bg2) 100%
);
}
.ptd-split-card-body { .ptd-split-card-body {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@@ -232,26 +274,279 @@
} }
.ptd-left-hint { .ptd-left-hint {
margin: 0; margin: 0;
font-size: 10px; font-size: 12px;
line-height: 1.45;
color: var(--accent); color: var(--accent);
} }
.ptd-left-status { .ptd-left-status {
margin: 0; margin: 0;
font-size: 10px; font-size: 12px;
color: var(--text3); line-height: 1.45;
padding: 6px 8px; color: var(--text2);
border-radius: 8px; padding: 0;
background: var(--bg3); border-radius: 0;
background: none;
} }
.ptd-left-status--on { .ptd-left-status--on {
color: #22c55e; color: #22c55e;
background: rgba(34, 197, 94, 0.1); background: none;
} }
.bps-page--ptd .ptd-left-hint,
.bps-page--ptd .ptd-left-status {
font-size: 13px;
}
/* 모의투자 — 보유·투자금 카드 목록 */
.ptd-item-card-section {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
min-height: 0;
}
.ptd-item-card-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
}
.ptd-item-card-list {
padding-right: 2px;
}
.ptd-item-card-empty {
margin: 0;
padding: 12px 4px;
font-size: 11px;
}
.ptd-item-card-metrics {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 8px;
}
.ptd-item-card-metrics .vtd-target-strategy-field {
margin-top: 0;
}
.ptd-alloc-card-section {
flex: 1 1 0;
min-height: 180px;
}
.ptd-holdings-card-section {
flex: 1 1 0;
min-height: 0;
}
.ptd-split-panel--left .ptd-split-card-body:has(.ptd-holdings-card-section) {
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
padding: 8px 10px 10px;
}
.ptd-alloc-card--inactive {
opacity: 0.72;
}
.ptd-alloc-card--inactive .vtd-target-ko {
color: var(--text3);
}
.ptd-holding-card .vtd-target-item-actions .vtd-target-price {
font-size: 12px;
}
/* 모의투자 목록 — 행·글자 약간 확대 (보유 종목·투자금 표/카드) */
.bps-page--ptd .ptd-table {
font-size: 12px;
}
.bps-page--ptd .ptd-table th,
.bps-page--ptd .ptd-table td {
padding: 10px 12px;
line-height: 1.45;
}
.bps-page--ptd .ptd-table--compact {
font-size: 11px;
}
.bps-page--ptd .ptd-table--compact th,
.bps-page--ptd .ptd-table--compact td {
padding: 9px 11px;
}
.bps-page--ptd .ptd-split-panel--left .ptd-split-card-body > .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 { .ptd-left-reset {
width: 100%; width: 100%;
margin-top: 4px; 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, .bps-right-body .ptd-right-body,
.trade-right-panel .bps-right-body .ptd-right-body { .trade-right-panel .bps-right-body .ptd-right-body {
flex: 1 1 0; flex: 1 1 0;
+24
View File
@@ -95,6 +95,8 @@
.trade-right-panel .ptd-split-panel--right .ptd-split-card-head { .trade-right-panel .ptd-split-panel--right .ptd-split-card-head {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
gap: 10px;
min-height: 42px; min-height: 42px;
padding: 12px 14px 11px; padding: 12px 14px 11px;
font-size: 13px; font-size: 13px;
@@ -103,6 +105,28 @@
box-sizing: border-box; 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 { .trade-right-panel .ptd-split-panel--right .ptd-split-card--top .ptd-split-card-head {
color: var(--gc-trade-buy); color: var(--gc-trade-buy);
border-bottom-color: color-mix(in srgb, var(--gc-trade-buy) 30%, var(--border)); border-bottom-color: color-mix(in srgb, var(--gc-trade-buy) 30%, var(--border));
+35 -16
View File
@@ -2807,7 +2807,17 @@
} }
.ptd-tabs--center { .ptd-tabs--center {
flex-shrink: 0; 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 { .ptd-invest-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -2860,40 +2870,49 @@
gap: 8px; gap: 8px;
} }
.ptd-invest-kpi-cell { .ptd-invest-kpi-cell {
padding: 8px 10px; display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 12px;
border-radius: 6px; border-radius: 6px;
background: var(--gc-surface-2, rgba(255,255,255,0.04)); 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--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-cell--down .ptd-invest-kpi-value { color: var(--gc-trade-sell, #4dabf7); }
.ptd-invest-kpi-label { .ptd-invest-kpi-label {
display: block; flex-shrink: 0;
font-size: 11px; font-size: 11px;
opacity: 0.7; font-weight: 600;
opacity: 0.75;
white-space: nowrap;
} }
.ptd-invest-kpi-value { .ptd-invest-kpi-value {
font-size: 15px;
font-weight: 600;
}
.ptd-alloc-table-wrap {
overflow: auto;
flex: 1; flex: 1;
min-height: 0; min-width: 0;
} font-size: 14px;
.ptd-alloc-row--selected { font-weight: 600;
background: rgba(255, 255, 255, 0.06); 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 { .ptd-alloc-toggle {
font-size: 11px; font-size: 11px;
padding: 2px 8px; padding: 2px 8px;
border-radius: 4px; border-radius: 4px;
border: 1px solid rgba(255,255,255,0.15); border: 1px solid color-mix(in srgb, var(--gc-trade-buy, #ef5350) 50%, transparent);
background: transparent; background: color-mix(in srgb, var(--gc-trade-buy, #ef5350) 12%, transparent);
color: var(--gc-trade-buy, #ef5350);
cursor: pointer; cursor: pointer;
} }
.ptd-alloc-toggle--on { .ptd-alloc-toggle--on {
border-color: var(--gc-trade-buy, #ef5350); border-color: color-mix(in srgb, #b8f397 65%, var(--se-border, var(--border)));
color: var(--gc-trade-buy, #ef5350); color: #b8f397;
background: color-mix(in srgb, #a3e635 20%, transparent);
} }
.ptd-footer-stack { .ptd-footer-stack {
display: flex; display: flex;