dashboard 형태의 UI 수정 및 고도화

This commit is contained in:
2026-03-04 08:29:39 +09:00
parent 618d5f8e6f
commit ccc9f7c634
17 changed files with 1296 additions and 224 deletions

View File

@@ -0,0 +1,106 @@
import React from 'react';
export const getFgColor = (score) => {
if (score <= 25) return '#ef4444';
if (score <= 45) return '#f97316';
if (score <= 55) return '#eab308';
if (score <= 75) return '#84cc16';
return '#22c55e';
};
export const getFgLabel = (score) => {
if (score <= 25) return '극단적 공포';
if (score <= 45) return '공포';
if (score <= 55) return '중립';
if (score <= 75) return '탐욕';
return '극단적 탐욕';
};
const FG_LEVELS = [
{
range: '0 25',
label: '극단적 공포',
color: '#ef4444',
desc: '투자자들이 극도로 불안해하는 상태. 역사적으로 매수 기회가 되기도 하나, 하락세가 이어질 수 있습니다.',
},
{
range: '26 45',
label: '공포',
color: '#f97316',
desc: '시장 심리가 위축된 상태. 불확실성이 높고, 매도 압력이 강합니다.',
},
{
range: '46 55',
label: '중립',
color: '#eab308',
desc: '공포와 탐욕이 균형을 이루는 상태. 뚜렷한 방향성 없이 관망세가 지속됩니다.',
},
{
range: '56 75',
label: '탐욕',
color: '#84cc16',
desc: '투자자들이 낙관적이고 시장에 적극 참여하는 상태. 과열 신호를 주의해야 합니다.',
},
{
range: '76 100',
label: '극단적 탐욕',
color: '#22c55e',
desc: '시장이 과열된 상태. 조정 가능성이 높아지므로 리스크 관리가 필요합니다.',
},
];
/**
* Fear & Greed 게이지 컴포넌트
* @param {{ score: number, date?: string, showLevels?: boolean }} props
*/
const FearGreedGauge = ({ score, date, showLevels = false }) => {
const color = getFgColor(score);
const label = getFgLabel(score);
return (
<div className="fg-wrap">
<div className="fg-panel">
<div className="fg-score-display">
<span className="fg-score-number" style={{ color }}>{score}</span>
<span className="fg-score-label" style={{ color }}>{label}</span>
{date && <span className="fg-score-date">{date}</span>}
</div>
<div className="fg-gauge">
<div className="fg-gauge__track">
<div
className="fg-gauge__needle"
style={{ left: `${Math.min(100, Math.max(0, score))}%` }}
/>
</div>
<div className="fg-gauge__labels">
<span>극단적 공포</span>
<span>공포</span>
<span>중립</span>
<span>탐욕</span>
<span>극단적 탐욕</span>
</div>
</div>
</div>
{showLevels && (
<div className="fg-levels">
{FG_LEVELS.map((lv) => (
<div
key={lv.label}
className={`fg-level${getFgLabel(score) === lv.label ? ' is-current' : ''}`}
>
<div className="fg-level__head">
<span className="fg-level__dot" style={{ background: lv.color }} />
<span className="fg-level__label" style={{ color: lv.color }}>{lv.label}</span>
<span className="fg-level__range">{lv.range}</span>
</div>
<p className="fg-level__desc">{lv.desc}</p>
</div>
))}
</div>
)}
</div>
);
};
export default FearGreedGauge;

View File

@@ -59,3 +59,15 @@ export const IconLab = () =>
<line x1="6.5" y1="15" x2="17.5" y2="15" />
</>
);
export const IconTodo = () =>
svg(
<>
<rect x="3" y="5" width="6" height="6" rx="1" />
<polyline points="9,8 11,10 15,6" />
<rect x="3" y="13" width="6" height="6" rx="1" />
<line x1="13" y1="16" x2="21" y2="16" />
<line x1="13" y1="8" x2="21" y2="8" />
<line x1="17" y1="12" x2="21" y2="12" />
</>
);

View File

@@ -62,12 +62,14 @@
.sidebar__brand-sub {
margin: 0;
font-size: 10px;
font-size: 9px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.18em;
letter-spacing: 0.12em;
color: var(--neon-cyan);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── 구분선 ──────────────────────────────────────────────────────────── */

View File

@@ -46,7 +46,7 @@ const Navbar = () => {
<img src={mainLogo} alt="Logo" className="sidebar__logo" />
<div className="sidebar__brand-text">
<p className="sidebar__brand-name">Jaeoh</p>
<p className="sidebar__brand-sub">Dashboard</p>
<p className="sidebar__brand-sub">MANAGEMENT ROOM</p>
</div>
</div>

View File

@@ -0,0 +1,67 @@
/* ── PageHeader ──────────────────────────────────────────────────────── */
.page-header {
padding: 0 0 20px;
margin-bottom: 4px;
}
.page-header__inner {
display: flex;
flex-direction: column;
gap: 4px;
}
.page-header__subtitle {
margin: 0;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.28em;
color: var(--page-accent, var(--neon-cyan));
font-family: var(--font-display, 'Space Grotesk', sans-serif);
display: flex;
align-items: center;
gap: 10px;
}
.page-header__subtitle::before {
content: '';
display: block;
width: 20px;
height: 1.5px;
background: var(--page-accent, var(--neon-cyan));
border-radius: 2px;
box-shadow: 0 0 6px var(--page-accent, var(--neon-cyan));
flex-shrink: 0;
}
.page-header__title {
margin: 0;
font-size: clamp(22px, 3vw, 32px);
font-weight: 800;
font-family: var(--font-display, 'Space Grotesk', sans-serif);
color: var(--text-bright, #fff);
letter-spacing: -0.03em;
line-height: 1.1;
}
.page-header__line {
height: 1px;
background: linear-gradient(
90deg,
var(--page-accent, var(--neon-cyan)) 0%,
transparent 60%
);
margin-top: 14px;
opacity: 0.3;
}
@media (max-width: 768px) {
.page-header {
padding: 0 0 16px;
}
.page-header__title {
font-size: clamp(18px, 5vw, 24px);
}
}

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { useLocation } from 'react-router-dom';
import { navLinks } from '../routes.jsx';
import './PageHeader.css';
const PageHeader = () => {
const { pathname } = useLocation();
// Home 페이지에서는 Hero 섹션이 있으므로 숨김
if (pathname === '/') return null;
// stock/trade 같은 하위 경로도 stock로 매칭
const current = navLinks.find((link) => {
if (link.path === '/') return false;
return pathname === link.path || pathname.startsWith(link.path + '/');
});
if (!current) return null;
return (
<header className="page-header" style={{ '--page-accent': current.accent }}>
<div className="page-header__inner">
<p className="page-header__subtitle">{current.subtitle}</p>
<h1 className="page-header__title">{current.label}</h1>
</div>
<div className="page-header__line" />
</header>
);
};
export default PageHeader;