Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .vscode/mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"servers": {
"github-mcp-server": {
"url": "https://api.githubcopilot.com/mcp/",
"type": "http"
}
},
"inputs": []
}
Binary file added __pycache__/app.cpython-311.pyc
Binary file not shown.
Binary file added __pycache__/test_app.cpython-311.pyc
Binary file not shown.
10 changes: 10 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
return render_template('index.html')

if __name__ == '__main__':
app.run(debug=True)
97 changes: 97 additions & 0 deletions architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# ポモドーロタイマーWebアプリケーション アーキテクチャ案

## 概要
Flask(Python)+HTML/CSS/JavaScriptで構築するポモドーロタイマーWebアプリのアーキテクチャ設計案です。

---

## 1. 構成概要

- **バックエンド**: Flask(Python)
- タイマー状態管理(必要ならセッションやDB)
- 設定値(作業時間・休憩時間など)の保存・取得API
- 履歴管理(拡張時)

- **フロントエンド**: HTML/CSS/JavaScript
- タイマー表示・操作(スタート/ストップ/リセット)
- 設定変更UI
- 状態の動的更新(JSでカウントダウン、通知)

- **通信**: REST API(FlaskのエンドポイントをAJAXで呼び出し)
- タイマー開始/停止/リセット
- 設定値取得・保存

---

## 2. ディレクトリ構成例

```
project-root/
├── app.py # Flaskアプリ本体
├── templates/
│ └── index.html # メイン画面
├── static/
│ ├── css/
│ │ └── style.css
│ └── js/
│ └── timer.js
├── models.py # 必要ならDBモデル
├── config.py # 設定管理
├── requirements.txt
├── architecture.md # アーキテクチャ設計書
└── README.md
```

---

## 3. 設計ポイント

- **タイマーは基本的にフロントエンド(JS)で動かす**
- サーバー負荷軽減&レスポンス向上
- 履歴や複数端末同期が必要ならサーバー側で状態管理

- **FlaskはAPIサーバーとして設計**
- `/api/start`, `/api/stop`, `/api/reset`, `/api/settings` などのエンドポイント
- 必要に応じてDB(SQLite等)で履歴保存

- **フロントはSPA的に動的UIを構築**
- タイマーの残り時間表示・進捗バー
- 作業/休憩の切り替え
- 通知(音・バイブ・ポップアップ)

- **テスト・保守性を意識**
- JSはモジュール化
- FlaskはBlueprintでAPI分離も検討

---

## 4. ユニットテスト容易性のための工夫

- バックエンドロジック(タイマー・設定管理)は関数・クラスとして分離し、Flaskルートから独立させる
- BlueprintでAPI分割
- 依存注入(DI)でDBや設定値をテスト用に差し替え可能に
- テスト用設定(config.py)
- APIレスポンスはJSONで統一
- JSは関数・クラス化し、Jest等でテスト可能に
- fetch/AJAX部はラップしてモック化しやすく
- Flaskの`test_client()`でAPI単体テスト
- テスト用DB(SQLiteインメモリ)やセッション利用

---

## 5. 拡張性

- ユーザー認証(履歴管理や複数端末同期)
- タスク管理機能
- 統計・グラフ表示

---

## まとめ

- タイマーはJSで動かし、FlaskはAPIと設定・履歴管理に特化
- ディレクトリ分離で保守性向上
- RESTfulなAPI設計+SPA的UIで快適な操作性
- ユニットテスト容易な設計・テストフレームワーク導入

この設計で進めることで、拡張性・保守性・品質の高いWebアプリが実現できます。
257 changes: 257 additions & 0 deletions delivery_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import time
import random
from typing import List, Callable, Optional
from dataclasses import dataclass, field
from enum import Enum


class EventArgs:
"""イベント引数の基底クラス"""
pass


class Event:
"""C#のeventに相当するクラス"""

def __init__(self):
self._handlers: List[Callable] = []

def add_handler(self, handler: Callable):
"""イベントハンドラーを追加"""
if handler not in self._handlers:
self._handlers.append(handler)

def remove_handler(self, handler: Callable):
"""イベントハンドラーを削除"""
if handler in self._handlers:
self._handlers.remove(handler)

def invoke(self, sender, args: EventArgs = None):
"""イベントを発火"""
for handler in self._handlers:
handler(sender, args or EventArgs())


@dataclass
class KitchenObjectSO:
"""キッチンオブジェクトのデータクラス"""
name: str
object_id: int


@dataclass
class RecipeSO:
"""レシピのデータクラス"""
name: str
kitchen_object_so_list: List[KitchenObjectSO] = field(default_factory=list)


@dataclass
class RecipeListSO:
"""レシピリストのデータクラス"""
recipe_so_list: List[RecipeSO] = field(default_factory=list)


class PlateKitchenObject:
"""皿のキッチンオブジェクト"""

def __init__(self):
self._kitchen_object_so_list: List[KitchenObjectSO] = []

def add_kitchen_object(self, kitchen_object: KitchenObjectSO):
"""キッチンオブジェクトを追加"""
self._kitchen_object_so_list.append(kitchen_object)

def get_kitchen_object_so_list(self) -> List[KitchenObjectSO]:
"""キッチンオブジェクトリストを取得"""
return self._kitchen_object_so_list.copy()


class KitchenGameManager:
"""キッチンゲームマネージャー(Singleton)"""

_instance: Optional['KitchenGameManager'] = None

def __init__(self):
self._is_game_playing = False

@classmethod
def get_instance(cls) -> 'KitchenGameManager':
"""Singletonインスタンスを取得"""
if cls._instance is None:
cls._instance = cls()
return cls._instance

def is_game_playing(self) -> bool:
"""ゲームが進行中かどうか"""
return self._is_game_playing

def start_game(self):
"""ゲーム開始"""
self._is_game_playing = True

def stop_game(self):
"""ゲーム停止"""
self._is_game_playing = False


class DeliveryManager:
"""配達管理クラス(Python版)"""

# クラス定数
SPAWN_RECIPE_TIMER_MAX = 4.0
WAITING_RECIPES_MAX = 4

_instance: Optional['DeliveryManager'] = None

def __init__(self, recipe_list_so: RecipeListSO):
# イベント定義
self.on_recipe_spawned = Event()
self.on_recipe_completed = Event()
self.on_recipe_success = Event()
self.on_recipe_failed = Event()

# プライベート変数
self._recipe_list_so = recipe_list_so
self._waiting_recipe_so_list: List[RecipeSO] = []
self._spawn_recipe_timer = 0.0
self._successful_recipes_amount = 0
self._last_update_time = time.time()

@classmethod
def initialize(cls, recipe_list_so: RecipeListSO):
"""DeliveryManagerを初期化"""
if cls._instance is None:
cls._instance = cls(recipe_list_so)

@classmethod
def get_instance(cls) -> 'DeliveryManager':
"""Singletonインスタンスを取得"""
if cls._instance is None:
raise ValueError("DeliveryManagerが初期化されていません。initialize()を先に呼び出してください")
return cls._instance

@classmethod
def reset(cls):
"""テスト用にインスタンスをリセット"""
cls._instance = None

def update(self):
"""フレーム更新処理(UnityのUpdate相当)"""
current_time = time.time()
delta_time = current_time - self._last_update_time
self._last_update_time = current_time

self._spawn_recipe_timer -= delta_time

if self._spawn_recipe_timer <= 0.0:
self._spawn_recipe_timer = self.SPAWN_RECIPE_TIMER_MAX

kitchen_game_manager = KitchenGameManager.get_instance()
if (kitchen_game_manager.is_game_playing() and
len(self._waiting_recipe_so_list) < self.WAITING_RECIPES_MAX):

# ランダムにレシピを選択
waiting_recipe_so = random.choice(self._recipe_list_so.recipe_so_list)
self._waiting_recipe_so_list.append(waiting_recipe_so)

# イベント発火
self.on_recipe_spawned.invoke(self)

def _ingredients_match(self, recipe_ingredients: List[KitchenObjectSO], plate_ingredients: List[KitchenObjectSO]) -> bool:
"""材料リストが一致するかセットで判定(順序・重複なし)"""
return set(recipe_ingredients) == set(plate_ingredients)

def deliver_recipe(self, plate_kitchen_object: Optional[PlateKitchenObject]):
"""レシピの材料と皿の材料が一致しているかどうかを確認する"""
if plate_kitchen_object is None:
print("Error: plate_kitchen_objectがNoneです")
self.on_recipe_failed.invoke(self)
return

plate_ingredients = plate_kitchen_object.get_kitchen_object_so_list()
if not plate_ingredients:
print("Error: 皿の材料が空です")
self.on_recipe_failed.invoke(self)
return

for i, waiting_recipe_so in enumerate(self._waiting_recipe_so_list):
recipe_ingredients = waiting_recipe_so.kitchen_object_so_list
if len(recipe_ingredients) != len(plate_ingredients):
continue # 材料数が違う場合はスキップ

if self._ingredients_match(recipe_ingredients, plate_ingredients):
self._successful_recipes_amount += 1
self._waiting_recipe_so_list.pop(i)
self.on_recipe_completed.invoke(self)
self.on_recipe_success.invoke(self)
return

# 一致するレシピが見つからなかった場合
print("Error: 一致するレシピが見つかりません")
self.on_recipe_failed.invoke(self)

def get_waiting_recipe_so_list(self) -> List[RecipeSO]:
"""待機中のレシピリストを取得"""
return self._waiting_recipe_so_list.copy()

def get_successful_recipes_amount(self) -> int:
"""成功したレシピ数を取得"""
return self._successful_recipes_amount


# 使用例
if __name__ == "__main__":
# サンプルデータ作成
tomato = KitchenObjectSO("Tomato", 1)
lettuce = KitchenObjectSO("Lettuce", 2)
bread = KitchenObjectSO("Bread", 3)

# サンプルレシピ
sandwich_recipe = RecipeSO("Sandwich", [bread, lettuce, tomato])
salad_recipe = RecipeSO("Salad", [lettuce, tomato])

recipe_list = RecipeListSO([sandwich_recipe, salad_recipe])

# ゲームマネージャーとデリバリーマネージャーを初期化
game_manager = KitchenGameManager.get_instance()
game_manager.start_game()

DeliveryManager.initialize(recipe_list)
delivery_manager = DeliveryManager.get_instance()

# イベントハンドラーの設定
def on_recipe_spawned(sender, args):
print("新しいレシピが生成されました!")

def on_recipe_success(sender, args):
print("レシピ配達成功!")

def on_recipe_failed(sender, args):
print("レシピ配達失敗...")

delivery_manager.on_recipe_spawned.add_handler(on_recipe_spawned)
delivery_manager.on_recipe_success.add_handler(on_recipe_success)
delivery_manager.on_recipe_failed.add_handler(on_recipe_failed)

# サンプル実行
print("ゲーム開始...")

# 5秒間更新処理を実行
start_time = time.time()
while time.time() - start_time < 5:
delivery_manager.update()
time.sleep(0.1) # 100ms間隔で更新

print(f"待機中のレシピ数: {len(delivery_manager.get_waiting_recipe_so_list())}")

# サンプル配達テスト
plate = PlateKitchenObject()
plate.add_kitchen_object(bread)
plate.add_kitchen_object(lettuce)
plate.add_kitchen_object(tomato)

print("サンドイッチを配達...")
delivery_manager.deliver_recipe(plate)

print(f"成功したレシピ数: {delivery_manager.get_successful_recipes_amount()}")
Loading