- 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
48 lines
1.6 KiB
JavaScript
48 lines
1.6 KiB
JavaScript
/* ── 관심종목 탭 순수 헬퍼 (라벨/색/시간) ── */
|
|
|
|
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) => {
|
|
if (Object.hasOwn(KIND_META, kind)) return KIND_META[kind];
|
|
return { ...FALLBACK_KIND, label: kind ?? '' };
|
|
};
|
|
|
|
export const CONDITION_LABEL = Object.freeze({
|
|
buy_ma20_pullback: 'MA20 눌림 반등',
|
|
buy_breakout: '박스 상단 돌파',
|
|
buy_rsi_bounce: 'RSI 과매도 반등',
|
|
sell_stop_loss: '손절 라인',
|
|
sell_ma_break: '이평선 이탈',
|
|
sell_take_profit: '목표가 도달',
|
|
sell_climax: '과열 소진',
|
|
sell_trailing_stop: '트레일링 스톱',
|
|
});
|
|
|
|
export const conditionLabel = (cond) =>
|
|
Object.hasOwn(CONDITION_LABEL, cond) ? CONDITION_LABEL[cond] : (cond ?? '');
|
|
|
|
export const normalizeTicker = (str) => String(str ?? '').trim();
|
|
|
|
export const relativeTime = (iso, now = Date.now()) => {
|
|
if (!iso) return '';
|
|
const then = new Date(iso).getTime();
|
|
if (Number.isNaN(then)) return '';
|
|
const diffMs = now - then;
|
|
if (diffMs < 0) return '방금';
|
|
const sec = Math.floor(diffMs / 1000);
|
|
if (sec < 60) return '방금';
|
|
const min = Math.floor(sec / 60);
|
|
if (min < 60) return `${min}분 전`;
|
|
const hr = Math.floor(min / 60);
|
|
if (hr < 24) return `${hr}시간 전`;
|
|
const day = Math.floor(hr / 24);
|
|
if (day === 1) return '어제';
|
|
if (day < 7) return `${day}일 전`;
|
|
return new Date(iso).toLocaleDateString('ko-KR');
|
|
};
|