From 3be16a2d7731ff6ef6803a3709de07d7fff39fa5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 08:19:59 +0000 Subject: [PATCH 1/4] Initial plan From b60ac5e95767b617a0a732d38a3744b547338ae4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 08:32:29 +0000 Subject: [PATCH 2/4] Implement frontend and API integration for Step 4 Co-authored-by: moulongzhang <39043782+moulongzhang@users.noreply.github.com> --- .gitignore | 1 + API_INTEGRATION_README.md | 142 +++++++++++ index.html | 503 ++++++++++++++++++++++++++++++++++++++ main.py | 194 +++++++++++++++ requirements.txt | 2 + test_api.py | 92 +++++++ 6 files changed, 934 insertions(+) create mode 100644 .gitignore create mode 100644 API_INTEGRATION_README.md create mode 100644 index.html create mode 100644 requirements.txt create mode 100644 test_api.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c18dd8d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/API_INTEGRATION_README.md b/API_INTEGRATION_README.md new file mode 100644 index 00000000..9b4180cc --- /dev/null +++ b/API_INTEGRATION_README.md @@ -0,0 +1,142 @@ +# フロントエンドとAPIの連携 (Step 4) + +## 概要 +このステップでは、フロントエンドとバックエンドAPIを連携させ、進捗データの取得・保存、作業完了時の更新、通信エラーハンドリングを実装しました。 + +## 実装内容 + +### 1. バックエンドAPI (main.py) +Flask RESTful APIを使用して以下のエンドポイントを実装: + +#### エンドポイント一覧: +- `GET /` - フロントエンドHTMLページを提供 +- `GET /api/progress` - 現在の進捗データを取得 +- `GET /api/recipes` - 待機中のレシピリストを取得 +- `POST /api/deliver` - レシピを配達 (材料をJSON形式で送信) +- `POST /api/start` - ゲームを開始 +- `POST /api/stop` - ゲームを停止 + +#### 主な機能: +- バックグラウンドスレッドでゲームロジックを継続的に更新 +- CORS対応 (フロントエンドとの通信を許可) +- エラーハンドリング (try-catchブロックで全エンドポイントを保護) +- JSON形式での統一されたレスポンス構造 + +### 2. フロントエンド (index.html) +モダンなUIを持つシングルページアプリケーション: + +#### 主な機能: +- **リアルタイム進捗表示**: 成功したレシピ数と待機中のレシピ数 +- **レシピリスト表示**: 待機中のレシピとその材料を視覚的に表示 +- **レシピ配達**: 材料選択と配達機能 +- **ゲームコントロール**: 開始/停止/データ更新 +- **自動更新**: 3秒ごとに進捗データを自動取得 +- **エラーハンドリング**: + - 通信エラーの検知と表示 + - HTTPステータスコードのチェック + - ユーザーフレンドリーなエラーメッセージ + - 成功メッセージの表示 + +### 3. エラーハンドリング +以下の種類のエラーに対応: + +- **ネットワークエラー**: fetch APIの例外キャッチ +- **HTTPエラー**: ステータスコードのチェック +- **アプリケーションエラー**: サーバーからのエラーレスポンス処理 +- **データ検証**: 不正なデータ入力時のエラー + +## 使用方法 + +### 1. 依存関係のインストール +```bash +pip install -r requirements.txt +``` + +### 2. サーバーの起動 +```bash +python3 main.py +``` + +### 3. フロントエンドへのアクセス +ブラウザで以下のURLを開く: +``` +http://localhost:5000/ +``` + +### 4. テストの実行 +```bash +python3 -m unittest test_api.py -v +``` + +## API使用例 + +### 進捗データの取得 +```bash +curl http://localhost:5000/api/progress +``` + +レスポンス: +```json +{ + "success": true, + "data": { + "successful_recipes": 5, + "waiting_recipes_count": 3 + } +} +``` + +### レシピの配達 +```bash +curl -X POST http://localhost:5000/api/deliver \ + -H "Content-Type: application/json" \ + -d '{"ingredients": ["Bread", "Lettuce", "Tomato"]}' +``` + +レスポンス: +```json +{ + "success": true, + "data": { + "delivered": true, + "successful_recipes": 6 + } +} +``` + +### エラー例 +```bash +curl -X POST http://localhost:5000/api/deliver \ + -H "Content-Type: application/json" \ + -d '{"ingredients": ["Unknown"]}' +``` + +レスポンス: +```json +{ + "success": false, + "error": "Unknown ingredient: Unknown" +} +``` + +## 技術スタック + +- **バックエンド**: Flask 3.0.0, Flask-CORS 4.0.0 +- **フロントエンド**: HTML5, CSS3, JavaScript (Vanilla JS) +- **API通信**: Fetch API +- **テスト**: Python unittest + +## セキュリティ考慮事項 + +- CORSを適切に設定 +- 入力データの検証 +- エラーメッセージに機密情報を含めない +- 本番環境ではFlaskのdebugモードを無効化すること + +## 今後の改善点 + +- 認証・認可の実装 +- WebSocketによるリアルタイム通信 +- より詳細なエラーログ +- パフォーマンスの最適化 +- E2Eテストの追加 diff --git a/index.html b/index.html new file mode 100644 index 00000000..d393e065 --- /dev/null +++ b/index.html @@ -0,0 +1,503 @@ + + + + + + キッチンゲーム - 進捗管理 + + + +
+

🍳 キッチンゲーム - 進捗管理

+ +
+
+

📊 成功したレシピ数

+
-
+
+ +
+

⏳ 待機中のレシピ数

+
-
+
+
+ +
+
+

📋 待機中のレシピ

+
+

読み込み中...

+
+
+ +
+

🎮 ゲームコントロール

+
+ + + +
+ +
+

🍽️ レシピを配達

+

材料を選択してください:

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+
+ + + + diff --git a/main.py b/main.py index e69de29b..ce346371 100644 --- a/main.py +++ b/main.py @@ -0,0 +1,194 @@ +from flask import Flask, jsonify, request, send_from_directory +from flask_cors import CORS +from deliverManager import ( + DeliveryManager, KitchenGameManager, RecipeListSO, RecipeSO, + KitchenObjectSO, PlateKitchenObject +) +import threading +import time +import os + +app = Flask(__name__) +CORS(app) + +# Initialize game data +tomato = KitchenObjectSO("Tomato", 1) +lettuce = KitchenObjectSO("Lettuce", 2) +bread = KitchenObjectSO("Bread", 3) +cheese = KitchenObjectSO("Cheese", 4) + +# Sample recipes +sandwich_recipe = RecipeSO("Sandwich", [bread, lettuce, tomato]) +salad_recipe = RecipeSO("Salad", [lettuce, tomato]) +cheese_sandwich_recipe = RecipeSO("Cheese Sandwich", [bread, cheese, lettuce]) + +recipe_list = RecipeListSO([sandwich_recipe, salad_recipe, cheese_sandwich_recipe]) + +# Initialize game manager and delivery manager +game_manager = KitchenGameManager.get_instance() +game_manager.start_game() + +delivery_manager = DeliveryManager.get_instance(recipe_list) + +# Background thread to update delivery manager +update_thread = None +should_stop = False + + +def update_loop(): + """Background thread to update the delivery manager""" + global should_stop + while not should_stop: + delivery_manager.update() + time.sleep(0.1) # Update every 100ms + + +@app.route('/') +def index(): + """Serve the frontend HTML page""" + return send_from_directory('.', 'index.html') + + +@app.route('/api/progress', methods=['GET']) +def get_progress(): + """Get current progress data""" + try: + progress_data = { + 'successful_recipes': delivery_manager.get_successful_recipes_amount(), + 'waiting_recipes_count': len(delivery_manager.get_waiting_recipe_so_list()) + } + return jsonify({ + 'success': True, + 'data': progress_data + }), 200 + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + +@app.route('/api/recipes', methods=['GET']) +def get_recipes(): + """Get list of waiting recipes""" + try: + waiting_recipes = delivery_manager.get_waiting_recipe_so_list() + recipes_data = [ + { + 'name': recipe.name, + 'ingredients': [ + {'name': ingredient.name, 'id': ingredient.object_id} + for ingredient in recipe.kitchen_object_so_list + ] + } + for recipe in waiting_recipes + ] + return jsonify({ + 'success': True, + 'data': recipes_data + }), 200 + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + +@app.route('/api/deliver', methods=['POST']) +def deliver_recipe(): + """Deliver a recipe with specified ingredients""" + try: + data = request.get_json() + if not data or 'ingredients' not in data: + return jsonify({ + 'success': False, + 'error': 'Missing ingredients data' + }), 400 + + # Create a plate with the specified ingredients + plate = PlateKitchenObject() + ingredient_objects = { + 'Tomato': tomato, + 'Lettuce': lettuce, + 'Bread': bread, + 'Cheese': cheese + } + + for ingredient_name in data['ingredients']: + if ingredient_name in ingredient_objects: + plate.add_kitchen_object(ingredient_objects[ingredient_name]) + else: + return jsonify({ + 'success': False, + 'error': f'Unknown ingredient: {ingredient_name}' + }), 400 + + # Try to deliver the recipe + initial_success_count = delivery_manager.get_successful_recipes_amount() + delivery_manager.deliver_recipe(plate) + final_success_count = delivery_manager.get_successful_recipes_amount() + + # Check if delivery was successful + is_successful = final_success_count > initial_success_count + + return jsonify({ + 'success': True, + 'data': { + 'delivered': is_successful, + 'successful_recipes': final_success_count + } + }), 200 + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + +@app.route('/api/start', methods=['POST']) +def start_game(): + """Start the game""" + try: + game_manager.start_game() + return jsonify({ + 'success': True, + 'message': 'Game started' + }), 200 + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + +@app.route('/api/stop', methods=['POST']) +def stop_game(): + """Stop the game""" + try: + game_manager.stop_game() + return jsonify({ + 'success': True, + 'message': 'Game stopped' + }), 200 + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + +def start_background_thread(): + """Start the background update thread""" + global update_thread, should_stop + should_stop = False + update_thread = threading.Thread(target=update_loop, daemon=True) + update_thread.start() + + +if __name__ == '__main__': + # Start the background thread for game updates + start_background_thread() + + # Run the Flask app + port = int(os.environ.get('PORT', 5000)) + app.run(host='0.0.0.0', port=port, debug=True, use_reloader=False) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..6acd0bf2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask==3.0.0 +flask-cors==4.0.0 diff --git a/test_api.py b/test_api.py new file mode 100644 index 00000000..304ece0d --- /dev/null +++ b/test_api.py @@ -0,0 +1,92 @@ +"""API統合テスト""" +import unittest +import json +from main import app, delivery_manager, game_manager + + +class TestAPIIntegration(unittest.TestCase): + """フロントエンドとAPIの統合テスト""" + + def setUp(self): + """テストセットアップ""" + self.app = app.test_client() + self.app.testing = True + game_manager.start_game() + + def test_get_progress(self): + """進捗データ取得のテスト""" + response = self.app.get('/api/progress') + self.assertEqual(response.status_code, 200) + + data = json.loads(response.data) + self.assertTrue(data['success']) + self.assertIn('successful_recipes', data['data']) + self.assertIn('waiting_recipes_count', data['data']) + + def test_get_recipes(self): + """レシピリスト取得のテスト""" + response = self.app.get('/api/recipes') + self.assertEqual(response.status_code, 200) + + data = json.loads(response.data) + self.assertTrue(data['success']) + self.assertIsInstance(data['data'], list) + + def test_deliver_recipe_success(self): + """レシピ配達成功のテスト""" + response = self.app.post( + '/api/deliver', + data=json.dumps({'ingredients': ['Lettuce', 'Tomato']}), + content_type='application/json' + ) + self.assertEqual(response.status_code, 200) + + data = json.loads(response.data) + self.assertTrue(data['success']) + self.assertIn('delivered', data['data']) + + def test_deliver_recipe_invalid_ingredient(self): + """無効な材料でのエラーハンドリングテスト""" + response = self.app.post( + '/api/deliver', + data=json.dumps({'ingredients': ['InvalidIngredient']}), + content_type='application/json' + ) + self.assertEqual(response.status_code, 400) + + data = json.loads(response.data) + self.assertFalse(data['success']) + self.assertIn('error', data) + + def test_deliver_recipe_missing_data(self): + """データ不足時のエラーハンドリングテスト""" + response = self.app.post( + '/api/deliver', + data=json.dumps({}), + content_type='application/json' + ) + self.assertEqual(response.status_code, 400) + + data = json.loads(response.data) + self.assertFalse(data['success']) + self.assertIn('error', data) + + def test_start_game(self): + """ゲーム開始のテスト""" + response = self.app.post('/api/start') + self.assertEqual(response.status_code, 200) + + data = json.loads(response.data) + self.assertTrue(data['success']) + + def test_stop_game(self): + """ゲーム停止のテスト""" + response = self.app.post('/api/stop') + self.assertEqual(response.status_code, 200) + + data = json.loads(response.data) + self.assertTrue(data['success']) + + +if __name__ == '__main__': + unittest.main() From 22b87c02c217d6522985be510e00fd67a0e3799c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 08:35:20 +0000 Subject: [PATCH 3/4] Address code review feedback - improve thread safety and security Co-authored-by: moulongzhang <39043782+moulongzhang@users.noreply.github.com> --- index.html | 3 ++- main.py | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/index.html b/index.html index d393e065..b5dbac71 100644 --- a/index.html +++ b/index.html @@ -297,7 +297,8 @@

🍽️ レシピを配達