page.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import '../style.scss';
  2. import '@/app/styles/data-table.scss';
  3. import { Metadata } from 'next';
  4. import Link from 'next/link';
  5. import MarketNav from '../_components/MarketNav';
  6. import MarketFilterTabs from '../_components/MarketFilterTabs';
  7. import FuturesTable from '../_components/FuturesTable';
  8. import OptionsTable from '../_components/OptionsTable';
  9. import Pager from '../_components/Pager';
  10. import { fetchMarketFutures, fetchMarketOptions } from '@/lib/api/market';
  11. import { FuturesKindName, OptionsKindName } from '@/types/market';
  12. import { formatNumber } from '@/lib/utils/stock';
  13. import { tradeDateLabel } from '@/lib/utils/market';
  14. export const metadata: Metadata = {
  15. title: '파생 (선물·옵션) — 개미투자',
  16. description: '국내 파생상품 시세 — 선물·옵션 현재가·정산가·미결제약정·거래량 (전일 종가 기준)'
  17. };
  18. const SECTION_TABS: { value: 'futures'|'options'; label: string }[] = [
  19. { value: 'futures', label: '선물' },
  20. { value: 'options', label: '옵션' }
  21. ];
  22. const KIND_TABS: { value: FuturesKindName&OptionsKindName; label: string }[] = [
  23. { value: 'General', label: '일반' },
  24. { value: 'StockKospi', label: '주식(코스피)' },
  25. { value: 'StockKosdaq', label: '주식(코스닥)' }
  26. ];
  27. const PER_PAGE = 20;
  28. type Props = {
  29. searchParams: Promise<{
  30. section?: string;
  31. kind?: string;
  32. page?: string;
  33. }>;
  34. };
  35. export default async function MarketDerivativesPage({ searchParams }: Props)
  36. {
  37. const query = await searchParams;
  38. const section: 'futures'|'options' = SECTION_TABS.some(c => c.value === query.section) ? (query.section as 'futures'|'options') : 'futures';
  39. const kind = KIND_TABS.some(c => c.value === query.kind) ? (query.kind as FuturesKindName&OptionsKindName) : 'General';
  40. const page = Math.max(Number(query.page) || 1, 1);
  41. const futuresRes = section === 'futures' ? await fetchMarketFutures({ kind, page }) : null;
  42. const optionsRes = section === 'options' ? await fetchMarketOptions({ kind, page }) : null;
  43. const res = section === 'futures' ? futuresRes! : optionsRes!;
  44. const data = res.success ? res.data : null;
  45. const tradeDate = section === 'futures' ? futuresRes?.data?.tradeDate : optionsRes?.data?.tradeDate;
  46. return (
  47. <div className='market'>
  48. <div className='market__head'>
  49. <h1 className='market__title'>파생</h1>
  50. <span className='market__basis'>{tradeDateLabel(tradeDate)} · 선물/옵션</span>
  51. </div>
  52. <MarketNav />
  53. {/* 선물/옵션 섹션 탭 (kind 는 섹션 변경 시 초기화) */}
  54. <nav className='market__filters' aria-label='파생 구분'>
  55. <div className='market__filter-group' role='group'>
  56. <span className='market__filter-label'>구분</span>
  57. {SECTION_TABS.map(tab => (
  58. <Link
  59. key={tab.value}
  60. href={`/market/derivatives?section=${tab.value}`}
  61. className={`market__tab${section === tab.value ? ' market__tab--active' : ''}`}
  62. {...(section === tab.value ? { 'aria-current': 'page' as const } : {})}
  63. >
  64. {tab.label}
  65. </Link>
  66. ))}
  67. </div>
  68. <MarketFilterTabs
  69. label='종류'
  70. paramKey='kind'
  71. tabs={KIND_TABS}
  72. active={kind}
  73. basePath='/market/derivatives'
  74. preserve={{ section }}
  75. />
  76. </nav>
  77. {!res.success ? (
  78. <div className='market__error' role='alert'>
  79. <strong>{section === 'futures' ? '선물' : '옵션'} 목록을 불러오지 못했습니다.</strong>
  80. 잠시 후 다시 시도해 주세요.
  81. </div>
  82. ) : !data || data.list.length === 0 ? (
  83. <div className='market__empty'>표시할 {section === 'futures' ? '선물' : '옵션'}이 없습니다.</div>
  84. ) : (
  85. <>
  86. <p className='market__total'>전체 {formatNumber(data.total)}종목</p>
  87. <div className='market__table-wrap'>
  88. {section === 'futures' ? (
  89. <FuturesTable rows={futuresRes!.data!.list} />
  90. ) : (
  91. <OptionsTable rows={optionsRes!.data!.list} />
  92. )}
  93. </div>
  94. <Pager
  95. total={data.total}
  96. page={page}
  97. perPage={PER_PAGE}
  98. basePath='/market/derivatives'
  99. query={{ section, kind }}
  100. />
  101. </>
  102. )}
  103. </div>
  104. );
  105. }