diff --git a/src/pages/subscription/Subscription.jsx b/src/pages/subscription/Subscription.jsx index f04ded3..5a02495 100644 --- a/src/pages/subscription/Subscription.jsx +++ b/src/pages/subscription/Subscription.jsx @@ -5,228 +5,30 @@ import './Subscription.css'; // ── 상수 ─────────────────────────────────────────────────────────────────────── const STATUS_CONFIG = { - '검토중': { color: '#94a3b8', bg: 'rgba(148,163,184,0.1)' }, - '신청예정': { color: '#00d4ff', bg: 'rgba(0,212,255,0.1)' }, - '신청완료': { color: '#8b5cf6', bg: 'rgba(139,92,246,0.1)' }, - '당첨': { color: '#34d399', bg: 'rgba(52,211,153,0.12)' }, - '탈락': { color: '#f87171', bg: 'rgba(248,113,113,0.1)' }, - '포기': { color: '#6b7280', bg: 'rgba(107,114,128,0.1)' }, + '청약예정': { 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 TYPE_CONFIG = { - '줍줍': { color: '#fbbf24' }, - '특공': { color: '#f43f5e' }, - '일반': { color: '#60a5fa' }, +const HOUSE_TYPE_LABELS = { + '01': '국민주택', + '02': '민영주택', + '03': '도시형생활주택', }; -const SPECIAL_TYPES = ['신혼부부', '생애최초', '다자녀', '노부모부양', '기관추천']; -const LOAN_TYPES = ['중도금대출', '잔금대출', '혼합', '자기자금']; -const SUPPLY_TYPES = ['가점', '추첨', '혼합']; -const RESIDENCE_AREAS = ['서울', '경기', '인천', '수도권', '부산', '대구', '대전', '광주', '전국']; -const SPECIAL_QUALS = ['신혼부부', '생애최초', '다자녀', '노부모부양', '기관추천']; -const TABS = ['청약 목록', '일정', '자금']; +const TABS = ['대시보드', '공고 목록', '매칭 결과', '내 프로필']; + +const STATUS_FILTERS = ['전체', '청약예정', '청약중', '결과발표', '완료']; const DEFAULT_PROFILE = { - isHouseholdHead: true, - isHomeless: true, - homelessPeriod: 60, - savingsMonths: 84, - savingsCount: 84, - dependents: 2, - residencyArea: '서울', - isMarried: true, - marriageMonths: 36, - monthlyIncome: 600, - specialQuals: ['신혼부부'], -}; - -const SAMPLE_ITEMS = [ - { - id: 1, - complexName: '힐스테이트 동탄', - address: '경기 화성시 동탄2신도시', - pyeong: '84㎡', - totalPrice: 55000, - type: '일반', - specialType: '', - supplyType: '가점', - status: '신청예정', - priority: 'high', - req_householdHead: true, - req_noHomePeriod: 24, - req_savingsCount: 24, - req_residencyArea: '수도권', - req_incomeLimit: 160, - req_dependentMin: 0, - req_notes: '수도권 2년 이상 거주 우선', - applicationStart: '2026-04-10', - applicationEnd: '2026-04-12', - winnerAnnouncement: '2026-04-17', - contractStart: '2026-05-01', - contractEnd: '2026-05-03', - interim1Date: '2026-10-01', interim1Ratio: 10, - interim2Date: '2027-04-01', interim2Ratio: 10, - interim3Date: '2027-10-01', interim3Ratio: 10, - balanceDate: '2028-06-01', - depositRatio: 10, - interimRatio: 60, - balanceRatio: 30, - loanAmount: 30000, - loanType: '중도금대출', - ownFunds: 25000, - memo: '동탄 핵심 입지, 가점으로 충분히 도전 가능.', - }, - { - id: 2, - complexName: '롯데캐슬 마곡', - address: '서울 강서구 마곡동', - pyeong: '59㎡', - totalPrice: 72000, - type: '특공', - specialType: '신혼부부', - supplyType: '추첨', - status: '신청예정', - priority: 'high', - req_householdHead: false, - req_noHomePeriod: 0, - req_savingsCount: 6, - req_residencyArea: '서울', - req_incomeLimit: 130, - req_dependentMin: 0, - req_notes: '혼인 7년 이내, 무주택 세대', - applicationStart: '2026-03-20', - applicationEnd: '2026-03-22', - winnerAnnouncement: '2026-03-27', - contractStart: '2026-04-10', - contractEnd: '2026-04-12', - interim1Date: '2026-09-15', interim1Ratio: 10, - interim2Date: '2027-03-15', interim2Ratio: 10, - interim3Date: '', interim3Ratio: 10, - balanceDate: '2027-12-01', - depositRatio: 10, - interimRatio: 60, - balanceRatio: 30, - loanAmount: 40000, - loanType: '혼합', - ownFunds: 32000, - memo: '신혼부부 특공. 소득 요건 130% 확인 필요.', - }, -]; - -const EMPTY_ITEM = { - complexName: '', address: '', pyeong: '', totalPrice: '', - type: '일반', specialType: '', supplyType: '가점', status: '검토중', priority: 'normal', - req_householdHead: true, req_noHomePeriod: 24, req_savingsCount: 24, - req_residencyArea: '수도권', req_incomeLimit: 160, req_dependentMin: 0, req_notes: '', - applicationStart: '', applicationEnd: '', winnerAnnouncement: '', - contractStart: '', contractEnd: '', - interim1Date: '', interim1Ratio: 10, - interim2Date: '', interim2Ratio: 10, - interim3Date: '', interim3Ratio: 10, - balanceDate: '', - depositRatio: 10, interimRatio: 60, balanceRatio: 30, - loanAmount: '', loanType: '중도금대출', ownFunds: '', memo: '', -}; - -// ── 가점 계산 ───────────────────────────────────────────────────────────────── -const calcHomelessScore = (isHomeless, months) => { - if (!isHomeless) return 0; - const years = Math.floor((months || 0) / 12); - return Math.min((years + 1) * 2, 32); -}; - -const calcSavingsScore = (months) => { - const m = months || 0; - if (m < 6) return 1; - if (m < 12) return 2; - return Math.min(Math.floor(m / 12) + 2, 17); -}; - -const calcDependentScore = (count) => Math.min(5 + (count || 0) * 5, 35); - -const calcTotalScore = (p) => ( - calcHomelessScore(p.isHomeless, p.homelessPeriod) + - calcSavingsScore(p.savingsMonths) + - calcDependentScore(p.dependents) -); - -// ── 요건 비교 ───────────────────────────────────────────────────────────────── -const checkRequirements = (item, profile) => { - const results = []; - - if (item.req_householdHead) { - results.push({ - label: '세대주', - required: '세대주 필요', - mine: profile.isHouseholdHead ? '세대주' : '세대원', - pass: profile.isHouseholdHead, - }); - } - - if (item.req_noHomePeriod > 0) { - const reqYr = Math.floor(item.req_noHomePeriod / 12); - const myYr = Math.floor((profile.homelessPeriod || 0) / 12); - results.push({ - label: '무주택 기간', - required: `${reqYr}년 이상`, - mine: profile.isHomeless ? `${myYr}년` : '유주택', - pass: profile.isHomeless && (profile.homelessPeriod || 0) >= item.req_noHomePeriod, - }); - } - - if (item.req_savingsCount > 0) { - results.push({ - label: '청약통장 납입', - required: `${item.req_savingsCount}회 이상`, - mine: `${profile.savingsCount || 0}회`, - pass: (profile.savingsCount || 0) >= item.req_savingsCount, - }); - } - - if (item.req_residencyArea && item.req_residencyArea !== '전국') { - const areaMap = { - '수도권': ['서울', '경기', '인천'], - '서울': ['서울'], - }; - const valid = areaMap[item.req_residencyArea] || [item.req_residencyArea]; - const pass = valid.some((a) => (profile.residencyArea || '').includes(a) || (profile.residencyArea || '') === a); - results.push({ - label: '거주 지역', - required: `${item.req_residencyArea} 거주`, - mine: profile.residencyArea || '-', - pass, - }); - } - - if (item.req_dependentMin > 0) { - results.push({ - label: '부양가족', - required: `${item.req_dependentMin}명 이상`, - mine: `${profile.dependents || 0}명`, - pass: (profile.dependents || 0) >= item.req_dependentMin, - }); - } - - if (item.type === '특공' && item.specialType) { - const hasQual = (profile.specialQuals || []).includes(item.specialType); - results.push({ - label: '특공 자격', - required: item.specialType, - mine: hasQual ? item.specialType : '해당없음', - pass: hasQual, - }); - if (item.specialType === '신혼부부') { - const reqMonths = 84; // 7년 - results.push({ - label: '혼인 기간', - required: '7년 이내', - mine: profile.marriageMonths ? `${Math.floor(profile.marriageMonths / 12)}년` : '-', - pass: (profile.marriageMonths || 0) <= reqMonths, - }); - } - } - - return results; + 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: '', }; // ── 유틸 ────────────────────────────────────────────────────────────────────── @@ -240,8 +42,6 @@ const fmtFull = (d) => { return new Date(d).toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' }); }; -const fmtPrice = (v) => v ? `${Number(v).toLocaleString()}만원` : '-'; - const getDDays = (d) => { if (!d) return null; const diff = Math.ceil((new Date(d) - new Date().setHours(0, 0, 0, 0)) / 86400000); @@ -249,976 +49,1005 @@ const getDDays = (d) => { return diff > 0 ? `D-${diff}` : `D+${Math.abs(diff)}`; }; -// ── 내 조건 카드 ─────────────────────────────────────────────────────────────── -const ProfileCard = ({ profile, onEdit }) => { - const total = calcTotalScore(profile); - const hScore = calcHomelessScore(profile.isHomeless, profile.homelessPeriod); - const sScore = calcSavingsScore(profile.savingsMonths); - const dScore = calcDependentScore(profile.dependents); - - const ScoreBar = ({ value, max, color }) => ( -
-
-
- ); - - return ( -
-
-
-

내 청약 조건

- -
-
- - {profile.isHouseholdHead ? '세대주' : '세대원'} - - - {profile.isHomeless ? `무주택 ${Math.floor((profile.homelessPeriod || 0) / 12)}년` : '유주택'} - - {profile.residencyArea} - 통장 {Math.floor((profile.savingsMonths || 0) / 12)}년 - 부양 {profile.dependents}명 - {(profile.specialQuals || []).map((q) => ( - {q} - ))} -
-
-
-
-
{total}
-
가점 / 84점
-
-
-
- 무주택 - - {hScore}/32 -
-
- 부양가족 - - {dScore}/35 -
-
- 통장기간 - - {sScore}/17 -
-
-
-
- ); +const getDDayColor = (d) => { + if (!d) return 'var(--text-dim)'; + const diff = Math.ceil((new Date(d) - new Date().setHours(0, 0, 0, 0)) / 86400000); + if (diff <= 0) return '#f87171'; + if (diff <= 3) return '#f59e0b'; + if (diff <= 7) return '#00d4ff'; + return 'var(--text-dim)'; }; -// ── 청약 카드 ───────────────────────────────────────────────────────────────── -const SubCard = ({ item, isSelected, onClick, profile }) => { - const scfg = STATUS_CONFIG[item.status] || STATUS_CONFIG['검토중']; - const tcfg = TYPE_CONFIG[item.type] || TYPE_CONFIG['일반']; - const checks = checkRequirements(item, profile); - const passCount = checks.filter((c) => c.pass).length; - const allPass = checks.length > 0 && passCount === checks.length; - const dday = getDDays(item.applicationStart); - - return ( -
-
- - {item.status} - -
- - {item.type} - - {item.specialType && {item.specialType}} -
-
-

{item.complexName}

-

{item.address}

-
- {item.pyeong || '-'} - · - {fmtPrice(item.totalPrice)} -
-
- {checks.length > 0 && ( - - {allPass ? `✓ 요건 충족 (${passCount}/${checks.length})` : `△ ${passCount}/${checks.length} 충족`} - - )} - {dday && {dday}} -
-
- ); +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', + }); }; -// ── 요건 비교표 ──────────────────────────────────────────────────────────────── -const RequirementComparison = ({ item, profile }) => { - const checks = checkRequirements(item, profile); - if (checks.length === 0) { - return

요건 정보가 입력되지 않았습니다.

; +async function apiPatch(path) { + const res = await fetch(path, { + method: 'PATCH', + headers: { 'Accept': 'application/json' }, + }); + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`); } + return res.json(); +} + +// ── StatusBadge ────────────────────────────────────────────────────────────── +function StatusBadge({ status, size }) { + const cfg = STATUS_CONFIG[status] || { color: '#94a3b8', bg: 'rgba(148,163,184,0.1)' }; + const cls = size === 'lg' ? 'sub-badge sub-badge--lg' : 'sub-badge'; return ( -
- {checks.map((c) => ( -
- {c.label} - {c.required} - {c.mine} - {c.pass ? '✓' : '✗'} -
- ))} - {item.req_notes && ( -

📌 {item.req_notes}

- )} -
+ + {status || '알 수 없음'} + ); -}; +} -// ── 일정 섹션 (상세 패널) ───────────────────────────────────────────────────── -const ScheduleSection = ({ item }) => { - const events = [ - { label: '청약 시작', date: item.applicationStart, type: 'start' }, - { label: '청약 마감', date: item.applicationEnd, type: 'mid' }, - { label: '당첨 발표', date: item.winnerAnnouncement, type: 'winner' }, - { label: '계약 시작', date: item.contractStart, type: 'contract' }, - { label: '계약 마감', date: item.contractEnd, type: 'mid' }, - item.interim1Date && { label: `중도금 1차 (${item.interim1Ratio}%)`, date: item.interim1Date, type: 'interim' }, - item.interim2Date && { label: `중도금 2차 (${item.interim2Ratio}%)`, date: item.interim2Date, type: 'interim' }, - item.interim3Date && { label: `중도금 3차 (${item.interim3Ratio}%)`, date: item.interim3Date, type: 'interim' }, - { label: '잔금 납부', date: item.balanceDate, type: 'balance' }, - ].filter(Boolean).filter((e) => e.date); +// ── DashboardTab ───────────────────────────────────────────────────────────── +function DashboardTab() { + const [dashboard, setDashboard] = useState(null); + const [collectStatus, setCollectStatus] = useState(null); + const [totalCount, setTotalCount] = useState(0); + const [collecting, setCollecting] = useState(false); + const [loading, setLoading] = useState(true); - const typeColor = { - start: '#00d4ff', mid: '#94a3b8', winner: '#34d399', - contract: '#8b5cf6', interim: '#f59e0b', balance: '#f43f5e', + const load = async () => { + setLoading(true); + try { + const [dash, status, ann] = await Promise.all([ + apiGet('/api/realestate/dashboard'), + apiGet('/api/realestate/collect/status').catch(() => null), + apiGet('/api/realestate/announcements?page=1&size=1'), + ]); + setDashboard(dash); + setCollectStatus(status); + setTotalCount(ann?.total || 0); + } catch (e) { + console.error('Dashboard load error:', e); + } finally { + setLoading(false); + } }; + useEffect(() => { load(); }, []); + + const handleCollect = async () => { + setCollecting(true); + try { + await apiPost('/api/realestate/collect'); + // Wait a moment then refresh status + setTimeout(async () => { + try { + const status = await apiGet('/api/realestate/collect/status'); + setCollectStatus(status); + } catch (_) {} + setCollecting(false); + load(); + }, 3000); + } catch (e) { + console.error('Collect error:', e); + setCollecting(false); + } + }; + + if (loading) return
불러오는 중...
; + return ( -
- {events.map((e, i) => ( -
-
-
- {e.label} - {fmtFull(e.date)} -
- - {getDDays(e.date) || ''} - +
+ {/* Stats Cards */} +
+
+

{dashboard?.active_count ?? 0}

+

진행중 공고

- ))} +
+

0 ? '#f43f5e' : undefined }}> + {dashboard?.new_match_count ?? 0} +

+

신규 매칭

+
+
+

{totalCount}

+

전체 공고

+
+
+ + {/* Collection Status */} +
+
+
+

데이터 수집

+

공공데이터 수집 현황

+ {collectStatus && ( +

+ 마지막 수집: {fmtDateTime(collectStatus.collected_at)} + {collectStatus.new_count != null && ` · 신규 ${collectStatus.new_count}건`} + {collectStatus.total_count != null && ` · 총 ${collectStatus.total_count}건`} + {collectStatus.error && · 오류: {collectStatus.error}} +

+ )} + {!collectStatus &&

수집 이력이 없습니다.

} +
+ +
+
+ + {/* Upcoming Schedules */} +
+
+
+

일정

+

다가오는 일정

+
+
+
+ {dashboard?.upcoming_schedules?.length > 0 ? ( +
+ {dashboard.upcoming_schedules.map((s, i) => { + const dday = getDDays(s.date); + return ( +
+
+
+ + {s.house_nm || s.label || '공고'} + + + {fmtFull(s.date)} · {s.event || s.type || '일정'} + +
+ {dday && ( + + {dday} + + )} +
+ ); + })} +
+ ) : ( +

다가오는 일정이 없습니다.

+ )} +
+
); -}; - -// ── 자금 섹션 (상세 패널) ───────────────────────────────────────────────────── -const FinanceSection = ({ item }) => { - const total = Number(item.totalPrice) || 0; - const deposit = Math.round(total * (item.depositRatio || 10) / 100); - const interim = Math.round(total * (item.interimRatio || 60) / 100); - const balance = total - deposit - interim; - const loan = Number(item.loanAmount) || 0; - const own = Number(item.ownFunds) || 0; - const loanRatio = total > 0 ? Math.round((loan / total) * 100) : 0; - const ownRatio = total > 0 ? Math.round((own / total) * 100) : 0; +} +// ── AnnouncementCard ───────────────────────────────────────────────────────── +function AnnouncementCard({ item, isSelected, onClick }) { + const dday = getDDays(item.receipt_start); return ( -
-
- 분양가 - {fmtPrice(total)} -
-
-
-
-
-
-
-
- 계약금 {item.depositRatio}% — {fmtPrice(deposit)} - 중도금 {item.interimRatio}% — {fmtPrice(interim)} - 잔금 {item.balanceRatio}% — {fmtPrice(balance)} +
+
+
+ + {item.house_secd && HOUSE_TYPE_LABELS[item.house_secd] && ( + + {HOUSE_TYPE_LABELS[item.house_secd]} + + )}
-
-
- 대출 계획 - {item.loanType} - {fmtPrice(loan)} ({loanRatio}%) -
-
- 자기자금 - - {fmtPrice(own)} ({ownRatio}%) -
- {total > 0 && loan + own !== total && ( -
- 미확보 자금 - - {fmtPrice(total - loan - own)} -
+

{item.house_nm || '(이름 없음)'}

+

{item.address || item.region_name || '-'}

+
+ {item.total_units ? `${item.total_units}세대` : '-'} + · + {item.region_name || '-'} +
+
+ {item.receipt_start && ( + + {fmt(item.receipt_start)} ~ {fmt(item.receipt_end)} + + )} + {dday && ( + + {dday} + )}
); -}; +} -// ── 상세 패널 ───────────────────────────────────────────────────────────────── -const SubDetail = ({ item, profile, onEdit, onDelete }) => { - const [section, setSection] = useState('요건'); - const sections = ['요건', '일정', '자금']; +// ── AnnouncementDetail ─────────────────────────────────────────────────────── +function AnnouncementDetail({ item }) { + const [detailTab, setDetailTab] = useState('info'); if (!item) { return (
-
📋
-

청약 항목을 선택하면
상세 정보가 표시됩니다

+ 🏠 + 공고를 선택하면 상세 정보가 표시됩니다.
); } - const scfg = STATUS_CONFIG[item.status] || STATUS_CONFIG['검토중']; - const tcfg = TYPE_CONFIG[item.type] || TYPE_CONFIG['일반']; - const checks = checkRequirements(item, profile); - const allPass = checks.length > 0 && checks.every((c) => c.pass); - return ( -
+
+ {/* Header */}
- - {item.status} - - - {item.type}{item.specialType ? ` · ${item.specialType}` : ''} - - {checks.length > 0 && ( - - {allPass ? '✓ 요건충족' : '△ 확인필요'} + + {item.house_secd && HOUSE_TYPE_LABELS[item.house_secd] && ( + + {HOUSE_TYPE_LABELS[item.house_secd]} )}
-

{item.complexName}

-

{item.address} · {item.pyeong || '-'}

+

{item.house_nm}

+

{item.address || item.region_name}

- - + {item.homepage_url && ( + + 홈페이지 + + )} + {item.pblanc_url && ( + + 공고문 + + )}
-
- 분양가 - {fmtPrice(item.totalPrice)} - 대출 {fmtPrice(item.loanAmount)} · 자기자금 {fmtPrice(item.ownFunds)} + {/* Section Tabs */} +
+ + + {item.models?.length > 0 && ( + + )}
-
-
- {sections.map((s) => ( - - ))} -
-
- {section === '요건' && } - {section === '일정' && } - {section === '자금' && } -
-
- - {item.memo && ( -
-

메모

-

{item.memo}

-
- )} -
- ); -}; - -// ── 일정 탭 ──────────────────────────────────────────────────────────────────── -const ScheduleTab = ({ items }) => { - const eventTypeColor = { - start: '#00d4ff', end: '#94a3b8', winner: '#34d399', - contract: '#8b5cf6', interim: '#f59e0b', balance: '#f43f5e', - }; - - const events = items.flatMap((item) => { - const list = [ - { date: item.applicationStart, label: '청약 시작', type: 'start', item }, - { date: item.applicationEnd, label: '청약 마감', type: 'end', item }, - { date: item.winnerAnnouncement, label: '당첨 발표', type: 'winner', item }, - { date: item.contractStart, label: '계약 시작', type: 'contract', item }, - { date: item.contractEnd, label: '계약 마감', type: 'end', item }, - item.interim1Date && { date: item.interim1Date, label: `중도금 1차 (${item.interim1Ratio}%)`, type: 'interim', item }, - item.interim2Date && { date: item.interim2Date, label: `중도금 2차 (${item.interim2Ratio}%)`, type: 'interim', item }, - item.interim3Date && { date: item.interim3Date, label: `중도금 3차 (${item.interim3Ratio}%)`, type: 'interim', item }, - { date: item.balanceDate, label: '잔금 납부', type: 'balance', item }, - ].filter(Boolean).filter((e) => e.date); - return list; - }).sort((a, b) => new Date(a.date) - new Date(b.date)); - - const today = new Date().setHours(0, 0, 0, 0); - const upcoming = events.filter((e) => new Date(e.date) >= today); - const past = events.filter((e) => new Date(e.date) < today).reverse(); - - const EventRow = ({ e }) => { - const color = eventTypeColor[e.type]; - const scfg = STATUS_CONFIG[e.item.status] || STATUS_CONFIG['검토중']; - return ( -
-
- {getDDays(e.date)} - {fmtFull(e.date)} -
-
-
- {e.item.complexName} - {e.label} - - {e.item.status} - -
-
- ); - }; - - return ( -
- {upcoming.length > 0 && ( -
-

예정 일정

- {upcoming.map((e, i) => )} -
- )} - {past.length > 0 && ( -
-

지난 일정

- {past.map((e, i) => )} -
- )} - {events.length === 0 &&

등록된 일정이 없습니다.

} -
- ); -}; - -// ── 자금 탭 ──────────────────────────────────────────────────────────────────── -const FinanceTab = ({ items }) => { - const totalAll = items.reduce((s, i) => s + (Number(i.totalPrice) || 0), 0); - const loanAll = items.reduce((s, i) => s + (Number(i.loanAmount) || 0), 0); - const ownAll = items.reduce((s, i) => s + (Number(i.ownFunds) || 0), 0); - - return ( -
-
-
-

총 분양가 합계

-

{fmtPrice(totalAll)}

-
-
-

총 대출 계획

-

{fmtPrice(loanAll)}

-
-
-

총 자기자금

-

{fmtPrice(ownAll)}

-
-
- - {items.map((item) => { - const total = Number(item.totalPrice) || 0; - const deposit = Math.round(total * (item.depositRatio || 10) / 100); - const interim = Math.round(total * (item.interimRatio || 60) / 100); - const balance = total - deposit - interim; - const loan = Number(item.loanAmount) || 0; - const own = Number(item.ownFunds) || 0; - const scfg = STATUS_CONFIG[item.status] || STATUS_CONFIG['검토중']; - - return ( -
-
-
-

자금 계획

-

{item.complexName}

-

{item.address} · {item.pyeong}

-
- - {item.status} - + {/* Section Content */} +
+ {detailTab === 'info' && ( +
+
+ 단지명 + {item.house_nm}
-
-
-
- 분양가 - {fmtPrice(total)} -
-
-
-
-
-
+
+ 지역 + {item.region_name || '-'} +
+
+ 주소 + {item.address || '-'} +
+
+ 총 세대수 + {item.total_units ? `${item.total_units}세대` : '-'} +
+
+ 시공사 + {item.constructor || '-'} +
+
+ 시행사 + {item.developer || '-'} +
+
+ 입주 예정 + {item.move_in_month || '-'} +
+
+ )} + + {detailTab === 'schedule' && ( +
+ {[ + { label: '특별공급 접수', start: item.spsply_start, end: item.spsply_end }, + { label: '1순위 접수', start: item.gnrl_rank1_start, end: item.gnrl_rank1_end }, + { label: '일반공급 접수', start: item.receipt_start, end: item.receipt_end }, + { label: '당첨자 발표', start: item.winner_date }, + { label: '계약', start: item.contract_start, end: item.contract_end }, + ].filter(s => s.start).map((s, i) => { + const dday = getDDays(s.start); + return ( +
+
+
+ {s.label} + + {fmtFull(s.start)}{s.end ? ` ~ ${fmtFull(s.end)}` : ''} +
-
- 계약금 {item.depositRatio}% — {fmtPrice(deposit)} - 중도금 {item.interimRatio}% — {fmtPrice(interim)} - 잔금 {item.balanceRatio}% — {fmtPrice(balance)} -
-
-
-
- 대출 ({item.loanType}) - - {fmtPrice(loan)} -
-
- 자기자금 - - {fmtPrice(own)} -
- {total > 0 && loan + own !== total && ( -
- 미확보 자금 - - {fmtPrice(total - loan - own)} -
+ {dday && ( + + {dday} + )}
-
-
+ ); + })} + {![item.spsply_start, item.gnrl_rank1_start, item.receipt_start, item.winner_date, item.contract_start].some(Boolean) && ( +

일정 정보가 없습니다.

+ )}
- ); - })} -
- ); -}; + )} -// ── 내 조건 편집 모달 ───────────────────────────────────────────────────────── -const ProfileModal = ({ profile, onClose, onSave }) => { - const [form, setForm] = useState({ ...profile }); - const set = (f) => (e) => setForm((p) => ({ ...p, [f]: e.target.value })); - const setNum = (f) => (e) => setForm((p) => ({ ...p, [f]: Number(e.target.value) })); - const setBool = (f) => (e) => setForm((p) => ({ ...p, [f]: e.target.checked })); - const toggleQual = (q) => setForm((p) => ({ - ...p, - specialQuals: p.specialQuals.includes(q) - ? p.specialQuals.filter((x) => x !== q) - : [...p.specialQuals, q], - })); - - const handleSubmit = (e) => { e.preventDefault(); onSave(form); }; - const score = calcTotalScore(form); - - return ( -
-
e.stopPropagation()}> -
-

내 청약 조건 편집

-
- 가점 {score}점 - -
-
-
-
-

기본 자격

-
- - - -
-
- - -
-
- - -
-
- -
-

청약 통장

-
- - -
-
- -
-

소득 · 특공 자격

- -
- {SPECIAL_QUALS.map((q) => ( - - ))} -
-
- -
- - -
-
-
-
- ); -}; - -// ── 청약 추가/편집 모달 ─────────────────────────────────────────────────────── -const SubModal = ({ item, onClose, onSave }) => { - const [form, setForm] = useState(item ? { ...item, totalPrice: String(item.totalPrice || ''), loanAmount: String(item.loanAmount || ''), ownFunds: String(item.ownFunds || '') } : { ...EMPTY_ITEM }); - const [tab, setTab] = useState('기본'); - const MODAL_TABS = ['기본', '요건', '일정', '자금']; - - const set = (f) => (e) => setForm((p) => ({ ...p, [f]: e.target.value })); - const setNum = (f) => (e) => setForm((p) => ({ ...p, [f]: Number(e.target.value) })); - const setBool = (f) => (e) => setForm((p) => ({ ...p, [f]: e.target.checked })); - - const handleSubmit = (e) => { - e.preventDefault(); - onSave({ ...form, totalPrice: Number(form.totalPrice) || 0, loanAmount: Number(form.loanAmount) || 0, ownFunds: Number(form.ownFunds) || 0 }); - }; - - return ( -
-
e.stopPropagation()}> -
-

{item ? '청약 편집' : '새 청약 추가'}

- -
-
- {MODAL_TABS.map((t) => ( - - ))} -
-
- {tab === '기본' && ( -
-
- - -
- -
- - {form.type === '특공' && ( - + {detailTab === 'models' && item.models?.length > 0 && ( +
+ {item.models.map((m, i) => ( +
+
+ + {m.model_nm || m.house_ty || `주택형 ${i + 1}`} + + {m.supply_count && ( + + {m.supply_count}세대 + + )} +
+ {m.exclusive_area && ( + 전용 {m.exclusive_area}m² + )} + {m.supply_price && ( + + 분양가 {Number(m.supply_price).toLocaleString()}만원 + )} - -
-
- -
-