docs(refactor): RealEstate 분해 설계·계획
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
# RealEstate.jsx 분해 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** `RealEstate.jsx`(909줄)를 기능 모듈 구조(shell + hooks/useComplexes + realEstateUtils + components/5)로 **동작 보존 분해**한다.
|
||||
|
||||
**Architecture:** 순수 추출. 상수·데이터·순수헬퍼→`realEstateUtils.js`, 표현 컴포넌트 5개→`components/*.jsx`(verbatim), 데이터 로직(complexes+CRUD)→`hooks/useComplexes.js`, `RealEstate.jsx`는 UI 상태+조합 shell(~150줄)로 축소. 각 Task는 코드 이동 후 build+전체 테스트 green으로 동작 보존 확인. 로직/JSX/스타일 불변.
|
||||
|
||||
**Tech Stack:** React 18 + Vite, react-leaflet/leaflet, recharts, Vitest.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **Behavior-preserving 순수 추출**: 옮기는 컴포넌트/헬퍼 본문(JSX·로직)은 verbatim. import 배선만 추가/변경. 동작·스타일·클래스명 불변.
|
||||
- 경로: `RealEstate.jsx`(`src/pages/realestate/`) api `'../../api'`; `components/*`·`hooks/*`는 api `'../../../api'`, utils `'../realEstateUtils'`; shell → utils `'./realEstateUtils'`·hook `'./hooks/useComplexes'`·components `'./components/*'`·CSS `'./RealEstate.css'`. `realEstateUtils.js`는 `leaflet`의 `L`만 import(createMarkerIcon), api import 없음.
|
||||
- 각 Task 완료 조건: `npm run build` 성공 + `npm run test:run` 전체 green(회귀 0) + `npm run lint` 신규/수정 파일 0 에러.
|
||||
- 테스트: Vitest `import { describe, it, expect } from 'vitest'`, jest-dom 전역(테스트 파일 import 불필요), `*.test.js` 동일 디렉토리.
|
||||
- 각 컴포넌트/파일은 실제 사용하는 심볼만 import(미사용 금지 — lint 검출). 커밋은 web-ui, 변경 파일만 `git add`. 커밋 trailer:
|
||||
```
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||||
Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1: `realEstateUtils.js` — 상수·데이터·순수헬퍼 추출 + 테스트
|
||||
|
||||
**Files:**
|
||||
- Create: `src/pages/realestate/realEstateUtils.js`
|
||||
- Test: `src/pages/realestate/realEstateUtils.test.js`
|
||||
- Modify: `src/pages/realestate/RealEstate.jsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `SAMPLE_COMPLEXES`, `STATUS_CONFIG`, `PRIORITY_LABELS`, `EMPTY_FORM`, `TABS`, `formatDate(d)`, `formatPrice(v)`, `getDDays(dateStr)`, `createMarkerIcon(status, isSelected)`.
|
||||
|
||||
- [ ] **Step 1: Write the failing test** — Create `src/pages/realestate/realEstateUtils.test.js`:
|
||||
|
||||
```js
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { formatDate, formatPrice, getDDays } from './realEstateUtils.js';
|
||||
|
||||
describe('formatPrice', () => {
|
||||
it('만원 표기 / falsy → -', () => {
|
||||
expect(formatPrice(4500)).toBe('4,500만원');
|
||||
expect(formatPrice(0)).toBe('-');
|
||||
expect(formatPrice(null)).toBe('-');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDDays (오늘 기준)', () => {
|
||||
const iso = (off) => {
|
||||
const d = new Date(); d.setHours(0,0,0,0); d.setDate(d.getDate() + off);
|
||||
const p = (n) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`;
|
||||
};
|
||||
it('오늘=D-Day, 미래=D-N, 과거=D+N, null→null', () => {
|
||||
expect(getDDays(iso(0))).toBe('D-Day');
|
||||
expect(getDDays(iso(4))).toBe('D-4');
|
||||
expect(getDDays(iso(-2))).toBe('D+2');
|
||||
expect(getDDays(null)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('유효 날짜 한국어 포맷 / falsy → -', () => {
|
||||
expect(formatDate('2026-04-10')).toContain('2026');
|
||||
expect(formatDate(null)).toBe('-');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `npm run test:run -- src/pages/realestate/realEstateUtils.test.js`
|
||||
Expected: FAIL — `Failed to resolve import "./realEstateUtils.js"`.
|
||||
|
||||
- [ ] **Step 3: Create realEstateUtils.js** — 상단 `import L from 'leaflet';` 후, 현 `RealEstate.jsx`의 아래 항목을 **verbatim** 이동하고 각각 `export`:
|
||||
- `SAMPLE_COMPLEXES` (현 RealEstate.jsx의 4개 단지 배열 전체를 그대로 복사) — `export const SAMPLE_COMPLEXES = [...]`
|
||||
- `STATUS_CONFIG`, `PRIORITY_LABELS`, `EMPTY_FORM`, `TABS` (verbatim) — 각 `export const`
|
||||
- `formatDate`, `formatPrice`, `getDDays` (verbatim) — 각 `export const`
|
||||
- `createMarkerIcon` (verbatim, `L.divIcon` 사용) — `export const createMarkerIcon = (status, isSelected = false) => {...}`
|
||||
|
||||
명시적 내용(작은 것들, verbatim):
|
||||
```js
|
||||
export const STATUS_CONFIG = {
|
||||
'청약예정': { color: '#00d4ff', bg: 'rgba(0,212,255,0.12)' },
|
||||
'청약중': { color: '#34d399', bg: 'rgba(52,211,153,0.12)' },
|
||||
'결과발표': { color: '#f59e0b', bg: 'rgba(245,158,11,0.12)' },
|
||||
'완료': { color: '#6b7280', bg: 'rgba(107,114,128,0.10)' },
|
||||
};
|
||||
export const PRIORITY_LABELS = { high: '★ 최우선', normal: '보통', low: '낮음' };
|
||||
export const EMPTY_FORM = {
|
||||
name: '', address: '', lat: '', lng: '',
|
||||
units: '', types: '', avgPricePerPyeong: '',
|
||||
subscriptionStart: '', subscriptionEnd: '', resultDate: '',
|
||||
status: '청약예정', priority: 'normal',
|
||||
tags: '', naverUrl: '', floorPlanUrl: '', memo: '',
|
||||
};
|
||||
export const TABS = ['목록', '일정', '분석'];
|
||||
|
||||
export const formatDate = (d) => {
|
||||
if (!d) return '-';
|
||||
return new Date(d).toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
};
|
||||
export const formatPrice = (v) => {
|
||||
if (!v) return '-';
|
||||
return `${v.toLocaleString()}만원`;
|
||||
};
|
||||
export const getDDays = (dateStr) => {
|
||||
if (!dateStr) return null;
|
||||
const target = new Date(dateStr);
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
target.setHours(0, 0, 0, 0);
|
||||
const diff = Math.ceil((target - today) / (1000 * 60 * 60 * 24));
|
||||
if (diff === 0) return 'D-Day';
|
||||
if (diff > 0) return `D-${diff}`;
|
||||
return `D+${Math.abs(diff)}`;
|
||||
};
|
||||
export const createMarkerIcon = (status, isSelected = false) => {
|
||||
const cfg = STATUS_CONFIG[status] || STATUS_CONFIG['완료'];
|
||||
const size = isSelected ? 18 : 12;
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="
|
||||
width:${size}px;height:${size}px;border-radius:50%;
|
||||
background:${cfg.color};
|
||||
box-shadow:0 0 ${isSelected ? 16 : 8}px ${cfg.color};
|
||||
border:2px solid rgba(255,255,255,${isSelected ? 0.6 : 0.25});
|
||||
transition:all 0.2s;
|
||||
"></div>`,
|
||||
iconSize: [size, size],
|
||||
iconAnchor: [size / 2, size / 2],
|
||||
popupAnchor: [0, -(size / 2 + 4)],
|
||||
});
|
||||
};
|
||||
```
|
||||
`SAMPLE_COMPLEXES`는 현 RealEstate.jsx:14-91의 배열을 그대로 옮겨 `export const SAMPLE_COMPLEXES = [ ... ];`로.
|
||||
|
||||
- [ ] **Step 4: Wire RealEstate.jsx** — 위 항목들의 기존 정의(SAMPLE_COMPLEXES/STATUS_CONFIG/PRIORITY_LABELS/EMPTY_FORM/TABS/formatDate/formatPrice/getDDays/createMarkerIcon)를 삭제하고, import 추가:
|
||||
```js
|
||||
import {
|
||||
SAMPLE_COMPLEXES, STATUS_CONFIG, PRIORITY_LABELS, EMPTY_FORM, TABS,
|
||||
formatDate, formatPrice, getDDays, createMarkerIcon,
|
||||
} from './realEstateUtils';
|
||||
```
|
||||
(이 시점엔 컴포넌트들이 아직 RealEstate.jsx 내부 → import한 심볼 사용. `import L from 'leaflet'`는 createMarkerIcon이 utils로 갔으니 RealEstate.jsx에서 더 이상 직접 안 쓰면 제거; 단 다른 곳(예: 없음)에서 안 쓰면 제거. `import 'leaflet/dist/leaflet.css'`는 지도 렌더 위해 shell 또는 RightPanel에 유지 필요 → 이번 단계는 RealEstate.jsx에 유지.)
|
||||
|
||||
- [ ] **Step 5: Run tests + build**
|
||||
|
||||
Run: `npm run test:run -- src/pages/realestate/realEstateUtils.test.js` → PASS.
|
||||
Run: `npm run test:run` → 전체 green. `npm run build` → `✓ built`. `npm run lint` → 신규/수정 파일 0 에러(미사용 import 없도록).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
```bash
|
||||
git add src/pages/realestate/realEstateUtils.js src/pages/realestate/realEstateUtils.test.js src/pages/realestate/RealEstate.jsx
|
||||
git commit -m "refactor(realestate): 상수·데이터·순수헬퍼를 realEstateUtils로 추출 + 테스트"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 표현 컴포넌트 5개 추출 (ComplexCard/RightPanel/ScheduleView/PriceAnalysis/ComplexModal)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/pages/realestate/components/ComplexCard.jsx`
|
||||
- Create: `src/pages/realestate/components/RightPanel.jsx`
|
||||
- Create: `src/pages/realestate/components/ScheduleView.jsx`
|
||||
- Create: `src/pages/realestate/components/PriceAnalysis.jsx`
|
||||
- Create: `src/pages/realestate/components/ComplexModal.jsx`
|
||||
- Modify: `src/pages/realestate/RealEstate.jsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes (Task 1): utils 심볼.
|
||||
- Produces: 각 기본 export — `ComplexCard({complex,isSelected,onClick})`, `RightPanel({complexes,selectedComplex,onSelectComplex,onEdit,onDelete})`, `ScheduleView({complexes})`, `PriceAnalysis({complexes})`, `ComplexModal({complex,onClose,onSave})`.
|
||||
|
||||
- [ ] **Step 1: Create the five components (본문 verbatim + 상단 import)**
|
||||
|
||||
각 새 파일 = 상단 import 블록 + 현 RealEstate.jsx의 해당 함수 본문 verbatim + `export default <Name>;`. **`MapFlyTo`는 RightPanel.jsx 안에 내부 컴포넌트로 함께 이동**(현 RealEstate.jsx의 MapFlyTo 정의를 RightPanel.jsx 상단에 그대로 두고 RightPanel이 사용). `EventItem`은 ScheduleView 내부, `CustomTooltip`은 PriceAnalysis 내부에 이미 중첩되어 있으므로 그대로.
|
||||
|
||||
import 블록(각 컴포넌트가 실제 사용하는 심볼만 — 아래는 현 코드 사용 기준):
|
||||
|
||||
`ComplexCard.jsx`:
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import { STATUS_CONFIG, formatPrice, getDDays } from '../realEstateUtils';
|
||||
```
|
||||
`RightPanel.jsx` (MapFlyTo 내부 포함):
|
||||
```jsx
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
|
||||
import { STATUS_CONFIG, PRIORITY_LABELS, formatDate, formatPrice, createMarkerIcon } from '../realEstateUtils';
|
||||
```
|
||||
`ScheduleView.jsx`:
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import { STATUS_CONFIG, formatDate, getDDays } from '../realEstateUtils';
|
||||
```
|
||||
`PriceAnalysis.jsx`:
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from 'recharts';
|
||||
import { STATUS_CONFIG, formatDate, formatPrice } from '../realEstateUtils';
|
||||
```
|
||||
`ComplexModal.jsx`:
|
||||
```jsx
|
||||
import React, { useState } from 'react';
|
||||
import { STATUS_CONFIG, EMPTY_FORM } from '../realEstateUtils';
|
||||
```
|
||||
> 실제 본문이 참조하는 심볼만 포함할 것(빌드/lint가 누락·미사용 검출). RightPanel은 `leaflet/dist/leaflet.css`를 shell에서 로드하므로 별도 import 불필요.
|
||||
|
||||
- [ ] **Step 2: Wire RealEstate.jsx** — 5개 컴포넌트 + MapFlyTo 정의 삭제, import 추가:
|
||||
```js
|
||||
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';
|
||||
```
|
||||
RealEstate.jsx 상단의 이제-미사용 import 정리: `MapContainer/TileLayer/Marker/Popup/useMap`(react-leaflet), `BarChart~Cell`(recharts)가 shell에서 더 이상 안 쓰이면 제거(shell은 지도/차트를 직접 렌더하지 않음). `import 'leaflet/dist/leaflet.css'`는 지도 스타일 위해 **shell에 유지**(RightPanel이 렌더될 때 필요). `useMemo`는 shell의 filteredComplexes/stats에서 계속 사용 → 유지.
|
||||
|
||||
- [ ] **Step 3: Run tests + build + lint**
|
||||
|
||||
Run: `npm run test:run` → 전체 green. `npm run build` → `✓ built`. `npm run lint` → 신규 5파일 + RealEstate.jsx 0 에러.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
```bash
|
||||
git add src/pages/realestate/components/ComplexCard.jsx src/pages/realestate/components/RightPanel.jsx src/pages/realestate/components/ScheduleView.jsx src/pages/realestate/components/PriceAnalysis.jsx src/pages/realestate/components/ComplexModal.jsx src/pages/realestate/RealEstate.jsx
|
||||
git commit -m "refactor(realestate): 카드/지도패널/일정/분석/모달 표현 컴포넌트 추출"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: `useComplexes` 훅 추출 → RealEstate를 shell 컨테이너로 + 최종 검증
|
||||
|
||||
**Files:**
|
||||
- Create: `src/pages/realestate/hooks/useComplexes.js`
|
||||
- Modify: `src/pages/realestate/RealEstate.jsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `useComplexes()` 기본 export → `{ complexes, addComplex, updateComplex, deleteComplex }`.
|
||||
|
||||
- [ ] **Step 1: Create useComplexes.js** — 현 메인의 데이터 로직을 그대로 옮김(UI 관심사는 제외):
|
||||
|
||||
```js
|
||||
import { useState, useEffect } from 'react';
|
||||
import { apiGet, apiPost, apiPut, apiDelete } from '../../../api';
|
||||
import { SAMPLE_COMPLEXES } from '../realEstateUtils';
|
||||
|
||||
export default function useComplexes() {
|
||||
const [complexes, setComplexes] = useState(SAMPLE_COMPLEXES);
|
||||
|
||||
useEffect(() => {
|
||||
apiGet('/api/realestate/complexes')
|
||||
.then((data) => {
|
||||
if (Array.isArray(data) && data.length > 0) setComplexes(data);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const addComplex = async (data) => {
|
||||
const newComplex = { ...data, id: Date.now() };
|
||||
setComplexes((prev) => [...prev, newComplex]);
|
||||
try { await apiPost('/api/realestate/complexes', data); } catch { /* noop */ }
|
||||
};
|
||||
|
||||
const updateComplex = async (data) => {
|
||||
setComplexes((prev) => prev.map((c) => (c.id === data.id ? data : c)));
|
||||
try { await apiPut(`/api/realestate/complexes/${data.id}`, data); } catch { /* noop */ }
|
||||
};
|
||||
|
||||
const deleteComplex = async (id) => {
|
||||
setComplexes((prev) => prev.filter((c) => c.id !== id));
|
||||
try { await apiDelete(`/api/realestate/complexes/${id}`); } catch { /* noop */ }
|
||||
};
|
||||
|
||||
return { complexes, addComplex, updateComplex, deleteComplex };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Reduce RealEstate.jsx to the shell container** — 컴포넌트 본문 최상단 부분(상태 선언 ~ 핸들러 ~ memo)을 아래로 교체. **`return (...)` JSX 블록은 verbatim 유지**(이미 `complexes`/`filteredComplexes`/`stats`/`selectedComplex`/`activeTab`/`filterStatus`/`handleDelete`/`handleModalSave`/`setSelectedComplex`/`setEditingComplex`/`setShowModal`/`setFilterStatus`/`setActiveTab`를 참조 — 이들은 아래 재구성으로 동일하게 제공됨).
|
||||
|
||||
RealEstate.jsx 상단 import는 다음으로 정리(현 사용 기준): React 훅(`useState`, `useMemo`), `Link`, utils(SAMPLE_COMPLEXES는 이제 훅이 씀 → shell에서 제거 가능; STATUS_CONFIG/TABS는 shell 렌더에서 사용 → 유지, 그 외 유틸 중 shell이 직접 쓰는 것만), `useComplexes`, 5개 컴포넌트, `import 'leaflet/dist/leaflet.css'`, `import './RealEstate.css'`. (미사용 import 제거로 lint 0.)
|
||||
|
||||
컴포넌트 함수 본문(선언부) 교체:
|
||||
```js
|
||||
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 (
|
||||
// ── 현 RealEstate.jsx의 return JSX 전체를 verbatim 유지 ──
|
||||
);
|
||||
};
|
||||
```
|
||||
> `handleAdd`/`handleUpdate`/`handleDelete`는 비동기 await를 훅으로 옮겼으므로 shell에선 동기(비-async). 원래도 optimistic 갱신 후 UI(setShowModal/setSelected)를 처리하고 API는 fire-and-forget이었으므로 동작 동일(setComplexes→UI→API 순서 보존). `handleModalSave`/`filteredComplexes`/`stats`와 return JSX는 원본 그대로.
|
||||
|
||||
- [ ] **Step 3: Run full suite + lint + build**
|
||||
|
||||
Run: `npm run test:run` → 전체 green(회귀 0). `npm run lint` → 0 새 에러. `npm run build` → `✓ built`. `wc -l src/pages/realestate/RealEstate.jsx` 로 shell 축소(~150줄) 확인.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
```bash
|
||||
git add src/pages/realestate/hooks/useComplexes.js src/pages/realestate/RealEstate.jsx
|
||||
git commit -m "refactor(realestate): 데이터 로직 useComplexes 훅 추출 → RealEstate shell 축소"
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Manual verification (dev server)**
|
||||
|
||||
`npm run dev` → `http://localhost:3007/realestate/property` 접속. 3탭(목록/일정/분석) 전환, 목록 카드 선택→우측 지도 flyTo+상세, 필터, 단지 추가/편집/삭제 모달, 지도 마커 클릭이 리팩토링 전과 **동일 동작**인지 확인. 청약 대시보드 링크(`/realestate`) 동작 확인.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review 결과
|
||||
|
||||
**Spec coverage** (설계 §1–§7):
|
||||
- §3 file map(utils/hook/components 5/shell) → Task 1/2/3 ✅
|
||||
- §4 useComplexes(데이터만, UI 경계는 shell) → Task 3 ✅ (confirm·selectedComplex 동기화 shell 유지)
|
||||
- §5 테스트(realEstateUtils.test + build/lint/수동) → Task 1 + 각 Task 검증 + Task 3 수동 ✅
|
||||
- §6 완료기준 → 각 Task 검증 ✅
|
||||
|
||||
**Placeholder scan:** 신규 코드(utils 작은 것들/훅/테스트/shell 선언부)는 전체 코드 명시. SAMPLE_COMPLEXES·표현 컴포넌트·return JSX는 "현 정의 verbatim 이동 + 명시된 import"로 지정(플레이스홀더 아님 — 원본 존재, import 심볼 명시). ✅
|
||||
|
||||
**Type consistency:** utils export(Task1) ↔ Task2/3 import 일치. 컴포넌트 기본 export ↔ shell import 일치. `useComplexes` 반환 `{complexes,addComplex,updateComplex,deleteComplex}` ↔ shell 사용 일치. 경로(shell `../../api` 아님— shell은 api 직접 안 씀; components/hooks `../../../api`) 일치. ✅
|
||||
|
||||
**참고:** Task 순서 의존(utils→components→hook/shell). behavior-preserving라 RED는 "신규 파일 미존재"로 성립, GREEN은 추출 후 전체 회귀 0.
|
||||
Reference in New Issue
Block a user