エラーハンドリングまでを追記

This commit is contained in:
2025-09-29 06:13:59 +09:00
parent f379dd5d8f
commit d387a00dff

View File

@@ -13,52 +13,108 @@ type Todo = {
type FilterType = 'all' | 'active' | 'completed'; type FilterType = 'all' | 'active' | 'completed';
// エラー型の定義
type AppError = {
type: 'STORAGE_ERROR' | 'IMPORT_ERROR' | 'VALIDATION_ERROR';
message: string;
};
export default function Home() { export default function Home() {
// Local Storage のキー // Local Storage のキー
const STORAGE_KEY = 'todo-app-data'; const STORAGE_KEY = 'todo-app-data';
// Todoリスト本体の状態管理 // 状態管理
const [todos, setTodos] = useState<Todo[]>([]); const [todos, setTodos] = useState<Todo[]>([]);
// 入力欄の状態管理
const [inputValue, setInputValue] = useState<string>(''); const [inputValue, setInputValue] = useState<string>('');
// 削除確認ダイアログの表示状態
const [showConfirm, setShowConfirm] = useState(false); const [showConfirm, setShowConfirm] = useState(false);
// 削除対象のTodoのID
const [targetId, setTargetId] = useState<number | null>(null); const [targetId, setTargetId] = useState<number | null>(null);
// 空欄入力時の警告ダイアログ表示状態
const [showInputAlert, setShowInputAlert] = useState(false); const [showInputAlert, setShowInputAlert] = useState(false);
// 編集機能のための状態管理
const [editingId, setEditingId] = useState<number | null>(null); const [editingId, setEditingId] = useState<number | null>(null);
// 編集中のテキスト
const [editText, setEditText] = useState(''); const [editText, setEditText] = useState('');
// フィルター状態の管理(全て、未完了、完了)
const [filter, setFilter] = useState<FilterType>('all'); const [filter, setFilter] = useState<FilterType>('all');
// エラー状態の管理
const [error, setError] = useState<AppError | null>(null);
const [showErrorDialog, setShowErrorDialog] = useState(false);
// フィルターに応じたTodoの数をカウント // フィルターに応じたTodoの数をカウント
const activeCount = todos.filter(todo => !todo.completed).length; const activeCount = todos.filter(todo => !todo.completed).length;
const completedCount = 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) => { const startEdit = (todo: Todo) => {
setEditingId(todo.id); setEditingId(todo.id);
setEditText(todo.text); setEditText(todo.text);
}; };
const saveEdit = () => { const saveEdit = () => {
if (editText.trim() === '') { if (!validateTodoText(editText)) {
alert('TODOが空です');
return; return;
} }
try {
const updatedTodos = todos.map(todo => { const updatedTodos = todos.map(todo => {
if (todo.id === editingId) { if (todo.id === editingId) {
return { ...todo, text: editText }; return { ...todo, text: editText.trim() };
} }
return todo; return todo;
}); });
@@ -66,22 +122,34 @@ export default function Home() {
setTodos(updatedTodos); setTodos(updatedTodos);
setEditingId(null); setEditingId(null);
setEditText(''); setEditText('');
} catch (error) {
handleError({
type: 'VALIDATION_ERROR',
message: 'TODOの更新に失敗しました'
});
}
}; };
// 新しいTODOを追加する関数 // 新しいTODOを追加する関数(エラーハンドリング追加)
const addTodo = () => { const addTodo = () => {
if (inputValue.trim() === '') { if (!validateTodoText(inputValue)) {
setShowInputAlert(true);
return; return;
} }
try {
const newTodo: Todo = { const newTodo: Todo = {
id: Date.now(), id: Date.now(),
text: inputValue, text: inputValue.trim(),
completed: false completed: false
}; };
setTodos([...todos, newTodo]); setTodos([...todos, newTodo]);
setInputValue(''); setInputValue('');
} catch (error) {
handleError({
type: 'VALIDATION_ERROR',
message: 'TODOの追加に失敗しました'
});
}
}; };
const cancelEdit = () => { const cancelEdit = () => {
@@ -97,6 +165,7 @@ export default function Home() {
// Todoの完了状態をトグルする関数 // Todoの完了状態をトグルする関数
const toggleTodo = (id: number) => { const toggleTodo = (id: number) => {
try {
const updatedTodos = todos.map(todo => { const updatedTodos = todos.map(todo => {
if (todo.id === id) { if (todo.id === id) {
return { ...todo, completed: !todo.completed }; return { ...todo, completed: !todo.completed };
@@ -104,6 +173,12 @@ export default function Home() {
return todo; return todo;
}); });
setTodos(updatedTodos); setTodos(updatedTodos);
} catch (error) {
handleError({
type: 'VALIDATION_ERROR',
message: 'TODO状態の更新に失敗しました'
});
}
}; };
// 削除ボタンが押されたときの処理(確認ダイアログ表示) // 削除ボタンが押されたときの処理(確認ダイアログ表示)
@@ -115,9 +190,16 @@ export default function Home() {
// ダイアログで「はい」が押されたときの本削除処理 // ダイアログで「はい」が押されたときの本削除処理
const confirmDelete = () => { const confirmDelete = () => {
if (targetId !== null) { if (targetId !== null) {
try {
setTodos(todos.filter(todo => todo.id !== targetId)); setTodos(todos.filter(todo => todo.id !== targetId));
setShowConfirm(false); setShowConfirm(false);
setTargetId(null); setTargetId(null);
} catch (error) {
handleError({
type: 'VALIDATION_ERROR',
message: 'TODOの削除に失敗しました'
});
}
} }
}; };
@@ -134,29 +216,50 @@ export default function Home() {
// 完了済みTODOをすべて削除 // 完了済みTODOをすべて削除
const clearCompleted = () => { const clearCompleted = () => {
try {
setTodos(todos.filter(todo => !todo.completed)); setTodos(todos.filter(todo => !todo.completed));
} catch (error) {
handleError({
type: 'VALIDATION_ERROR',
message: '完了済みTODOの削除に失敗しました'
});
}
}; };
// アプリ起動時にデータを読み込み // アプリ起動時にデータを読み込み(エラーハンドリング強化)
useEffect(() => { useEffect(() => {
const savedTodos = localStorage.getItem(STORAGE_KEY); const savedTodos = safeLocalStorageGet(STORAGE_KEY);
if (savedTodos) { if (savedTodos) {
try { try {
const parsedTodos = JSON.parse(savedTodos); 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) { } catch (error) {
console.error('データの読み込みに失敗しました:', error); handleError({
type: 'STORAGE_ERROR',
message: '保存されたデータの形式が正しくありません'
});
} }
} }
}, []); }, []);
// TODOが変更されるたびに保存 // TODOが変更されるたびに保存(エラーハンドリング追加)
useEffect(() => { useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); safeLocalStorageSet(STORAGE_KEY, JSON.stringify(todos));
}, [todos]); }, [todos]);
// データのエクスポート機能 // データのエクスポート機能(エラーハンドリング追加)
const exportData = () => { const exportData = () => {
try {
const dataStr = JSON.stringify(todos, null, 2); const dataStr = JSON.stringify(todos, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' }); const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob); const url = URL.createObjectURL(dataBlob);
@@ -165,21 +268,60 @@ export default function Home() {
link.download = 'todos.json'; link.download = 'todos.json';
link.click(); link.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} catch (error) {
handleError({
type: 'STORAGE_ERROR',
message: 'データのエクスポートに失敗しました'
});
}
}; };
// データのインポート機能 // データのインポート機能(エラーハンドリング強化)
const importData = (event: React.ChangeEvent<HTMLInputElement>) => { const importData = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
if (file) { if (file) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
try { try {
const importedTodos = JSON.parse(e.target?.result as string); const importedData = JSON.parse(e.target?.result as string);
setTodos(importedTodos);
// インポートデータの妥当性をチェック
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) { } 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); reader.readAsText(file);
} }
}; };
@@ -187,7 +329,7 @@ export default function Home() {
return ( return (
<main className="min-h-screen p-8 bg-gray-50"> <main className="min-h-screen p-8 bg-gray-50">
<div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6"> <div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
{/* ヘッダー部分にエクスポート・インポート機能を追加 */} {/* ヘッダー部分 */}
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-800">TODOアプリ</h1> <h1 className="text-2xl font-bold text-gray-800">TODOアプリ</h1>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -217,6 +359,7 @@ export default function Home() {
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => setInputValue(e.target.value)}
placeholder="TODOを入力してEnterキー" placeholder="TODOを入力してEnterキー"
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
maxLength={100}
/> />
<button <button
type="submit" type="submit"
@@ -276,6 +419,29 @@ export default function Home() {
</div> </div>
</div> </div>
)} )}
{/* エラーダイアログ */}
{showErrorDialog && error && (
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
<div className="bg-white p-6 rounded shadow max-w-sm mx-4">
<div className="flex items-center mb-4">
<div className="bg-red-100 p-2 rounded-full mr-3">
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900"></h3>
</div>
<p className="mb-4 text-gray-700">{error.message}</p>
<button
onClick={closeErrorDialog}
className="w-full px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors"
>
</button>
</div>
</div>
)}
</div> </div>
</main> </main>
); );