# migrate-local-to-db > 既存のLocalStorageデータを読み出し、Supabaseへ一括アップロードする。 - Author: nekomasaru - Repository: nekomasaru/LineageDoc - Version: 20260205053519 - Stars: 0 - Forks: 0 - Last Updated: 2026-02-06 - Source: https://github.com/nekomasaru/LineageDoc - Web: https://mule.run/skillshub/@@nekomasaru/LineageDoc~migrate-local-to-db:20260205053519 --- --- name: migrate-local-to-db description: 既存のLocalStorageデータを読み出し、Supabaseへ一括アップロードする。 allowed-tools: [file_edit] meta: domain: migration role: data-migration tech_stack: supabase, localStorage phase: 2 estimated_time: 60min dependencies: [api-client-save] --- # このスキルでやること 既存のLineaDocがLocalStorageに保存しているドキュメントデータを、Supabaseの `documents` テーブルに移行する一括アップロード機能を実装する。 # 設計思想 ## なぜ移行が必要か - 既存ユーザーのデータを失わない - LocalStorage(ブラウザ依存)からクラウド(Supabase)への移行 - 一度きりの処理だが、ユーザーが安心して実行できるUIが必要 ## 移行フロー ``` ┌─────────────────────────────────────────┐ │ LocalStorage │ │ - documents[] │ │ - lineageData{} │ └───────────────┬─────────────────────────┘ │ migrateLocalData() ↓ ┌─────────────────────────────────────────┐ │ Supabase │ │ - documents テーブル │ │ - versions テーブル(初期バージョン) │ └─────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────┐ │ LocalStorage クリア(オプション) │ └─────────────────────────────────────────┘ ``` # 作成するファイル ## `src/lib/migration/localStorageMigration.ts` ```typescript import { supabase } from '@/lib/supabase'; interface LocalDocument { id?: string; title: string; content: string; createdAt?: string; updatedAt?: string; parentId?: string; branchName?: string; } interface MigrationResult { success: boolean; migratedCount: number; skippedCount: number; errors: string[]; } const LOCAL_STORAGE_KEY = 'lineadoc_documents'; /** * LocalStorageからドキュメントデータを読み出す */ export function getLocalDocuments(): LocalDocument[] { try { const data = localStorage.getItem(LOCAL_STORAGE_KEY); if (!data) return []; return JSON.parse(data); } catch (error) { console.error('Failed to read localStorage:', error); return []; } } /** * LocalStorageにドキュメントデータが存在するかチェック */ export function hasLocalData(): boolean { return getLocalDocuments().length > 0; } /** * LocalStorageのデータをSupabaseに移行する */ export async function migrateLocalToSupabase(): Promise { const localDocs = getLocalDocuments(); const result: MigrationResult = { success: true, migratedCount: 0, skippedCount: 0, errors: [], }; if (localDocs.length === 0) { return result; } // 現在のユーザーを取得 const { data: { user } } = await supabase.auth.getUser(); if (!user) { result.success = false; result.errors.push('ログインが必要です'); return result; } for (const doc of localDocs) { try { // 1. documents テーブルに INSERT const { data: newDoc, error: docError } = await supabase .from('documents') .insert({ title: doc.title || '無題のドキュメント(移行)', content: doc.content || '', created_by: user.id, parent_id: doc.parentId || null, branch_name: doc.branchName || null, }) .select() .single(); if (docError) { result.errors.push(`"${doc.title}": ${docError.message}`); result.skippedCount++; continue; } // 2. 初期バージョンを versions テーブルに INSERT const { error: versionError } = await supabase .from('versions') .insert({ document_id: newDoc.id, version_number: 1, content: doc.content || '', commit_message: 'LocalStorageからの移行', }); if (versionError) { console.warn(`Version creation failed for ${newDoc.id}:`, versionError); // バージョン作成失敗は警告のみ(文書は作成済み) } result.migratedCount++; } catch (error) { result.errors.push(`"${doc.title}": ${error instanceof Error ? error.message : 'Unknown error'}`); result.skippedCount++; } } result.success = result.errors.length === 0; return result; } /** * 移行完了後にLocalStorageをクリアする */ export function clearLocalData(): void { localStorage.removeItem(LOCAL_STORAGE_KEY); // 関連データもクリア localStorage.removeItem('lineadoc_lineage'); localStorage.removeItem('lineadoc_settings'); } /** * LocalStorageデータをバックアップ(JSONダウンロード) */ export function downloadLocalBackup(): void { const localDocs = getLocalDocuments(); const blob = new Blob([JSON.stringify(localDocs, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `lineadoc_backup_${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); } ``` ## `src/components/_features/migration/MigrationPanel.tsx` ```tsx 'use client'; import { useState, useEffect } from 'react'; import { Upload, Download, Trash2, CheckCircle, AlertTriangle } from 'lucide-react'; import { hasLocalData, getLocalDocuments, migrateLocalToSupabase, clearLocalData, downloadLocalBackup, } from '@/lib/migration/localStorageMigration'; export function MigrationPanel() { const [localCount, setLocalCount] = useState(0); const [isMigrating, setIsMigrating] = useState(false); const [migrationResult, setMigrationResult] = useState<{ success: boolean; migratedCount: number; errors: string[]; } | null>(null); useEffect(() => { setLocalCount(getLocalDocuments().length); }, []); if (localCount === 0) { return null; // 移行データがなければ表示しない } const handleMigrate = async () => { setIsMigrating(true); const result = await migrateLocalToSupabase(); setMigrationResult(result); setIsMigrating(false); if (result.success) { setLocalCount(0); } }; const handleBackup = () => { downloadLocalBackup(); }; const handleClear = () => { if (confirm('LocalStorageのデータを削除しますか?この操作は取り消せません。')) { clearLocalData(); setLocalCount(0); } }; return (

ローカルデータの移行

ブラウザに保存された {localCount} 件のドキュメントがあります。 クラウドに移行することで、他のデバイスからもアクセスできるようになります。

{migrationResult && (
{migrationResult.success ? (

{migrationResult.migratedCount} 件の移行が完了しました

) : (

一部のドキュメントの移行に失敗しました:

    {migrationResult.errors.map((err, i) => (
  • {err}
  • ))}
)}
)}
); } ``` # 使用例 ```tsx // ダッシュボードページなどで表示 import { MigrationPanel } from '@/components/_features/migration/MigrationPanel'; function DashboardPage() { return (
{/* 移行が必要な場合のみ表示される */} {/* 通常のダッシュボードコンテンツ */}
); } ``` # セキュリティ考慮 - **認証必須**: ログイン済みユーザーのみ移行可能 - **ユーザー紐付け**: `created_by` に現在のユーザーIDを設定 - **バックアップ推奨**: 移行前にJSONダウンロードを推奨 # 禁止事項 - **確認なしでのLocalStorageクリア**: 必ずconfirmダイアログを表示 - **エラー時の無視**: 失敗したドキュメントを明示 - **二重移行**: 一度移行したデータを再度移行しない仕組みを検討 # 完了条件 - [ ] `localStorageMigration.ts` が作成されている - [ ] `MigrationPanel.tsx` が作成されている - [ ] LocalStorageからSupabaseへの移行が動作する - [ ] バックアップダウンロードが動作する - [ ] エラーハンドリングが適切に機能する - [ ] 移行完了後にパネルが非表示になる # 全スキル完了! これで全17スキルの作成が完了しました。 Week 1(Day 1-5)のハイブリッドエディタ完成に向けて、順番に実装を進めてください。