Files
web-page/docs/superpowers/plans/2026-07-10-fe-module-refactor-instacards.md
2026-07-10 11:29:17 +09:00

20 KiB
Raw Blame History

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.jsxfmtDate(2634) 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.jsxusePollTask(4582) 함수 본문 verbatim 이동, import { useState, useEffect, useRef } from 'react';(실제 사용 훅) + import { getInstaTask } from '../../../api'; + export default function usePollTask(onDone) { ... }. 내부 getInstaTask 참조 그대로.

  • Step 6: components/StatusBadge.jsx 작성 — 현 StatusBadge(3642) 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(8499) 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(471521) 함수 본문 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(527637) 함수 본문 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(733817) verbatim 이동. imports: import React, { useState, useCallback, useEffect, useRef } from 'react';(실제 사용) + import { getInstaAssetUrl } from '../../../api';. export default PagesStrip;.

  • Step 2: components/SlateDetail.jsx 작성 — 현 SlateDetail(818940) 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(638732) 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에서 미사용: StatusBadge import(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 탭 우측)"

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(101169) verbatim 이동. imports: import React, { useState, useCallback, useEffect } from 'react'; + import { getInstaPreferences, putInstaPreferences } from '../../../api';(실제 사용). export default AccountFocusPanel;.

  • Step 2: components/ExternalTrendsPanel.jsx 작성 — 현 ExternalTrendsPanel(175267) 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(268300) verbatim 이동. imports: import React, { useState, useEffect } from 'react'; + import { getInstaPreferences } from '../../../api';(실제 사용). export default PreferenceImpactPanel;.

  • Step 4: components/PromptTemplatesEditor.jsx 작성 — 현 PromptTemplatesEditor(9431013) 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·fmtDate import 전부 제거(모두 컴포넌트로 이동).

  • 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 devhttp://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 14 + 각 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).