From ca248891c21017eee5823a76a3ba344abd1f5e95 Mon Sep 17 00:00:00 2001 From: gahusb Date: Wed, 13 May 2026 12:26:16 +0900 Subject: [PATCH] =?UTF-8?q?feat(stock):=20=EC=8A=A4=ED=81=AC=EB=A6=AC?= =?UTF-8?q?=EB=84=88=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20+=20=EB=B9=84=EA=B5=90?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 데스크탑은 기존 테이블 유지, <768px에서는 종목별 카드로 전환: - 카드 헤더: #순위 | 종목명+코드 | 총점 - 비교 모드 ON 시: 순위Δ/점수Δ 두 줄 - 노드 칩 (가로 wrap) - 진입/손절/익절/위험 2×2 그리드 (라벨 + 원 단위) - 빠진 종목(OUT)도 카드로 회색 표시 CSS: .screener-mobile-list / .screener-mcard / .screener-result-head / .screener-warn 추가. useIsMobile 훅으로 분기. --- src/pages/stock/screener/Screener.css | 104 +++++++++ .../stock/screener/components/ResultTable.jsx | 199 ++++++++++++------ 2 files changed, 237 insertions(+), 66 deletions(-) diff --git a/src/pages/stock/screener/Screener.css b/src/pages/stock/screener/Screener.css index ea0a224..25ec012 100644 --- a/src/pages/stock/screener/Screener.css +++ b/src/pages/stock/screener/Screener.css @@ -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; +} diff --git a/src/pages/stock/screener/components/ResultTable.jsx b/src/pages/stock/screener/components/ResultTable.jsx index 94d0162..ff80c2d 100644 --- a/src/pages/stock/screener/components/ResultTable.jsx +++ b/src/pages/stock/screener/components/ResultTable.jsx @@ -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 NEW; } - const diff = prev.rank - current.rank; // 양수: 순위 상승 + const diff = prev.rank - current.rank; if (diff === 0) return ; const up = diff > 0; return ( @@ -57,13 +56,71 @@ function DeltaScore({ current, prev }) { ); } +function MobileCard({ r, prev, hasCompare }) { + return ( +
+
+
#{r.rank}
+
+
{r.name}
+
{r.ticker}
+
+
+
{r.total_score?.toFixed(1)}
+
총점
+
+
+ {hasCompare && ( +
+ 순위 + 점수 +
+ )} +
+ +
+
+
진입{r.entry_price?.toLocaleString?.()}원
+
손절{r.stop_price?.toLocaleString?.()}원
+
익절{r.target_price?.toLocaleString?.()}원
+
위험{r.r_pct?.toFixed?.(1)}%
+
+
+ ); +} + +function MobileOutCard({ r }) { + return ( +
+
+
+ OUT +
+
+
{r.name}
+
{r.ticker}
+
+
+
{r.total_score?.toFixed(1)}
+
이전
+
+
+
+ +
+
+ ); +} + export default function ResultTable({ result, compareWith, compareLabel }) { + const isMobile = useIsMobile(); + if (!result) { return (

아직 결과 없음. "지금 실행"을 눌러보세요.

- 💡 각 점수·가격 컬럼 헤더와 노드 칩에 마우스를 올리면 의미가 표시됩니다. + 💡 컬럼/칩에 마우스를 올리면 의미가 표시됩니다 (PC).

); @@ -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 (
-
+

Top {result.top_n} · 통과 {result.survivors_count} · {result.asof} {hasCompare && ( @@ -90,75 +145,87 @@ export default function ResultTable({ result, compareWith, compareLabel }) { )}

{result.warnings?.length > 0 && ( -
+
⚠ {result.warnings.join(' · ')}
)}

- 💡 컬럼 헤더와 노드 칩에 마우스를 올리면 의미가 표시됩니다. - {hasCompare && ' · 비교 모드 ON — ▲▼NEW/OUT으로 변화 표시'} + {isMobile + ? `💡 종목 카드를 위아래로 스크롤하며 확인${hasCompare ? ' · 비교 모드 ON' : ''}` + : `💡 컬럼/칩에 마우스를 올리면 의미가 표시됩니다${hasCompare ? ' · 비교 모드 ON — ▲▼NEW/OUT 변화 표시' : ''}`}

-
- - - - - - - {hasCompare && } - {hasCompare && } - - - - - - - - - {(result.results || []).map((r) => { - const prev = cmpIdx?.get(r.ticker); - return ( - - - - - {hasCompare && } - {hasCompare && } - - - - - - - ); - })} - {hasCompare && onlyInCompare.length > 0 && ( - <> - - {onlyInCompare.map((r) => ( - - + {isMobile ? ( +
+ {(result.results || []).map((r) => ( + + ))} + {hasCompare && onlyInCompare.length > 0 && ( + <> +
── 이번엔 빠진 종목 ──
+ {onlyInCompare.map((r) => )} + + )} +
+ ) : ( +
+
#종목총점순위Δ점수Δ노드진입(원)손절(원)익절(원)R%
{r.rank}{r.name}
{r.ticker}
{r.total_score?.toFixed(1)}{r.entry_price?.toLocaleString?.()}{r.stop_price?.toLocaleString?.()}{r.target_price?.toLocaleString?.()}{r.r_pct?.toFixed?.(1)}
- ── 이번엔 빠진 종목 (비교 대상에만 존재) ── -
+ + + + + + {hasCompare && } + {hasCompare && } + + + + + + + + + {(result.results || []).map((r) => { + const prev = cmpIdx?.get(r.ticker); + return ( + + - - - + + {hasCompare && } + {hasCompare && } - + + + + - ))} - - )} - -
#종목총점순위Δ점수Δ노드진입(원)손절(원)익절(원)R%
{r.rank} {r.name}
{r.ticker}
{r.total_score?.toFixed(1)}OUT{r.total_score?.toFixed(1)}{r.entry_price?.toLocaleString?.()}{r.stop_price?.toLocaleString?.()}{r.target_price?.toLocaleString?.()}{r.r_pct?.toFixed?.(1)}
-
+ ); + })} + {hasCompare && onlyInCompare.length > 0 && ( + <> + + ── 이번엔 빠진 종목 (비교 대상에만 존재) ── + + {onlyInCompare.map((r) => ( + + — + {r.name}
{r.ticker} + {r.total_score?.toFixed(1)} + OUT + — + + — + + ))} + + )} + + +
+ )}
); }