StockRow.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. 'use client';
  2. // D6 §5.1 — 종목행(StockRow) 열 구성 빌더 + StockDataTable 편의 래퍼.
  3. // 한 줄 완결: 종목명+코드 | 현재가 | 등락률(색+부호) | 거래대금 | 시가총액 | 시장.
  4. // 정렬: 종목명·현재가·등락률·거래대금·시가총액 헤더 클릭 → SSR 재조회(URL sort/order). 기본 시가총액 desc.
  5. // 거래대금/시총 등 우선순위 낮은 열은 mobile drop (data-priority='low').
  6. import Link from 'next/link';
  7. import { useRouter } from 'next/navigation';
  8. import { useCallback, useTransition } from 'react';
  9. import DataTable, { DataTableColumn } from '@/components/table/DataTable';
  10. import PriceChange from '@/components/table/PriceChange';
  11. import { StockListRow, StockSortKey, StockSortOrder } from '@/types/stock';
  12. import { formatKrwCompact, formatNumber, tradingStatusLabel } from '@/lib/utils/stock';
  13. // 활성 정렬 컬럼만 방향, 나머지 정렬 가능 컬럼은 null(비활성)
  14. function colSort(key: StockSortKey, activeSort: StockSortKey, order: StockSortOrder): StockSortOrder|null {
  15. return key === activeSort ? order : null;
  16. }
  17. export function buildStockColumns(sort: StockSortKey, order: StockSortOrder): DataTableColumn<StockListRow>[] {
  18. return [
  19. {
  20. key: 'name',
  21. header: '종목명',
  22. align: 'left',
  23. priority: 'high',
  24. sort: colSort('name', sort, order),
  25. cellClassName: 'data-table__cell--name',
  26. cell: (row) => {
  27. const status = tradingStatusLabel(row.tradingStatus);
  28. return (
  29. <Link href={`/stock/${row.code}`} className='stock-cell'>
  30. <span className='stock-cell__name'>{row.name}</span>
  31. <span className='stock-cell__code'>{row.code}</span>
  32. {status && (
  33. <span className={`stock-badge stock-badge--${row.tradingStatus.toLowerCase()}`}>{status}</span>
  34. )}
  35. </Link>
  36. );
  37. }
  38. },
  39. {
  40. key: 'price',
  41. header: <>현재가 <small>(전일종가)</small></>,
  42. align: 'right',
  43. priority: 'high',
  44. sort: colSort('price', sort, order),
  45. cell: (row) => formatNumber(row.closePrice)
  46. },
  47. {
  48. key: 'change',
  49. header: '등락률',
  50. align: 'right',
  51. priority: 'high',
  52. sort: colSort('change', sort, order),
  53. cell: (row) => <PriceChange rate={row.changeRate} />
  54. },
  55. {
  56. key: 'value',
  57. header: '거래대금',
  58. align: 'right',
  59. priority: 'low',
  60. sort: colSort('value', sort, order),
  61. cell: (row) => formatKrwCompact(row.tradingValue)
  62. },
  63. {
  64. key: 'cap',
  65. header: '시가총액',
  66. align: 'right',
  67. priority: 'low',
  68. sort: colSort('cap', sort, order),
  69. cell: (row) => formatKrwCompact(row.marketCap)
  70. },
  71. {
  72. key: 'market',
  73. header: '시장',
  74. align: 'center',
  75. priority: 'medium',
  76. cell: (row) => <span className={`stock-badge stock-badge--${row.market.toLowerCase()}`}>{row.market}</span>
  77. }
  78. ];
  79. }
  80. interface StockDataTableProps {
  81. rows: StockListRow[];
  82. sort: StockSortKey;
  83. order: StockSortOrder;
  84. // 정렬 URL 재구성 시 유지할 현재 필터
  85. market?: string;
  86. q?: string;
  87. caption?: string;
  88. emptyMessage?: string;
  89. }
  90. // 종목 목록 편의 래퍼 — 정렬 헤더 클릭 시 sort/order URL 로 SSR 재조회(필터 유지·페이지 1로 리셋).
  91. // useTransition 으로 재조회 중 현재 표 유지(스켈레톤 플래시 없이) + 흐림 표시.
  92. export default function StockDataTable({ rows, sort, order, market, q, caption, emptyMessage }: StockDataTableProps)
  93. {
  94. const router = useRouter();
  95. const [isPending, startTransition] = useTransition();
  96. const handleSort = useCallback((key: string) => {
  97. const k = key as StockSortKey;
  98. // 같은 컬럼 재클릭 → 방향 토글, 새 컬럼 → 기본 방향(종목명 asc, 숫자열 desc)
  99. const nextOrder: StockSortOrder = k === sort ? (order === 'desc' ? 'asc' : 'desc') : (k === 'name' ? 'asc' : 'desc');
  100. const params = new URLSearchParams();
  101. if (market) {
  102. params.set('market', market);
  103. }
  104. if (q) {
  105. params.set('q', q);
  106. }
  107. params.set('sort', k);
  108. params.set('order', nextOrder);
  109. startTransition(() => {
  110. router.push(`/stock?${params.toString()}`);
  111. });
  112. }, [router, sort, order, market, q]);
  113. return (
  114. <div className={isPending ? 'data-table--pending' : undefined}>
  115. <DataTable<StockListRow>
  116. caption={caption ?? '종목 목록 — 종목명, 현재가(전일 종가), 등락률, 거래대금, 시가총액, 시장'}
  117. columns={buildStockColumns(sort, order)}
  118. rows={rows}
  119. rowKey={(row) => row.code}
  120. onSort={handleSort}
  121. emptyMessage={emptyMessage}
  122. />
  123. </div>
  124. );
  125. }