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}
- ))}
-
-
-
-
-
-
- 무주택
-
- {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}
+
+
신규 매칭
+
+
+
+
+ {/* 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 && (
-
- )}
-
- );
-};
-
-// ── 일정 탭 ────────────────────────────────────────────────────────────────────
-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}점
-
-
-
-
-
-
- );
-};
-
-// ── 청약 추가/편집 모달 ───────────────────────────────────────────────────────
-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) => (
-
- ))}
-
-