feat(stock): 모바일 반응형 — 캐러셀 지표 + 스와이프 탭 + FAB
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2943,3 +2943,41 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* 필터 가로 스크롤 */
|
||||||
|
.stock-filter-row {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-filter-row > * {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 지표 카드 가로 스크롤 캐러셀 */
|
||||||
|
.stock-snapshot {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
gap: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-snapshot > * {
|
||||||
|
flex: 0 0 200px;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 뉴스 1컬럼 */
|
||||||
|
.stock-news-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 매크로 지표 1컬럼 */
|
||||||
|
.stock-macro-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { getStockIndices, getStockNews, getFearAndGreed, getVix, getTreasury10Y, getWTI, getBrent } from '../../api';
|
import { getStockIndices, getStockNews, getFearAndGreed, getVix, getTreasury10Y, getWTI, getBrent } from '../../api';
|
||||||
import Loading from '../../components/Loading';
|
import Loading from '../../components/Loading';
|
||||||
import FearGreedGauge from '../../components/FearGreedGauge';
|
import FearGreedGauge from '../../components/FearGreedGauge';
|
||||||
|
import { useIsMobile } from '../../hooks/useIsMobile';
|
||||||
|
import PullToRefresh from '../../components/PullToRefresh';
|
||||||
|
import FAB from '../../components/FAB';
|
||||||
import './Stock.css';
|
import './Stock.css';
|
||||||
|
|
||||||
const formatDate = (value) => {
|
const formatDate = (value) => {
|
||||||
@@ -109,6 +112,7 @@ const getVixLevel = (score) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Stock = () => {
|
const Stock = () => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
const [newsDomestic, setNewsDomestic] = useState([]);
|
const [newsDomestic, setNewsDomestic] = useState([]);
|
||||||
const [newsOverseas, setNewsOverseas] = useState([]);
|
const [newsOverseas, setNewsOverseas] = useState([]);
|
||||||
const [newsCategory, setNewsCategory] = useState('domestic');
|
const [newsCategory, setNewsCategory] = useState('domestic');
|
||||||
@@ -146,6 +150,10 @@ const Stock = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRefresh = useCallback(async () => {
|
||||||
|
await loadNews();
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const loadIndices = async () => {
|
const loadIndices = async () => {
|
||||||
setIndicesLoading(true);
|
setIndicesLoading(true);
|
||||||
setIndicesError('');
|
setIndicesError('');
|
||||||
@@ -217,6 +225,7 @@ const Stock = () => {
|
|||||||
newsCategory === 'domestic' ? newsDomestic : newsOverseas;
|
newsCategory === 'domestic' ? newsDomestic : newsOverseas;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<PullToRefresh onRefresh={handleRefresh}>
|
||||||
<div className="stock">
|
<div className="stock">
|
||||||
<header className="stock-header">
|
<header className="stock-header">
|
||||||
<div>
|
<div>
|
||||||
@@ -559,6 +568,13 @@ const Stock = () => {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
<FAB onClick={loadNews} label="뉴스 새로고침" icon={
|
||||||
|
<svg className="fab__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<polyline points="23 4 23 10 17 10"/>
|
||||||
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
||||||
|
</svg>
|
||||||
|
} />
|
||||||
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import './Stock.css';
|
import './Stock.css';
|
||||||
|
import { useIsMobile } from '../../hooks/useIsMobile';
|
||||||
|
import SwipeableView from '../../components/SwipeableView';
|
||||||
import {
|
import {
|
||||||
formatNumber, formatPercent,
|
formatNumber, formatPercent,
|
||||||
toNumeric, profitColorClass,
|
toNumeric, profitColorClass,
|
||||||
@@ -28,6 +30,12 @@ import SellHistoryDrawer from './components/SellHistoryDrawer';
|
|||||||
|
|
||||||
const StockTrade = () => {
|
const StockTrade = () => {
|
||||||
const [activeTab, setActiveTab] = React.useState(TAB_REPORT);
|
const [activeTab, setActiveTab] = React.useState(TAB_REPORT);
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
const TAB_ORDER = [TAB_PORTFOLIO, TAB_AI, TAB_REPORT, TAB_ADVISOR];
|
||||||
|
const tabLabels = ['포트폴리오', 'AI 트레이드', '리포트', '어드바이저'];
|
||||||
|
const tabIndex = TAB_ORDER.indexOf(activeTab);
|
||||||
|
const handleTabChange = useCallback((idx) => setActiveTab(TAB_ORDER[idx]), []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
/* ── hooks ────────────────────────────────────────────────────── */
|
/* ── hooks ────────────────────────────────────────────────────── */
|
||||||
const pf = usePortfolio();
|
const pf = usePortfolio();
|
||||||
@@ -166,7 +174,25 @@ const StockTrade = () => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Tab bar */}
|
{/* Tab bar + Tab content */}
|
||||||
|
{isMobile ? (
|
||||||
|
<SwipeableView
|
||||||
|
tabs={TAB_ORDER.map((tabId, i) => ({
|
||||||
|
key: tabId,
|
||||||
|
label: tabLabels[i],
|
||||||
|
content: tabId === TAB_PORTFOLIO
|
||||||
|
? <PortfolioTab pf={pf} asset={asset} handleSell={handleSell} handleSaveSnapshot={handleSaveSnapshot} />
|
||||||
|
: tabId === TAB_AI
|
||||||
|
? <AiTradeTab aib={aib} />
|
||||||
|
: tabId === TAB_REPORT
|
||||||
|
? <ReportTab pf={pf} report={report} ai={ai} marketCtx={marketCtx} />
|
||||||
|
: <AdvisorTab pf={pf} advisor={advisor} />,
|
||||||
|
}))}
|
||||||
|
activeIndex={tabIndex}
|
||||||
|
onTabChange={handleTabChange}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div className="stock-main-tabs">
|
<div className="stock-main-tabs">
|
||||||
{[
|
{[
|
||||||
{ id: TAB_PORTFOLIO, icon: '💼', label: '쟁승토리 계좌', badge: pf.portfolioHoldings.length || null },
|
{ id: TAB_PORTFOLIO, icon: '💼', label: '쟁승토리 계좌', badge: pf.portfolioHoldings.length || null },
|
||||||
@@ -188,13 +214,14 @@ const StockTrade = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab content */}
|
|
||||||
{activeTab === TAB_PORTFOLIO && (
|
{activeTab === TAB_PORTFOLIO && (
|
||||||
<PortfolioTab pf={pf} asset={asset} handleSell={handleSell} handleSaveSnapshot={handleSaveSnapshot} />
|
<PortfolioTab pf={pf} asset={asset} handleSell={handleSell} handleSaveSnapshot={handleSaveSnapshot} />
|
||||||
)}
|
)}
|
||||||
{activeTab === TAB_AI && <AiTradeTab aib={aib} />}
|
{activeTab === TAB_AI && <AiTradeTab aib={aib} />}
|
||||||
{activeTab === TAB_REPORT && <ReportTab pf={pf} report={report} ai={ai} marketCtx={marketCtx} />}
|
{activeTab === TAB_REPORT && <ReportTab pf={pf} report={report} ai={ai} marketCtx={marketCtx} />}
|
||||||
{activeTab === TAB_ADVISOR && <AdvisorTab pf={pf} advisor={advisor} />}
|
{activeTab === TAB_ADVISOR && <AdvisorTab pf={pf} advisor={advisor} />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Sell history drawer (always mounted) */}
|
{/* Sell history drawer (always mounted) */}
|
||||||
<SellHistoryDrawer
|
<SellHistoryDrawer
|
||||||
|
|||||||
Reference in New Issue
Block a user