# 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// ├── .jsx # 페이지 shell — 라우팅 진입, 탭바/헤더/레이아웃, 하위 조합만 ├── .css # 스타일 (shell에서 1회 import → 전역 클래스 하위 공유) ├── components/ # 표현 단위 (카드/탭/상세/리스트…) 각 단일 책임 ├── hooks/ # 상태·API·부수효과 훅 (선택) └── Utils.js # 순수 헬퍼(포맷/매핑/계산) + 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.