前回の記事では、AIがプロジェクト全体の構造を理解し、影響範囲を把握できるようにするための基盤作りについて紹介しました。
今回は続編として、実際に要件定義から実装までのプロセスをAIとどのように進めていくかに焦点を当てます。
単にコードを書く補助としてAIを使うのではなく、要件の整理や仕様の明確化、さらには実装フェーズに至るまで、AIを“チームメンバーの1人”としてどう活用できるか。
その具体的な流れや工夫、そして運用上のポイントを、実際の検証を交えながら紹介していきます。
要件定義から実装・テストまで
1人で悩む時間を減らして、より効率的に開発を進めるために以下のものをAIに手伝ってもらいます。
- 要件の整理: 「何を作りたいのか」を明確にする
- 設計の検討: 「どうやって作るのか」を考える
- 実装の計画を立てる: タスクを細分化
- 実装: AIと一緒にコードを書く
- テスト: 正しく動くか確認する
今回はこれらを実現するためにRulesとCommandsを用意しました。
1.要件の整理
前回の記事で紹介したサンプルプロジェクト(ToDoリスト)では、ページリロードを行うことでデータがリセットされる設計になっています。
そこで、新たなタスクとして「データを保持できるようにする」という要件が与えられたとします。
このタスクをAIに依頼する際は、次のように指示をします。
/spec-driven-dev タスクデータを永続化できるようにしたい
このプロンプトを投げると、AIは指示の内容に基づいて成果物として以下を作成してくれます。
# タスクデータ永続化機能 - 要件定義
## 1. 目的の明確化
### 達成したいこと
- タスク、タグ、リストのデータをブラウザを閉じても保持できるようにする
- ユーザーが作成したタスクが失われないようにする
- アプリケーションの再読み込み時に、最後の状態を復元できるようにする
### 解決したい課題
- 現在は`useState`でメモリ上にのみデータを保持しているため、ページリロードやブラウザを閉じるとデータが失われる
- サンプルデータのハイドレーション機能はあるが、ユーザーが作成したデータの永続化機能がない
## 2. 現状分析
### 既存コードの状態
#### 状態管理構造
- **TaskContext** (`src/context/TaskContext.jsx`)
- tasks配列をuseStateで管理(メモリ上のみ)
- 操作: addTask, toggleTask, deleteTask, completeAllTasks, deleteCompletedTasks
- タグ関連: updateTasksWithEditedTag, updateTasksWithDeletedTag
- **TagContext** (`src/context/TagContext.jsx`)
- tags配列をuseStateで管理(メモリ上のみ)
- tasksから自動的にタグを収集
- **ListContext** (`src/context/ListContext.jsx`)
- taskLists配列をuseStateで管理(メモリ上のみ)
- 操作: addTaskList, updateTaskList, deleteTaskList
- フィルタリング: getFilteredTasks
#### データハイドレーション機能
- `DataHydrationService` (`src/common/utils/DataHydrationService.js`)が存在
- 環境変数`VITE_ENABLE_DATA_HYDRATION`でON/OFF切り替え可能
- `initialData.json`からサンプルデータをロード
- メソッド: getInitialTasks(), getInitialTags(), getInitialTaskLists(), shouldHydrate()
#### データ構造
```javascript
// Task
{
id: number,
title: string,
description: string,
isCompleted: boolean,
tags: string[]
}
// Tag
string[] // 文字列の配列
// TaskList
{
id: string,
title: string,
filters: [
{ type: 'tag' | 'completed', value: any }
]
}
```
#### 技術スタック
- React 19.1.0(Hooks使用)
- Vite 7.0.6(ビルドツール)
- localStorage利用可能(ブラウザ標準API)
- 外部データベースライブラリは未使用
## 3. 機能要件の定義
### MUST(必須機能)
#### M-1: LocalStorageへの自動保存
- タスクの追加・更新・削除時に自動的にlocalStorageに保存
- タグの追加・更新・削除時に自動的にlocalStorageに保存
- リストの追加・更新・削除時に自動的にlocalStorageに保存
- 完了条件: 全てのCRUD操作後にlocalStorageに反映されること
#### M-2: アプリケーション起動時の自動復元
- ブラウザ起動時にlocalStorageから最後の状態を復元
- localStorageにデータがない場合の初期化ロジック:
- VITE_ENABLE_DATA_HYDRATION=trueの場合: サンプルデータで初期化
- それ以外: 空の状態で初期化(tasksリストのみdefaultリストを作成)
- 完了条件: ブラウザリロード後も前回の状態が復元されること
#### M-3: 既存機能との互換性
- 現在の全てのCRUD操作が正常に動作すること
- DataHydrationServiceの機能と共存すること
- 既存のコンポーネントコードを変更しないこと(Context層のみの変更)
- 完了条件: 既存の全テストがパスすること
### SHOULD(推奨機能)
#### S-1: データマイグレーション対応
- データ構造にバージョン番号を付与
- 古いバージョンのデータを検出して新バージョンに変換
- 完了条件: バージョン情報がデータに含まれ、移行ロジックが実装されること
#### S-2: エラーハンドリング
- localStorage容量超過時のエラーハンドリングとユーザー通知
- JSONパースエラー時のフォールバック処理(初期状態に戻す)
- localStorageが利用できない環境での適切な対応
- 完了条件: エラー発生時もアプリケーションがクラッシュしないこと
#### S-3: パフォーマンス最適化
- 保存処理のdebounce(短時間に複数回の保存を防ぐ)
- 大量データ(1000件)でもパフォーマンス劣化しないこと
- 完了条件: タスク1000件でも操作がスムーズに行えること
### COULD(可能であれば実装したい機能)
#### C-1: データエクスポート/インポート機能
- JSON形式でのデータエクスポート
- インポートによるデータ復元
- 完了条件: エクスポート/インポートボタンが動作すること
#### C-2: ストレージ使用量の表示
- 現在のlocalStorage使用量を表示
- 容量の残りを可視化
- 完了条件: 設定画面に使用量が表示されること
### WON'T(対象外)
#### W-1: サーバーサイドでの永続化
- バックエンドAPIの実装は含まない
- 理由: フロントエンドのみで完結するデモアプリケーションのため
#### W-2: 認証・マルチユーザー対応
- ユーザー管理機能は含まない
- 理由: 単一ユーザー向けのアプリケーションのため
#### W-3: リアルタイム同期
- WebSocketなどのリアルタイム同期は含まない
- 理由: localStorageは単一ブラウザ環境のため
## 4. 非機能要件
### パフォーマンス要件
- データ保存時の遅延は50ms以内
- アプリケーション起動時のデータ読み込みは100ms以内
- 大量データ(タスク1000件程度)でも快適に動作
### セキュリティ要件
- localStorageは同一オリジンのみアクセス可能(ブラウザ標準動作)
- XSS対策としてデータのサニタイズ(必要に応じて)
- 機密情報は保存しない前提
### 保守性・拡張性要件
- 専用のStorageServiceを作成し、永続化ロジックを分離
- 将来的に別の永続化手段(IndexedDB、サーバーサイドなど)への移行を容易にする
- DataHydrationServiceのパターンを参考に設計
- 各Contextから独立したモジュールとして実装
### ユーザビリティ要件
- ユーザーは永続化を意識する必要がない(自動保存)
- データが失われることがない信頼性
- エラーが発生した場合も適切なフィードバックを提供
## 5. 制約条件
### 技術的制約
- ブラウザのlocalStorage容量制限(通常5-10MB)
- localStorageは同期的なAPIのため、大量データでブロッキングの可能性
- JSONシリアライズ可能なデータのみ保存可能
- 既存のReact Context APIを使用した状態管理を維持
### リソース制約
- 既存コードへの影響を最小限にする
- 新規ライブラリの追加は最小限にする(できればゼロ)
- Context層のみの変更でコンポーネント層は変更しない
### スケジュール制約
- 既存機能を壊さないこと
- 段階的な実装が可能な設計にすること
- 各タスクは1-3日程度で完了できる粒度にすること
## 6. 優先度の考え方
1. **最優先**: M-1(自動保存)とM-2(自動復元)の基本機能
2. **次点**: S-2(エラーハンドリング)の実装
3. **その後**: S-1(マイグレーション)、S-3(パフォーマンス最適化)
4. **余裕があれば**: C-1(エクスポート/インポート)
---
**作成日**: 2025-10-23
**プロジェクト名**: task-persistence
**機能名**: タスクデータ永続化
出力内容を見てみると、以下のような整理になっています。
- 原因:データがメモリ(
useState)にしかなく、リロードで消える。 - 解決策:データをブラウザのlocalStorageに保存する。
- 実装方針:localStorageへの保存・読込を行うStorageServiceを新しく作り、既存のContextから呼び出して使う。
これで方向性が固まりました。
出力内容を確認後、問題点を洗い出し、これをたたき台として質疑応答を通じて原因と方針を決定します。その後、内容のブラッシュアップを行います。
2.設計の検討
要件定義が完了したら、次のステップは設計です。
ここではAIに対して、以下のコマンドを実行します。
/continue-design
実際に出力された結果がこちらです。
# タスクデータ永続化機能 - 設計書
## 1. アーキテクチャ設計
### 1.1 システム全体の構造
```
┌─────────────────────────────────────────────────────────┐
│ UI Components │
│ (TaskItem, TaskList, TagManager, TaskBoard, etc.) │
└─────────────────────┬───────────────────────────────────┘
│ useContext
↓
┌─────────────────────────────────────────────────────────┐
│ React Context Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │TaskContext │ │TagContext │ │ListContext │ │
│ │(tasks state) │ │(tags state) │ │(lists state) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
└─────────┼──────────────────┼──────────────────┼─────────┘
│ │ │
│ useEffect │ useEffect │ useEffect
│ (auto save) │ (auto save) │ (auto save)
↓ ↓ ↓
┌─────────────────────────────────────────────────────────┐
│ StorageService (新規作成) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ saveTasks() saveTags() saveTaskLists() │ │
│ │ loadTasks() loadTags() loadTaskLists() │ │
│ │ clearStorage() isStorageAvailable() │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────┬───────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────┐
│ Browser localStorage API │
│ Keys: 'taskDashboard_tasks' │
│ 'taskDashboard_tags' │
│ 'taskDashboard_taskLists' │
│ 'taskDashboard_version' │
└─────────────────────────────────────────────────────────┘
```
### 1.2 主要なコンポーネント/モジュールの構成
#### 新規作成モジュール
1. **StorageService** (`src/common/utils/StorageService.js`)
- localStorageへの読み書きを抽象化
- データのシリアライズ/デシリアライズ
- エラーハンドリング
- バージョン管理
#### 修正対象モジュール
1. **TaskContext** (`src/context/TaskContext.jsx`)
- useEffectでtasksの変更を監視し、自動保存
- 初期化時にStorageServiceからデータをロード
2. **TagContext** (`src/context/TagContext.jsx`)
- useEffectでtagsの変更を監視し、自動保存
- 初期化時にStorageServiceからデータをロード
3. **ListContext** (`src/context/ListContext.jsx`)
- useEffectでtaskListsの変更を監視し、自動保存
- 初期化時にStorageServiceからデータをロード
### 1.3 コンポーネント間の関係性と責務
| コンポーネント | 責務 | 依存関係 |
|---------------|------|---------|
| **StorageService** | localStorage操作の抽象化、データの永続化・復元 | なし(localStorage APIのみ) |
| **TaskContext** | タスク状態管理、CRUD操作、StorageService呼び出し | StorageService |
| **TagContext** | タグ状態管理、CRUD操作、StorageService呼び出し | StorageService、TaskContext |
| **ListContext** | リスト状態管理、CRUD操作、StorageService呼び出し | StorageService |
| **UI Components** | UIレンダリング、ユーザー操作のハンドリング | 各Context(変更なし) |
### 1.4 既存コードとの整合性
- **DataHydrationService**のパターンを踏襲:
- 同じディレクトリ(`src/common/utils/`)に配置
- オブジェクトリテラル形式のサービス設計
- JSDocコメントで文書化
- export const形式
- **既存Contextの構造を維持**:
- 提供するAPIは変更なし
- useStateとuseEffectの組み合わせを継続使用
- コンポーネント層は一切変更不要
## 2. データ設計
### 2.1 localStorageのキー設計
プレフィックス: `taskDashboard_` (アプリケーション名を含める)
| キー | データ型 | 説明 |
|------|---------|------|
| `taskDashboard_tasks` | JSON文字列 | タスクの配列 |
| `taskDashboard_tags` | JSON文字列 | タグの配列 |
| `taskDashboard_taskLists` | JSON文字列 | タスクリストの配列 |
| `taskDashboard_version` | 文字列 | データ構造のバージョン(例: "1.0.0") |
### 2.2 データモデル
#### Task
```javascript
{
id: number, // 一意のID
title: string, // タスクのタイトル
description: string, // 説明
isCompleted: boolean, // 完了状態
tags: string[] // タグの配列
}
```
#### Tag
```javascript
string // タグ名(文字列の配列として保存)
```
#### TaskList
```javascript
{
id: string, // 一意のID
title: string, // リストのタイトル
filters: [ // フィルター設定
{
type: 'tag' | 'completed',
value: any
}
]
}
```
#### Version Information
```javascript
{
version: "1.0.0", // セマンティックバージョニング
lastUpdated: number // タイムスタンプ
}
```
### 2.3 データの流れ
#### 保存フロー
```
User Action → Context.setState → useEffect検知
→ StorageService.save() → JSON.stringify
→ localStorage.setItem
```
#### 読み込みフロー
```
App起動 → Context初期化 → useState初期値設定
→ StorageService.load() → localStorage.getItem
→ JSON.parse → 検証 → 状態に反映
```
#### 初期化ロジックのフロー
```
App起動
↓
StorageService.loadTasks()を呼び出し
↓
localStorageにデータあり?
YES → データを復元して返す
NO → DataHydrationService.shouldHydrate()を確認
YES → サンプルデータを返す
NO → 空配列/デフォルト値を返す
```
### 2.4 データの永続化方法
- **自動保存**: 状態変更時にuseEffectで自動的に保存
- **同期的処理**: localStorageは同期APIのため、即座に保存完了
- **エラー時**: console.errorでログ出力し、アプリケーションは継続動作
## 3. インターフェース設計
### 3.1 StorageService API設計
```javascript
/**
* localStorageを使用したデータ永続化サービス
*/
export const StorageService = {
// ========== タスク関連 ==========
/**
* タスクをlocalStorageに保存
* @param {Array} tasks - タスクの配列
* @returns {boolean} 成功時true、失敗時false
*/
saveTasks: (tasks) => boolean,
/**
* タスクをlocalStorageから読み込み
* @returns {Array|null} タスクの配列、データがない場合null
*/
loadTasks: () => Array|null,
// ========== タグ関連 ==========
/**
* タグをlocalStorageに保存
* @param {Array} tags - タグの配列
* @returns {boolean} 成功時true、失敗時false
*/
saveTags: (tags) => boolean,
/**
* タグをlocalStorageから読み込み
* @returns {Array|null} タグの配列、データがない場合null
*/
loadTags: () => Array|null,
// ========== タスクリスト関連 ==========
/**
* タスクリストをlocalStorageに保存
* @param {Array} taskLists - タスクリストの配列
* @returns {boolean} 成功時true、失敗時false
*/
saveTaskLists: (taskLists) => boolean,
/**
* タスクリストをlocalStorageから読み込み
* @returns {Array|null} タスクリストの配列、データがない場合null
*/
loadTaskLists: () => Array|null,
// ========== バージョン管理 ==========
/**
* データバージョンを保存
* @param {string} version - バージョン番号
* @returns {boolean} 成功時true、失敗時false
*/
saveVersion: (version) => boolean,
/**
* データバージョンを読み込み
* @returns {string|null} バージョン番号、データがない場合null
*/
loadVersion: () => string|null,
// ========== ユーティリティ ==========
/**
* 全てのアプリケーションデータをlocalStorageから削除
* @returns {boolean} 成功時true、失敗時false
*/
clearAll: () => boolean,
/**
* localStorageが利用可能かチェック
* @returns {boolean} 利用可能な場合true
*/
isAvailable: () => boolean,
/**
* localStorageの使用容量を取得(概算)
* @returns {number} 使用バイト数
*/
getStorageSize: () => number
};
```
### 3.2 既存Contextとの互換性
既存のContext APIは**一切変更なし**。コンポーネント層への影響はゼロ。
内部実装のみ以下を追加:
- `useState`の初期化関数でStorageServiceからデータをロード
- `useEffect`で状態変更時にStorageServiceへ保存
## 4. 処理フロー設計
### 4.1 アプリケーション起動時の処理
```
1. App.jsxのレンダリング開始
↓
2. TaskProvider初期化
├─ useState初期化関数実行
│ ├─ StorageService.loadTasks()呼び出し
│ ├─ データあり → そのまま使用
│ └─ データなし → DataHydrationService.shouldHydrate()確認
│ ├─ true → サンプルデータ使用
│ └─ false → 空配列
└─ 初期状態セット
↓
3. TagProvider初期化(同様の流れ)
↓
4. ListProvider初期化(同様の流れ)
↓
5. UIレンダリング完了
```
### 4.2 タスク追加時の処理
```
1. ユーザーがタスク追加ボタンをクリック
↓
2. GlobalTaskForm.addTask()呼び出し
↓
3. TaskContext.addTask()実行
├─ 新しいタスクオブジェクト作成
├─ setTasks([...tasks, newTask])
└─ 状態更新
↓
4. useEffect([tasks])が発火
├─ StorageService.saveTasks(tasks)呼び出し
├─ JSON.stringify(tasks)
└─ localStorage.setItem('taskDashboard_tasks', json)
↓
5. UIが再レンダリング
↓
6. 完了(データはlocalStorageに永続化済み)
```
### 4.3 タスク更新/削除時の処理
更新・削除も追加と同様のフローで、`setTasks`実行後に`useEffect`が発火して自動保存。
### 4.4 エラーハンドリング
#### localStorageが利用できない場合
```javascript
try {
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
return true;
} catch (e) {
console.error('localStorage is not available:', e);
return false;
}
```
#### 容量超過エラー
```javascript
try {
localStorage.setItem(key, value);
return true;
} catch (e) {
if (e.name === 'QuotaExceededError') {
console.error('localStorage quota exceeded');
// 将来的にはユーザーに通知する機能を追加可能
}
return false;
}
```
#### JSONパースエラー
```javascript
try {
const data = localStorage.getItem(key);
if (!data) return null;
return JSON.parse(data);
} catch (e) {
console.error('Failed to parse stored data:', e);
// 破損したデータは無視し、nullを返す
return null;
}
```
### 4.5 状態管理
各Contextで以下のパターンを実装:
```javascript
// 初期化時にlocalStorageから読み込み
const [tasks, setTasks] = useState(() => {
const stored = StorageService.loadTasks();
if (stored !== null) {
return stored;
}
// localStorageにデータがない場合
return DataHydrationService.shouldHydrate()
? DataHydrationService.getInitialTasks()
: [];
});
// 状態変更時に自動保存
useEffect(() => {
StorageService.saveTasks(tasks);
}, [tasks]);
```
## 5. 技術選定
### 5.1 localStorage を選択した理由
| 選択肢 | メリット | デメリット | 採用判断 |
|--------|---------|----------|---------|
| **localStorage** | ・標準API(外部ライブラリ不要)<br>・シンプルな実装<br>・同期的なAPI<br>・5-10MBの容量 | ・同期処理(大量データで遅延の可能性)<br>・文字列のみ保存可能 | ✅ **採用** |
| IndexedDB | ・大容量対応<br>・非同期API<br>・複雑なクエリ可能 | ・複雑なAPI<br>・オーバースペック<br>・実装コスト高 | ❌ 不採用 |
| sessionStorage | ・標準API | ・タブを閉じるとデータ消失 | ❌ 不採用 |
| サーバーDB | ・多端末同期可能<br>・大容量 | ・バックエンド実装が必要<br>・認証が必要<br>・対象外機能 | ❌ 対象外 |
**結論**: localStorageが要件に最適
- タスク管理アプリとして必要十分な容量
- 外部ライブラリ不要で保守性が高い
- 既存コードとの統合が容易
- フロントエンドのみで完結
### 5.2 使用するライブラリやフレームワーク
**新規追加ライブラリ: なし**
既存の技術スタックのみで実装:
- React 19.1.0(useState、useEffect)
- JavaScript標準API(localStorage、JSON)
### 5.3 既存の技術スタックとの整合性
| 既存技術 | 整合性 |
|---------|-------|
| React Context | ✅ Context内部でuseEffectを追加するのみ |
| DataHydrationService | ✅ 同じ設計パターンを踏襲 |
| 関数コンポーネント + Hooks | ✅ useStateとuseEffectで実装 |
| Vitest + Testing Library | ✅ 同じテストパターンで実装可能 |
## 6. 実装詳細の設計
### 6.1 新規作成するファイル
#### `src/common/utils/StorageService.js`
```javascript
/**
* Browser localStorage wrapper service for Task Dashboard
* Provides persistent storage for tasks, tags, and task lists
*/
const STORAGE_KEYS = {
TASKS: 'taskDashboard_tasks',
TAGS: 'taskDashboard_tags',
TASK_LISTS: 'taskDashboard_taskLists',
VERSION: 'taskDashboard_version'
};
const CURRENT_VERSION = '1.0.0';
/**
* Generic function to save data to localStorage
* @private
*/
const saveData = (key, data) => {
try {
if (!isAvailable()) {
console.warn('localStorage is not available');
return false;
}
const jsonString = JSON.stringify(data);
localStorage.setItem(key, jsonString);
return true;
} catch (error) {
if (error.name === 'QuotaExceededError') {
console.error('localStorage quota exceeded:', error);
} else {
console.error(`Failed to save data to ${key}:`, error);
}
return false;
}
};
/**
* Generic function to load data from localStorage
* @private
*/
const loadData = (key) => {
try {
if (!isAvailable()) {
return null;
}
const jsonString = localStorage.getItem(key);
if (jsonString === null) {
return null;
}
return JSON.parse(jsonString);
} catch (error) {
console.error(`Failed to load data from ${key}:`, error);
return null;
}
};
/**
* Check if localStorage is available
* @private
*/
const isAvailable = () => {
try {
const testKey = '__storage_test__';
localStorage.setItem(testKey, 'test');
localStorage.removeItem(testKey);
return true;
} catch (e) {
return false;
}
};
export const StorageService = {
// Tasks
saveTasks: (tasks) => saveData(STORAGE_KEYS.TASKS, tasks),
loadTasks: () => loadData(STORAGE_KEYS.TASKS),
// Tags
saveTags: (tags) => saveData(STORAGE_KEYS.TAGS, tags),
loadTags: () => loadData(STORAGE_KEYS.TAGS),
// Task Lists
saveTaskLists: (taskLists) => saveData(STORAGE_KEYS.TASK_LISTS, taskLists),
loadTaskLists: () => loadData(STORAGE_KEYS.TASK_LISTS),
// Version
saveVersion: (version) => saveData(STORAGE_KEYS.VERSION, {
version,
lastUpdated: Date.now()
}),
loadVersion: () => {
const versionData = loadData(STORAGE_KEYS.VERSION);
return versionData ? versionData.version : null;
},
// Utilities
clearAll: () => {
try {
Object.values(STORAGE_KEYS).forEach(key => {
localStorage.removeItem(key);
});
return true;
} catch (error) {
console.error('Failed to clear storage:', error);
return false;
}
},
isAvailable: isAvailable,
getStorageSize: () => {
let total = 0;
Object.values(STORAGE_KEYS).forEach(key => {
const value = localStorage.getItem(key);
if (value) {
total += value.length + key.length;
}
});
return total;
}
};
```
#### `src/common/utils/StorageService.test.js`
```javascript
import { describe, test, expect, beforeEach, vi } from 'vitest';
import { StorageService } from './StorageService';
describe('StorageService', () => {
beforeEach(() => {
localStorage.clear();
});
describe('Tasks', () => {
test('should save and load tasks', () => {
const tasks = [
{ id: 1, title: 'Test Task', isCompleted: false, tags: [] }
];
const saved = StorageService.saveTasks(tasks);
expect(saved).toBe(true);
const loaded = StorageService.loadTasks();
expect(loaded).toEqual(tasks);
});
test('should return null when no tasks stored', () => {
const loaded = StorageService.loadTasks();
expect(loaded).toBeNull();
});
});
describe('Tags', () => {
test('should save and load tags', () => {
const tags = ['work', 'personal'];
const saved = StorageService.saveTags(tags);
expect(saved).toBe(true);
const loaded = StorageService.loadTags();
expect(loaded).toEqual(tags);
});
});
describe('TaskLists', () => {
test('should save and load task lists', () => {
const lists = [
{ id: 'default', title: 'All Tasks', filters: [] }
];
const saved = StorageService.saveTaskLists(lists);
expect(saved).toBe(true);
const loaded = StorageService.loadTaskLists();
expect(loaded).toEqual(lists);
});
});
describe('Utilities', () => {
test('should clear all storage', () => {
StorageService.saveTasks([{ id: 1 }]);
StorageService.saveTags(['tag']);
const cleared = StorageService.clearAll();
expect(cleared).toBe(true);
expect(StorageService.loadTasks()).toBeNull();
expect(StorageService.loadTags()).toBeNull();
});
test('should check storage availability', () => {
expect(StorageService.isAvailable()).toBe(true);
});
test('should calculate storage size', () => {
StorageService.saveTasks([{ id: 1, title: 'Task' }]);
const size = StorageService.getStorageSize();
expect(size).toBeGreaterThan(0);
});
});
});
```
### 6.2 修正が必要な既存ファイル
#### `src/context/TaskContext.jsx` の修正内容
**修正箇所1: インポート追加**
```javascript
// 既存のインポート
import React, { createContext, useState, useContext, useEffect } from 'react';
import { DataHydrationService } from '../common/utils/DataHydrationService';
// ↓ 追加
import { StorageService } from '../common/utils/StorageService';
```
**修正箇所2: useState初期化の変更**
```javascript
// 修正前
const [tasks, setTasks] = useState(() => {
return DataHydrationService.shouldHydrate()
? DataHydrationService.getInitialTasks()
: [];
});
// 修正後
const [tasks, setTasks] = useState(() => {
// まずlocalStorageから読み込み
const storedTasks = StorageService.loadTasks();
if (storedTasks !== null) {
return storedTasks;
}
// localStorageにデータがない場合、ハイドレーションを確認
return DataHydrationService.shouldHydrate()
? DataHydrationService.getInitialTasks()
: [];
});
```
**修正箇所3: 自動保存のuseEffect追加**
```javascript
// 既存のuseEffect(stats更新)の後に追加
// タスクをlocalStorageに自動保存
useEffect(() => {
StorageService.saveTasks(tasks);
}, [tasks]);
```
#### `src/context/TagContext.jsx` の修正内容
**修正箇所1: インポート追加**
```javascript
import { StorageService } from '../common/utils/StorageService';
```
**修正箇所2: useState初期化の変更**
```javascript
// 修正前
const [tags, setTags] = useState(() => {
return DataHydrationService.shouldHydrate()
? DataHydrationService.getInitialTags()
: [];
});
// 修正後
const [tags, setTags] = useState(() => {
const storedTags = StorageService.loadTags();
if (storedTags !== null) {
return storedTags;
}
return DataHydrationService.shouldHydrate()
? DataHydrationService.getInitialTags()
: [];
});
```
**修正箇所3: 自動保存のuseEffect追加**
```javascript
// タグをlocalStorageに自動保存
useEffect(() => {
StorageService.saveTags(tags);
}, [tags]);
```
#### `src/context/ListContext.jsx` の修正内容
**修正箇所1: インポート追加**
```javascript
import { StorageService } from '../common/utils/StorageService';
```
**修正箇所2: useState初期化の変更**
```javascript
// 修正前
const [taskLists, setTaskLists] = useState(() => {
return DataHydrationService.shouldHydrate()
? DataHydrationService.getInitialTaskLists()
: [{ id: 'default', title: 'All Tasks', filters: [] }];
});
// 修正後
const [taskLists, setTaskLists] = useState(() => {
const storedLists = StorageService.loadTaskLists();
if (storedLists !== null) {
return storedLists;
}
return DataHydrationService.shouldHydrate()
? DataHydrationService.getInitialTaskLists()
: [{ id: 'default', title: 'All Tasks', filters: [] }];
});
```
**修正箇所3: 自動保存のuseEffect追加**
```javascript
// タスクリストをlocalStorageに自動保存
useEffect(() => {
StorageService.saveTaskLists(taskLists);
}, [taskLists]);
```
### 6.3 各関数/メソッドのシグネチャと役割
#### StorageServiceの各関数
| 関数名 | 引数 | 戻り値 | 役割 |
|--------|------|--------|------|
| `saveTasks` | `tasks: Array` | `boolean` | タスク配列をlocalStorageに保存 |
| `loadTasks` | なし | `Array\|null` | localStorageからタスク配列を読み込み |
| `saveTags` | `tags: Array` | `boolean` | タグ配列をlocalStorageに保存 |
| `loadTags` | なし | `Array\|null` | localStorageからタグ配列を読み込み |
| `saveTaskLists` | `taskLists: Array` | `boolean` | タスクリスト配列をlocalStorageに保存 |
| `loadTaskLists` | なし | `Array\|null` | localStorageからタスクリスト配列を読み込み |
| `saveVersion` | `version: string` | `boolean` | バージョン情報を保存 |
| `loadVersion` | なし | `string\|null` | バージョン情報を読み込み |
| `clearAll` | なし | `boolean` | 全データを削除 |
| `isAvailable` | なし | `boolean` | localStorage利用可否チェック |
| `getStorageSize` | なし | `number` | 使用容量を取得(バイト数) |
### 6.4 疑似コード/フローチャート
#### 初期化フロー(TaskContextの例)
```
START TaskProvider初期化
↓
useState初期化関数を実行
↓
StorageService.loadTasks()を呼び出し
↓
localStorageからデータ取得
↓
データは存在する?
├─ YES → パース成功?
│ ├─ YES → そのデータを使用
│ └─ NO → null返却 → 次へ
└─ NO → null返却 → 次へ
↓
DataHydrationService.shouldHydrate()?
├─ YES → getInitialTasks()からサンプルデータ取得
└─ NO → 空配列 []
↓
初期状態をセット
↓
useEffectリスナー登録
↓
END
```
#### 自動保存フロー
```
START タスク追加/更新/削除
↓
setTasks(newTasks)実行
↓
Reactが状態更新を検知
↓
useEffect([tasks])が発火
↓
StorageService.saveTasks(tasks)呼び出し
↓
JSON.stringify(tasks)
↓
try {
localStorage.setItem(key, json)
成功 → return true
} catch (error) {
QuotaExceededError?
├─ YES → console.error('容量超過')
└─ NO → console.error('保存失敗')
return false
}
↓
END
```
### 6.5 エッジケースの考慮
| エッジケース | 対処方法 |
|-------------|---------|
| localStorageが無効 | `isAvailable()`で事前チェック、無効時はメモリ上のみで動作 |
| 容量超過 | QuotaExceededErrorをキャッチしてログ出力、アプリは継続動作 |
| JSONパースエラー | try-catchでキャッチ、nullを返して初期状態にフォールバック |
| 破損したデータ | パース失敗時はnullとして扱い、初期化ロジックにフォールバック |
| 空の配列 vs null | nullは「データなし」、[]は「空リスト」として明確に区別 |
| 初回起動 | localStorageが空→ハイドレーションチェック→適切な初期値 |
| ブラウザ互換性 | try-catchとisAvailable()で確実にチェック |
### 6.6 使用するSerenaツール(実装フェーズ用)
| 作業内容 | 使用するツール |
|---------|--------------|
| StorageService.js作成 | `write` または `mcp_serena_insert_before_symbol` |
| TaskContext修正(import追加) | `mcp_serena_insert_before_symbol` |
| TaskContext修正(useState変更) | `mcp_serena_replace_symbol_body` |
| TaskContext修正(useEffect追加) | `mcp_serena_insert_after_symbol` |
| TagContext修正 | 同上 |
| ListContext修正 | 同上 |
| テストファイル作成 | `write` |
## 7. リスク管理
### 7.1 技術的リスク
| リスク | 影響度 | 対策 |
|--------|-------|------|
| localStorage容量超過 | 中 | エラーハンドリング実装、将来的に古いデータの削除機能検討 |
| ブラウザ互換性 | 低 | 主要ブラウザは対応済み、isAvailable()で事前チェック |
| データ破損 | 中 | try-catchで適切に処理、初期状態にフォールバック |
| パフォーマンス劣化 | 低 | JSON操作は軽量、debounceは将来検討 |
### 7.2 設計上のリスク
| リスク | 影響度 | 対策 |
|--------|-------|------|
| 既存機能の破壊 | 高 | 既存テストを全て実行して確認、Context APIは変更なし |
| データマイグレーション | 中 | バージョン管理を実装、将来的な移行準備 |
| 過度な自動保存 | 低 | 現状は問題なし、将来debounce検討可能 |
## 8. 完成時の動作イメージ
### 8.1 ユーザーシナリオ1: 通常利用
```
1. ユーザーがアプリを開く
→ localStorageから前回のデータを自動復元
2. タスクを3つ追加
→ 各追加ごとに自動的にlocalStorageに保存
3. ブラウザを閉じる
→ 特別な操作は不要(既に保存済み)
4. 翌日アプリを開く
→ 昨日追加した3つのタスクが表示されている ✅
```
### 8.2 ユーザーシナリオ2: 初回起動
```
1. 初めてアプリを開く
→ localStorageは空
2. VITE_ENABLE_DATA_HYDRATION=trueの場合
→ サンプルデータが表示される
3. タスクを1つ追加
→ サンプルデータ + 新規タスクがlocalStorageに保存
4. ブラウザをリロード
→ サンプルデータ + 新規タスクが表示される ✅
```
### 8.3 ユーザーシナリオ3: エラー発生時
```
1. localStorage容量が満杯の状態
2. タスクを追加
→ 保存に失敗
→ console.errorにログ出力
→ アプリケーションは正常に動作継続 ✅
→ UIには新しいタスクが表示される(メモリ上では存在)
```
---
**作成日**: 2025-10-23
**プロジェクト名**: task-persistence
**フェーズ**: 設計
**次フェーズ**: 実装計画
この設計書には、「何を」「どこに」「どのように」実装すべきかが明確に示されています。
どういう理由でこの選択肢を取ったのかも書かれているので、内容に問題がないことを確認できたら実装計画を作成してもらいます。
3.実装の計画を立てる
設計が完了したら、次は実装計画フェーズに進みます。
ここでは、設計内容を実際のタスクに分解し、優先順位をつけて実装の順序を決定します。
/continue-implementation-plan
実際に出力されたものがこちらです。
# タスクデータ永続化機能 - 実装計画
## 概要
本実装計画は、設計書に基づいてタスクデータ永続化機能を段階的に実装するためのロードマップです。各タスクは独立して実装可能で、1〜3日程度で完了できる粒度に分解されています。
## タスク一覧
| ID | タスク名 | 優先度 | 見積工数 | 依存タスク | ステータス |
|----|---------|--------|---------|-----------|----------|
| TASK-001 | StorageService実装 | High | 2-3時間 | なし | 未着手 |
| TASK-002 | StorageServiceテスト実装 | High | 1-2時間 | TASK-001 | 未着手 |
| TASK-003 | TaskContext修正 | High | 1時間 | TASK-001 | 未着手 |
| TASK-004 | TagContext修正 | High | 1時間 | TASK-001 | 未着手 |
| TASK-005 | ListContext修正 | High | 1時間 | TASK-001 | 未着手 |
| TASK-006 | 既存テスト実行と修正 | High | 1-2時間 | TASK-003, TASK-004, TASK-005 | 未着手 |
| TASK-007 | E2Eテストと動作確認 | Medium | 1-2時間 | TASK-006 | 未着手 |
**総見積工数**: 8-12時間(1-2日)
## 依存関係グラフ
```
TASK-001 (StorageService実装)
├─→ TASK-002 (テスト実装)
├─→ TASK-003 (TaskContext修正)
├─→ TASK-004 (TagContext修正)
└─→ TASK-005 (ListContext修正)
↓
TASK-006 (既存テスト実行)
↓
TASK-007 (E2Eテスト)
```
## 実装順序
### フェーズ1: コア機能実装(MUST)
1. **TASK-001**: StorageService実装
2. **TASK-002**: StorageServiceテスト実装
### フェーズ2: Context統合(MUST)
3. **TASK-003**: TaskContext修正
4. **TASK-004**: TagContext修正
5. **TASK-005**: ListContext修正
### フェーズ3: 検証(MUST)
6. **TASK-006**: 既存テスト実行と修正
7. **TASK-007**: E2Eテストと動作確認
---
## TASK-001: StorageService実装
### タスク概要
localStorageを抽象化したStorageServiceを実装します。DataHydrationServiceと同様のパターンで、タスク、タグ、タスクリストの永続化機能を提供します。
### 優先度
**High** - 全ての機能の基盤となるため最優先
### 実装内容
1. `src/common/utils/StorageService.js`を新規作成
2. 以下の機能を実装:
- `saveTasks()`, `loadTasks()` - タスクの保存・読み込み
- `saveTags()`, `loadTags()` - タグの保存・読み込み
- `saveTaskLists()`, `loadTaskLists()` - タスクリストの保存・読み込み
- `saveVersion()`, `loadVersion()` - バージョン管理
- `clearAll()` - 全データ削除
- `isAvailable()` - localStorage利用可否チェック
- `getStorageSize()` - 使用容量計算
3. エラーハンドリング実装:
- QuotaExceededError(容量超過)
- JSONパースエラー
- localStorage利用不可
### 完了条件(Definition of Done)
- [ ] StorageService.jsファイルが作成されている
- [ ] 全ての公開APIが実装されている
- [ ] JSDocコメントが全関数に記載されている
- [ ] エラーハンドリングが適切に実装されている
- [ ] DataHydrationServiceと同じコーディングスタイルに従っている
- [ ] ESLintエラーがない
### ファイル作成・修正リスト
- **新規作成**: `src/common/utils/StorageService.js`
### 使用するツール
- `write` - 新規ファイル作成
### 実装コード(設計書より)
```javascript
/**
* Browser localStorage wrapper service for Task Dashboard
* Provides persistent storage for tasks, tags, and task lists
*/
const STORAGE_KEYS = {
TASKS: 'taskDashboard_tasks',
TAGS: 'taskDashboard_tags',
TASK_LISTS: 'taskDashboard_taskLists',
VERSION: 'taskDashboard_version'
};
const CURRENT_VERSION = '1.0.0';
/**
* Generic function to save data to localStorage
* @private
*/
const saveData = (key, data) => {
try {
if (!isAvailable()) {
console.warn('localStorage is not available');
return false;
}
const jsonString = JSON.stringify(data);
localStorage.setItem(key, jsonString);
return true;
} catch (error) {
if (error.name === 'QuotaExceededError') {
console.error('localStorage quota exceeded:', error);
} else {
console.error(`Failed to save data to ${key}:`, error);
}
return false;
}
};
/**
* Generic function to load data from localStorage
* @private
*/
const loadData = (key) => {
try {
if (!isAvailable()) {
return null;
}
const jsonString = localStorage.getItem(key);
if (jsonString === null) {
return null;
}
return JSON.parse(jsonString);
} catch (error) {
console.error(`Failed to load data from ${key}:`, error);
return null;
}
};
/**
* Check if localStorage is available
* @private
*/
const isAvailable = () => {
try {
const testKey = '__storage_test__';
localStorage.setItem(testKey, 'test');
localStorage.removeItem(testKey);
return true;
} catch (e) {
return false;
}
};
export const StorageService = {
// Tasks
saveTasks: (tasks) => saveData(STORAGE_KEYS.TASKS, tasks),
loadTasks: () => loadData(STORAGE_KEYS.TASKS),
// Tags
saveTags: (tags) => saveData(STORAGE_KEYS.TAGS, tags),
loadTags: () => loadData(STORAGE_KEYS.TAGS),
// Task Lists
saveTaskLists: (taskLists) => saveData(STORAGE_KEYS.TASK_LISTS, taskLists),
loadTaskLists: () => loadData(STORAGE_KEYS.TASK_LISTS),
// Version
saveVersion: (version) => saveData(STORAGE_KEYS.VERSION, {
version,
lastUpdated: Date.now()
}),
loadVersion: () => {
const versionData = loadData(STORAGE_KEYS.VERSION);
return versionData ? versionData.version : null;
},
// Utilities
clearAll: () => {
try {
Object.values(STORAGE_KEYS).forEach(key => {
localStorage.removeItem(key);
});
return true;
} catch (error) {
console.error('Failed to clear storage:', error);
return false;
}
},
isAvailable: isAvailable,
getStorageSize: () => {
let total = 0;
Object.values(STORAGE_KEYS).forEach(key => {
const value = localStorage.getItem(key);
if (value) {
total += value.length + key.length;
}
});
return total;
}
};
```
### 注意点・リスク
- **localStorage利用可否**: プライベートブラウジングモードではlocalStorageが無効な場合があるため、`isAvailable()`で必ず確認
- **容量制限**: localStorageは通常5-10MBだが、ブラウザにより異なる。QuotaExceededErrorの適切なハンドリングが必須
- **JSONシリアライズ**: 循環参照や特殊なオブジェクトは保存できないが、現在のデータモデルでは問題なし
- **コーディングスタイル**: DataHydrationService.jsと同じスタイルで実装すること
### 見積工数
**2-3時間**
### 依存タスク
なし
---
## TASK-002: StorageServiceテスト実装
### タスク概要
StorageServiceの単体テストを実装します。既存のテストパターン(Vitest + Testing Library)に従い、全ての公開APIをテストします。
### 優先度
**High** - コア機能の品質保証
### 実装内容
1. `src/common/utils/StorageService.test.js`を新規作成
2. 以下のテストケースを実装:
- **Tasks**: 保存・読み込み、データなし時の挙動
- **Tags**: 保存・読み込み、データなし時の挙動
- **TaskLists**: 保存・読み込み、データなし時の挙動
- **Version**: 保存・読み込み
- **Utilities**: clearAll, isAvailable, getStorageSize
- **エラーケース**: localStorage無効時、容量超過時(モック)
3. beforeEachでlocalStorageをクリア
### 完了条件(Definition of Done)
- [ ] StorageService.test.jsファイルが作成されている
- [ ] 全ての公開APIがテストされている
- [ ] 正常系・異常系の両方がカバーされている
- [ ] テストカバレッジが90%以上
- [ ] 全てのテストがパスする
- [ ] テストコードが既存のテストパターンに従っている
### ファイル作成・修正リスト
- **新規作成**: `src/common/utils/StorageService.test.js`
### 使用するツール
- `write` - 新規ファイル作成
### 実装コード(設計書より)
```javascript
import { describe, test, expect, beforeEach, vi } from 'vitest';
import { StorageService } from './StorageService';
describe('StorageService', () => {
beforeEach(() => {
localStorage.clear();
});
describe('Tasks', () => {
test('should save and load tasks', () => {
const tasks = [
{ id: 1, title: 'Test Task', isCompleted: false, tags: [] }
];
const saved = StorageService.saveTasks(tasks);
expect(saved).toBe(true);
const loaded = StorageService.loadTasks();
expect(loaded).toEqual(tasks);
});
test('should return null when no tasks stored', () => {
const loaded = StorageService.loadTasks();
expect(loaded).toBeNull();
});
});
describe('Tags', () => {
test('should save and load tags', () => {
const tags = ['work', 'personal'];
const saved = StorageService.saveTags(tags);
expect(saved).toBe(true);
const loaded = StorageService.loadTags();
expect(loaded).toEqual(tags);
});
});
describe('TaskLists', () => {
test('should save and load task lists', () => {
const lists = [
{ id: 'default', title: 'All Tasks', filters: [] }
];
const saved = StorageService.saveTaskLists(lists);
expect(saved).toBe(true);
const loaded = StorageService.loadTaskLists();
expect(loaded).toEqual(lists);
});
});
describe('Version', () => {
test('should save and load version', () => {
const saved = StorageService.saveVersion('1.0.0');
expect(saved).toBe(true);
const loaded = StorageService.loadVersion();
expect(loaded).toBe('1.0.0');
});
});
describe('Utilities', () => {
test('should clear all storage', () => {
StorageService.saveTasks([{ id: 1 }]);
StorageService.saveTags(['tag']);
const cleared = StorageService.clearAll();
expect(cleared).toBe(true);
expect(StorageService.loadTasks()).toBeNull();
expect(StorageService.loadTags()).toBeNull();
});
test('should check storage availability', () => {
expect(StorageService.isAvailable()).toBe(true);
});
test('should calculate storage size', () => {
StorageService.saveTasks([{ id: 1, title: 'Task' }]);
const size = StorageService.getStorageSize();
expect(size).toBeGreaterThan(0);
});
});
});
```
### 注意点・リスク
- **localStorage.clear()**: 各テスト前に必ずクリアすること
- **モック不要**: localStorageはJSDOMで自動的にモック化される
- **エラーケーステスト**: 容量超過のテストは実装が複雑なため、初期実装では省略可能
### 見積工数
**1-2時間**
### 依存タスク
- TASK-001(StorageService実装)
---
## TASK-003: TaskContext修正
### タスク概要
TaskContextにStorageServiceを統合し、タスクの自動保存・自動復元機能を実装します。
### 優先度
**High** - メイン機能(タスク管理)の永続化
### 実装内容
1. インポート追加:`import { StorageService } from '../common/utils/StorageService';`
2. useState初期化ロジック修正:
- まずStorageService.loadTasks()を試行
- データがあればそれを使用
- なければ既存のハイドレーションロジックを使用
3. useEffect追加:
- tasksの変更を監視
- StorageService.saveTasks(tasks)を呼び出し
### 完了条件(Definition of Done)
- [ ] StorageServiceがインポートされている
- [ ] useState初期化でlocalStorageから読み込み
- [ ] useEffectで自動保存が実装されている
- [ ] 既存のCRUD操作が正常に動作する
- [ ] ESLintエラーがない
- [ ] ブラウザリロード後にタスクが復元される
### ファイル修正リスト
- **修正**: `src/context/TaskContext.jsx`
### 使用するSerenaツール
1. `mcp_serena_insert_before_symbol` - インポート文追加(TaskContextシンボルの前)
2. `mcp_serena_find_symbol` - TaskProvider内のuseState部分を特定
3. `search_replace` または 手動編集 - useState初期化ロジック変更
4. `mcp_serena_insert_after_symbol` - useEffect追加(既存のuseEffectの後)
### 修正箇所
#### 修正1: インポート追加
```javascript
// 既存のインポート
import React, { createContext, useState, useContext, useEffect } from 'react';
import { DataHydrationService } from '../common/utils/DataHydrationService';
// ↓ 追加
import { StorageService } from '../common/utils/StorageService';
```
#### 修正2: useState初期化の変更
```javascript
// 修正前
const [tasks, setTasks] = useState(() => {
return DataHydrationService.shouldHydrate()
? DataHydrationService.getInitialTasks()
: [];
});
// 修正後
const [tasks, setTasks] = useState(() => {
// まずlocalStorageから読み込み
const storedTasks = StorageService.loadTasks();
if (storedTasks !== null) {
return storedTasks;
}
// localStorageにデータがない場合、ハイドレーションを確認
return DataHydrationService.shouldHydrate()
? DataHydrationService.getInitialTasks()
: [];
});
```
#### 修正3: useEffect追加
```javascript
// 既存のuseEffect(stats更新)の後に追加
// タスクをlocalStorageに自動保存
useEffect(() => {
StorageService.saveTasks(tasks);
}, [tasks]);
```
### 注意点・リスク
- **初回レンダリング**: useState初期化時に即座に保存が走るが、これは意図した動作
- **パフォーマンス**: タスク数が多い場合でも、JSON.stringifyは十分高速
- **既存機能**: 全てのCRUD操作が変更なく動作することを確認
### 見積工数
**1時間**
### 依存タスク
- TASK-001(StorageService実装)
---
## TASK-004: TagContext修正
### タスク概要
TagContextにStorageServiceを統合し、タグの自動保存・自動復元機能を実装します。
### 優先度
**High** - タグ機能の永続化
### 実装内容
1. インポート追加:`import { StorageService } from '../common/utils/StorageService';`
2. useState初期化ロジック修正:
- まずStorageService.loadTags()を試行
- データがあればそれを使用
- なければ既存のハイドレーションロジックを使用
3. useEffect追加:
- tagsの変更を監視
- StorageService.saveTags(tags)を呼び出し
### 完了条件(Definition of Done)
- [ ] StorageServiceがインポートされている
- [ ] useState初期化でlocalStorageから読み込み
- [ ] useEffectで自動保存が実装されている
- [ ] 既存のタグ操作が正常に動作する
- [ ] ESLintエラーがない
- [ ] ブラウザリロード後にタグが復元される
### ファイル修正リスト
- **修正**: `src/context/TagContext.jsx`
### 使用するSerenaツール
1. `mcp_serena_insert_before_symbol` - インポート文追加
2. `search_replace` - useState初期化ロジック変更
3. `mcp_serena_insert_after_symbol` - useEffect追加
### 修正箇所
#### 修正1: インポート追加
```javascript
import { StorageService } from '../common/utils/StorageService';
```
#### 修正2: useState初期化の変更
```javascript
// 修正前
const [tags, setTags] = useState(() => {
return DataHydrationService.shouldHydrate()
? DataHydrationService.getInitialTags()
: [];
});
// 修正後
const [tags, setTags] = useState(() => {
const storedTags = StorageService.loadTags();
if (storedTags !== null) {
return storedTags;
}
return DataHydrationService.shouldHydrate()
? DataHydrationService.getInitialTags()
: [];
});
```
#### 修正3: useEffect追加
```javascript
// タグをlocalStorageに自動保存
useEffect(() => {
StorageService.saveTags(tags);
}, [tags]);
```
### 注意点・リスク
- **タスクとの連携**: TagContextはTaskContextに依存しているが、永続化は独立して動作
- **タグの自動収集**: tasksからのタグ収集ロジックは既存のまま維持
### 見積工数
**1時間**
### 依存タスク
- TASK-001(StorageService実装)
---
## TASK-005: ListContext修正
### タスク概要
ListContextにStorageServiceを統合し、タスクリストの自動保存・自動復元機能を実装します。
### 優先度
**High** - リスト機能の永続化
### 実装内容
1. インポート追加:`import { StorageService } from '../common/utils/StorageService';`
2. useState初期化ロジック修正:
- まずStorageService.loadTaskLists()を試行
- データがあればそれを使用
- なければ既存のハイドレーションロジックを使用
3. useEffect追加:
- taskListsの変更を監視
- StorageService.saveTaskLists(taskLists)を呼び出し
### 完了条件(Definition of Done)
- [ ] StorageServiceがインポートされている
- [ ] useState初期化でlocalStorageから読み込み
- [ ] useEffectで自動保存が実装されている
- [ ] 既存のリスト操作が正常に動作する
- [ ] ESLintエラーがない
- [ ] ブラウザリロード後にタスクリストが復元される
### ファイル修正リスト
- **修正**: `src/context/ListContext.jsx`
### 使用するSerenaツール
1. `mcp_serena_insert_before_symbol` - インポート文追加
2. `search_replace` - useState初期化ロジック変更
3. `mcp_serena_insert_after_symbol` - useEffect追加
### 修正箇所
#### 修正1: インポート追加
```javascript
import { StorageService } from '../common/utils/StorageService';
```
#### 修正2: useState初期化の変更
```javascript
// 修正前
const [taskLists, setTaskLists] = useState(() => {
return DataHydrationService.shouldHydrate()
? DataHydrationService.getInitialTaskLists()
: [{ id: 'default', title: 'All Tasks', filters: [] }];
});
// 修正後
const [taskLists, setTaskLists] = useState(() => {
const storedLists = StorageService.loadTaskLists();
if (storedLists !== null) {
return storedLists;
}
return DataHydrationService.shouldHydrate()
? DataHydrationService.getInitialTaskLists()
: [{ id: 'default', title: 'All Tasks', filters: [] }];
});
```
#### 修正3: useEffect追加
```javascript
// タスクリストをlocalStorageに自動保存
useEffect(() => {
StorageService.saveTaskLists(taskLists);
}, [taskLists]);
```
### 注意点・リスク
- **デフォルトリスト**: localStorageが空の場合でも、デフォルトの'All Tasks'リストは必ず作成される
- **フィルター設定**: フィルター設定も含めて永続化されることを確認
### 見積工数
**1時間**
### 依存タスク
- TASK-001(StorageService実装)
---
## TASK-006: 既存テスト実行と修正
### タスク概要
既存の全テストを実行し、永続化機能の追加により影響を受けるテストを修正します。
### 優先度
**High** - 既存機能の破壊防止
### 実装内容
1. `npm test`または`npm run test`で全テストを実行
2. 失敗したテストを特定
3. 失敗原因を分析:
- localStorage関連のモックが必要か
- テストの前提条件が変わったか
4. 必要に応じてテストを修正:
- beforeEachでlocalStorage.clear()を追加
- StorageServiceをモック化
5. 全テストがパスすることを確認
### 完了条件(Definition of Done)
- [ ] 全ての既存テストが実行されている
- [ ] 失敗したテストが特定されている
- [ ] 失敗原因が分析されている
- [ ] 必要な修正が実施されている
- [ ] 全テストがパスする
- [ ] テストカバレッジが低下していない
### ファイル修正リスト
- **修正候補**:
- `src/context/TaskContext.test.jsx`(存在する場合)
- `src/context/TagContext.test.jsx`(存在する場合)
- `src/context/ListContext.test.jsx`(存在する場合)
- `src/features/tasks/components/*.test.jsx`
- `src/features/tags/components/*.test.jsx`
- `src/features/lists/components/*.test.jsx`
### 使用するツール
- `run_terminal_cmd` - テスト実行
- `grep` - 失敗したテストの検索
- `read_file` - テストファイルの確認
- `search_replace` - テストコードの修正
### テスト修正パターン
#### パターン1: localStorageのクリア追加
```javascript
import { describe, test, expect, beforeEach } from 'vitest';
describe('TaskContext', () => {
beforeEach(() => {
// 各テスト前にlocalStorageをクリア
localStorage.clear();
});
// ... テストケース
});
```
#### パターン2: StorageServiceのモック
```javascript
import { vi } from 'vitest';
// StorageServiceをモック化
vi.mock('../common/utils/StorageService', () => ({
StorageService: {
loadTasks: vi.fn(() => null),
saveTasks: vi.fn(() => true),
// ... 他のメソッド
}
}));
```
### 注意点・リスク
- **Context層のテスト**: 既存でContext層の単体テストがない可能性が高い
- **コンポーネントテスト**: コンポーネント層のテストは影響を受けにくいが、念のため確認
- **localStorage依存**: テスト環境(JSDOM)でlocalStorageが正しく動作することを確認
### 見積工数
**1-2時間**
### 依存タスク
- TASK-003(TaskContext修正)
- TASK-004(TagContext修正)
- TASK-005(ListContext修正)
---
## TASK-007: E2Eテストと動作確認
### タスク概要
ブラウザで実際にアプリケーションを動かし、永続化機能が正しく動作することを確認します。
### 優先度
**Medium** - 最終確認
### 実装内容
1. 開発サーバーを起動:`npm run dev`
2. 以下のシナリオを手動テスト:
**シナリオ1: 初回起動**
- localStorageをクリア(DevTools)
- アプリを開く
- ハイドレーション設定により適切な初期状態が表示されることを確認
**シナリオ2: タスク追加と復元**
- 新しいタスクを3つ追加
- ブラウザをリロード(F5)
- 追加した3つのタスクが表示されることを確認
**シナリオ3: タスク編集と復元**
- タスクを完了状態に変更
- タグを追加
- ブラウザをリロード
- 変更が保持されていることを確認
**シナリオ4: タスクリスト作成と復元**
- 新しいタスクリストを作成
- フィルター設定を追加
- ブラウザをリロード
- リストとフィルター設定が保持されていることを確認
**シナリオ5: タグ管理**
- タグを追加・編集・削除
- ブラウザをリロード
- タグの変更が保持されていることを確認
**シナリオ6: データ削除**
- 全タスクを削除
- ブラウザをリロード
- 空の状態が保持されていることを確認
3. DevToolsでlocalStorageの内容を確認:
- `taskDashboard_tasks`
- `taskDashboard_tags`
- `taskDashboard_taskLists`
- データ形式が正しいことを確認
4. エラーログの確認:
- コンソールにエラーが出ていないことを確認
- ネットワークエラーがないことを確認
### 完了条件(Definition of Done)
- [ ] 全シナリオがテストされている
- [ ] 全シナリオが期待通りに動作する
- [ ] localStorageに正しいデータが保存されている
- [ ] コンソールエラーがない
- [ ] パフォーマンス劣化がない
- [ ] 動作確認結果がドキュメント化されている
### 使用するツール
- `run_terminal_cmd` - 開発サーバー起動
- ブラウザDevTools - localStorage確認、コンソール確認
### チェックリスト
| シナリオ | 期待結果 | 実際の結果 | 合否 |
|---------|---------|-----------|------|
| 初回起動 | ハイドレーションまたは空状態 | | |
| タスク追加と復元 | リロード後も表示される | | |
| タスク編集と復元 | 変更が保持される | | |
| リスト作成と復元 | リストとフィルターが保持される | | |
| タグ管理 | タグの変更が保持される | | |
| データ削除 | 空の状態が保持される | | |
### 注意点・リスク
- **ハイドレーション設定**: VITE_ENABLE_DATA_HYDRATION環境変数の値により挙動が変わる
- **ブラウザ依存**: 異なるブラウザで動作確認することが望ましい
- **プライベートモード**: プライベートブラウジングモードでの動作も確認
### 見積工数
**1-2時間**
### 依存タスク
- TASK-006(既存テスト実行と修正)
---
## 実装時の全体的な注意事項
### コーディング規約
- 既存のコードスタイル(DataHydrationServiceのパターン)に従う
- JSDocコメントを必ず記載
- ESLintルールに従う
- PascalCase、camelCaseの命名規約を守る
### テスト方針
- 全ての公開APIをテスト
- 正常系・異常系の両方をカバー
- localStorageは各テスト前にクリア
### エラーハンドリング
- try-catchで適切にエラーを捕捉
- console.errorでログ出力
- アプリケーションはクラッシュさせない
### パフォーマンス
- JSON.stringifyは十分高速
- debounceは初期実装では不要
- 将来的に必要になれば検討
### ドキュメント
- 重要な設計判断はコメントで記録
- README.mdの更新は不要(機能追加のみ)
---
## マイルストーン
### マイルストーン1: コア機能完成
- **タスク**: TASK-001, TASK-002
- **ゴール**: StorageServiceが完成し、単体テストがパス
- **期間**: 3-5時間
### マイルストーン2: Context統合完成
- **タスク**: TASK-003, TASK-004, TASK-005
- **ゴール**: 全Contextで永続化が動作
- **期間**: 3時間
### マイルストーン3: リリース準備完了
- **タスク**: TASK-006, TASK-007
- **ゴール**: 全テストがパスし、動作確認完了
- **期間**: 2-4時間
**総期間**: 8-12時間(1-2日)
---
## リスクと対策
| リスク | 影響 | 対策 |
|--------|------|------|
| 既存テストの失敗 | 高 | localStorage.clear()を追加、モック化 |
| localStorage利用不可 | 中 | isAvailable()でチェック、フォールバック |
| パフォーマンス劣化 | 低 | 大量データでテスト、必要に応じてdebounce |
| データ破損 | 中 | try-catchで適切に処理、初期状態に戻す |
---
## 次のステップ
実装計画が承認されたら、以下の手順で実装を進めます:
1. **TASK-001**から順番に実装
2. 各タスク完了時に動作確認
3. 問題があれば即座に修正
4. 全タスク完了後、最終レビュー
**実装計画フェーズが完了しました。実装を進めてよろしいですか?**
---
**作成日**: 2025-10-23
**プロジェクト名**: task-persistence
**フェーズ**: 実装計画
**次フェーズ**: 実装
4.実装
ここまでで作成した「要件定義」「設計」「実装計画」のドキュメントをもとに、最後に実装フェーズを進めていきます。
実装の開始には、この記事で用意した /start-implementation コマンドを使いました。
/start-implementation
このコマンドを実行すると、Serena MCP に保存しておいた実装計画(タスク一覧)を読み込み、タスクの順にコード修正を進めてくれます。こちらは、
- 変更内容のサマリを確認して意図とズレていないかレビューする
- 必要に応じて「このケースも考慮してほしい」など追加条件を伝える
といった“方向づけ”に集中し、実際のファイル探索や置き換え作業は AI に任せる形で実装を進めました。
5.テスト
実装が完了したら、ブラウザ上で動作をテストします。
その結果、ページをリロードしてもタスクが正しく保持され、デグレ(既存機能の不具合)も発生していないことを確認できました。
まとめ
今回は、小規模なプロジェクトを題材に、AIと協働しながら要件定義から実装までを一貫して進める手法を検証を行いました。
その結果、期待された成果が得られ、AIを活用した開発プロセスが有効であることが実証されました。
得られた効果
- 一貫した開発プロセス:要件定義から実装まで、迷いなく進められる
- 品質の向上:設計をしっかり行うことで、バグが少ないコードの実現が可能に
- 学習効果:AIと協働することで、ベストプラクティスを自然に身につけられる
今後の展望
今回の手法は小規模プロジェクトでの検証でしたが、実際の開発現場では以下のような大規模プロジェクト特有の課題への対応が求められると感じています。
- 複数のマイクロサービス間の依存関係の把握
- チーム開発での一貫した開発プロセスの維持
今後取り組むべき方向性としては、次のようなものが考えられます。
- ドキュメント連携の強化:設計書、API仕様書、データベーススキーマなどをAIに効率的に提供する仕組みの構築
- チーム展開:個人の手法からチーム全体の標準的な開発プロセスへの発展
- 品質評価:開発効率やコード品質の定量的な改善を測定する仕組みの構築
こうした取り組みを通じて、AIがより深く開発プロセスに溶け込み、「チームの一員として活躍する未来」へと近づいていけると感じています。
最後までお読みいただきありがとうございました。








