page.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import '../style.scss';
  2. import '@/app/styles/data-table.scss';
  3. import { Metadata } from 'next';
  4. import MarketNav from '../_components/MarketNav';
  5. import Sparkline from '../_components/Sparkline';
  6. import MacroTable from '../_components/MacroTable';
  7. import { fetchMacroIndicators } from '@/lib/api/macro';
  8. import { MACRO_CODES, MACRO_CODE_LABEL, MacroIndicator } from '@/types/macro';
  9. import { changeDirection, changeSymbol } from '@/lib/utils/stock';
  10. import { macroPeriodLabel, formatMacroValue } from '@/lib/utils/market';
  11. export const metadata: Metadata = {
  12. title: '거시지표 — 개미투자',
  13. description: '한국 거시경제지표 대시보드 — 고용률·실업률·취업자수·소비자물가지수·수출입액 추이 (KOSIS 수집)'
  14. };
  15. // 지표당 조회할 최근 시점 수 (월간 기준 1년)
  16. const MACRO_LIMIT = 12;
  17. // 백엔드는 코드 오름차순 반환 → 화면 고정 순서(MACRO_CODES)로 재정렬 (미정의 코드는 뒤로)
  18. function sortByCode(list: MacroIndicator[]): MacroIndicator[] {
  19. const order = new Map<string, number>(MACRO_CODES.map((code, index) => [code, index]));
  20. return [...list].sort((a, b) => (order.get(a.code) ?? MACRO_CODES.length) - (order.get(b.code) ?? MACRO_CODES.length));
  21. }
  22. export default async function MarketMacroPage()
  23. {
  24. const res = await fetchMacroIndicators({ codes: [...MACRO_CODES], limit: MACRO_LIMIT });
  25. const indicators = res.success ? sortByCode(res.data?.list ?? []) : [];
  26. return (
  27. <div className='market'>
  28. <div className='market__head'>
  29. <h1 className='market__title'>거시지표</h1>
  30. <span className='market__basis'>고용·물가·수출입 최근 추이 (KOSIS)</span>
  31. </div>
  32. <MarketNav />
  33. {!res.success ? (
  34. <div className='market__error' role='alert'>
  35. <strong>거시지표 데이터를 불러오지 못했습니다.</strong>
  36. 잠시 후 다시 시도해 주세요.
  37. </div>
  38. ) : indicators.length === 0 ? (
  39. <div className='market__empty'>표시할 거시지표 데이터가 없습니다. 수집이 시작되면 자동으로 표시됩니다.</div>
  40. ) : (
  41. <>
  42. {/* 지표별 카드 — 최신값·단위·직전 대비 증감·추이 */}
  43. <div className='market__series-grid'>
  44. {indicators.map(indicator => {
  45. const label = MACRO_CODE_LABEL[indicator.code] ?? indicator.name;
  46. const previous = indicator.points.length > 1 ? indicator.points[indicator.points.length - 2] : null;
  47. const delta = indicator.latestValue !== null && previous !== null ? indicator.latestValue - previous.value : null;
  48. const direction = changeDirection(delta);
  49. return (
  50. <div key={indicator.code} className='market__series-card'>
  51. <div className='market__series-head'>
  52. <span className='market__series-name' title={indicator.name}>{label}</span>
  53. <span className='market__cls-badge'>{macroPeriodLabel(indicator.latestPeriod)}</span>
  54. </div>
  55. <span className='market__series-latest'>
  56. {formatMacroValue(indicator.latestValue)}
  57. {indicator.unit ? <span className='market__series-unit'>{indicator.unit}</span> : null}
  58. </span>
  59. <span className={`market__index-change market__index-change--${direction}`}>
  60. {delta === null ? (
  61. '직전 대비 —'
  62. ) : (
  63. <>
  64. <span aria-hidden='true'>{changeSymbol(direction)}</span> {formatMacroValue(Math.abs(delta))} <span className='market__series-delta-caption'>직전 대비</span>
  65. </>
  66. )}
  67. </span>
  68. <Sparkline
  69. values={indicator.points.map(p => p.value)}
  70. label={`${label} 추이`}
  71. />
  72. <div className='market__series-sub'>
  73. <span>{macroPeriodLabel(indicator.points[0]?.period)} ~ {macroPeriodLabel(indicator.latestPeriod)}</span>
  74. <span>{indicator.points.length}개 시점</span>
  75. </div>
  76. </div>
  77. );
  78. })}
  79. </div>
  80. {/* 최근 N기간 매트릭스 */}
  81. <section className='market__section market__section--macro-table'>
  82. <div className='market__section-head'>
  83. <h2 className='market__section-title'>최근 추이</h2>
  84. <span className='market__basis'>지표별 최근 {MACRO_LIMIT}개 시점</span>
  85. </div>
  86. <div className='market__table-wrap'>
  87. <MacroTable indicators={indicators} maxRows={MACRO_LIMIT} />
  88. </div>
  89. </section>
  90. </>
  91. )}
  92. </div>
  93. );
  94. }