추세검색 화면 적용
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { TrendSearchResultDto } from '../../utils/trendSearchApi';
|
||||
|
||||
interface Props {
|
||||
results: TrendSearchResultDto[];
|
||||
selectedMarket: string | null;
|
||||
onSelect: (row: TrendSearchResultDto) => void;
|
||||
loading?: boolean;
|
||||
flashMarkets?: Set<string>;
|
||||
}
|
||||
|
||||
function fmtPrice(n: number): string {
|
||||
if (n >= 1_000_000) return n.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
|
||||
if (n >= 100) return n.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
|
||||
return n.toLocaleString('ko-KR', { maximumFractionDigits: 4 });
|
||||
}
|
||||
|
||||
function fmtPct(n: number): string {
|
||||
const sign = n >= 0 ? '+' : '';
|
||||
return `${sign}${n.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
function coinLabel(market: string, name?: string): string {
|
||||
const code = market.replace(/^KRW-/, '');
|
||||
return name ? `${code}/KRW` : `${code}/KRW`;
|
||||
}
|
||||
|
||||
const TrendSearchResultsGrid: React.FC<Props> = ({
|
||||
results,
|
||||
selectedMarket,
|
||||
onSelect,
|
||||
loading,
|
||||
flashMarkets,
|
||||
}) => {
|
||||
const avgMatch = useMemo(() => {
|
||||
if (!results.length) return 0;
|
||||
return Math.round(results.reduce((s, r) => s + r.matchRate, 0) / results.length);
|
||||
}, [results]);
|
||||
|
||||
return (
|
||||
<div className="tsd-results">
|
||||
<div className="tsd-results-head">
|
||||
<div>
|
||||
<h3 className="tsd-panel-title">실시간 종목 일치율 그리드</h3>
|
||||
<p className="tsd-panel-sub">Search Condition Match Rate % Sorting · 100ms 실시간 처리</p>
|
||||
</div>
|
||||
<div className="tsd-results-meta">
|
||||
<span className="tsd-meta-chip">평균 {avgMatch}%</span>
|
||||
<span className="tsd-meta-chip tsd-meta-chip--live">● LIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tsd-results-table-wrap">
|
||||
<table className="tsd-results-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>종목명</th>
|
||||
<th>현재가</th>
|
||||
<th>전일대비</th>
|
||||
<th>일치율</th>
|
||||
<th>비고</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading && results.length === 0 && (
|
||||
<tr><td colSpan={5} className="tsd-empty">스캔 중…</td></tr>
|
||||
)}
|
||||
{!loading && results.length === 0 && (
|
||||
<tr><td colSpan={5} className="tsd-empty">조건에 맞는 종목이 없습니다. 필터를 조정해 보세요.</td></tr>
|
||||
)}
|
||||
{results.map(row => {
|
||||
const up = row.changeRate >= 0;
|
||||
const active = selectedMarket === row.market;
|
||||
const flash = flashMarkets?.has(row.market);
|
||||
return (
|
||||
<tr
|
||||
key={row.market}
|
||||
className={[
|
||||
active ? 'tsd-row--sel' : '',
|
||||
flash ? (up ? 'tsd-row--flash-up' : 'tsd-row--flash-down') : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
onClick={() => onSelect(row)}
|
||||
>
|
||||
<td className="tsd-col-symbol">
|
||||
<span className="tsd-symbol">{coinLabel(row.market, row.koreanName)}</span>
|
||||
</td>
|
||||
<td className="tsd-col-price">{fmtPrice(row.currentPrice)}</td>
|
||||
<td className={`tsd-col-change${up ? ' up' : ' down'}`}>{fmtPct(row.changeRate)}</td>
|
||||
<td className="tsd-col-match">
|
||||
<div className="tsd-match-bar-wrap">
|
||||
<div className="tsd-match-bar">
|
||||
<div className="tsd-match-bar-fill" style={{ width: `${row.matchRate}%` }} />
|
||||
</div>
|
||||
<span className="tsd-match-pct">{row.matchRate}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="tsd-col-badge">
|
||||
{row.highMatch && <span className="tsd-badge-high">HIGH MATCH</span>}
|
||||
{!row.highMatch && row.matchRate >= 70 && <span className="tsd-badge-mid">MATCH</span>}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrendSearchResultsGrid;
|
||||
Reference in New Issue
Block a user