# 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 유지**, `onCreateSlate` prop으로 KeywordsPanel·ExternalTrendsPanel에 전달. - 렌더: 탭바 + progress 배너 + Cards 탭(PullToRefresh > 헤더 + TriggerPanel + KeywordsPanel + SlatesPanel + PromptTemplatesEditor) + Trends 탭(AccountFocusPanel + ExternalTrendsPanel + PreferenceImpactPanel). --- ## 5. 테스트 (안전망 — 현재 InstaCards 테스트 0) 1. **`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). 회귀 안전망. 2. 나머지 표현/패널 컴포넌트는 로컬 상태+API 위주 → 이번 범위 스모크 최소화(추출 검증은 build+lint+수동). StatusBadge 스모크(선택) 가능. 3. **검증 게이트:** 각 단계 `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:run` green. - [ ] `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, 최대·마지막).