diff --git a/backend_training/app/src/index.php b/backend_training/app/src/index.php index 09e9aa6..a0dcfbc 100644 --- a/backend_training/app/src/index.php +++ b/backend_training/app/src/index.php @@ -15,18 +15,18 @@ $routes = [ 'GET' => [ - '#^/todos$#' => 'handleGetTodos', // Match only `/todos` with no query params or other path '#^/health$#' => 'handleHealthCheck', - // TODO: 他のエンドポイントを追加 + '#^/todos$#' => 'handleGetTodos', // `/todos` → 全てのTODOを取得 + '#^/todos/(\d+)$#' => 'handleGetTodoById', // `/todos/{id}` → 特定のTODOを取得 ], 'POST' => [ - // TODO: 他のエンドポイントを追加 + '#^/todos$#' => 'handlePostTodos', // `/todos` → 新しいTodoを作成 ], 'PUT' => [ - // TODO: 他のエンドポイントを追加 + '#^/todos(?:\?id=(\d+))?$#' => 'handlePutTodoById', // `/todos?id={id}` → 特定のTODOを更新 ], 'DELETE' => [ - // TODO: 他のエンドポイントを追加 + '#^/todos(?:\?id=(\d+))?$#' => 'handleDeleteTodoById', // `/todos?id={id}` → 特定のTODOを削除 ] ]; @@ -40,8 +40,8 @@ } } -http_response_code(404); -echo json_encode(['error' => 'Not Found']); +http_response_code(500); +echo json_encode(['error' => 'Internal Server Error']); exit; /** @@ -90,7 +90,7 @@ function handleGetTodos(PDO $pdo): void $result = $stmt->fetchAll(PDO::FETCH_ASSOC); // レスポンスを返却 - echo json_encode(['status' => 'ok', 'data' => $result]); + echo json_encode(['status' => 'ok', 'todos' => $result]); } catch (Exception $e) { // クエリエラー時のレスポンス http_response_code(500); @@ -102,3 +102,278 @@ function handleGetTodos(PDO $pdo): void } exit; } + +/** + * `/todos/{id}` エンドポイントを処理します。 + * + * @param PDO $pdo データベース接続のためのPDOインスタンス + * @param int $id 取得するTODOのID + * @return void + */ +function handleGetTodoById(PDO $pdo, int $id): void +{ + try { + // IDを指定してTodoを取得 + $stmt = $pdo->prepare("SELECT todos.id, todos.title, statuses.name FROM todos JOIN statuses ON todos.status_id = statuses.id WHERE todos.id = :id;"); + $stmt->bindParam(':id', $id, PDO::PARAM_INT); + $stmt->execute(); + $todo = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($todo) { + // Todoが見つかった場合 + http_response_code(200); + echo json_encode(['status' => 'ok', 'todos' => $todo]); + } else { + // Todoが見つからない場合 + http_response_code(404); + echo json_encode([ + 'status' => 'error', + 'message' => 'Todoが見つかりません' + ], JSON_UNESCAPED_UNICODE); + } + } catch (Exception $e) { + // クエリエラー時のレスポンス + http_response_code(500); + echo json_encode([ + 'status' => 'error', + 'message' => 'Failed to get todo', + 'error' => $e->getMessage() + ]); + } + exit; +} + +/** + * `/todos` エンドポイントを処理します。(新しいTodoを作成) + * + * @param PDO $pdo データベース接続のためのPDOインスタンス + * @return void + */ +function handlePostTodos(PDO $pdo): void +{ + try { + // リクエストボディを取得 + $input = json_decode(file_get_contents("php://input"), true); + + // JSONのバリデーション(titleがあるか) + if (!isset($input['title']) || empty(trim($input['title']))) { + http_response_code(400); + echo json_encode(['error' => 'Title is required']); + exit; + } + + // `status_id` のデフォルト値を設定(例: 1 = "pending") + $defaultStatusId = 1; + + // データベースに新しいTodoを挿入 + $stmt = $pdo->prepare("INSERT INTO todos (title, status_id) VALUES (:title, :status_id)"); + $stmt->bindParam(':title', $input['title'], PDO::PARAM_STR); + $stmt->bindParam(':status_id', $defaultStatusId, PDO::PARAM_INT); + $stmt->execute(); + + // 挿入されたIDを取得 + $id = $pdo->lastInsertId(); + + // 挿入されたデータを返す + http_response_code(201); + echo json_encode([ + 'status' => 'ok', + 'todos' => [ + 'id' => $id, + 'title' => $input['title'], + 'status_id' => $defaultStatusId + ] + ], JSON_UNESCAPED_UNICODE); + } catch (Exception $e) { + // クエリエラー時のレスポンス + http_response_code(500); + echo json_encode([ + 'status' => 'error', + 'message' => 'Failed to create todo', + 'error' => $e->getMessage() + ], JSON_UNESCAPED_UNICODE); + } + exit; +} + +/** + * `/todos/{id}` エンドポイントを処理します。(特定のTodoを更新) + * + * @param PDO $pdo データベース接続のためのPDOインスタンス + * @param int $id 更新するTODOのID + * @return void + */ +function handlePutTodoById(PDO $pdo, int $id): void +{ + try { + // トランザクション開始 + $pdo->beginTransaction(); + + // リクエストボディを取得 + $input = json_decode(file_get_contents("php://input"), true); + + // JSONのバリデーション(title または status のどちらかが必要) + if (!is_array($input) || (!isset($input['title']) && !isset($input['status']))) { + http_response_code(400); + echo json_encode([ + 'status' => 'error', + 'message' => 'Either title or status must be provided' + ], JSON_UNESCAPED_UNICODE); + exit; + } + + // `title` がある場合は trim() して空白のみを防ぐ + if (isset($input['title']) && trim($input['title']) === '') { + http_response_code(400); + echo json_encode([ + 'status' => 'error', + 'message' => 'Title cannot be empty' + ], JSON_UNESCAPED_UNICODE); + exit; + } + + // 指定されたIDのTodoが存在するか確認 + $stmt = $pdo->prepare("SELECT id FROM todos WHERE id = :id"); + $stmt->bindParam(':id', $id, PDO::PARAM_INT); + $stmt->execute(); + + if (!$stmt->fetch()) { + http_response_code(404); + echo json_encode([ + 'status' => 'error', + 'message' => 'Todoが見つかりません' + ], JSON_UNESCAPED_UNICODE); + exit; + } + + // 更新用のSQLを動的に組み立て + $updateFields = []; + if (isset($input['title'])) { + $updateFields[] = "title = :title"; + } + + if (isset($input['status'])) { + // `status` の値に応じて `status_id` を取得 + $stmt = $pdo->prepare("SELECT id FROM statuses WHERE name = :status"); + $stmt->bindParam(':status', $input['status'], PDO::PARAM_STR); + $stmt->execute(); + $statusId = $stmt->fetchColumn(); + + if (!$statusId) { + http_response_code(400); + echo json_encode([ + 'status' => 'error', + 'message' => 'Invalid status value' + ], JSON_UNESCAPED_UNICODE); + exit; + } + + $updateFields[] = "status_id = :status_id"; + } + + if (empty($updateFields)) { + http_response_code(400); + echo json_encode([ + 'status' => 'error', + 'message' => 'No valid fields provided for update' + ], JSON_UNESCAPED_UNICODE); + exit; + } + + $updateSQL = "UPDATE todos SET " . implode(", ", $updateFields) . " WHERE id = :id"; + $stmt = $pdo->prepare($updateSQL); + + if (isset($input['title'])) { + $stmt->bindParam(':title', $input['title'], PDO::PARAM_STR); + } + if (isset($statusId)) { + $stmt->bindParam(':status_id', $statusId, PDO::PARAM_INT); + } + $stmt->bindParam(':id', $id, PDO::PARAM_INT); + $stmt->execute(); + + // コミット + $pdo->commit(); + + // 更新されたデータを取得 + $stmt = $pdo->prepare("SELECT todos.id, todos.title, statuses.name AS status FROM todos JOIN statuses ON todos.status_id = statuses.id WHERE todos.id = :id;"); + $stmt->bindParam(':id', $id, PDO::PARAM_INT); + $stmt->execute(); + $updatedTodo = $stmt->fetch(PDO::FETCH_ASSOC); + + // 更新結果を返す + http_response_code(200); + echo json_encode([ + 'status' => 'ok', + 'todos' => $updatedTodo + ], JSON_UNESCAPED_UNICODE); + } catch (Exception $e) { + $pdo->rollBack(); // 失敗時にロールバック + http_response_code(500); + echo json_encode([ + 'status' => 'error', + 'message' => 'Failed to update todo', + 'error' => $e->getMessage() + ], JSON_UNESCAPED_UNICODE); + } + exit; +} + + +/** + * `/todos/{id}` エンドポイントを処理します。(特定のTodoを削除) + * + * @param PDO $pdo データベース接続のためのPDOインスタンス + * @param int $id 削除するTODOのID + * @return void + */ +function handleDeleteTodoById(PDO $pdo, int $id): void +{ + try { + // 削除前に対象のTodoを取得 + $stmt = $pdo->prepare("SELECT * FROM todos WHERE id = :id;"); + $stmt->bindParam(':id', $id, PDO::PARAM_INT); + $stmt->execute(); + $todo = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$todo) { + // Todoが見つからない場合 + http_response_code(404); + echo json_encode([ + 'status' => 'error', + 'message' => 'Todoが見つかりません' + ], JSON_UNESCAPED_UNICODE); + exit; + } + + // Todoを削除 + $stmt = $pdo->prepare("DELETE FROM todos WHERE id = :id;"); + $stmt->bindParam(':id', $id, PDO::PARAM_INT); + $stmt->execute(); + + if ($stmt->rowCount() > 0) { + // 削除成功時 + http_response_code(200); + echo json_encode([ + 'status' => 'ok', + 'todos' => $todo // 削除前のデータを返す + ], JSON_UNESCAPED_UNICODE); + } else { + // 予期しないエラー(通常は発生しないが念のため) + http_response_code(500); + echo json_encode([ + 'status' => 'error', + 'message' => '削除に失敗しました' + ], JSON_UNESCAPED_UNICODE); + } + } catch (Exception $e) { + // クエリエラー時のレスポンス + http_response_code(500); + echo json_encode([ + 'status' => 'error', + 'message' => 'エラーが発生しました', + 'error' => $e->getMessage() + ], JSON_UNESCAPED_UNICODE); + } + exit; +} diff --git a/backend_training/bin/test_api.sh b/backend_training/bin/test_api.sh index 7f23bca..e582716 100644 --- a/backend_training/bin/test_api.sh +++ b/backend_training/bin/test_api.sh @@ -181,4 +181,4 @@ menu() { } # Run all tests by default if no input is provided -menu +menu \ No newline at end of file diff --git a/frontend_training/src/App.css b/frontend_training/src/App.css index cce452c..d0d95af 100644 --- a/frontend_training/src/App.css +++ b/frontend_training/src/App.css @@ -1,8 +1,120 @@ #root { + display: flex; + flex-direction: column; + align-items: center; max-width: 1280px; + width: 100%; margin: 0 auto; padding: 2rem; +} + +.input { display: flex; - flex-direction: column; - align-items: center; + justify-content: center; + width: 100%; + gap: 10px; +} + +.todo-input { + width: 50%; + padding: 8px 12px; + font-size: 12px; + border: 1px solid rgb(184, 184, 184); + border-radius: 4px; + outline: none; +} + +.todo-input:focus { + border-color: rgb(58, 117, 194); + box-shadow: 0 0 0 1px rgb(58, 117, 194); +} + +.input .add-button { + display: inline-block; + padding: 8px 12px; + font-size: 12px; + font-weight: bold; + color: white; + background-color: black; + border: 1px solid black; + border-radius: 4px; + cursor: pointer; +} + +.input .add-button:hover { + opacity: 0.7; +} + +li { + margin-bottom: 20px; + cursor: pointer; } + +li.editing { + list-style-type: none; +} + +.complete-button { + display: inline-block; + margin-left: 30px; + padding: 2px 12px; + font-size: 12px; + font-weight: bold; + color: white; + background-color: green; + border: 1px solid green; + border-radius: 4px; + cursor: pointer; +} + +.complete-button:hover { + opacity: 0.7; +} + +.button-group { + display: flex; + justify-content: center; + width: 100%; + gap: 10px; +} + +.update-button { + display: inline-block; + margin-left: 30px; + padding: 2px 12px; + font-size: 14px; + font-weight: bold; + color: white; + background-color: rgb(0, 102, 255); + border: 1px solid rgb(0, 102, 255); + border-radius: 4px; + cursor: pointer; + white-space: nowrap; +} + +.update-button:hover { + opacity: 0.7; +} + +.cancel-button { + display: inline-block; + margin-left: 5px; + padding: 2px 12px; + font-size: 14px; + font-weight: bold; + color: white; + background-color: red; + border: 1px solid red; + border-radius: 4px; + cursor: pointer; + white-space: nowrap; +} + +.cancel-button:hover { + opacity: 0.7; +} + +.edit-container { + display: flex; + gap: 1px; +} \ No newline at end of file diff --git a/frontend_training/src/App.tsx b/frontend_training/src/App.tsx index 200cfb8..40c0936 100644 --- a/frontend_training/src/App.tsx +++ b/frontend_training/src/App.tsx @@ -1,12 +1,108 @@ -import './App.css' +import { useState } from "react"; +import { v4 as uuidv4 } from "uuid"; +import "./App.css"; + +function EditTodo({ + todo, + saveEdit, + cancelEdit +}: { + todo: { id: string; name: string }; + saveEdit: (id: string, newName: string) => void; + cancelEdit: (id: string) => void; +}) { + const [editName, setEditName] = useState(todo.name); + + return ( +
+ {/* 横並びの要素 */} + setEditName(e.target.value)} + /> + + +
+ ); +} function App() { + const [name, setName] = useState(""); + const [todos, setTodos] = useState<{ id: string; name: string; isComplete: boolean; isEdit: boolean }[]>([]); + + function toggleEdit(id: string) { + setTodos(todos.map(todo => + ({ ...todo, isEdit: todo.id === id }) + )); + } + + function saveEdit(id: string, newName: string) { + setTodos(todos.map(todo => + todo.id === id ? { ...todo, name: newName, isEdit: false } : todo + )); + } + + function cancelEdit(id: string) { + setTodos(todos.map(todo => + todo.id === id ? { ...todo, isEdit: false } : todo + )); + } return ( <>

TODOアプリ

+
+ setName(e.target.value)} + /> + +
+ + - ) + ); } -export default App +export default App;