| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156 |
- import './style.scss';
- import '@/app/styles/stock-common.scss';
- import { Metadata } from 'next';
- import Link from 'next/link';
- import { fetchStockList } from '@/lib/api/stock';
- import { StockMarketName } from '@/types/stock';
- import { changeDirection, changeSymbol, formatChangeRate, formatKrwCompact, formatNumber, tradingStatusLabel } from '@/lib/utils/stock';
- import Pager from './_components/Pager';
- export const metadata: Metadata = {
- title: '종목 — 개미투자',
- description: '국내 상장 전 종목 목록 — 전일 종가·등락률·시가총액 (KOSPI/KOSDAQ/KONEX)'
- };
- const MARKET_TABS: { value: ''|StockMarketName; label: string }[] = [
- { value: '', label: '전체' },
- { value: 'KOSPI', label: 'KOSPI' },
- { value: 'KOSDAQ', label: 'KOSDAQ' },
- { value: 'KONEX', label: 'KONEX' }
- ];
- const PER_PAGE = 30;
- type Props = {
- searchParams: Promise<{
- market?: string;
- q?: string;
- page?: string;
- }>;
- };
- export default async function StockListPage({ searchParams }: Props)
- {
- const query = await searchParams;
- const market = MARKET_TABS.some(c => c.value === query.market) ? (query.market as ''|StockMarketName) : '';
- const q = (query.q ?? '').trim();
- const page = Math.max(Number(query.page) || 1, 1);
- // 백엔드 미기동 시에도 페이지 자체는 렌더 — 테이블 영역만 에러 표시 (request-time fetch)
- const res = await fetchStockList({ q, market, page, perPage: PER_PAGE });
- const data = res.success ? res.data : null;
- const buildHref = (nextMarket: ''|StockMarketName) => {
- const params = new URLSearchParams();
- if (nextMarket) {
- params.set('market', nextMarket);
- }
- if (q) {
- params.set('q', q);
- }
- const qs = params.toString();
- return qs ? `/stock?${qs}` : '/stock';
- };
- return (
- <div id='stock'>
- <div className='stock__head'>
- <h1>종목</h1>
- <span className='stock__basis'>전일 종가 기준 (T+1)</span>
- </div>
- <div className='stock__controls'>
- {/* 시장 탭 */}
- <nav className='stock__tabs' aria-label='시장 구분'>
- {MARKET_TABS.map(tab => (
- <Link
- key={tab.label}
- href={buildHref(tab.value)}
- {...(market === tab.value ? { 'aria-current': 'page' as const } : {})}
- >
- {tab.label}
- </Link>
- ))}
- </nav>
- {/* 종목명/코드 검색 (GET — SSR 재조회) */}
- <form className='stock__search' action='/stock' method='get' role='search'>
- {market && <input type='hidden' name='market' value={market} />}
- <input
- type='search'
- name='q'
- defaultValue={q}
- placeholder='종목명 또는 코드'
- aria-label='종목 검색'
- autoComplete='off'
- />
- <button type='submit'>검색</button>
- </form>
- </div>
- {!res.success ? (
- <div className='stock__error' role='alert'>
- <strong>종목 목록을 불러오지 못했습니다.</strong>
- 잠시 후 다시 시도해 주세요.
- </div>
- ) : !data || data.list.length === 0 ? (
- <div className='stock__empty'>
- {q ? `'${q}' 검색 결과가 없습니다.` : '표시할 종목이 없습니다.'}
- </div>
- ) : (
- <>
- <p className='stock__total'>전체 {formatNumber(data.total)}종목</p>
- <table className='stock__table'>
- <caption>종목 목록 — 종목명, 현재가(전일 종가), 등락률, 시가총액, 시장</caption>
- <thead>
- <tr>
- <th scope='col'>종목명</th>
- <th scope='col'>현재가 <small>(전일종가)</small></th>
- <th scope='col'>등락률</th>
- <th scope='col' className='stock__col--cap'>시가총액</th>
- <th scope='col' className='stock__col--market'>시장</th>
- </tr>
- </thead>
- <tbody>
- {data.list.map(row => {
- const direction = changeDirection(row.changeRate);
- const statusLabel = tradingStatusLabel(row.tradingStatus);
- return (
- <tr key={row.code}>
- <td className='stock__name'>
- <Link href={`/stock/${row.code}`}>
- <span className='stock__name-text'>{row.name}</span>
- <span className='stock__code'>{row.code}</span>
- {statusLabel && (
- <span className={`stock-badge stock-badge--${row.tradingStatus.toLowerCase()}`}>{statusLabel}</span>
- )}
- </Link>
- </td>
- <td>{formatNumber(row.closePrice)}</td>
- <td className={`stock-change stock-change--${direction}`}>
- {changeSymbol(direction)} {formatChangeRate(row.changeRate)}
- </td>
- <td className='stock__col--cap'>{formatKrwCompact(row.marketCap)}</td>
- <td className='stock__col--market'>
- <span className={`stock-badge stock-badge--${row.market.toLowerCase()}`}>{row.market}</span>
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- <Pager
- total={data.total}
- page={page}
- perPage={PER_PAGE}
- basePath='/stock'
- query={{ ...(market ? { market } : {}), ...(q ? { q } : {}) }}
- />
- </>
- )}
- </div>
- );
- }
|