Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS
6.4 KiB
RealEstate.jsx 분해 리팩토링 — 설계 (FE 모듈 sweep 서브프로젝트 2)
- 작성일: 2026-07-09
- 역할/저장소: FE (
web-ui) - 범위: FE 대형 컴포넌트 분해 sweep의 2번째 서브프로젝트. 컨벤션은 sub-project 1(Subscription)에서 확립됨(web-ui/CLAUDE.md "기능 모듈 구조") — 이번은 적용만.
- 성격: behavior-preserving 순수 추출. 로직/JSX/스타일 변경 없음.
1. 배경 & 목표
src/pages/realestate/RealEstate.jsx(909줄, /realestate/property 관심 단지 페이지)를 기능 모듈 구조로 동작 보존 분해한다. Subscription과 달리 메인이 데이터 상태(complexes)+CRUD API를 직접 보유하고 하위는 순수 표현 컴포넌트(props)인 "스마트 컨테이너 + 표현" 구조 → 데이터 로직은 hooks/useComplexes.js로 추출(컨벤션: shell은 조합만).
비목표: 동작/스타일 변경, 신규 기능, MapFlyTo/EventItem/CustomTooltip 같은 소형 내부 컴포넌트의 과분해.
2. 현 구조 맵 (RealEstate.jsx 909줄)
- 상수/데이터:
SAMPLE_COMPLEXES(폴백 샘플 4개),STATUS_CONFIG,PRIORITY_LABELS,EMPTY_FORM,TABS(['목록','일정','분석']) - 순수 헬퍼:
formatDate,formatPrice,getDDays,createMarkerIcon(leafletL.divIcon) - 컴포넌트:
MapFlyTo(react-leaflet 훅),ComplexCard,RightPanel(지도+상세, ~183줄, MapFlyTo 사용),ScheduleView(내부EventItem),PriceAnalysis(차트+표, 내부CustomTooltip),ComplexModal(폼 모달) - 메인
RealEstate: 상태(complexes/selectedComplex/activeTab/filterStatus/showModal/editingComplex) +useEffect로드 +handleAdd/handleUpdate/handleDelete/handleModalSave+filteredComplexes/statsmemo + 헤더/탭/목록·일정·분석 렌더 + 모달.
API: apiGet/apiPost/apiPut/apiDelete → /api/realestate/complexes(+/:id).
3. 목표 file map
src/pages/realestate/
├── RealEstate.jsx # shell/컨테이너 (~150줄): UI 상태(selected/activeTab/filter/modal) + useComplexes 소비 + 조합
├── RealEstate.css # 변경 없음 (shell에서 import 유지)
├── realEstateUtils.js # SAMPLE_COMPLEXES·STATUS_CONFIG·PRIORITY_LABELS·EMPTY_FORM·TABS + formatDate·formatPrice·getDDays·createMarkerIcon
├── realEstateUtils.test.js # 순수헬퍼 유닛 (신규)
├── hooks/
│ └── useComplexes.js # complexes 상태 + 로드 + add/update/delete (낙관적 UI + API), { complexes, addComplex, updateComplex, deleteComplex } 반환
└── components/
├── ComplexCard.jsx # ({ complex, isSelected, onClick })
├── RightPanel.jsx # ({ complexes, selectedComplex, onSelectComplex, onEdit, onDelete }) — MapFlyTo 내부 유지
├── ScheduleView.jsx # ({ complexes }) — EventItem 내부 중첩 유지
├── PriceAnalysis.jsx # ({ complexes }) — CustomTooltip 내부 중첩 유지
└── ComplexModal.jsx # ({ complex, onClose, onSave })
import 경로 규칙: components/hooks → api '../../../api'; components/hooks → utils '../realEstateUtils'; shell → utils './realEstateUtils', hook './hooks/useComplexes', components './components/*', CSS './RealEstate.css'. realEstateUtils.js는 leaflet의 L만 import(createMarkerIcon용), api import 없음. RightPanel.jsx는 leaflet/react-leaflet import 필요.
4. useComplexes 훅 (데이터 레이어 추출)
현 메인의 데이터 로직을 그대로 옮김(동작 보존):
- 상태
complexes(초기값SAMPLE_COMPLEXES),useEffect로GET /api/realestate/complexes로드(배열 있으면 교체). addComplex(data)— 낙관적 append(id: Date.now()) +POST.updateComplex(data)— 낙관적 map 교체 +PUT /:id.deleteComplex(id)— 낙관적 filter +DELETE /:id. (confirm은 shell에 유지할지 훅에 둘지 — 동작 보존 위해 현 위치(핸들러) 그대로:handleDelete의confirm은 shell에 남기고 훅은 순수 삭제만 담당. 즉 훅은deleteComplex(id)가 낙관적 삭제+API, shell이 confirm 후 호출.)- 반환:
{ complexes, addComplex, updateComplex, deleteComplex }. selectedComplex 동기화(삭제/수정 시)는 shell이 담당(UI 상태이므로).
주의: 현 코드의
handleUpdate가selectedComplex?.id === data.id면setSelectedComplex(data)도 함. 이는 UI 상태(selectedComplex) 갱신이므로 shell에 유지. 훅은 complexes만 관리. shell의handleUpdate래퍼가updateComplex(data)호출 + selectedComplex 동기화.
5. 테스트 (안전망 — 현재 RealEstate 테스트 0)
realEstateUtils.test.js(신규, 필수) — 순수 헬퍼:formatPrice: 4500→"4,500만원", 0/falsy→"-".getDDays: 오늘=D-Day, 미래 D-N, 과거 D+N, null→null.formatDate: 유효 날짜 한국어 포맷, falsy→"-".
- 컴포넌트는 표현 위주 → 이번 범위 스모크 최소화(추출 검증은 build+lint+수동). ComplexCard 스모크(선택) 정도 가능.
- 검증 게이트: 각 단계
npm run build+npm run test:run전체 green(회귀 0) +npm run lint신규 파일 0 에러. 최종 개발서버/realestate/property3탭(목록/일정/분석)·지도 마커·단지 추가/편집/삭제 모달이 리팩토링 전과 동일 동작.
6. 완료 기준
- RealEstate.jsx가 shell(~150줄) + hooks/useComplexes + realEstateUtils(+test) + components/5로 분해, 동작/스타일 불변.
realEstateUtils.test.js통과, 전체npm run test:rungreen.npm run lint·npm run build통과./realestate/property수동 검증 동일 동작(지도·모달·탭·필터).
7. 리스크 / 오픈 이슈
- 테스트 부재 컴포넌트 리팩토링: 순수 추출 + build/lint/유닛/수동으로 위험 최소화.
- 훅 추출 시 상태 경계: complexes(데이터)는 훅, selectedComplex/activeTab/filter/modal(UI)은 shell. 현 동작(특히 update 시 selectedComplex 동기화, delete 시 confirm)을 그대로 재현.
- leaflet/recharts import: RightPanel(leaflet)·PriceAnalysis(recharts)가 각자 필요한 것만 import. createMarkerIcon은 utils에서
Limport. - 후속 follow-up: RightPanel(~200줄)은 지도/상세 2책임 — 필요 시 후속 분할 여지 기록.
- 다음 서브프로젝트: PortfolioTab(671, stock/) → InstaCards(1013) → MusicStudio(1936).