Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS
91 lines
6.4 KiB
Markdown
91 lines
6.4 KiB
Markdown
# 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`(leaflet `L.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`/`stats` memo + 헤더/탭/목록·일정·분석 렌더 + 모달.
|
|
|
|
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)
|
|
|
|
1. **`realEstateUtils.test.js`(신규, 필수)** — 순수 헬퍼:
|
|
- `formatPrice`: 4500→"4,500만원", 0/falsy→"-".
|
|
- `getDDays`: 오늘=D-Day, 미래 D-N, 과거 D+N, null→null.
|
|
- `formatDate`: 유효 날짜 한국어 포맷, falsy→"-".
|
|
2. 컴포넌트는 표현 위주 → 이번 범위 스모크 최소화(추출 검증은 build+lint+수동). ComplexCard 스모크(선택) 정도 가능.
|
|
3. **검증 게이트:** 각 단계 `npm run build` + `npm run test:run` 전체 green(회귀 0) + `npm run lint` 신규 파일 0 에러. 최종 개발서버 `/realestate/property` 3탭(목록/일정/분석)·지도 마커·단지 추가/편집/삭제 모달이 리팩토링 전과 동일 동작.
|
|
|
|
---
|
|
|
|
## 6. 완료 기준
|
|
|
|
- [ ] RealEstate.jsx가 shell(~150줄) + hooks/useComplexes + realEstateUtils(+test) + components/5로 분해, 동작/스타일 불변.
|
|
- [ ] `realEstateUtils.test.js` 통과, 전체 `npm run test:run` green.
|
|
- [ ] `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에서 `L` import.
|
|
- **후속 follow-up**: RightPanel(~200줄)은 지도/상세 2책임 — 필요 시 후속 분할 여지 기록.
|
|
- **다음 서브프로젝트**: PortfolioTab(671, stock/) → InstaCards(1013) → MusicStudio(1936).
|