From 25a8be6e238f989d3484496034f8604742c518d5 Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 10 Jul 2026 11:29:17 +0900 Subject: [PATCH] =?UTF-8?q?docs(refactor):=20InstaCards=20=EB=B6=84?= =?UTF-8?q?=ED=95=B4=20=EA=B5=AC=ED=98=84=20=EA=B3=84=ED=9A=8D=20(4=20task?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS --- ...026-07-10-fe-module-refactor-instacards.md | 281 ++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-10-fe-module-refactor-instacards.md diff --git a/docs/superpowers/plans/2026-07-10-fe-module-refactor-instacards.md b/docs/superpowers/plans/2026-07-10-fe-module-refactor-instacards.md new file mode 100644 index 0000000..0cfb272 --- /dev/null +++ b/docs/superpowers/plans/2026-07-10-fe-module-refactor-instacards.md @@ -0,0 +1,281 @@ +# 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)** + +```js +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` 부여: + +```js +// 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 }) => ({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개 제거. 상단에 추가: +```jsx +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** +```bash +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: +```jsx +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: +```jsx +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { getInstaKeywords } from '../../../api'; +``` +(실제 사용 훅/헬퍼만 — 원본이 useState/useEffect/useCallback/useMemo와 getInstaKeywords, onCreateSlate prop 사용.) 상수: +```jsx +const CATEGORIES = ['전체', 'economy', 'psychology', 'celebrity']; +const KEYWORDS_PER_PAGE = 10; +``` +`export default KeywordsPanel;`. + +- [ ] **Step 3: InstaCards.jsx 수정** — `TriggerPanel`/`KeywordsPanel` 인라인 정의 + `CATEGORIES`/`KEYWORDS_PER_PAGE` 상수 제거. 추가: +```jsx +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** +```bash +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: +```jsx +import React from 'react'; +import StatusBadge from './StatusBadge'; +import PagesStrip from './PagesStrip'; +import { getInstaAssetUrl, instaPackageUrl } from '../../../api'; +``` +(실제 사용 심볼만 — 원본 본문이 참조하는 것으로 확정; React 훅 사용 시 추가.) 내부 ``(832)·``(845) 참조 그대로. `export default SlateDetail;`. + +- [ ] **Step 3: components/SlatesPanel.jsx 작성** — 현 `SlatesPanel`(638–732) verbatim 이동. imports: +```jsx +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'; +``` +(실제 사용만 — 원본이 ``(711)·``(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** +```bash +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: +```jsx +import React, { useState, useCallback, useEffect } from 'react'; +import TaskStatusBox from './TaskStatusBox'; +import { fmtDate } from '../instaUtils'; +import { getInstaTrends, instaCollectTrends, getInstaTask } from '../../../api'; +``` +(실제 사용만 — 원본이 ``(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: +```jsx +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` 상수 제거. 추가: +```jsx +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** +```bash +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).