모의투자 화면수정
This commit is contained in:
@@ -26,57 +26,101 @@ const PaperAllocationTable: React.FC<Props> = ({
|
||||
}, [onChanged]);
|
||||
|
||||
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 (
|
||||
<div className="ptd-alloc-table-wrap">
|
||||
<table className="ptd-table ptd-alloc-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>종목</th>
|
||||
<th>비중</th>
|
||||
<th>한도</th>
|
||||
<th>투입</th>
|
||||
<th>평가</th>
|
||||
<th>수익률</th>
|
||||
<th>활성</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<div className="ptd-item-card-section ptd-alloc-card-section">
|
||||
<div className="vtd-target-list-head">
|
||||
<span>종목별 한도</span>
|
||||
<span className="vtd-target-count">{allocations.length}종목</span>
|
||||
</div>
|
||||
<div className="ptd-item-card-scroll">
|
||||
<div className="vtd-target-list ptd-item-card-list">
|
||||
{allocations.map(row => {
|
||||
const ret = row.symbolReturnPct ?? 0;
|
||||
const selected = row.symbol === selectedMarket;
|
||||
const name = row.koreanName || coinCode(row.symbol);
|
||||
const code = coinCode(row.symbol);
|
||||
const up = ret >= 0;
|
||||
|
||||
return (
|
||||
<tr
|
||||
<div
|
||||
key={row.symbol}
|
||||
className={selected ? 'ptd-alloc-row--selected' : ''}
|
||||
onClick={() => onSelectMarket?.(row.symbol)}
|
||||
className={[
|
||||
'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"
|
||||
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>
|
||||
<td>{(row.weightPct ?? 0).toFixed(1)}%</td>
|
||||
<td>{fmtKrw(row.maxInvestKrw)}</td>
|
||||
<td>{fmtKrw(row.investedKrw ?? 0)}</td>
|
||||
<td>{fmtKrw(row.evalAmount ?? 0)}</td>
|
||||
<td className={ret >= 0 ? 'up' : 'down'}>{ret >= 0 ? '+' : ''}{ret.toFixed(2)}%</td>
|
||||
<td onClick={e => e.stopPropagation()}>
|
||||
<button
|
||||
type="button"
|
||||
className={`ptd-alloc-toggle${row.isActive !== false ? ' ptd-alloc-toggle--on' : ''}`}
|
||||
onClick={() => void toggleActive(row)}
|
||||
title={row.isActive !== false ? '신규 매수 허용' : '신규 매수 차단'}
|
||||
>
|
||||
{row.isActive !== false ? 'ON' : 'OFF'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<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>
|
||||
<div className="vtd-target-item-actions">
|
||||
<button
|
||||
type="button"
|
||||
className={`ptd-alloc-toggle${row.isActive !== false ? ' ptd-alloc-toggle--on' : ''}`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
void toggleActive(row);
|
||||
}}
|
||||
title={row.isActive !== false ? '신규 매수 허용' : '신규 매수 차단'}
|
||||
>
|
||||
{row.isActive !== false ? 'ON' : 'OFF'}
|
||||
</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>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,13 @@ interface Props {
|
||||
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 { defaults, save, isLoaded } = useAppSettings();
|
||||
|
||||
@@ -28,101 +35,100 @@ const PaperLeftSettingsTab: React.FC<Props> = ({ onSettingsSaved, onAccountReset
|
||||
};
|
||||
|
||||
const top = (
|
||||
<>
|
||||
<div className="ptd-settings-panel">
|
||||
{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>
|
||||
)}
|
||||
<div className="ptd-left-section">
|
||||
<label className="ptd-left-row">
|
||||
<span>모의투자 활성화</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={defaults.paperTradingEnabled}
|
||||
onChange={e => patch({ paperTradingEnabled: e.target.checked })}
|
||||
/>
|
||||
</label>
|
||||
<div className="vtd-target-item ptd-item-card ptd-settings-card">
|
||||
<div className="ptd-item-card-metrics">
|
||||
<SettingsFieldRow label="모의투자 활성화">
|
||||
<label className="vtd-trade-meta-value ptd-settings-value-box ptd-settings-value-box--check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={defaults.paperTradingEnabled}
|
||||
onChange={e => patch({ paperTradingEnabled: e.target.checked })}
|
||||
/>
|
||||
</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 className="ptd-left-section">
|
||||
<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>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
|
||||
const bottom = (
|
||||
<>
|
||||
<div className="ptd-left-section">
|
||||
<label className="ptd-left-field">
|
||||
<span>초기 자본 (KRW)</span>
|
||||
<input
|
||||
type="number"
|
||||
className="ptd-left-input"
|
||||
min={100000}
|
||||
step={100000}
|
||||
value={defaults.paperInitialCapital}
|
||||
onChange={e => patch({ paperInitialCapital: Number(e.target.value) })}
|
||||
/>
|
||||
</label>
|
||||
<label className="ptd-left-field">
|
||||
<span>수수료율 (%)</span>
|
||||
<input
|
||||
type="number"
|
||||
className="ptd-left-input"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={defaults.paperFeeRatePct}
|
||||
onChange={e => patch({ paperFeeRatePct: Number(e.target.value) })}
|
||||
/>
|
||||
</label>
|
||||
<label className="ptd-left-field">
|
||||
<span>슬리피지 (%)</span>
|
||||
<input
|
||||
type="number"
|
||||
className="ptd-left-input"
|
||||
min={0}
|
||||
max={5}
|
||||
step={0.01}
|
||||
value={defaults.paperSlippagePct}
|
||||
onChange={e => patch({ paperSlippagePct: Number(e.target.value) })}
|
||||
/>
|
||||
</label>
|
||||
<label className="ptd-left-field">
|
||||
<span>최소 주문 (KRW)</span>
|
||||
<input
|
||||
type="number"
|
||||
className="ptd-left-input"
|
||||
min={1000}
|
||||
step={1000}
|
||||
value={defaults.paperMinOrderKrw}
|
||||
onChange={e => patch({ paperMinOrderKrw: Number(e.target.value) })}
|
||||
/>
|
||||
</label>
|
||||
<div className="ptd-settings-panel ptd-settings-panel--account">
|
||||
<div className="vtd-target-item ptd-item-card ptd-settings-card">
|
||||
<div className="ptd-item-card-metrics">
|
||||
<SettingsFieldRow label="초기 자본 (KRW)">
|
||||
<input
|
||||
type="number"
|
||||
className="vtd-trade-meta-value ptd-settings-input"
|
||||
min={100000}
|
||||
step={100000}
|
||||
value={defaults.paperInitialCapital}
|
||||
onChange={e => patch({ paperInitialCapital: Number(e.target.value) })}
|
||||
/>
|
||||
</SettingsFieldRow>
|
||||
<SettingsFieldRow label="수수료율 (%)">
|
||||
<input
|
||||
type="number"
|
||||
className="vtd-trade-meta-value ptd-settings-input"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={defaults.paperFeeRatePct}
|
||||
onChange={e => patch({ paperFeeRatePct: Number(e.target.value) })}
|
||||
/>
|
||||
</SettingsFieldRow>
|
||||
<SettingsFieldRow label="슬리피지 (%)">
|
||||
<input
|
||||
type="number"
|
||||
className="vtd-trade-meta-value ptd-settings-input"
|
||||
min={0}
|
||||
max={5}
|
||||
step={0.01}
|
||||
value={defaults.paperSlippagePct}
|
||||
onChange={e => patch({ paperSlippagePct: Number(e.target.value) })}
|
||||
/>
|
||||
</SettingsFieldRow>
|
||||
<SettingsFieldRow label="최소 주문 (KRW)">
|
||||
<input
|
||||
type="number"
|
||||
className="vtd-trade-meta-value ptd-settings-input"
|
||||
min={1000}
|
||||
step={1000}
|
||||
value={defaults.paperMinOrderKrw}
|
||||
onChange={e => patch({ paperMinOrderKrw: Number(e.target.value) })}
|
||||
/>
|
||||
</SettingsFieldRow>
|
||||
</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>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -91,33 +91,90 @@ const PaperLeftStrategyTab: React.FC<Props> = ({
|
||||
</>
|
||||
);
|
||||
|
||||
const positions = summary?.positions ?? [];
|
||||
|
||||
const bottom = (
|
||||
<table className="ptd-table ptd-table--compact">
|
||||
<thead>
|
||||
<tr><th>자산</th><th>수량</th><th>손익</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(summary?.positions ?? []).length === 0 ? (
|
||||
<tr><td colSpan={3} className="ptd-muted">보유 없음</td></tr>
|
||||
) : summary!.positions.map(p => {
|
||||
const code = p.symbol.replace(/^KRW-/, '');
|
||||
const up = (p.profitLoss ?? 0) >= 0;
|
||||
return (
|
||||
<tr
|
||||
key={p.symbol}
|
||||
className={market === p.symbol ? 'ptd-row--sel' : ''}
|
||||
onClick={() => onMarketSelect?.(p.symbol)}
|
||||
>
|
||||
<td>{code}</td>
|
||||
<td>{p.quantity.toFixed(4)}</td>
|
||||
<td className={up ? 'up' : 'down'}>
|
||||
{p.profitLoss != null ? `${up ? '+' : ''}${fmtKrw(p.profitLoss)}` : '—'}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="ptd-item-card-section ptd-holdings-card-section">
|
||||
<div className="vtd-target-list-head">
|
||||
<span>보유 목록</span>
|
||||
<span className="vtd-target-count">{positions.length}종목</span>
|
||||
</div>
|
||||
{positions.length === 0 ? (
|
||||
<p className="ptd-muted ptd-item-card-empty">보유 종목이 없습니다.</p>
|
||||
) : (
|
||||
<div className="ptd-item-card-scroll">
|
||||
<div className="vtd-target-list ptd-item-card-list">
|
||||
{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 (
|
||||
<div
|
||||
key={p.symbol}
|
||||
className={[
|
||||
'vtd-target-item',
|
||||
'ptd-item-card',
|
||||
'ptd-holding-card',
|
||||
selected ? 'vtd-target-item--active' : '',
|
||||
].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 (
|
||||
|
||||
@@ -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<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}` : ''}`}>
|
||||
<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>
|
||||
</section>
|
||||
<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>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user