page.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  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 DisclosureList from './_components/DisclosureList';
  7. import { fetchMarketIndices, fetchMarketExchangeRates, fetchMarketInterestRates, fetchDisclosures } from '@/lib/api/market';
  8. import { MARKET_INDEX_SERIES_LABEL } from '@/types/market';
  9. import { changeDirection, changeSymbol } from '@/lib/utils/stock';
  10. import { formatDecimal, formatBpRate, formatRatePercent, tradeDateLabel } from '@/lib/utils/market';
  11. export const metadata: Metadata = {
  12. title: '시장 — 개미투자',
  13. description: '국내 증시 지수·환율·금리·최근 공시 한눈에 (KOSPI/KOSDAQ/KRX·매매기준율·국제금리)'
  14. };
  15. // 개요 미니 위젯 노출 개수
  16. const RATE_MINI_COUNT = 6;
  17. const FX_MINI_COUNT = 6;
  18. const DISCLOSURE_MINI_COUNT = 12;
  19. export default async function MarketOverviewPage()
  20. {
  21. // 지수·환율·금리·공시 병렬 조회 (전부 익명 · request-time)
  22. const [indicesRes, fxRes, ratesRes, disclosuresRes] = await Promise.all([
  23. fetchMarketIndices(),
  24. fetchMarketExchangeRates({}),
  25. fetchMarketInterestRates({ type: 'International' }),
  26. fetchDisclosures({ page: 1 })
  27. ]);
  28. const indices = indicesRes.success ? (indicesRes.data?.list ?? []) : [];
  29. const fxRows = fxRes.success ? (fxRes.data?.list ?? []) : [];
  30. const rateRows = ratesRes.success ? (ratesRes.data?.list ?? []) : [];
  31. const disclosures = disclosuresRes.success ? (disclosuresRes.data?.list ?? []) : [];
  32. return (
  33. <div className='market'>
  34. <div className='market__head'>
  35. <h1 className='market__title'>시장</h1>
  36. <span className='market__basis'>전일 종가 기준 (T+1) · 지수/환율/금리/공시</span>
  37. </div>
  38. <MarketNav />
  39. {/* 지수 요약 카드 */}
  40. <section className='market__section'>
  41. <div className='market__section-head'>
  42. <h2 className='market__section-title'>주요 지수</h2>
  43. </div>
  44. {indices.length === 0 ? (
  45. <p className='market__empty'>지수 데이터를 준비 중입니다.</p>
  46. ) : (
  47. <div className='market__index-grid'>
  48. {indices.map(row => {
  49. const direction = changeDirection(row.flucRateBp);
  50. return (
  51. <div key={`${row.series}-${row.name}`} className='market__index-card'>
  52. <span className='market__index-name'>{MARKET_INDEX_SERIES_LABEL[row.series] ?? row.name} · {row.name}</span>
  53. <span className='market__index-close'>{formatDecimal(row.close)}</span>
  54. <span className={`market__index-change market__index-change--${direction}`}>
  55. <span aria-hidden='true'>{changeSymbol(direction)}</span> {formatDecimal(row.changeVal)} ({formatBpRate(row.flucRateBp)})
  56. </span>
  57. </div>
  58. );
  59. })}
  60. </div>
  61. )}
  62. </section>
  63. {/* 환율 + 금리 미니 위젯 */}
  64. <section className='market__section'>
  65. <div className='market__widget-row'>
  66. <div>
  67. <div className='market__section-head'>
  68. <h2 className='market__section-title'>환율</h2>
  69. <span className='market__basis'>{tradeDateLabel(fxRes.data?.tradeDate)}</span>
  70. </div>
  71. {fxRows.length === 0 ? (
  72. <p className='market__empty'>환율 데이터를 준비 중입니다.</p>
  73. ) : (
  74. <div className='market__mini-list'>
  75. {fxRows.slice(0, FX_MINI_COUNT).map(row => (
  76. <div key={row.currencyUnit} className='market__mini-item'>
  77. <span className='market__mini-label'>{row.currencyName} ({row.currencyUnit})</span>
  78. <span className='market__mini-value'>{formatDecimal(row.dealBasRate)}</span>
  79. </div>
  80. ))}
  81. </div>
  82. )}
  83. </div>
  84. <div>
  85. <div className='market__section-head'>
  86. <h2 className='market__section-title'>국제 금리</h2>
  87. <span className='market__basis'>{tradeDateLabel(ratesRes.data?.tradeDate)}</span>
  88. </div>
  89. {rateRows.length === 0 ? (
  90. <p className='market__empty'>금리 데이터를 준비 중입니다.</p>
  91. ) : (
  92. <div className='market__mini-list'>
  93. {rateRows.slice(0, RATE_MINI_COUNT).map((row, index) => (
  94. <div key={`${row.itemName}-${index}`} className='market__mini-item'>
  95. <span className='market__mini-label'>{row.itemName}</span>
  96. <span className='market__mini-value'>{formatRatePercent(row.rate)}</span>
  97. </div>
  98. ))}
  99. </div>
  100. )}
  101. </div>
  102. </div>
  103. </section>
  104. {/* 최근 공시 */}
  105. <section className='market__section'>
  106. <div className='market__section-head'>
  107. <h2 className='market__section-title'>최근 공시</h2>
  108. <Link href='/disclosure' className='market__section-link'>전체 보기</Link>
  109. </div>
  110. <DisclosureList rows={disclosures.slice(0, DISCLOSURE_MINI_COUNT)} />
  111. </section>
  112. </div>
  113. );
  114. }