Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS
191 lines
7.9 KiB
JavaScript
191 lines
7.9 KiB
JavaScript
import React, { useState, useMemo } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import {
|
|
STATUS_CONFIG, TABS,
|
|
} from './realEstateUtils';
|
|
import useComplexes from './hooks/useComplexes';
|
|
import ComplexCard from './components/ComplexCard';
|
|
import RightPanel from './components/RightPanel';
|
|
import ScheduleView from './components/ScheduleView';
|
|
import PriceAnalysis from './components/PriceAnalysis';
|
|
import ComplexModal from './components/ComplexModal';
|
|
import 'leaflet/dist/leaflet.css';
|
|
import './RealEstate.css';
|
|
|
|
// ── 메인 컴포넌트 ──────────────────────────────────────────────────────────────
|
|
const RealEstate = () => {
|
|
const { complexes, addComplex, updateComplex, deleteComplex } = useComplexes();
|
|
const [selectedComplex, setSelectedComplex] = useState(null);
|
|
const [activeTab, setActiveTab] = useState('목록');
|
|
const [filterStatus, setFilterStatus] = useState('전체');
|
|
const [showModal, setShowModal] = useState(false);
|
|
const [editingComplex, setEditingComplex] = useState(null);
|
|
|
|
const handleAdd = (data) => {
|
|
addComplex(data);
|
|
setShowModal(false);
|
|
};
|
|
|
|
const handleUpdate = (data) => {
|
|
updateComplex(data);
|
|
if (selectedComplex?.id === data.id) setSelectedComplex(data);
|
|
setEditingComplex(null);
|
|
setShowModal(false);
|
|
};
|
|
|
|
const handleDelete = (id) => {
|
|
if (!confirm('삭제하시겠습니까?')) return;
|
|
deleteComplex(id);
|
|
if (selectedComplex?.id === id) setSelectedComplex(null);
|
|
};
|
|
|
|
const handleModalSave = (data) => {
|
|
if (editingComplex) {
|
|
handleUpdate({ ...editingComplex, ...data });
|
|
} else {
|
|
handleAdd(data);
|
|
}
|
|
};
|
|
|
|
const filteredComplexes = useMemo(() => {
|
|
if (filterStatus === '전체') return complexes;
|
|
return complexes.filter((c) => c.status === filterStatus);
|
|
}, [complexes, filterStatus]);
|
|
|
|
const stats = useMemo(() => ({
|
|
total: complexes.length,
|
|
upcoming: complexes.filter((c) => c.status === '청약예정').length,
|
|
active: complexes.filter((c) => c.status === '청약중').length,
|
|
avgPrice: complexes.length
|
|
? Math.round(complexes.reduce((s, c) => s + c.avgPricePerPyeong, 0) / complexes.length)
|
|
: 0,
|
|
}), [complexes]);
|
|
|
|
return (
|
|
<div className="re">
|
|
{/* 헤더 */}
|
|
<header className="re-header">
|
|
<div>
|
|
<p className="re-kicker">부동산 정보</p>
|
|
<h1>관심 단지 관리</h1>
|
|
<p className="re-sub">관심 있는 아파트 단지 정보를 수집하고 분석합니다.</p>
|
|
<div className="re-header-actions">
|
|
<button
|
|
className="button primary"
|
|
onClick={() => { setEditingComplex(null); setShowModal(true); }}
|
|
>
|
|
+ 단지 추가
|
|
</button>
|
|
<Link to="/realestate" className="button ghost">← 청약 대시보드</Link>
|
|
<a href="https://www.applyhome.co.kr" target="_blank" rel="noreferrer" className="button ghost">
|
|
청약홈 →
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div className="re-stats-bar">
|
|
<div className="re-stat-item">
|
|
<p className="re-stat-item__value">{stats.total}</p>
|
|
<p className="re-stat-item__label">관심 단지</p>
|
|
</div>
|
|
<div className="re-stat-item">
|
|
<p className="re-stat-item__value" style={{ color: '#00d4ff' }}>{stats.upcoming}</p>
|
|
<p className="re-stat-item__label">청약 예정</p>
|
|
</div>
|
|
<div className="re-stat-item">
|
|
<p className="re-stat-item__value" style={{ color: '#34d399' }}>{stats.active}</p>
|
|
<p className="re-stat-item__label">청약 중</p>
|
|
</div>
|
|
<div className="re-stat-item">
|
|
<p className="re-stat-item__value" style={{ color: '#f59e0b' }}>
|
|
{stats.avgPrice.toLocaleString()}만
|
|
</p>
|
|
<p className="re-stat-item__label">평균 평당가</p>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* 탭 바 */}
|
|
<div className="re-tabs-bar">
|
|
<div className="re-tabs">
|
|
{TABS.map((tab) => (
|
|
<button
|
|
key={tab}
|
|
className={`re-tab ${activeTab === tab ? 'is-active' : ''}`}
|
|
onClick={() => setActiveTab(tab)}
|
|
>
|
|
{tab}
|
|
</button>
|
|
))}
|
|
</div>
|
|
{activeTab === '목록' && (
|
|
<div className="re-filter">
|
|
{['전체', ...Object.keys(STATUS_CONFIG)].map((s) => (
|
|
<button
|
|
key={s}
|
|
className={`re-filter-btn ${filterStatus === s ? 'is-active' : ''}`}
|
|
onClick={() => setFilterStatus(s)}
|
|
>
|
|
{s}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 목록 탭 — 카드 + 지도/상세 */}
|
|
{activeTab === '목록' && (
|
|
<div className="re-list-layout">
|
|
<div className="re-card-grid">
|
|
{filteredComplexes.length === 0 ? (
|
|
<p className="re-empty">등록된 단지가 없습니다.</p>
|
|
) : (
|
|
filteredComplexes.map((c) => (
|
|
<ComplexCard
|
|
key={c.id}
|
|
complex={c}
|
|
isSelected={selectedComplex?.id === c.id}
|
|
onClick={() => setSelectedComplex(c)}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
<RightPanel
|
|
complexes={complexes}
|
|
selectedComplex={selectedComplex}
|
|
onSelectComplex={setSelectedComplex}
|
|
onEdit={() => { setEditingComplex(selectedComplex); setShowModal(true); }}
|
|
onDelete={() => handleDelete(selectedComplex.id)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === '일정' && (
|
|
<div className="re-panel">
|
|
<div className="re-panel__head">
|
|
<div>
|
|
<p className="re-panel__eyebrow">캘린더</p>
|
|
<h3>청약 일정</h3>
|
|
<p className="re-panel__sub">청약 시작·마감·당첨 발표일을 타임라인으로 확인합니다.</p>
|
|
</div>
|
|
</div>
|
|
<ScheduleView complexes={complexes} />
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === '분석' && (
|
|
<PriceAnalysis complexes={complexes} />
|
|
)}
|
|
|
|
{showModal && (
|
|
<ComplexModal
|
|
complex={editingComplex}
|
|
onClose={() => { setShowModal(false); setEditingComplex(null); }}
|
|
onSave={handleModalSave}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RealEstate;
|