diff --git a/src/pages/subscription/Subscription.css b/src/pages/subscription/Subscription.css index c095b2e..ca355dd 100644 --- a/src/pages/subscription/Subscription.css +++ b/src/pages/subscription/Subscription.css @@ -1534,3 +1534,18 @@ input.sub-toggle:checked + .sub-toggle__label { color: var(--accent-subscription grid-template-columns: 1fr; } } + +/* === 신규: 알림 대상 카운트 뱃지 ===================================== */ +.ns-pass-count { + display: inline-block; + margin-left: 8px; + padding: 1px 8px; + border-radius: 10px; + background: rgba(0, 212, 255, 0.12); + color: #00d4ff; + font-size: 11px; + font-weight: 600; +} +.ns-pass-count strong { + font-weight: 700; +} diff --git a/src/pages/subscription/Subscription.jsx b/src/pages/subscription/Subscription.jsx index 19b9844..6e764db 100644 --- a/src/pages/subscription/Subscription.jsx +++ b/src/pages/subscription/Subscription.jsx @@ -618,16 +618,52 @@ function AnnouncementDetail({ item, onBookmark }) { {item.match_score !== undefined && item.match_score !== null && (
-
-

매칭 분석

- - ⭐ {item.match_score} / 100 - +
+
+

매칭 분석

+ + ⭐ {item.match_score} / 100 + +
+ {item.score_breakdown && ( +
+

📊 점수 분석

+
+ {[ + { key: 'region', label: '지역', max: 35, color: '#00d4ff' }, + { key: 'type', label: '유형', max: 10, color: '#8b5cf6' }, + { key: 'area', label: '면적', max: 15, color: '#f59e0b' }, + { key: 'price', label: '가격', max: 15, color: '#f43f5e' }, + { key: 'eligibility', label: '자격', max: 25, color: '#34d399' }, + ].map(({ key, label, max, color }) => { + const v = item.score_breakdown[key] ?? 0; + return ( +
+
+ {label} + + {v} + / {max} + +
+
+
+
+
+ ); + })} +
+
+ )} + {item.match_reasons && item.match_reasons.length > 0 && (
-

💡 매칭 사유

+

💡 매칭 사유

    {item.match_reasons.map((r, idx) => (
  • {r}
  • @@ -973,6 +1009,24 @@ function MatchesTab() { ).join(' · ')}
)} + {match.score_breakdown && ( +
+ {[ + { key: 'region', max: 35, color: '#00d4ff' }, + { key: 'type', max: 10, color: '#8b5cf6' }, + { key: 'area', max: 15, color: '#f59e0b' }, + { key: 'price', max: 15, color: '#f43f5e' }, + { key: 'eligibility', max: 25, color: '#34d399' }, + ].map(({ key, max, color }) => { + const v = match.score_breakdown[key] ?? 0; + return ( +
+
+
+ ); + })} +
+ )}
@@ -1026,6 +1080,7 @@ function MatchesTab() { // ── ProfileTab ──────────────────────────────────────────────────────────────── function ProfileTab() { const [profile, setProfile] = useState({ ...DEFAULT_PROFILE }); + const [passCount, setPassCount] = useState(null); const [saving, setSaving] = useState(false); const [loading, setLoading] = useState(true); const [message, setMessage] = useState(''); @@ -1034,13 +1089,17 @@ function ProfileTab() { (async () => { setLoading(true); try { - const data = await apiGet('/api/realestate/profile'); + const [data, dash] = await Promise.all([ + apiGet('/api/realestate/profile'), + apiGet('/api/realestate/dashboard').catch(() => null), + ]); if (data && Object.keys(data).length > 0) { const display = { ...DEFAULT_PROFILE, ...data }; if (Array.isArray(display.preferred_regions)) display.preferred_regions = display.preferred_regions.join(', '); if (Array.isArray(display.preferred_types)) display.preferred_types = display.preferred_types.join(', '); setProfile(display); } + if (dash?.pass_count != null) setPassCount(dash.pass_count); } catch (e) { console.error('Profile load error:', e); } finally { @@ -1379,6 +1438,7 @@ function ProfileTab() { minScore={profile.min_match_score ?? 70} notifyEnabled={profile.notify_enabled ?? true} onChange={(patch) => setProfile(prev => ({ ...prev, ...patch }))} + passCount={passCount} />
diff --git a/src/pages/subscription/components/NotificationSettings.jsx b/src/pages/subscription/components/NotificationSettings.jsx index d65443e..bde5027 100644 --- a/src/pages/subscription/components/NotificationSettings.jsx +++ b/src/pages/subscription/components/NotificationSettings.jsx @@ -1,4 +1,4 @@ -export default function NotificationSettings({ minScore, notifyEnabled, onChange }) { +export default function NotificationSettings({ minScore, notifyEnabled, onChange, passCount }) { const score = minScore ?? 70; const enabled = notifyEnabled ?? true; @@ -42,9 +42,16 @@ export default function NotificationSettings({ minScore, notifyEnabled, onChange

- {enabled - ? `💡 ${score}점 이상 매치 시 텔레그램에 자동 알림합니다.` - : "⚠️ 알림 OFF — 임계값을 통과한 매칭이 있어도 메시지가 발송되지 않습니다."} + {enabled ? ( + <> + 💡 {score}점 이상 매치 시 텔레그램에 자동 알림합니다. + {passCount != null && ( + + 현재 {passCount}건 대상 + + )} + + ) : "⚠️ 알림 OFF — 임계값을 통과한 매칭이 있어도 메시지가 발송되지 않습니다."}