From d82550610a537be2ad734e83f9434af6abbc4e40 Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 9 Jul 2026 17:47:08 +0900 Subject: [PATCH] =?UTF-8?q?refactor(subscription):=20=EC=83=81=EC=88=98?= =?UTF-8?q?=C2=B7=EC=88=9C=EC=88=98=ED=97=AC=ED=8D=BC=EB=A5=BC=20subscript?= =?UTF-8?q?ionUtils=EB=A1=9C=20=EC=B6=94=EC=B6=9C=20+=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS --- src/pages/subscription/Subscription.jsx | 114 +----------------- src/pages/subscription/subscriptionUtils.js | 105 ++++++++++++++++ .../subscription/subscriptionUtils.test.js | 42 +++++++ 3 files changed, 151 insertions(+), 110 deletions(-) create mode 100644 src/pages/subscription/subscriptionUtils.js create mode 100644 src/pages/subscription/subscriptionUtils.test.js diff --git a/src/pages/subscription/Subscription.jsx b/src/pages/subscription/Subscription.jsx index 08630bc..9881475 100644 --- a/src/pages/subscription/Subscription.jsx +++ b/src/pages/subscription/Subscription.jsx @@ -6,116 +6,10 @@ import FAB from '../../components/FAB'; import DistrictTierEditor from './components/DistrictTierEditor'; import NotificationSettings from './components/NotificationSettings'; import './Subscription.css'; - -// ── 상수 ─────────────────────────────────────────────────────────────────────── -const STATUS_CONFIG = { - '청약예정': { color: '#00d4ff', bg: 'rgba(0,212,255,0.1)' }, - '청약중': { color: '#8b5cf6', bg: 'rgba(139,92,246,0.1)' }, - '결과발표': { color: '#f59e0b', bg: 'rgba(245,158,11,0.1)' }, - '완료': { color: '#34d399', bg: 'rgba(52,211,153,0.12)' }, -}; - -const HOUSE_TYPE_LABELS = { - '01': '국민주택', - '02': '민영주택', - '03': '도시형생활주택', -}; - -const TABS = ['대시보드', '공고 목록', '매칭 결과', '내 프로필']; - -const STATUS_FILTERS = ['전체', '청약예정', '청약중', '결과발표', '완료']; - -const DEFAULT_PROFILE = { - name: '', age: '', is_homeless: false, is_householder: false, - subscription_months: '', subscription_amount: '', - family_members: '', has_dependents: false, children_count: '', - is_newlywed: false, marriage_months: '', - has_newborn: false, is_first_home: false, income_level: '', - preferred_regions: '', preferred_types: '', - min_area: '', max_area: '', max_price: '', - // 신규 (자치구 5티어 + 알림 설정) - preferred_districts: {}, - min_match_score: 70, - notify_enabled: true, -}; - -// ── 유틸 ────────────────────────────────────────────────────────────────────── - -// 매칭 reasons에서 자치구 티어를 추출 ("자치구 S티어: 강남구 (+25)" → "S") -function extractTier(reasons) { - for (const r of reasons || []) { - const m = r.match(/자치구 ([SABCD])티어/); - if (m) return m[1]; - } - return null; -} - -const fmt = (d) => { - if (!d) return '-'; - return new Date(d).toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' }); -}; - -const fmtFull = (d) => { - if (!d) return '-'; - return new Date(d).toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' }); -}; - -const _diffDays = (d) => { - if (!d) return null; - // 로컬 타임존으로 통일하여 D-day 계산 (UTC 파싱 방지) - const [y, m, day] = d.split('-').map(Number); - const target = new Date(y, m - 1, day); - const today = new Date(); - today.setHours(0, 0, 0, 0); - return Math.round((target - today) / 86400000); -}; - -const getDDays = (d) => { - const diff = _diffDays(d); - if (diff === null) return null; - if (diff === 0) return 'D-Day'; - return diff > 0 ? `D-${diff}` : `D+${Math.abs(diff)}`; -}; - -const getDDayColor = (d) => { - const diff = _diffDays(d); - if (diff === null) return 'var(--text-dim)'; - if (diff <= 0) return '#f87171'; - if (diff <= 3) return '#f59e0b'; - if (diff <= 7) return '#00d4ff'; - return 'var(--text-dim)'; -}; - -const fmtDateTime = (d) => { - if (!d) return '-'; - return new Date(d).toLocaleString('ko-KR', { - year: 'numeric', month: '2-digit', day: '2-digit', - hour: '2-digit', minute: '2-digit', - }); -}; - -async function apiPatch(path, body) { - const opts = { - method: 'PATCH', - headers: { 'Accept': 'application/json' }, - }; - if (body !== undefined) { - opts.headers['Content-Type'] = 'application/json'; - opts.body = JSON.stringify(body); - } - const res = await fetch(path, opts); - if (!res.ok) { - const text = await res.text().catch(() => ''); - throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`); - } - return res.json(); -} - -const fmtPrice = (v) => { - if (v == null) return null; - if (v >= 10000) return `${(v / 10000).toFixed(v % 10000 === 0 ? 0 : 1)}억`; - return `${v.toLocaleString()}만`; -}; +import { + STATUS_CONFIG, HOUSE_TYPE_LABELS, TABS, STATUS_FILTERS, DEFAULT_PROFILE, + extractTier, fmt, fmtFull, getDDays, getDDayColor, fmtDateTime, apiPatch, fmtPrice, +} from './subscriptionUtils'; // ── StatusBadge ────────────────────────────────────────────────────────────── function StatusBadge({ status, size }) { diff --git a/src/pages/subscription/subscriptionUtils.js b/src/pages/subscription/subscriptionUtils.js new file mode 100644 index 0000000..efca61c --- /dev/null +++ b/src/pages/subscription/subscriptionUtils.js @@ -0,0 +1,105 @@ +/* 청약(Subscription) 모듈 상수·순수 헬퍼 */ + +export const STATUS_CONFIG = { + '청약예정': { color: '#00d4ff', bg: 'rgba(0,212,255,0.1)' }, + '청약중': { color: '#8b5cf6', bg: 'rgba(139,92,246,0.1)' }, + '결과발표': { color: '#f59e0b', bg: 'rgba(245,158,11,0.1)' }, + '완료': { color: '#34d399', bg: 'rgba(52,211,153,0.12)' }, +}; + +export const HOUSE_TYPE_LABELS = { + '01': '국민주택', + '02': '민영주택', + '03': '도시형생활주택', +}; + +export const TABS = ['대시보드', '공고 목록', '매칭 결과', '내 프로필']; + +export const STATUS_FILTERS = ['전체', '청약예정', '청약중', '결과발표', '완료']; + +export const DEFAULT_PROFILE = { + name: '', age: '', is_homeless: false, is_householder: false, + subscription_months: '', subscription_amount: '', + family_members: '', has_dependents: false, children_count: '', + is_newlywed: false, marriage_months: '', + has_newborn: false, is_first_home: false, income_level: '', + preferred_regions: '', preferred_types: '', + min_area: '', max_area: '', max_price: '', + preferred_districts: {}, + min_match_score: 70, + notify_enabled: true, +}; + +export function extractTier(reasons) { + for (const r of reasons || []) { + const m = r.match(/자치구 ([SABCD])티어/); + if (m) return m[1]; + } + return null; +} + +export const fmt = (d) => { + if (!d) return '-'; + return new Date(d).toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' }); +}; + +export const fmtFull = (d) => { + if (!d) return '-'; + return new Date(d).toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' }); +}; + +const _diffDays = (d) => { + if (!d) return null; + const [y, m, day] = d.split('-').map(Number); + const target = new Date(y, m - 1, day); + const today = new Date(); + today.setHours(0, 0, 0, 0); + return Math.round((target - today) / 86400000); +}; + +export const getDDays = (d) => { + const diff = _diffDays(d); + if (diff === null) return null; + if (diff === 0) return 'D-Day'; + return diff > 0 ? `D-${diff}` : `D+${Math.abs(diff)}`; +}; + +export const getDDayColor = (d) => { + const diff = _diffDays(d); + if (diff === null) return 'var(--text-dim)'; + if (diff <= 0) return '#f87171'; + if (diff <= 3) return '#f59e0b'; + if (diff <= 7) return '#00d4ff'; + return 'var(--text-dim)'; +}; + +export const fmtDateTime = (d) => { + if (!d) return '-'; + return new Date(d).toLocaleString('ko-KR', { + year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', + }); +}; + +export async function apiPatch(path, body) { + const opts = { + method: 'PATCH', + headers: { 'Accept': 'application/json' }, + }; + if (body !== undefined) { + opts.headers['Content-Type'] = 'application/json'; + opts.body = JSON.stringify(body); + } + const res = await fetch(path, opts); + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`); + } + return res.json(); +} + +export const fmtPrice = (v) => { + if (v == null) return null; + if (v >= 10000) return `${(v / 10000).toFixed(v % 10000 === 0 ? 0 : 1)}억`; + return `${v.toLocaleString()}만`; +}; diff --git a/src/pages/subscription/subscriptionUtils.test.js b/src/pages/subscription/subscriptionUtils.test.js new file mode 100644 index 0000000..ebfee8e --- /dev/null +++ b/src/pages/subscription/subscriptionUtils.test.js @@ -0,0 +1,42 @@ +import { describe, it, expect } from 'vitest'; +import { extractTier, getDDays, getDDayColor, fmtPrice } from './subscriptionUtils.js'; + +describe('extractTier', () => { + it('자치구 티어 문자열에서 등급 추출', () => { + expect(extractTier(['자치구 S티어: 강남구 (+25)'])).toBe('S'); + expect(extractTier(['면적 +10', '자치구 B티어: 노원구 (+15)'])).toBe('B'); + }); + it('미매치/빈 입력 → null', () => { + expect(extractTier(['면적 적합'])).toBe(null); + expect(extractTier(null)).toBe(null); + }); +}); + +describe('getDDays / getDDayColor (오늘 기준 경계)', () => { + const iso = (offsetDays) => { + const d = new Date(); d.setHours(0,0,0,0); d.setDate(d.getDate() + offsetDays); + const p = (n) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`; + }; + it('오늘=D-Day, 미래=D-N, 과거=D+N', () => { + expect(getDDays(iso(0))).toBe('D-Day'); + expect(getDDays(iso(5))).toBe('D-5'); + expect(getDDays(iso(-3))).toBe('D+3'); + expect(getDDays(null)).toBe(null); + }); + it('색 임계: 지남=빨강, ≤3=주황, ≤7=파랑, 그 외=dim', () => { + expect(getDDayColor(iso(-1))).toBe('#f87171'); + expect(getDDayColor(iso(2))).toBe('#f59e0b'); + expect(getDDayColor(iso(6))).toBe('#00d4ff'); + expect(getDDayColor(iso(30))).toBe('var(--text-dim)'); + }); +}); + +describe('fmtPrice (현 구현 동작 고정)', () => { + it('억/만 변환', () => { + expect(fmtPrice(10000)).toBe('1억'); + expect(fmtPrice(15000)).toBe('1.5억'); + expect(fmtPrice(9999)).toBe('9,999만'); + expect(fmtPrice(null)).toBe(null); + }); +});