feat(stock): 스크리너 모바일 카드 레이아웃 + 비교 적용
데스크탑은 기존 테이블 유지, <768px에서는 종목별 카드로 전환: - 카드 헤더: #순위 | 종목명+코드 | 총점 - 비교 모드 ON 시: 순위Δ/점수Δ 두 줄 - 노드 칩 (가로 wrap) - 진입/손절/익절/위험 2×2 그리드 (라벨 + 원 단위) - 빠진 종목(OUT)도 카드로 회색 표시 CSS: .screener-mobile-list / .screener-mcard / .screener-result-head / .screener-warn 추가. useIsMobile 훅으로 분기.
This commit is contained in:
@@ -80,3 +80,107 @@
|
||||
.screener-table th { text-align: left; padding: 8px; background: #0a0f1a; color: #9ca3af; font-weight: 500; border-bottom: 1px solid #1f2937; }
|
||||
.screener-table td { padding: 8px; border-bottom: 1px solid #1a2230; vertical-align: middle; }
|
||||
.screener-table tr:hover { background: #0a0f1a; }
|
||||
|
||||
/* === 결과 표 헤더 === */
|
||||
.screener-result-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.screener-warn {
|
||||
background: #7c2d12;
|
||||
color: #fde68a;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* === 모바일 카드 layout === */
|
||||
.screener-mobile-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.screener-mcard {
|
||||
background: #0a0f1a;
|
||||
border: 1px solid #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.screener-mcard-head {
|
||||
display: grid;
|
||||
grid-template-columns: 36px 1fr auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.screener-mcard-rank {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #fbbf24;
|
||||
text-align: center;
|
||||
}
|
||||
.screener-mcard-name-main {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.screener-mcard-name-sub {
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
margin-top: 2px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.screener-mcard-score {
|
||||
text-align: right;
|
||||
}
|
||||
.screener-mcard-score-val {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
.screener-mcard-score-lbl {
|
||||
font-size: 10px;
|
||||
color: #6b7280;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.screener-mcard-delta {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
background: #0f1623;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.screener-mcard-delta span { display: flex; gap: 4px; align-items: center; }
|
||||
.screener-mcard-chips { padding: 0; }
|
||||
.screener-mcard-prices {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px 12px;
|
||||
font-size: 12px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid #1f2937;
|
||||
}
|
||||
.screener-mcard-prices > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
.screener-mcard-prices .lbl {
|
||||
color: #6b7280;
|
||||
font-size: 11px;
|
||||
}
|
||||
.screener-out-divider {
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
font-size: 11px;
|
||||
padding: 12px 0 4px;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ScoreChips from './ScoreChips';
|
||||
import { useIsMobile } from '../../../../hooks/useIsMobile';
|
||||
|
||||
const COL_TIPS = {
|
||||
rank: '순위 — 종합 점수가 높은 순서',
|
||||
@@ -25,9 +26,7 @@ function Th({ k, children }) {
|
||||
function buildCompareIndex(compareWith) {
|
||||
if (!compareWith?.results) return null;
|
||||
const idx = new Map();
|
||||
for (const r of compareWith.results) {
|
||||
idx.set(r.ticker, r);
|
||||
}
|
||||
for (const r of compareWith.results) idx.set(r.ticker, r);
|
||||
return idx;
|
||||
}
|
||||
|
||||
@@ -35,7 +34,7 @@ function DeltaRank({ current, prev }) {
|
||||
if (!prev) {
|
||||
return <span style={{ color: '#22c55e', fontSize: 11, fontWeight: 600 }}>NEW</span>;
|
||||
}
|
||||
const diff = prev.rank - current.rank; // 양수: 순위 상승
|
||||
const diff = prev.rank - current.rank;
|
||||
if (diff === 0) return <span style={{ color: '#9ca3af', fontSize: 11 }}>─</span>;
|
||||
const up = diff > 0;
|
||||
return (
|
||||
@@ -57,13 +56,71 @@ function DeltaScore({ current, prev }) {
|
||||
);
|
||||
}
|
||||
|
||||
function MobileCard({ r, prev, hasCompare }) {
|
||||
return (
|
||||
<div className="screener-mcard">
|
||||
<div className="screener-mcard-head">
|
||||
<div className="screener-mcard-rank">#{r.rank}</div>
|
||||
<div className="screener-mcard-name">
|
||||
<div className="screener-mcard-name-main">{r.name}</div>
|
||||
<div className="screener-mcard-name-sub">{r.ticker}</div>
|
||||
</div>
|
||||
<div className="screener-mcard-score">
|
||||
<div className="screener-mcard-score-val">{r.total_score?.toFixed(1)}</div>
|
||||
<div className="screener-mcard-score-lbl">총점</div>
|
||||
</div>
|
||||
</div>
|
||||
{hasCompare && (
|
||||
<div className="screener-mcard-delta">
|
||||
<span>순위 <DeltaRank current={r} prev={prev} /></span>
|
||||
<span>점수 <DeltaScore current={r} prev={prev} /></span>
|
||||
</div>
|
||||
)}
|
||||
<div className="screener-mcard-chips">
|
||||
<ScoreChips scores={r.scores} />
|
||||
</div>
|
||||
<div className="screener-mcard-prices">
|
||||
<div><span className="lbl">진입</span><span>{r.entry_price?.toLocaleString?.()}원</span></div>
|
||||
<div><span className="lbl">손절</span><span>{r.stop_price?.toLocaleString?.()}원</span></div>
|
||||
<div><span className="lbl">익절</span><span>{r.target_price?.toLocaleString?.()}원</span></div>
|
||||
<div><span className="lbl">위험</span><span>{r.r_pct?.toFixed?.(1)}%</span></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileOutCard({ r }) {
|
||||
return (
|
||||
<div className="screener-mcard" style={{ opacity: 0.55 }}>
|
||||
<div className="screener-mcard-head">
|
||||
<div className="screener-mcard-rank">
|
||||
<span style={{ color: '#ef4444', fontWeight: 600 }}>OUT</span>
|
||||
</div>
|
||||
<div className="screener-mcard-name">
|
||||
<div className="screener-mcard-name-main">{r.name}</div>
|
||||
<div className="screener-mcard-name-sub">{r.ticker}</div>
|
||||
</div>
|
||||
<div className="screener-mcard-score">
|
||||
<div className="screener-mcard-score-val">{r.total_score?.toFixed(1)}</div>
|
||||
<div className="screener-mcard-score-lbl">이전</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="screener-mcard-chips">
|
||||
<ScoreChips scores={r.scores} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ResultTable({ result, compareWith, compareLabel }) {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
if (!result) {
|
||||
return (
|
||||
<section className="screener-card">
|
||||
<p style={{ color: '#9ca3af' }}>아직 결과 없음. "지금 실행"을 눌러보세요.</p>
|
||||
<p style={{ color: '#6b7280', fontSize: 12, marginTop: 8 }}>
|
||||
💡 각 점수·가격 컬럼 헤더와 노드 칩에 마우스를 올리면 의미가 표시됩니다.
|
||||
💡 컬럼/칩에 마우스를 올리면 의미가 표시됩니다 (PC).
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
@@ -71,8 +128,6 @@ export default function ResultTable({ result, compareWith, compareLabel }) {
|
||||
|
||||
const cmpIdx = buildCompareIndex(compareWith);
|
||||
const hasCompare = !!cmpIdx;
|
||||
|
||||
// 비교 모드에서, 비교 대상에만 있는 ticker도 추가 (OUT 표시)
|
||||
const currentTickers = new Set((result.results || []).map((r) => r.ticker));
|
||||
const onlyInCompare = hasCompare
|
||||
? (compareWith.results || []).filter((r) => !currentTickers.has(r.ticker))
|
||||
@@ -80,7 +135,7 @@ export default function ResultTable({ result, compareWith, compareLabel }) {
|
||||
|
||||
return (
|
||||
<section className="screener-card">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div className="screener-result-head">
|
||||
<h3 style={{ margin: 0 }}>
|
||||
Top {result.top_n} · 통과 {result.survivors_count} · {result.asof}
|
||||
{hasCompare && (
|
||||
@@ -90,20 +145,31 @@ export default function ResultTable({ result, compareWith, compareLabel }) {
|
||||
)}
|
||||
</h3>
|
||||
{result.warnings?.length > 0 && (
|
||||
<div style={{
|
||||
background: '#7c2d12', color: '#fde68a', padding: '4px 10px',
|
||||
borderRadius: 4, fontSize: 12,
|
||||
}}>
|
||||
<div className="screener-warn">
|
||||
⚠ {result.warnings.join(' · ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p style={{ color: '#6b7280', fontSize: 11, marginTop: 8, marginBottom: 0 }}>
|
||||
💡 컬럼 헤더와 노드 칩에 마우스를 올리면 의미가 표시됩니다.
|
||||
{hasCompare && ' · 비교 모드 ON — ▲▼NEW/OUT으로 변화 표시'}
|
||||
{isMobile
|
||||
? `💡 종목 카드를 위아래로 스크롤하며 확인${hasCompare ? ' · 비교 모드 ON' : ''}`
|
||||
: `💡 컬럼/칩에 마우스를 올리면 의미가 표시됩니다${hasCompare ? ' · 비교 모드 ON — ▲▼NEW/OUT 변화 표시' : ''}`}
|
||||
</p>
|
||||
|
||||
{isMobile ? (
|
||||
<div className="screener-mobile-list">
|
||||
{(result.results || []).map((r) => (
|
||||
<MobileCard key={r.ticker} r={r} prev={cmpIdx?.get(r.ticker)} hasCompare={hasCompare} />
|
||||
))}
|
||||
{hasCompare && onlyInCompare.length > 0 && (
|
||||
<>
|
||||
<div className="screener-out-divider">── 이번엔 빠진 종목 ──</div>
|
||||
{onlyInCompare.map((r) => <MobileOutCard key={`out-${r.ticker}`} r={r} />)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ overflowX: 'auto', marginTop: 12 }}>
|
||||
<table className="screener-table">
|
||||
<thead>
|
||||
@@ -159,6 +225,7 @@ export default function ResultTable({ result, compareWith, compareLabel }) {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user