feat(stock): 매매알람 카드에 reason(근거 문자열) 표시

BE가 /api/stock/trade-alerts에 reason 필드 추가(사람이 읽는 조건별 근거,
텔레그램 💡 라인과 동일). AlertCard가 reason을 primary 내러티브로,
formatDetail(detail)을 secondary 숫자 근거로 표시. reason은 문자열이라
안전(detail 객체는 formatDetail로 이미 크래시 방지).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS
This commit is contained in:
2026-07-09 13:05:57 +09:00
parent d57f9b9b65
commit dec696b16e
3 changed files with 17 additions and 0 deletions

View File

@@ -3349,4 +3349,5 @@
}
.wl-cond { font-size: 13px; font-weight: 600; }
.wl-alert__price { font-size: 13px; color: var(--muted); }
.wl-alert__reason { font-size: 12.5px; line-height: 1.45; color: #cbd5e1; }
.wl-alert__detail { font-size: 12px; color: var(--muted); }

View File

@@ -24,6 +24,7 @@ const AlertCard = ({ a }) => {
<span className="wl-cond">{conditionLabel(a.condition)}</span>
{a.price != null && <span className="wl-alert__price">{formatNumber(a.price)}</span>}
</div>
{a.reason && <div className="wl-alert__reason">{a.reason}</div>}
{detailText && <div className="wl-alert__detail">{detailText}</div>}
</div>
);

View File

@@ -44,4 +44,19 @@ describe('WatchlistTab', () => {
// 객체 detail이 문자열로 안전 렌더됨 (severity 값 노출)
expect(screen.getByText(/normal/)).toBeInTheDocument();
});
it('alert.reason(근거 문자열)을 condition 라벨과 함께 렌더', () => {
const wl = {
...baseWl,
alerts: [{
id: 40, ticker: '000660', name: 'SK하이닉스', kind: 'sell', condition: 'sell_ma_break',
price: 2120000, detail: { ma50: 2123160, ma200: null, severity: 'normal' },
fired_at: '2026-07-09T02:55:00Z',
reason: '주요 이평선(MA50/200) 이탈 — 추세 훼손, 보유 재검토',
}],
};
render(<WatchlistTab wl={wl} />);
expect(screen.getByText('주요 이평선(MA50/200) 이탈 — 추세 훼손, 보유 재검토')).toBeInTheDocument();
expect(screen.getByText('이평선 이탈')).toBeInTheDocument();
});
});