Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS
20 KiB
InstaCards.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: src/pages/insta/InstaCards.jsx(1013줄)를 shell(~140줄) + components/11 + hooks/usePollTask + instaUtils(+test)로 동작 보존 분해한다.
Architecture: behavior-preserving 순수 추출. 각 컴포넌트/훅/헬퍼를 현 정의 그대로(verbatim) 새 파일로 옮기고 import로 연결. 로직/JSX/스타일/prop shape 불변. 컴포넌트를 이름으로 찾아(정의 시그니처 기준) 옮기며, 매 task 후 build+lint+전체 test green(회귀 0)을 유지한다.
Tech Stack: React 18 + Vite + Vitest + @testing-library/react. 설계: docs/superpowers/specs/2026-07-10-fe-module-refactor-instacards-design.md.
Global Constraints
- behavior-preserving: 옮기는 코드는 verbatim. 로직/JSX/className/prop 참조/스타일 변경 금지. 리팩토링은 "정의를 파일로 이동 + import 연결"만.
- line 번호는 pre-refactor(1013줄) 기준 힌트. 추출이 진행되면 InstaCards.jsx 줄이 바뀌므로 컴포넌트/함수 이름·시그니처로 위치를 찾을 것(예:
function KeywordsPanel({ onCreateSlate }) {). - import 경로:
components/*.jsx·hooks/*.js→ api'../../../api', utils'../instaUtils', 형제 컴포넌트'./X', 훅'../hooks/usePollTask'. shell(InstaCards.jsx) → 컴포넌트'./components/X', 훅'./hooks/usePollTask', utils'./instaUtils',PullToRefresh'../../components/PullToRefresh', CSS'./InstaCards.css'. - import 최소·완전: 각 파일은 실제 참조하는 심볼만 import. 매 task에서 shell의 이제 안 쓰는 api/컴포넌트 import 제거.
npm run lint(no-unused-vars)와npm run build가 누락·미사용을 검출 → 둘 다 0 에러로 몰 것. - 각 컴포넌트는 default export.
- 검증 게이트(매 task):
npm run test:run전체 green(회귀 0, 시작 165) +npm run lint신규/수정 파일 0 에러 +npm run build✓ built. - 단일 사용 상수 co-locate:
CATEGORIES/KEYWORDS_PER_PAGE→ KeywordsPanel.jsx,PROMPT_NAMES→ PromptTemplatesEditor.jsx,CATEGORY_COLORS→ ExternalTrendsPanel.jsx. 다중 사용fmtDate→ instaUtils.js.
파일 구조 (분해 후)
src/pages/insta/
├── InstaCards.jsx # shell(~140줄): 탭바+상태헤더+progress배너+handleCreateSlate+2탭 조합
├── InstaCards.css # 변경 없음
├── instaUtils.js # fmtDate
├── instaUtils.test.js # fmtDate 유닛
├── hooks/usePollTask.js # { taskId, task, start, clear }
└── components/
├── StatusBadge.jsx TaskStatusBox.jsx TriggerPanel.jsx KeywordsPanel.jsx
├── SlatesPanel.jsx SlateDetail.jsx PagesStrip.jsx
└── AccountFocusPanel.jsx ExternalTrendsPanel.jsx PreferenceImpactPanel.jsx PromptTemplatesEditor.jsx
shell이 직접 렌더하는 7 컴포넌트: TriggerPanel, KeywordsPanel, SlatesPanel, PromptTemplatesEditor, AccountFocusPanel, ExternalTrendsPanel, PreferenceImpactPanel. (StatusBadge/TaskStatusBox/SlateDetail/PagesStrip은 다른 컴포넌트가 렌더 → shell import 아님.)
컴포넌트 참조 그래프: TriggerPanel→usePollTask+TaskStatusBox / ExternalTrendsPanel→TaskStatusBox+CATEGORY_COLORS+fmtDate / SlatesPanel→SlateDetail+StatusBadge+fmtDate / SlateDetail→PagesStrip+StatusBadge / PromptTemplatesEditor→fmtDate+PROMPT_NAMES.
Task 1: 기반 — instaUtils(+test) + usePollTask 훅 + 리프 컴포넌트(StatusBadge, TaskStatusBox)
Files:
- Create:
src/pages/insta/instaUtils.js,src/pages/insta/instaUtils.test.js - Create:
src/pages/insta/hooks/usePollTask.js - Create:
src/pages/insta/components/StatusBadge.jsx,src/pages/insta/components/TaskStatusBox.jsx - Modify:
src/pages/insta/InstaCards.jsx(정의 4개 제거 + import)
Interfaces:
-
Produces:
fmtDate(iso) → string;usePollTask(onDone?) → { taskId, task, start(id), clear() };StatusBadge({ status });TaskStatusBox({ task })(모두 default export, usePollTask는 named 아님 default). -
Step 1: instaUtils.test.js 작성(RED)
import { describe, it, expect } from 'vitest';
import { fmtDate } from './instaUtils';
describe('instaUtils.fmtDate', () => {
it('returns empty string for falsy input', () => {
expect(fmtDate('')).toBe('');
expect(fmtDate(null)).toBe('');
expect(fmtDate(undefined)).toBe('');
});
it('returns a non-empty string for a valid ISO date (locale/tz-dependent format)', () => {
const out = fmtDate('2026-07-10T09:30:00Z');
expect(typeof out).toBe('string');
expect(out.length).toBeGreaterThan(0);
});
});
-
Step 2: RED 확인 — Run:
npx vitest run src/pages/insta/instaUtils.test.js. Expected: FAIL(fmtDate/모듈 미존재). -
Step 3: instaUtils.js 작성(GREEN) — 현
InstaCards.jsx의fmtDate(26–34) verbatim,export부여:
// src/pages/insta/instaUtils.js
export function fmtDate(iso) {
if (!iso) return '';
return new Date(iso).toLocaleDateString('ko-KR', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
-
Step 4: GREEN 확인 — Run:
npx vitest run src/pages/insta/instaUtils.test.js. Expected: PASS(2 tests). -
Step 5: hooks/usePollTask.js 작성 — 현
InstaCards.jsx의usePollTask(45–82) 함수 본문 verbatim 이동,import { useState, useEffect, useRef } from 'react';(실제 사용 훅) +import { getInstaTask } from '../../../api';+export default function usePollTask(onDone) { ... }. 내부getInstaTask참조 그대로. -
Step 6: components/StatusBadge.jsx 작성 — 현
StatusBadge(36–42) verbatim:import React from 'react'; const StatusBadge = ({ status }) => (<span className={\ic-status-badge ic-status-badge--${status || 'draft'}`}>{status || 'draft'}); export default StatusBadge;` (JSX는 원본과 동일하게). -
Step 7: components/TaskStatusBox.jsx 작성 — 현
TaskStatusBox(84–99) verbatim 이동.import React from 'react';+ 원본이 참조하는 심볼(예: 상태 텍스트/StatusBadge? — 원본 본문 확인해 실제 참조만 import; 순수 표현이면 React만).export default TaskStatusBox;. -
Step 8: InstaCards.jsx 수정 —
fmtDate/usePollTask/StatusBadge/TaskStatusBox인라인 정의 4개 제거. 상단에 추가:
import { fmtDate } from './instaUtils';
import usePollTask from './hooks/usePollTask';
import StatusBadge from './components/StatusBadge';
import TaskStatusBox from './components/TaskStatusBox';
아직 인라인인 컴포넌트(TriggerPanel/ExternalTrendsPanel/SlatesPanel/SlateDetail/PromptTemplatesEditor)가 이 심볼들을 참조 → 동일 모듈 scope에서 import된 심볼 사용(동작 보존). api import 블록에서 이제 shell 파일 어디서도 안 쓰는 것이 있으면 제거(단 아직 인라인 컴포넌트가 쓰는 것은 유지). getInstaTask는 usePollTask로 이동했지만 shell handleCreateSlate가 여전히 직접 사용 → 유지.
-
Step 9: 검증 — Run:
npm run test:run(전체 green, ≥165+2=167),npm run lint(0 에러),npm run build(✓ built). -
Step 10: Commit
git add src/pages/insta/instaUtils.js src/pages/insta/instaUtils.test.js src/pages/insta/hooks/usePollTask.js src/pages/insta/components/StatusBadge.jsx src/pages/insta/components/TaskStatusBox.jsx src/pages/insta/InstaCards.jsx
git commit -m "refactor(insta): instaUtils(fmtDate)+usePollTask 훅+StatusBadge/TaskStatusBox 추출"
Task 2: Cards 탭 좌측 — TriggerPanel + KeywordsPanel
Files:
- Create:
src/pages/insta/components/TriggerPanel.jsx,src/pages/insta/components/KeywordsPanel.jsx - Modify:
src/pages/insta/InstaCards.jsx
Interfaces:
-
Consumes:
usePollTask(Task1),TaskStatusBox(Task1). -
Produces:
TriggerPanel(),KeywordsPanel({ onCreateSlate })(default export). -
Step 1: components/TriggerPanel.jsx 작성 — 현
TriggerPanel(471–521) 함수 본문 verbatim 이동. imports:
import React from 'react';
import usePollTask from '../hooks/usePollTask';
import TaskStatusBox from './TaskStatusBox';
import { instaCollectNews, instaExtractKeywords } from '../../../api';
내부 usePollTask()×2, TaskStatusBox, instaCollectNews/instaExtractKeywords 참조 그대로. export default TriggerPanel;.
- Step 2: components/KeywordsPanel.jsx 작성 — 현
KeywordsPanel(527–637) 함수 본문 verbatim 이동 + 상수CATEGORIES(524)·KEYWORDS_PER_PAGE(525)를 이 파일 상단에 co-locate. imports:
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { getInstaKeywords } from '../../../api';
(실제 사용 훅/헬퍼만 — 원본이 useState/useEffect/useCallback/useMemo와 getInstaKeywords, onCreateSlate prop 사용.) 상수:
const CATEGORIES = ['전체', 'economy', 'psychology', 'celebrity'];
const KEYWORDS_PER_PAGE = 10;
export default KeywordsPanel;.
- Step 3: InstaCards.jsx 수정 —
TriggerPanel/KeywordsPanel인라인 정의 +CATEGORIES/KEYWORDS_PER_PAGE상수 제거. 추가:
import TriggerPanel from './components/TriggerPanel';
import KeywordsPanel from './components/KeywordsPanel';
이제 shell에서 안 쓰는 api import 제거: instaCollectNews/instaExtractKeywords(TriggerPanel로 이동), getInstaKeywords(KeywordsPanel로 이동)가 shell 다른 곳에서 안 쓰이면 shell import에서 삭제. TaskStatusBox shell import는 아직 인라인 ExternalTrendsPanel(245)이 사용 → 유지. usePollTask shell import는 이제 shell 인라인에서 미사용(TriggerPanel 이동) → 제거.
-
Step 4: 검증 —
npm run test:run(green),npm run lint(0),npm run build(✓). -
Step 5: Commit
git add src/pages/insta/components/TriggerPanel.jsx src/pages/insta/components/KeywordsPanel.jsx src/pages/insta/InstaCards.jsx
git commit -m "refactor(insta): TriggerPanel/KeywordsPanel 추출 (Cards 탭 좌측)"
Task 3: Cards 탭 우측 (중첩 트리) — SlatesPanel + SlateDetail + PagesStrip
Files:
- Create:
src/pages/insta/components/SlatesPanel.jsx,src/pages/insta/components/SlateDetail.jsx,src/pages/insta/components/PagesStrip.jsx - Modify:
src/pages/insta/InstaCards.jsx
Interfaces:
-
Consumes:
StatusBadge(Task1),fmtDate(Task1). -
Produces:
SlatesPanel({ selectedId, onSelect }),SlateDetail({ slate, onDelete, onRender }),PagesStrip({ slateId, pageCount })(default export). -
Step 1: components/PagesStrip.jsx 작성 — 현
PagesStrip(733–817) verbatim 이동. imports:import React, { useState, useCallback, useEffect, useRef } from 'react';(실제 사용) +import { getInstaAssetUrl } from '../../../api';.export default PagesStrip;. -
Step 2: components/SlateDetail.jsx 작성 — 현
SlateDetail(818–940) verbatim 이동. imports:
import React from 'react';
import StatusBadge from './StatusBadge';
import PagesStrip from './PagesStrip';
import { getInstaAssetUrl, instaPackageUrl } from '../../../api';
(실제 사용 심볼만 — 원본 본문이 참조하는 것으로 확정; React 훅 사용 시 추가.) 내부 <StatusBadge>(832)·<PagesStrip>(845) 참조 그대로. export default SlateDetail;.
- Step 3: components/SlatesPanel.jsx 작성 — 현
SlatesPanel(638–732) verbatim 이동. imports:
import React, { useState, useEffect, useCallback } from 'react';
import StatusBadge from './StatusBadge';
import SlateDetail from './SlateDetail';
import { fmtDate } from '../instaUtils';
import { getInstaSlates, getInstaSlate, getInstaAssetUrl, renderInstaSlate, deleteInstaSlate } from '../../../api';
(실제 사용만 — 원본이 <StatusBadge>(711)·<SlateDetail>(722)·fmtDate(710)·slate 목록/상세/렌더/삭제 api 사용. onDelete/onRender 핸들러가 renderInstaSlate/deleteInstaSlate를 어디서 호출하는지 원본 따라 배치.) export default SlatesPanel;.
-
Step 4: InstaCards.jsx 수정 —
SlatesPanel/SlateDetail/PagesStrip인라인 정의 제거. 추가:import SlatesPanel from './components/SlatesPanel';. (SlateDetail/PagesStrip은 shell이 직접 렌더 안 함 → shell import 아님.) 이제 shell에서 미사용:StatusBadgeimport(SlatesPanel/SlateDetail 이동으로 shell 인라인 미사용) → 제거; 관련 api import(getInstaSlates/getInstaSlate/getInstaAssetUrl/renderInstaSlate/deleteInstaSlate/instaPackageUrl 등)가 shell 인라인에서 미사용이면 제거. -
Step 5: 검증 —
npm run test:run(green),npm run lint(0),npm run build(✓). -
Step 6: Commit
git add src/pages/insta/components/SlatesPanel.jsx src/pages/insta/components/SlateDetail.jsx src/pages/insta/components/PagesStrip.jsx src/pages/insta/InstaCards.jsx
git commit -m "refactor(insta): SlatesPanel/SlateDetail/PagesStrip 추출 (Cards 탭 우측)"
Task 4: Trends 탭 3패널 + PromptTemplatesEditor + shell 확정
Files:
- Create:
src/pages/insta/components/AccountFocusPanel.jsx,src/pages/insta/components/ExternalTrendsPanel.jsx,src/pages/insta/components/PreferenceImpactPanel.jsx,src/pages/insta/components/PromptTemplatesEditor.jsx - Modify:
src/pages/insta/InstaCards.jsx
Interfaces:
-
Consumes:
TaskStatusBox(Task1),fmtDate(Task1). -
Produces:
AccountFocusPanel(),ExternalTrendsPanel({ onCreateSlate }),PreferenceImpactPanel(),PromptTemplatesEditor()(default export). -
Step 1: components/AccountFocusPanel.jsx 작성 — 현
AccountFocusPanel(101–169) verbatim 이동. imports:import React, { useState, useCallback, useEffect } from 'react';+import { getInstaPreferences, putInstaPreferences } from '../../../api';(실제 사용).export default AccountFocusPanel;. -
Step 2: components/ExternalTrendsPanel.jsx 작성 — 현
ExternalTrendsPanel(175–267) verbatim 이동 + 상수CATEGORY_COLORS(170)를 이 파일 상단에 co-locate. imports:
import React, { useState, useCallback, useEffect } from 'react';
import TaskStatusBox from './TaskStatusBox';
import { fmtDate } from '../instaUtils';
import { getInstaTrends, instaCollectTrends, getInstaTask } from '../../../api';
(실제 사용만 — 원본이 <TaskStatusBox>(245)·fmtDate(238)·CATEGORY_COLORS(221,252)·trends api·인라인 폴링 getInstaTask 사용.) 상수 const CATEGORY_COLORS = { ... };(원본 170 블록 verbatim). export default ExternalTrendsPanel;.
-
Step 3: components/PreferenceImpactPanel.jsx 작성 — 현
PreferenceImpactPanel(268–300) verbatim 이동. imports:import React, { useState, useEffect } from 'react';+import { getInstaPreferences } from '../../../api';(실제 사용).export default PreferenceImpactPanel;. -
Step 4: components/PromptTemplatesEditor.jsx 작성 — 현
PromptTemplatesEditor(943–1013) verbatim 이동 + 상수PROMPT_NAMES(941)를 이 파일 상단에 co-locate. imports:
import React, { useState, useEffect } from 'react';
import { fmtDate } from '../instaUtils';
import { getInstaPrompt, putInstaPrompt } from '../../../api';
(실제 사용만 — 원본이 fmtDate(984)·prompt api·PROMPT_NAMES 사용.) 상수 const PROMPT_NAMES = ['slate_writer', 'category_seeds'];. export default PromptTemplatesEditor;.
- Step 5: InstaCards.jsx 최종 확정(shell) — 4개 인라인 정의 +
CATEGORY_COLORS/PROMPT_NAMES상수 제거. 추가:
import AccountFocusPanel from './components/AccountFocusPanel';
import ExternalTrendsPanel from './components/ExternalTrendsPanel';
import PreferenceImpactPanel from './components/PreferenceImpactPanel';
import PromptTemplatesEditor from './components/PromptTemplatesEditor';
shell import 최종 정리 — shell이 실제 쓰는 것만 남긴다:
-
React:
import React, { useState, useEffect, useCallback } from 'react';(shell이 실제 사용하는 훅만; useRef/useMemo는 shell 미사용이면 제거) -
import PullToRefresh from '../../components/PullToRefresh';(유지) -
7 컴포넌트 import(TriggerPanel/KeywordsPanel/SlatesPanel/PromptTemplatesEditor/AccountFocusPanel/ExternalTrendsPanel/PreferenceImpactPanel)
-
api: shell이 직접 쓰는 것만 —
import { getInstaStatus, createInstaSlate, getInstaTask } from '../../../api';(loadStatus=getInstaStatus, handleCreateSlate=createInstaSlate+getInstaTask). 나머지 api·TaskStatusBox·StatusBadge·usePollTask·fmtDateimport 전부 제거(모두 컴포넌트로 이동). -
import './InstaCards.css';(유지) 결과InstaCards.jsx는 shell(~140줄). -
Step 6: 검증 — Run:
npm run test:run(전체 green, 회귀 0),npm run lint(0 에러, 미사용 import 0),npm run build(✓ built),wc -l src/pages/insta/InstaCards.jsx(~140줄 확인). -
Step 7: Commit
git add src/pages/insta/components/AccountFocusPanel.jsx src/pages/insta/components/ExternalTrendsPanel.jsx src/pages/insta/components/PreferenceImpactPanel.jsx src/pages/insta/components/PromptTemplatesEditor.jsx src/pages/insta/InstaCards.jsx
git commit -m "refactor(insta): Trends 3패널+PromptTemplatesEditor 추출 → InstaCards shell 확정"
- Step 8: Manual verification (dev server) —
npm run dev→http://localhost:3007/insta. 확인: 2탭(Cards/Trends) 전환 + URL?tab=trends동기화 / 트리거(뉴스 수집·키워드 추출 버튼 + 스피너 + TaskStatusBox) / 키워드 목록·카테고리·페이지네이션 / 슬레이트 생성(키워드 클릭 → progress 배너 → 완료 시 Cards 탭 자동 전환 + 상세) / 슬레이트 목록·상세(SlateDetail)·페이지 스트립(PagesStrip 스크롤·키보드)·삭제·렌더 / 프롬프트 편집(저장) / Trends 3패널(AccountFocus·ExternalTrends 수집·PreferenceImpact)이 리팩토링 전과 동일 동작.
Self-Review 결과
Spec coverage (설계 §1–§7):
- §3 file map(instaUtils/usePollTask/11 컴포넌트/shell) → Task 1(utils+hook+2리프) / Task 2(2) / Task 3(3) / Task 4(4+shell) ✅ (11 = 2+2+3+4)
- §4 shell 책임(탭·progress·handleCreateSlate 유지, 7 컴포넌트 조합) → Task 4 Step 5 ✅
- §5 테스트(instaUtils fmtDate 유닛 + build/lint/전체/수동) → Task 1 Step 1–4 + 각 task 검증 + Task 4 Step 8 ✅
- §6 완료기준 → 각 task 검증 + Task 4 line count ✅
- §3 상수 배치(CATEGORIES/KEYWORDS_PER_PAGE→KeywordsPanel, PROMPT_NAMES→PromptTemplatesEditor, CATEGORY_COLORS→ExternalTrendsPanel co-locate, fmtDate→instaUtils) → 각 Task 명시 ✅
Placeholder scan: fmtDate 테스트/구현 완전 명시. 리프트 컴포넌트는 "현 정의(이름·line 힌트) verbatim + 명시 import/export/상수 co-locate"로 지정(원본 코드 존재 → 재현 대신 위치+연결 지정, 서브프로젝트 1~3 동일 컨벤션). 일부 컴포넌트 import는 "원본이 참조하는 심볼만(build/lint 검출)"으로 위임 — 리프트 특성상 정확 목록은 원본 참조와 동일. ✅
Type consistency: usePollTask 반환 { taskId, task, start, clear } ↔ TriggerPanel 사용 일치. 컴포넌트 props(status/task/onCreateSlate/selectedId/onSelect/slate/onDelete/onRender/slateId/pageCount) ↔ shell/부모 전달 일치. 경로(components→../../../api·../instaUtils·./sibling·../hooks/usePollTask) 일관. StatusBadge/TaskStatusBox는 Task1 추출 → 소비처(Task2/3/4)가 직접 import, shell은 소비 인라인 남아있는 동안만 브릿지 import 유지 후 제거. ✅
참고: Task 순서 의존(Task1 리프/훅/유틸 → Task2/3/4가 import). behavior-preserving라 컴포넌트 RED는 "신규 파일 미존재"로 성립, GREEN은 추출 후 전체 회귀 0. fmtDate만 명시적 TDD(RED→GREEN).