MarketMovers.tsx 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. import './market-movers.scss';
  2. import { moveDir, formatFlucRate } from '@/types/worldIndex';
  3. import { type MarketMoversResponse, type MarketMoverGroup, MARKET_LABEL, formatStockPrice, formatMoverCount } from '@/types/marketMover';
  4. import Sparkline from './Sparkline';
  5. // 등락 상위 종목 — 상승/하락/상한/하한 4그룹 각 상위 5. 종목명·시장·코드 + 미니 스파크라인 + 현재가·등락률.
  6. // 상승/하락은 상한·하한 제외(비중복). 등락색 --price-up/down(상승 적/하락 청).
  7. type Props = {
  8. data: MarketMoversResponse;
  9. };
  10. const GROUPS: { key: 'advances'|'declines'|'limitUp'|'limitDown'; label: string; tone: 'up'|'down' }[] = [
  11. { key: 'advances', label: '상승', tone: 'up' },
  12. { key: 'declines', label: '하락', tone: 'down' },
  13. { key: 'limitUp', label: '상한', tone: 'up' },
  14. { key: 'limitDown', label: '하한', tone: 'down' }
  15. ];
  16. const ARROW = { up: '▲', down: '▼', flat: '–' } as const;
  17. export default function MarketMovers({ data }: Props)
  18. {
  19. const hasAny = data.advances.items.length > 0
  20. || data.declines.items.length > 0
  21. || data.limitUp.items.length > 0
  22. || data.limitDown.items.length > 0;
  23. if (!hasAny) {
  24. return null;
  25. }
  26. return (
  27. <section className='mov' aria-label='등락 상위 종목'>
  28. <div className='mov__head'>
  29. <h2 className='mov__title'>등락 종목</h2>
  30. {data.tradeDate && <span className='mov__asof'>{data.tradeDate} 기준</span>}
  31. </div>
  32. <div className='mov__grid'>
  33. {GROUPS.map((g) => (
  34. <GroupPanel key={g.key} label={g.label} tone={g.tone} group={data[g.key]} />
  35. ))}
  36. </div>
  37. </section>
  38. );
  39. }
  40. function GroupPanel({ label, tone, group }: { label: string; tone: 'up'|'down'; group: MarketMoverGroup })
  41. {
  42. return (
  43. <div className='mov__panel'>
  44. <div className={`mov__panel-head mov__panel-head--${tone}`}>
  45. <span className='mov__panel-label'>{label}</span>
  46. <span className='mov__panel-count'>{formatMoverCount(group.total)}</span>
  47. </div>
  48. {group.items.length === 0 ? (
  49. <p className='mov__empty'>해당 종목 없음</p>
  50. ) : (
  51. <ul className='mov__list'>
  52. {group.items.map((item) => {
  53. const dir = moveDir(item.flucRateBp);
  54. return (
  55. <li key={item.code} className='mov__row'>
  56. <div className='mov__meta'>
  57. <span className='mov__name'>{item.name}</span>
  58. <span className='mov__sub'>{MARKET_LABEL[item.market] ?? ''} · {item.code}</span>
  59. </div>
  60. <div className='mov__spark'>
  61. <Sparkline data={item.spark} dir={dir} />
  62. </div>
  63. <div className='mov__nums'>
  64. <span className='mov__price'>{formatStockPrice(item.close)}</span>
  65. <span className={`mov__rate mov__rate--${dir}`}>
  66. <i className='mov__arrow' aria-hidden='true'>{ARROW[dir]}</i>
  67. {formatFlucRate(item.flucRateBp)}
  68. </span>
  69. </div>
  70. </li>
  71. );
  72. })}
  73. </ul>
  74. )}
  75. </div>
  76. );
  77. }