docs(refactor): 기능 모듈 구조 컨벤션 + Subscription 분해 설계·계획

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:
2026-07-09 17:43:09 +09:00
parent 1b4fba3c19
commit 15cb30311e
2 changed files with 677 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
# FE 기능 모듈 구조 컨벤션 + Subscription 분해 리팩토링 — 설계
- **작성일**: 2026-07-09
- **역할/저장소**: FE (`web-ui`)
- **범위**: FE 대형 컴포넌트 책임 분해 리팩토링의 **1번째 서브프로젝트**. (전체 분해: Subscription→RealEstate→PortfolioTab→InstaCards→MusicStudio 순차, 각 별도 spec/plan.)
- **성격**: **behavior-preserving(동작 보존) 순수 추출**. 로직 재작성 금지, UI/동작 변경 없음.
---
## 1. 배경 & 목표
기능이 늘며 일부 페이지 컴포넌트가 단일 파일에 과도한 책임을 담게 됨(예: `Subscription.jsx` 1647줄에 4탭+카드+상세+캘린더+헬퍼 전부). 하네스 엔지니어링 원칙(단일 책임·명확한 경계·CLAUDE.md 계층 문서화)에 맞춰 **기능 모듈 구조**로 분해하고, 그 컨벤션을 하네스(web-ui/CLAUDE.md)와 README에 기록한다.
이 스펙은 두 부분:
- **Part A (공통 컨벤션 + 문서화)**: "기능 모듈 구조" 표준 정의 → 이후 모든 서브프로젝트가 참조. web-ui/CLAUDE.md + README 기록.
- **Part B (Subscription 분해)**: 컨벤션을 `Subscription.jsx`에 첫 적용.
비목표: 동작/스타일 변경, 신규 기능, api.js 중복(local apiPatch) 정리 외 무관 리팩토링.
---
## 2. Part A — 기능 모듈 구조 컨벤션 (하네스 기록)
**표준 디렉토리 형태** (레퍼런스: `src/pages/listings/`):
```
src/pages/<feature>/
├── <Feature>.jsx # 페이지 shell — 라우팅 진입, 탭바/헤더/레이아웃, 하위 조합만
├── <Feature>.css # 스타일 (shell에서 1회 import → 전역 클래스 하위 공유)
├── components/ # 표현 단위 (카드/탭/상세/리스트…) 각 단일 책임
├── hooks/ # 상태·API·부수효과 훅 (선택)
└── <feature>Utils.js # 순수 헬퍼(포맷/매핑/계산) + <feature>Utils.test.js
```
**규칙:**
- 파일당 **단일 책임**, 권장 상한 **~300줄**(초과 시 분할 신호).
- 순수 로직(포맷/매핑/계산)은 `*Utils.js`로 뽑고 **유닛 테스트 필수**.
- 상태+API가 얽힌 로직은 `hooks/` 또는 자립형 탭 컴포넌트로.
- shell은 "조합"만: 데이터 페칭·비즈니스 로직을 직접 갖지 않음.
- 컴포넌트는 서로 잘 정의된 props/훅 인터페이스로만 통신.
**문서화 대상:**
- `web-ui/CLAUDE.md`: "기능 모듈 구조" 섹션 신설(위 형태·규칙), 페이지 구조 표에 리팩토링 반영.
- `README.md`: 프로젝트 통계/구조 설명에 모듈 구조 컨벤션 1문단 추가.
---
## 3. Part B — Subscription.jsx 분해 (file map)
`src/pages/subscription/Subscription.jsx`(1647줄)를 아래로 분해. **코드 이동만**(내용 동일), import 배선만 추가.
```
src/pages/subscription/
├── Subscription.jsx # shell: PullToRefresh + 헤더 + 탭바 + FAB + activeTab/refreshKey. 4탭 조합. (~70줄)
├── Subscription.css # (변경 없음, shell에서 import 유지)
├── subscriptionUtils.js # 상수·순수헬퍼 이동
├── subscriptionUtils.test.js # 순수헬퍼 유닛 테스트 (신규)
└── components/
├── StatusBadge.jsx # (기존 컴포넌트 이동)
├── DashboardTab.jsx # 자립형(self-fetch)
├── AnnouncementCard.jsx
├── AnnouncementDetail.jsx # detailTab 상태 + 정보/일정/주택형/매칭분석
├── CalendarView.jsx
├── AnnouncementsTab.jsx # AnnouncementCard/AnnouncementDetail/CalendarView 조합
├── MatchesTab.jsx
├── ProfileTab.jsx # DistrictTierEditor/NotificationSettings(기존) 사용
├── DistrictTierEditor.jsx # (기존 유지)
└── NotificationSettings.jsx # (기존 유지)
```
**`subscriptionUtils.js`로 이동할 항목:**
- 상수: `STATUS_CONFIG`, `HOUSE_TYPE_LABELS`, `TABS`, `STATUS_FILTERS`, `DEFAULT_PROFILE`
- 순수 헬퍼: `extractTier`, `fmt`, `fmtFull`, `_diffDays`, `getDDays`, `getDDayColor`, `fmtDateTime`, `fmtPrice`
- `apiPatch`(현재 Subscription 로컬 정의) — 이동. (⚠️ `src/api.js`에 동일 기능 `apiPatch` 이미 존재 — 이번엔 **로컬 것을 utils로 그대로 이동**(동작 보존), api.js와의 dedupe는 후속 follow-up으로 기록.)
**의존 관계(이동 시 import 배선):**
- `DashboardTab` → apiGet/apiPost, STATUS_CONFIG, fmt류, getDDays/getDDayColor, fmtDateTime, fmtPrice, StatusBadge
- `AnnouncementCard`/`AnnouncementDetail` → STATUS_CONFIG, HOUSE_TYPE_LABELS, extractTier, fmt/fmtFull, getDDays/getDDayColor, fmtPrice, StatusBadge
- `AnnouncementsTab` → apiGet/apiDelete/apiPatch, STATUS_FILTERS, AnnouncementCard/AnnouncementDetail/CalendarView
- `MatchesTab` → apiGet/apiPost/apiPatch, extractTier, fmt/getDDays/getDDayColor, StatusBadge
- `ProfileTab` → apiGet/apiPut, DEFAULT_PROFILE, DistrictTierEditor/NotificationSettings
- `Subscription`(shell) → TABS, 4탭 컴포넌트, PullToRefresh/FAB/Link
**주의:** `/realestate/listings` 교차링크(현 shell의 Link)는 그대로 유지.
---
## 4. 테스트 (안전망 — 현재 Subscription은 테스트 0)
behavior-preserving라 기존 동작 유지가 핵심. 안전망:
1. **`subscriptionUtils.test.js` (신규, 필수)** — 순수 헬퍼 유닛:
- `extractTier`: "자치구 S티어: 강남구 (+25)" → "S"; 미매치 → null.
- `getDDays`/`getDDayColor`: D-day 경계(오늘=D-Day, 미래 D-N, 과거 D+N; 색 임계 0/3/7).
- `fmtPrice`(현 구현 동작 고정): 10000→"1억", 15000→"1.5억", 9999→"9,999만", null→null.
- `_diffDays`: 로컬 타임존 파싱 경계.
2. **`StatusBadge.test.jsx` (신규, 스모크)** — status→색/라벨, 미정의 status 폴백.
3. 각 탭 컴포넌트는 자립형(api 페치) → 이번 범위는 **스모크 최소화**(추출 검증은 build+lint+수동). 탭 스모크(vi.mock api)는 권장이나 필수 아님(후속).
**검증 게이트(품질 게이트):** `npm run test:run`(신규 유닛 포함 전체 green) + `npm run lint`(신규 파일 0 에러) + `npm run build`(성공) + 개발서버 `/realestate` 4탭 수동 확인(공고 목록/상세/캘린더/매칭/프로필 저장 동작 동일).
---
## 5. 컴포넌트 구조 (분해 후 검증 관점)
각 파일이 "무엇을 하는가 / 어떻게 쓰는가 / 무엇에 의존하는가"로 독립 이해 가능해야 함:
- `AnnouncementDetail`(현 272줄) → 여전히 큰 편이나 단일 책임(공고 상세). 내부 detailTab 3섹션은 한 컴포넌트 응집. 300줄 근처 → 필요 시 후속 분할 여지 기록.
- `ProfileTab`(현 390줄) → 폼이 커서 300줄 초과. 이번엔 파일 추출까지만, 폼 섹션(기본/자격/선호) 하위 분할은 후속 follow-up으로 기록(과분해 회피, YAGNI).
---
## 6. 완료 기준 (Acceptance)
- [ ] Part A: web-ui/CLAUDE.md "기능 모듈 구조" 섹션 + README 컨벤션 문단 기록.
- [ ] Part B: Subscription.jsx가 shell(~70줄) + components/8 + subscriptionUtils(+test)로 분해, 동작/스타일 불변.
- [ ] `subscriptionUtils.test.js`·`StatusBadge.test.jsx` 통과, 전체 `npm run test:run` green.
- [ ] `npm run lint`·`npm run build` 통과.
- [ ] `/realestate` 4탭 수동 검증 동일 동작.
---
## 7. 리스크 / 오픈 이슈
- **테스트 부재 컴포넌트의 리팩토링**: 순수 추출로 위험 최소화 + build/lint/유닛테스트/수동검증. 로직 한 줄도 바꾸지 않음.
- **거대 import 배선 실수 가능**: 추출 시 각 파일의 사용 심볼을 정확히 import해야 함(누락 시 build 실패로 즉시 검출).
- **후속 follow-up(문서화, 이번 미적용)**: ProfileTab 폼 섹션 분할; local apiPatch ↔ api.js apiPatch dedupe; AnnouncementDetail 추가 분할.
- **다음 서브프로젝트**: RealEstate(909)→PortfolioTab(671)→InstaCards(1013)→MusicStudio(1936), 각 동일 컨벤션 별도 spec/plan.