|
|
@@ -8,13 +8,13 @@ import FuturesTable from '../_components/FuturesTable';
|
|
|
import OptionsTable from '../_components/OptionsTable';
|
|
|
import Pager from '../_components/Pager';
|
|
|
import { fetchMarketFutures, fetchMarketOptions } from '@/lib/api/market';
|
|
|
-import { FuturesKindName, OptionsKindName } from '@/types/market';
|
|
|
+import { FuturesKindName, OptionsKindName, MarketSession, MarketSessionName } from '@/types/market';
|
|
|
import { formatNumber } from '@/lib/utils/stock';
|
|
|
import { tradeDateLabel } from '@/lib/utils/market';
|
|
|
|
|
|
export const metadata: Metadata = {
|
|
|
title: '파생 (선물·옵션) — 개미투자',
|
|
|
- description: '국내 파생상품 시세 — 선물·옵션 현재가·정산가·미결제약정·거래량 (전일 종가 기준)'
|
|
|
+ description: '국내 파생상품 시세 — 선물·옵션 현재가·정산가·미결제약정·거래량 (정규/야간 세션, 전일 종가 기준)'
|
|
|
};
|
|
|
|
|
|
const SECTION_TABS: { value: 'futures'|'options'; label: string }[] = [
|
|
|
@@ -28,12 +28,19 @@ const KIND_TABS: { value: FuturesKindName&OptionsKindName; label: string }[] = [
|
|
|
{ value: 'StockKosdaq', label: '주식(코스닥)' }
|
|
|
];
|
|
|
|
|
|
+// 세션 토글 (정규/야간) — API 로는 숫자 1/2 전송 (MarketSession)
|
|
|
+const SESSION_TABS: { value: MarketSessionName; label: string }[] = [
|
|
|
+ { value: 'Regular', label: '정규' },
|
|
|
+ { value: 'Night', label: '야간' }
|
|
|
+];
|
|
|
+
|
|
|
const PER_PAGE = 20;
|
|
|
|
|
|
type Props = {
|
|
|
searchParams: Promise<{
|
|
|
section?: string;
|
|
|
kind?: string;
|
|
|
+ session?: string;
|
|
|
page?: string;
|
|
|
}>;
|
|
|
};
|
|
|
@@ -43,32 +50,35 @@ export default async function MarketDerivativesPage({ searchParams }: Props)
|
|
|
const query = await searchParams;
|
|
|
const section: 'futures'|'options' = SECTION_TABS.some(c => c.value === query.section) ? (query.section as 'futures'|'options') : 'futures';
|
|
|
const kind = KIND_TABS.some(c => c.value === query.kind) ? (query.kind as FuturesKindName&OptionsKindName) : 'General';
|
|
|
+ const session: MarketSessionName = query.session === 'Night' ? 'Night' : 'Regular';
|
|
|
+ const isNight = session === 'Night';
|
|
|
const page = Math.max(Number(query.page) || 1, 1);
|
|
|
|
|
|
- const futuresRes = section === 'futures' ? await fetchMarketFutures({ kind, page }) : null;
|
|
|
- const optionsRes = section === 'options' ? await fetchMarketOptions({ kind, page }) : null;
|
|
|
+ const futuresRes = section === 'futures' ? await fetchMarketFutures({ kind, page, session: MarketSession[session] }) : null;
|
|
|
+ const optionsRes = section === 'options' ? await fetchMarketOptions({ kind, page, session: MarketSession[session] }) : null;
|
|
|
|
|
|
const res = section === 'futures' ? futuresRes! : optionsRes!;
|
|
|
const data = res.success ? res.data : null;
|
|
|
const tradeDate = section === 'futures' ? futuresRes?.data?.tradeDate : optionsRes?.data?.tradeDate;
|
|
|
+ const sectionLabel = section === 'futures' ? '선물' : '옵션';
|
|
|
|
|
|
return (
|
|
|
<div className='market'>
|
|
|
<div className='market__head'>
|
|
|
<h1 className='market__title'>파생</h1>
|
|
|
- <span className='market__basis'>{tradeDateLabel(tradeDate)} · 선물/옵션</span>
|
|
|
+ <span className='market__basis'>{tradeDateLabel(tradeDate)} · 선물/옵션{isNight ? ' · 야간 세션' : ''}</span>
|
|
|
</div>
|
|
|
|
|
|
<MarketNav />
|
|
|
|
|
|
- {/* 선물/옵션 섹션 탭 (kind 는 섹션 변경 시 초기화) */}
|
|
|
+ {/* 선물/옵션 섹션 탭 (kind 는 섹션 변경 시 초기화, 세션은 유지) */}
|
|
|
<nav className='market__filters' aria-label='파생 구분'>
|
|
|
<div className='market__filter-group' role='group'>
|
|
|
<span className='market__filter-label'>구분</span>
|
|
|
{SECTION_TABS.map(tab => (
|
|
|
<Link
|
|
|
key={tab.value}
|
|
|
- href={`/market/derivatives?section=${tab.value}`}
|
|
|
+ href={`/market/derivatives?section=${tab.value}${isNight ? '&session=Night' : ''}`}
|
|
|
className={`market__tab${section === tab.value ? ' market__tab--active' : ''}`}
|
|
|
{...(section === tab.value ? { 'aria-current': 'page' as const } : {})}
|
|
|
>
|
|
|
@@ -83,25 +93,38 @@ export default async function MarketDerivativesPage({ searchParams }: Props)
|
|
|
tabs={KIND_TABS}
|
|
|
active={kind}
|
|
|
basePath='/market/derivatives'
|
|
|
- preserve={{ section }}
|
|
|
+ preserve={isNight ? { section, session } : { section }}
|
|
|
+ />
|
|
|
+
|
|
|
+ <MarketFilterTabs
|
|
|
+ label='세션'
|
|
|
+ paramKey='session'
|
|
|
+ tabs={SESSION_TABS}
|
|
|
+ active={session}
|
|
|
+ basePath='/market/derivatives'
|
|
|
+ preserve={{ section, kind }}
|
|
|
/>
|
|
|
</nav>
|
|
|
|
|
|
{!res.success ? (
|
|
|
<div className='market__error' role='alert'>
|
|
|
- <strong>{section === 'futures' ? '선물' : '옵션'} 목록을 불러오지 못했습니다.</strong>
|
|
|
+ <strong>{sectionLabel} 목록을 불러오지 못했습니다.</strong>
|
|
|
잠시 후 다시 시도해 주세요.
|
|
|
</div>
|
|
|
) : !data || data.list.length === 0 ? (
|
|
|
- <div className='market__empty'>표시할 {section === 'futures' ? '선물' : '옵션'}이 없습니다.</div>
|
|
|
+ <div className='market__empty'>
|
|
|
+ {isNight
|
|
|
+ ? `야간 세션 ${sectionLabel} 데이터가 없습니다. 야간(글로벌) 시세 수집 전이거나 휴장일 수 있습니다.`
|
|
|
+ : `표시할 ${sectionLabel}이 없습니다.`}
|
|
|
+ </div>
|
|
|
) : (
|
|
|
<>
|
|
|
<p className='market__total'>전체 {formatNumber(data.total)}종목</p>
|
|
|
<div className='market__table-wrap'>
|
|
|
{section === 'futures' ? (
|
|
|
- <FuturesTable rows={futuresRes!.data!.list} />
|
|
|
+ <FuturesTable rows={futuresRes!.data!.list} showSession={isNight} />
|
|
|
) : (
|
|
|
- <OptionsTable rows={optionsRes!.data!.list} />
|
|
|
+ <OptionsTable rows={optionsRes!.data!.list} showSession={isNight} />
|
|
|
)}
|
|
|
</div>
|
|
|
<Pager
|
|
|
@@ -109,10 +132,10 @@ export default async function MarketDerivativesPage({ searchParams }: Props)
|
|
|
page={page}
|
|
|
perPage={PER_PAGE}
|
|
|
basePath='/market/derivatives'
|
|
|
- query={{ section, kind }}
|
|
|
+ query={isNight ? { section, kind, session } : { section, kind }}
|
|
|
/>
|
|
|
</>
|
|
|
)}
|
|
|
</div>
|
|
|
);
|
|
|
-}
|
|
|
+}
|