diff --git a/src/app/page.tsx b/src/app/page.tsx index 8fb65a2..60c5120 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -13,75 +13,143 @@ type Todo = { type FilterType = 'all' | 'active' | 'completed'; +// エラー型の定義 +type AppError = { + type: 'STORAGE_ERROR' | 'IMPORT_ERROR' | 'VALIDATION_ERROR'; + message: string; +}; + export default function Home() { // Local Storage のキー const STORAGE_KEY = 'todo-app-data'; - // Todoリスト本体の状態管理 + // 状態管理 const [todos, setTodos] = useState([]); - - // 入力欄の状態管理 const [inputValue, setInputValue] = useState(''); - - // 削除確認ダイアログの表示状態 const [showConfirm, setShowConfirm] = useState(false); - - // 削除対象のTodoのID const [targetId, setTargetId] = useState(null); - - // 空欄入力時の警告ダイアログ表示状態 const [showInputAlert, setShowInputAlert] = useState(false); - - // 編集機能のための状態管理 const [editingId, setEditingId] = useState(null); - - // 編集中のテキスト const [editText, setEditText] = useState(''); - - // フィルター状態の管理(全て、未完了、完了) const [filter, setFilter] = useState('all'); + + // エラー状態の管理 + const [error, setError] = useState(null); + const [showErrorDialog, setShowErrorDialog] = useState(false); // フィルターに応じたTodoの数をカウント const activeCount = todos.filter(todo => !todo.completed).length; const completedCount = todos.filter(todo => todo.completed).length; + // エラー処理用のヘルパー関数 + const handleError = (error: AppError) => { + console.error('アプリケーションエラー:', error); + setError(error); + setShowErrorDialog(true); + }; + + // エラーダイアログを閉じる + const closeErrorDialog = () => { + setShowErrorDialog(false); + setError(null); + }; + + // バリデーション関数 + const validateTodoText = (text: string): boolean => { + if (!text || text.trim().length === 0) { + handleError({ + type: 'VALIDATION_ERROR', + message: 'TODOの内容を入力してください' + }); + return false; + } + + if (text.trim().length > 100) { + handleError({ + type: 'VALIDATION_ERROR', + message: 'TODOは100文字以内で入力してください' + }); + return false; + } + + return true; + }; + + // Local Storage操作をtry-catchで包む + const safeLocalStorageGet = (key: string): string | null => { + try { + return localStorage.getItem(key); + } catch (error) { + handleError({ + type: 'STORAGE_ERROR', + message: 'データの読み込みに失敗しました' + }); + return null; + } + }; + + const safeLocalStorageSet = (key: string, value: string): boolean => { + try { + localStorage.setItem(key, value); + return true; + } catch (error) { + handleError({ + type: 'STORAGE_ERROR', + message: 'データの保存に失敗しました' + }); + return false; + } + }; + const startEdit = (todo: Todo) => { setEditingId(todo.id); setEditText(todo.text); }; const saveEdit = () => { - if (editText.trim() === '') { - alert('TODOが空です'); + if (!validateTodoText(editText)) { return; } - const updatedTodos = todos.map(todo => { - if (todo.id === editingId) { - return { ...todo, text: editText }; - } - return todo; - }); + try { + const updatedTodos = todos.map(todo => { + if (todo.id === editingId) { + return { ...todo, text: editText.trim() }; + } + return todo; + }); - setTodos(updatedTodos); - setEditingId(null); - setEditText(''); + setTodos(updatedTodos); + setEditingId(null); + setEditText(''); + } catch (error) { + handleError({ + type: 'VALIDATION_ERROR', + message: 'TODOの更新に失敗しました' + }); + } }; - // 新しいTODOを追加する関数 + // 新しいTODOを追加する関数(エラーハンドリング追加) const addTodo = () => { - if (inputValue.trim() === '') { - setShowInputAlert(true); + if (!validateTodoText(inputValue)) { return; } - const newTodo: Todo = { - id: Date.now(), - text: inputValue, - completed: false - }; - setTodos([...todos, newTodo]); - setInputValue(''); + try { + const newTodo: Todo = { + id: Date.now(), + text: inputValue.trim(), + completed: false + }; + setTodos([...todos, newTodo]); + setInputValue(''); + } catch (error) { + handleError({ + type: 'VALIDATION_ERROR', + message: 'TODOの追加に失敗しました' + }); + } }; const cancelEdit = () => { @@ -97,13 +165,20 @@ export default function Home() { // Todoの完了状態をトグルする関数 const toggleTodo = (id: number) => { - const updatedTodos = todos.map(todo => { - if (todo.id === id) { - return { ...todo, completed: !todo.completed }; - } - return todo; - }); - setTodos(updatedTodos); + try { + const updatedTodos = todos.map(todo => { + if (todo.id === id) { + return { ...todo, completed: !todo.completed }; + } + return todo; + }); + setTodos(updatedTodos); + } catch (error) { + handleError({ + type: 'VALIDATION_ERROR', + message: 'TODO状態の更新に失敗しました' + }); + } }; // 削除ボタンが押されたときの処理(確認ダイアログ表示) @@ -115,9 +190,16 @@ export default function Home() { // ダイアログで「はい」が押されたときの本削除処理 const confirmDelete = () => { if (targetId !== null) { - setTodos(todos.filter(todo => todo.id !== targetId)); - setShowConfirm(false); - setTargetId(null); + try { + setTodos(todos.filter(todo => todo.id !== targetId)); + setShowConfirm(false); + setTargetId(null); + } catch (error) { + handleError({ + type: 'VALIDATION_ERROR', + message: 'TODOの削除に失敗しました' + }); + } } }; @@ -134,52 +216,112 @@ export default function Home() { // 完了済みTODOをすべて削除 const clearCompleted = () => { - setTodos(todos.filter(todo => !todo.completed)); + try { + setTodos(todos.filter(todo => !todo.completed)); + } catch (error) { + handleError({ + type: 'VALIDATION_ERROR', + message: '完了済みTODOの削除に失敗しました' + }); + } }; - // アプリ起動時にデータを読み込み + // アプリ起動時にデータを読み込み(エラーハンドリング強化) useEffect(() => { - const savedTodos = localStorage.getItem(STORAGE_KEY); + const savedTodos = safeLocalStorageGet(STORAGE_KEY); if (savedTodos) { try { const parsedTodos = JSON.parse(savedTodos); - setTodos(parsedTodos); + + // データの妥当性をチェック + if (Array.isArray(parsedTodos)) { + const validTodos = parsedTodos.filter(todo => + todo && + typeof todo.id === 'number' && + typeof todo.text === 'string' && + typeof todo.completed === 'boolean' + ); + setTodos(validTodos); + } } catch (error) { - console.error('データの読み込みに失敗しました:', error); + handleError({ + type: 'STORAGE_ERROR', + message: '保存されたデータの形式が正しくありません' + }); } } }, []); - // TODOが変更されるたびに保存 + // TODOが変更されるたびに保存(エラーハンドリング追加) useEffect(() => { - localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); + safeLocalStorageSet(STORAGE_KEY, JSON.stringify(todos)); }, [todos]); - // データのエクスポート機能 + // データのエクスポート機能(エラーハンドリング追加) const exportData = () => { - const dataStr = JSON.stringify(todos, null, 2); - const dataBlob = new Blob([dataStr], { type: 'application/json' }); - const url = URL.createObjectURL(dataBlob); - const link = document.createElement('a'); - link.href = url; - link.download = 'todos.json'; - link.click(); - URL.revokeObjectURL(url); + try { + const dataStr = JSON.stringify(todos, null, 2); + const dataBlob = new Blob([dataStr], { type: 'application/json' }); + const url = URL.createObjectURL(dataBlob); + const link = document.createElement('a'); + link.href = url; + link.download = 'todos.json'; + link.click(); + URL.revokeObjectURL(url); + } catch (error) { + handleError({ + type: 'STORAGE_ERROR', + message: 'データのエクスポートに失敗しました' + }); + } }; - // データのインポート機能 + // データのインポート機能(エラーハンドリング強化) const importData = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { try { - const importedTodos = JSON.parse(e.target?.result as string); - setTodos(importedTodos); + const importedData = JSON.parse(e.target?.result as string); + + // インポートデータの妥当性をチェック + if (!Array.isArray(importedData)) { + throw new Error('無効なデータ形式です'); + } + + const validTodos = importedData.filter(todo => + todo && + typeof todo.id === 'number' && + typeof todo.text === 'string' && + typeof todo.completed === 'boolean' + ); + + if (validTodos.length === 0) { + throw new Error('有効なTODOデータが見つかりません'); + } + + setTodos(validTodos); + + // ファイル入力をリセット + event.target.value = ''; } catch (error) { - alert('ファイルの読み込みに失敗しました'); + handleError({ + type: 'IMPORT_ERROR', + message: 'ファイルの読み込みに失敗しました。正しいJSONファイルを選択してください。' + }); + event.target.value = ''; } }; + + reader.onerror = () => { + handleError({ + type: 'IMPORT_ERROR', + message: 'ファイルの読み込み中にエラーが発生しました' + }); + event.target.value = ''; + }; + reader.readAsText(file); } }; @@ -187,7 +329,7 @@ export default function Home() { return (
- {/* ヘッダー部分にエクスポート・インポート機能を追加 */} + {/* ヘッダー部分 */}

TODOアプリ

@@ -217,6 +359,7 @@ export default function Home() { onChange={(e) => setInputValue(e.target.value)} placeholder="TODOを入力してEnterキー" className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + maxLength={100} />
)} + + {/* エラーダイアログ */} + {showErrorDialog && error && ( +
+
+
+
+ + + +
+

エラー

+
+

{error.message}

+ +
+
+ )}
);