diff --git a/src/pages/listings/listingsUtils.js b/src/pages/listings/listingsUtils.js new file mode 100644 index 0000000..2d0f3e1 --- /dev/null +++ b/src/pages/listings/listingsUtils.js @@ -0,0 +1,40 @@ +/* 실매물 판정 tier / 금액 포맷 순수 헬퍼 */ +const C = { green: '#34d399', yellow: '#f59e0b', red: '#f87171', gray: '#94a3b8' }; + +export const SAFETY_TIER_META = Object.freeze({ + '안전': { emoji: '🟢', color: C.green }, + '주의': { emoji: '🟡', color: C.yellow }, + '위험': { emoji: '🔴', color: C.red }, + '보류': { emoji: '⚪', color: C.gray }, +}); + +export const VALUATION_TIER_META = Object.freeze({ + '저평가': { emoji: '🟢', color: C.green }, + '시세': { emoji: '🟡', color: C.yellow }, + '고가': { emoji: '🔴', color: C.red }, + '보류': { emoji: '⚪', color: C.gray }, +}); + +export const tierMeta = (kind, tier) => { + const map = kind === 'valuation' ? VALUATION_TIER_META : SAFETY_TIER_META; + if (Object.hasOwn(map, tier)) return { ...map[tier], label: tier }; + return { emoji: '⚪', color: C.gray, label: tier ?? '' }; +}; + +export const formatMoney = (manwon) => { + if (manwon == null || manwon === '') return '-'; + const n = Number(manwon); + if (Number.isNaN(n)) return '-'; + const eok = Math.floor(n / 10000); + const rest = n % 10000; + if (eok > 0 && rest > 0) return `${eok}억 ${rest.toLocaleString('ko-KR')}만`; + if (eok > 0) return `${eok}억`; + return `${n.toLocaleString('ko-KR')}만`; +}; + +export const formatRatio = (ratio) => { + if (ratio == null) return '-'; + const n = Number(ratio); + if (Number.isNaN(n)) return '-'; + return `${(n * 100).toFixed(1)}%`; +}; diff --git a/src/pages/listings/listingsUtils.test.js b/src/pages/listings/listingsUtils.test.js new file mode 100644 index 0000000..77d79ac --- /dev/null +++ b/src/pages/listings/listingsUtils.test.js @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest'; +import { tierMeta, formatMoney, formatRatio } from './listingsUtils.js'; + +describe('tierMeta', () => { + it('safety 4종 이모지·색·label', () => { + expect(tierMeta('safety', '안전')).toMatchObject({ emoji: '🟢', color: '#34d399', label: '안전' }); + expect(tierMeta('safety', '주의').emoji).toBe('🟡'); + expect(tierMeta('safety', '위험').color).toBe('#f87171'); + expect(tierMeta('safety', '보류').emoji).toBe('⚪'); + }); + it('valuation 4종', () => { + expect(tierMeta('valuation', '저평가')).toMatchObject({ emoji: '🟢', color: '#34d399', label: '저평가' }); + expect(tierMeta('valuation', '시세').emoji).toBe('🟡'); + expect(tierMeta('valuation', '고가').color).toBe('#f87171'); + }); + it('미정의 tier는 회색⚪ + 원문 label, undefined는 빈 label', () => { + expect(tierMeta('safety', '없는거')).toMatchObject({ emoji: '⚪', color: '#94a3b8', label: '없는거' }); + expect(tierMeta('valuation', undefined).label).toBe(''); + }); +}); + +describe('formatMoney (만원 정밀 표기)', () => { + it('만/억 경계', () => { + expect(formatMoney(9999)).toBe('9,999만'); + expect(formatMoney(10000)).toBe('1억'); + expect(formatMoney(15000)).toBe('1억 5,000만'); + expect(formatMoney(32000)).toBe('3억 2,000만'); + }); + it('null/빈값/NaN → -', () => { + expect(formatMoney(null)).toBe('-'); + expect(formatMoney('')).toBe('-'); + expect(formatMoney('abc')).toBe('-'); + }); +}); + +describe('formatRatio', () => { + it('비율 → 퍼센트 1자리', () => { + expect(formatRatio(0.812)).toBe('81.2%'); + expect(formatRatio(1.05)).toBe('105.0%'); + }); + it('null → -', () => { + expect(formatRatio(null)).toBe('-'); + }); +});