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

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

View File

@@ -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<Todo[]>([]);
// 入力欄の状態管理
const [inputValue, setInputValue] = useState<string>('');
// 削除確認ダイアログの表示状態
const [showConfirm, setShowConfirm] = useState(false);
// 削除対象のTodoのID
const [targetId, setTargetId] = useState<number | null>(null);
// 空欄入力時の警告ダイアログ表示状態
const [showInputAlert, setShowInputAlert] = useState(false);
// 編集機能のための状態管理
const [editingId, setEditingId] = useState<number | null>(null);
// 編集中のテキスト
const [editText, setEditText] = useState('');
// フィルター状態の管理(全て、未完了、完了)
const [filter, setFilter] = useState<FilterType>('all');
// エラー状態の管理
const [error, setError] = useState<AppError | null>(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<HTMLInputElement>) => {
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 (
<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="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-800">TODOアプリ</h1>
<div className="flex gap-2">
@@ -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}
/>
<button
type="submit"
@@ -276,6 +419,29 @@ export default function Home() {
</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>
</main>
);