From 9142f62de2d06bf83af970192e4494f9076271ac Mon Sep 17 00:00:00 2001 From: antigravity Date: Thu, 16 Apr 2026 08:25:44 +0800 Subject: [PATCH 01/11] docs: update commit skill --- .agents/skills/commit/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/skills/commit/SKILL.md b/.agents/skills/commit/SKILL.md index e9858756..3300e386 100644 --- a/.agents/skills/commit/SKILL.md +++ b/.agents/skills/commit/SKILL.md @@ -119,4 +119,4 @@ git push **Q: 不小心 commit 了不該 commit 的檔案?** -告訴 AI:「我剛才不小心 commit 了 [檔案名稱],請幫我移除。」 +告訴 AI:「我剛才不小心 commit 了 [檔案名稱],請幫我移除。」 \ No newline at end of file From ebd903c95e9904f910bd6382eac1ff873127fa78 Mon Sep 17 00:00:00 2001 From: antigravity Date: Thu, 16 Apr 2026 08:29:53 +0800 Subject: [PATCH 02/11] docs: add PRD --- docs/PRD.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 docs/PRD.md diff --git a/docs/PRD.md b/docs/PRD.md new file mode 100644 index 00000000..ee29c7e9 --- /dev/null +++ b/docs/PRD.md @@ -0,0 +1,52 @@ +# 產品需求文件 (PRD):食譜管理系統 + +## 1. 專案概述 +- **背景與動機**:現代人生活忙碌,常常不知道要煮什麼,或是想要把好吃的食譜記錄下來以便未來查詢。此系統提供一個簡單、直覺的平台讓使用者存放、尋找,甚至根據手邊現有食材自動尋找推薦的食譜。 +- **目標用戶**:我 (個人愛好者、家庭煮夫/婦、對料理有熱忱的自煮族)。 +- **核心價值主張**:更有效率地管理個人食譜,並解決「今天不知道要吃什麼」的煩惱,輔助使用者最大化利用家中現存食材。 + +## 2. 功能需求 +系統將提供以下主要功能: + +1. **新增食譜** + - *使用者故事*:作為一位熱愛烹飪的使用者,我希望能夠新增食譜(記錄名稱、食材、步驟等),以便儲存我喜歡的菜餚。 +2. **搜尋食譜** + - *使用者故事*:作為一位使用者,我希望能透過關鍵字搜尋食譜名稱,以便快速找到我想做的菜。 +3. **用食材推薦食譜** + - *使用者故事*:作為一位面對冰箱剩餘食材的使用者,我希望能夠輸入手邊現有食材,由系統過濾並推薦能做的食譜,以便不浪費食物。 +4. **編輯與刪除食譜** + - *使用者故事*:作為一位記錄者,我希望可以能夠編輯或刪除已經存入的食譜,以便我的食譜庫隨時保持正確、最新。 +5. **食譜分類與標籤** + - *使用者故事*:作為一位習慣有條理整理食譜的使用者,我希望可以為食譜加上分類或標籤(如:前菜、甜點、全素、低卡等),以便後續依照標籤快速篩選食譜。 + +## 3. 非功能需求 +- **技術限制**: + - 後端框架:Python + Flask + - 模板引擎:Jinja2 + - 資料庫:SQLite +- **效能與安全考量**: + - 確保食譜搜尋可快速回傳結果。 + - 使用適當的安全機制來防範常見的安全漏洞(如:防護 SQL Injection, Jinja2 預設防護 XSS)。 + +## 4. MVP 範圍 + +### Must Have (必備功能 - 初期實作) +- 新增與儲存食譜功能。 +- 食譜總覽與明細頁面。 +- 透過關鍵字搜尋食譜。 + +### Should Have (應有功能 - 中期實作) +- 用食材推薦/篩選食譜功能。 +- 編輯與刪除食譜。 + +### Nice to Have (加分功能 - 未來擴充) +- 食譜分類與標籤。 +- 食譜圖片上傳功能。 + +## 5. 專案成員與分工 + +| 角色 | 姓名 | 負責項目 | +| --- | --- | --- | +| PM | [請填寫] | 需求定義與時程控管 | +| 前端開發 | [請填寫] | 網頁介面設計、Jinja2 模板實作 | +| 後端開發 | [請填寫] | Flask API 與路由設定、SQLite 結構實作 | From a36d46955170c410013d3cfab56490fe86556666 Mon Sep 17 00:00:00 2001 From: antigravity Date: Thu, 16 Apr 2026 08:30:58 +0800 Subject: [PATCH 03/11] docs: add system architecture --- docs/ARCHITECTURE.md | 84 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 docs/ARCHITECTURE.md diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 00000000..88f96938 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,84 @@ +# 系統架構設計文件:食譜管理系統 + +## 1. 技術架構說明 + +本專案採用輕量級的 Python 網頁框架設計,不採用前後端分離,由後端直接渲染 HTML 頁面返回給使用者的瀏覽器。 + +### 選用技術與原因 +- **後端框架:Python + Flask** + - **原因**:Flask 相較於 Django 更為輕量與彈性,非常適合用來開發這類中小型內容管理系統。其學習曲線平緩且易於快速打造原型。 +- **模板引擎:Jinja2** + - **原因**:與 Flask 高度整合,能直接將後端資料無縫嵌入前端 HTML 中,有效降低開發複雜度,並且內建自動轉義防護 XSS 攻擊。 +- **資料庫:SQLite** + - **原因**:不需額外安裝資料庫伺服器,資料儲存在單一檔案中,易於備份與部署,完全符合 MVP (最小可行性產品) 與個人使用的效能需求。 + +### Flask MVC 模式說明 +雖然 Flask 本身不強制要求 MVC 架構,但我們將依循 MVC (Model-View-Controller) 的概念來組織程式碼: +- **Model (模型)**:負責與 SQLite 資料庫溝通,定義「食譜 (Recipe)」、「標籤 (Tag)」等資料結構與操作。 +- **View (視圖)**:由 Jinja2 模板擔任,負責產生最終的 HTML 介面,呈現資料給使用者。 +- **Controller (控制器)**:由 Flask 的路由 (Routes) 擔任,負責接收使用者請求、調用 Model 獲取或更新資料、接著把資料傳遞給 View 進行渲染。 + +## 2. 專案資料夾結構 + +以下是本專案的資料夾結構與各階層職責說明: + +```text +web_app_development/ +├── app/ # 應用程式主目錄 +│ ├── __init__.py # 初始化 Flask 應用程式與套件配置 +│ ├── models/ # (Model) 資料庫模型定義與操作 +│ │ └── recipe.py # 食譜相關的資料庫邏輯 +│ ├── routes/ # (Controller) Flask 路由處理邏輯 +│ │ ├── __init__.py +│ │ └── recipe.py # 處理食譜的增刪改查與搜尋推薦路由 +│ ├── templates/ # (View) Jinja2 HTML 頁面模板 +│ │ ├── base.html # 共用版型 (標題、導覽列) +│ │ └── recipes/ # 食譜相關頁面 +│ │ ├── index.html # 食譜列表/首頁 +│ │ ├── show.html # 食譜明細頁面 +│ │ └── form.html # 新增與編輯表單 +│ └── static/ # css, js, 圖片等靜態資源 +│ ├── css/ +│ │ └── style.css +│ └── images/ # 存放使用者預設圖片等 +├── instance/ # 存放敏感或變動性的本地端資料 +│ └── database.db # SQLite 資料庫檔案 +├── docs/ # 專案文件 +│ ├── PRD.md # 產品需求文件 +│ └── ARCHITECTURE.md # 系統架構文件 +├── requirements.txt # Python 依賴套件清單 +└── app.py # 應用程式啟動入口 +``` + +## 3. 元件關係圖 + +以下展示使用者與系統互動時,各元件如何協作產生回應: + +```mermaid +sequenceDiagram + participant Browser as 瀏覽器 (使用者) + participant Route as Flask Route (Controller) + participant Model as Model (資料操作) + participant DB as SQLite 資料庫 + participant Template as Jinja2 Template (View) + + Browser->>Route: 1. 發送 HTTP 請求 (例如:搜尋食譜) + Route->>Model: 2. 呼叫邏輯獲取資料 + Model->>DB: 3. 執行 SQL 查詢 + DB-->>Model: 4. 回傳查詢結果 + Model-->>Route: 5. 回傳 Python 結構化資料 + Route->>Template: 6. 將資料傳入模板渲染 + Template-->>Route: 7. 產生最終 HTML 字串 + Route-->>Browser: 8. 回傳 HTTP 回應 (顯示網頁) +``` + +## 4. 關鍵設計決策 + +1. **採用 SSR (伺服器端渲染) 而非 SPA (單頁應用)** + - 作為初期 MVP 專案,使用 Flask + Jinja2 直接渲染頁面是最快看到成果的方式,避免了繁瑣的 API 介接與跨域問題,能讓重點集中於食譜核心邏輯。 +2. **採用 SQLite 單一檔案資料庫** + - 避免引入如 MySQL 或 PostgreSQL 帶來額外的效能與維護成本,SQLite 非常輕量且零設定,對於主要是單人使用的儲存與搜尋已綽綽有餘。 +3. **Model 與 Controller 邏輯分離** + - 雖然初期專案較小,但不把所有的資料庫存取層直接寫在 `app.py` 或具體的 route 中。將資料庫層抽象出 `app/models/`,能確保留下擴展「食譜標籤系統」與「食材推薦演算法」的彈性空間。 +4. **使用模板繼承機制 (共用 Base)** + - 透過 `base.html` 共用 Header、導覽列及自訂的 CSS 資源。日後不論是新增編輯頁面還是推薦食譜頁面,只需專注頁面本體,可達到最好的維護性並維持設計的一致感。 From 52b61c6dfbae203abee25b0f5c3640a145d0f487 Mon Sep 17 00:00:00 2001 From: antigravity Date: Thu, 16 Apr 2026 08:31:49 +0800 Subject: [PATCH 04/11] docs: add user flowchart --- docs/FLOWCHART.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/FLOWCHART.md diff --git a/docs/FLOWCHART.md b/docs/FLOWCHART.md new file mode 100644 index 00000000..26428f1c --- /dev/null +++ b/docs/FLOWCHART.md @@ -0,0 +1,79 @@ +# 流程圖設計文件:食譜管理系統 + +本文件根據產品需求文件 (PRD) 與系統架構文件,視覺化使用者在食譜管理系統中的操作流程、系統背後的處理步驟,以及功能與路由的對照表。 + +## 1. 使用者流程圖(User Flow) + +以下流程圖說明當使用者開啟網頁後,可以執行的各項功能及頁面轉換路徑: + +```mermaid +flowchart LR + A([使用者開啟網站]) --> B[首頁 - 食譜列表] + + B --> C{選擇欲執行的功能} + + %% 新增食譜路線 + C -->|點擊「新增食譜」| D[填寫新增表單頁面] + D -->|送出表單| B + + %% 搜尋/篩選路線 + C -->|輸入「關鍵字 / 食材」| E[呈現篩選後的列表] + E --> C + + %% 查看與編輯/刪除路線 + C -->|點擊某個「食譜項目」| F[食譜明細頁面] + F --> G{在明細頁中選擇操作} + + G -->|返回| B + G -->|點擊「編輯食譜」| H[填寫編輯表單頁面] + H -->|送出表單| F + G -->|點擊「刪除食譜」| I[確認並刪除] + I -->|刪除成功| B +``` + +## 2. 系統序列圖(Sequence Diagram) + +以下序列圖以核心功能**「新增食譜」**為例,展示從使用者介面送出資料到成功寫入資料庫並重導向的完整過程: + +```mermaid +sequenceDiagram + actor User as 使用者 + participant Browser as 瀏覽器 (模板渲染) + participant Flask Route as 路由 (Controller) + participant Model as 邏輯模型 (Model) + participant DB as SQLite 資料庫 + + User->>Browser: 在表單頁面填妥食譜資訊並點擊送出 + Browser->>Flask Route: 發送 POST /recipes 請求 (攜帶表單資料) + + Note over Flask Route, DB: 開始處理新增邏輯 + + Flask Route->>Model: 呼叫 Recipe.create(data) 傳入解析後的資料 + Model->>DB: 執行 SQL INSERT INTO recipes ... + DB-->>Model: 回傳寫入成功訊息 + Model-->>Flask Route: 回傳新建立的 Recipe 物件 + + Note over Flask Route, Browser: 處理畫面重導向 + + Flask Route-->>Browser: 回傳 302 Redirect 至首頁 (食譜列表) + Browser->>Flask Route: 發送 GET / 請求 + Flask Route->>Model: 取得最新所有食譜列表 + Model->>DB: 執行 SELECT * FROM recipes + DB-->>Model: 回傳新列資料 + Model-->>Flask Route: 列表資料 + Flask Route-->>Browser: 使用最新資料重新渲染 index.html (首頁) +``` + +## 3. 功能清單對照表 + +對應上述流程與 PRD 需求,以下為系統功能對應的 URL 路徑與 HTTP 方法整理,提供後續 API/路由設計的參考: + +| 功能項目說明 | HTTP 方法 | 預計對應的 URL 路徑 | View (Jinja2) | 備註 | +| --- | :---: | --- | --- | --- | +| **首頁 / 所有食譜總覽** | `GET` | `/` 或 `/recipes` | `index.html` | 可結合查詢參數 `?q=關鍵字` 處理搜尋與食材推薦功能。 | +| **進入新增食譜表單頁** | `GET` | `/recipes/new` | `form.html` | 呈現空白的輸入表單。 | +| **提交新增食譜資料** | `POST` | `/recipes` | *(處理無畫面)* | 處理完後 302 重導向回首頁。 | +| **查看單一食譜明細** | `GET` | `/recipes/` | `show.html` | 顯示特定 ID 的食譜完整步驟與內容。 | +| **進入編輯食譜表單頁** | `GET` | `/recipes//edit` | `form.html` | 呈現帶有原始資料的編輯表單。 | +| **提交更新的食譜資料** | `POST` | `/recipes//update` | *(處理無畫面)* | 使用 HTML form 故用 POST,更新完成後重導回明細頁。 | +| **確定刪除食譜** | `POST` | `/recipes//delete` | *(處理無畫面)* | 使用 form 觸發 POST,刪除完成後重導回首頁。 | From 392a284908dd6f000204e2a7acb089bc1142e402 Mon Sep 17 00:00:00 2001 From: t11003 Date: Thu, 16 Apr 2026 15:55:07 +0800 Subject: [PATCH 05/11] Update SKILL.md to include .gitignore check Add section on checking .gitignore before pushing changes. --- .agents/skills/commit/SKILL.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.agents/skills/commit/SKILL.md b/.agents/skills/commit/SKILL.md index 3300e386..a0ab7154 100644 --- a/.agents/skills/commit/SKILL.md +++ b/.agents/skills/commit/SKILL.md @@ -13,6 +13,9 @@ description: 提交並推送程式碼變更。用於每個開發階段完成後 - 要把程式碼推送到 GitHub,讓組員可以取得最新版本 - 遇到 Git 要求設定 `user.name` 或 `user.email` 的提示 +## 檢查 .gitignore +在推送之前檢查 .gitignore,確保虛擬環境、環境設定與非必要檔案有正確被排除在版本控制之外。 + ## ⚠️ 設定 Git 使用者身份 如果 Git 顯示以下提示,需要先設定身份才能 commit: @@ -119,4 +122,4 @@ git push **Q: 不小心 commit 了不該 commit 的檔案?** -告訴 AI:「我剛才不小心 commit 了 [檔案名稱],請幫我移除。」 \ No newline at end of file +告訴 AI:「我剛才不小心 commit 了 [檔案名稱],請幫我移除。」 From d2aff399c4790cd69719485fea608c5c48e9a1ba Mon Sep 17 00:00:00 2001 From: antigravity Date: Tue, 21 Apr 2026 17:47:06 +0800 Subject: [PATCH 06/11] feat: add database schema, route skeleton and templates plan --- .gitignore | 22 ++++++ README.md | 11 +-- app.py | 16 ++++ app/models/__init__.py | 1 + app/models/recipe.py | 73 +++++++++++++++++++ app/routes/__init__.py | 2 + app/routes/recipe.py | 56 ++++++++++++++ database/schema.sql | 9 +++ docs/DB_DESIGN.md | 57 +++++++++++++++ docs/ROUTES.md | 70 ++++++++++++++++++ ...46\344\275\234\350\252\252\346\230\216.md" | 6 +- 11 files changed, 311 insertions(+), 12 deletions(-) create mode 100644 .gitignore create mode 100644 app.py create mode 100644 app/models/__init__.py create mode 100644 app/models/recipe.py create mode 100644 app/routes/__init__.py create mode 100644 app/routes/recipe.py create mode 100644 database/schema.sql create mode 100644 docs/DB_DESIGN.md create mode 100644 docs/ROUTES.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8760da8c --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Environment +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# SQLite database and instance directory +instance/ +*.db +*.sqlite3 + +# OS generated files +.DS_Store +Thumbs.db diff --git a/README.md b/README.md index 62cd639b..cc3edf2d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - [系統簡介](#系統簡介) - [技術棧](#技術棧) -- [組員與分工](#組員與分工) +- [系統截圖](#系統截圖) - [個人心得](#個人心得) --- @@ -25,13 +25,8 @@ --- -## 組員與分工(先寫自己就好) - -**第 X 組** - -| 姓名 | 學號 | 負責部分 | -| ---- | ---- | -------- | -| | | | +## 系統截圖 +請在此貼上系統的截圖畫面, 選擇一個功能邊操作邊截圖 --- diff --git a/app.py b/app.py new file mode 100644 index 00000000..41549209 --- /dev/null +++ b/app.py @@ -0,0 +1,16 @@ +from flask import Flask +from app.routes.recipe import recipe_bp + +def create_app(): + # 指定 template 與 static 的存放路徑,確保與專案結構相符 + app = Flask(__name__, template_folder='app/templates', static_folder='app/static') + + # 註冊 Blueprints + app.register_blueprint(recipe_bp) + + return app + +app = create_app() + +if __name__ == '__main__': + app.run(debug=True) diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 00000000..f01acc75 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1 @@ +# initialize app/models module diff --git a/app/models/recipe.py b/app/models/recipe.py new file mode 100644 index 00000000..4102d3fa --- /dev/null +++ b/app/models/recipe.py @@ -0,0 +1,73 @@ +import sqlite3 +import os +from contextlib import contextmanager + +# 取得資料庫檔案存放的目錄 instance/ (位於專案根目錄) +DB_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'instance') +DB_PATH = os.path.join(DB_DIR, 'database.db') + +@contextmanager +def get_db_connection(): + # 若目錄不存在則建立一個 + os.makedirs(DB_DIR, exist_ok=True) + conn = sqlite3.connect(DB_PATH) + # 將回傳結果設為 dict-like 的 Row 物件 + conn.row_factory = sqlite3.Row + try: + yield conn + finally: + conn.commit() + conn.close() + +class Recipe: + @staticmethod + def create(data): + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO recipes (title, description, ingredients, steps) + VALUES (?, ?, ?, ?) + ''', (data['title'], data.get('description', ''), data['ingredients'], data['steps'])) + return cursor.lastrowid + + @staticmethod + def get_all(query=None): + with get_db_connection() as conn: + cursor = conn.cursor() + if query: + # 簡單支援對 title 或 ingredients 的 LIKE 搜尋 (搜尋推薦食譜) + search_term = f"%{query}%" + cursor.execute(''' + SELECT * FROM recipes + WHERE title LIKE ? OR ingredients LIKE ? + ORDER BY created_at DESC + ''', (search_term, search_term)) + else: + cursor.execute('SELECT * FROM recipes ORDER BY created_at DESC') + return [dict(row) for row in cursor.fetchall()] + + @staticmethod + def get_by_id(recipe_id): + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT * FROM recipes WHERE id = ?', (recipe_id,)) + row = cursor.fetchone() + return dict(row) if row else None + + @staticmethod + def update(recipe_id, data): + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + UPDATE recipes + SET title = ?, description = ?, ingredients = ?, steps = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + ''', (data['title'], data.get('description', ''), data['ingredients'], data['steps'], recipe_id)) + return cursor.rowcount > 0 + + @staticmethod + def delete(recipe_id): + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute('DELETE FROM recipes WHERE id = ?', (recipe_id,)) + return cursor.rowcount > 0 diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 00000000..d1a57de5 --- /dev/null +++ b/app/routes/__init__.py @@ -0,0 +1,2 @@ +# init routes +from app.routes.recipe import recipe_bp diff --git a/app/routes/recipe.py b/app/routes/recipe.py new file mode 100644 index 00000000..d446bcbc --- /dev/null +++ b/app/routes/recipe.py @@ -0,0 +1,56 @@ +from flask import Blueprint, request, render_template, redirect, url_for, abort + +recipe_bp = Blueprint('recipe', __name__) + +@recipe_bp.route('/') +def index(): + """ + 列出所有食譜。 + 若有 ?q= 參數則篩選標題與食材。 + """ + pass + +@recipe_bp.route('/recipes/new') +def new_recipe(): + """ + 顯示新增食譜表單頁面。 + """ + pass + +@recipe_bp.route('/recipes', methods=['POST']) +def create_recipe(): + """ + 處理新增食譜請求。 + 接收表單資料,寫入 DB,完成後導回首頁。 + """ + pass + +@recipe_bp.route('/recipes/') +def show_recipe(id): + """ + 顯示單一食譜的詳細資料。 + """ + pass + +@recipe_bp.route('/recipes//edit') +def edit_recipe(id): + """ + 顯示修改食譜的表單頁面,將原資料填入表單中。 + """ + pass + +@recipe_bp.route('/recipes//update', methods=['POST']) +def update_recipe(id): + """ + 處裡食譜修改請求並寫入。 + 更新後導回食譜明細頁。 + """ + pass + +@recipe_bp.route('/recipes//delete', methods=['POST']) +def delete_recipe(id): + """ + 刪除指定食譜。 + 刪除後導向回首頁。 + """ + pass diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 00000000..e356c0e1 --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS recipes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + ingredients TEXT NOT NULL, + steps TEXT NOT NULL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP +); diff --git a/docs/DB_DESIGN.md b/docs/DB_DESIGN.md new file mode 100644 index 00000000..855ce95d --- /dev/null +++ b/docs/DB_DESIGN.md @@ -0,0 +1,57 @@ +# DB Design Document — 資料庫設計文件 + +## 1. ER 圖 + +```mermaid +erDiagram + RECIPES { + INTEGER id PK + TEXT title "食譜名稱" + TEXT description "簡介" + TEXT ingredients "所需食材" + TEXT steps "烹飪步驟" + TEXT created_at "建立時間" + TEXT updated_at "最後更新時間" + } +``` + +## 2. 資料表詳細說明 + +### `recipes` 資料表 +本系統採用單一資料表設計以符合 MVP 需求,各欄位詳細定義如下: + +| 欄位名稱 | 型別 | 必填 | 說明 | +| --- | --- | --- | --- | +| `id` | `INTEGER` | 是 | Primary Key, 自動遞增 (AUTOINCREMENT)。 | +| `title` | `TEXT` | 是 | 食譜名稱。 | +| `description` | `TEXT` | 否 | 食譜的簡單介紹。 | +| `ingredients` | `TEXT` | 是 | 食譜的所需食材,為求簡便與彈性,儲存為純文字 (可依照換行或逗點分隔)。未來依食材推薦將利用 `LIKE` 查詢運算實作。 | +| `steps` | `TEXT` | 是 | 烹飪步驟,儲存為純文字即可。 | +| `created_at` | `TEXT` | 是 | 建立時間,使用 `CURRENT_TIMESTAMP` 的 ISO 格式儲存。 | +| `updated_at` | `TEXT` | 是 | 最後更新時間,編輯資料時觸發更新。 | + +## 3. SQL 建表語法 + +實作的 SQL 建表存放於 `database/schema.sql` 中: + +```sql +CREATE TABLE IF NOT EXISTS recipes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + ingredients TEXT NOT NULL, + steps TEXT NOT NULL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP +); +``` + +## 4. Python Model + +封裝的檔案位於 `app/models/recipe.py`。 +使用內建的 `sqlite3` 操作 `instance/database.db`,並支援: +- `create(data)` +- `get_all(query=None)`: 實作按標題與食材進行簡單字串 `LIKE` 搜尋。 +- `get_by_id(recipe_id)` +- `update(recipe_id, data)` +- `delete(recipe_id)` diff --git a/docs/ROUTES.md b/docs/ROUTES.md new file mode 100644 index 00000000..2514b340 --- /dev/null +++ b/docs/ROUTES.md @@ -0,0 +1,70 @@ +# 路由設計文件 (API Design) + +## 1. 路由總覽列表 +| 功能 | HTTP 方法 | URL 路徑 | 對應模板 | 說明 | +| --- | --- | --- | --- | --- | +| 食譜列表 (首頁) | `GET` | `/` | `templates/recipes/index.html` | 顯示所有食譜,支援 `?q=` 查詢參數進行名稱/食材搜尋。 | +| 新增食譜頁面 | `GET` | `/recipes/new` | `templates/recipes/form.html` | 顯示空白的新增食譜表單。 | +| 建立食譜 | `POST` | `/recipes` | — | 接收表單資料、存入 DB,成功後重導回首頁。 | +| 食譜明細 | `GET` | `/recipes/` | `templates/recipes/show.html` | 顯示特定食譜的食材與步驟。若查無資料回傳 404。 | +| 編輯食譜頁面 | `GET` | `/recipes//edit` | `templates/recipes/form.html` | 顯示現有食譜資料供修改。 | +| 更新食譜 | `POST` | `/recipes//update` | — | 接收更新表單並寫回 DB,完成後重導至食譜明細。 | +| 刪除食譜 | `POST` | `/recipes//delete` | — | 從 DB 中刪除該筆資料,完成後重導回首頁。 | + +--- + +## 2. 路由詳細說明 + +### `GET /` (食譜列表) +- **輸入**: URL 查詢參數 `?q=keyword` (可選) +- **處理邏輯**: + - 若有 `q` 則呼叫 `Recipe.get_all(q)` 並搜尋。 + - 若無則呼叫 `Recipe.get_all()` 取得最新食譜清單。 +- **輸出**: 渲染 `recipes/index.html`,傳入食譜列表變數 `recipes`。 +- **錯誤處理**: 資料庫如讀取錯誤報 500。 + +### `GET /recipes/new` (新增輸入頁面) +- **輸入**: 無 +- **處理邏輯**: 準備畫面即可。 +- **輸出**: 渲染 `recipes/form.html`。 +- **錯誤處理**: 無特殊錯誤。 + +### `POST /recipes` (建立食譜) +- **輸入**: Form Data 包含 `title`, `description`, `ingredients`, `steps`。 +- **處理邏輯**: 接收 Form,呼叫 `Recipe.create(data)`。 +- **輸出**: Http 302 重新導向至 `/` (首頁)。 +- **錯誤處理**: 若 `title`, `ingredients`, `steps` 等必填欄位缺失,透過 Flash Message 提示並導回 `/recipes/new`。 + +### `GET /recipes/` (食譜明細) +- **輸入**: URL 參數 `id`。 +- **處理邏輯**: 呼叫 `Recipe.get_by_id(id)` 取得指定食譜。 +- **輸出**: 渲染 `recipes/show.html`,傳入 `recipe`。 +- **錯誤處理**: 若 `recipe` 為空 (None),拋出 HTTP 404 (Not Found)。 + +### `GET /recipes//edit` (編輯食譜頁面) +- **輸入**: URL 參數 `id`。 +- **處理邏輯**: 呼叫 `Recipe.get_by_id(id)` 取得指定食譜。 +- **輸出**: 渲染 `recipes/form.html`,傳入 `recipe` 將現有值帶入欄位。 +- **錯誤處理**: 若查無結果,拋出 404。 + +### `POST /recipes//update` (更新食譜) +- **輸入**: URL 參數 `id` 與被更新的表單資料。 +- **處理邏輯**: 呼叫 `Recipe.update(id, data)`。 +- **輸出**: 更新成功後重新導向回詳細頁面 `/recipes/`。 +- **錯誤處理**: 查無食譜 (404) 或欄位缺失等驗證同建立食譜的處理方式。 + +### `POST /recipes//delete` (刪除食譜) +- **輸入**: URL 參數 `id`。 +- **處理邏輯**: 呼叫 `Recipe.delete(id)` 進行刪除。 +- **輸出**: 刪除成功後重新導向至 `/`。 +- **錯誤處理**: 若查無食譜則拋出 404。 + +--- + +## 3. Jinja2 模板清單 + +以下檔案後續將於 `app/templates` 建立: +1. `base.html`: 包含全站共用的 ``、導覽列與外觀樣式。 +2. `recipes/index.html`: 繼承 `base.html`,以卡片或列表結構呈現食譜總覽。 +3. `recipes/show.html`: 繼承 `base.html`,全版詳細呈現食材清單與步驟說明。 +4. `recipes/form.html`: 繼承 `base.html`,一個頁面同時支援新增 (Create) 與編輯 (Update)。依據後端是否丟出已存在的 `recipe` 來判斷是 POST 到 `/recipes` 或 `/recipes//update`。 diff --git "a/\345\257\246\344\275\234\350\252\252\346\230\216.md" "b/\345\257\246\344\275\234\350\252\252\346\230\216.md" index 83b7a6b9..cc8c9399 100644 --- "a/\345\257\246\344\275\234\350\252\252\346\230\216.md" +++ "b/\345\257\246\344\275\234\350\252\252\346\230\216.md" @@ -195,8 +195,7 @@ commit 訊息:docs: add user flowchart **Step 4.4** — 請 AI 提交並推送: ``` -請幫我 commit 並推送目前的變更。 -commit 訊息:feat: add database schema and models +/commit ``` --- @@ -222,8 +221,7 @@ commit 訊息:feat: add database schema and models **Step 5.4** — 請 AI 提交並推送: ``` -請幫我 commit 並推送目前的變更。 -commit 訊息:feat: add route skeleton and template plan +/commit ``` --- From f399f0bd2596ab32f52d48becd2b69f6461b858c Mon Sep 17 00:00:00 2001 From: antigravity Date: Thu, 23 Apr 2026 08:35:35 +0800 Subject: [PATCH 07/11] docs: add PRD --- docs/PRD.md | 86 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 37 deletions(-) diff --git a/docs/PRD.md b/docs/PRD.md index ee29c7e9..1512b84e 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -1,52 +1,64 @@ -# 產品需求文件 (PRD):食譜管理系統 +# 產品需求文件 (PRD) + +## 專案名稱:個人記帳簿 (Personal Expense Tracker) + +**專案描述**:一款簡潔易用的個人記帳應用,幫助使用者輕鬆記錄每日收支並透過視覺化圖表追蹤財務狀況。 + +--- ## 1. 專案概述 -- **背景與動機**:現代人生活忙碌,常常不知道要煮什麼,或是想要把好吃的食譜記錄下來以便未來查詢。此系統提供一個簡單、直覺的平台讓使用者存放、尋找,甚至根據手邊現有食材自動尋找推薦的食譜。 -- **目標用戶**:我 (個人愛好者、家庭煮夫/婦、對料理有熱忱的自煮族)。 -- **核心價值主張**:更有效率地管理個人食譜,並解決「今天不知道要吃什麼」的煩惱,輔助使用者最大化利用家中現存食材。 +- **背景與動機**:現代人常因缺乏記帳習慣或覺得市售記帳軟體過於繁瑣而放棄記帳,導致無法掌握金錢流向。本專案旨在提供一個輕量、直覺的記帳系統,幫助使用者無痛建立記帳習慣。 +- **目標用戶**:想要掌握個人財務狀況、減少不必要開銷的學生、上班族與一般大眾。 +- **核心價值主張**:簡單、快速、一目了然的財務管理,讓每個人都能輕鬆追蹤收支。 ## 2. 功能需求 -系統將提供以下主要功能: - -1. **新增食譜** - - *使用者故事*:作為一位熱愛烹飪的使用者,我希望能夠新增食譜(記錄名稱、食材、步驟等),以便儲存我喜歡的菜餚。 -2. **搜尋食譜** - - *使用者故事*:作為一位使用者,我希望能透過關鍵字搜尋食譜名稱,以便快速找到我想做的菜。 -3. **用食材推薦食譜** - - *使用者故事*:作為一位面對冰箱剩餘食材的使用者,我希望能夠輸入手邊現有食材,由系統過濾並推薦能做的食譜,以便不浪費食物。 -4. **編輯與刪除食譜** - - *使用者故事*:作為一位記錄者,我希望可以能夠編輯或刪除已經存入的食譜,以便我的食譜庫隨時保持正確、最新。 -5. **食譜分類與標籤** - - *使用者故事*:作為一位習慣有條理整理食譜的使用者,我希望可以為食譜加上分類或標籤(如:前菜、甜點、全素、低卡等),以便後續依照標籤快速篩選食譜。 +以下為本系統的主要核心功能及對應的使用者故事: + +1. **財務總覽 (Dashboard)** + - **作為** 使用者,**我希望** 在首頁能一眼看到當月總收入、總支出與目前結餘,**以便** 快速掌握當下的財務健康狀態。 + +2. **收支記錄管理** + - **作為** 使用者,**我希望** 能夠快速新增、編輯和刪除記帳紀錄(包含金額、日期、分類與備註),**以便** 詳細記錄每一筆金錢的流向。 + +3. **分類管理** + - **作為** 使用者,**我希望** 系統提供預設分類,並且能讓我自訂常用的收支分類(如:伙食、交通、娛樂),**以便** 將帳務資料做系統化的整理。 + +4. **圖表統計分析** + - **作為** 使用者,**我希望** 能透過長條圖或圓餅圖檢視每週/每月的各類別收支比例,**以便** 清楚分析我的花費結構與習慣。 + +5. **預算追蹤設定** + - **作為** 使用者,**我希望** 能針對特定支出分類設定每月預算,並能看到預算使用進度條,**以便** 提醒自己控制消費,避免超支。 ## 3. 非功能需求 - **技術限制**: - - 後端框架:Python + Flask - - 模板引擎:Jinja2 - - 資料庫:SQLite + - 後端:**Flask (Python)** 框架。 + - 前端:HTML + Vanilla CSS,並使用 **Jinja2** 作為模板引擎。 + - 資料庫:**SQLite**,適合單一使用者的輕量級資料儲存。 - **效能與安全考量**: - - 確保食譜搜尋可快速回傳結果。 - - 使用適當的安全機制來防範常見的安全漏洞(如:防護 SQL Injection, Jinja2 預設防護 XSS)。 + - **響應式設計 (RWD)**:介面需適配桌機與手機螢幕尺寸,提供友善操作體驗。 + - **資料安全**:對於表單輸入資料需進行基本的邏輯驗證與過濾(防範 XSS 和 SQL Injection)。 + - **效能考量**:系統載入與操作回應時間應保持在 1-2 秒以內,以維持流暢的使用體驗。 ## 4. MVP 範圍 +為了在初期快速推出可運作的版本,功能範圍劃分如下: -### Must Have (必備功能 - 初期實作) -- 新增與儲存食譜功能。 -- 食譜總覽與明細頁面。 -- 透過關鍵字搜尋食譜。 - -### Should Have (應有功能 - 中期實作) -- 用食材推薦/篩選食譜功能。 -- 編輯與刪除食譜。 - -### Nice to Have (加分功能 - 未來擴充) -- 食譜分類與標籤。 -- 食譜圖片上傳功能。 +- **Must Have (必須要有 - MVP)** + - 首頁財務總覽(當月收入、支出、結餘計算)。 + - 收支紀錄的 CRUD (新增、讀取、修改、刪除)。 + - 基礎預設分類列表。 +- **Should Have (應該要有)** + - 簡單的圓餅圖表/長條圖統計分析。 + - 使用者自訂新增收支分類。 +- **Nice to Have (有會更好)** + - 每月花費預算設定與超支提醒進度條。 + - 資料匯出(如 CSV 格式)功能。 ## 5. 專案成員與分工 | 角色 | 姓名 | 負責項目 | -| --- | --- | --- | -| PM | [請填寫] | 需求定義與時程控管 | -| 前端開發 | [請填寫] | 網頁介面設計、Jinja2 模板實作 | -| 後端開發 | [請填寫] | Flask API 與路由設定、SQLite 結構實作 | +| --- | --- | ------- | +| **Project Manager** / Planner | | | +| **UI/UX Designer** | | | +| **Frontend Developer** | | | +| **Backend Developer** | | | +| **QA / Tester** | | | From 9613c847bf0cd0c1dbb2ab502a80ac8d73838b4c Mon Sep 17 00:00:00 2001 From: antigravity Date: Thu, 23 Apr 2026 08:37:34 +0800 Subject: [PATCH 08/11] docs: add system architecture --- docs/ARCHITECTURE.md | 128 ++++++++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 63 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 88f96938..e6c20f27 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,84 +1,86 @@ -# 系統架構設計文件:食譜管理系統 +# 系統架構文件 (Architecture) -## 1. 技術架構說明 +## 專案名稱:個人記帳簿 (Personal Expense Tracker) -本專案採用輕量級的 Python 網頁框架設計,不採用前後端分離,由後端直接渲染 HTML 頁面返回給使用者的瀏覽器。 +### 1. 技術架構說明 +- **選用技術與原因**: + - **後端 (Python + Flask)**:Flask 是一個輕量級且靈活的 Web 框架,非常適合快速構建 MVP 與中小型應用。Python 生態系豐富且語法簡潔,能大幅提升開發效率。 + - **視圖渲染 (Jinja2)**:與 Flask 深度整合,讓伺服器端能夠在回傳 HTML 前動態填入資料,不需要額外維護一套複雜的前後端分離架構,降低初期開發門檻。 + - **資料庫 (SQLite)**:內建於 Python 環境的檔案型資料庫,無需額外建立或維護資料庫伺服器(如 MySQL 或 PostgreSQL)。對於輕量級記帳系統而言,效能已非常足夠,且易於備份與轉移。 + - **前端樣式 (Vanilla CSS + HTML5)**:直接使用純 CSS 搭配 HTML5 控制版面,確保專案輕量並達成響應式設計 (RWD)。圖表渲染部分可考慮引入輕量級 JS 函式庫 (如 Chart.js)。 -### 選用技術與原因 -- **後端框架:Python + Flask** - - **原因**:Flask 相較於 Django 更為輕量與彈性,非常適合用來開發這類中小型內容管理系統。其學習曲線平緩且易於快速打造原型。 -- **模板引擎:Jinja2** - - **原因**:與 Flask 高度整合,能直接將後端資料無縫嵌入前端 HTML 中,有效降低開發複雜度,並且內建自動轉義防護 XSS 攻擊。 -- **資料庫:SQLite** - - **原因**:不需額外安裝資料庫伺服器,資料儲存在單一檔案中,易於備份與部署,完全符合 MVP (最小可行性產品) 與個人使用的效能需求。 +- **Flask MVC 模式說明**: + - **Model (模型)**:負責定義資料表結構(Schema)並與 SQLite 進行互動。封裝了與資料庫溝通的相關羅輯(如當月總收支計算、單筆紀錄的新增刪除)。 + - **View (視圖)**:Jinja2 樣板檔案 (`.html`),負責決定使用者在瀏覽器上看到的畫面樣貌,包含變數的顯示與邏輯分頁。 + - **Controller (控制器/路由)**:Flask 中的路由功能(Route),負責接收瀏覽器傳來的要求(Request)、向 Model 請求必要資料,最後把資料拋給 View 進行渲染拼裝,再回傳給使用者(Response)。 -### Flask MVC 模式說明 -雖然 Flask 本身不強制要求 MVC 架構,但我們將依循 MVC (Model-View-Controller) 的概念來組織程式碼: -- **Model (模型)**:負責與 SQLite 資料庫溝通,定義「食譜 (Recipe)」、「標籤 (Tag)」等資料結構與操作。 -- **View (視圖)**:由 Jinja2 模板擔任,負責產生最終的 HTML 介面,呈現資料給使用者。 -- **Controller (控制器)**:由 Flask 的路由 (Routes) 擔任,負責接收使用者請求、調用 Model 獲取或更新資料、接著把資料傳遞給 View 進行渲染。 +--- -## 2. 專案資料夾結構 +### 2. 專案資料夾結構 -以下是本專案的資料夾結構與各階層職責說明: +本專案依照關注點分離 (Separation of Concerns) 的原則,將程式目錄架構規劃如下: ```text web_app_development/ -├── app/ # 應用程式主目錄 -│ ├── __init__.py # 初始化 Flask 應用程式與套件配置 -│ ├── models/ # (Model) 資料庫模型定義與操作 -│ │ └── recipe.py # 食譜相關的資料庫邏輯 -│ ├── routes/ # (Controller) Flask 路由處理邏輯 +├── app/ +│ ├── __init__.py ← 初始化 Flask 應用程式、載入設定 +│ ├── models/ ← 資料庫模型目錄 (MVC 的 M) │ │ ├── __init__.py -│ │ └── recipe.py # 處理食譜的增刪改查與搜尋推薦路由 -│ ├── templates/ # (View) Jinja2 HTML 頁面模板 -│ │ ├── base.html # 共用版型 (標題、導覽列) -│ │ └── recipes/ # 食譜相關頁面 -│ │ ├── index.html # 食譜列表/首頁 -│ │ ├── show.html # 食譜明細頁面 -│ │ └── form.html # 新增與編輯表單 -│ └── static/ # css, js, 圖片等靜態資源 +│ │ └── database.py ← 資料庫連線處理、Schema 定義與所有 CRUD 查詢函式 +│ ├── routes/ ← 路由目錄 (MVC 的 C) +│ │ ├── __init__.py +│ │ ├── index.py ← 首頁總覽 (Dashboard) 的路由邏輯 +│ │ └── expense.py ← 新增/修改/刪除記帳紀錄的路由邏輯 +│ ├── templates/ ← HTML 模板目錄 (MVC 的 V) +│ │ ├── base.html ← 共用的版面 (如 Navbar 頭部、尾部) +│ │ ├── index.html ← 首頁總覽頁面 (顯示財務總覽、統計圖表) +│ │ └── expense.html ← 記帳紀錄表單頁面 +│ └── static/ ← 靜態資源檔案 │ ├── css/ -│ │ └── style.css -│ └── images/ # 存放使用者預設圖片等 -├── instance/ # 存放敏感或變動性的本地端資料 -│ └── database.db # SQLite 資料庫檔案 -├── docs/ # 專案文件 -│ ├── PRD.md # 產品需求文件 -│ └── ARCHITECTURE.md # 系統架構文件 -├── requirements.txt # Python 依賴套件清單 -└── app.py # 應用程式啟動入口 +│ │ └── style.css← 共用的樣式定義 +│ └── js/ +│ └── main.js ← 前端互動邏輯 (如表單驗證、圖表繪製) +├── instance/ +│ └── expense.db ← SQLite 資料庫檔案 (運作時將動態產生) +├── docs/ ← 專案文件 (包含 PRD.md, ARCHITECTURE.md 等) +├── .gitignore ← 忽略加入版本控制的檔案清單 +└── app.py ← 專案啟動入口程式,負責執行 Server ``` -## 3. 元件關係圖 +--- + +### 3. 元件關係圖 -以下展示使用者與系統互動時,各元件如何協作產生回應: +以下展示了系統核心處理「取得當月財務總覽」的要求時,各元件如何進行互動: ```mermaid sequenceDiagram - participant Browser as 瀏覽器 (使用者) - participant Route as Flask Route (Controller) - participant Model as Model (資料操作) - participant DB as SQLite 資料庫 - participant Template as Jinja2 Template (View) + participant B as 瀏覽器 (Browser) + participant C as Flask Route (Controller) + participant M as Model (database.py) + participant DB as SQLite (expense.db) + participant V as Jinja2 Template (index.html) - Browser->>Route: 1. 發送 HTTP 請求 (例如:搜尋食譜) - Route->>Model: 2. 呼叫邏輯獲取資料 - Model->>DB: 3. 執行 SQL 查詢 - DB-->>Model: 4. 回傳查詢結果 - Model-->>Route: 5. 回傳 Python 結構化資料 - Route->>Template: 6. 將資料傳入模板渲染 - Template-->>Route: 7. 產生最終 HTML 字串 - Route-->>Browser: 8. 回傳 HTTP 回應 (顯示網頁) + B->>C: 1. 發送 GET / 請求 (造訪首頁) + C->>M: 2. 呼叫 get_dashboard_summary() + M->>DB: 3. 執行 SQL 查詢 + DB-->>M: 4. 回傳當月總收入與支出資料 + M-->>C: 5. 整理為 Dict 回傳給 Route + C->>V: 6. 將資料傳入 index.html 進行模板渲染 + V-->>C: 7. 產生含有完整資料的 HTML 字串 + C-->>B: 8. 回傳 HTTP Response (展示網頁畫面給使用者) ``` -## 4. 關鍵設計決策 +--- + +### 4. 關鍵設計決策 -1. **採用 SSR (伺服器端渲染) 而非 SPA (單頁應用)** - - 作為初期 MVP 專案,使用 Flask + Jinja2 直接渲染頁面是最快看到成果的方式,避免了繁瑣的 API 介接與跨域問題,能讓重點集中於食譜核心邏輯。 -2. **採用 SQLite 單一檔案資料庫** - - 避免引入如 MySQL 或 PostgreSQL 帶來額外的效能與維護成本,SQLite 非常輕量且零設定,對於主要是單人使用的儲存與搜尋已綽綽有餘。 -3. **Model 與 Controller 邏輯分離** - - 雖然初期專案較小,但不把所有的資料庫存取層直接寫在 `app.py` 或具體的 route 中。將資料庫層抽象出 `app/models/`,能確保留下擴展「食譜標籤系統」與「食材推薦演算法」的彈性空間。 -4. **使用模板繼承機制 (共用 Base)** - - 透過 `base.html` 共用 Header、導覽列及自訂的 CSS 資源。日後不論是新增編輯頁面還是推薦食譜頁面,只需專注頁面本體,可達到最好的維護性並維持設計的一致感。 +1. **選擇 Server-Side Rendering (SSR) 單體式架構** + - **決策**:網頁畫面將由 Jinja2 在伺服器端渲染完成後,以完整 HTML 回傳給瀏覽器,而非採用前端框架 (SPA) 來抓取 API。 + - **原因**:初期專案的主要功能為 CRUD 展示,重視開發效率與展示可行性(MVP)。此架構能免去前端 API 狀態管理的負擔,開發起來最迅速。 +2. **選用輕量級 SQLite 資料庫** + - **決策**:直接使用檔案型 SQLite,不建置額外的 MySQL 伺服器。 + - **原因**:個人記帳系統的使用者只有單人,也不具有複雜的多人寫入衝突,且可有效減少開發環境的配置複雜度。後續若需要升級也能輕鬆轉移。 +3. **模組化的路由結構設計 (Modular Routing)** + - **決策**:引入 Blueprint 或是把路由打散到 `routes/` 資料夾,不將所有邏輯全寫在 `app.py` 內。 + - **原因**:由於系統有許多潛在功能 (如收支紀錄、分類管理、圖表分析),拆分資料夾可預防單一檔案落落長;若未來要增加「預算追蹤設定」,可直接在 `routes` 理增添 `budget.py`,保持系統擴充性。 From 4b19de5ffdf6bf42b79f8ebbd10fccee7dd1fbc5 Mon Sep 17 00:00:00 2001 From: antigravity Date: Thu, 23 Apr 2026 08:39:02 +0800 Subject: [PATCH 09/11] docs: add system architecture --- docs/FLOWCHART.md | 108 +++++++++++++++++++++++----------------------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/docs/FLOWCHART.md b/docs/FLOWCHART.md index 26428f1c..3f4c7384 100644 --- a/docs/FLOWCHART.md +++ b/docs/FLOWCHART.md @@ -1,79 +1,81 @@ -# 流程圖設計文件:食譜管理系統 +# 流程圖文件 (Flowchart) -本文件根據產品需求文件 (PRD) 與系統架構文件,視覺化使用者在食譜管理系統中的操作流程、系統背後的處理步驟,以及功能與路由的對照表。 +## 專案名稱:個人記帳簿 (Personal Expense Tracker) -## 1. 使用者流程圖(User Flow) +### 1. 使用者流程圖 (User Flow) -以下流程圖說明當使用者開啟網頁後,可以執行的各項功能及頁面轉換路徑: +此流程圖展示使用者進入「個人記帳簿」系統後,所能進行的操作路徑,包含瀏覽總覽、新增收支紀錄、編輯與刪除等核心功能。 ```mermaid flowchart LR - A([使用者開啟網站]) --> B[首頁 - 食譜列表] + A([使用者造訪首頁]) --> B[首頁 - 財務總覽 (Dashboard)] - B --> C{選擇欲執行的功能} + B --> C{要執行什麼操作?} - %% 新增食譜路線 - C -->|點擊「新增食譜」| D[填寫新增表單頁面] - D -->|送出表單| B + C -->|查看明細| D[記帳紀錄清單頁 (瀏覽所有的收支明細)] + C -->|新增紀錄| E[新增收支頁面 (填寫表單)] - %% 搜尋/篩選路線 - C -->|輸入「關鍵字 / 食材」| E[呈現篩選後的列表] - E --> C + D -->|點擊編輯| F[編輯收支頁面 (修改表單)] + D -->|點擊刪除| G[確認刪除視窗] - %% 查看與編輯/刪除路線 - C -->|點擊某個「食譜項目」| F[食譜明細頁面] - F --> G{在明細頁中選擇操作} + E -->|送出表單| H((儲存成功)) + F -->|送出修改| H + G -->|確認| I((刪除成功)) - G -->|返回| B - G -->|點擊「編輯食譜」| H[填寫編輯表單頁面] - H -->|送出表單| F - G -->|點擊「刪除食譜」| I[確認並刪除] - I -->|刪除成功| B + H -->|自動重導向| D + I -->|自動重導向| D + D -->|返回| B ``` -## 2. 系統序列圖(Sequence Diagram) +--- -以下序列圖以核心功能**「新增食譜」**為例,展示從使用者介面送出資料到成功寫入資料庫並重導向的完整過程: +### 2. 系統序列圖 (Sequence Diagram) + +此序列圖描述「使用者點擊新增一筆記帳紀錄」到「資料被寫入 SQLite 並重新顯示列表」的完整系統流轉過程。 ```mermaid sequenceDiagram actor User as 使用者 - participant Browser as 瀏覽器 (模板渲染) - participant Flask Route as 路由 (Controller) - participant Model as 邏輯模型 (Model) + participant Browser as 瀏覽器 (HTML/CSS/JS) + participant Route as Flask Route (expense.py) + participant Model as Flask Model (database.py) participant DB as SQLite 資料庫 - - User->>Browser: 在表單頁面填妥食譜資訊並點擊送出 - Browser->>Flask Route: 發送 POST /recipes 請求 (攜帶表單資料) - - Note over Flask Route, DB: 開始處理新增邏輯 - Flask Route->>Model: 呼叫 Recipe.create(data) 傳入解析後的資料 - Model->>DB: 執行 SQL INSERT INTO recipes ... - DB-->>Model: 回傳寫入成功訊息 - Model-->>Flask Route: 回傳新建立的 Recipe 物件 + User->>Browser: 1. 在新增表單填寫金額、分類並點擊「送出」 + Browser->>Route: 2. 發送 POST /expense/add 請求 (附帶表單資料) - Note over Flask Route, Browser: 處理畫面重導向 + Route->>Route: 3. 驗證表單資料是否齊全與合法 - Flask Route-->>Browser: 回傳 302 Redirect 至首頁 (食譜列表) - Browser->>Flask Route: 發送 GET / 請求 - Flask Route->>Model: 取得最新所有食譜列表 - Model->>DB: 執行 SELECT * FROM recipes - DB-->>Model: 回傳新列資料 - Model-->>Flask Route: 列表資料 - Flask Route-->>Browser: 使用最新資料重新渲染 index.html (首頁) + alt 資料無效 + Route-->>Browser: 4a. 顯示錯誤訊息 (400 Bad Request) + else 資料有效 + Route->>Model: 4b. 呼叫 add_expense(amount, category, note, date) + Model->>DB: 5. 執行 INSERT INTO expenses 語法 + + DB-->>Model: 6. 回傳資料庫執行成功結果 + Model-->>Route: 7. 回傳新增成功結果 + + Route-->>Browser: 8. 回傳 HTTP 302 Redirect 重導向 + Browser->>Route: 9. 發送 GET /expense (重新載入列表頁) + Route->>Model: 10. 取得最新資料並由 Jinja2 渲染 + Route-->>Browser: 11. 顯示最新的記帳清單頁面 + end ``` -## 3. 功能清單對照表 +--- + +### 3. 功能清單與 API 對照表 (Routes & Endpoint Mapping) + +根據在架構中所定義的 Flask Routes,各功能的預期存取路徑與對應 HTTP 方法如下: -對應上述流程與 PRD 需求,以下為系統功能對應的 URL 路徑與 HTTP 方法整理,提供後續 API/路由設計的參考: +| 功能描述 | 模組 (Route) | HTTP 方法 | URL 路徑 (Endpoint) | 返回操作 (Response) | +| --- | --- | --- | --- | --- | +| **首頁:財務總覽** | index.py | `GET` | `/` | 回傳 `index.html` (含統計圖表) | +| **收支清單:檢視所有紀錄** | expense.py | `GET` | `/expense` | 回傳 `expense_list.html` | +| **新增紀錄:顯示表單** | expense.py | `GET` | `/expense/add` | 回傳 `expense_form.html` (供填寫) | +| **新增紀錄:送出資料** | expense.py | `POST` | `/expense/add` | 資料庫 INSERT,重導向至 `/expense` | +| **編輯紀錄:顯示表單** | expense.py | `GET` | `/expense/edit/` | 回傳 `expense_form.html` (帶入原資料)| +| **編輯紀錄:送出修改** | expense.py | `POST` | `/expense/edit/` | 資料庫 UPDATE,重導向至 `/expense` | +| **刪除紀錄:送出刪除** | expense.py | `POST` | `/expense/delete/`| 資料庫 DELETE,重導向至 `/expense` | -| 功能項目說明 | HTTP 方法 | 預計對應的 URL 路徑 | View (Jinja2) | 備註 | -| --- | :---: | --- | --- | --- | -| **首頁 / 所有食譜總覽** | `GET` | `/` 或 `/recipes` | `index.html` | 可結合查詢參數 `?q=關鍵字` 處理搜尋與食材推薦功能。 | -| **進入新增食譜表單頁** | `GET` | `/recipes/new` | `form.html` | 呈現空白的輸入表單。 | -| **提交新增食譜資料** | `POST` | `/recipes` | *(處理無畫面)* | 處理完後 302 重導向回首頁。 | -| **查看單一食譜明細** | `GET` | `/recipes/` | `show.html` | 顯示特定 ID 的食譜完整步驟與內容。 | -| **進入編輯食譜表單頁** | `GET` | `/recipes//edit` | `form.html` | 呈現帶有原始資料的編輯表單。 | -| **提交更新的食譜資料** | `POST` | `/recipes//update` | *(處理無畫面)* | 使用 HTML form 故用 POST,更新完成後重導回明細頁。 | -| **確定刪除食譜** | `POST` | `/recipes//delete` | *(處理無畫面)* | 使用 form 觸發 POST,刪除完成後重導回首頁。 | +> **備註**:由於一般的 HTML `
` 只能使用 GET 與 POST,因此刪除與修改送出暫時不使用 RESTful 嚴格定義的 `PUT`/`DELETE`,而統一使用 `POST`。 From fc34120df5068b241a7114d8260999a65311a4a9 Mon Sep 17 00:00:00 2001 From: antigravity Date: Thu, 23 Apr 2026 08:42:49 +0800 Subject: [PATCH 10/11] feat: add database schema and models --- app/models/category.py | 56 ++++++++++++++++++++++++ app/models/db.py | 24 +++++++++++ app/models/expense.py | 72 +++++++++++++++++++++++++++++++ database/schema.sql | 40 ++++++++++++++--- docs/DB_DESIGN.md | 97 ++++++++++++++++++++++-------------------- 5 files changed, 237 insertions(+), 52 deletions(-) create mode 100644 app/models/category.py create mode 100644 app/models/db.py create mode 100644 app/models/expense.py diff --git a/app/models/category.py b/app/models/category.py new file mode 100644 index 00000000..3a27c6d1 --- /dev/null +++ b/app/models/category.py @@ -0,0 +1,56 @@ +from .db import get_db + +class Category: + @staticmethod + def get_all(): + """取得所有分類""" + conn = get_db() + cursor = conn.execute('SELECT * FROM categories ORDER BY type, id') + rows = cursor.fetchall() + conn.close() + return [dict(row) for row in rows] + + @staticmethod + def get_by_id(cat_id): + """根據 ID 取得特定分類""" + conn = get_db() + cursor = conn.execute('SELECT * FROM categories WHERE id = ?', (cat_id,)) + row = cursor.fetchone() + conn.close() + return dict(row) if row else None + + @staticmethod + def create(name, type_, is_preset=0): + """新增自訂分類""" + conn = get_db() + cursor = conn.execute( + 'INSERT INTO categories (name, type, is_preset) VALUES (?, ?, ?)', + (name, type_, is_preset) + ) + conn.commit() + new_id = cursor.lastrowid + conn.close() + return new_id + + @staticmethod + def update(cat_id, name): + """更新分類名稱 (限自訂分類)""" + conn = get_db() + cursor = conn.execute( + 'UPDATE categories SET name = ? WHERE id = ? AND is_preset = 0', + (name, cat_id) + ) + conn.commit() + affected = cursor.rowcount + conn.close() + return affected > 0 + + @staticmethod + def delete(cat_id): + """刪除分類 (檢查限自訂分類)""" + conn = get_db() + cursor = conn.execute('DELETE FROM categories WHERE id = ? AND is_preset = 0', (cat_id,)) + conn.commit() + affected = cursor.rowcount + conn.close() + return affected > 0 diff --git a/app/models/db.py b/app/models/db.py new file mode 100644 index 00000000..44614cec --- /dev/null +++ b/app/models/db.py @@ -0,0 +1,24 @@ +import sqlite3 +import os + +# 自動推導 instance/expense.db 路徑 +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) +DB_PATH = os.path.join(BASE_DIR, 'instance', 'expense.db') + +def get_db(): + conn = sqlite3.connect(DB_PATH) + # 使用 sqlite3.Row 讓查詢結果能像 dictionary 那樣透過欄位名稱取值 + conn.row_factory = sqlite3.Row + return conn + +def init_db(): + schema_path = os.path.join(BASE_DIR, 'database', 'schema.sql') + os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + + with open(schema_path, 'r', encoding='utf-8') as f: + schema_sql = f.read() + + conn = get_db() + conn.executescript(schema_sql) + conn.commit() + conn.close() diff --git a/app/models/expense.py b/app/models/expense.py new file mode 100644 index 00000000..09d46d0a --- /dev/null +++ b/app/models/expense.py @@ -0,0 +1,72 @@ +from .db import get_db +from datetime import datetime + +class Expense: + @staticmethod + def get_all(): + """取得所有收支紀錄 (包含分類名稱)""" + conn = get_db() + query = ''' + SELECT e.*, c.name as category_name, c.type as category_type + FROM expenses e + JOIN categories c ON e.category_id = c.id + ORDER BY e.date DESC, e.created_at DESC + ''' + cursor = conn.execute(query) + rows = cursor.fetchall() + conn.close() + return [dict(row) for row in rows] + + @staticmethod + def get_by_id(expense_id): + """根據 ID 取得單筆收支明細""" + conn = get_db() + query = ''' + SELECT e.*, c.name as category_name, c.type as category_type + FROM expenses e + JOIN categories c ON e.category_id = c.id + WHERE e.id = ? + ''' + cursor = conn.execute(query, (expense_id,)) + row = cursor.fetchone() + conn.close() + return dict(row) if row else None + + @staticmethod + def create(amount, category_id, note, date): + """新增一筆收支紀錄""" + conn = get_db() + created_at = datetime.now().isoformat() + cursor = conn.execute( + 'INSERT INTO expenses (amount, category_id, note, date, created_at) VALUES (?, ?, ?, ?, ?)', + (amount, category_id, note, date, created_at) + ) + conn.commit() + new_id = cursor.lastrowid + conn.close() + return new_id + + @staticmethod + def update(expense_id, amount, category_id, note, date): + """更新一筆收支紀錄""" + conn = get_db() + cursor = conn.execute( + '''UPDATE expenses + SET amount = ?, category_id = ?, note = ?, date = ? + WHERE id = ?''', + (amount, category_id, note, date, expense_id) + ) + conn.commit() + affected = cursor.rowcount + conn.close() + return affected > 0 + + @staticmethod + def delete(expense_id): + """刪除一筆收支紀錄""" + conn = get_db() + cursor = conn.execute('DELETE FROM expenses WHERE id = ?', (expense_id,)) + conn.commit() + affected = cursor.rowcount + conn.close() + return affected > 0 diff --git a/database/schema.sql b/database/schema.sql index e356c0e1..63677e64 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -1,9 +1,35 @@ -CREATE TABLE IF NOT EXISTS recipes ( +CREATE TABLE IF NOT EXISTS categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - description TEXT, - ingredients TEXT NOT NULL, - steps TEXT NOT NULL, - created_at TEXT DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT DEFAULT CURRENT_TIMESTAMP + name TEXT NOT NULL, + type TEXT NOT NULL, + is_preset INTEGER DEFAULT 1 ); + +CREATE TABLE IF NOT EXISTS expenses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + amount INTEGER NOT NULL, + category_id INTEGER NOT NULL, + note TEXT, + date TEXT NOT NULL, + created_at TEXT NOT NULL, + FOREIGN KEY (category_id) REFERENCES categories(id) +); + +CREATE TABLE IF NOT EXISTS budgets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER NOT NULL, + monthly_limit INTEGER NOT NULL, + month TEXT NOT NULL, + FOREIGN KEY (category_id) REFERENCES categories(id) +); + +-- 初始化時塞入預設的收支分類 +INSERT INTO categories (name, type, is_preset) VALUES +('飲食', 'expense', 1), +('交通', 'expense', 1), +('娛樂', 'expense', 1), +('居家', 'expense', 1), +('醫療', 'expense', 1), +('薪資', 'income', 1), +('獎金', 'income', 1), +('投資', 'income', 1); diff --git a/docs/DB_DESIGN.md b/docs/DB_DESIGN.md index 855ce95d..69723e57 100644 --- a/docs/DB_DESIGN.md +++ b/docs/DB_DESIGN.md @@ -1,57 +1,64 @@ -# DB Design Document — 資料庫設計文件 +# 資料庫設計文件 (DB Design) -## 1. ER 圖 +## 1. ER 圖 (實體關係圖) ```mermaid erDiagram - RECIPES { + CATEGORIES { INTEGER id PK - TEXT title "食譜名稱" - TEXT description "簡介" - TEXT ingredients "所需食材" - TEXT steps "烹飪步驟" - TEXT created_at "建立時間" - TEXT updated_at "最後更新時間" + TEXT name + TEXT type + INTEGER is_preset } + EXPENSES { + INTEGER id PK + INTEGER amount + INTEGER category_id FK + TEXT note + TEXT date + TEXT created_at + } + BUDGETS { + INTEGER id PK + INTEGER category_id FK + INTEGER monthly_limit + TEXT month + } + + CATEGORIES ||--o{ EXPENSES : "contains" + CATEGORIES ||--o{ BUDGETS : "has" ``` ## 2. 資料表詳細說明 -### `recipes` 資料表 -本系統採用單一資料表設計以符合 MVP 需求,各欄位詳細定義如下: - -| 欄位名稱 | 型別 | 必填 | 說明 | -| --- | --- | --- | --- | -| `id` | `INTEGER` | 是 | Primary Key, 自動遞增 (AUTOINCREMENT)。 | -| `title` | `TEXT` | 是 | 食譜名稱。 | -| `description` | `TEXT` | 否 | 食譜的簡單介紹。 | -| `ingredients` | `TEXT` | 是 | 食譜的所需食材,為求簡便與彈性,儲存為純文字 (可依照換行或逗點分隔)。未來依食材推薦將利用 `LIKE` 查詢運算實作。 | -| `steps` | `TEXT` | 是 | 烹飪步驟,儲存為純文字即可。 | -| `created_at` | `TEXT` | 是 | 建立時間,使用 `CURRENT_TIMESTAMP` 的 ISO 格式儲存。 | -| `updated_at` | `TEXT` | 是 | 最後更新時間,編輯資料時觸發更新。 | - -## 3. SQL 建表語法 - -實作的 SQL 建表存放於 `database/schema.sql` 中: - -```sql -CREATE TABLE IF NOT EXISTS recipes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - description TEXT, - ingredients TEXT NOT NULL, - steps TEXT NOT NULL, - created_at TEXT DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT DEFAULT CURRENT_TIMESTAMP -); -``` +### CATEGORIES (分類資料表) +負責儲存收支的類別(例如:飲食、交通、薪資等),包含系統預設分類與使用者自訂分類。 +- `id` (INTEGER): Primary Key,自動遞增。 +- `name` (TEXT): 分類名稱,必填。 +- `type` (TEXT): 收支類型,限定為 `'income'` (收入) 或 `'expense'` (支出),必填。 +- `is_preset` (INTEGER): 判斷是否為預設分類,`1` 為預設,`0` 為使用者自訂。預設值為 `1`。 + +### EXPENSES (收支紀錄表) +儲存每一筆記帳明細,是最核心的交易紀錄表。 +- `id` (INTEGER): Primary Key,自動遞增。 +- `amount` (INTEGER): 交易金額,必填。 +- `category_id` (INTEGER): Foreign Key,對應 `categories.id`,必填。 +- `note` (TEXT): 消費備註與說明文字,選填。 +- `date` (TEXT): 交易發生日期,格式為 `YYYY-MM-DD`,必填。 +- `created_at` (TEXT): 該筆資料寫入系統的時間戳記 (ISO 格式),必填。 + +### BUDGETS (預算設定表) +因應 Nice to Have 需求所規劃的各分類月預算設定。 +- `id` (INTEGER): Primary Key,自動遞增。 +- `category_id` (INTEGER): Foreign Key,對應 `categories.id`,必填。 +- `monthly_limit` (INTEGER): 設定的該月預算金額上限,必填。 +- `month` (TEXT): 對應的月份,格式為 `YYYY-MM`,必填。 -## 4. Python Model +## 3. SQL 建表語法與位置 +完整的 SQLite CREATE 語法以及預設值的 INSERT 腳本,已產出並儲存於專案內部的 `database/schema.sql` 檔案中。 -封裝的檔案位於 `app/models/recipe.py`。 -使用內建的 `sqlite3` 操作 `instance/database.db`,並支援: -- `create(data)` -- `get_all(query=None)`: 實作按標題與食材進行簡單字串 `LIKE` 搜尋。 -- `get_by_id(recipe_id)` -- `update(recipe_id, data)` -- `delete(recipe_id)` +## 4. Python Model 實作 +後端以 Python `sqlite3` 提供單純且高效的連線存取: +- **`app/models/db.py`**: 提供統一的 `get_db()` 資料庫連線函式,並負責處理資料庫初始化 (`init_db`)。 +- **`app/models/category.py`**: 分類模型的 CRUD。 +- **`app/models/expense.py`**: 紀錄模型的 CRUD,包含合併分類查詢的 JOIN 指令。 From 0f1f2f0c533a88def7bff7de8bde23a964b4fa13 Mon Sep 17 00:00:00 2001 From: antigravity Date: Thu, 23 Apr 2026 08:46:00 +0800 Subject: [PATCH 11/11] feat: add route skeleton and template plan --- app/routes/category.py | 64 +++++++++++++++++ app/routes/expense.py | 64 +++++++++++++++++ app/routes/index.py | 19 +++++ app/templates/base.html | 23 ++++++ app/templates/categories/form.html | 10 +++ app/templates/categories/index.html | 8 +++ app/templates/expenses/form.html | 10 +++ app/templates/expenses/index.html | 8 +++ app/templates/index.html | 9 +++ docs/ROUTES.md | 107 +++++++++++----------------- 10 files changed, 255 insertions(+), 67 deletions(-) create mode 100644 app/routes/category.py create mode 100644 app/routes/expense.py create mode 100644 app/routes/index.py create mode 100644 app/templates/base.html create mode 100644 app/templates/categories/form.html create mode 100644 app/templates/categories/index.html create mode 100644 app/templates/expenses/form.html create mode 100644 app/templates/expenses/index.html create mode 100644 app/templates/index.html diff --git a/app/routes/category.py b/app/routes/category.py new file mode 100644 index 00000000..4fa81991 --- /dev/null +++ b/app/routes/category.py @@ -0,0 +1,64 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash + +category_bp = Blueprint('category', __name__, url_prefix='/categories') + +@category_bp.route('/') +def list_categories(): + """ + 分類列表 + HTTP Method: GET + 處理邏輯: 呼叫 Category.get_all() 取得所有分類資料 + 輸出: render_template('categories/index.html', categories=categories) + """ + pass + +@category_bp.route('/new', methods=['GET', 'POST']) +def new_category(): + """ + 新增分類 + HTTP Method: GET, POST + + [GET] + 處理邏輯: 單純回傳新增表單的介面 + 輸出: render_template('categories/form.html') + + [POST] + 輸入: request.form (包含 name, type) + 處理邏輯: 檢查名稱不為空,呼叫 Category.create(name, type, is_preset=0) + 輸出: redirect(url_for('category.list_categories')) + 錯誤處理: 名稱為空或類型不合法時,使用 flash() 紀錄錯誤,並回傳原表單頁面 + """ + pass + +@category_bp.route('//edit', methods=['GET', 'POST']) +def edit_category(id): + """ + 編輯自訂分類 + HTTP Method: GET, POST + + [GET] + 處理邏輯: + 1. Category.get_by_id(id) 若找不到回傳 404。 + 2. 檢查 is_preset == 1,若是預設分類則 flash 錯誤並拒絕編輯。 + 輸出: render_template('categories/form.html', category=category) + + [POST] + 輸入: request.form (包含 name) + 處理邏輯: Category.update(id, name) [僅允許更改名稱] + 輸出: redirect(url_for('category.list_categories')) + """ + pass + +@category_bp.route('//delete', methods=['POST']) +def delete_category(id): + """ + 刪除自訂分類 + HTTP Method: POST + + 處理邏輯: + 1. Category.get_by_id(id),檢查是否為預設分類,若是則拒絕。 + 2. (Nice to Have) 檢查是否有 expenses 依賴該分類,若有則閃爍錯誤提示不允許刪除。 + 3. Category.delete(id) + 輸出: redirect(url_for('category.list_categories')) + """ + pass diff --git a/app/routes/expense.py b/app/routes/expense.py new file mode 100644 index 00000000..5b2cc329 --- /dev/null +++ b/app/routes/expense.py @@ -0,0 +1,64 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash + +expense_bp = Blueprint('expense', __name__, url_prefix='/expenses') + +@expense_bp.route('/') +def list_expenses(): + """ + 列出所有收支明細 + HTTP Method: GET + + 輸入: (可選) Query String 提供月份過濾,例如 ?month=2024-03 + 處理邏輯: 呼叫 Expense.get_all() 載入所有紀錄 + 輸出: render_template('expenses/index.html', expenses=expenses) + """ + pass + +@expense_bp.route('/new', methods=['GET', 'POST']) +def new_expense(): + """ + 新增收支紀錄 + HTTP Method: GET, POST + + [GET] + 處理邏輯: 呼叫 Category.get_all() 將分類選項餵給前端的下拉選單 + 輸出: render_template('expenses/form.html', categories=categories) + + [POST] + 輸入: request.form (包含 amount, category_id, date, note) + 處理邏輯: 確保金額為正數、必填欄位不為空,接著呼叫 Expense.create() + 輸出: 新增成功後 redirect(url_for('expense.list_expenses')) + 錯誤處理: 資料缺失或格式錯誤時,使用 flash() 紀錄錯誤並重導回表單頁面 + """ + pass + +@expense_bp.route('//edit', methods=['GET', 'POST']) +def edit_expense(id): + """ + 編輯收支紀錄 + HTTP Method: GET, POST + + [GET] + 處理邏輯: + 1. Expense.get_by_id(id) 取得該筆紀錄內容,如果不存在則丟出 404。 + 2. 取 Category.get_all() 餵給前端的下拉選單。 + 輸出: render_template('expenses/form.html', expense=expense, categories=categories) + + [POST] + 輸入: request.form (包含 amount, category_id, date, note) + 處理邏輯: 基礎邏輯檢查,執行 Expense.update() + 輸出: redirect(url_for('expense.list_expenses')) + """ + pass + +@expense_bp.route('//delete', methods=['POST']) +def delete_expense(id): + """ + 刪除收支紀錄 + HTTP Method: POST + + 處理邏輯: 呼叫 Expense.delete(id) + 輸出: redirect(url_for('expense.list_expenses')) + 錯誤處理: 找不到對應 ID 紀錄時可回傳 404 或 flash() + """ + pass diff --git a/app/routes/index.py b/app/routes/index.py new file mode 100644 index 00000000..d8d1a6fb --- /dev/null +++ b/app/routes/index.py @@ -0,0 +1,19 @@ +from flask import Blueprint, render_template + +index_bp = Blueprint('index', __name__) + +@index_bp.route('/') +def dashboard(): + """ + 首頁:財務總覽 Dashboard + HTTP Method: GET + + 處理邏輯: + 1. 呼叫 Expense.get_all() 並過濾出「當月」的所有紀錄。 + 2. 根據 category_type 統計當月「總收入、總支出」,並算出「當前結餘」。 + 3. 準備收支結構圖表所需的圓餅圖資料。 + + 輸出: render_template('index.html', summary=..., chart_data=...) + 錯誤處理: 若當月尚無紀錄,則總計皆回傳 0,前端顯示空白或引導提示。 + """ + pass diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 00000000..d7a96afe --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,23 @@ + + + + + + {% block title %}個人記帳簿{% endblock %} + + + + + +
+ {% block content %} + {% endblock %} +
+ +
+ +
+ + diff --git a/app/templates/categories/form.html b/app/templates/categories/form.html new file mode 100644 index 00000000..77194722 --- /dev/null +++ b/app/templates/categories/form.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block title %}收支分類表單{% endblock %} + +{% block content %} + + + + +{% endblock %} diff --git a/app/templates/categories/index.html b/app/templates/categories/index.html new file mode 100644 index 00000000..578c8e03 --- /dev/null +++ b/app/templates/categories/index.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block title %}收支分類管理{% endblock %} + +{% block content %} +

分類列表

+ +{% endblock %} diff --git a/app/templates/expenses/form.html b/app/templates/expenses/form.html new file mode 100644 index 00000000..268a0c85 --- /dev/null +++ b/app/templates/expenses/form.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block title %}收支紀錄表單{% endblock %} + +{% block content %} + +
+ +
+{% endblock %} diff --git a/app/templates/expenses/index.html b/app/templates/expenses/index.html new file mode 100644 index 00000000..d683c30f --- /dev/null +++ b/app/templates/expenses/index.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block title %}收支明細列表{% endblock %} + +{% block content %} +

收支明細

+ +{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 00000000..40e18759 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block title %}首頁 - 財務總覽{% endblock %} + +{% block content %} +

財務總覽

+ + +{% endblock %} diff --git a/docs/ROUTES.md b/docs/ROUTES.md index 2514b340..290be81a 100644 --- a/docs/ROUTES.md +++ b/docs/ROUTES.md @@ -1,70 +1,43 @@ -# 路由設計文件 (API Design) +# 路由設計文件 (ROUTES) -## 1. 路由總覽列表 -| 功能 | HTTP 方法 | URL 路徑 | 對應模板 | 說明 | -| --- | --- | --- | --- | --- | -| 食譜列表 (首頁) | `GET` | `/` | `templates/recipes/index.html` | 顯示所有食譜,支援 `?q=` 查詢參數進行名稱/食材搜尋。 | -| 新增食譜頁面 | `GET` | `/recipes/new` | `templates/recipes/form.html` | 顯示空白的新增食譜表單。 | -| 建立食譜 | `POST` | `/recipes` | — | 接收表單資料、存入 DB,成功後重導回首頁。 | -| 食譜明細 | `GET` | `/recipes/` | `templates/recipes/show.html` | 顯示特定食譜的食材與步驟。若查無資料回傳 404。 | -| 編輯食譜頁面 | `GET` | `/recipes//edit` | `templates/recipes/form.html` | 顯示現有食譜資料供修改。 | -| 更新食譜 | `POST` | `/recipes//update` | — | 接收更新表單並寫回 DB,完成後重導至食譜明細。 | -| 刪除食譜 | `POST` | `/recipes//delete` | — | 從 DB 中刪除該筆資料,完成後重導回首頁。 | - ---- - -## 2. 路由詳細說明 - -### `GET /` (食譜列表) -- **輸入**: URL 查詢參數 `?q=keyword` (可選) -- **處理邏輯**: - - 若有 `q` 則呼叫 `Recipe.get_all(q)` 並搜尋。 - - 若無則呼叫 `Recipe.get_all()` 取得最新食譜清單。 -- **輸出**: 渲染 `recipes/index.html`,傳入食譜列表變數 `recipes`。 -- **錯誤處理**: 資料庫如讀取錯誤報 500。 - -### `GET /recipes/new` (新增輸入頁面) -- **輸入**: 無 -- **處理邏輯**: 準備畫面即可。 -- **輸出**: 渲染 `recipes/form.html`。 -- **錯誤處理**: 無特殊錯誤。 - -### `POST /recipes` (建立食譜) -- **輸入**: Form Data 包含 `title`, `description`, `ingredients`, `steps`。 -- **處理邏輯**: 接收 Form,呼叫 `Recipe.create(data)`。 -- **輸出**: Http 302 重新導向至 `/` (首頁)。 -- **錯誤處理**: 若 `title`, `ingredients`, `steps` 等必填欄位缺失,透過 Flash Message 提示並導回 `/recipes/new`。 +## 專案名稱:個人記帳簿 (Personal Expense Tracker) -### `GET /recipes/` (食譜明細) -- **輸入**: URL 參數 `id`。 -- **處理邏輯**: 呼叫 `Recipe.get_by_id(id)` 取得指定食譜。 -- **輸出**: 渲染 `recipes/show.html`,傳入 `recipe`。 -- **錯誤處理**: 若 `recipe` 為空 (None),拋出 HTTP 404 (Not Found)。 +### 1. 路由總覽表格 -### `GET /recipes//edit` (編輯食譜頁面) -- **輸入**: URL 參數 `id`。 -- **處理邏輯**: 呼叫 `Recipe.get_by_id(id)` 取得指定食譜。 -- **輸出**: 渲染 `recipes/form.html`,傳入 `recipe` 將現有值帶入欄位。 -- **錯誤處理**: 若查無結果,拋出 404。 - -### `POST /recipes//update` (更新食譜) -- **輸入**: URL 參數 `id` 與被更新的表單資料。 -- **處理邏輯**: 呼叫 `Recipe.update(id, data)`。 -- **輸出**: 更新成功後重新導向回詳細頁面 `/recipes/`。 -- **錯誤處理**: 查無食譜 (404) 或欄位缺失等驗證同建立食譜的處理方式。 - -### `POST /recipes//delete` (刪除食譜) -- **輸入**: URL 參數 `id`。 -- **處理邏輯**: 呼叫 `Recipe.delete(id)` 進行刪除。 -- **輸出**: 刪除成功後重新導向至 `/`。 -- **錯誤處理**: 若查無食譜則拋出 404。 - ---- - -## 3. Jinja2 模板清單 - -以下檔案後續將於 `app/templates` 建立: -1. `base.html`: 包含全站共用的 ``、導覽列與外觀樣式。 -2. `recipes/index.html`: 繼承 `base.html`,以卡片或列表結構呈現食譜總覽。 -3. `recipes/show.html`: 繼承 `base.html`,全版詳細呈現食材清單與步驟說明。 -4. `recipes/form.html`: 繼承 `base.html`,一個頁面同時支援新增 (Create) 與編輯 (Update)。依據後端是否丟出已存在的 `recipe` 來判斷是 POST 到 `/recipes` 或 `/recipes//update`。 +| 功能 | HTTP 方法 | URL 路徑 | 對應模板 | 說明 | +| --- | --- | --- | --- | --- | +| **首頁:財務總覽** | GET | `/` | `templates/index.html` | 首頁,顯示單月總收支與圖表 | +| **收支列表** | GET | `/expenses` | `templates/expenses/index.html` | 列出所有收支明細 | +| **新增收支頁面** | GET | `/expenses/new` | `templates/expenses/form.html` | 提供新增明細的表單 | +| **送出新增紀錄** | POST | `/expenses/new` | — | 接收新增表單並存入 DB,重導向到 `/expenses` | +| **編輯收支頁面** | GET | `/expenses//edit` | `templates/expenses/form.html` | 提供編輯明細的表單 | +| **送出更新紀錄** | POST | `/expenses//edit` | — | 接收編輯表單並更新 DB,重導向到 `/expenses` | +| **刪除紀錄** | POST | `/expenses//delete` | — | 刪除單筆明細,重導向到 `/expenses` | +| **分類列表** | GET | `/categories` | `templates/categories/index.html` | 列出所有分類 (包含預設與自訂) | +| **新增分類頁面** | GET | `/categories/new` | `templates/categories/form.html` | 提供新增自訂分類的表單 | +| **送出新增分類** | POST | `/categories/new` | — | 接收分類表單並存入 DB,重導向到 `/categories` | +| **編輯分類頁面** | GET | `/categories//edit` | `templates/categories/form.html` | 提供編輯自訂分類的表單 | +| **送出更新分類** | POST | `/categories//edit` | — | 接收分類更新並寫入 DB,重導向到 `/categories` | +| **刪除分類** | POST | `/categories//delete` | — | 刪除單筆自訂分類,重導向到 `/categories` | + +### 2. 每個路由的詳細說明 + +各 Endpoint 的邏輯規劃、呼叫的 Models Methods、輸入以及錯誤處理 (Error Handling),都已寫在 `app/routes/` 裡的各個 `.py` 檔案的 **Docstrings** 之中。詳細邏輯包括「不能刪除預設分類」、「表單漏填驗證的 Flash 提示」等。 + +### 3. Jinja2 模板清單 + +所有的視圖介面規劃如下,部分模板會共用: + +1. **`templates/base.html`**: 共用全站骨架,定義 `{% block content %}`。包含頂部的 Navbar 和引入共用 CSS 的設置。 +2. **`templates/index.html`**: 首頁總覽,繼承自 `base.html`,展示上方的 Dashboard 卡片與下方的 Chart 圖表。 +3. **`templates/expenses/index.html`**: 收支明細列表表格,繼承自 `base.html`。 +4. **`templates/expenses/form.html`**: 收支的「新增」與「編輯」共用表單版面,透過 Action URL 或變數切換狀態,繼承自 `base.html`。 +5. **`templates/categories/index.html`**: 分類清單,展示收入/支出類別列表,繼承自 `base.html`。 +6. **`templates/categories/form.html`**: 分類的「新增」與「編輯」共用表單版面,繼承自 `base.html`。 + +### 4. 路由骨架程式碼 + +基於模組化原則,我們為首頁、收支、與分類分別設立 Flask Blueprint (藍圖): +- `app/routes/index.py`: 處理 `/` +- `app/routes/expense.py`: 處理 `/expenses/*` +- `app/routes/category.py`: 處理 `/categories/*`