feat(realestate): 청약 가점 현황 카드 + 매칭 가점 비교

- 내 프로필 탭: 가점 현황 카드 (무주택/부양가족/통장 프로그레스 바)
- 매칭 결과 탭: 상단에 내 가점 뱃지, 각 카드에 가점 표시

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 00:27:57 +09:00
parent 45b74e672a
commit 6786f8c883

View File

@@ -741,6 +741,7 @@ function AnnouncementsTab() {
function MatchesTab() { function MatchesTab() {
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [myPoints, setMyPoints] = useState(null);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -753,6 +754,7 @@ function MatchesTab() {
const data = await apiGet(`/api/realestate/matches?page=${page}&size=${size}`); const data = await apiGet(`/api/realestate/matches?page=${page}&size=${size}`);
setItems(data.items || []); setItems(data.items || []);
setTotal(data.total || 0); setTotal(data.total || 0);
if (data.my_points) setMyPoints(data.my_points);
} catch (e) { } catch (e) {
console.error('Matches load error:', e); console.error('Matches load error:', e);
setItems([]); setItems([]);
@@ -789,9 +791,20 @@ function MatchesTab() {
return ( return (
<div style={{ display: 'grid', gap: 16 }}> <div style={{ display: 'grid', gap: 16 }}>
<div className="sub-tabs-bar"> <div className="sub-tabs-bar">
<p style={{ margin: 0, fontSize: 13, color: 'var(--text-dim)' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
{total}건의 매칭 결과 <p style={{ margin: 0, fontSize: 13, color: 'var(--text-dim)' }}>
</p> {total}건의 매칭 결과
</p>
{myPoints && (
<span className="sub-badge" style={{
color: myPoints.total >= 60 ? '#34d399' : myPoints.total >= 40 ? '#f59e0b' : '#f87171',
background: myPoints.total >= 60 ? 'rgba(52,211,153,0.1)' : myPoints.total >= 40 ? 'rgba(245,158,11,0.1)' : 'rgba(248,113,113,0.1)',
fontWeight: 700, fontSize: 12,
}}>
가점 {myPoints.total}/{myPoints.max_total}
</span>
)}
</div>
<button <button
className="sub-filter-btn is-active" className="sub-filter-btn is-active"
onClick={handleRefresh} onClick={handleRefresh}
@@ -868,19 +881,31 @@ function MatchesTab() {
</div> </div>
)} )}
</div> </div>
<div style={{ textAlign: 'center', flexShrink: 0 }}> <div style={{ textAlign: 'center', flexShrink: 0, display: 'grid', gap: 6 }}>
<div style={{ <div>
fontFamily: 'var(--font-display)', <div style={{
fontSize: 28, fontFamily: 'var(--font-display)',
fontWeight: 700, fontSize: 28,
color: (match.match_score ?? 0) >= 70 ? '#34d399' : (match.match_score ?? 0) >= 40 ? '#f59e0b' : '#f87171', fontWeight: 700,
lineHeight: 1, color: (match.match_score ?? 0) >= 70 ? '#34d399' : (match.match_score ?? 0) >= 40 ? '#f59e0b' : '#f87171',
}}> lineHeight: 1,
{match.match_score ?? '-'} }}>
</div> {match.match_score ?? '-'}
<div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 4 }}> </div>
매칭 점수 <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 4 }}>
매칭 점수
</div>
</div> </div>
{myPoints && (
<div style={{
fontSize: 11, padding: '3px 8px', borderRadius: 4,
background: myPoints.total >= 50 ? 'rgba(52,211,153,0.1)' : 'rgba(248,113,113,0.1)',
color: myPoints.total >= 50 ? '#34d399' : '#f87171',
fontWeight: 600, whiteSpace: 'nowrap',
}}>
가점 {myPoints.total}
</div>
)}
</div> </div>
</div> </div>
))} ))}
@@ -985,7 +1010,53 @@ function ProfileTab() {
if (loading) return <div className="sub-empty">불러오는 ...</div>; if (loading) return <div className="sub-empty">불러오는 ...</div>;
const pts = profile.subscription_points;
return ( return (
<div style={{ display: 'grid', gap: 16 }}>
{/* 가점 카드 */}
{pts && pts.total > 0 && (
<div className="sub-panel">
<div className="sub-panel__head">
<div>
<p className="sub-panel__eyebrow">청약 가점</p>
<h3> 가점 현황</h3>
</div>
<div style={{
fontFamily: 'var(--font-display)', fontSize: 36, fontWeight: 700,
color: pts.total >= 60 ? '#34d399' : pts.total >= 40 ? '#f59e0b' : '#f87171',
lineHeight: 1,
}}>
{pts.total}<span style={{ fontSize: 16, color: 'var(--text-muted)', fontWeight: 400 }}> / {pts.max_total}</span>
</div>
</div>
<div className="sub-panel__body" style={{ display: 'grid', gap: 12 }}>
{[
{ label: '무주택기간', data: pts.homeless_duration, color: '#00d4ff' },
{ label: '부양가족 수', data: pts.dependents, color: '#8b5cf6' },
{ label: '청약통장 가입기간', data: pts.subscription_period, color: '#f59e0b' },
].map(({ label, data, color }) => (
<div key={label} style={{ display: 'grid', gap: 4 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
<span style={{ color: 'var(--text-bright)', fontWeight: 500 }}>{label}</span>
<span>
<span style={{ fontWeight: 700, color }}>{data.score}</span>
<span style={{ color: 'var(--text-dim)' }}> / {data.max}</span>
</span>
</div>
<div style={{ height: 6, borderRadius: 3, background: 'var(--surface-raised)', overflow: 'hidden' }}>
<div style={{
height: '100%', borderRadius: 3, background: color,
width: `${(data.score / data.max) * 100}%`, transition: 'width 0.3s',
}} />
</div>
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}>{data.detail}</span>
</div>
))}
</div>
</div>
)}
<div className="sub-panel"> <div className="sub-panel">
<div className="sub-panel__head"> <div className="sub-panel__head">
<div> <div>
@@ -1198,6 +1269,7 @@ function ProfileTab() {
</div> </div>
</div> </div>
</div> </div>
</div>
); );
} }