Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS
7.4 KiB
InstaCards.jsx 분해 리팩토링 — 설계 (FE 모듈 sweep 서브프로젝트 4)
- 작성일: 2026-07-10
- 역할/저장소: FE (
web-ui) - 범위: FE 대형 컴포넌트 분해 sweep의 4번째 서브프로젝트. 컨벤션은 sub-project 1~3에서 확립됨(web-ui/CLAUDE.md "기능 모듈 구조") — 이번은 적용만.
- 성격: behavior-preserving 순수 추출. 로직/JSX/스타일 변경 없음.
1. 배경 & 목표
src/pages/insta/InstaCards.jsx(1013줄, /insta 뉴스 키워드 → AI 카드 자동 생성 페이지)를 기능 모듈 구조로 동작 보존 분해한다. 단일 파일에 메인 shell + 11개 표현/패널 컴포넌트 + 1개 훅(usePollTask) + 헬퍼/상수가 몰려 있다. 각 컴포넌트는 대부분 로컬 상태 + API를 직접 보유하는 자족적 패널이라 그대로 components/로 lift 한다.
비목표: 동작/스타일 변경, 신규 기능, 컴포넌트 내부 재분해(SlateDetail/KeywordsPanel 등), handleCreateSlate의 훅 추출.
2. 현 구조 맵 (InstaCards.jsx 1013줄)
- 헬퍼/상수:
fmtDate(26),CATEGORY_COLORS(170),CATEGORIES(524),KEYWORDS_PER_PAGE(525),PROMPT_NAMES(941) - 소형 표현:
StatusBadge(36–43),TaskStatusBox(84–99) - 훅:
usePollTask(45–82) —{ taskId, task, start(taskId), clear() }반환, TriggerPanel이 사용 - 패널:
AccountFocusPanel(101–169),ExternalTrendsPanel(175–267),PreferenceImpactPanel(268–300),TriggerPanel(471–521),KeywordsPanel(527–637),SlatesPanel(638–732),PagesStrip(733–817),SlateDetail(818–940),PromptTemplatesEditor(943–1013) - 메인
InstaCards()shell(301–468, ~168줄): status 상태 + 탭(cards/trends, URL 동기화switchTab) +slateProgress배너 +handleCreateSlate(createInstaSlate → getInstaTask 인라인 폴링 최대 8분 → 완료 시 cards 탭 전환 + 슬레이트 선택) + 2탭 레이아웃 조합
컴포넌트 중첩: SlatesPanel → SlateDetail → PagesStrip. TriggerPanel → usePollTask + TaskStatusBox.
의존: PullToRefresh(src/components), ../../api 헬퍼(getInstaStatus/createInstaSlate/getInstaTask/getInstaKeywords/getInstaSlates/getInstaSlate/renderInstaSlate/deleteInstaSlate/getInstaAssetUrl/instaPackageUrl/getInstaPrompt/putInstaPrompt/getInstaTrends/instaCollectTrends/getInstaPreferences/putInstaPreferences/instaCollectNews/instaExtractKeywords).
3. 목표 file map
src/pages/insta/
├── InstaCards.jsx # shell(~140줄): 탭바 + 상태헤더 + progress배너 + handleCreateSlate + 2탭 레이아웃 조합
├── InstaCards.css # 변경 없음 (shell에서 import 유지)
├── instaUtils.js # fmtDate(순수) + 다중 사용 상수(CATEGORY_COLORS)
├── instaUtils.test.js # fmtDate 유닛 (신규)
├── hooks/
│ └── usePollTask.js # 태스크 폴링 훅 { taskId, task, start, clear }
└── components/
├── StatusBadge.jsx # ({ status })
├── TaskStatusBox.jsx # ({ task })
├── TriggerPanel.jsx # () — usePollTask×2 + TaskStatusBox
├── KeywordsPanel.jsx # ({ onCreateSlate }) — CATEGORIES/KEYWORDS_PER_PAGE co-locate
├── SlatesPanel.jsx # ({ selectedId, onSelect }) — SlateDetail 렌더
├── SlateDetail.jsx # ({ slate, onDelete, onRender }) — PagesStrip 렌더
├── PagesStrip.jsx # ({ slateId, pageCount })
├── AccountFocusPanel.jsx # ()
├── ExternalTrendsPanel.jsx# ({ onCreateSlate })
├── PreferenceImpactPanel.jsx # ()
└── PromptTemplatesEditor.jsx # () — PROMPT_NAMES co-locate
import 경로 규칙:
components/*.jsx·hooks/*.js→ api'../../../api'; utils'../instaUtils'; 형제 컴포넌트/훅은'./TaskStatusBox'·'../hooks/usePollTask'등 상대 경로.shell(InstaCards.jsx)→ 하위'./components/*', 훅'./hooks/usePollTask', utils'./instaUtils', CSS'./InstaCards.css',PullToRefresh'../../components/PullToRefresh'(기존 유지).- 각 컴포넌트가 실제 사용하는 심볼만 import(빌드/lint가 누락·미사용 검출).
상수 배치: fmtDate + CATEGORY_COLORS(다중 컴포넌트 사용 가능성 → 공유) = instaUtils. CATEGORIES/KEYWORDS_PER_PAGE(KeywordsPanel 단독) → KeywordsPanel.jsx co-locate. PROMPT_NAMES(PromptTemplatesEditor 단독) → PromptTemplatesEditor.jsx co-locate. (구현 시 실제 사용처가 단일이면 co-locate, 2+면 instaUtils로 승격.)
4. shell 책임 (분해 후)
현 InstaCards()의 조합/오케스트레이션만 남긴다(동작 보존):
- 상태:
status,selectedSlateId,slateProgress,activeTab. switchTab(URL 동기화),loadStatus(getInstaStatus),useEffect로드.handleCreateSlate(createInstaSlate → getInstaTask 인라인 폴링 → cards 탭 전환 + 슬레이트 선택 + progress 배너) — shell 유지,onCreateSlateprop으로 KeywordsPanel·ExternalTrendsPanel에 전달.- 렌더: 탭바 + progress 배너 + Cards 탭(PullToRefresh > 헤더 + TriggerPanel + KeywordsPanel + SlatesPanel + PromptTemplatesEditor) + Trends 탭(AccountFocusPanel + ExternalTrendsPanel + PreferenceImpactPanel).
5. 테스트 (안전망 — 현재 InstaCards 테스트 0)
instaUtils.test.js(신규, 필수) —fmtDate(현 구현: falsy →''; 유효 ISO →new Date(iso).toLocaleDateString('ko-KR', {month:'short', day:'numeric', hour:'2-digit', minute:'2-digit'})): falsy 3케이스(''/null/undefined) →''고정; 유효 ISO → 비어있지 않은 문자열(타임존/로케일 의존이라 정확 포맷 대신 non-empty smoke). 회귀 안전망.- 나머지 표현/패널 컴포넌트는 로컬 상태+API 위주 → 이번 범위 스모크 최소화(추출 검증은 build+lint+수동). StatusBadge 스모크(선택) 가능.
- 검증 게이트: 각 단계
npm run build+npm run test:run전체 green(회귀 0) +npm run lint신규/수정 파일 0 에러. 최종 개발서버/insta: 2탭(Cards/Trends) 전환·URL 동기화, 트리거(뉴스 수집·키워드 추출), 키워드 목록·페이지네이션, 슬레이트 생성(progress 배너 → 완료 자동 전환)·목록·상세(SlateDetail)·페이지 스트립(PagesStrip)·삭제·렌더, 프롬프트 편집, Trends 3패널이 리팩토링 전과 동일 동작.
6. 완료 기준
- InstaCards.jsx가 shell(~140줄) + hooks/usePollTask + instaUtils(+test) + components/11로 분해, 동작/스타일 불변.
instaUtils.test.js통과, 전체npm run test:rungreen.npm run lint·npm run build통과./insta수동 검증 동일 동작(2탭·트리거·키워드·슬레이트 생성/상세/렌더/삭제·프롬프트·Trends).
7. 리스크 / 오픈 이슈
- 테스트 부재 컴포넌트 리팩토링: 순수 추출 + build/lint/유닛/수동으로 위험 최소화.
- 컴포넌트 중첩 wiring: SlatesPanel→SlateDetail→PagesStrip, TriggerPanel→usePollTask/TaskStatusBox 상대 import 경로 정확성(빌드가 검출).
- 상수 배치 판정: 실제 사용처 수에 따라 co-locate vs instaUtils 승격(구현 시 grep 확인).
- 후속 follow-up: SlateDetail(~122줄)·KeywordsPanel(~110줄) 내부 분할 여지 기록.
- 다음 서브프로젝트: MusicStudio(1936, 최대·마지막).