fix(stock): watchlist 렌더 크래시 가드·성공 시 폼 리셋·정렬 테스트

- watchlistUtils: Object.hasOwn 가드 + Object.freeze (프로토타입 키 → 함수 반환 방지)
- useWatchlist.add: boolean 반환 + 재진입 가드; 성공 시에만 폼 리셋
- byFiredAtDesc 멀티 알림 정렬 테스트 추가

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-03 02:13:59 +09:00
parent 3656ee9a59
commit 6bf36f34f0
5 changed files with 51 additions and 17 deletions

View File

@@ -1,19 +1,18 @@
/* ── 관심종목 탭 순수 헬퍼 (라벨/색/시간) ── */
export const KIND_META = {
buy: { label: '매수', color: '#22c55e', bg: 'rgba(34,197,94,0.12)' },
sell: { label: '매도', color: '#ef4444', bg: 'rgba(239,68,68,0.12)' },
};
export const KIND_META = Object.freeze({
buy: Object.freeze({ label: '매수', color: '#22c55e', bg: 'rgba(34,197,94,0.12)' }),
sell: Object.freeze({ label: '매도', color: '#ef4444', bg: 'rgba(239,68,68,0.12)' }),
});
const FALLBACK_KIND = { color: '#94a3b8', bg: 'rgba(148,163,184,0.12)' };
export const kindMeta = (kind) => {
const meta = KIND_META[kind];
if (meta) return meta;
if (Object.hasOwn(KIND_META, kind)) return KIND_META[kind];
return { ...FALLBACK_KIND, label: kind ?? '' };
};
export const CONDITION_LABEL = {
export const CONDITION_LABEL = Object.freeze({
buy_ma20_pullback: 'MA20 눌림 반등',
buy_breakout: '박스 상단 돌파',
buy_rsi_bounce: 'RSI 과매도 반등',
@@ -22,9 +21,10 @@ export const CONDITION_LABEL = {
sell_take_profit: '목표가 도달',
sell_climax: '과열 소진',
sell_trailing_stop: '트레일링 스톱',
};
});
export const conditionLabel = (cond) => CONDITION_LABEL[cond] ?? cond ?? '';
export const conditionLabel = (cond) =>
Object.hasOwn(CONDITION_LABEL, cond) ? CONDITION_LABEL[cond] : (cond ?? '');
export const normalizeTicker = (str) => String(str ?? '').trim();