diff --git a/CLAUDE.md b/CLAUDE.md index 692288a..c97fc34 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ | `/lotto` | `Lotto` | 로또 추천/통계 | | `/stock` | `Stock` | 주식 뉴스/지수 | | `/stock/trade` | `StockTrade` | 주식 트레이딩 | -| `/realestate` | `Subscription` | 청약 자격·일정 관리 | +| `/realestate` | `Subscription` | 청약 자격·일정 관리
• **프로필 탭**: 자치구 5티어 분류(드래그&드롭, PC 전용 / 모바일 read-only), 매칭 임계값 슬라이더, 텔레그램 알림 토글
• **카드/매칭 결과**: district 뱃지 + 5티어(S/A/B/C/D) 뱃지 표시
• **상세 모달**: 매칭 분석 섹션 (점수 + 사유 + 신청 자격)
• 백엔드 스펙: `web-backend/docs/superpowers/specs/2026-04-28-realestate-targeting-enhancement-design.md` | | `/realestate/property` | `RealEstate` | 관심 단지 정보 | | `/travel` | `Travel` | 여행 사진 갤러리 (Dark Room 테마) | | `/lab` | `EffectLab` | UI/UX 실험 허브 | @@ -113,7 +113,8 @@ proxy: { | 에이전트 | POST | `/api/agent-office/command`, `/api/agent-office/approve` | | 에이전트 | WS | `/api/agent-office/ws` | | 부동산 | GET | `/api/realestate/announcements`, `/api/realestate/matches` | -| 부동산 | PUT | `/api/realestate/profile` | +| 부동산 | GET | `/api/realestate/profile` — 프로필 조회 | +| 부동산 | PUT | `/api/realestate/profile` — body: `{ preferred_districts: { "S": [...], "A": [...], "B": [...], "C": [...], "D": [...] }, min_match_score: int, notify_enabled: bool, ... }` | | AI 큐레이터 | GET | `/api/lotto/briefing/latest`, `/api/lotto/curator/usage` | | 포트폴리오 | GET | `/api/profile/public` — personal 서비스 | | 포트폴리오 | POST | `/api/profile/auth` — personal 서비스 | diff --git a/src/pages/subscription/Subscription.css b/src/pages/subscription/Subscription.css index aa83260..ecb042a 100644 --- a/src/pages/subscription/Subscription.css +++ b/src/pages/subscription/Subscription.css @@ -1178,3 +1178,231 @@ white-space: nowrap; } } + +/* === 신규: 자치구 5티어 + district 뱃지 ============================== */ +.sub-chip--district { + background: #f3f4f6; + color: #374151; + border-color: #d1d5db; +} +.sub-chip--tier { + font-weight: 700; +} +.sub-chip--tier-S { background: #fee2e2; color: #dc2626; border-color: #fca5a5; } +.sub-chip--tier-A { background: #fef3c7; color: #d97706; border-color: #fcd34d; } +.sub-chip--tier-B { background: #d1fae5; color: #059669; border-color: #6ee7b7; } +.sub-chip--tier-C { background: #dbeafe; color: #2563eb; border-color: #93c5fd; } +.sub-chip--tier-D { background: #ede9fe; color: #7c3aed; border-color: #c4b5fd; } + +/* === 신규: DistrictTierEditor ====================================== */ +.dte-pool { + border: 1px dashed var(--border-soft, #e5e7eb); + border-radius: 12px; + padding: 12px; + transition: background 0.15s, border-color 0.15s; +} +.dte-pool--over { + background: #f0f9ff; + border-color: #38bdf8; +} +.dte-pool__title { + margin: 0 0 8px; + font-size: 12px; + color: var(--text-muted, #6b7280); + text-transform: uppercase; + letter-spacing: 0.05em; +} +.dte-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.dte-chip { + cursor: grab; + user-select: none; +} +.dte-chip:active { cursor: grabbing; } +.dte-chip__remove { + background: transparent; + border: 0; + color: inherit; + margin-left: 4px; + padding: 0 2px; + cursor: pointer; + font-size: 14px; + line-height: 1; + opacity: 0.6; +} +.dte-chip__remove:hover { opacity: 1; } + +.dte-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 8px; +} +.dte-zone { + border: 1px solid var(--border-soft, #e5e7eb); + border-radius: 12px; + padding: 8px; + min-height: 120px; + transition: background 0.15s, border-color 0.15s; +} +.dte-zone--over { + background: #f0f9ff; + border-color: #38bdf8; +} +.dte-zone__head { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 8px; + border-radius: 6px; + font-weight: 700; + margin-bottom: 8px; +} +.dte-zone__weight { + font-size: 11px; + font-weight: 500; + opacity: 0.8; +} +.dte-zone__chips { + display: flex; + flex-direction: column; + gap: 4px; +} + +/* 모바일 read-only 뷰 */ +.dte-row { + display: grid; + grid-template-columns: 80px 1fr; + align-items: center; + gap: 12px; + padding: 8px 0; + border-bottom: 1px solid var(--border-soft, #e5e7eb); +} +.dte-row:last-of-type { border-bottom: 0; } +.dte-row__list { + color: var(--text, #1f2937); + font-size: 14px; +} +.dte-empty { + color: var(--text-muted, #6b7280); + font-style: italic; +} +.dte-mobile-hint { + margin: 4px 0 0; + color: var(--text-muted, #6b7280); + font-size: 13px; + text-align: center; +} + +/* === 신규: NotificationSettings ==================================== */ +.ns-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} +.ns-row--column { + flex-direction: column; + align-items: stretch; +} +.ns-row__label { + font-weight: 600; + color: var(--text, #1f2937); +} +.ns-toggle { + display: inline-flex; + align-items: center; + gap: 8px; +} +.sub-toggle { + appearance: none; + width: 40px; + height: 22px; + background: #d1d5db; + border-radius: 11px; + position: relative; + cursor: pointer; + transition: background 0.2s; + margin: 0; +} +.sub-toggle::before { + content: ""; + position: absolute; + top: 2px; + left: 2px; + width: 18px; + height: 18px; + background: #fff; + border-radius: 50%; + transition: transform 0.2s; +} +.sub-toggle:checked { + background: #10b981; +} +.sub-toggle:checked::before { + transform: translateX(18px); +} +.sub-toggle__label { + font-size: 12px; + font-weight: 600; + color: var(--text-muted, #6b7280); +} +.ns-slider { + width: 100%; + margin: 8px 0; +} +.ns-slider:disabled { + opacity: 0.5; +} +.ns-scale { + display: flex; + justify-content: space-between; + font-size: 11px; + color: var(--text-muted, #6b7280); +} +.ns-hint { + margin: 0; + font-size: 13px; + color: var(--text-muted, #6b7280); + line-height: 1.5; +} + +/* === 신규: 매칭 분석 섹션 ========================================== */ +.sub-match-analysis { + display: grid; + gap: 12px; + padding: 16px; + background: var(--surface-soft, #f9fafb); + border-radius: 12px; + margin-top: 16px; +} +.sub-match-analysis__score { + font-family: var(--font-display, system-ui); + font-size: 28px; + font-weight: 700; + color: var(--accent, #3b82f6); +} +.sub-match-analysis__reasons { + margin: 0; + padding-left: 18px; + color: var(--text, #1f2937); + font-size: 14px; + line-height: 1.7; +} +.sub-match-analysis__reasons li { + margin: 2px 0; +} +.sub-match-analysis__elig { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +/* 모바일 dte-grid → 1칼럼 */ +@media (max-width: 767px) { + .dte-grid { + grid-template-columns: 1fr; + } +} diff --git a/src/pages/subscription/Subscription.jsx b/src/pages/subscription/Subscription.jsx index af6febc..19b9844 100644 --- a/src/pages/subscription/Subscription.jsx +++ b/src/pages/subscription/Subscription.jsx @@ -2,6 +2,8 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { apiGet, apiPost, apiPut, apiDelete } from '../../api'; import PullToRefresh from '../../components/PullToRefresh'; import FAB from '../../components/FAB'; +import DistrictTierEditor from './components/DistrictTierEditor'; +import NotificationSettings from './components/NotificationSettings'; import './Subscription.css'; // ── 상수 ─────────────────────────────────────────────────────────────────────── @@ -30,9 +32,23 @@ const DEFAULT_PROFILE = { 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' }); @@ -341,6 +357,17 @@ function AnnouncementCard({ item, isSelected, onClick, onBookmark }) { {item.match_score}점 )} + {item.district && ( + {item.district} + )} + {(() => { + const tier = extractTier(item.match_reasons); + return tier ? ( + + {tier}티어 + + ) : null; + })()} + + ))} + + + ))} + + + + ); +} diff --git a/src/pages/subscription/components/NotificationSettings.jsx b/src/pages/subscription/components/NotificationSettings.jsx new file mode 100644 index 0000000..d65443e --- /dev/null +++ b/src/pages/subscription/components/NotificationSettings.jsx @@ -0,0 +1,52 @@ +export default function NotificationSettings({ minScore, notifyEnabled, onChange }) { + const score = minScore ?? 70; + const enabled = notifyEnabled ?? true; + + return ( +
+
+

알림 설정

+

🔔 텔레그램 알림

+
+
+ + + + +

+ {enabled + ? `💡 ${score}점 이상 매치 시 텔레그램에 자동 알림합니다.` + : "⚠️ 알림 OFF — 임계값을 통과한 매칭이 있어도 메시지가 발송되지 않습니다."} +

+
+
+ ); +}