매매 시그널 알림 화면 수정
This commit is contained in:
@@ -38,29 +38,28 @@ const VirtualSignalEqualizer: React.FC<Props> = ({ matchRate }) => {
|
||||
return items;
|
||||
}, [litCount, rate]);
|
||||
|
||||
const ticks = [100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 0];
|
||||
/** 신호등 열 높이에 맞춘 20% 눈금 (가상매매·알림 공통) */
|
||||
const ticks = [100, 80, 60, 40, 20, 0];
|
||||
|
||||
return (
|
||||
<div className="vtd-sig-eq">
|
||||
<div className="vtd-sig-eq-scale">
|
||||
<div className="vtd-sig-eq-scale" aria-hidden>
|
||||
{ticks.map(t => (
|
||||
<span key={t} className="vtd-sig-eq-tick">{t}%</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="vtd-sig-eq-column">
|
||||
<div className="vtd-sig-eq-bar-wrap">
|
||||
<div className="vtd-sig-eq-bar">
|
||||
{segments.map((seg, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={[
|
||||
'vtd-sig-eq-seg',
|
||||
seg.lit ? 'vtd-sig-eq-seg--lit' : '',
|
||||
seg.lit ? `vtd-sig-eq-seg--${seg.tone}` : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="vtd-sig-eq-bar-wrap">
|
||||
<div className="vtd-sig-eq-bar">
|
||||
{segments.map((seg, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={[
|
||||
'vtd-sig-eq-seg',
|
||||
seg.lit ? 'vtd-sig-eq-seg--lit' : '',
|
||||
seg.lit ? `vtd-sig-eq-seg--${seg.tone}` : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{rate >= 100 && (
|
||||
<div className="vtd-sig-eq-badge">100% MATCH</div>
|
||||
|
||||
@@ -19,6 +19,9 @@ import VirtualTargetCardFoot from './VirtualTargetCardFoot';
|
||||
|
||||
export type VirtualCardDisplayMode = 'signal' | 'chart';
|
||||
|
||||
/** default: 우측 액션 버튼 · inline-quote: 우측 현재가·등락·실시간 배지 */
|
||||
export type VirtualCardHeadVariant = 'default' | 'inline-quote';
|
||||
|
||||
interface Props {
|
||||
market: string;
|
||||
strategy: StrategyDto | undefined;
|
||||
@@ -43,6 +46,10 @@ interface Props {
|
||||
chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions;
|
||||
chartLiveReceiveHighlight?: boolean;
|
||||
ticker?: TickerData;
|
||||
/** 알림 목록 등 — 푸터 전략 셀렉트 읽기 전용 라벨 */
|
||||
readOnlyStrategyLabel?: string;
|
||||
/** 헤더 우측 레이아웃 (알림 목록 신호 슬롯: inline-quote) */
|
||||
headVariant?: VirtualCardHeadVariant;
|
||||
}
|
||||
|
||||
const VirtualTargetCard: React.FC<Props> = ({
|
||||
@@ -69,7 +76,10 @@ const VirtualTargetCard: React.FC<Props> = ({
|
||||
chartPaneSeparator,
|
||||
chartLiveReceiveHighlight = true,
|
||||
ticker,
|
||||
readOnlyStrategyLabel,
|
||||
headVariant = 'default',
|
||||
}) => {
|
||||
const inlineHeadQuote = headVariant === 'inline-quote';
|
||||
const ko = getKoreanName(market);
|
||||
const sym = market.replace(/^KRW-/, '');
|
||||
const rows = snapshot?.rows ?? [];
|
||||
@@ -101,6 +111,7 @@ const VirtualTargetCard: React.FC<Props> = ({
|
||||
highlightReceiving ? 'vtd-card--receiving' : '',
|
||||
isDetail ? 'vtd-card--detail' : 'vtd-card--summary',
|
||||
isChart ? 'vtd-card--chart-mode' : '',
|
||||
inlineHeadQuote ? 'vtd-card--head-inline-quote' : '',
|
||||
selected ? 'vtd-card--selected' : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
onClick={onSelect}
|
||||
@@ -121,51 +132,60 @@ const VirtualTargetCard: React.FC<Props> = ({
|
||||
<span className="vtd-card-sym">{sym}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="vtd-card-head-actions">
|
||||
{onEnterFocus && (
|
||||
<button
|
||||
type="button"
|
||||
className="vtd-card-focus-btn"
|
||||
onClick={e => { e.stopPropagation(); onEnterFocus(); }}
|
||||
title="전체화면 보기 (차트 + 신호)"
|
||||
aria-label="전체화면 보기"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||
<polyline points="5,1 1,1 1,5" />
|
||||
<polyline points="9,13 13,13 13,9" />
|
||||
<line x1="1" y1="1" x2="6" y2="6" />
|
||||
<line x1="13" y1="13" x2="8" y2="8" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<div className="vtd-card-mode-toggle" role="group" aria-label="이 종목 표시" onClick={e => e.stopPropagation()}>
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-card-mode-btn${displayMode === 'signal' ? ' vtd-card-mode-btn--on' : ''}`}
|
||||
onClick={() => onDisplayModeChange?.('signal')}
|
||||
title="이 종목만 신호 보기"
|
||||
>
|
||||
신호
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-card-mode-btn${displayMode === 'chart' ? ' vtd-card-mode-btn--on' : ''}`}
|
||||
onClick={() => onDisplayModeChange?.('chart')}
|
||||
title="이 종목만 차트 보기"
|
||||
>
|
||||
차트
|
||||
</button>
|
||||
{inlineHeadQuote ? (
|
||||
<div className="vtd-card-head-quote" onClick={e => e.stopPropagation()}>
|
||||
<VirtualTargetQuote market={market} ticker={ticker} compact showPriceLabel={false} />
|
||||
{running && <VirtualLiveBadge status={liveStatus} receiving={receiving} />}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="vtd-card-head-actions">
|
||||
{onEnterFocus && (
|
||||
<button
|
||||
type="button"
|
||||
className="vtd-card-focus-btn"
|
||||
onClick={e => { e.stopPropagation(); onEnterFocus(); }}
|
||||
title="전체화면 보기 (차트 + 신호)"
|
||||
aria-label="전체화면 보기"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||
<polyline points="5,1 1,1 1,5" />
|
||||
<polyline points="9,13 13,13 13,9" />
|
||||
<line x1="1" y1="1" x2="6" y2="6" />
|
||||
<line x1="13" y1="13" x2="8" y2="8" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<div className="vtd-card-mode-toggle" role="group" aria-label="이 종목 표시" onClick={e => e.stopPropagation()}>
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-card-mode-btn${displayMode === 'signal' ? ' vtd-card-mode-btn--on' : ''}`}
|
||||
onClick={() => onDisplayModeChange?.('signal')}
|
||||
title="이 종목만 신호 보기"
|
||||
>
|
||||
신호
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-card-mode-btn${displayMode === 'chart' ? ' vtd-card-mode-btn--on' : ''}`}
|
||||
onClick={() => onDisplayModeChange?.('chart')}
|
||||
title="이 종목만 차트 보기"
|
||||
>
|
||||
차트
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="vtd-card-meta">
|
||||
<VirtualTargetQuote market={market} ticker={ticker} compact />
|
||||
{!isChart && isDetail && evalCount > 0 && (
|
||||
<span className="vtd-card-met-count">{metCount}/{evalCount} 조건 충족</span>
|
||||
)}
|
||||
{running && <VirtualLiveBadge status={liveStatus} receiving={receiving} />}
|
||||
</div>
|
||||
{!inlineHeadQuote && (
|
||||
<div className="vtd-card-meta">
|
||||
<VirtualTargetQuote market={market} ticker={ticker} compact />
|
||||
{!isChart && isDetail && evalCount > 0 && (
|
||||
<span className="vtd-card-met-count">{metCount}/{evalCount} 조건 충족</span>
|
||||
)}
|
||||
{running && <VirtualLiveBadge status={liveStatus} receiving={receiving} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isChart ? (
|
||||
<VirtualTargetCardChart
|
||||
@@ -193,6 +213,7 @@ const VirtualTargetCard: React.FC<Props> = ({
|
||||
strategyId={strategyId}
|
||||
globalStrategyId={globalStrategyId}
|
||||
onStrategyChange={onStrategyChange}
|
||||
readOnlyStrategyLabel={readOnlyStrategyLabel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,8 @@ interface Props {
|
||||
strategyId: number | null;
|
||||
globalStrategyId: number | null;
|
||||
onStrategyChange?: (strategyId: number | null) => void;
|
||||
/** 변경 불가 시 셀렉트 대신 표시할 전략명 (알림 목록 등) */
|
||||
readOnlyStrategyLabel?: string;
|
||||
}
|
||||
|
||||
/** 카드 하단 — 갱신 시각(좌) · 전략 드롭다운(우) */
|
||||
@@ -23,29 +25,48 @@ const VirtualTargetCardFoot: React.FC<Props> = ({
|
||||
strategyId,
|
||||
globalStrategyId,
|
||||
onStrategyChange,
|
||||
}) => (
|
||||
<div className="vtd-card-foot">
|
||||
<span className="vtd-card-updated">갱신 {formatUpdatedTime(snapshot?.updatedAt)}</span>
|
||||
<div className="vtd-card-foot-controls" onClick={e => e.stopPropagation()}>
|
||||
<label className="vtd-card-foot-field">
|
||||
<select
|
||||
className="vtd-card-foot-select"
|
||||
aria-label="투자전략"
|
||||
value={targetStrategySelectValue({ strategyId })}
|
||||
onChange={e => {
|
||||
onStrategyChange?.(parseTargetStrategySelectValue(e.target.value));
|
||||
}}
|
||||
>
|
||||
<option value={targetStrategySelectValue({ strategyId: null })}>
|
||||
{defaultStrategyOptionLabel(globalStrategyId, strategies)}
|
||||
</option>
|
||||
{strategies.map(s => (
|
||||
<option key={s.id} value={String(s.id)}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
readOnlyStrategyLabel,
|
||||
}) => {
|
||||
const resolvedName =
|
||||
readOnlyStrategyLabel?.trim()
|
||||
|| strategies.find(s => s.id === strategyId)?.name
|
||||
|| '';
|
||||
|
||||
return (
|
||||
<div className="vtd-card-foot">
|
||||
<span className="vtd-card-updated">갱신 {formatUpdatedTime(snapshot?.updatedAt)}</span>
|
||||
<div className="vtd-card-foot-controls" onClick={e => e.stopPropagation()}>
|
||||
<label className="vtd-card-foot-field">
|
||||
{readOnlyStrategyLabel != null && !onStrategyChange ? (
|
||||
<span
|
||||
className="vtd-card-foot-select vtd-card-foot-select--readonly"
|
||||
title={resolvedName}
|
||||
>
|
||||
{resolvedName || '전략'}
|
||||
</span>
|
||||
) : (
|
||||
<select
|
||||
className="vtd-card-foot-select"
|
||||
aria-label="투자전략"
|
||||
value={targetStrategySelectValue({ strategyId })}
|
||||
disabled={!onStrategyChange}
|
||||
title={!onStrategyChange ? resolvedName : undefined}
|
||||
onChange={e => {
|
||||
onStrategyChange?.(parseTargetStrategySelectValue(e.target.value));
|
||||
}}
|
||||
>
|
||||
<option value={targetStrategySelectValue({ strategyId: null })}>
|
||||
{defaultStrategyOptionLabel(globalStrategyId, strategies)}
|
||||
</option>
|
||||
{strategies.map(s => (
|
||||
<option key={s.id} value={String(s.id)}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default VirtualTargetCardFoot;
|
||||
|
||||
@@ -27,9 +27,11 @@ interface Props {
|
||||
ticker?: TickerData;
|
||||
/** 카드 메타 행 — 현재가만 간략 표시 */
|
||||
compact?: boolean;
|
||||
/** compact 시 "현재가" 라벨 표시 (기본 true) */
|
||||
showPriceLabel?: boolean;
|
||||
}
|
||||
|
||||
const VirtualTargetQuote: React.FC<Props> = ({ market, ticker, compact = false }) => {
|
||||
const VirtualTargetQuote: React.FC<Props> = ({ market, ticker, compact = false, showPriceLabel = true }) => {
|
||||
const code = market.replace(/^KRW-/, '');
|
||||
const change: ChangeDir = ticker?.change ?? 'EVEN';
|
||||
const colorCls = change === 'RISE' ? 'vtd-target-up' : change === 'FALL' ? 'vtd-target-dn' : 'vtd-target-even';
|
||||
@@ -54,7 +56,7 @@ const VirtualTargetQuote: React.FC<Props> = ({ market, ticker, compact = false }
|
||||
if (compact) {
|
||||
return (
|
||||
<div className={`vtd-target-quote vtd-target-quote--compact ${flashClass}`.trim()} aria-label="현재가">
|
||||
<span className="vtd-card-price-label">현재가</span>
|
||||
{showPriceLabel && <span className="vtd-card-price-label">현재가</span>}
|
||||
<span className={`vtd-target-price ${colorCls}`}>
|
||||
{ticker ? fmtKrw(ticker.tradePrice) : '-'}
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useSignalVisualRailHeight } from '../../hooks/useSignalVisualRailHeight';
|
||||
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
|
||||
import {
|
||||
buildConditionMetrics,
|
||||
@@ -36,6 +37,12 @@ const VirtualTargetSignalPanel: React.FC<Props> = ({
|
||||
[metrics, snapshot?.matchRate],
|
||||
);
|
||||
const trafficState = useMemo(() => getTrafficLightState(matchRate, metrics), [matchRate, metrics]);
|
||||
const { visualRef, lightRef } = useSignalVisualRailHeight([
|
||||
rows.length,
|
||||
matchRate,
|
||||
trafficState,
|
||||
viewMode,
|
||||
]);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
@@ -58,12 +65,18 @@ const VirtualTargetSignalPanel: React.FC<Props> = ({
|
||||
>
|
||||
<div className={`vtd-sig-panel${isDetail ? ' vtd-sig-panel--detail-top' : ' vtd-sig-panel--summary'}`}>
|
||||
<div className="vtd-sig-panel-title">SIGNAL INTELLIGENCE & MATCH RATES</div>
|
||||
<div className="vtd-sig-visual">
|
||||
<div className="vtd-sig-visual-eq">
|
||||
<div className="vtd-sig-visual" ref={visualRef}>
|
||||
<div className="vtd-sig-visual-pane vtd-sig-visual-eq">
|
||||
<VirtualSignalEqualizer matchRate={matchRate} />
|
||||
</div>
|
||||
<VirtualConditionList metrics={metrics} />
|
||||
<VirtualSignalTrafficLight state={trafficState} matchRate={matchRate} />
|
||||
<div className="vtd-sig-visual-pane vtd-sig-visual-heat">
|
||||
<VirtualConditionList metrics={metrics} />
|
||||
</div>
|
||||
<div className="vtd-sig-visual-pane vtd-sig-visual-light">
|
||||
<div ref={lightRef} className="vtd-sig-light-rail">
|
||||
<VirtualSignalTrafficLight state={trafficState} matchRate={matchRate} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isDetail && (
|
||||
|
||||
Reference in New Issue
Block a user