diff --git a/src/api.js b/src/api.js index d132658..b3d361f 100644 --- a/src/api.js +++ b/src/api.js @@ -58,12 +58,16 @@ export function deleteHistory(id) { return apiDelete(`/api/history/${id}`); } -export function getStockNews(limit = 20) { - return apiGet(`/api/stock/news?limit=${limit}`); +export function getStockNews(limit = 20, category) { + const qs = new URLSearchParams({ limit: String(limit) }); + if (category) { + qs.set("category", category); + } + return apiGet(`/api/stock/news?${qs.toString()}`); } export function triggerStockScrap() { - return apiPost("/api/admin/stock/scrap"); + return apiPost("/api/stock/scrap"); } export function getStockHealth() { diff --git a/src/pages/stock/Stock.css b/src/pages/stock/Stock.css index 96761df..43b5451 100644 --- a/src/pages/stock/Stock.css +++ b/src/pages/stock/Stock.css @@ -116,7 +116,7 @@ .stock-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 18px; } @@ -174,6 +174,7 @@ .stock-snapshot { display: grid; gap: 12px; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); } .stock-snapshot__card { @@ -183,6 +184,7 @@ display: grid; gap: 6px; background: rgba(0, 0, 0, 0.2); + min-height: 94px; } .stock-snapshot__card.is-highlight { @@ -262,6 +264,28 @@ gap: 14px; } +.stock-tabs { + display: flex; + gap: 8px; + margin-bottom: 10px; +} + +.stock-tab { + border: 1px solid var(--line); + background: rgba(0, 0, 0, 0.2); + color: var(--muted); + border-radius: 999px; + padding: 6px 12px; + font-size: 12px; + cursor: pointer; + transition: border-color 0.2s ease, color 0.2s ease; +} + +.stock-tab.is-active { + border-color: rgba(255, 255, 255, 0.5); + color: var(--text); +} + .stock-news__item { border: 1px solid var(--line); border-radius: 16px; diff --git a/src/pages/stock/Stock.jsx b/src/pages/stock/Stock.jsx index 4f56584..9c65ce3 100644 --- a/src/pages/stock/Stock.jsx +++ b/src/pages/stock/Stock.jsx @@ -26,7 +26,9 @@ const getLatestBy = (items, key) => { }; const Stock = () => { - const [news, setNews] = useState([]); + const [newsDomestic, setNewsDomestic] = useState([]); + const [newsOverseas, setNewsOverseas] = useState([]); + const [newsCategory, setNewsCategory] = useState('domestic'); const [limit, setLimit] = useState(20); const [loading, setLoading] = useState(false); const [scraping, setScraping] = useState(false); @@ -43,21 +45,29 @@ const Stock = () => { const [indicesAt, setIndicesAt] = useState(''); const [autoRefreshMs] = useState(180000); + const combinedNews = useMemo( + () => [...newsDomestic, ...newsOverseas], + [newsDomestic, newsOverseas] + ); const latestCrawled = useMemo( - () => getLatestBy(news, 'crawled_at'), - [news] + () => getLatestBy(combinedNews, 'crawled_at'), + [combinedNews] ); const latestPublished = useMemo( - () => getLatestBy(news, 'pub_date'), - [news] + () => getLatestBy(combinedNews, 'pub_date'), + [combinedNews] ); const loadNews = async () => { setLoading(true); setNewsError(''); try { - const data = await getStockNews(limit); - setNews(Array.isArray(data) ? data : []); + const [domestic, overseas] = await Promise.all([ + getStockNews(limit, 'domestic'), + getStockNews(limit, 'overseas'), + ]); + setNewsDomestic(Array.isArray(domestic) ? domestic : []); + setNewsOverseas(Array.isArray(overseas) ? overseas : []); } catch (err) { setNewsError(err?.message ?? String(err)); } finally { @@ -175,7 +185,7 @@ const Stock = () => {
기사 수 - {news.length}건 + {combinedNews.length}건
{health.message ? ( @@ -223,20 +233,32 @@ const Stock = () => { ) : ( [...indices] .sort((a, b) => { - const order = ['KOSPI', 'KOSDAQ', 'KOSPI200']; - const aIdx = order.indexOf(a.name); - const bIdx = order.indexOf(b.name); - if (aIdx === -1 && bIdx === -1) return 0; - if (aIdx === -1) return 1; - if (bIdx === -1) return -1; - return aIdx - bIdx; + const typeOrder = ['domestic', 'overseas']; + const order = { + domestic: ['KOSPI', 'KOSDAQ', 'KOSPI200'], + overseas: ['NASDAQ', 'NAS', 'S&P500'], + }; + const aTypeIdx = typeOrder.indexOf(a.type); + const bTypeIdx = typeOrder.indexOf(b.type); + if (aTypeIdx !== bTypeIdx) { + return ( + (aTypeIdx === -1 ? 99 : aTypeIdx) - + (bTypeIdx === -1 ? 99 : bTypeIdx) + ); + } + const aOrder = + order[a.type]?.indexOf(a.name) ?? 999; + const bOrder = + order[b.type]?.indexOf(b.name) ?? 999; + return aOrder - bOrder; }) .map((item) => (
{
-
-
-
-

Schedule

-

스크랩 일정

-

- 매일 오전 8시에 자동 스크랩이 실행됩니다. -

-
-
-
-
- 자동 실행 - 08:00 KST -
-
- 수동 실행 - 관리자 전용 -
-
- 최근 스크랩 - {formatDate(latestCrawled)} -
-
-
-
@@ -338,7 +334,10 @@ const Stock = () => { {loading ? ( 불러오는 중 ) : null} - {news.length}건 + + 국내 {newsDomestic.length} · 해외{' '} + {newsOverseas.length} +
@@ -346,34 +345,71 @@ const Stock = () => {

뉴스를 불러오는 중...

) : newsError ? (

{newsError}

- ) : news.length === 0 ? ( + ) : combinedNews.length === 0 ? (

표시할 뉴스가 없습니다.

) : ( -
- {news.map((item) => ( - - ))} -
+
+

+ {item.title} +

+

+ {item.summary} +

+
+
+ {item.press} + {item.pub_date} + + 원문 보기 + +
+ + ))} +
+ )} + )}