Spaces:
Sleeping
Sleeping
TOMOCHIN4 commited on
Commit ·
148cde7
1
Parent(s): 15488d0
docs: v1.7.1 STABLE リリース・バックアップ作成
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- STATUS.md +19 -5
- V1.7.1/CLAUDE.md +759 -0
- V1.7.1/Dockerfile +16 -0
- V1.7.1/README.md +78 -0
- V1.7.1/RECOVERY.md +42 -0
- V1.7.1/STATUS.md +125 -0
- V1.7.1/app.py +689 -0
- V1.7.1/docs/FUTURE_VISION.md +356 -0
- V1.7.1/docs/GEMINI_API_CONFIG.md +223 -0
- V1.7.1/docs/GEMINI_STRUCTURED_OUTPUT.txt +540 -0
- V1.7.1/docs/GenreMaster.js +431 -0
- V1.7.1/docs/PLAN_v1.4.md +315 -0
- V1.7.1/docs/dify-prompts.md +299 -0
- V1.7.1/gas/.clasp.json +4 -0
- V1.7.1/gas/Code.js +2246 -0
- V1.7.1/gas/DifyService.js +458 -0
- V1.7.1/gas/GenreMaster.js +431 -0
- V1.7.1/gas/appsscript.json +15 -0
- V1.7.1/gas/questiondb_functions.js +816 -0
- V1.7.1/gas/setup_dify_properties.js +468 -0
- V1.7.1/gas/setup_sheets_v2.js +524 -0
- V1.7.1/gas/setup_sheets_v3.js +401 -0
- V1.7.1/knowledge/ORIGINAL/中学入試 でる順 ポケでる国語 漢字・熟語 四訂版.md +0 -0
- V1.7.1/knowledge/ORIGINAL/合格物語.md +0 -0
- V1.7.1/knowledge/ORIGINAL/合格論説文.md +0 -0
- V1.7.1/knowledge/ORIGINAL/理科/改訂版 中学入試にでる順 理科 力・運動・電気・光、物質・エネルギー.md +0 -0
- V1.7.1/knowledge/ORIGINAL/理科/改訂版 中学入試にでる順 理科 植物・動物・人体、地球・宇宙.md +0 -0
- V1.7.1/knowledge/ORIGINAL/社会/中学入試にでる順 地理(改訂版).md +0 -0
- V1.7.1/knowledge/ORIGINAL/社会/中学入試にでる順 社会 歴史(改訂版).md +0 -0
- V1.7.1/knowledge/ORIGINAL/社会/中学受験 社会 裏ワザテクニック Wチェック問題集 歴史編.md +0 -0
- V1.7.1/knowledge/ORIGINAL/社会/中学受験 社会の裏ワザテクニックWチェック問題集 地理編.md +0 -0
- V1.7.1/knowledge/ORIGINAL/算数/つまずきやすいところが絶対つまずかない! 小学校6年間の図形の教え方.md +832 -0
- V1.7.1/knowledge/ORIGINAL/算数/中学入試 割合と比:食塩水、割合、相当算.md +0 -0
- V1.7.1/knowledge/ORIGINAL/算数/中学入試攻略 下克上算数ドリル【速さ編】.md +808 -0
- V1.7.1/knowledge/ORIGINAL/算数/四天王寺対策算数.md +0 -0
- V1.7.1/knowledge/ORIGINAL/算数/算数出る順文章題.md +0 -0
- V1.7.1/knowledge/japanese.md +0 -0
- V1.7.1/knowledge/math.txt +0 -0
- V1.7.1/knowledge/science.md +0 -0
- V1.7.1/knowledge/social.md +0 -0
- V1.7.1/requirements.txt +18 -0
- V1.7.1/src/__init__.py +1 -0
- V1.7.1/src/models/__init__.py +1 -0
- V1.7.1/src/models/gemini_schemas.py +169 -0
- V1.7.1/src/prompts/__init__.py +1 -0
- V1.7.1/src/prompts/evaluation_prompts.py +210 -0
- V1.7.1/src/prompts/question_prompts.py +292 -0
- V1.7.1/src/prompts/validation_prompts.py +164 -0
- V1.7.1/src/services/__init__.py +1 -0
- V1.7.1/src/services/auth_service.py +89 -0
STATUS.md
CHANGED
|
@@ -1,14 +1,27 @@
|
|
| 1 |
-
# プロジェクト: 超天才クイズ v1.7.
|
| 2 |
|
| 3 |
## ステータス概要
|
| 4 |
-
- **現在地**: v1.7.
|
| 5 |
-
- **完成度**:
|
| 6 |
-
- **最大課題**:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
## v1.7.0 新機能: 制限時間
|
| 9 |
|
| 10 |
### 実装内容
|
| 11 |
-
- 1科目あたり
|
| 12 |
- カウントダウンタイマーをヘッダー右上に常時表示
|
| 13 |
- 時間切れ時: ダイアログ表示 → OK → 未回答は不正解 → 結果画面へ
|
| 14 |
- 各問題の回答時間(time_taken_seconds)をGASに記録
|
|
@@ -97,6 +110,7 @@
|
|
| 97 |
|
| 98 |
| フォルダ | バージョン | 説明 |
|
| 99 |
|---------|-----------|------|
|
|
|
|
| 100 |
| `V1.6.15/` | v1.6.15 STABLE | 完全復旧可能なバックアップ(RECOVERY.md含む) |
|
| 101 |
|
| 102 |
## 今後の構想
|
|
|
|
| 1 |
+
# プロジェクト: 超天才クイズ v1.7.1
|
| 2 |
|
| 3 |
## ステータス概要
|
| 4 |
+
- **現在地**: v1.7.1 STABLE リリース完了
|
| 5 |
+
- **完成度**: 100%
|
| 6 |
+
- **最大課題**: なし(安定運用中)
|
| 7 |
+
|
| 8 |
+
## v1.7.1 修正内容
|
| 9 |
+
|
| 10 |
+
### E2Eテスト結果(2025-12-21)
|
| 11 |
+
- ✅ タイマー表示・カウントダウン正常動作
|
| 12 |
+
- ✅ 時間切れダイアログ正常表示
|
| 13 |
+
- ✅ 未回答の不正解処理
|
| 14 |
+
- ⚠️ 制限時間5分は長い → 3分に短縮
|
| 15 |
+
- ⚠️ 算数27問生成 → 10問制限のスライス処理追加
|
| 16 |
+
|
| 17 |
+
### v1.7.1 修正
|
| 18 |
+
1. **制限時間短縮**: 5分 → 3分(180秒)に変更
|
| 19 |
+
2. **問題数制限**: Geminiが10問超生成時にスライスで10問に制限
|
| 20 |
|
| 21 |
## v1.7.0 新機能: 制限時間
|
| 22 |
|
| 23 |
### 実装内容
|
| 24 |
+
- 1科目あたり3分(180秒)の制限時間
|
| 25 |
- カウントダウンタイマーをヘッダー右上に常時表示
|
| 26 |
- 時間切れ時: ダイアログ表示 → OK → 未回答は不正解 → 結果画面へ
|
| 27 |
- 各問題の回答時間(time_taken_seconds)をGASに記録
|
|
|
|
| 110 |
|
| 111 |
| フォルダ | バージョン | 説明 |
|
| 112 |
|---------|-----------|------|
|
| 113 |
+
| `V1.7.1/` | v1.7.1 STABLE | 制限時間機能追加版(RECOVERY.md含む) |
|
| 114 |
| `V1.6.15/` | v1.6.15 STABLE | 完全復旧可能なバックアップ(RECOVERY.md含む) |
|
| 115 |
|
| 116 |
## 今後の構想
|
V1.7.1/CLAUDE.md
ADDED
|
@@ -0,0 +1,759 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CLAUDE.md — ClaudeCode単体運用 プロジェクト管理ガイド
|
| 2 |
+
|
| 3 |
+
> **対象**: ClaudeCode(Sonnet 4.5以降)による単独開発プロジェクト
|
| 4 |
+
> **原則**: 一次情報最優先 / 小さく回す / 差分最小 / 再現・ロールバック可能
|
| 5 |
+
> **目的**: 要件整理・設計・実装・レビューを一人で回し、確実に進める
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 【最重要】作業開始時の必須手順
|
| 10 |
+
|
| 11 |
+
**すべての作業開始前に、以下を実行すること:**
|
| 12 |
+
|
| 13 |
+
1. ✅ **`STATUS.md` を最優先で確認**(プロジェクトの現在地を3行で把握)
|
| 14 |
+
2. ✅ **`log/LOG.md` の最新エントリを確認**(直近の変更と未解決事項を把握)
|
| 15 |
+
3. ✅ **不明点があれば、関連するドキュメントを確認**(`DOCS_INDEX.md` 参照)
|
| 16 |
+
|
| 17 |
+
**STATUS.md の位置づけ**:
|
| 18 |
+
- プロジェクトの現在地、完成度、次のステップを一元管理
|
| 19 |
+
- このファイルを読まずに作業を開始してはならない
|
| 20 |
+
- 必要に応じて各セッション開始時に更新
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
## 0) 原則(ポリシー・意思決定の優先順位)
|
| 25 |
+
|
| 26 |
+
### 0.1 一次情報と再現性
|
| 27 |
+
|
| 28 |
+
- **一次情報最優先**: 公式Docs・標準・リリースノート・ソースコード
|
| 29 |
+
- **根拠の可搬性**: 決定にはリンク/バージョン/日付/コミットIDを必ず添付
|
| 30 |
+
- **不確実性の明示**: わからない点は「仮説」として明示し、検証方法と期限をセット
|
| 31 |
+
|
| 32 |
+
### 0.2 小さく早く、失敗に強く
|
| 33 |
+
|
| 34 |
+
- **スコープ最小化**: 1サイクル = 60–120分、1目的、1–2コミット
|
| 35 |
+
- **ロールバック容易性**: 常に「1つのrevertで戻せる」サイズに分割
|
| 36 |
+
- **破壊的操作の標準手順**: Dry-run → Diff確認 → Backup確認 → 最小適用
|
| 37 |
+
|
| 38 |
+
### 0.3 品質の定義(Quality Gates)
|
| 39 |
+
|
| 40 |
+
- **必須ゲート**: lint / type-check / 主要ケースのテスト
|
| 41 |
+
- **任意ゲート**: security scan / performance(閾値定義時)
|
| 42 |
+
- **運用ルール**: ゲート未達は次のサイクルで是正
|
| 43 |
+
|
| 44 |
+
### 0.4 リスクと不確実性
|
| 45 |
+
|
| 46 |
+
- 不確実は**仮説**として扱う(H0/H1・測定方法・合否基準・期限)
|
| 47 |
+
- 主要リスク最大3点に圧縮し、Plan B と撤退条件も併記
|
| 48 |
+
|
| 49 |
+
---
|
| 50 |
+
|
| 51 |
+
## 1) ClaudeCode単体の役割設計
|
| 52 |
+
|
| 53 |
+
### 1.1 ClaudeCodeの責務(このプロセスの全フェーズ)
|
| 54 |
+
|
| 55 |
+
| フェーズ | 責務 | 成果物 |
|
| 56 |
+
|--------|------|--------|
|
| 57 |
+
| **要件整理** | 曖昧な要件を言語化、スコープ確定 | PLAN(目的/最小スコープ/受入基準) |
|
| 58 |
+
| **設計** | アーキテクチャ・設計方針・技術選定 | 設計ドキュメント / 参照Docs リスト |
|
| 59 |
+
| **実装** | 実装・テスト・差分生成・機械的修正 | ソースコード / `git diff` / テスト結果 |
|
| 60 |
+
| **レビュー** | 自己レビュー・品質ゲート判定・承認 | REVIEW(合否/修正指示/ロールバック手順) |
|
| 61 |
+
| **ドキュメント** | 変更の理由・根拠・計測結果を記録 | LOG.md更新 / 次のタスク定義 |
|
| 62 |
+
|
| 63 |
+
### 1.2 ClaudeCodeの得意領域
|
| 64 |
+
|
| 65 |
+
- 曖昧要件の言語化、長文脈での整合性判定
|
| 66 |
+
- 非機能要求の網羅(性能/セキュリティ/拡張性)
|
| 67 |
+
- 機械的変換・差分最適化・型整合
|
| 68 |
+
- テストコード雛形、lint/security要約
|
| 69 |
+
- ドキュメント構造化・時系列管理
|
| 70 |
+
|
| 71 |
+
### 1.3 非責務(外部リソース等に依存する場合は明示)
|
| 72 |
+
|
| 73 |
+
- サードパーティの商用API利用(費用承認が必要な場合)
|
| 74 |
+
- 大規模インフラの構築(本ガイド外の決定が必要な場合)
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
## 2) 基本ループ(イベント駆動で小さく回す)
|
| 79 |
+
|
| 80 |
+
### 2.1 サイクルの流れ
|
| 81 |
+
|
| 82 |
+
```
|
| 83 |
+
┌─────────────────────────────────────────────────────────┐
|
| 84 |
+
│ 1. PLAN(目的・スコープ・受入基準・リスク) │
|
| 85 |
+
│ ↓ │
|
| 86 |
+
│ 2. BUILD(実装・テスト・差分生成) │
|
| 87 |
+
│ ↓ │
|
| 88 |
+
│ 3. REVIEW(自己レビュー・品質ゲート判定) │
|
| 89 |
+
│ ↓ │
|
| 90 |
+
│ 4. APPLY(差し戻しか採用か) │
|
| 91 |
+
│ ├─→ 採用 → DOC記録 → 次のサイクルへ │
|
| 92 |
+
│ └─→ 修正 → 新PLANへ │
|
| 93 |
+
└─────────────────────────────────────────────────────────┘
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
### 2.2 各フェーズの詳細
|
| 97 |
+
|
| 98 |
+
#### フェーズ 1: PLAN(Claude)
|
| 99 |
+
|
| 100 |
+
**入力**: 要件・背景・リスク・参考情報
|
| 101 |
+
**出力**: 以下を含むPLAN文書
|
| 102 |
+
|
| 103 |
+
```
|
| 104 |
+
## PLAN: [タスク名]
|
| 105 |
+
|
| 106 |
+
### 目的(1行)
|
| 107 |
+
[やること・なぜやるか]
|
| 108 |
+
|
| 109 |
+
### 最小スコープ(3–5項目)
|
| 110 |
+
- [ ] タスク1
|
| 111 |
+
- [ ] タスク2
|
| 112 |
+
- [ ] タスク3
|
| 113 |
+
|
| 114 |
+
### 受入基準
|
| 115 |
+
- **機能**: [仕様リンク]の動作を満たす
|
| 116 |
+
- **品質**: lint/type OK、主要テスト Green
|
| 117 |
+
- **性能**: [SLO](ある場合)
|
| 118 |
+
- **互換性**: 破壊的変更なし or 合意あり
|
| 119 |
+
|
| 120 |
+
### 参照Docs(一次情報)
|
| 121 |
+
- [公式Docs URL + 版/日付]
|
| 122 |
+
- [リリースノート/コミット]
|
| 123 |
+
- [既存実装リンク]
|
| 124 |
+
|
| 125 |
+
### 主要リスク(最大3)
|
| 126 |
+
1. [リスク](発生確度:High/Med/Low)→ 代替案: [Plan B]
|
| 127 |
+
2. [リスク]
|
| 128 |
+
3. [リスク]
|
| 129 |
+
|
| 130 |
+
### 測定方法
|
| 131 |
+
- [合否判定の具体的手段]
|
| 132 |
+
- [ロールバック手順]
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
#### フェーズ 2: BUILD(実装・自動テスト)
|
| 136 |
+
|
| 137 |
+
**入力**: PLAN
|
| 138 |
+
**出力**: 以下を含むBUILD成果物
|
| 139 |
+
|
| 140 |
+
```
|
| 141 |
+
## BUILD RESULT
|
| 142 |
+
|
| 143 |
+
### 実装内容
|
| 144 |
+
- ファイル A: [変更内容]
|
| 145 |
+
- ファイル B: [変更内容]
|
| 146 |
+
|
| 147 |
+
### git diff
|
| 148 |
+
[標準的なdiff形式、または patch ファイル]
|
| 149 |
+
|
| 150 |
+
### テスト結果
|
| 151 |
+
- Unit: [テスト数] / [成功数] ✅
|
| 152 |
+
- Integration: [テスト数] / [成功数] ✅
|
| 153 |
+
- Manual: [確認内容] ✅
|
| 154 |
+
|
| 155 |
+
### 品質ゲート結果
|
| 156 |
+
- lint: ✅ OK
|
| 157 |
+
- type-check: ✅ OK
|
| 158 |
+
- security: ✅ OK / ⚠️ 注意 / ❌ NG
|
| 159 |
+
- license: ✅ OK
|
| 160 |
+
|
| 161 |
+
### オープン質問(仕様疑義があれば)
|
| 162 |
+
- Q1: [質問]
|
| 163 |
+
- Q2: [質問]
|
| 164 |
+
|
| 165 |
+
### 再現手順
|
| 166 |
+
```bash
|
| 167 |
+
npm install
|
| 168 |
+
npm run build
|
| 169 |
+
npm run test
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
### 予想される次のステップ
|
| 173 |
+
- [修正が必要な場合の内容]
|
| 174 |
+
- [採用時の次タスク]
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
#### フェーズ 3: REVIEW(自己レビュー)
|
| 178 |
+
|
| 179 |
+
**入力**: BUILD成果物
|
| 180 |
+
**出力**: REVIEW判定 + 修正指示
|
| 181 |
+
|
| 182 |
+
```
|
| 183 |
+
## REVIEW DECISION
|
| 184 |
+
|
| 185 |
+
### 設計整合性
|
| 186 |
+
- 元の PLAN と一致しているか: ✅ / ❌
|
| 187 |
+
- スコープの逸脱: ✅ なし / ❌ [逸脱内容]
|
| 188 |
+
|
| 189 |
+
### 品質ゲート判定
|
| 190 |
+
- 全ゲート合格: ✅ YES / ❌ NO [NG項目]
|
| 191 |
+
|
| 192 |
+
### 差分最小性
|
| 193 |
+
- 必要最小限か: ✅ / ❌ [削除可能な変更]
|
| 194 |
+
|
| 195 |
+
### 採否判定
|
| 196 |
+
- 【採用】- そのままmerge可能
|
| 197 |
+
- 【軽微修正で採用】- 以下の修正後adopt:
|
| 198 |
+
- [ ] 修正1
|
| 199 |
+
- [ ] 修正2
|
| 200 |
+
- 【差し戻し】- 再検討が必要:
|
| 201 |
+
- 理由: [具体的な理由]
|
| 202 |
+
- 新PLAN: [方針]
|
| 203 |
+
|
| 204 |
+
### ロールバック手順
|
| 205 |
+
```bash
|
| 206 |
+
git revert [commit-id]
|
| 207 |
+
# または
|
| 208 |
+
git reset --hard [previous-commit]
|
| 209 |
+
```
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
#### フェーズ 4: APPLY
|
| 213 |
+
|
| 214 |
+
**決定内容**:
|
| 215 |
+
- ✅ **採用** → DOC記録 → commit & push → 次サイクル開始
|
| 216 |
+
- ⚠️ **軽微修正で採用** → 修正実施 → 再REVIEW → APPLY
|
| 217 |
+
- ❌ **差し戻し** → 新PLAN作成 → BUILD再実施
|
| 218 |
+
|
| 219 |
+
---
|
| 220 |
+
|
| 221 |
+
### 2.3 タイムボックス・粒度
|
| 222 |
+
|
| 223 |
+
- **1サイクル**: 60–120分 目安
|
| 224 |
+
- **1コミット**: 1目的原則(複合変更を避ける)
|
| 225 |
+
- **粒度**: 1つのrevertで戻せるサイズ
|
| 226 |
+
|
| 227 |
+
### 2.4 受入基準テンプレ(カスタマイズ例)
|
| 228 |
+
|
| 229 |
+
```
|
| 230 |
+
### 受入基準
|
| 231 |
+
- **機能**: [仕様URL]の要件を実装・テスト済み
|
| 232 |
+
- **品質**:
|
| 233 |
+
- lint: ✅ Green
|
| 234 |
+
- type: ✅ Green
|
| 235 |
+
- unit test: 主要ケース Green + カバレッジ +5%
|
| 236 |
+
- **性能**: p95 < 200ms @ 標準負荷
|
| 237 |
+
- **互換性**: 公開IF破壊なし or Majorバンプ合意済み
|
| 238 |
+
- **運用**: コマンド再現可能 / ロールバック手順明記
|
| 239 |
+
```
|
| 240 |
+
|
| 241 |
+
---
|
| 242 |
+
|
| 243 |
+
## 3) 作業ログ管理ルール(継続性の確保)
|
| 244 |
+
|
| 245 |
+
### 3.1 STATUS.md 更新ルール【最優先】
|
| 246 |
+
|
| 247 |
+
**更新タイミング**:
|
| 248 |
+
- プロジェクト開始時に初期化
|
| 249 |
+
- 主要コンポーネント完成度に変化があるとき
|
| 250 |
+
- 開発フェーズが切り替わるとき
|
| 251 |
+
- **新しいセッション開始前(現状確認)**
|
| 252 |
+
|
| 253 |
+
**STATUS.md の内容**:
|
| 254 |
+
|
| 255 |
+
```markdown
|
| 256 |
+
# プロジェクト: [プロジェクト名]
|
| 257 |
+
|
| 258 |
+
## ステータス概要(3行)
|
| 259 |
+
- 現在地: [フェーズ]
|
| 260 |
+
- 完成度: [%]
|
| 261 |
+
- 最大課題: [1行]
|
| 262 |
+
|
| 263 |
+
## コンポーネント完成度
|
| 264 |
+
|
| 265 |
+
| コンポーネント | 完成度 | ステータス | 最終更新 |
|
| 266 |
+
|-------------|--------|-----------|--------|
|
| 267 |
+
| 基本構造 | 100% | ✅ 完了 | YYYY-MM-DD |
|
| 268 |
+
| 機能A | 75% | 🔄 実装中 | YYYY-MM-DD |
|
| 269 |
+
| テスト | 50% | 🔲 未着手 | - |
|
| 270 |
+
| ドキュメント | 60% | 🔄 作成中 | YYYY-MM-DD |
|
| 271 |
+
|
| 272 |
+
## 最近の主要変更
|
| 273 |
+
- YYYY-MM-DD: [変更内容]
|
| 274 |
+
- YYYY-MM-DD: [変更内容]
|
| 275 |
+
|
| 276 |
+
## 次のステップ
|
| 277 |
+
1. [タスク1]
|
| 278 |
+
2. [タスク2]
|
| 279 |
+
3. [タスク3]
|
| 280 |
+
|
| 281 |
+
## ブロッカー / 未解決事項
|
| 282 |
+
- [Issue1] → 予定解決日: YYYY-MM-DD
|
| 283 |
+
- [Issue2] → 検討中
|
| 284 |
+
|
| 285 |
+
---
|
| 286 |
+
**最終更新**: YYYY-MM-DD HH:MM
|
| 287 |
+
```
|
| 288 |
+
|
| 289 |
+
### 3.2 タイムライン式作業ログ(LOG.md)
|
| 290 |
+
|
| 291 |
+
**更新タイミング**: 編集・実装のたびに必ず更新
|
| 292 |
+
**形式**: タイムライン形式(日付+時刻でセクション分け)
|
| 293 |
+
|
| 294 |
+
```markdown
|
| 295 |
+
# 開発ログ
|
| 296 |
+
|
| 297 |
+
### 2025-01-15 14:30 - ユーザー認証機能 実装完了 ✅
|
| 298 |
+
|
| 299 |
+
#### 実施作業
|
| 300 |
+
- `src/auth/` ディレクトリ作成
|
| 301 |
+
- JWT認証ロジック実装
|
| 302 |
+
- ユニットテスト追加(カバレッジ +8%)
|
| 303 |
+
|
| 304 |
+
#### 完成したもの
|
| 305 |
+
- ✅ ログイン機能
|
| 306 |
+
- ✅ トークン検証
|
| 307 |
+
- ✅ エラーハンドリング
|
| 308 |
+
|
| 309 |
+
#### ステータス
|
| 310 |
+
- ✅ 実装完了
|
| 311 |
+
- ✅ テスト合格
|
| 312 |
+
- 🔲 E2Eテスト(次回タスク)
|
| 313 |
+
|
| 314 |
+
#### 備考
|
| 315 |
+
- JWT秘密鍵は環境変数に移行
|
| 316 |
+
- ロールバック: `git revert abc1234`
|
| 317 |
+
|
| 318 |
+
#### 次回開始時の指針
|
| 319 |
+
- 次: E2Eテスト追加(予定時間: 60分)
|
| 320 |
+
- ブロッカーなし
|
| 321 |
+
|
| 322 |
+
---
|
| 323 |
+
|
| 324 |
+
### 2025-01-15 10:15 - DB接続テスト 🔄 中断
|
| 325 |
+
|
| 326 |
+
#### 実施作業
|
| 327 |
+
- `src/db/connection.ts` 実装開始
|
| 328 |
+
- エラー: PostgreSQL接続タイムアウト
|
| 329 |
+
|
| 330 |
+
#### 未解決の問題
|
| 331 |
+
- ❌ コネクションプール設定不適切
|
| 332 |
+
- 原因: [調査結果]
|
| 333 |
+
- 次回対応: リトライロジック追加
|
| 334 |
+
|
| 335 |
+
#### 次回再開時の指針
|
| 336 |
+
- リトライのコード例を `docs/db-retry.md` に記載
|
| 337 |
+
- 予定時間: 90分
|
| 338 |
+
```
|
| 339 |
+
|
| 340 |
+
### 3.3 ログ記録の実例フォーマット
|
| 341 |
+
|
| 342 |
+
```markdown
|
| 343 |
+
### YYYY-MM-DD HH:MM - [タスク名] [ステータス記号]
|
| 344 |
+
|
| 345 |
+
#### 実施作業
|
| 346 |
+
- [変更内容1]
|
| 347 |
+
- [変更内容2]
|
| 348 |
+
- [ファイル追加/削除]
|
| 349 |
+
|
| 350 |
+
#### 完成したもの
|
| 351 |
+
- ✅ [機能1]
|
| 352 |
+
- ✅ [機能2]
|
| 353 |
+
|
| 354 |
+
#### ステータス
|
| 355 |
+
- ✅ 完了
|
| 356 |
+
- 🔄 作業中
|
| 357 |
+
- 🔲 未着手
|
| 358 |
+
- ❌ 未解決
|
| 359 |
+
|
| 360 |
+
#### 品質ゲート結果
|
| 361 |
+
- lint: ✅ OK
|
| 362 |
+
- test: ✅ 12/12 Green
|
| 363 |
+
- type: ✅ OK
|
| 364 |
+
|
| 365 |
+
#### 未解決の問題 / ブロッカー
|
| 366 |
+
- ❌ [Issue]: [詳細] → 予定解決: YYYY-MM-DD
|
| 367 |
+
- ⚠️ [Caution]: [詳細]
|
| 368 |
+
|
| 369 |
+
#### コミット情報
|
| 370 |
+
- コミットID: `abc1234d`
|
| 371 |
+
- ロールバック: `git revert abc1234`
|
| 372 |
+
|
| 373 |
+
#### 次回開始時の指針
|
| 374 |
+
- 次のタスク: [タスク名]
|
| 375 |
+
- 予定時間: XX分
|
| 376 |
+
- 継続的な注意点: [注記]
|
| 377 |
+
```
|
| 378 |
+
|
| 379 |
+
### 3.4 ステータス記号
|
| 380 |
+
|
| 381 |
+
| 記号 | 意味 | 使用時機 |
|
| 382 |
+
|------|------|---------|
|
| 383 |
+
| ✅ | 完了 | タスク完成、テスト合格 |
|
| 384 |
+
| 🔄 | 作業中 | 進行中、後続あり |
|
| 385 |
+
| 🔲 | 未着手 | 今後のタスク |
|
| 386 |
+
| ❌ | 未解決/失敗 | エラー、ブロック中 |
|
| 387 |
+
| ⚠️ | 注意 | リスク、要観察 |
|
| 388 |
+
|
| 389 |
+
### 3.5 ログ管理の運用ルール
|
| 390 |
+
|
| 391 |
+
- **小さな変更でも記録**: 1ファイル編集でもログ更新を徹底
|
| 392 |
+
- **時系列の一貫性**: 作業順に追記、過去エントリは編集しない
|
| 393 |
+
- **検索可能性**: キーワード(機能名・ファイル名・エラー内容)を明記
|
| 394 |
+
- **可読性重視**: 見出し・箇条書き・ステータス記号を活用
|
| 395 |
+
|
| 396 |
+
---
|
| 397 |
+
|
| 398 |
+
## 4) ディレクトリ構造(テンプレート)
|
| 399 |
+
|
| 400 |
+
```
|
| 401 |
+
[project-root]/
|
| 402 |
+
├── STATUS.md ⭐ 最優先確認ファイル
|
| 403 |
+
├── CLAUDE.md(このファイル)
|
| 404 |
+
├── DOCS_INDEX.md(ドキュメント案内)
|
| 405 |
+
│
|
| 406 |
+
├── log/
|
| 407 |
+
│ └── LOG.md ⭐ タイムライン式開発ログ
|
| 408 |
+
│
|
| 409 |
+
├── docs/
|
| 410 |
+
│ ├── architecture.md(アーキテクチャ)
|
| 411 |
+
│ ├── api.md(API仕様)
|
| 412 |
+
│ ├── setup.md(セットアップ手順)
|
| 413 |
+
│ └── [その他ドキュメント]
|
| 414 |
+
│
|
| 415 |
+
├── src/
|
| 416 |
+
│ ├── index.ts
|
| 417 |
+
│ ├── components/
|
| 418 |
+
│ ├── utils/
|
| 419 |
+
│ └── ...
|
| 420 |
+
│
|
| 421 |
+
├── tests/
|
| 422 |
+
│ ├── unit/
|
| 423 |
+
│ ├── integration/
|
| 424 |
+
│ └── ...
|
| 425 |
+
│
|
| 426 |
+
├── .env.example
|
| 427 |
+
├── package.json
|
| 428 |
+
├── tsconfig.json
|
| 429 |
+
├── jest.config.js
|
| 430 |
+
└── .gitignore
|
| 431 |
+
```
|
| 432 |
+
|
| 433 |
+
---
|
| 434 |
+
|
| 435 |
+
## 5) 実装フロー(PLAN → BUILD → REVIEW → APPLY の具体例)
|
| 436 |
+
|
| 437 |
+
### 例: 「ユーザー認証機能の追加」
|
| 438 |
+
|
| 439 |
+
#### Step 1: PLAN
|
| 440 |
+
|
| 441 |
+
```
|
| 442 |
+
## PLAN: ユーザー認証機能(JWT)の実装
|
| 443 |
+
|
| 444 |
+
### 目的
|
| 445 |
+
ユーザーのログイン・ログアウト機能を実装し、
|
| 446 |
+
API エンドポイントを JWT で保護する
|
| 447 |
+
|
| 448 |
+
### 最小スコープ
|
| 449 |
+
- [ ] JWT ライブラリ統合(jsonwebtoken)
|
| 450 |
+
- [ ] ログイン エンドポイント作成(POST /auth/login)
|
| 451 |
+
- [ ] トークン検証ミドルウェア実装
|
| 452 |
+
- [ ] 主要ケースのユニットテスト
|
| 453 |
+
- [ ] 環境変数管理(秘密鍵)
|
| 454 |
+
|
| 455 |
+
### 受入基準
|
| 456 |
+
- 機能: [Docs: JWT認証仕様](https://....) を満たす
|
| 457 |
+
- 品質: lint OK / type OK / unit test Green
|
| 458 |
+
- 性能: ログイン応答時間 < 100ms
|
| 459 |
+
- 互換性: 既存API との breaking change なし
|
| 460 |
+
|
| 461 |
+
### 参照Docs
|
| 462 |
+
- https://github.com/auth0/node-jsonwebtoken (v9.0+)
|
| 463 |
+
- [既存認証パターン](./docs/auth-pattern.md)
|
| 464 |
+
|
| 465 |
+
### 主要リスク
|
| 466 |
+
1. 秘密鍵の漏洩 → Plan B: 環境変数化、ローテーション仕組み
|
| 467 |
+
2. トークン有効期限の管理 → Plan B: リフレッシュトークン実装
|
| 468 |
+
3. 既存ユーザー互換性 → Plan B: 段階的導入 + fallback
|
| 469 |
+
|
| 470 |
+
### 測定方法
|
| 471 |
+
- ユニットテスト全て Green
|
| 472 |
+
- `npm run lint` / `npm run type-check` OK
|
| 473 |
+
- 手動テスト: curl で /auth/login 確認
|
| 474 |
+
```
|
| 475 |
+
|
| 476 |
+
#### Step 2: BUILD
|
| 477 |
+
|
| 478 |
+
```
|
| 479 |
+
## BUILD RESULT
|
| 480 |
+
|
| 481 |
+
### 実装内容
|
| 482 |
+
- src/auth/jwt.ts: JWT処理関数(sign/verify)
|
| 483 |
+
- src/routes/auth.ts: /auth/login エンドポイント
|
| 484 |
+
- src/middleware/authGuard.ts: トークン検証ミドルウェア
|
| 485 |
+
- tests/auth.test.ts: 12個のユニットテスト
|
| 486 |
+
- .env.example: JWT_SECRET 追加
|
| 487 |
+
|
| 488 |
+
### git diff
|
| 489 |
+
[差分内容...]
|
| 490 |
+
|
| 491 |
+
### テスト結果
|
| 492 |
+
- Unit: 12/12 ✅ Green
|
| 493 |
+
- Manual: /auth/login → token取得確認 ✅
|
| 494 |
+
|
| 495 |
+
### 品質ゲート結果
|
| 496 |
+
- lint: ✅ OK (eslint)
|
| 497 |
+
- type-check: ✅ OK (tsc)
|
| 498 |
+
- security: ✅ OK (no hardcoded secrets)
|
| 499 |
+
|
| 500 |
+
### 再現手順
|
| 501 |
+
npm run build
|
| 502 |
+
npm run test
|
| 503 |
+
npm run start
|
| 504 |
+
# 別ウィンドウで:
|
| 505 |
+
curl -X POST http://localhost:3000/auth/login -d '{"user":"test"}'
|
| 506 |
+
```
|
| 507 |
+
|
| 508 |
+
#### Step 3: REVIEW
|
| 509 |
+
|
| 510 |
+
```
|
| 511 |
+
## REVIEW DECISION
|
| 512 |
+
|
| 513 |
+
### 設計整合性
|
| 514 |
+
- 元の PLAN との一致: ✅ YES
|
| 515 |
+
- スコープの逸脱: ✅ なし
|
| 516 |
+
|
| 517 |
+
### 品質ゲート判定
|
| 518 |
+
- 全ゲート合格: ✅ YES
|
| 519 |
+
|
| 520 |
+
### 採否判定
|
| 521 |
+
【採用】全ての基準をクリアしています。
|
| 522 |
+
```
|
| 523 |
+
|
| 524 |
+
#### Step 4: APPLY
|
| 525 |
+
|
| 526 |
+
```
|
| 527 |
+
✅ APPROVED - Merge & Commit
|
| 528 |
+
|
| 529 |
+
commit abc1234d "feat: JWT認証機能の実装
|
| 530 |
+
|
| 531 |
+
- JWT sign/verify ロジック実装
|
| 532 |
+
- /auth/login エンドポイント追加
|
| 533 |
+
- authGuard ミドルウェア実装
|
| 534 |
+
- ユニットテスト 12個追加
|
| 535 |
+
|
| 536 |
+
Refs: PLAN 2025-01-15-auth
|
| 537 |
+
Tests: 12/12 Green
|
| 538 |
+
"
|
| 539 |
+
```
|
| 540 |
+
|
| 541 |
+
#### Step 5: DOC & NEXT
|
| 542 |
+
|
| 543 |
+
```markdown
|
| 544 |
+
### 2025-01-15 15:45 - JWT認証機能 実装完了 ✅
|
| 545 |
+
|
| 546 |
+
#### 実施作業
|
| 547 |
+
- JWT認証ロジック実装
|
| 548 |
+
- ログインエンドポイント作成
|
| 549 |
+
- トークン検証ミドルウェア実装
|
| 550 |
+
- ユニットテスト 12個追加
|
| 551 |
+
|
| 552 |
+
#### ステータス
|
| 553 |
+
- ✅ 実装完了
|
| 554 |
+
- ✅ テスト合格
|
| 555 |
+
- ✅ Commit: abc1234d
|
| 556 |
+
|
| 557 |
+
#### 次回開始時の指針
|
| 558 |
+
- 次: E2Eテスト追加(ユーザーフロー全体検証)
|
| 559 |
+
- 予定時間: 60分
|
| 560 |
+
- 注意: 環境変数 JWT_SECRET は .env に設定を忘れずに
|
| 561 |
+
```
|
| 562 |
+
|
| 563 |
+
---
|
| 564 |
+
|
| 565 |
+
## 6) トラブルシューティング・アンチパターン
|
| 566 |
+
|
| 567 |
+
### 6.1 避けるべきパターン
|
| 568 |
+
|
| 569 |
+
| ❌ アンチパターン | ✅ 改善案 |
|
| 570 |
+
|-------------|---------|
|
| 571 |
+
| 仕様未確定のまま大きな実装に着手 | 必ず PLAN で仕様確認・スコープ確定 |
|
| 572 |
+
| 受入基準なしのレビュー | PLAN 時点で受入基準を明確化 |
|
| 573 |
+
| 差分未提示 / コミットメッセージ不備 | `git diff` + `git show` で常に確認 |
|
| 574 |
+
| セキュリティ/ライセンス無視 | 品質ゲートに security スキャン追加 |
|
| 575 |
+
| 証跡欠落(リンク/版/日付なし) | 一次情報リンク + 日付を必ず記載 |
|
| 576 |
+
| 複合変更を1コミットに | 1目的 = 1コミット 原則 |
|
| 577 |
+
| ログ更新を怠る | セッション終了時に必ず LOG.md 更新 |
|
| 578 |
+
|
| 579 |
+
### 6.2 よくある失敗と対策
|
| 580 |
+
|
| 581 |
+
**失敗1: 大きな機能を1サイクルで完結させようとする**
|
| 582 |
+
- ❌ 100行のコード + 多数のテスト + ドキュメント
|
| 583 |
+
- ✅ 20–30行 × 複数サイクル に分割
|
| 584 |
+
|
| 585 |
+
**失敗2: テスト後に「やっぱり仕様変更」**
|
| 586 |
+
- ❌ 受入基準なしのまま実装開始
|
| 587 |
+
- ✅ PLAN時点で受入基準を明確化・検証
|
| 588 |
+
|
| 589 |
+
**失敗3: ロールバック手順が不明**
|
| 590 |
+
- ❌ 「git reset...」(どれだ?)
|
| 591 |
+
- ✅ コミットID・コマンド・復帰手順を記録
|
| 592 |
+
|
| 593 |
+
**失敗4: 新しいセッション開始時、前の状況が分からない**
|
| 594 |
+
- ❌ ログがない、STATUS.md が古い
|
| 595 |
+
- ✅ セッション終了時に LOG.md + STATUS.md を更新
|
| 596 |
+
|
| 597 |
+
---
|
| 598 |
+
|
| 599 |
+
## 7) チェックリスト(各フェーズ)
|
| 600 |
+
|
| 601 |
+
### 7.1 PLAN チェックリスト
|
| 602 |
+
|
| 603 |
+
- [ ] 目的が1行で言えるか
|
| 604 |
+
- [ ] スコープが3–5項目か(多すぎないか)
|
| 605 |
+
- [ ] 受入基準が具体的か(テスト可能か)
|
| 606 |
+
- [ ] 参照Docs(一次情報)を記載したか
|
| 607 |
+
- [ ] リスクを最大3点に限定したか
|
| 608 |
+
- [ ] ロールバック手順が明記されているか
|
| 609 |
+
|
| 610 |
+
### 7.2 BUILD チェックリスト
|
| 611 |
+
|
| 612 |
+
- [ ] 実装内容がスコープ内か
|
| 613 |
+
- [ ] テストコード を書いたか
|
| 614 |
+
- [ ] lint / type-check を実行したか
|
| 615 |
+
- [ ] 再現手順を記載したか
|
| 616 |
+
- [ ] git diff が明確か
|
| 617 |
+
- [ ] ブロッカー/疑義は「open_questions」で明示したか
|
| 618 |
+
|
| 619 |
+
### 7.3 REVIEW チェックリスト
|
| 620 |
+
|
| 621 |
+
- [ ] PLAN と一致しているか
|
| 622 |
+
- [ ] スコープの逸脱はないか
|
| 623 |
+
- [ ] 品質ゲート全て合格か
|
| 624 |
+
- [ ] 差分は最小か(削除できる変更はないか)
|
| 625 |
+
- [ ] ロールバック手順は実行可能か
|
| 626 |
+
- [ ] 採否判定を明記したか
|
| 627 |
+
|
| 628 |
+
### 7.4 APPLY チェックリスト
|
| 629 |
+
|
| 630 |
+
- [ ] コミットメッセージは簡潔・明確か
|
| 631 |
+
- [ ] コミットID を LOG.md に記載したか
|
| 632 |
+
- [ ] STATUS.md を更新したか
|
| 633 |
+
- [ ] 次のタスクを定義したか
|
| 634 |
+
- [ ] ブロッカーはないか
|
| 635 |
+
|
| 636 |
+
---
|
| 637 |
+
|
| 638 |
+
## 8) コマンド集(よく使う操作)
|
| 639 |
+
|
| 640 |
+
```bash
|
| 641 |
+
# 状況確認
|
| 642 |
+
git status
|
| 643 |
+
git log --oneline -5
|
| 644 |
+
|
| 645 |
+
# 差分確認
|
| 646 |
+
git diff HEAD
|
| 647 |
+
git diff HEAD~1
|
| 648 |
+
|
| 649 |
+
# テスト実行
|
| 650 |
+
npm run test
|
| 651 |
+
npm run test -- --coverage
|
| 652 |
+
|
| 653 |
+
# 品質ゲート
|
| 654 |
+
npm run lint
|
| 655 |
+
npm run type-check
|
| 656 |
+
npm run security-audit
|
| 657 |
+
|
| 658 |
+
# ロールバック
|
| 659 |
+
git revert <commit-id>
|
| 660 |
+
git reset --hard HEAD~1
|
| 661 |
+
|
| 662 |
+
# ログ確認
|
| 663 |
+
tail -50 log/LOG.md
|
| 664 |
+
cat STATUS.md
|
| 665 |
+
```
|
| 666 |
+
|
| 667 |
+
---
|
| 668 |
+
|
| 669 |
+
## 9) テンプレート集
|
| 670 |
+
|
| 671 |
+
### 9.1 PLAN テンプレ(コピー&ペースト用)
|
| 672 |
+
|
| 673 |
+
```
|
| 674 |
+
## PLAN: [タスク名]
|
| 675 |
+
|
| 676 |
+
### 目的
|
| 677 |
+
[1行]
|
| 678 |
+
|
| 679 |
+
### 最小スコープ
|
| 680 |
+
- [ ] タスク1
|
| 681 |
+
- [ ] タスク2
|
| 682 |
+
- [ ] タスク3
|
| 683 |
+
|
| 684 |
+
### 受入基準
|
| 685 |
+
- **機能**:
|
| 686 |
+
- **品質**:
|
| 687 |
+
- **性能**:
|
| 688 |
+
- **互換性**:
|
| 689 |
+
|
| 690 |
+
### 参照Docs
|
| 691 |
+
- [URL + 版/日付]
|
| 692 |
+
|
| 693 |
+
### 主要リスク(最大3)
|
| 694 |
+
1. [リスク] → Plan B: [代替案]
|
| 695 |
+
|
| 696 |
+
### 測定方法
|
| 697 |
+
- [具体的な手段]
|
| 698 |
+
```
|
| 699 |
+
|
| 700 |
+
### 9.2 BUILD テンプレ
|
| 701 |
+
|
| 702 |
+
```
|
| 703 |
+
## BUILD RESULT
|
| 704 |
+
|
| 705 |
+
### 実装内容
|
| 706 |
+
- ファイルA: [変更]
|
| 707 |
+
- ファイルB: [変更]
|
| 708 |
+
|
| 709 |
+
### git diff
|
| 710 |
+
[差分]
|
| 711 |
+
|
| 712 |
+
### テスト結果
|
| 713 |
+
- Unit: [数]/[数] ✅
|
| 714 |
+
- Manual: [確認内容] ✅
|
| 715 |
+
|
| 716 |
+
### 品質ゲート
|
| 717 |
+
- lint: ✅ OK
|
| 718 |
+
- type: ✅ OK
|
| 719 |
+
|
| 720 |
+
### 再現手順
|
| 721 |
+
[コマンド]
|
| 722 |
+
```
|
| 723 |
+
|
| 724 |
+
### 9.3 LOG エントリテンプレ
|
| 725 |
+
|
| 726 |
+
```
|
| 727 |
+
### YYYY-MM-DD HH:MM - [タスク名] [ステータス]
|
| 728 |
+
|
| 729 |
+
#### 実施作業
|
| 730 |
+
- [変更1]
|
| 731 |
+
|
| 732 |
+
#### ステータス
|
| 733 |
+
- ✅ 完了 / 🔄 中断 / ❌ 失敗
|
| 734 |
+
|
| 735 |
+
#### 品質ゲート結果
|
| 736 |
+
- lint: ✅
|
| 737 |
+
- test: ✅
|
| 738 |
+
|
| 739 |
+
#### 次回開始時の指針
|
| 740 |
+
- 次のタスク: [タスク]
|
| 741 |
+
- 予定時間: XX分
|
| 742 |
+
```
|
| 743 |
+
|
| 744 |
+
---
|
| 745 |
+
|
| 746 |
+
## 最終的なルール(必読)
|
| 747 |
+
|
| 748 |
+
✅ **作業を始める前**に STATUS.md + LOG.md の最新を確認
|
| 749 |
+
✅ **すべての作業** を PLAN → BUILD → REVIEW → APPLY のループで実行
|
| 750 |
+
✅ **毎セッション終了時** に LOG.md + STATUS.md を更新
|
| 751 |
+
✅ **参照情報** には必ず一次情報・URL・日付・バージョンを添付
|
| 752 |
+
✅ **小さく回す** — 1サイクル 60–120分、1目的、1コミット
|
| 753 |
+
✅ **再現可能** — ロールバック・コマンド・再現手順を常に明記
|
| 754 |
+
|
| 755 |
+
**これらのルールを守ることで、確実で透明性のある開発が実現します。**
|
| 756 |
+
|
| 757 |
+
---
|
| 758 |
+
|
| 759 |
+
**このファイルは各プロジェクトのルートに配置し、必要に応じてカスタマイズしてください。**
|
V1.7.1/Dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# 依存パッケージインストール
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
# アプリケーションコードをコピー
|
| 10 |
+
COPY . .
|
| 11 |
+
|
| 12 |
+
# ポート公開
|
| 13 |
+
EXPOSE 7860
|
| 14 |
+
|
| 15 |
+
# 起動コマンド
|
| 16 |
+
CMD ["python", "app.py"]
|
V1.7.1/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: 超天才クイズ V3
|
| 3 |
+
emoji: 🧠
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# 超天才クイズ V3
|
| 12 |
+
|
| 13 |
+
中学受験対策4択クイズアプリ - Gemini AI搭載版
|
| 14 |
+
|
| 15 |
+
## 概要
|
| 16 |
+
|
| 17 |
+
このアプリは、中学受験を目指す生徒向けの4択クイズアプリです。Google Gemini APIを使用して、高品質な問題を自動生成します。
|
| 18 |
+
|
| 19 |
+
## 主な機能
|
| 20 |
+
|
| 21 |
+
- **4教科対応**: 国語・算数・理科・社会
|
| 22 |
+
- **AI問題生成**: Gemini APIによる自動問題生成
|
| 23 |
+
- **進捗管理**: ユーザーごとの学習履歴・統計
|
| 24 |
+
- **評価フィードバック**: AIによる学習アドバイス
|
| 25 |
+
|
| 26 |
+
## アーキテクチャ(v3)
|
| 27 |
+
|
| 28 |
+
```
|
| 29 |
+
Frontend (React+Tailwind)
|
| 30 |
+
↓
|
| 31 |
+
HF Spaces (FastAPI + Gemini直接)
|
| 32 |
+
↓
|
| 33 |
+
GAS API (Google Sheets操作)
|
| 34 |
+
↓
|
| 35 |
+
Google Sheets (データ永続化)
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
### v1からの改善点
|
| 39 |
+
- Difyを排除、Gemini APIを直接呼び出し
|
| 40 |
+
- GASはシート操作のみに限定(タイムアウト回避)
|
| 41 |
+
- HF Spacesで長時間処理に対応
|
| 42 |
+
|
| 43 |
+
## 環境変数
|
| 44 |
+
|
| 45 |
+
このアプリは以下の環境変数が必要です(HF SpacesのSecretsで設定):
|
| 46 |
+
|
| 47 |
+
```
|
| 48 |
+
GEMINI_API_KEY=xxx # Google Gemini API Key
|
| 49 |
+
GAS_API_URL=xxx # Google Apps Script Web App URL
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## 技術スタック
|
| 53 |
+
|
| 54 |
+
- **Backend**: FastAPI (Python 3.11)
|
| 55 |
+
- **AI**: Google Gemini API
|
| 56 |
+
- **Frontend**: Vanilla JS + Tailwind CSS
|
| 57 |
+
- **Database**: Google Sheets (via GAS API)
|
| 58 |
+
- **Deployment**: HuggingFace Spaces (Docker SDK)
|
| 59 |
+
|
| 60 |
+
## ローカル開発
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
# 依存関係インストール
|
| 64 |
+
pip install -r requirements.txt
|
| 65 |
+
|
| 66 |
+
# 環境変数設定
|
| 67 |
+
cp .env.example .env
|
| 68 |
+
# .envファイルを編集してAPIキーを設定
|
| 69 |
+
|
| 70 |
+
# サーバー起動
|
| 71 |
+
python app.py
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
アプリは http://localhost:7860 で起動します。
|
| 75 |
+
|
| 76 |
+
## License
|
| 77 |
+
|
| 78 |
+
MIT
|
V1.7.1/RECOVERY.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# v1.7.1 復旧手順書
|
| 2 |
+
|
| 3 |
+
## バージョン情報
|
| 4 |
+
- **バージョン**: v1.7.1
|
| 5 |
+
- **リリース日**: 2025-12-21
|
| 6 |
+
- **タグ**: v1.7.1
|
| 7 |
+
- **コミット**: 15488d0
|
| 8 |
+
|
| 9 |
+
## 新機能
|
| 10 |
+
- 1科目3分(180秒)の制限時間
|
| 11 |
+
- カウントダウンタイマー表示(ヘッダー右上)
|
| 12 |
+
- 時間切れダイアログ → 未回答は不正解として処理
|
| 13 |
+
- 各問題の回答時間(time_taken_seconds)をGASに記録
|
| 14 |
+
- 各教科10問に制限(Gemini過剰生成対策)
|
| 15 |
+
|
| 16 |
+
## 復旧手順
|
| 17 |
+
|
| 18 |
+
### 方法1: Gitタグから復旧
|
| 19 |
+
```bash
|
| 20 |
+
cd /path/to/project
|
| 21 |
+
git checkout v1.7.1
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
### 方法2: このフォルダから復旧
|
| 25 |
+
```bash
|
| 26 |
+
# 1. 現在の作業をバックアップ
|
| 27 |
+
cp -r /path/to/project /path/to/project_backup
|
| 28 |
+
|
| 29 |
+
# 2. このフォルダの内容をコピー
|
| 30 |
+
cp -r V1.7.1/* /path/to/project/
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
## GAS情報
|
| 34 |
+
- **Version**: @42 (v1.6.15から変更なし)
|
| 35 |
+
- **URL**: `https://script.google.com/macros/s/AKfycbw52gHnVanyvr91As9BhXd2Tv-2bYjwns8dR-6tt59FR3K2xfktMLoiTPsEAIffJ5ED/exec`
|
| 36 |
+
|
| 37 |
+
## 動作確認済み項目
|
| 38 |
+
- [x] タイマー表示・カウントダウン
|
| 39 |
+
- [x] 時間切れダイアログ
|
| 40 |
+
- [x] 未回答の不正解処理
|
| 41 |
+
- [x] time_spent記録(Answersシート)
|
| 42 |
+
- [x] 各教科10問制限
|
V1.7.1/STATUS.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# プロジェクト: 超天才クイズ v1.7.0
|
| 2 |
+
|
| 3 |
+
## ステータス概要
|
| 4 |
+
- **現在地**: v1.7.1 制限時間機能 修正完了 → E2Eテスト待ち
|
| 5 |
+
- **完成度**: 98%
|
| 6 |
+
- **最大課題**: 問題数・タイマー動作確認
|
| 7 |
+
|
| 8 |
+
## v1.7.1 修正内容
|
| 9 |
+
|
| 10 |
+
### E2Eテスト結果(2025-12-21)
|
| 11 |
+
- ✅ タイマー表示・カウントダウン正常動作
|
| 12 |
+
- ✅ 時間切れダイアログ正常表示
|
| 13 |
+
- ✅ 未回答の不正解処理
|
| 14 |
+
- ⚠️ 制限時間5分は長い → 3分に短縮
|
| 15 |
+
- ⚠️ 算数27問生成 → 10問制限のスライス処理追加
|
| 16 |
+
|
| 17 |
+
### v1.7.1 修正
|
| 18 |
+
1. **制限時間短縮**: 5分 → 3分(180秒)に変更
|
| 19 |
+
2. **問題数制限**: Geminiが10問超生成時にスライスで10問に制限
|
| 20 |
+
|
| 21 |
+
## v1.7.0 新機能: 制限時間
|
| 22 |
+
|
| 23 |
+
### 実装内容
|
| 24 |
+
- 1科目あたり3分(180秒)の制限時間
|
| 25 |
+
- カウントダウンタイマーをヘッダー右上に常時表示
|
| 26 |
+
- 時間切れ時: ダイアログ表示 → OK → 未回答は不正解 → 結果画面へ
|
| 27 |
+
- 各問題の回答時間(time_taken_seconds)をGASに記録
|
| 28 |
+
|
| 29 |
+
### 変更ファイル
|
| 30 |
+
- `static/js/components.js`: App, SubjectSelectScreen, QuizScreen修正
|
| 31 |
+
|
| 32 |
+
### GAS変更
|
| 33 |
+
- なし(既存の`time_taken_seconds`インフラを活用)
|
| 34 |
+
|
| 35 |
+
---
|
| 36 |
+
|
| 37 |
+
## v1.6.15 修正内容
|
| 38 |
+
|
| 39 |
+
### USAGE_COUNT抽出時更新 ✅
|
| 40 |
+
- **問題**: v1.6.14でもUSAGE_COUNTが更新されなかった
|
| 41 |
+
- **原因**: Gemini生成結果に`answer_id`が含まれないため、app.pyからの更新呼び出しが空リストになっていた
|
| 42 |
+
- **修正**:
|
| 43 |
+
- GAS: `get_questions_by_genre_config`で問題抽出時にUSAGE_COUNT+1
|
| 44 |
+
- app.py: 不要な`update_usage_count`呼び出しを削除
|
| 45 |
+
- **GAS Version**: @42
|
| 46 |
+
|
| 47 |
+
### v1.6.13 累積成績問題修正 ✅
|
| 48 |
+
- app.pyの`get_statistics`をGASの新形式に対応
|
| 49 |
+
- **コミット**: bd111b9
|
| 50 |
+
|
| 51 |
+
## 機能一覧
|
| 52 |
+
|
| 53 |
+
| 項目 | ステータス | 備考 |
|
| 54 |
+
|------|-----------|------|
|
| 55 |
+
| ログイン | ✅ OK | 正常動作 |
|
| 56 |
+
| 問題生成 | ✅ OK | Gemini API連携 |
|
| 57 |
+
| 正解判定 | ✅ OK | 動作確認済み |
|
| 58 |
+
| SCORE表示 | ✅ OK | 正常表示 |
|
| 59 |
+
| 累積成績 | ✅ OK | v1.6.13で修正 |
|
| 60 |
+
| **均等出題** | ✅ OK | v1.6.14で実装 |
|
| 61 |
+
|
| 62 |
+
## デプロイ情報
|
| 63 |
+
|
| 64 |
+
| 項目 | 値 |
|
| 65 |
+
|------|-----|
|
| 66 |
+
| **HF Space** | leave-everything/ChoTensai_V3 |
|
| 67 |
+
| **GAS Version** | @42(v1.6.15) |
|
| 68 |
+
| **GAS URL** | `https://script.google.com/macros/s/AKfycbw52gHnVanyvr91As9BhXd2Tv-2bYjwns8dR-6tt59FR3K2xfktMLoiTPsEAIffJ5ED/exec` |
|
| 69 |
+
| **Spreadsheet ID** | `10JLP5ds2CNDOEYTxEzDyY82dErbD-InkTeuyzS_w_3U` |
|
| 70 |
+
|
| 71 |
+
## コンポーネント完成度
|
| 72 |
+
|
| 73 |
+
| コンポーネント | 完成度 | ステータス | 備考 |
|
| 74 |
+
|-------------|--------|-----------|------|
|
| 75 |
+
| 基本構造 | 100% | ✅ 完了 | - |
|
| 76 |
+
| 問題生成(Dify) | 100% | ✅ 完了 | temperature=1.0統一済み |
|
| 77 |
+
| GAS API | 100% | ✅ 完了 | SCORE問題修正済み |
|
| 78 |
+
| Backend(app.py) | 100% | ✅ 完了 | v1.6.13で累積成績修正 |
|
| 79 |
+
| フロントエンド | 100% | ✅ 完了 | - |
|
| 80 |
+
| E2Eテスト | 100% | ✅ 完了 | 全項目合格 |
|
| 81 |
+
|
| 82 |
+
## 作業環境
|
| 83 |
+
|
| 84 |
+
**唯一の作業フォルダ**:
|
| 85 |
+
```
|
| 86 |
+
/mnt/c/Users/tomo2/OneDrive/デスクトップ/開発/HuggingFace/超天才クイズv3/
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
## 次のステップ
|
| 90 |
+
|
| 91 |
+
1. **運用開始**
|
| 92 |
+
- 全機能が正常動作することを確認済み
|
| 93 |
+
- ユーザーからのフィードバック収集
|
| 94 |
+
|
| 95 |
+
2. **今後の改善候補**(優先度: 低)
|
| 96 |
+
- パフォーマンス最適化
|
| 97 |
+
- UI/UX改善
|
| 98 |
+
|
| 99 |
+
## 最新の変更履歴
|
| 100 |
+
|
| 101 |
+
- **2025-12-21**: v1.6.15 STABLEバックアップ作成(V1.6.15フォルダ)
|
| 102 |
+
- **2025-12-21**: 今後の構想ドキュメント作成(docs/FUTURE_VISION.md)
|
| 103 |
+
- **2025-12-21**: v1.6.15リリース(USAGE_COUNT抽出時更新)
|
| 104 |
+
- **2025-12-21**: v1.6.14リリース(USAGE_COUNT均等出題機能)
|
| 105 |
+
- **2025-12-21**: v1.6.13リリース(累積成績問題修正)
|
| 106 |
+
- **2025-12-20**: v1.6.12 E2Eテスト実施(SCORE表示正常化確認)
|
| 107 |
+
- **2025-12-20**: v1.6.11リリース(SCORE問題修正)
|
| 108 |
+
|
| 109 |
+
## バックアップ
|
| 110 |
+
|
| 111 |
+
| フォルダ | バージョン | 説明 |
|
| 112 |
+
|---------|-----------|------|
|
| 113 |
+
| `V1.6.15/` | v1.6.15 STABLE | 完全復旧可能なバックアップ(RECOVERY.md含む) |
|
| 114 |
+
|
| 115 |
+
## 今後の構想
|
| 116 |
+
|
| 117 |
+
詳細は `docs/FUTURE_VISION.md` を参照
|
| 118 |
+
|
| 119 |
+
| 区分 | 内容 |
|
| 120 |
+
|------|------|
|
| 121 |
+
| 【A】本体改善 | ユーザー単位管理、制限時間設定 |
|
| 122 |
+
| 【B】派生サービス | 問題配信、ランキング、クラス制 |
|
| 123 |
+
|
| 124 |
+
---
|
| 125 |
+
**最終更新**: 2025-12-21
|
V1.7.1/app.py
ADDED
|
@@ -0,0 +1,689 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
超天才クイズ v3 - FastAPI Backend
|
| 3 |
+
HuggingFace Spaces Docker SDK
|
| 4 |
+
|
| 5 |
+
Difyを排除し、Gemini APIを直接呼び出すアーキテクチャ
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import json
|
| 10 |
+
import logging
|
| 11 |
+
import asyncio
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from fastapi import FastAPI, HTTPException
|
| 14 |
+
from fastapi.staticfiles import StaticFiles
|
| 15 |
+
from fastapi.responses import FileResponse
|
| 16 |
+
from pydantic import BaseModel
|
| 17 |
+
from typing import List, Optional, Dict
|
| 18 |
+
from dotenv import load_dotenv
|
| 19 |
+
|
| 20 |
+
# .envファイルを読み込み
|
| 21 |
+
load_dotenv()
|
| 22 |
+
|
| 23 |
+
# サービス
|
| 24 |
+
from src.services.gemini_service import GeminiService
|
| 25 |
+
from src.services.gas_client import GASClient, extract_exclude_keywords
|
| 26 |
+
# KnowledgeService削除 - QuestionDB駆動に移行(v1.6.0)
|
| 27 |
+
from src.services.auth_service import AuthService
|
| 28 |
+
from src.services.questiondb_service import QuestionDBService
|
| 29 |
+
|
| 30 |
+
# ロギング設定
|
| 31 |
+
logging.basicConfig(level=logging.INFO)
|
| 32 |
+
logger = logging.getLogger(__name__)
|
| 33 |
+
|
| 34 |
+
# FastAPIアプリ
|
| 35 |
+
app = FastAPI(
|
| 36 |
+
title="超天才クイズ v3 API",
|
| 37 |
+
description="中学受験対策4択クイズ - Gemini直接統合版",
|
| 38 |
+
version="3.0.0"
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# サービス初期化
|
| 42 |
+
gemini_service = GeminiService()
|
| 43 |
+
gas_client = GASClient()
|
| 44 |
+
# KnowledgeService削除 - QuestionDB駆動に移行(v1.6.0)
|
| 45 |
+
questiondb_service = QuestionDBService(gas_client=gas_client, gemini_service=gemini_service)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# =============================================================================
|
| 49 |
+
# リクエスト/レスポンスモデル
|
| 50 |
+
# =============================================================================
|
| 51 |
+
|
| 52 |
+
class RegisterUserRequest(BaseModel):
|
| 53 |
+
username: str
|
| 54 |
+
password: str
|
| 55 |
+
invite_code: str
|
| 56 |
+
|
| 57 |
+
class StartSessionRequest(BaseModel):
|
| 58 |
+
user_id: str
|
| 59 |
+
subjects: List[str]
|
| 60 |
+
|
| 61 |
+
class GenerateQuestionsRequest(BaseModel):
|
| 62 |
+
session_id: str
|
| 63 |
+
subjects: List[str]
|
| 64 |
+
user_id: Optional[str] = None # v1.4.0: 要約・ジャンルカウント取得用
|
| 65 |
+
|
| 66 |
+
class Answer(BaseModel):
|
| 67 |
+
question_id: str
|
| 68 |
+
selected_answer: int
|
| 69 |
+
user_answer: Optional[int] = None # GAS APIに渡す用(数値型で統一)
|
| 70 |
+
correct_answer: Optional[int] = None # GAS APIに渡す用
|
| 71 |
+
subject: Optional[str] = None
|
| 72 |
+
category: Optional[str] = None
|
| 73 |
+
time_taken_seconds: Optional[int] = 0
|
| 74 |
+
|
| 75 |
+
class SubmitAnswersRequest(BaseModel):
|
| 76 |
+
session_id: str
|
| 77 |
+
answers: List[Answer]
|
| 78 |
+
|
| 79 |
+
class GetStatisticsRequest(BaseModel):
|
| 80 |
+
user_id: str
|
| 81 |
+
subjects: Optional[List[str]] = None
|
| 82 |
+
|
| 83 |
+
class GetEvaluationRequest(BaseModel):
|
| 84 |
+
session_id: str
|
| 85 |
+
subjects: Optional[List[str]] = None
|
| 86 |
+
|
| 87 |
+
class LoginRequest(BaseModel):
|
| 88 |
+
username: str
|
| 89 |
+
password: str
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# =============================================================================
|
| 93 |
+
# バックグラウンドタスク
|
| 94 |
+
# =============================================================================
|
| 95 |
+
|
| 96 |
+
async def background_summary_generation(
|
| 97 |
+
session_id: str,
|
| 98 |
+
validated_by_subject: Dict[str, List[Dict]]
|
| 99 |
+
):
|
| 100 |
+
"""
|
| 101 |
+
バックグラウンドでサマリー生成→GAS保存
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
session_id: セッションID
|
| 105 |
+
validated_by_subject: 教科別問題辞書
|
| 106 |
+
"""
|
| 107 |
+
try:
|
| 108 |
+
logger.info(f"background_summary_generation: Starting for session_id={session_id}")
|
| 109 |
+
|
| 110 |
+
# 各教科のサマリーを生成
|
| 111 |
+
for subject, questions in validated_by_subject.items():
|
| 112 |
+
if not questions:
|
| 113 |
+
continue
|
| 114 |
+
|
| 115 |
+
try:
|
| 116 |
+
# Geminiでサマリー生成
|
| 117 |
+
logger.info(f"background_summary_generation: Generating summary for {subject} ({len(questions)} questions)")
|
| 118 |
+
summary_data = await gemini_service.generate_summary(subject, questions)
|
| 119 |
+
|
| 120 |
+
# GASに保存
|
| 121 |
+
logger.info(f"background_summary_generation: Saving summary for {subject} to GAS")
|
| 122 |
+
save_result = await gas_client.save_summary(
|
| 123 |
+
session_id=session_id,
|
| 124 |
+
subject=subject,
|
| 125 |
+
summary_data=summary_data
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
if save_result.get("success"):
|
| 129 |
+
logger.info(f"background_summary_generation: Summary saved successfully for {subject}")
|
| 130 |
+
else:
|
| 131 |
+
logger.warning(f"background_summary_generation: Failed to save summary for {subject}: {save_result}")
|
| 132 |
+
|
| 133 |
+
except Exception as subject_error:
|
| 134 |
+
logger.error(f"background_summary_generation: Error processing {subject}: {subject_error}")
|
| 135 |
+
# 1教科の失敗は他に影響させない
|
| 136 |
+
continue
|
| 137 |
+
|
| 138 |
+
logger.info(f"background_summary_generation: Completed for session_id={session_id}")
|
| 139 |
+
|
| 140 |
+
except Exception as e:
|
| 141 |
+
logger.error(f"background_summary_generation: Fatal error for session_id={session_id}: {e}")
|
| 142 |
+
# バックグラウンドタスクなので例外は握りつぶす
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
# =============================================================================
|
| 146 |
+
# APIエンドポイント
|
| 147 |
+
# =============================================================================
|
| 148 |
+
|
| 149 |
+
@app.get("/api/health")
|
| 150 |
+
async def health_check():
|
| 151 |
+
"""ヘルスチェック"""
|
| 152 |
+
return {
|
| 153 |
+
"status": "healthy",
|
| 154 |
+
"version": "3.0.0",
|
| 155 |
+
"services": {
|
| 156 |
+
"gemini": gemini_service.is_available(),
|
| 157 |
+
"gas": gas_client.is_available(),
|
| 158 |
+
"questiondb": True # QuestionDB駆動に移行(v1.6.0)
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
@app.post("/api/register_user")
|
| 164 |
+
async def register_user(request: RegisterUserRequest):
|
| 165 |
+
"""ユーザー登録"""
|
| 166 |
+
try:
|
| 167 |
+
# 招待コード検証
|
| 168 |
+
valid_invite_code = os.environ.get("INVITE_CODE", "")
|
| 169 |
+
|
| 170 |
+
if not valid_invite_code:
|
| 171 |
+
logger.error("register_user: INVITE_CODE environment variable is not set")
|
| 172 |
+
raise HTTPException(status_code=403, detail="招待コードが設定されていません")
|
| 173 |
+
|
| 174 |
+
if request.invite_code != valid_invite_code:
|
| 175 |
+
logger.warning(f"register_user: Invalid invite code attempt for user '{request.username}'")
|
| 176 |
+
raise HTTPException(status_code=403, detail="招待コードが無効です")
|
| 177 |
+
|
| 178 |
+
logger.info(f"register_user: Invite code verified for user '{request.username}'")
|
| 179 |
+
|
| 180 |
+
# パスワードをハッシュ化
|
| 181 |
+
hashed_password = AuthService.hash_password(request.password)
|
| 182 |
+
logger.info(f"register_user: Password hashed for user '{request.username}'")
|
| 183 |
+
|
| 184 |
+
# GAS連携処理(ハッシュ化されたパスワードを渡す)
|
| 185 |
+
result = await gas_client.register_user(request.username, hashed_password)
|
| 186 |
+
|
| 187 |
+
# 既存ユーザーの場合はエラー
|
| 188 |
+
if result.get("data", {}).get("is_new") == False:
|
| 189 |
+
raise HTTPException(status_code=409, detail="このユーザー名は既に登録されています")
|
| 190 |
+
|
| 191 |
+
return result
|
| 192 |
+
except HTTPException:
|
| 193 |
+
# HTTPExceptionはそのまま再送出
|
| 194 |
+
raise
|
| 195 |
+
except Exception as e:
|
| 196 |
+
logger.error(f"register_user error: {e}")
|
| 197 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
@app.post("/api/login")
|
| 201 |
+
async def login(request: LoginRequest):
|
| 202 |
+
"""ログイン"""
|
| 203 |
+
try:
|
| 204 |
+
# GASからユーザー情報取得
|
| 205 |
+
result = await gas_client.login(request.username)
|
| 206 |
+
|
| 207 |
+
# ユーザーが見つからない場合
|
| 208 |
+
if not result.get("found"):
|
| 209 |
+
logger.warning(f"login: User not found: '{request.username}'")
|
| 210 |
+
raise HTTPException(status_code=401, detail="ユーザー名またはパスワードが正しくありません")
|
| 211 |
+
|
| 212 |
+
data = result.get("data", {})
|
| 213 |
+
stored_hash = data.get("password_hash")
|
| 214 |
+
|
| 215 |
+
# パスワード未設定の既存ユーザー(v1.0からの移行ユーザー)
|
| 216 |
+
if not stored_hash:
|
| 217 |
+
logger.info(f"login: User '{request.username}' needs password migration")
|
| 218 |
+
raise HTTPException(
|
| 219 |
+
status_code=403,
|
| 220 |
+
detail="パスワードが未設定です。新規登録画面からパスワードを設定してください。"
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
# パスワード検証
|
| 224 |
+
if not AuthService.verify_password(request.password, stored_hash):
|
| 225 |
+
logger.warning(f"login: Invalid password for user '{request.username}'")
|
| 226 |
+
raise HTTPException(status_code=401, detail="ユーザー名またはパスワードが正しくありません")
|
| 227 |
+
|
| 228 |
+
logger.info(f"login: User '{request.username}' logged in successfully")
|
| 229 |
+
|
| 230 |
+
return {
|
| 231 |
+
"success": True,
|
| 232 |
+
"data": {
|
| 233 |
+
"user_id": data.get("user_id"),
|
| 234 |
+
"username": data.get("username")
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
except HTTPException:
|
| 238 |
+
raise
|
| 239 |
+
except Exception as e:
|
| 240 |
+
logger.error(f"login error: {e}")
|
| 241 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
@app.post("/api/start_session")
|
| 245 |
+
async def start_session(request: StartSessionRequest):
|
| 246 |
+
"""セッション開始"""
|
| 247 |
+
try:
|
| 248 |
+
result = await gas_client.start_session(request.user_id, request.subjects)
|
| 249 |
+
return result
|
| 250 |
+
except Exception as e:
|
| 251 |
+
logger.error(f"start_session error: {e}")
|
| 252 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
@app.post("/api/generate_questions")
|
| 256 |
+
async def generate_questions(request: GenerateQuestionsRequest):
|
| 257 |
+
"""問題生成 - QuestionDB駆動(高速版)
|
| 258 |
+
|
| 259 |
+
v1.6.0: QuestionDB駆動に移行
|
| 260 |
+
- Before: Gemini API 2回呼び出し(問題生成+検証)→ 20-60秒
|
| 261 |
+
- After: QuestionDB取得 + Gemini 1回(選択肢のみ)→ 2-5秒
|
| 262 |
+
"""
|
| 263 |
+
try:
|
| 264 |
+
logger.info(f"generate_questions: Starting QuestionDB-driven generation for {len(request.subjects)} subjects: {request.subjects}")
|
| 265 |
+
|
| 266 |
+
# Phase 0: 統計・要約取得(優先ジャンル・除外ID用)
|
| 267 |
+
priority_genres = None
|
| 268 |
+
exclude_ids = None
|
| 269 |
+
|
| 270 |
+
if request.user_id:
|
| 271 |
+
try:
|
| 272 |
+
# 統計取得(ジャンルカウント含む)
|
| 273 |
+
stats_result = await gas_client.get_statistics(request.user_id, request.subjects)
|
| 274 |
+
priority_genres = stats_result.get("data", {}).get("priority_genres", {})
|
| 275 |
+
logger.info(f"generate_questions: Got priority_genres for {len(priority_genres)} subjects")
|
| 276 |
+
|
| 277 |
+
# 要約取得(重複問題除外用)
|
| 278 |
+
summaries_result = await gas_client.get_question_summaries(request.user_id, limit=5)
|
| 279 |
+
exclude_keywords = extract_exclude_keywords(summaries_result.get("data", summaries_result))
|
| 280 |
+
# TODO: exclude_keywords から exclude_ids に変換する処理(将来実装)
|
| 281 |
+
logger.info(f"generate_questions: Got {len(exclude_keywords)} exclude keywords")
|
| 282 |
+
except Exception as e:
|
| 283 |
+
logger.warning(f"generate_questions: Failed to get stats/summaries (non-fatal): {e}")
|
| 284 |
+
|
| 285 |
+
# Phase 1: QuestionDBから問題取得 + 選択肢生成
|
| 286 |
+
logger.info("generate_questions: Fetching questions from QuestionDB")
|
| 287 |
+
questions_by_subject = await questiondb_service.get_questions_from_db(
|
| 288 |
+
subjects=request.subjects,
|
| 289 |
+
count_per_subject=10,
|
| 290 |
+
priority_genres=priority_genres,
|
| 291 |
+
exclude_ids=exclude_ids
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
# 取得結果の確認
|
| 295 |
+
total_generated = sum(len(qs) for qs in questions_by_subject.values())
|
| 296 |
+
logger.info(f"generate_questions: Retrieved {total_generated} questions from DB")
|
| 297 |
+
|
| 298 |
+
# 取得失敗の確認
|
| 299 |
+
failed_subjects = [s for s in request.subjects if len(questions_by_subject.get(s, [])) == 0]
|
| 300 |
+
if len(failed_subjects) == len(request.subjects):
|
| 301 |
+
raise HTTPException(
|
| 302 |
+
status_code=500,
|
| 303 |
+
detail="QuestionDatabaseから問題を取得できませんでした。もう一度お試しください。"
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
# Phase 2: 並列保存
|
| 307 |
+
async def save_for_subject(subject: str, questions: list):
|
| 308 |
+
"""教科ごとのGAS保存処理"""
|
| 309 |
+
saved = await gas_client.save_questions(
|
| 310 |
+
session_id=request.session_id,
|
| 311 |
+
subject=subject,
|
| 312 |
+
questions=questions
|
| 313 |
+
)
|
| 314 |
+
logger.info(f"generate_questions: Saved {len(saved)} questions for '{subject}'")
|
| 315 |
+
return {"subject": subject, "saved": saved}
|
| 316 |
+
|
| 317 |
+
save_tasks = [
|
| 318 |
+
save_for_subject(subject, questions)
|
| 319 |
+
for subject, questions in questions_by_subject.items()
|
| 320 |
+
if questions # 空でない場合のみ保存
|
| 321 |
+
]
|
| 322 |
+
save_results = await asyncio.gather(*save_tasks, return_exceptions=True)
|
| 323 |
+
|
| 324 |
+
# 教科名マッピング
|
| 325 |
+
subject_names = {
|
| 326 |
+
"jp": "国語",
|
| 327 |
+
"math": "算数",
|
| 328 |
+
"sci": "理科",
|
| 329 |
+
"soc": "社会"
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
# 保存結果を集約(元データ + question_id マージ)
|
| 333 |
+
all_questions = []
|
| 334 |
+
logger.info(f"generate_questions: Question save summary - total_subjects={len(save_results)}")
|
| 335 |
+
for idx, result in enumerate(save_results):
|
| 336 |
+
if isinstance(result, Exception):
|
| 337 |
+
logger.error(f"generate_questions: Save failed for task {idx}: {result}")
|
| 338 |
+
else:
|
| 339 |
+
subject = result.get("subject", "unknown")
|
| 340 |
+
saved = result.get("saved", [])
|
| 341 |
+
original = questions_by_subject.get(subject, []) # 元のGemini生成データ
|
| 342 |
+
|
| 343 |
+
# question_id をマージして完全なデータを構築
|
| 344 |
+
for i, orig_q in enumerate(original):
|
| 345 |
+
if i < len(saved):
|
| 346 |
+
orig_q["question_id"] = saved[i].get("question_id")
|
| 347 |
+
orig_q["subject"] = subject
|
| 348 |
+
orig_q["subject_name"] = subject_names.get(subject, subject)
|
| 349 |
+
|
| 350 |
+
all_questions.extend(original)
|
| 351 |
+
logger.info(f"generate_questions: Saved summary - subject={subject}, saved_count={len(original)}")
|
| 352 |
+
|
| 353 |
+
logger.info(f"generate_questions: All questions saved - total_count={len(all_questions)}")
|
| 354 |
+
|
| 355 |
+
# Phase 3: usage_count更新
|
| 356 |
+
# v1.6.15: GAS側で抽出時に更新するため、ここでの呼び出しは不要
|
| 357 |
+
# (抽出時更新により、Gemini生成失敗時もカウントされるがシンプルさを優先)
|
| 358 |
+
|
| 359 |
+
# Phase 4: バックグラウンドでサマリー生成(UIレスポンスを待たせない)
|
| 360 |
+
asyncio.create_task(
|
| 361 |
+
background_summary_generation(
|
| 362 |
+
session_id=request.session_id,
|
| 363 |
+
validated_by_subject=questions_by_subject
|
| 364 |
+
)
|
| 365 |
+
)
|
| 366 |
+
logger.info(f"generate_questions: Background summary generation task created")
|
| 367 |
+
|
| 368 |
+
# レスポンス構築
|
| 369 |
+
response_data = {
|
| 370 |
+
"session_id": request.session_id,
|
| 371 |
+
"questions": all_questions,
|
| 372 |
+
"total_count": len(all_questions)
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
if failed_subjects:
|
| 376 |
+
response_data["warnings"] = {
|
| 377 |
+
"failed_subjects": failed_subjects,
|
| 378 |
+
"message": f"以下の教科で��題取得に失敗しました: {', '.join(failed_subjects)}"
|
| 379 |
+
}
|
| 380 |
+
logger.warning(f"generate_questions: Partial success - failed subjects: {failed_subjects}")
|
| 381 |
+
|
| 382 |
+
logger.info(f"generate_questions: Completed with {len(all_questions)} total questions")
|
| 383 |
+
|
| 384 |
+
return {
|
| 385 |
+
"success": True,
|
| 386 |
+
"data": response_data
|
| 387 |
+
}
|
| 388 |
+
except HTTPException:
|
| 389 |
+
raise
|
| 390 |
+
except Exception as e:
|
| 391 |
+
logger.error(f"generate_questions error: {e}")
|
| 392 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
@app.post("/api/submit_answers")
|
| 396 |
+
async def submit_answers(request: SubmitAnswersRequest):
|
| 397 |
+
"""解答送信"""
|
| 398 |
+
try:
|
| 399 |
+
result = await gas_client.submit_answers(
|
| 400 |
+
session_id=request.session_id,
|
| 401 |
+
answers=[a.model_dump() for a in request.answers]
|
| 402 |
+
)
|
| 403 |
+
logger.info(f"[submit_answers] GAS response: {result}")
|
| 404 |
+
return result
|
| 405 |
+
except Exception as e:
|
| 406 |
+
logger.error(f"submit_answers error: {e}")
|
| 407 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
@app.post("/api/get_statistics")
|
| 411 |
+
async def get_statistics(request: GetStatisticsRequest):
|
| 412 |
+
"""統計取得
|
| 413 |
+
|
| 414 |
+
v1.6.13: GASからのcumulative/subjectsをそのまま使用するように修正
|
| 415 |
+
"""
|
| 416 |
+
try:
|
| 417 |
+
result = await gas_client.get_statistics(
|
| 418 |
+
user_id=request.user_id,
|
| 419 |
+
subjects=request.subjects
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
# GASレスポンスをフロントエンド期待形式に変換
|
| 423 |
+
if result.get("success"):
|
| 424 |
+
gas_data = result.get("data", {})
|
| 425 |
+
logger.debug(f"get_statistics: GAS response keys: {gas_data.keys()}")
|
| 426 |
+
|
| 427 |
+
# v1.6.13: GASからの cumulative と subjects をそのまま使用
|
| 428 |
+
# GASは以下の形式で返す:
|
| 429 |
+
# {
|
| 430 |
+
# "user_id": "...",
|
| 431 |
+
# "subjects": [...],
|
| 432 |
+
# "radar_chart": {...},
|
| 433 |
+
# "cumulative": { "total_sessions", "total_questions", "total_correct", "overall_accuracy" }
|
| 434 |
+
# }
|
| 435 |
+
|
| 436 |
+
gas_cumulative = gas_data.get("cumulative", {})
|
| 437 |
+
gas_subjects = gas_data.get("subjects", [])
|
| 438 |
+
|
| 439 |
+
logger.debug(f"get_statistics: cumulative from GAS: {gas_cumulative}")
|
| 440 |
+
logger.debug(f"get_statistics: subjects count from GAS: {len(gas_subjects)}")
|
| 441 |
+
|
| 442 |
+
# 教科名マッピング(GASにない場合のフォールバック用)
|
| 443 |
+
subject_names = {
|
| 444 |
+
"jp": "国語",
|
| 445 |
+
"math": "算数",
|
| 446 |
+
"sci": "理科",
|
| 447 |
+
"soc": "社会"
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
# ジャンル名マッピング(CLAUDE.md準拠)
|
| 451 |
+
genre_names = {
|
| 452 |
+
# 国語
|
| 453 |
+
"JP01": "漢字・語彙(読み書き、四字熟語、慣用句、ことわざ)",
|
| 454 |
+
"JP02": "文法・言葉のきまり(品詞、敬語、文の成分、修飾関係)",
|
| 455 |
+
"JP03": "物語文読解(心情理解、場面把握、人物関係)",
|
| 456 |
+
"JP04": "説明文・論説文読解(要旨、段落構成、筆者の主張)",
|
| 457 |
+
"JP05": "随筆文読解(筆者の体験・感想の読み取り)",
|
| 458 |
+
"JP06": "詩・韻文(詩、短歌、俳句、表現技法)",
|
| 459 |
+
"JP07": "記述問題(理由説明、要約、意見記述)",
|
| 460 |
+
"JP08": "知識・文学史(作家、作品名、文学的常識)",
|
| 461 |
+
# 算数
|
| 462 |
+
"MA01": "計算",
|
| 463 |
+
"MA02": "数の性質",
|
| 464 |
+
"MA03": "割合・比",
|
| 465 |
+
"MA04": "速さ",
|
| 466 |
+
"MA05": "文章題(その他)",
|
| 467 |
+
"MA06": "平面図形",
|
| 468 |
+
"MA07": "立体図形",
|
| 469 |
+
"MA08": "場合の数・確率",
|
| 470 |
+
"MA09": "グラフ・表",
|
| 471 |
+
"MA10": "特殊算",
|
| 472 |
+
# 理科
|
| 473 |
+
"SC01": "力・運動",
|
| 474 |
+
"SC02": "電気",
|
| 475 |
+
"SC03": "光・音・熱",
|
| 476 |
+
"SC04": "物質の性質",
|
| 477 |
+
"SC05": "水溶液",
|
| 478 |
+
"SC06": "燃焼・化学変化",
|
| 479 |
+
"SC07": "植物",
|
| 480 |
+
"SC08": "動物",
|
| 481 |
+
"SC09": "人体",
|
| 482 |
+
"SC10": "天体",
|
| 483 |
+
"SC11": "気象",
|
| 484 |
+
"SC12": "地学",
|
| 485 |
+
# 社会
|
| 486 |
+
"SO01": "日本地理(国土・自然)",
|
| 487 |
+
"SO02": "日本地理(産業)",
|
| 488 |
+
"SO03": "世界地理",
|
| 489 |
+
"SO04": "歴史(古代〜平安)",
|
| 490 |
+
"SO05": "歴史(鎌倉〜室町)",
|
| 491 |
+
"SO06": "歴史(安土桃山〜江戸)",
|
| 492 |
+
"SO07": "歴史(明治〜現代)",
|
| 493 |
+
"SO08": "公民(政治・憲法)",
|
| 494 |
+
"SO09": "公民(経済・国際)",
|
| 495 |
+
"SO10": "時事問題"
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
# subjects配列を処理(ジャンル名を補完)
|
| 499 |
+
subjects_list = []
|
| 500 |
+
for subj in gas_subjects:
|
| 501 |
+
# subject_nameがない場合はマッピングから取得
|
| 502 |
+
if not subj.get("subject_name"):
|
| 503 |
+
subj["subject_name"] = subject_names.get(subj.get("subject", ""), subj.get("subject", ""))
|
| 504 |
+
|
| 505 |
+
# genresのジャンル名を補完
|
| 506 |
+
genres = subj.get("genres", [])
|
| 507 |
+
for genre in genres:
|
| 508 |
+
if not genre.get("genre_name") or genre.get("genre_name") == "不明":
|
| 509 |
+
genre["genre_name"] = genre_names.get(genre.get("genre_id", ""), genre.get("genre_id", ""))
|
| 510 |
+
|
| 511 |
+
# total_attemptedフィールドを追加(フロントエンド互換)
|
| 512 |
+
if "total_attempted" not in subj:
|
| 513 |
+
subj["total_attempted"] = subj.get("total_questions", 0)
|
| 514 |
+
|
| 515 |
+
subjects_list.append(subj)
|
| 516 |
+
|
| 517 |
+
# 累積統計を構築(GASからのデータを使用)
|
| 518 |
+
cumulative = {
|
| 519 |
+
"total_sessions": gas_cumulative.get("total_sessions", 0),
|
| 520 |
+
"total_questions": gas_cumulative.get("total_questions", 0),
|
| 521 |
+
"overall_accuracy": gas_cumulative.get("overall_accuracy", 0.0)
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
transformed_result = {
|
| 525 |
+
"success": True,
|
| 526 |
+
"data": {
|
| 527 |
+
"cumulative": cumulative,
|
| 528 |
+
"subjects": subjects_list
|
| 529 |
+
}
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
logger.info(f"get_statistics: Returning cumulative={cumulative}")
|
| 533 |
+
return transformed_result
|
| 534 |
+
else:
|
| 535 |
+
# GASがエラーを返した場合はそのまま返す
|
| 536 |
+
return result
|
| 537 |
+
|
| 538 |
+
except Exception as e:
|
| 539 |
+
logger.error(f"get_statistics error: {e}")
|
| 540 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 541 |
+
|
| 542 |
+
|
| 543 |
+
@app.post("/api/get_evaluation")
|
| 544 |
+
async def get_evaluation(request: GetEvaluationRequest):
|
| 545 |
+
"""評価生成 - Gemini API直接呼び出し(一括評価対応)
|
| 546 |
+
|
| 547 |
+
v1.4.0: 要約ステータス・クイズ内容表示対応
|
| 548 |
+
"""
|
| 549 |
+
try:
|
| 550 |
+
# セッションの統計・結果を取得
|
| 551 |
+
logger.info(f"get_evaluation: Fetching session results for {request.session_id}")
|
| 552 |
+
session_data = await gas_client.get_session_results(request.session_id)
|
| 553 |
+
logger.info(f"get_evaluation: Session data received: {session_data.get('success', 'no success key')}")
|
| 554 |
+
|
| 555 |
+
# v1.4.0: 要約ステータスチェック
|
| 556 |
+
summary_status = None
|
| 557 |
+
quiz_summary = None
|
| 558 |
+
try:
|
| 559 |
+
summary_result = await gas_client.check_summary_status(request.session_id)
|
| 560 |
+
if summary_result.get("success") or summary_result.get("completed"):
|
| 561 |
+
summary_status = summary_result
|
| 562 |
+
# 要約テキストを構築(今回のクイズ内容)
|
| 563 |
+
summaries = summary_result.get("summaries", [])
|
| 564 |
+
if summaries:
|
| 565 |
+
quiz_summary = "、".join([s.get("summary", "") for s in summaries if s.get("summary")])
|
| 566 |
+
logger.info(f"get_evaluation: Summary status - completed: {summary_result.get('completed')}, count: {summary_result.get('count')}")
|
| 567 |
+
except Exception as e:
|
| 568 |
+
logger.warning(f"get_evaluation: Failed to get summary status (non-fatal): {e}")
|
| 569 |
+
|
| 570 |
+
# セッションデータの検証
|
| 571 |
+
if not session_data.get('success') and not session_data.get('data'):
|
| 572 |
+
logger.warning(f"get_evaluation: No valid session data for {request.session_id}")
|
| 573 |
+
return {
|
| 574 |
+
"success": False,
|
| 575 |
+
"error": "セッションデータが見つかりません"
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
# GASからのデータを取得
|
| 579 |
+
raw_data = session_data.get('data', session_data)
|
| 580 |
+
|
| 581 |
+
# resultsをresults_by_subject形式に変換
|
| 582 |
+
results = raw_data.get('results', [])
|
| 583 |
+
results_by_subject = {}
|
| 584 |
+
for r in results:
|
| 585 |
+
subject = r.get('subject', '')
|
| 586 |
+
if subject:
|
| 587 |
+
if subject not in results_by_subject:
|
| 588 |
+
results_by_subject[subject] = []
|
| 589 |
+
results_by_subject[subject].append(r)
|
| 590 |
+
|
| 591 |
+
# 変換したデータ構造を作成
|
| 592 |
+
transformed_data = {
|
| 593 |
+
'session_id': raw_data.get('session_id'),
|
| 594 |
+
'results': results,
|
| 595 |
+
'results_by_subject': results_by_subject,
|
| 596 |
+
'summary': raw_data.get('summary', {}),
|
| 597 |
+
'statistics': {} # 必要に応じて統計データも追加
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
logger.info(f"get_evaluation: Transformed data - subjects: {list(results_by_subject.keys())}, total_results: {len(results)}")
|
| 601 |
+
|
| 602 |
+
# Gemini APIで評価生成(一括評価)
|
| 603 |
+
logger.info(f"get_evaluation: Generating batch evaluation via Gemini")
|
| 604 |
+
batch_result = await gemini_service.generate_evaluation_batch(
|
| 605 |
+
session_data=transformed_data
|
| 606 |
+
)
|
| 607 |
+
|
| 608 |
+
# 結果をフロントエンド期待形式に変換
|
| 609 |
+
subject_evaluations = batch_result.get("subject_evaluations", {})
|
| 610 |
+
overall_evaluation = batch_result.get("overall_evaluation")
|
| 611 |
+
|
| 612 |
+
# evaluations配列を構築(フロントエンド互換)
|
| 613 |
+
evaluations = []
|
| 614 |
+
for subject, eval_data in subject_evaluations.items():
|
| 615 |
+
evaluations.append({
|
| 616 |
+
"subject": subject,
|
| 617 |
+
**eval_data
|
| 618 |
+
})
|
| 619 |
+
|
| 620 |
+
# 全体評価があれば追加
|
| 621 |
+
if overall_evaluation:
|
| 622 |
+
evaluations.append({
|
| 623 |
+
"subject": "overall",
|
| 624 |
+
**overall_evaluation
|
| 625 |
+
})
|
| 626 |
+
|
| 627 |
+
logger.info(f"get_evaluation: Generated {len(evaluations)} evaluations (including overall)")
|
| 628 |
+
|
| 629 |
+
# GASに評価を保存(エラーがあってもフロントエンドには成功を返す)
|
| 630 |
+
try:
|
| 631 |
+
save_result = await gas_client.save_evaluations(
|
| 632 |
+
session_id=request.session_id,
|
| 633 |
+
evaluations=evaluations
|
| 634 |
+
)
|
| 635 |
+
logger.info(f"get_evaluation: Save result: {save_result.get('success', 'unknown')}")
|
| 636 |
+
except Exception as save_error:
|
| 637 |
+
logger.error(f"get_evaluation: Failed to save evaluations: {save_error}")
|
| 638 |
+
# 保存に失敗しても、生成した評価は返す
|
| 639 |
+
|
| 640 |
+
# v1.4.0: 要約情報を含めたレスポンス構築
|
| 641 |
+
response_data = {
|
| 642 |
+
"session_id": request.session_id,
|
| 643 |
+
"evaluations": evaluations
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
if summary_status:
|
| 647 |
+
response_data["summary_status"] = {
|
| 648 |
+
"completed": summary_status.get("completed", False),
|
| 649 |
+
"count": summary_status.get("count", 0)
|
| 650 |
+
}
|
| 651 |
+
if quiz_summary:
|
| 652 |
+
response_data["quiz_summary"] = quiz_summary
|
| 653 |
+
|
| 654 |
+
return {
|
| 655 |
+
"success": True,
|
| 656 |
+
"data": response_data
|
| 657 |
+
}
|
| 658 |
+
except Exception as e:
|
| 659 |
+
logger.error(f"get_evaluation error: {e}")
|
| 660 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 661 |
+
|
| 662 |
+
|
| 663 |
+
# =============================================================================
|
| 664 |
+
# 静的ファイル配信
|
| 665 |
+
# =============================================================================
|
| 666 |
+
|
| 667 |
+
# 静的ファイル(CSS, JS)
|
| 668 |
+
app.mount("/css", StaticFiles(directory="static/css"), name="css")
|
| 669 |
+
app.mount("/js", StaticFiles(directory="static/js"), name="js")
|
| 670 |
+
|
| 671 |
+
@app.get("/")
|
| 672 |
+
async def root():
|
| 673 |
+
"""メインページ"""
|
| 674 |
+
return FileResponse("static/index.html")
|
| 675 |
+
|
| 676 |
+
|
| 677 |
+
# =============================================================================
|
| 678 |
+
# エントリーポイント
|
| 679 |
+
# =============================================================================
|
| 680 |
+
|
| 681 |
+
if __name__ == "__main__":
|
| 682 |
+
import uvicorn
|
| 683 |
+
port = int(os.environ.get("PORT", 7860))
|
| 684 |
+
uvicorn.run(
|
| 685 |
+
app,
|
| 686 |
+
host="0.0.0.0",
|
| 687 |
+
port=port,
|
| 688 |
+
timeout_keep_alive=300 # タイムアウト300秒に延長
|
| 689 |
+
)
|
V1.7.1/docs/FUTURE_VISION.md
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 超天才クイズ - 今後の構想
|
| 2 |
+
|
| 3 |
+
> このドキュメントは他のLLMとの壁打ち用です。
|
| 4 |
+
> 自由にアイデアを膨らませ、洗練させてください。
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## 現状のシステム概要
|
| 9 |
+
|
| 10 |
+
### プロダクト名
|
| 11 |
+
**超天才クイズ v1.6.15 (STABLE)**
|
| 12 |
+
|
| 13 |
+
### 概要
|
| 14 |
+
中学受験対策の4択クイズアプリ。AIが問題を生成し、学習者の理解度を分析してアドバイスを提供する。
|
| 15 |
+
|
| 16 |
+
### 技術スタック
|
| 17 |
+
- **フロントエンド**: HTML/CSS/JavaScript(静的ファイル)
|
| 18 |
+
- **バックエンド**: FastAPI (Python)
|
| 19 |
+
- **AI**: Gemini API(問題生成・評価生成)
|
| 20 |
+
- **データ永続化**: Google Apps Script + Google Spreadsheet
|
| 21 |
+
- **ホスティング**: Hugging Face Spaces (Docker)
|
| 22 |
+
|
| 23 |
+
### 現在の機能
|
| 24 |
+
- ユーザーログイン(簡易的、ユーザー名のみ)
|
| 25 |
+
- 4教科対応(国語・算数・理科・社会)
|
| 26 |
+
- QuestionDatabase(問題素材DB)からの問題生成
|
| 27 |
+
- 4択クイズ出題・正誤判定
|
| 28 |
+
- AI先生によるアドバイス(Gemini生成)
|
| 29 |
+
- 累積成績・詳細分析
|
| 30 |
+
- 均等出題(USAGE_COUNTで同じ問題の繰り返しを防止)
|
| 31 |
+
|
| 32 |
+
### 対象ユーザー
|
| 33 |
+
- 中学受験を目指す小学生(小4〜小6)
|
| 34 |
+
- その保護者
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
## 今後の構想
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## 【A】超天才クイズ 本体の改善
|
| 43 |
+
|
| 44 |
+
> 現在のシステム(v1.6.15)への機能追加・改善
|
| 45 |
+
|
| 46 |
+
### A-1. ユーザー単位での管理
|
| 47 |
+
|
| 48 |
+
**現状の課題**:
|
| 49 |
+
- 現在は全ユーザー共通でUSAGE_COUNTを管理
|
| 50 |
+
- 成績も厳密なユーザー分離ができていない
|
| 51 |
+
|
| 52 |
+
**やりたいこと**:
|
| 53 |
+
- 成績をユーザーごとに分離・管理
|
| 54 |
+
- USAGE_COUNTもユーザー単位で管理
|
| 55 |
+
- 同じ問題でも別ユーザーには出題される
|
| 56 |
+
- 各ユーザーが全問題を公平に体験できる
|
| 57 |
+
- ユーザー別の苦手分野分析
|
| 58 |
+
|
| 59 |
+
### A-2. 制限時間の設定
|
| 60 |
+
|
| 61 |
+
**目的**:
|
| 62 |
+
- 本番の試験を意識した緊張感
|
| 63 |
+
- ダラダラ解くのを防止
|
| 64 |
+
- 回答スピードの記録・分析
|
| 65 |
+
|
| 66 |
+
**やりたいこと**:
|
| 67 |
+
- 1科目あたり5分の制限時間
|
| 68 |
+
- カウントダウンタイマーを常に表示
|
| 69 |
+
- 時間切れ時の挙動:
|
| 70 |
+
- 強制終了
|
| 71 |
+
- 未回答問題は不正解扱い
|
| 72 |
+
- 自動的に評価画面へ遷移
|
| 73 |
+
- 各問題の回答時間をシートに記録
|
| 74 |
+
- 「この問題に何秒かかったか」を分析に活用
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
## 【B】派生サービス:問題配信サービス(仮称)
|
| 79 |
+
|
| 80 |
+
> 超天才クイズをベースにした**別サービス**として展開
|
| 81 |
+
> ランキング・クラス制・定期配信などソーシャル要素を持つ
|
| 82 |
+
|
| 83 |
+
### B-1. サービス概要
|
| 84 |
+
|
| 85 |
+
**コンセプト**:
|
| 86 |
+
- 超天才クイズの問題生成エンジンを活用
|
| 87 |
+
- ソーシャルゲーム的なレート制で継続的な学習モチベーションを維持
|
| 88 |
+
- 登録ユーザー向けの定期問題配信サービス
|
| 89 |
+
|
| 90 |
+
---
|
| 91 |
+
|
| 92 |
+
### B-2. 問題配信機能【確定】
|
| 93 |
+
|
| 94 |
+
**配信スケジュール**:
|
| 95 |
+
- **デイリー配信**(毎日)
|
| 96 |
+
- 朝(登校前):2教科
|
| 97 |
+
- 夕方(下校前):2教科
|
| 98 |
+
- 1日合計4教科
|
| 99 |
+
|
| 100 |
+
**出題形式**:
|
| 101 |
+
- 1教科あたり10問
|
| 102 |
+
- 制限時間:1教科5分(300秒)
|
| 103 |
+
|
| 104 |
+
**回答期限**:
|
| 105 |
+
- 当日23:59まで
|
| 106 |
+
|
| 107 |
+
---
|
| 108 |
+
|
| 109 |
+
### B-3. ポイント計算【確定】
|
| 110 |
+
|
| 111 |
+
**計算式**:
|
| 112 |
+
```
|
| 113 |
+
正答ポイント = 正解数 × 100
|
| 114 |
+
時間ボーナス = 残り秒数 × 正答率
|
| 115 |
+
合計ポイント = 正答ポイント + 時間ボーナス
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
**設計意図**:
|
| 119 |
+
- 正答率が時間ボーナスに係数として掛かることで、適当に連打するハックを防止
|
| 120 |
+
- 正確さと速さの両方を評価
|
| 121 |
+
- 中学受験本番を意識した設計
|
| 122 |
+
|
| 123 |
+
**得点シミュレーション**:
|
| 124 |
+
|
| 125 |
+
| ケース | 正答P | 時間B | 合計 |
|
| 126 |
+
|--------|-------|-------|------|
|
| 127 |
+
| 全問正解・残り180秒 | 1000 | 180 | **1180点** |
|
| 128 |
+
| 全問正解・残り60秒 | 1000 | 60 | **1060点** |
|
| 129 |
+
| 8問正解・残り120秒 | 800 | 96 | **896点** |
|
| 130 |
+
| 5問正解・残り200秒 | 500 | 100 | **600点** |
|
| 131 |
+
| 連打3問・残り270秒 | 300 | 81 | **381点** |
|
| 132 |
+
|
| 133 |
+
理論上の最高点:約1300点/教科(全問正解+残り300秒)
|
| 134 |
+
現実的な上限:1100〜1200点/教科
|
| 135 |
+
|
| 136 |
+
---
|
| 137 |
+
|
| 138 |
+
### B-4. ランキング機能【確定】
|
| 139 |
+
|
| 140 |
+
**ランキング種類**(5種類):
|
| 141 |
+
- 国語
|
| 142 |
+
- 算数
|
| 143 |
+
- 理科
|
| 144 |
+
- 社会
|
| 145 |
+
- **総合**(4教科合算)
|
| 146 |
+
|
| 147 |
+
**表示項目**:
|
| 148 |
+
- 正答率
|
| 149 |
+
- 回答時間
|
| 150 |
+
- 獲得ポイント
|
| 151 |
+
|
| 152 |
+
**ランキング表示期間**:
|
| 153 |
+
- 当日分のテスト結果は、翌日まる1日ランキングとして表示
|
| 154 |
+
- 例:12/21のテスト → 12/22の1日間ランキング表示
|
| 155 |
+
|
| 156 |
+
---
|
| 157 |
+
|
| 158 |
+
### B-5. クラス制【確定】
|
| 159 |
+
|
| 160 |
+
**クラス構成**(6段階):
|
| 161 |
+
|
| 162 |
+
| クラス | 名称 | 難易度 | 対応レベル |
|
| 163 |
+
|--------|------|--------|------------|
|
| 164 |
+
| 6 | **超天才** | 難 | 最難関校受験 |
|
| 165 |
+
| 5 | **天才** | 難 | 難関校上位 |
|
| 166 |
+
| 4 | **もうすぐ天才** | 中 | 難関校受験 |
|
| 167 |
+
| 3 | **天才かも** | 中 | 中堅校受験 |
|
| 168 |
+
| 2 | **天才の見習い** | 易 | 基礎固め |
|
| 169 |
+
| 1 | **天才のたまご** | 易 | 学校授業サポート |
|
| 170 |
+
|
| 171 |
+
**成長ストーリー**:
|
| 172 |
+
```
|
| 173 |
+
たまご → 見習い → かも? → もうすぐ! → 天才! → 超天才!!
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
**問題プール**:
|
| 177 |
+
- 難易度別に3セット(易・中・難)で運用
|
| 178 |
+
- クラス1-2:易
|
| 179 |
+
- クラス3-4:中
|
| 180 |
+
- クラス5-6:難
|
| 181 |
+
|
| 182 |
+
**将来の拡張**:
|
| 183 |
+
- ユーザー数が増えたら、正規分布の中央値付近のクラスを増設
|
| 184 |
+
|
| 185 |
+
---
|
| 186 |
+
|
| 187 |
+
### B-6. クラス判定方式【確定】
|
| 188 |
+
|
| 189 |
+
**判定タイミング**:
|
| 190 |
+
- 毎日判定(リアルタイムでクラスが変動)
|
| 191 |
+
|
| 192 |
+
**判定方式**:
|
| 193 |
+
- **過去7日間の加重移動平均**
|
| 194 |
+
- 開始7日未満のユーザーは、経過日数で割った平均
|
| 195 |
+
|
| 196 |
+
**加重の重み**(緩やか設定):
|
| 197 |
+
|
| 198 |
+
| 日 | 重み |
|
| 199 |
+
|----|------|
|
| 200 |
+
| 今日 | ×1.5 |
|
| 201 |
+
| 1日前 | ×1.4 |
|
| 202 |
+
| 2日前 | ×1.3 |
|
| 203 |
+
| 3日前 | ×1.2 |
|
| 204 |
+
| 4日前 | ×1.1 |
|
| 205 |
+
| 5日前 | ×1.0 |
|
| 206 |
+
| 6日前 | ×0.9 |
|
| 207 |
+
|
| 208 |
+
**計算式**:
|
| 209 |
+
```
|
| 210 |
+
加重平均 = (今日の点数×1.5 + 1日前×1.4 + ... + 6日前×0.9) ÷ 7
|
| 211 |
+
```
|
| 212 |
+
※ 参加しなかった日は0点としてカウント(継続参加のインセンティブ)
|
| 213 |
+
|
| 214 |
+
**クラス閾値**(加重平均点):
|
| 215 |
+
|
| 216 |
+
| クラス | 加重平均点 |
|
| 217 |
+
|--------|------------|
|
| 218 |
+
| 超天才 | 3572点以上 |
|
| 219 |
+
| 天才 | 2858〜3571点 |
|
| 220 |
+
| もうすぐ天才 | 2143〜2857点 |
|
| 221 |
+
| 天才かも | 1429〜2142点 |
|
| 222 |
+
| 天才の見習い | 715〜1428点 |
|
| 223 |
+
| 天才のたまご | 0〜714点 |
|
| 224 |
+
|
| 225 |
+
**設計意図**:
|
| 226 |
+
- 毎日「今日頑張ればクラス上がるかも」という緊張感
|
| 227 |
+
- 新規ユーザーは少ない日数で割るため、初日から上位クラス体験できる可能性
|
| 228 |
+
- サボると徐々に落ちる(急降下ではない)
|
| 229 |
+
- 継続参加しないとクラス維持できない
|
| 230 |
+
|
| 231 |
+
---
|
| 232 |
+
|
| 233 |
+
### B-7. 週間アイテム(鉛筆シリーズ)【確定】
|
| 234 |
+
|
| 235 |
+
**コンセプト**:
|
| 236 |
+
- 週間ポイントに応じてアイテム(鉛筆)を付与
|
| 237 |
+
- アイテム所持中は毎日ポイントボーナス
|
| 238 |
+
- 有効期間は1週間
|
| 239 |
+
- 控えめなボーナスで逆転可能な設計
|
| 240 |
+
|
| 241 |
+
**アイテム一覧**:
|
| 242 |
+
|
| 243 |
+
| アイテム | 週間ポイント閾値 | ボーナス | 有効期間 |
|
| 244 |
+
|----------|------------------|----------|----------|
|
| 245 |
+
| 金の鉛筆 | 25000点以上 | +100点/日 | 1週間 |
|
| 246 |
+
| 銀の鉛筆 | 20000点以上 | +50点/日 | 1週間 |
|
| 247 |
+
| 銅の鉛筆 | 15000点以上 | +25点/日 | 1週間 |
|
| 248 |
+
|
| 249 |
+
**週間ボーナス影響**:
|
| 250 |
+
|
| 251 |
+
| アイテム | 週間合計ボーナス | 週間ポイントに対する影響度 |
|
| 252 |
+
|----------|------------------|----------------------------|
|
| 253 |
+
| 金の鉛筆 | +700点/週 | 約2〜3% |
|
| 254 |
+
| 銀の鉛筆 | +350点/週 | 約1〜2% |
|
| 255 |
+
| 銅の鉛筆 | +175点/週 | 約0.5〜1% |
|
| 256 |
+
|
| 257 |
+
**設計意図**:
|
| 258 |
+
- 「ないよりマシ」程度の恩恵で、実力勝負を維持
|
| 259 |
+
- 強者がより強くなりすぎる問題を回避
|
| 260 |
+
- 週間で頑張るモチベーションになる
|
| 261 |
+
- 勉強道具(鉛筆)で教育サービスとしての統一感
|
| 262 |
+
|
| 263 |
+
**その他の表彰**:
|
| 264 |
+
- ゲーミフィケーションは控えめに(過度な演出は避ける)
|
| 265 |
+
- 成果に対する表彰は大事
|
| 266 |
+
- 昇格時の表彰
|
| 267 |
+
- 週間ランキング上位者の発表
|
| 268 |
+
- 連続正解記録など
|
| 269 |
+
|
| 270 |
+
---
|
| 271 |
+
|
| 272 |
+
### B-8. 保護者向けの配慮
|
| 273 |
+
|
| 274 |
+
**大前提**:
|
| 275 |
+
- **利用料金無料**を維持
|
| 276 |
+
- 保護者の理解・信頼を得ることが最重要
|
| 277 |
+
|
| 278 |
+
**スマホ利用時間と学習効率のバランス**:
|
| 279 |
+
- ダラダラ使わせない設計
|
| 280 |
+
- 配信回数が決まっているため自然と上限あり
|
| 281 |
+
- 短時間で効果的な学習
|
| 282 |
+
- 1教科5分で完結
|
| 283 |
+
- 隙間時間で取り組める
|
| 284 |
+
|
| 285 |
+
**保護者の理解を得る仕組み**:
|
| 286 |
+
- 学習レポートの共有
|
| 287 |
+
- 「今週は算数の割合が苦手でした」
|
| 288 |
+
- メールorLINE通知?
|
| 289 |
+
- 利用時間の可視化
|
| 290 |
+
- 「今週は合計45分学習しました」
|
| 291 |
+
- 保護者向けダッシュボード?
|
| 292 |
+
|
| 293 |
+
---
|
| 294 |
+
|
| 295 |
+
## 検討が必要なポイント
|
| 296 |
+
|
| 297 |
+
### ビジネスモデル
|
| 298 |
+
- 無料維持の場合、どう運営コストを賄うか?
|
| 299 |
+
- 広告?寄付?有料オプション?
|
| 300 |
+
|
| 301 |
+
### 技術的課題
|
| 302 |
+
- ユーザー認証の強化(現在は名前のみ)
|
| 303 |
+
- リアルタイムランキングのスケーラビリティ
|
| 304 |
+
- 問題数の拡充(現在は各カテゴリ数十問程度)
|
| 305 |
+
|
| 306 |
+
### 教育的配慮
|
| 307 |
+
- 競争を煽りすぎない設計
|
| 308 |
+
- 「負け」の演出をどうするか(降格時など)
|
| 309 |
+
- 学習の本質(理解)vs ゲーム的快感(勝利)のバランス
|
| 310 |
+
|
| 311 |
+
### 法的・倫理的配慮
|
| 312 |
+
- 小学生の個人情報保護
|
| 313 |
+
- 保護者の同意取得フロー
|
| 314 |
+
- 利用規約・プライバシーポリシー
|
| 315 |
+
|
| 316 |
+
---
|
| 317 |
+
|
| 318 |
+
## 未検討項目
|
| 319 |
+
|
| 320 |
+
- 予告→予習の具体的な仕組み
|
| 321 |
+
- 保護者通��の設計(手段・内容・頻度)
|
| 322 |
+
- ランキング画面のUI詳細
|
| 323 |
+
- 新規ユーザーの初期クラス設定方法
|
| 324 |
+
|
| 325 |
+
---
|
| 326 |
+
|
| 327 |
+
## 参考:現在のシステム構成図
|
| 328 |
+
|
| 329 |
+
```
|
| 330 |
+
[ユーザー]
|
| 331 |
+
↓ ブラウザ
|
| 332 |
+
[HuggingFace Spaces]
|
| 333 |
+
├── static/ (HTML/CSS/JS)
|
| 334 |
+
└── FastAPI (app.py)
|
| 335 |
+
↓ API呼び出し
|
| 336 |
+
[Gemini API] ← 問題生成・評価生成
|
| 337 |
+
↓
|
| 338 |
+
[Google Apps Script]
|
| 339 |
+
↓
|
| 340 |
+
[Google Spreadsheet]
|
| 341 |
+
├── Users
|
| 342 |
+
├── Sessions
|
| 343 |
+
├── QuestionDatabase
|
| 344 |
+
├── GeneratedQuestions
|
| 345 |
+
├── Answers
|
| 346 |
+
├── Evaluations
|
| 347 |
+
├── Statistics
|
| 348 |
+
└── Summaries
|
| 349 |
+
```
|
| 350 |
+
|
| 351 |
+
---
|
| 352 |
+
|
| 353 |
+
**作成日**: 2025-12-21
|
| 354 |
+
**最終更新**: 2025-12-21
|
| 355 |
+
**バージョン**: v1.6.15 STABLE ベース
|
| 356 |
+
**ステータス**: 派生サービス設計進行中
|
V1.7.1/docs/GEMINI_API_CONFIG.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gemini API リクエスト設定
|
| 2 |
+
|
| 3 |
+
> 超天才クイズ v3 における Gemini API 呼び出しの詳細設定
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 1. 基本設定
|
| 8 |
+
|
| 9 |
+
| 項目 | 値 |
|
| 10 |
+
|------|-----|
|
| 11 |
+
| **モデル** | `gemini-2.5-flash` |
|
| 12 |
+
| **API キー** | 環境変数 `GEMINI_API_KEY` |
|
| 13 |
+
| **SDK** | `google-generativeai` (Python) |
|
| 14 |
+
|
| 15 |
+
```python
|
| 16 |
+
import google.generativeai as genai
|
| 17 |
+
genai.configure(api_key=os.environ.get("GEMINI_API_KEY"))
|
| 18 |
+
self.model = genai.GenerativeModel("gemini-2.5-flash")
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## 2. 用途別パラメータ設定
|
| 24 |
+
|
| 25 |
+
### 2.1 問題生成 (`generate_questions`)
|
| 26 |
+
|
| 27 |
+
| パラメータ | 値 | 説明 |
|
| 28 |
+
|-----------|-----|------|
|
| 29 |
+
| `temperature` | **0.7** | 創造性高め(多様な問題生成) |
|
| 30 |
+
| `max_output_tokens` | **16384** | 5問分の出力を保証 |
|
| 31 |
+
| `response_mime_type` | `application/json` | JSON形式で直接取得 |
|
| 32 |
+
|
| 33 |
+
```python
|
| 34 |
+
generation_config=genai.GenerationConfig(
|
| 35 |
+
temperature=0.7,
|
| 36 |
+
max_output_tokens=16384,
|
| 37 |
+
response_mime_type="application/json"
|
| 38 |
+
)
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
### 2.2 教科別評価生成 (`generate_evaluation`)
|
| 42 |
+
|
| 43 |
+
| パラメータ | 値 | 説明 |
|
| 44 |
+
|-----------|-----|------|
|
| 45 |
+
| `temperature` | **0.5** | 一貫性重視(安定した評価) |
|
| 46 |
+
| `max_output_tokens` | **2048** | 1教科分の評価に十分 |
|
| 47 |
+
| `response_mime_type` | `application/json` | JSON形式で直接取得 |
|
| 48 |
+
|
| 49 |
+
```python
|
| 50 |
+
generation_config=genai.GenerationConfig(
|
| 51 |
+
temperature=0.5,
|
| 52 |
+
max_output_tokens=2048,
|
| 53 |
+
response_mime_type="application/json"
|
| 54 |
+
)
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
### 2.3 全体評価生成 (`_generate_overall_evaluation`)
|
| 58 |
+
|
| 59 |
+
| パラメータ | 値 | 説明 |
|
| 60 |
+
|-----------|-----|------|
|
| 61 |
+
| `temperature` | **0.5** | 一貫性重視 |
|
| 62 |
+
| `max_output_tokens` | **1024** | 全体サマリーに十分 |
|
| 63 |
+
| `response_mime_type` | `application/json` | JSON形式で直接取得 |
|
| 64 |
+
|
| 65 |
+
---
|
| 66 |
+
|
| 67 |
+
## 3. システムインストラクション(プロンプト)
|
| 68 |
+
|
| 69 |
+
### 3.1 問題生成プロンプト
|
| 70 |
+
|
| 71 |
+
**ロール定義**:
|
| 72 |
+
```
|
| 73 |
+
あなたは中学受験対策の{教科名}問題作成の専門家です。
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
**主要指示**:
|
| 77 |
+
- Knowledge Base(教材)に基づいた問題を生成
|
| 78 |
+
- 指定されたジャンルID(JP01, MA01等)を使用
|
| 79 |
+
- 難易度を「基本」「標準」「応用」でバランス
|
| 80 |
+
- 5問生成
|
| 81 |
+
|
| 82 |
+
**出力形式**:
|
| 83 |
+
```json
|
| 84 |
+
[
|
| 85 |
+
{
|
| 86 |
+
"category": "ジャンルID",
|
| 87 |
+
"difficulty": "標準",
|
| 88 |
+
"question": "問題文",
|
| 89 |
+
"choices": ["選択肢1", "選択肢2", "選択肢3", "選択肢4"],
|
| 90 |
+
"correct_answer": 0,
|
| 91 |
+
"explanation": "解説文"
|
| 92 |
+
}
|
| 93 |
+
]
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
**オプション追加情報**:
|
| 97 |
+
- ユーザー統計(正答率60%未満は基本中心、60%以上は標準・応用)
|
| 98 |
+
- 直近20問の履歴(重複回避用)
|
| 99 |
+
|
| 100 |
+
### 3.2 評価生成プロンプト
|
| 101 |
+
|
| 102 |
+
**ロール定義**:
|
| 103 |
+
```
|
| 104 |
+
あなたは中学受験の{教科名}指導の専門家です。
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
**入力データ**:
|
| 108 |
+
- 今回の結果(問題数、正解数、正答率)
|
| 109 |
+
- カテゴリ別結果
|
| 110 |
+
- 解答詳細(最大10問)
|
| 111 |
+
- 累積統計(オプション)
|
| 112 |
+
|
| 113 |
+
**出力形式**:
|
| 114 |
+
```json
|
| 115 |
+
{
|
| 116 |
+
"advice": "総合的なアドバイス(2-3文)",
|
| 117 |
+
"strengths": ["強み・得意分野1", "強み・得意分野2"],
|
| 118 |
+
"weaknesses": ["改善点・苦手分野1", "改善点・苦手分野2"],
|
| 119 |
+
"recommended_topics": ["おすすめの学習トピック1", "おすすめの学習トピック2"]
|
| 120 |
+
}
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
**注意事項**:
|
| 124 |
+
- 具体的で建設的なアドバイス
|
| 125 |
+
- 小学生に分かりやすい表現
|
| 126 |
+
- 励ましの言葉を含める
|
| 127 |
+
|
| 128 |
+
### 3.3 全体評価プロンプト
|
| 129 |
+
|
| 130 |
+
**入力**: 教科別評価のJSON
|
| 131 |
+
|
| 132 |
+
**出力形式**:
|
| 133 |
+
```json
|
| 134 |
+
{
|
| 135 |
+
"overall_advice": "全体的なアドバイス(2-3文)",
|
| 136 |
+
"strengths": ["強み1", "強み2"],
|
| 137 |
+
"weaknesses": ["改善点1", "改善点2"],
|
| 138 |
+
"next_steps": ["次のステップ1", "次のステップ2"]
|
| 139 |
+
}
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
---
|
| 143 |
+
|
| 144 |
+
## 4. ジャンルID一覧
|
| 145 |
+
|
| 146 |
+
### 国語 (jp)
|
| 147 |
+
| ID | 名称 |
|
| 148 |
+
|----|------|
|
| 149 |
+
| JP01 | 漢字・語彙 |
|
| 150 |
+
| JP02 | 文法・言葉のきまり |
|
| 151 |
+
| JP03 | 物語文読解 |
|
| 152 |
+
| JP04 | 説明文・論説文読解 |
|
| 153 |
+
| JP05 | 随筆文読解 |
|
| 154 |
+
| JP06 | 詩・韻文 |
|
| 155 |
+
| JP07 | 記述問題 |
|
| 156 |
+
| JP08 | 知識・文学史 |
|
| 157 |
+
|
| 158 |
+
### 算数 (math)
|
| 159 |
+
| ID | 名称 |
|
| 160 |
+
|----|------|
|
| 161 |
+
| MA01 | 計算 |
|
| 162 |
+
| MA02 | 数の性質 |
|
| 163 |
+
| MA03 | 割合・比 |
|
| 164 |
+
| MA04 | 速さ |
|
| 165 |
+
| MA05 | 文章題(その他) |
|
| 166 |
+
| MA06 | 平面図形 |
|
| 167 |
+
| MA07 | 立体図形 |
|
| 168 |
+
| MA08 | 場合の数・確率 |
|
| 169 |
+
| MA09 | グラフ・表 |
|
| 170 |
+
| MA10 | 特殊算 |
|
| 171 |
+
|
| 172 |
+
### 理科 (sci)
|
| 173 |
+
| ID | 名称 |
|
| 174 |
+
|----|------|
|
| 175 |
+
| SC01 | 力・運動 |
|
| 176 |
+
| SC02 | 電気 |
|
| 177 |
+
| SC03 | 光・音・熱 |
|
| 178 |
+
| SC04 | 物質の性質 |
|
| 179 |
+
| SC05 | 水溶液 |
|
| 180 |
+
| SC06 | 燃焼・化学変化 |
|
| 181 |
+
| SC07 | 植物 |
|
| 182 |
+
| SC08 | 動物 |
|
| 183 |
+
| SC09 | 人体 |
|
| 184 |
+
| SC10 | 天体 |
|
| 185 |
+
| SC11 | 気象 |
|
| 186 |
+
| SC12 | 地学 |
|
| 187 |
+
|
| 188 |
+
### 社会 (soc)
|
| 189 |
+
| ID | 名称 |
|
| 190 |
+
|----|------|
|
| 191 |
+
| SO01 | 日本地理(���土・自然) |
|
| 192 |
+
| SO02 | 日本地理(産業) |
|
| 193 |
+
| SO03 | 世界地理 |
|
| 194 |
+
| SO04 | 歴史(古代〜平安) |
|
| 195 |
+
| SO05 | 歴史(鎌倉〜室町) |
|
| 196 |
+
| SO06 | 歴史(安土桃山〜江戸) |
|
| 197 |
+
| SO07 | 歴史(明治〜現代) |
|
| 198 |
+
| SO08 | 公民(政治・憲法) |
|
| 199 |
+
| SO09 | 公民(経済・国際) |
|
| 200 |
+
| SO10 | 時事問題 |
|
| 201 |
+
|
| 202 |
+
---
|
| 203 |
+
|
| 204 |
+
## 5. 関連ファイル
|
| 205 |
+
|
| 206 |
+
| ファイル | 説明 |
|
| 207 |
+
|---------|------|
|
| 208 |
+
| `src/services/gemini_service.py` | Gemini API呼び出しメインロジック |
|
| 209 |
+
| `src/prompts/question_prompts.py` | 問題生成プロンプト定義 |
|
| 210 |
+
| `src/prompts/evaluation_prompts.py` | 評価生成プロンプト定義 |
|
| 211 |
+
| `src/knowledge/*.md` | 教科別Knowledge Base |
|
| 212 |
+
|
| 213 |
+
---
|
| 214 |
+
|
| 215 |
+
## 6. 設定変更時の注意
|
| 216 |
+
|
| 217 |
+
- **temperature**: 上げると多様性増加、下げると一貫性増加
|
| 218 |
+
- **max_output_tokens**: 出力が途切れる場合は増やす
|
| 219 |
+
- **モデル変更**: `gemini-2.5-flash` は高速・低コスト。精度重視なら `gemini-2.0-pro` 検討
|
| 220 |
+
|
| 221 |
+
---
|
| 222 |
+
|
| 223 |
+
**最終更新**: 2025-12-12
|
V1.7.1/docs/GEMINI_STRUCTURED_OUTPUT.txt
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<br />
|
| 2 |
+
|
| 3 |
+
You can configure Gemini models to generate responses that adhere to a provided JSON Schema. This capability guarantees predictable and parsable results, ensures format and type-safety, enables the programmatic detection of refusals, and simplifies prompting.
|
| 4 |
+
|
| 5 |
+
Using structured outputs is ideal for a wide range of applications:
|
| 6 |
+
|
| 7 |
+
- **Data extraction:**Pull specific information from unstructured text, like extracting names, dates, and amounts from an invoice.
|
| 8 |
+
- **Structured classification:**Classify text into predefined categories and assign structured labels, such as categorizing customer feedback by sentiment and topic.
|
| 9 |
+
- **Agentic workflows:**Generate structured data that can be used to call other tools or APIs, like creating a character sheet for a game or filling out a form.
|
| 10 |
+
|
| 11 |
+
In addition to supporting JSON Schema in the REST API, the Google GenAI SDKs for Python and JavaScript also make it easy to define object schemas using[Pydantic](https://docs.pydantic.dev/latest/)and[Zod](https://zod.dev/), respectively. The example below demonstrates how to extract information from unstructured text that conforms to a schema defined in code.
|
| 12 |
+
|
| 13 |
+
Recipe ExtractorContent ModerationRecursive Structures
|
| 14 |
+
|
| 15 |
+
This example demonstrates how to extract structured data from text using basic JSON Schema types like`object`,`array`,`string`, and`integer`.
|
| 16 |
+
|
| 17 |
+
### Python
|
| 18 |
+
|
| 19 |
+
from google import genai
|
| 20 |
+
from pydantic import BaseModel, Field
|
| 21 |
+
from typing import List, Optional
|
| 22 |
+
|
| 23 |
+
class Ingredient(BaseModel):
|
| 24 |
+
name: str = Field(description="Name of the ingredient.")
|
| 25 |
+
quantity: str = Field(description="Quantity of the ingredient, including units.")
|
| 26 |
+
|
| 27 |
+
class Recipe(BaseModel):
|
| 28 |
+
recipe_name: str = Field(description="The name of the recipe.")
|
| 29 |
+
prep_time_minutes: Optional[int] = Field(description="Optional time in minutes to prepare the recipe.")
|
| 30 |
+
ingredients: List[Ingredient]
|
| 31 |
+
instructions: List[str]
|
| 32 |
+
|
| 33 |
+
client = genai.Client()
|
| 34 |
+
|
| 35 |
+
prompt = """
|
| 36 |
+
Please extract the recipe from the following text.
|
| 37 |
+
The user wants to make delicious chocolate chip cookies.
|
| 38 |
+
They need 2 and 1/4 cups of all-purpose flour, 1 teaspoon of baking soda,
|
| 39 |
+
1 teaspoon of salt, 1 cup of unsalted butter (softened), 3/4 cup of granulated sugar,
|
| 40 |
+
3/4 cup of packed brown sugar, 1 teaspoon of vanilla extract, and 2 large eggs.
|
| 41 |
+
For the best part, they'll need 2 cups of semisweet chocolate chips.
|
| 42 |
+
First, preheat the oven to 375°F (190°C). Then, in a small bowl, whisk together the flour,
|
| 43 |
+
baking soda, and salt. In a large bowl, cream together the butter, granulated sugar, and brown sugar
|
| 44 |
+
until light and fluffy. Beat in the vanilla and eggs, one at a time. Gradually beat in the dry
|
| 45 |
+
ingredients until just combined. Finally, stir in the chocolate chips. Drop by rounded tablespoons
|
| 46 |
+
onto ungreased baking sheets and bake for 9 to 11 minutes.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
response = client.models.generate_content(
|
| 50 |
+
model="gemini-2.5-flash",
|
| 51 |
+
contents=prompt,
|
| 52 |
+
config={
|
| 53 |
+
"response_mime_type": "application/json",
|
| 54 |
+
"response_json_schema": Recipe.model_json_schema(),
|
| 55 |
+
},
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
recipe = Recipe.model_validate_json(response.text)
|
| 59 |
+
print(recipe)
|
| 60 |
+
|
| 61 |
+
### JavaScript
|
| 62 |
+
|
| 63 |
+
import { GoogleGenAI } from "@google/genai";
|
| 64 |
+
import { z } from "zod";
|
| 65 |
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
| 66 |
+
|
| 67 |
+
const ingredientSchema = z.object({
|
| 68 |
+
name: z.string().describe("Name of the ingredient."),
|
| 69 |
+
quantity: z.string().describe("Quantity of the ingredient, including units."),
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
const recipeSchema = z.object({
|
| 73 |
+
recipe_name: z.string().describe("The name of the recipe."),
|
| 74 |
+
prep_time_minutes: z.number().optional().describe("Optional time in minutes to prepare the recipe."),
|
| 75 |
+
ingredients: z.array(ingredientSchema),
|
| 76 |
+
instructions: z.array(z.string()),
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
const ai = new GoogleGenAI({});
|
| 80 |
+
|
| 81 |
+
const prompt = `
|
| 82 |
+
Please extract the recipe from the following text.
|
| 83 |
+
The user wants to make delicious chocolate chip cookies.
|
| 84 |
+
They need 2 and 1/4 cups of all-purpose flour, 1 teaspoon of baking soda,
|
| 85 |
+
1 teaspoon of salt, 1 cup of unsalted butter (softened), 3/4 cup of granulated sugar,
|
| 86 |
+
3/4 cup of packed brown sugar, 1 teaspoon of vanilla extract, and 2 large eggs.
|
| 87 |
+
For the best part, they'll need 2 cups of semisweet chocolate chips.
|
| 88 |
+
First, preheat the oven to 375°F (190°C). Then, in a small bowl, whisk together the flour,
|
| 89 |
+
baking soda, and salt. In a large bowl, cream together the butter, granulated sugar, and brown sugar
|
| 90 |
+
until light and fluffy. Beat in the vanilla and eggs, one at a time. Gradually beat in the dry
|
| 91 |
+
ingredients until just combined. Finally, stir in the chocolate chips. Drop by rounded tablespoons
|
| 92 |
+
onto ungreased baking sheets and bake for 9 to 11 minutes.
|
| 93 |
+
`;
|
| 94 |
+
|
| 95 |
+
const response = await ai.models.generateContent({
|
| 96 |
+
model: "gemini-2.5-flash",
|
| 97 |
+
contents: prompt,
|
| 98 |
+
config: {
|
| 99 |
+
responseMimeType: "application/json",
|
| 100 |
+
responseJsonSchema: zodToJsonSchema(recipeSchema),
|
| 101 |
+
},
|
| 102 |
+
});
|
| 103 |
+
|
| 104 |
+
const recipe = recipeSchema.parse(JSON.parse(response.text));
|
| 105 |
+
console.log(recipe);
|
| 106 |
+
|
| 107 |
+
### Go
|
| 108 |
+
|
| 109 |
+
package main
|
| 110 |
+
|
| 111 |
+
import (
|
| 112 |
+
"context"
|
| 113 |
+
"fmt"
|
| 114 |
+
"log"
|
| 115 |
+
|
| 116 |
+
"google.golang.org/genai"
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
func main() {
|
| 120 |
+
ctx := context.Background()
|
| 121 |
+
client, err := genai.NewClient(ctx, nil)
|
| 122 |
+
if err != nil {
|
| 123 |
+
log.Fatal(err)
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
prompt := `
|
| 127 |
+
Please extract the recipe from the following text.
|
| 128 |
+
The user wants to make delicious chocolate chip cookies.
|
| 129 |
+
They need 2 and 1/4 cups of all-purpose flour, 1 teaspoon of baking soda,
|
| 130 |
+
1 teaspoon of salt, 1 cup of unsalted butter (softened), 3/4 cup of granulated sugar,
|
| 131 |
+
3/4 cup of packed brown sugar, 1 teaspoon of vanilla extract, and 2 large eggs.
|
| 132 |
+
For the best part, they'll need 2 cups of semisweet chocolate chips.
|
| 133 |
+
First, preheat the oven to 375°F (190°C). Then, in a small bowl, whisk together the flour,
|
| 134 |
+
baking soda, and salt. In a large bowl, cream together the butter, granulated sugar, and brown sugar
|
| 135 |
+
until light and fluffy. Beat in the vanilla and eggs, one at a time. Gradually beat in the dry
|
| 136 |
+
ingredients until just combined. Finally, stir in the chocolate chips. Drop by rounded tablespoons
|
| 137 |
+
onto ungreased baking sheets and bake for 9 to 11 minutes.
|
| 138 |
+
`
|
| 139 |
+
config := &genai.GenerateContentConfig{
|
| 140 |
+
ResponseMIMEType: "application/json",
|
| 141 |
+
ResponseJsonSchema: map[string]any{
|
| 142 |
+
"type": "object",
|
| 143 |
+
"properties": map[string]any{
|
| 144 |
+
"recipe_name": map[string]any{
|
| 145 |
+
"type": "string",
|
| 146 |
+
"description": "The name of the recipe.",
|
| 147 |
+
},
|
| 148 |
+
"prep_time_minutes": map[string]any{
|
| 149 |
+
"type": "integer",
|
| 150 |
+
"description": "Optional time in minutes to prepare the recipe.",
|
| 151 |
+
},
|
| 152 |
+
"ingredients": map[string]any{
|
| 153 |
+
"type": "array",
|
| 154 |
+
"items": map[string]any{
|
| 155 |
+
"type": "object",
|
| 156 |
+
"properties": map[string]any{
|
| 157 |
+
"name": map[string]any{
|
| 158 |
+
"type": "string",
|
| 159 |
+
"description": "Name of the ingredient.",
|
| 160 |
+
},
|
| 161 |
+
"quantity": map[string]any{
|
| 162 |
+
"type": "string",
|
| 163 |
+
"description": "Quantity of the ingredient, including units.",
|
| 164 |
+
},
|
| 165 |
+
},
|
| 166 |
+
"required": []string{"name", "quantity"},
|
| 167 |
+
},
|
| 168 |
+
},
|
| 169 |
+
"instructions": map[string]any{
|
| 170 |
+
"type": "array",
|
| 171 |
+
"items": map[string]any{"type": "string"},
|
| 172 |
+
},
|
| 173 |
+
},
|
| 174 |
+
"required": []string{"recipe_name", "ingredients", "instructions"},
|
| 175 |
+
},
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
result, err := client.Models.GenerateContent(
|
| 179 |
+
ctx,
|
| 180 |
+
"gemini-2.5-flash",
|
| 181 |
+
genai.Text(prompt),
|
| 182 |
+
config,
|
| 183 |
+
)
|
| 184 |
+
if err != nil {
|
| 185 |
+
log.Fatal(err)
|
| 186 |
+
}
|
| 187 |
+
fmt.Println(result.Text())
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
### REST
|
| 191 |
+
|
| 192 |
+
curl "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent" \
|
| 193 |
+
-H "x-goog-api-key: $GEMINI_API_KEY" \
|
| 194 |
+
-H 'Content-Type: application/json' \
|
| 195 |
+
-X POST \
|
| 196 |
+
-d '{
|
| 197 |
+
"contents": [{
|
| 198 |
+
"parts":[
|
| 199 |
+
{ "text": "Please extract the recipe from the following text.\nThe user wants to make delicious chocolate chip cookies.\nThey need 2 and 1/4 cups of all-purpose flour, 1 teaspoon of baking soda,\n1 teaspoon of salt, 1 cup of unsalted butter (softened), 3/4 cup of granulated sugar,\n3/4 cup of packed brown sugar, 1 teaspoon of vanilla extract, and 2 large eggs.\nFor the best part, they will need 2 cups of semisweet chocolate chips.\nFirst, preheat the oven to 375°F (190°C). Then, in a small bowl, whisk together the flour,\nbaking soda, and salt. In a large bowl, cream together the butter, granulated sugar, and brown sugar\nuntil light and fluffy. Beat in the vanilla and eggs, one at a time. Gradually beat in the dry\ningredients until just combined. Finally, stir in the chocolate chips. Drop by rounded tablespoons\nonto ungreased baking sheets and bake for 9 to 11 minutes." }
|
| 200 |
+
]
|
| 201 |
+
}],
|
| 202 |
+
"generationConfig": {
|
| 203 |
+
"responseMimeType": "application/json",
|
| 204 |
+
"responseJsonSchema": {
|
| 205 |
+
"type": "object",
|
| 206 |
+
"properties": {
|
| 207 |
+
"recipe_name": {
|
| 208 |
+
"type": "string",
|
| 209 |
+
"description": "The name of the recipe."
|
| 210 |
+
},
|
| 211 |
+
"prep_time_minutes": {
|
| 212 |
+
"type": "integer",
|
| 213 |
+
"description": "Optional time in minutes to prepare the recipe."
|
| 214 |
+
},
|
| 215 |
+
"ingredients": {
|
| 216 |
+
"type": "array",
|
| 217 |
+
"items": {
|
| 218 |
+
"type": "object",
|
| 219 |
+
"properties": {
|
| 220 |
+
"name": { "type": "string", "description": "Name of the ingredient."},
|
| 221 |
+
"quantity": { "type": "string", "description": "Quantity of the ingredient, including units."}
|
| 222 |
+
},
|
| 223 |
+
"required": ["name", "quantity"]
|
| 224 |
+
}
|
| 225 |
+
},
|
| 226 |
+
"instructions": {
|
| 227 |
+
"type": "array",
|
| 228 |
+
"items": { "type": "string" }
|
| 229 |
+
}
|
| 230 |
+
},
|
| 231 |
+
"required": ["recipe_name", "ingredients", "instructions"]
|
| 232 |
+
}
|
| 233 |
+
}
|
| 234 |
+
}'
|
| 235 |
+
|
| 236 |
+
**Example Response:**
|
| 237 |
+
|
| 238 |
+
{
|
| 239 |
+
"recipe_name": "Delicious Chocolate Chip Cookies",
|
| 240 |
+
"ingredients": [
|
| 241 |
+
{
|
| 242 |
+
"name": "all-purpose flour",
|
| 243 |
+
"quantity": "2 and 1/4 cups"
|
| 244 |
+
},
|
| 245 |
+
{
|
| 246 |
+
"name": "baking soda",
|
| 247 |
+
"quantity": "1 teaspoon"
|
| 248 |
+
},
|
| 249 |
+
{
|
| 250 |
+
"name": "salt",
|
| 251 |
+
"quantity": "1 teaspoon"
|
| 252 |
+
},
|
| 253 |
+
{
|
| 254 |
+
"name": "unsalted butter (softened)",
|
| 255 |
+
"quantity": "1 cup"
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
"name": "granulated sugar",
|
| 259 |
+
"quantity": "3/4 cup"
|
| 260 |
+
},
|
| 261 |
+
{
|
| 262 |
+
"name": "packed brown sugar",
|
| 263 |
+
"quantity": "3/4 cup"
|
| 264 |
+
},
|
| 265 |
+
{
|
| 266 |
+
"name": "vanilla extract",
|
| 267 |
+
"quantity": "1 teaspoon"
|
| 268 |
+
},
|
| 269 |
+
{
|
| 270 |
+
"name": "large eggs",
|
| 271 |
+
"quantity": "2"
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"name": "semisweet chocolate chips",
|
| 275 |
+
"quantity": "2 cups"
|
| 276 |
+
}
|
| 277 |
+
],
|
| 278 |
+
"instructions": [
|
| 279 |
+
"Preheat the oven to 375°F (190°C).",
|
| 280 |
+
"In a small bowl, whisk together the flour, baking soda, and salt.",
|
| 281 |
+
"In a large bowl, cream together the butter, granulated sugar, and brown sugar until light and fluffy.",
|
| 282 |
+
"Beat in the vanilla and eggs, one at a time.",
|
| 283 |
+
"Gradually beat in the dry ingredients until just combined.",
|
| 284 |
+
"Stir in the chocolate chips.",
|
| 285 |
+
"Drop by rounded tablespoons onto ungreased baking sheets and bake for 9 to 11 minutes."
|
| 286 |
+
]
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
## Streaming
|
| 290 |
+
|
| 291 |
+
You can stream structured outputs, which allows you to start processing the response as it's being generated, without having to wait for the entire output to be complete. This can improve the perceived performance of your application.
|
| 292 |
+
|
| 293 |
+
The streamed chunks will be valid partial JSON strings, which can be concatenated to form the final, complete JSON object.
|
| 294 |
+
|
| 295 |
+
### Python
|
| 296 |
+
|
| 297 |
+
from google import genai
|
| 298 |
+
from pydantic import BaseModel, Field
|
| 299 |
+
from typing import Literal
|
| 300 |
+
|
| 301 |
+
class Feedback(BaseModel):
|
| 302 |
+
sentiment: Literal["positive", "neutral", "negative"]
|
| 303 |
+
summary: str
|
| 304 |
+
|
| 305 |
+
client = genai.Client()
|
| 306 |
+
prompt = "The new UI is incredibly intuitive and visually appealing. Great job. Add a very long summary to test streaming!"
|
| 307 |
+
|
| 308 |
+
response_stream = client.models.generate_content_stream(
|
| 309 |
+
model="gemini-2.5-flash",
|
| 310 |
+
contents=prompt,
|
| 311 |
+
config={
|
| 312 |
+
"response_mime_type": "application/json",
|
| 313 |
+
"response_json_schema": Feedback.model_json_schema(),
|
| 314 |
+
},
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
for chunk in response_stream:
|
| 318 |
+
print(chunk.candidates[0].content.parts[0].text)
|
| 319 |
+
|
| 320 |
+
### JavaScript
|
| 321 |
+
|
| 322 |
+
import { GoogleGenAI } from "@google/genai";
|
| 323 |
+
import { z } from "zod";
|
| 324 |
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
| 325 |
+
|
| 326 |
+
const ai = new GoogleGenAI({});
|
| 327 |
+
const prompt = "The new UI is incredibly intuitive and visually appealing. Great job! Add a very long summary to test streaming!";
|
| 328 |
+
|
| 329 |
+
const feedbackSchema = z.object({
|
| 330 |
+
sentiment: z.enum(["positive", "neutral", "negative"]),
|
| 331 |
+
summary: z.string(),
|
| 332 |
+
});
|
| 333 |
+
|
| 334 |
+
const stream = await ai.models.generateContentStream({
|
| 335 |
+
model: "gemini-2.5-flash",
|
| 336 |
+
contents: prompt,
|
| 337 |
+
config: {
|
| 338 |
+
responseMimeType: "application/json",
|
| 339 |
+
responseJsonSchema: zodToJsonSchema(feedbackSchema),
|
| 340 |
+
},
|
| 341 |
+
});
|
| 342 |
+
|
| 343 |
+
for await (const chunk of stream) {
|
| 344 |
+
console.log(chunk.candidates[0].content.parts[0].text)
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
## Structured outputs with tools
|
| 348 |
+
|
| 349 |
+
| **Preview:** This is a feature available only for the Gemini 3 series models,`gemini-3-pro-preview`and`gemini-3-flash-preview`.
|
| 350 |
+
|
| 351 |
+
Gemini 3 lets you combine Structured Outputs with built-in tools, including[Grounding with Google Search](https://ai.google.dev/gemini-api/docs/google-search),[URL Context](https://ai.google.dev/gemini-api/docs/url-context), and[Code Execution](https://ai.google.dev/gemini-api/docs/code-execution).
|
| 352 |
+
|
| 353 |
+
### Python
|
| 354 |
+
|
| 355 |
+
from google import genai
|
| 356 |
+
from pydantic import BaseModel, Field
|
| 357 |
+
from typing import List
|
| 358 |
+
|
| 359 |
+
class MatchResult(BaseModel):
|
| 360 |
+
winner: str = Field(description="The name of the winner.")
|
| 361 |
+
final_match_score: str = Field(description="The final match score.")
|
| 362 |
+
scorers: List[str] = Field(description="The name of the scorer.")
|
| 363 |
+
|
| 364 |
+
client = genai.Client()
|
| 365 |
+
|
| 366 |
+
response = client.models.generate_content(
|
| 367 |
+
model="gemini-3-pro-preview",
|
| 368 |
+
contents="Search for all details for the latest Euro.",
|
| 369 |
+
config={
|
| 370 |
+
"tools": [
|
| 371 |
+
{"google_search": {}},
|
| 372 |
+
{"url_context": {}}
|
| 373 |
+
],
|
| 374 |
+
"response_mime_type": "application/json",
|
| 375 |
+
"response_json_schema": MatchResult.model_json_schema(),
|
| 376 |
+
},
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
result = MatchResult.model_validate_json(response.text)
|
| 380 |
+
print(result)
|
| 381 |
+
|
| 382 |
+
### JavaScript
|
| 383 |
+
|
| 384 |
+
import { GoogleGenAI } from "@google/genai";
|
| 385 |
+
import { z } from "zod";
|
| 386 |
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
| 387 |
+
|
| 388 |
+
const ai = new GoogleGenAI({});
|
| 389 |
+
|
| 390 |
+
const matchSchema = z.object({
|
| 391 |
+
winner: z.string().describe("The name of the winner."),
|
| 392 |
+
final_match_score: z.string().describe("The final score."),
|
| 393 |
+
scorers: z.array(z.string()).describe("The name of the scorer.")
|
| 394 |
+
});
|
| 395 |
+
|
| 396 |
+
async function run() {
|
| 397 |
+
const response = await ai.models.generateContent({
|
| 398 |
+
model: "gemini-3-pro-preview",
|
| 399 |
+
contents: "Search for all details for the latest Euro.",
|
| 400 |
+
config: {
|
| 401 |
+
tools: [
|
| 402 |
+
{ googleSearch: {} },
|
| 403 |
+
{ urlContext: {} }
|
| 404 |
+
],
|
| 405 |
+
responseMimeType: "application/json",
|
| 406 |
+
responseJsonSchema: zodToJsonSchema(matchSchema),
|
| 407 |
+
},
|
| 408 |
+
});
|
| 409 |
+
|
| 410 |
+
const match = matchSchema.parse(JSON.parse(response.text));
|
| 411 |
+
console.log(match);
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
run();
|
| 415 |
+
|
| 416 |
+
### REST
|
| 417 |
+
|
| 418 |
+
curl "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-preview:generateContent" \
|
| 419 |
+
-H "x-goog-api-key: $GEMINI_API_KEY" \
|
| 420 |
+
-H 'Content-Type: application/json' \
|
| 421 |
+
-X POST \
|
| 422 |
+
-d '{
|
| 423 |
+
"contents": [{
|
| 424 |
+
"parts": [{"text": "Search for all details for the latest Euro."}]
|
| 425 |
+
}],
|
| 426 |
+
"tools": [
|
| 427 |
+
{"googleSearch": {}},
|
| 428 |
+
{"urlContext": {}}
|
| 429 |
+
],
|
| 430 |
+
"generationConfig": {
|
| 431 |
+
"responseMimeType": "application/json",
|
| 432 |
+
"responseJsonSchema": {
|
| 433 |
+
"type": "object",
|
| 434 |
+
"properties": {
|
| 435 |
+
"winner": {"type": "string", "description": "The name of the winner."},
|
| 436 |
+
"final_match_score": {"type": "string", "description": "The final score."},
|
| 437 |
+
"scorers": {
|
| 438 |
+
"type": "array",
|
| 439 |
+
"items": {"type": "string"},
|
| 440 |
+
"description": "The name of the scorer."
|
| 441 |
+
}
|
| 442 |
+
},
|
| 443 |
+
"required": ["winner", "final_match_score", "scorers"]
|
| 444 |
+
}
|
| 445 |
+
}
|
| 446 |
+
}'
|
| 447 |
+
|
| 448 |
+
## JSON schema support
|
| 449 |
+
|
| 450 |
+
To generate a JSON object, set the`response_mime_type`in the generation configuration to`application/json`and provide a`response_json_schema`. The schema must be a valid[JSON Schema](https://json-schema.org/)that describes the desired output format.
|
| 451 |
+
|
| 452 |
+
The model will then generate a response that is a syntactically valid JSON string matching the provided schema. When using structured outputs, the model will produce outputs in the same order as the keys in the schema.
|
| 453 |
+
|
| 454 |
+
Gemini's structured output mode supports a subset of the[JSON Schema](https://json-schema.org)specification.
|
| 455 |
+
|
| 456 |
+
The following values of`type`are supported:
|
| 457 |
+
|
| 458 |
+
- **`string`**: For text.
|
| 459 |
+
- **`number`**: For floating-point numbers.
|
| 460 |
+
- **`integer`**: For whole numbers.
|
| 461 |
+
- **`boolean`**: For true/false values.
|
| 462 |
+
- **`object`**: For structured data with key-value pairs.
|
| 463 |
+
- **`array`**: For lists of items.
|
| 464 |
+
- **`null`** : To allow a property to be null, include`"null"`in the type array (e.g.,`{"type": ["string", "null"]}`).
|
| 465 |
+
|
| 466 |
+
These descriptive properties help guide the model:
|
| 467 |
+
|
| 468 |
+
- **`title`**: A short description of a property.
|
| 469 |
+
- **`description`**: A longer and more detailed description of a property.
|
| 470 |
+
|
| 471 |
+
### Type-specific properties
|
| 472 |
+
|
| 473 |
+
**For`object`values:**
|
| 474 |
+
|
| 475 |
+
- **`properties`**: An object where each key is a property name and each value is a schema for that property.
|
| 476 |
+
- **`required`**: An array of strings, listing which properties are mandatory.
|
| 477 |
+
- **`additionalProperties`** : Controls whether properties not listed in`properties`are allowed. Can be a boolean or a schema.
|
| 478 |
+
|
| 479 |
+
**For`string`values:**
|
| 480 |
+
|
| 481 |
+
- **`enum`**: Lists a specific set of possible strings for classification tasks.
|
| 482 |
+
- **`format`** : Specifies a syntax for the string, such as`date-time`,`date`,`time`.
|
| 483 |
+
|
| 484 |
+
**For`number`and`integer`values:**
|
| 485 |
+
|
| 486 |
+
- **`enum`**: Lists a specific set of possible numeric values.
|
| 487 |
+
- **`minimum`**: The minimum inclusive value.
|
| 488 |
+
- **`maximum`**: The maximum inclusive value.
|
| 489 |
+
|
| 490 |
+
**For`array`values:**
|
| 491 |
+
|
| 492 |
+
- **`items`**: Defines the schema for all items in the array.
|
| 493 |
+
- **`prefixItems`**: Defines a list of schemas for the first N items, allowing for tuple-like structures.
|
| 494 |
+
- **`minItems`**: The minimum number of items in the array.
|
| 495 |
+
- **`maxItems`**: The maximum number of items in the array.
|
| 496 |
+
|
| 497 |
+
## Model support
|
| 498 |
+
|
| 499 |
+
The following models support structured output:
|
| 500 |
+
|
| 501 |
+
| Model | Structured Outputs |
|
| 502 |
+
|------------------------|--------------------|
|
| 503 |
+
| Gemini 3 Pro Preview | ✔️ |
|
| 504 |
+
| Gemini 3 Flash Preview | ✔️ |
|
| 505 |
+
| Gemini 2.5 Pro | ✔️ |
|
| 506 |
+
| Gemini 2.5 Flash | ✔️ |
|
| 507 |
+
| Gemini 2.5 Flash-Lite | ✔️ |
|
| 508 |
+
| Gemini 2.0 Flash | ✔️\* |
|
| 509 |
+
| Gemini 2.0 Flash-Lite | ✔️\* |
|
| 510 |
+
|
| 511 |
+
*\* Note that Gemini 2.0 requires an explicit`propertyOrdering`list within the JSON input to define the preferred structure. You can find an example in this[cookbook](https://github.com/google-gemini/cookbook/blob/main/examples/Pdf_structured_outputs_on_invoices_and_forms.ipynb).*
|
| 512 |
+
|
| 513 |
+
## Structured outputs vs. function calling
|
| 514 |
+
|
| 515 |
+
Both structured outputs and function calling use JSON schemas, but they serve different purposes:
|
| 516 |
+
|
| 517 |
+
| Feature | Primary Use Case |
|
| 518 |
+
|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
| 519 |
+
| **Structured Outputs** | **Formatting the final response to the user.** Use this when you want the model's*answer*to be in a specific format (e.g., extracting data from a document to save to a database). |
|
| 520 |
+
| **Function Calling** | **Taking action during the conversation.** Use this when the model needs to*ask you*to perform a task (e.g., "get current weather") before it can provide a final answer. |
|
| 521 |
+
|
| 522 |
+
## Best practices
|
| 523 |
+
|
| 524 |
+
- **Clear descriptions:** Use the`description`field in your schema to provide clear instructions to the model about what each property represents. This is crucial for guiding the model's output.
|
| 525 |
+
- **Strong typing:** Use specific types (`integer`,`string`,`enum`) whenever possible. If a parameter has a limited set of valid values, use an`enum`.
|
| 526 |
+
- **Prompt engineering:**Clearly state in your prompt what you want the model to do. For example, "Extract the following information from the text..." or "Classify this feedback according to the provided schema...".
|
| 527 |
+
- **Validation:**While structured output guarantees syntactically correct JSON, it does not guarantee the values are semantically correct. Always validate the final output in your application code before using it.
|
| 528 |
+
- **Error handling:**Implement robust error handling in your application to gracefully manage cases where the model's output, while schema-compliant, may not meet your business logic requirements.
|
| 529 |
+
|
| 530 |
+
## Limitations
|
| 531 |
+
|
| 532 |
+
- **Schema subset:**Not all features of the JSON Schema specification are supported. The model ignores unsupported properties.
|
| 533 |
+
- **Schema complexity:**The API may reject very large or deeply nested schemas. If you encounter errors, try simplifying your schema by shortening property names, reducing nesting, or limiting the number of constraints.
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
|
V1.7.1/docs/GenreMaster.js
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* ジャンルマスターデータ
|
| 3 |
+
*
|
| 4 |
+
* 中学受験4教科のジャンル分類を定義
|
| 5 |
+
*
|
| 6 |
+
* @version 1.0.0
|
| 7 |
+
* @date 2025-12-07
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
// ============================================================================
|
| 11 |
+
// ジャンルマスターデータ
|
| 12 |
+
// ============================================================================
|
| 13 |
+
|
| 14 |
+
/**
|
| 15 |
+
* 中学受験4教科のジャンル分類マスターデータ
|
| 16 |
+
* @constant {Object}
|
| 17 |
+
*/
|
| 18 |
+
var GENRE_MASTER = {
|
| 19 |
+
// 国語(8ジャンル)
|
| 20 |
+
jp: {
|
| 21 |
+
subject_id: 'jp',
|
| 22 |
+
subject_name: '国語',
|
| 23 |
+
genres: [
|
| 24 |
+
{
|
| 25 |
+
id: 'JP01',
|
| 26 |
+
name: '漢字・語彙',
|
| 27 |
+
description: '読み書き、四字熟語、慣用句、ことわざ'
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
id: 'JP02',
|
| 31 |
+
name: '文法・言葉のきまり',
|
| 32 |
+
description: '品詞、敬語、文の成分、修飾関係'
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
id: 'JP03',
|
| 36 |
+
name: '物語文読解',
|
| 37 |
+
description: '心情理解、場面把握、人物関係'
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
id: 'JP04',
|
| 41 |
+
name: '説明文・論説文読解',
|
| 42 |
+
description: '要旨、段落構成、筆者の主張'
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
id: 'JP05',
|
| 46 |
+
name: '随筆文読解',
|
| 47 |
+
description: '筆者の体験・感想の読み取り'
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
id: 'JP06',
|
| 51 |
+
name: '詩・韻文',
|
| 52 |
+
description: '詩、短歌、俳句、表現技法'
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
id: 'JP07',
|
| 56 |
+
name: '記述問題',
|
| 57 |
+
description: '理由説明、要約、意見記述'
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
id: 'JP08',
|
| 61 |
+
name: '知識・文学史',
|
| 62 |
+
description: '作家、作品名、文学的常識'
|
| 63 |
+
}
|
| 64 |
+
]
|
| 65 |
+
},
|
| 66 |
+
|
| 67 |
+
// 算数(10ジャンル)
|
| 68 |
+
math: {
|
| 69 |
+
subject_id: 'math',
|
| 70 |
+
subject_name: '算数',
|
| 71 |
+
genres: [
|
| 72 |
+
{
|
| 73 |
+
id: 'MA01',
|
| 74 |
+
name: '計算',
|
| 75 |
+
description: '四則演算、分数・小数、逆算'
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
id: 'MA02',
|
| 79 |
+
name: '数の性質',
|
| 80 |
+
description: '約数・倍数、素因数分解、規則性'
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
id: 'MA03',
|
| 84 |
+
name: '割合・比',
|
| 85 |
+
description: '割合、比、百分率、歩合'
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
id: 'MA04',
|
| 89 |
+
name: '速さ',
|
| 90 |
+
description: '旅人算、通過算、流水算、時計算'
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
id: 'MA05',
|
| 94 |
+
name: '文章題(その他)',
|
| 95 |
+
description: '濃度、仕事算、ニュートン算、差集め算'
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
id: 'MA06',
|
| 99 |
+
name: '平面図形',
|
| 100 |
+
description: '面積、角度、相似、合同'
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
id: 'MA07',
|
| 104 |
+
name: '立体図形',
|
| 105 |
+
description: '体積、表面積、展開図、切断'
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
id: 'MA08',
|
| 109 |
+
name: '場合の数・確率',
|
| 110 |
+
description: '順列、組み合わせ、確率'
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
id: 'MA09',
|
| 114 |
+
name: 'グラフ・表',
|
| 115 |
+
description: '統計、変化のグラフ、ダイヤグラム'
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
id: 'MA10',
|
| 119 |
+
name: '特殊算',
|
| 120 |
+
description: 'つるかめ算、消去算、過不足算'
|
| 121 |
+
}
|
| 122 |
+
]
|
| 123 |
+
},
|
| 124 |
+
|
| 125 |
+
// 理科(12ジャンル)
|
| 126 |
+
sci: {
|
| 127 |
+
subject_id: 'sci',
|
| 128 |
+
subject_name: '理科',
|
| 129 |
+
genres: [
|
| 130 |
+
{
|
| 131 |
+
id: 'SC01',
|
| 132 |
+
name: '力・運動',
|
| 133 |
+
description: 'てこ、滑車、ばね、浮力、振り子'
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
id: 'SC02',
|
| 137 |
+
name: '電気',
|
| 138 |
+
description: '回路、抵抗、電磁石、発熱'
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
id: 'SC03',
|
| 142 |
+
name: '光・音・熱',
|
| 143 |
+
description: '反射、屈折、レンズ、音の性質'
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
id: 'SC04',
|
| 147 |
+
name: '物質の性質',
|
| 148 |
+
description: '金属、気体、密度、状態変化'
|
| 149 |
+
},
|
| 150 |
+
{
|
| 151 |
+
id: 'SC05',
|
| 152 |
+
name: '水溶液',
|
| 153 |
+
description: '酸・アルカリ、中和、溶解度'
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
id: 'SC06',
|
| 157 |
+
name: '燃焼・化学変化',
|
| 158 |
+
description: '燃焼、酸化還元、化合'
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
id: 'SC07',
|
| 162 |
+
name: '植物',
|
| 163 |
+
description: 'つくり、光合成、蒸散、分類'
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
id: 'SC08',
|
| 167 |
+
name: '動物',
|
| 168 |
+
description: 'からだのつくり、行動、分類'
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
id: 'SC09',
|
| 172 |
+
name: '人体',
|
| 173 |
+
description: '消化、呼吸、血液循環、感覚器官'
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
id: 'SC10',
|
| 177 |
+
name: '天体',
|
| 178 |
+
description: '太陽、月、星座、地球の運動'
|
| 179 |
+
},
|
| 180 |
+
{
|
| 181 |
+
id: 'SC11',
|
| 182 |
+
name: '気象',
|
| 183 |
+
description: '天気、気温、湿度、雲、季節風'
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
id: 'SC12',
|
| 187 |
+
name: '地学',
|
| 188 |
+
description: '地層、岩石、火山、地震'
|
| 189 |
+
}
|
| 190 |
+
]
|
| 191 |
+
},
|
| 192 |
+
|
| 193 |
+
// 社会(10ジャンル)
|
| 194 |
+
soc: {
|
| 195 |
+
subject_id: 'soc',
|
| 196 |
+
subject_name: '社会',
|
| 197 |
+
genres: [
|
| 198 |
+
{
|
| 199 |
+
id: 'SO01',
|
| 200 |
+
name: '日本地理(国土・自然)',
|
| 201 |
+
description: '地形、気候、都道府県'
|
| 202 |
+
},
|
| 203 |
+
{
|
| 204 |
+
id: 'SO02',
|
| 205 |
+
name: '日本地理(産業)',
|
| 206 |
+
description: '農業、工業、水産業、商業'
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
id: 'SO03',
|
| 210 |
+
name: '世界地理',
|
| 211 |
+
description: '大陸、国、貿易、環境問題'
|
| 212 |
+
},
|
| 213 |
+
{
|
| 214 |
+
id: 'SO04',
|
| 215 |
+
name: '歴史(古代〜平安)',
|
| 216 |
+
description: '旧石器〜平安時代'
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
id: 'SO05',
|
| 220 |
+
name: '歴史(鎌倉〜室町)',
|
| 221 |
+
description: '武士の台頭、文化'
|
| 222 |
+
},
|
| 223 |
+
{
|
| 224 |
+
id: 'SO06',
|
| 225 |
+
name: '歴史(安土桃山〜江戸)',
|
| 226 |
+
description: '統一、鎖国、元禄・化政文化'
|
| 227 |
+
},
|
| 228 |
+
{
|
| 229 |
+
id: 'SO07',
|
| 230 |
+
name: '歴史(明治〜現代)',
|
| 231 |
+
description: '近代化、戦争、戦後'
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
id: 'SO08',
|
| 235 |
+
name: '公民(政治・憲法)',
|
| 236 |
+
description: '三権分立、選挙、人権'
|
| 237 |
+
},
|
| 238 |
+
{
|
| 239 |
+
id: 'SO09',
|
| 240 |
+
name: '公民(経済・国際)',
|
| 241 |
+
description: '経済の仕組み、国際機関、SDGs'
|
| 242 |
+
},
|
| 243 |
+
{
|
| 244 |
+
id: 'SO10',
|
| 245 |
+
name: '時事問題',
|
| 246 |
+
description: '直近1〜2年のニュース'
|
| 247 |
+
}
|
| 248 |
+
]
|
| 249 |
+
}
|
| 250 |
+
};
|
| 251 |
+
|
| 252 |
+
// ============================================================================
|
| 253 |
+
// ヘルパー関数
|
| 254 |
+
// ============================================================================
|
| 255 |
+
|
| 256 |
+
/**
|
| 257 |
+
* ジャンルIDからジャンル情報を取得
|
| 258 |
+
*
|
| 259 |
+
* @param {string} genreId - ジャンルID(例: 'JP01', 'MA05')
|
| 260 |
+
* @returns {Object|null} - ジャンル情報 { id, name, description, subject_id, subject_name } または null
|
| 261 |
+
*
|
| 262 |
+
* @example
|
| 263 |
+
* var genre = getGenreById('JP01');
|
| 264 |
+
* // => { id: 'JP01', name: '漢字・語彙', description: '...', subject_id: 'jp', subject_name: '国語' }
|
| 265 |
+
*/
|
| 266 |
+
function getGenreById(genreId) {
|
| 267 |
+
if (!genreId || typeof genreId !== 'string') {
|
| 268 |
+
return null;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
var upperGenreId = genreId.toUpperCase();
|
| 272 |
+
|
| 273 |
+
// 全教科を検索
|
| 274 |
+
var subjects = ['jp', 'math', 'sci', 'soc'];
|
| 275 |
+
for (var i = 0; i < subjects.length; i++) {
|
| 276 |
+
var subjectId = subjects[i];
|
| 277 |
+
var subjectData = GENRE_MASTER[subjectId];
|
| 278 |
+
var genres = subjectData.genres;
|
| 279 |
+
|
| 280 |
+
for (var j = 0; j < genres.length; j++) {
|
| 281 |
+
var genre = genres[j];
|
| 282 |
+
if (genre.id === upperGenreId) {
|
| 283 |
+
// ジャンル情報に教科情報を追加して返却
|
| 284 |
+
return {
|
| 285 |
+
id: genre.id,
|
| 286 |
+
name: genre.name,
|
| 287 |
+
description: genre.description,
|
| 288 |
+
subject_id: subjectData.subject_id,
|
| 289 |
+
subject_name: subjectData.subject_name
|
| 290 |
+
};
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
return null;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
/**
|
| 299 |
+
* 教科のジャンル一覧を取得
|
| 300 |
+
*
|
| 301 |
+
* @param {string} subjectId - 教科ID(jp, math, sci, soc)
|
| 302 |
+
* @returns {Array} - ジャンル配列(見つからない場合は空配列)
|
| 303 |
+
*
|
| 304 |
+
* @example
|
| 305 |
+
* var genres = getGenresBySubject('jp');
|
| 306 |
+
* // => [{ id: 'JP01', name: '漢字・語彙', ... }, ...]
|
| 307 |
+
*/
|
| 308 |
+
function getGenresBySubject(subjectId) {
|
| 309 |
+
if (!subjectId || typeof subjectId !== 'string') {
|
| 310 |
+
return [];
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
var subjectData = GENRE_MASTER[subjectId.toLowerCase()];
|
| 314 |
+
if (!subjectData) {
|
| 315 |
+
return [];
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
return subjectData.genres;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
/**
|
| 322 |
+
* 全ジャンル数を取得
|
| 323 |
+
*
|
| 324 |
+
* @returns {number} - 総ジャンル数(40)
|
| 325 |
+
*
|
| 326 |
+
* @example
|
| 327 |
+
* var total = getTotalGenreCount();
|
| 328 |
+
* // => 40
|
| 329 |
+
*/
|
| 330 |
+
function getTotalGenreCount() {
|
| 331 |
+
var count = 0;
|
| 332 |
+
var subjects = ['jp', 'math', 'sci', 'soc'];
|
| 333 |
+
|
| 334 |
+
for (var i = 0; i < subjects.length; i++) {
|
| 335 |
+
var subjectId = subjects[i];
|
| 336 |
+
count += GENRE_MASTER[subjectId].genres.length;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
return count;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
/**
|
| 343 |
+
* ジャンルIDの検証
|
| 344 |
+
*
|
| 345 |
+
* @param {string} genreId - ジャンルID
|
| 346 |
+
* @returns {boolean} - 有効なジャンルIDかどうか
|
| 347 |
+
*
|
| 348 |
+
* @example
|
| 349 |
+
* isValidGenreId('JP01'); // => true
|
| 350 |
+
* isValidGenreId('XX99'); // => false
|
| 351 |
+
*/
|
| 352 |
+
function isValidGenreId(genreId) {
|
| 353 |
+
return getGenreById(genreId) !== null;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
/**
|
| 357 |
+
* Dify用のジャンルリストを生成(問題生成プロンプト用)
|
| 358 |
+
*
|
| 359 |
+
* @param {string} subjectId - 教科ID(jp, math, sci, soc)
|
| 360 |
+
* @returns {string} - ジャンルリストの文字列(改行区切り)
|
| 361 |
+
*
|
| 362 |
+
* @example
|
| 363 |
+
* var list = getGenreListForDify('jp');
|
| 364 |
+
* // => "JP01: 漢字・語彙(読み書き、四字熟語、慣用句、ことわざ)\n..."
|
| 365 |
+
*/
|
| 366 |
+
function getGenreListForDify(subjectId) {
|
| 367 |
+
var genres = getGenresBySubject(subjectId);
|
| 368 |
+
|
| 369 |
+
if (genres.length === 0) {
|
| 370 |
+
return '';
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
var lines = [];
|
| 374 |
+
for (var i = 0; i < genres.length; i++) {
|
| 375 |
+
var genre = genres[i];
|
| 376 |
+
var line = genre.id + ': ' + genre.name + '(' + genre.description + ')';
|
| 377 |
+
lines.push(line);
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
return lines.join('\n');
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
// ============================================================================
|
| 384 |
+
// テスト関数(GASエディタで実行可能)
|
| 385 |
+
// ============================================================================
|
| 386 |
+
|
| 387 |
+
/**
|
| 388 |
+
* GenreMaster.jsのテスト関数
|
| 389 |
+
* GASエディタで実行して動作確認
|
| 390 |
+
*/
|
| 391 |
+
function testGenreMaster() {
|
| 392 |
+
Logger.log('=== GenreMaster.js テスト開始 ===');
|
| 393 |
+
|
| 394 |
+
// テスト1: ジャンル総数
|
| 395 |
+
var totalCount = getTotalGenreCount();
|
| 396 |
+
Logger.log('テスト1: 総ジャンル数 = ' + totalCount + ' (期待値: 40)');
|
| 397 |
+
|
| 398 |
+
// テスト2: ジャンルID検索
|
| 399 |
+
var genre1 = getGenreById('JP01');
|
| 400 |
+
Logger.log('テスト2: getGenreById("JP01") = ' + JSON.stringify(genre1));
|
| 401 |
+
|
| 402 |
+
var genre2 = getGenreById('MA05');
|
| 403 |
+
Logger.log('テスト2: getGenreById("MA05") = ' + JSON.stringify(genre2));
|
| 404 |
+
|
| 405 |
+
// テスト3: 教科別ジャンル取得
|
| 406 |
+
var jpGenres = getGenresBySubject('jp');
|
| 407 |
+
Logger.log('テスト3: 国語ジャンル数 = ' + jpGenres.length + ' (期待値: 8)');
|
| 408 |
+
|
| 409 |
+
var mathGenres = getGenresBySubject('math');
|
| 410 |
+
Logger.log('テスト3: 算数ジャンル数 = ' + mathGenres.length + ' (期待値: 10)');
|
| 411 |
+
|
| 412 |
+
var sciGenres = getGenresBySubject('sci');
|
| 413 |
+
Logger.log('テスト3: 理科ジャンル数 = ' + sciGenres.length + ' (期待値: 12)');
|
| 414 |
+
|
| 415 |
+
var socGenres = getGenresBySubject('soc');
|
| 416 |
+
Logger.log('テスト3: 社会ジャンル数 = ' + socGenres.length + ' (期待値: 10)');
|
| 417 |
+
|
| 418 |
+
// テスト4: ジャンルID検証
|
| 419 |
+
var valid1 = isValidGenreId('JP01');
|
| 420 |
+
Logger.log('テスト4: isValidGenreId("JP01") = ' + valid1 + ' (期待値: true)');
|
| 421 |
+
|
| 422 |
+
var valid2 = isValidGenreId('XX99');
|
| 423 |
+
Logger.log('テスト4: isValidGenreId("XX99") = ' + valid2 + ' (期待値: false)');
|
| 424 |
+
|
| 425 |
+
// テスト5: Dify用ジャンルリスト生成
|
| 426 |
+
var jpList = getGenreListForDify('jp');
|
| 427 |
+
Logger.log('テスト5: 国語ジャンルリスト(最初の100文字):');
|
| 428 |
+
Logger.log(jpList.substring(0, 100) + '...');
|
| 429 |
+
|
| 430 |
+
Logger.log('=== GenreMaster.js テスト完了 ===');
|
| 431 |
+
}
|
V1.7.1/docs/PLAN_v1.4.md
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PLAN: v1.4.0 出題バランス改善(LLM要約 + ジャンル優先方式)
|
| 2 |
+
|
| 3 |
+
## 目的
|
| 4 |
+
テストを繰り返しても、Knowledge Base内を網羅的に出題し、ジャンル・題材の偏りを解消する
|
| 5 |
+
|
| 6 |
+
## 問題点
|
| 7 |
+
|
| 8 |
+
### 1. ジャンルの偏り
|
| 9 |
+
- 国語8ジャンル中4ジャンルしか出題されない
|
| 10 |
+
- 未出題: JP05(随筆文)、JP06(詩・韻文)、JP07(記述問題)、JP08(文学史)
|
| 11 |
+
|
| 12 |
+
### 2. 問題内容の重複
|
| 13 |
+
- 「いたわる」など同じ題材が繰り返し出題
|
| 14 |
+
- Knowledge Baseの特定部分しか使われない
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 解決策: LLM要約 + ジャンル優先方式
|
| 19 |
+
|
| 20 |
+
### アーキテクチャ
|
| 21 |
+
```
|
| 22 |
+
[問題生成完了]
|
| 23 |
+
↓
|
| 24 |
+
[GAS] 問題保存 + Gemini要約リクエスト(非同期)
|
| 25 |
+
↓ ※解答中に並行処理
|
| 26 |
+
[GAS] 要約結果をQuestionSummariesシートに記録
|
| 27 |
+
↓
|
| 28 |
+
[評価生成時] HFから要約完了チェック → UI表示
|
| 29 |
+
↓
|
| 30 |
+
[詳細分析] 「今回のクイズ内容」として要約表示
|
| 31 |
+
↓
|
| 32 |
+
[次回問題生成] 要約を除外リスト + ジャンルカウントで優先度計算
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
### メリット
|
| 36 |
+
1. **プロンプト削減**: 問題文全文ではなく要約キーワードのみ
|
| 37 |
+
2. **非同期処理**: 解答中に要約完了、ユーザー待機時間なし
|
| 38 |
+
3. **UI向上**: 詳細分析に「今回のクイズ内容」を表示
|
| 39 |
+
4. **信頼性**: 要約完了ステータスを評価画面で確認可能
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
## 既存GAS構造
|
| 44 |
+
|
| 45 |
+
### 現在のシート
|
| 46 |
+
- `Users`: ユーザー情報
|
| 47 |
+
- `Sessions`: セッション管理
|
| 48 |
+
- `Questions`: 問題データ
|
| 49 |
+
- `Answers`: 解答履歴
|
| 50 |
+
- `Statistics`: カテゴリ別統計
|
| 51 |
+
- `Evaluations`: 評価データ
|
| 52 |
+
|
| 53 |
+
### 追加するシート
|
| 54 |
+
- `QuestionSummaries`: 問題要約データ(新規)
|
| 55 |
+
|
| 56 |
+
---
|
| 57 |
+
|
| 58 |
+
## SUBAGENT タスク定義
|
| 59 |
+
|
| 60 |
+
### T1: GAS - 要約生成・保存機能(SUBAGENT-A)
|
| 61 |
+
**対象ファイル**: `gas/Code.gs`
|
| 62 |
+
|
| 63 |
+
**実装内容**:
|
| 64 |
+
```javascript
|
| 65 |
+
// 新規シート
|
| 66 |
+
const SHEETS = {
|
| 67 |
+
...existing,
|
| 68 |
+
QUESTION_SUMMARIES: 'QuestionSummaries'
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
// 問題保存後に要約リクエスト発火
|
| 72 |
+
function saveQuestions(sessionId, subject, questions) {
|
| 73 |
+
// ...既存の保存処理...
|
| 74 |
+
|
| 75 |
+
// 非同期で要約生成をトリガー
|
| 76 |
+
triggerSummaryGeneration(sessionId, subject, questions);
|
| 77 |
+
|
| 78 |
+
return { questions: savedQuestions, count: savedQuestions.length };
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// 要約生成(Gemini API呼び出し)
|
| 82 |
+
function triggerSummaryGeneration(sessionId, subject, questions) {
|
| 83 |
+
const GEMINI_API_KEY = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
|
| 84 |
+
const prompt = buildSummaryPrompt(questions);
|
| 85 |
+
|
| 86 |
+
// Gemini API呼び出し
|
| 87 |
+
const response = callGeminiAPI(prompt, GEMINI_API_KEY);
|
| 88 |
+
|
| 89 |
+
// 結果をシートに保存
|
| 90 |
+
saveSummary(sessionId, subject, response);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// 要約プロンプト構築
|
| 94 |
+
function buildSummaryPrompt(questions) {
|
| 95 |
+
return `以下の問題群から、重複防止用のキーワードリストを抽出してください。
|
| 96 |
+
|
| 97 |
+
問題:
|
| 98 |
+
${questions.map(q => q.question).join('\n')}
|
| 99 |
+
|
| 100 |
+
出力形式(JSON):
|
| 101 |
+
{
|
| 102 |
+
"keywords": ["キーワード1", "キーワード2", ...],
|
| 103 |
+
"topics": ["トピック1", "トピック2", ...],
|
| 104 |
+
"summary": "問題群の概要(50文字以内)"
|
| 105 |
+
}`;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// 要約保存
|
| 109 |
+
function saveSummary(sessionId, subject, summaryData) {
|
| 110 |
+
const headers = ['summary_id', 'session_id', 'subject', 'keywords', 'topics', 'summary', 'created_at'];
|
| 111 |
+
const sheet = getOrCreateSheet(SHEETS.QUESTION_SUMMARIES, headers);
|
| 112 |
+
// ...保存処理...
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// 要約取得(HFから呼び出し)
|
| 116 |
+
function getQuestionSummaries(userId, limit = 5) {
|
| 117 |
+
// 直近N セッションの要約を取得
|
| 118 |
+
// ジャンル別カウントも併せて返却
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// 要約ステータスチェック(HFから呼び出し)
|
| 122 |
+
function checkSummaryStatus(sessionId) {
|
| 123 |
+
// 要約完了状態を返却
|
| 124 |
+
}
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
---
|
| 128 |
+
|
| 129 |
+
### T2: GAS - ジャンル別カウント取得(SUBAGENT-B)
|
| 130 |
+
**対象ファイル**: `gas/Code.gs`
|
| 131 |
+
|
| 132 |
+
**実装内容**:
|
| 133 |
+
```javascript
|
| 134 |
+
// 統計取得を拡張(ジャンル別出題カウント追加)
|
| 135 |
+
function getStatistics(userId, subjects) {
|
| 136 |
+
// ...既存処理...
|
| 137 |
+
|
| 138 |
+
// ジャンル別出題カウントを追加
|
| 139 |
+
result.genre_counts = getGenreCounts(userId, subjects);
|
| 140 |
+
|
| 141 |
+
return result;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// ジャンル別出題カウント
|
| 145 |
+
function getGenreCounts(userId, subjects) {
|
| 146 |
+
const answersSheet = getOrCreateSheet(SHEETS.ANSWERS, []);
|
| 147 |
+
const sessionsSheet = getOrCreateSheet(SHEETS.SESSIONS, []);
|
| 148 |
+
|
| 149 |
+
// ユーザーのセッション一覧を取得
|
| 150 |
+
// 各セッションの解答からカテゴリ別にカウント
|
| 151 |
+
|
| 152 |
+
return {
|
| 153 |
+
jp: { JP01: 15, JP02: 5, JP03: 6, JP04: 4, JP05: 0, JP06: 0, JP07: 0, JP08: 0 },
|
| 154 |
+
math: { MA01: 10, ... },
|
| 155 |
+
...
|
| 156 |
+
};
|
| 157 |
+
}
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
---
|
| 161 |
+
|
| 162 |
+
### T3: HF - 要約取得・除外リスト生成(SUBAGENT-C)
|
| 163 |
+
**対象ファイル**: `src/services/gas_client.py`
|
| 164 |
+
|
| 165 |
+
**実装内容**:
|
| 166 |
+
```python
|
| 167 |
+
async def get_question_summaries(
|
| 168 |
+
self,
|
| 169 |
+
user_id: str,
|
| 170 |
+
limit: int = 5
|
| 171 |
+
) -> Dict[str, Any]:
|
| 172 |
+
"""直近セッションの問題要約を取得"""
|
| 173 |
+
return await self._post({
|
| 174 |
+
"action": "get_question_summaries",
|
| 175 |
+
"user_id": user_id,
|
| 176 |
+
"limit": limit
|
| 177 |
+
})
|
| 178 |
+
|
| 179 |
+
async def check_summary_status(
|
| 180 |
+
self,
|
| 181 |
+
session_id: str
|
| 182 |
+
) -> Dict[str, Any]:
|
| 183 |
+
"""要約完了ステータスを確認"""
|
| 184 |
+
return await self._post({
|
| 185 |
+
"action": "check_summary_status",
|
| 186 |
+
"session_id": session_id
|
| 187 |
+
})
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
---
|
| 191 |
+
|
| 192 |
+
### T4: HF - プロンプト改修(SUBAGENT-D)
|
| 193 |
+
**対象ファイル**: `src/prompts/question_prompts.py`
|
| 194 |
+
|
| 195 |
+
**実装内容**:
|
| 196 |
+
```python
|
| 197 |
+
def get_batch_question_prompt(
|
| 198 |
+
subjects: List[str],
|
| 199 |
+
knowledge_bases: Dict[str, str],
|
| 200 |
+
priority_genres: Dict[str, List[str]] = None, # 追加
|
| 201 |
+
exclude_keywords: List[str] = None # 追加
|
| 202 |
+
) -> str:
|
| 203 |
+
# ... 既存のプロンプト構築 ...
|
| 204 |
+
|
| 205 |
+
# 優先ジャンル指定を追加
|
| 206 |
+
if priority_genres:
|
| 207 |
+
prompt += "\n## 出題優先ジャンル\n"
|
| 208 |
+
prompt += "以下のジャンルからの出題を優先してください(出題が少ない分野):\n"
|
| 209 |
+
for subject in subjects:
|
| 210 |
+
if subject in priority_genres and priority_genres[subject]:
|
| 211 |
+
subject_name = SUBJECT_NAMES.get(subject, subject)
|
| 212 |
+
genres = priority_genres[subject][:5] # 上位5ジャンル
|
| 213 |
+
prompt += f"- {subject_name}: {', '.join(genres)}\n"
|
| 214 |
+
|
| 215 |
+
# 除外キーワード指定を追加
|
| 216 |
+
if exclude_keywords and len(exclude_keywords) > 0:
|
| 217 |
+
prompt += f"\n## 除外キーワード\n"
|
| 218 |
+
prompt += "以下のキーワード・トピックに関する問題は避けてください(直近で出題済み):\n"
|
| 219 |
+
prompt += f"{', '.join(exclude_keywords[:30])}\n"
|
| 220 |
+
|
| 221 |
+
return prompt
|
| 222 |
+
```
|
| 223 |
+
|
| 224 |
+
---
|
| 225 |
+
|
| 226 |
+
### T5: HF - app.py統合(SUBAGENT-E)
|
| 227 |
+
**対象ファイル**: `app.py`
|
| 228 |
+
|
| 229 |
+
**実装内容**:
|
| 230 |
+
```python
|
| 231 |
+
@app.post("/api/generate_questions")
|
| 232 |
+
async def generate_questions(request: GenerateQuestionsRequest):
|
| 233 |
+
# Phase 0: 統計・要約取得(新規追加)
|
| 234 |
+
stats = await gas_client.get_statistics(request.user_id, request.subjects)
|
| 235 |
+
summaries = await gas_client.get_question_summaries(request.user_id, limit=5)
|
| 236 |
+
|
| 237 |
+
# Phase 0.5: 優先ジャンル・除外キーワード計算(新規追加)
|
| 238 |
+
priority_genres = calculate_priority_genres(stats.get("genre_counts", {}), request.subjects)
|
| 239 |
+
exclude_keywords = extract_exclude_keywords(summaries)
|
| 240 |
+
|
| 241 |
+
# Phase 1-4: 既存フロー(引数追加)
|
| 242 |
+
questions_by_subject = await gemini_service.generate_questions_batch(
|
| 243 |
+
subjects=request.subjects,
|
| 244 |
+
knowledge_bases=knowledge_bases,
|
| 245 |
+
priority_genres=priority_genres,
|
| 246 |
+
exclude_keywords=exclude_keywords
|
| 247 |
+
)
|
| 248 |
+
...
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
---
|
| 252 |
+
|
| 253 |
+
### T6: HF - 評価画面に要約ステータス表示(SUBAGENT-F)
|
| 254 |
+
**対象ファイル**: `app.py`, `static/index.html`
|
| 255 |
+
|
| 256 |
+
**実装内容**:
|
| 257 |
+
```python
|
| 258 |
+
@app.post("/api/get_evaluation")
|
| 259 |
+
async def get_evaluation(request):
|
| 260 |
+
# ...既存処理...
|
| 261 |
+
|
| 262 |
+
# 要約ステータスチェック追加
|
| 263 |
+
summary_status = await gas_client.check_summary_status(request.session_id)
|
| 264 |
+
|
| 265 |
+
return {
|
| 266 |
+
"success": True,
|
| 267 |
+
"data": {
|
| 268 |
+
...existing,
|
| 269 |
+
"summary_status": summary_status,
|
| 270 |
+
"quiz_summary": summary_status.get("summary", "") # 今回のクイズ内容
|
| 271 |
+
}
|
| 272 |
+
}
|
| 273 |
+
```
|
| 274 |
+
|
| 275 |
+
---
|
| 276 |
+
|
| 277 |
+
## ファイル担当マップ
|
| 278 |
+
|
| 279 |
+
| ファイル | Task | 変更内容 |
|
| 280 |
+
|---------|------|---------|
|
| 281 |
+
| `gas/Code.gs` | T1, T2 | 要約生成・保存、ジャンルカウント |
|
| 282 |
+
| `src/services/gas_client.py` | T3 | 要約取得API追加 |
|
| 283 |
+
| `src/prompts/question_prompts.py` | T4 | プロンプト拡張 |
|
| 284 |
+
| `src/services/gemini_service.py` | T4 | 引数追加 |
|
| 285 |
+
| `app.py` | T5, T6 | フロー統合、ステータス表示 |
|
| 286 |
+
| `static/index.html` | T6 | UI更新 |
|
| 287 |
+
|
| 288 |
+
---
|
| 289 |
+
|
| 290 |
+
## 競合回避
|
| 291 |
+
- T1, T2: GAS側(同一ファイルだが関数単位で分離)
|
| 292 |
+
- T3, T4: HF側(異なるファイル、並列可)
|
| 293 |
+
- T5, T6: T1-T4完了後に実行
|
| 294 |
+
|
| 295 |
+
## 実行順序
|
| 296 |
+
|
| 297 |
+
```
|
| 298 |
+
[並列実行可能]
|
| 299 |
+
T1 (GAS要約生成) ─────────────────┐
|
| 300 |
+
T2 (GASジャンルカウント) ─────────┼→ T5 (app.py統合) → T6 (UI) → E2E
|
| 301 |
+
T3 (HF要約取得) ──────────────────┤
|
| 302 |
+
T4 (HFプロンプト改修) ────────────┘
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
---
|
| 306 |
+
|
| 307 |
+
## 期待効果
|
| 308 |
+
|
| 309 |
+
| 指標 | v1.3.0 | v1.4.0 |
|
| 310 |
+
|-----|--------|--------|
|
| 311 |
+
| ジャンル網羅率 | 50%(4/8) | 100%(8/8) |
|
| 312 |
+
| 題材重複率 | 高い | 低い |
|
| 313 |
+
| LLM呼び出し(問題生成) | 2回 | 2回(変更なし)|
|
| 314 |
+
| LLM呼び出し(要約)| 0��� | 1回/教科(GAS側、非同期)|
|
| 315 |
+
| ユーザー待機時間 | - | 増加なし(解答中に処理)|
|
V1.7.1/docs/dify-prompts.md
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dify 問題生成LLMプロンプト集
|
| 2 |
+
|
| 3 |
+
**目的**: 4教科の問題生成LLMノードに設定するプロンプト
|
| 4 |
+
**使用方法**: 各教科のプロンプトをコピーして、対応するLLMノードにペースト
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## 国語問題生成
|
| 9 |
+
|
| 10 |
+
```
|
| 11 |
+
あなたは中学受験対策の国語問題作成の専門家です。
|
| 12 |
+
|
| 13 |
+
## タスク
|
| 14 |
+
以下の条件に基づき、国語の4択問題を20問生成してください。
|
| 15 |
+
|
| 16 |
+
## ジャンル一覧(必ず以下のジャンルIDを使用)
|
| 17 |
+
- JP01: 漢字・語彙(読み書き、四字熟語、慣用句、ことわざ)
|
| 18 |
+
- JP02: 文法・言葉のきまり(品詞、敬語、文の成分、修飾関係)
|
| 19 |
+
- JP03: 物語文読解(心情理解、場面把握、人物関係)
|
| 20 |
+
- JP04: 説明文・論説文読解(要旨、段落構成、筆者の主張)
|
| 21 |
+
- JP05: 随筆文読解(筆者の体験・感想の読み取り)
|
| 22 |
+
- JP06: 詩・韻文(詩、短歌、俳句、表現技法)
|
| 23 |
+
- JP07: 記述問題(理由説明、要約、意見記述)
|
| 24 |
+
- JP08: 知識・文学史(作家、作品名、文学的常識)
|
| 25 |
+
|
| 26 |
+
## 入力データ
|
| 27 |
+
- **分野別正答率**: {{#1751785054001.statistics#}}
|
| 28 |
+
- **直近20問の履歴**: {{#1751785054001.recent_questions#}}
|
| 29 |
+
|
| 30 |
+
## 条件
|
| 31 |
+
1. **分野構成**: 上記8ジャンルから均等に出題(各2〜3問)
|
| 32 |
+
2. **難易度調整**: 正答率60%未満のジャンルは「基本」レベル、60%以上は「標準」「応用」をミックス
|
| 33 |
+
3. **重複回避**: 直近20問と類似した問題は避ける
|
| 34 |
+
4. **問題形式**: 問題文、4つの選択肢、正解番号(0〜3)、解説を含む
|
| 35 |
+
5. **ジャンルID使用**: categoryフィールドには必ずジャンルID(JP01〜JP08)を使用すること
|
| 36 |
+
|
| 37 |
+
## 出力形式
|
| 38 |
+
**重要**: categoryフィールドには必ずジャンルID(JP01, JP02, JP03, JP04, JP05, JP06, JP07, JP08)を使用してください。
|
| 39 |
+
日本語のジャンル名(「漢字・語彙」等)は使用しないでください。
|
| 40 |
+
|
| 41 |
+
JSON形式で以下の構造で返してください:
|
| 42 |
+
[
|
| 43 |
+
{
|
| 44 |
+
"question_id": "koku_001",
|
| 45 |
+
"category": "JP01",
|
| 46 |
+
"difficulty": "標準",
|
| 47 |
+
"question": "問題文",
|
| 48 |
+
"choices": ["選択肢1", "選択肢2", "選択肢3", "選択肢4"],
|
| 49 |
+
"correct_answer": 2,
|
| 50 |
+
"explanation": "解説文"
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"question_id": "koku_002",
|
| 54 |
+
"category": "JP02",
|
| 55 |
+
"difficulty": "基本",
|
| 56 |
+
"question": "問題文",
|
| 57 |
+
"choices": ["選択肢1", "選択肢2", "選択肢3", "選択肢4"],
|
| 58 |
+
"correct_answer": 0,
|
| 59 |
+
"explanation": "解説文"
|
| 60 |
+
}
|
| 61 |
+
... (合計20問)
|
| 62 |
+
]
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
---
|
| 66 |
+
|
| 67 |
+
## 算数問題生成
|
| 68 |
+
|
| 69 |
+
```
|
| 70 |
+
あなたは中学受験対策の算数問題作成の専門家です。
|
| 71 |
+
|
| 72 |
+
## タスク
|
| 73 |
+
以下の条件に基づき、算数の4択問題を20問生成してください。
|
| 74 |
+
|
| 75 |
+
## ジャンル一覧(必ず以下のジャンルIDを使用)
|
| 76 |
+
- MA01: 計算(四則演算、分数・小数、逆算)
|
| 77 |
+
- MA02: 数の性質(約数・倍数、素因数分解、規則性)
|
| 78 |
+
- MA03: 割合・比(割合、比、百分率、歩合)
|
| 79 |
+
- MA04: 速さ(旅人算、通過算、流水算、時計算)
|
| 80 |
+
- MA05: 文章題(その他)(濃度、仕事算、ニュートン算、差集め算)
|
| 81 |
+
- MA06: 平面図形(面積、角度、相似、合同)
|
| 82 |
+
- MA07: 立体図形(体積、表面積、展開図、切断)
|
| 83 |
+
- MA08: 場合の数・確率(順列、組み合わせ、確率)
|
| 84 |
+
- MA09: グラフ・表(統計、変化のグラフ、ダイヤグラム)
|
| 85 |
+
- MA10: 特殊算(つるかめ算、消去算、過不足算)
|
| 86 |
+
|
| 87 |
+
## 入力データ
|
| 88 |
+
- **分野別正答率**: {{#1751785054001.statistics#}}
|
| 89 |
+
- **直近20問の履歴**: {{#1751785054001.recent_questions#}}
|
| 90 |
+
|
| 91 |
+
## 条件
|
| 92 |
+
1. **分野構成**: 上記10ジャンルから幅広く出題(各1〜3問)
|
| 93 |
+
2. **難易度調整**: 正答率60%未満のジャンルは「基本」レベル、60%以上は「標準」「応用」をミックス
|
| 94 |
+
3. **重複回避**: 直近20問と類似した問題は避ける
|
| 95 |
+
4. **問題形式**: 問題文、4つの選択肢、正解番号(0〜3)、解説を含む
|
| 96 |
+
5. **ジャンルID使用**: categoryフィールドには必ずジャンルID(MA01〜MA10)を使用すること
|
| 97 |
+
|
| 98 |
+
## 出力形式
|
| 99 |
+
**重要**: categoryフィールドには必ずジャンルID(MA01, MA02, MA03, MA04, MA05, MA06, MA07, MA08, MA09, MA10)を使用してください。
|
| 100 |
+
日本語のジャンル名(「平面図形」等)は使用しないでください。
|
| 101 |
+
|
| 102 |
+
JSON形式で以下の構造で返してください:
|
| 103 |
+
[
|
| 104 |
+
{
|
| 105 |
+
"question_id": "san_001",
|
| 106 |
+
"category": "MA01",
|
| 107 |
+
"difficulty": "標準",
|
| 108 |
+
"question": "問題文",
|
| 109 |
+
"choices": ["選択肢1", "選択肢2", "選択肢3", "選択肢4"],
|
| 110 |
+
"correct_answer": 0,
|
| 111 |
+
"explanation": "解説文"
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"question_id": "san_002",
|
| 115 |
+
"category": "MA06",
|
| 116 |
+
"difficulty": "応用",
|
| 117 |
+
"question": "問題文",
|
| 118 |
+
"choices": ["選択肢1", "選択肢2", "選択肢3", "選択肢4"],
|
| 119 |
+
"correct_answer": 2,
|
| 120 |
+
"explanation": "解説文"
|
| 121 |
+
}
|
| 122 |
+
... (合計20問)
|
| 123 |
+
]
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
---
|
| 127 |
+
|
| 128 |
+
## 理科問題生成
|
| 129 |
+
|
| 130 |
+
```
|
| 131 |
+
あなたは中学受験対策の理科問題作成の専門家です。
|
| 132 |
+
|
| 133 |
+
## タスク
|
| 134 |
+
以下の条件に基づき、理科の4択問題を20問生成してください。
|
| 135 |
+
|
| 136 |
+
## ジャンル一覧(必ず以下のジャンルIDを使用)
|
| 137 |
+
- SC01: 力・運動(てこ、滑車、ばね、浮力、振り子)
|
| 138 |
+
- SC02: 電気(回路、抵抗、電磁石、発熱)
|
| 139 |
+
- SC03: 光・音・熱(反射、屈折、レンズ、音の性質)
|
| 140 |
+
- SC04: 物質の性質(金属、気体、密度、状態変化)
|
| 141 |
+
- SC05: 水溶液(酸・アルカリ、中和、溶解度)
|
| 142 |
+
- SC06: 燃焼・化学変化(燃焼、酸化還元、化合)
|
| 143 |
+
- SC07: 植物(つくり、光合成、蒸散、分類)
|
| 144 |
+
- SC08: 動物(からだのつくり、行動、分類)
|
| 145 |
+
- SC09: 人体(消化、呼吸、血液循環、感覚器官)
|
| 146 |
+
- SC10: 天体(太陽、月、星座、地球の運動)
|
| 147 |
+
- SC11: 気象(天気、気温、湿度、雲、季節風)
|
| 148 |
+
- SC12: 地学(地層、岩石、火山、地震)
|
| 149 |
+
|
| 150 |
+
## 入力データ
|
| 151 |
+
- **分野別正答率**: {{#1751785054001.statistics#}}
|
| 152 |
+
- **直近20問の履歴**: {{#1751785054001.recent_questions#}}
|
| 153 |
+
|
| 154 |
+
## 条件
|
| 155 |
+
1. **分野構成**: 上記12ジャンルから幅広く出題(各1〜2問)
|
| 156 |
+
2. **難易度調整**: 正答率60%未満のジャンルは「基本」レベル、60%以上は「標準」「応用」をミックス
|
| 157 |
+
3. **重複回避**: 直近20問と類似した問題は避ける
|
| 158 |
+
4. **問題形式**: 問題文、4つの選択肢、正解番号(0〜3)、解説を含む
|
| 159 |
+
5. **ジャンルID使用**: categoryフィールドには必ずジャンルID(SC01〜SC12)を使用すること
|
| 160 |
+
|
| 161 |
+
## 出力形式
|
| 162 |
+
**重要**: categoryフィールドには必ずジャンルID(SC01, SC02, SC03, SC04, SC05, SC06, SC07, SC08, SC09, SC10, SC11, SC12)を使用してください。
|
| 163 |
+
日本語のジャンル名(「力・運動」等)は使用しないでください。
|
| 164 |
+
|
| 165 |
+
JSON形式で以下の構造で返してください:
|
| 166 |
+
[
|
| 167 |
+
{
|
| 168 |
+
"question_id": "rika_001",
|
| 169 |
+
"category": "SC01",
|
| 170 |
+
"difficulty": "標準",
|
| 171 |
+
"question": "問題文",
|
| 172 |
+
"choices": ["選択肢1", "選択肢2", "選択肢3", "選択肢4"],
|
| 173 |
+
"correct_answer": 1,
|
| 174 |
+
"explanation": "解説文"
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"question_id": "rika_002",
|
| 178 |
+
"category": "SC07",
|
| 179 |
+
"difficulty": "基本",
|
| 180 |
+
"question": "問題文",
|
| 181 |
+
"choices": ["選択肢1", "選択肢2", "選択肢3", "選択肢4"],
|
| 182 |
+
"correct_answer": 3,
|
| 183 |
+
"explanation": "解説文"
|
| 184 |
+
}
|
| 185 |
+
... (合計20問)
|
| 186 |
+
]
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
---
|
| 190 |
+
|
| 191 |
+
## 社会問題生成
|
| 192 |
+
|
| 193 |
+
```
|
| 194 |
+
あなたは中学受験対策の社会問題作成の専門家です。
|
| 195 |
+
|
| 196 |
+
## タスク
|
| 197 |
+
以下の条件に基づき、社会の4択問題を20問生成してください。
|
| 198 |
+
|
| 199 |
+
## ジャンル一覧(必ず以下のジャンルIDを使用)
|
| 200 |
+
- SO01: 日本地理(国土・自然)(地形、気候、都道府県)
|
| 201 |
+
- SO02: 日本地理(産業)(農業、工業、水産業、商業)
|
| 202 |
+
- SO03: 世界地理(大陸、国、貿易、環境問題)
|
| 203 |
+
- SO04: 歴史(古代〜平安)(旧石器〜平安時代)
|
| 204 |
+
- SO05: 歴史(鎌倉〜室町)(武士の台頭、文化)
|
| 205 |
+
- SO06: 歴史(安土桃山〜江戸)(統一、鎖国、元禄・化政文化)
|
| 206 |
+
- SO07: 歴史(明治〜現代)(近代化、戦争、戦後)
|
| 207 |
+
- SO08: 公民(政治・憲法)(三権分立、選挙、人権)
|
| 208 |
+
- SO09: 公民(経済・国際)(経済の仕組み、国際機関、SDGs)
|
| 209 |
+
- SO10: 時事問題(直近1〜2年のニュース)
|
| 210 |
+
|
| 211 |
+
## 入力データ
|
| 212 |
+
- **分野別正答率**: {{#1751785054001.statistics#}}
|
| 213 |
+
- **直近20問の履歴**: {{#1751785054001.recent_questions#}}
|
| 214 |
+
|
| 215 |
+
## 条件
|
| 216 |
+
1. **分野構成**: 上記10ジャンルから幅広く出題(各1〜3問)
|
| 217 |
+
2. **難易度調整**: 正答率60%未満のジャンルは「基本」レベル、60%以上は「標準」「応用」をミックス
|
| 218 |
+
3. **重複回避**: 直近20問と類似した問題は避ける
|
| 219 |
+
4. **問題形式**: 問題文、4つの選択肢、正解番号(0〜3)、解説を含む
|
| 220 |
+
5. **ジャンルID使用**: categoryフィールドには必ずジャンルID(SO01〜SO10)を使用すること
|
| 221 |
+
|
| 222 |
+
## 出力形式
|
| 223 |
+
**重要**: categoryフィールドには必ずジャンルID(SO01, SO02, SO03, SO04, SO05, SO06, SO07, SO08, SO09, SO10)を使用してください。
|
| 224 |
+
日本語のジャンル名(「日本地理(国土・自然)」等)は使用しないでください。
|
| 225 |
+
|
| 226 |
+
JSON形式で以下の構造で返してください:
|
| 227 |
+
[
|
| 228 |
+
{
|
| 229 |
+
"question_id": "soci_001",
|
| 230 |
+
"category": "SO01",
|
| 231 |
+
"difficulty": "標準",
|
| 232 |
+
"question": "問題文",
|
| 233 |
+
"choices": ["選択肢1", "選択肢2", "選択肢3", "選択肢4"],
|
| 234 |
+
"correct_answer": 2,
|
| 235 |
+
"explanation": "解説文"
|
| 236 |
+
},
|
| 237 |
+
{
|
| 238 |
+
"question_id": "soci_002",
|
| 239 |
+
"category": "SO08",
|
| 240 |
+
"difficulty": "応用",
|
| 241 |
+
"question": "問題文",
|
| 242 |
+
"choices": ["選択肢1", "選択肢2", "選択肢3", "選���肢4"],
|
| 243 |
+
"correct_answer": 1,
|
| 244 |
+
"explanation": "解説文"
|
| 245 |
+
}
|
| 246 |
+
... (合計20問)
|
| 247 |
+
]
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
---
|
| 251 |
+
|
| 252 |
+
## ジャンルID一覧(参照用)
|
| 253 |
+
|
| 254 |
+
| 教科 | ジャンルID | ジャンル名 |
|
| 255 |
+
|------|-----------|-----------|
|
| 256 |
+
| **国語** | JP01 | 漢字・語彙 |
|
| 257 |
+
| | JP02 | 文法・言葉のきまり |
|
| 258 |
+
| | JP03 | 物語文読解 |
|
| 259 |
+
| | JP04 | 説明文・論説文読解 |
|
| 260 |
+
| | JP05 | 随筆文読解 |
|
| 261 |
+
| | JP06 | 詩・韻文 |
|
| 262 |
+
| | JP07 | 記述問題 |
|
| 263 |
+
| | JP08 | 知識・文学史 |
|
| 264 |
+
| **算数** | MA01 | 計算 |
|
| 265 |
+
| | MA02 | 数の性質 |
|
| 266 |
+
| | MA03 | 割合・比 |
|
| 267 |
+
| | MA04 | 速さ |
|
| 268 |
+
| | MA05 | 文章題(その他) |
|
| 269 |
+
| | MA06 | 平面図形 |
|
| 270 |
+
| | MA07 | 立体図形 |
|
| 271 |
+
| | MA08 | 場合の数・確率 |
|
| 272 |
+
| | MA09 | グラフ・表 |
|
| 273 |
+
| | MA10 | 特殊算 |
|
| 274 |
+
| **理科** | SC01 | 力・運動 |
|
| 275 |
+
| | SC02 | 電気 |
|
| 276 |
+
| | SC03 | 光・音・熱 |
|
| 277 |
+
| | SC04 | 物質の性質 |
|
| 278 |
+
| | SC05 | 水溶液 |
|
| 279 |
+
| | SC06 | 燃焼・化学変化 |
|
| 280 |
+
| | SC07 | 植物 |
|
| 281 |
+
| | SC08 | 動物 |
|
| 282 |
+
| | SC09 | 人体 |
|
| 283 |
+
| | SC10 | 天体 |
|
| 284 |
+
| | SC11 | 気象 |
|
| 285 |
+
| | SC12 | 地学 |
|
| 286 |
+
| **社会** | SO01 | 日本地理(国土・自然) |
|
| 287 |
+
| | SO02 | 日本地理(産業) |
|
| 288 |
+
| | SO03 | 世界地理 |
|
| 289 |
+
| | SO04 | 歴史(古代〜平安) |
|
| 290 |
+
| | SO05 | 歴史(鎌倉〜室町) |
|
| 291 |
+
| | SO06 | 歴史(安土桃山〜江戸) |
|
| 292 |
+
| | SO07 | 歴史(明治〜現代) |
|
| 293 |
+
| | SO08 | 公民(政治・憲法) |
|
| 294 |
+
| | SO09 | 公民(経済・国際) |
|
| 295 |
+
| | SO10 | 時事問題 |
|
| 296 |
+
|
| 297 |
+
---
|
| 298 |
+
|
| 299 |
+
**注意**: `{{#1751785054001.statistics#}}` と `{{#1751785054001.recent_questions#}}` は、Difyワークフロー内の変数参照です。実際のノードIDに合わせて調整してください。
|
V1.7.1/gas/.clasp.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"scriptId": "1rBj6xUhat3KZ1Iy9WXLdMiRh4CTKqCJHK3G149plkLJsPeuGFa5DuyGX",
|
| 3 |
+
"rootDir": "."
|
| 4 |
+
}
|
V1.7.1/gas/Code.js
ADDED
|
@@ -0,0 +1,2246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* 超天才クイズAPI - Google Apps Script メインファイル
|
| 3 |
+
*
|
| 4 |
+
* @version 1.6.12
|
| 5 |
+
* @date 2025-12-20
|
| 6 |
+
*
|
| 7 |
+
* 変更履歴:
|
| 8 |
+
* - v1.6.12: デバッグロギング追加
|
| 9 |
+
* - v1.6.11: handleSubmitAnswers レスポンスキー修正 (correct_count→correct, total_questions→total, score→accuracy)
|
| 10 |
+
* - v1.6.10: getSessionResults カラムインデックス修正 (row[5],row[7],row[8])
|
| 11 |
+
* - v1.6.9: correct_answer 0-indexed統一, temperature=1.0
|
| 12 |
+
* - v1.6.8: shuffle_choices→convert_correct_answer_index, MAX_OUTPUT_TOKENS=65536
|
| 13 |
+
* - v1.6.6: Answersシートに subject,category,correct_answer カラム追加
|
| 14 |
+
*/
|
| 15 |
+
|
| 16 |
+
// ============================================================================
|
| 17 |
+
// 定数定義
|
| 18 |
+
// ============================================================================
|
| 19 |
+
|
| 20 |
+
// 超天才クイズv3 本番スプレッドシート
|
| 21 |
+
// URL: https://docs.google.com/spreadsheets/d/10JLP5ds2CNDOEYTxEzDyY82dErbD-InkTeuyzS_w_3U/
|
| 22 |
+
const SPREADSHEET_ID = '10JLP5ds2CNDOEYTxEzDyY82dErbD-InkTeuyzS_w_3U';
|
| 23 |
+
const SHEET_NAMES = {
|
| 24 |
+
USERS: 'Users',
|
| 25 |
+
SESSIONS: 'Sessions',
|
| 26 |
+
QUESTIONS: 'Questions',
|
| 27 |
+
ANSWERS: 'Answers',
|
| 28 |
+
STATISTICS: 'Statistics',
|
| 29 |
+
EVALUATIONS: 'Evaluations',
|
| 30 |
+
KNOWLEDGE_BASE: 'Knowledge_Base',
|
| 31 |
+
GENERATED_QUESTIONS: 'GeneratedQuestions',
|
| 32 |
+
SUMMARIES: 'Summaries'
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
// 教科マッピング(DifyService.jsと共有)
|
| 36 |
+
const SUBJECT_MAP = {
|
| 37 |
+
jp: { id: 'jp', name: '国語' },
|
| 38 |
+
math: { id: 'math', name: '算数' },
|
| 39 |
+
sci: { id: 'sci', name: '理科' },
|
| 40 |
+
soc: { id: 'soc', name: '社会' }
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
// ============================================================================
|
| 44 |
+
// エントリーポイント(HTTPリクエストハンドラ)
|
| 45 |
+
// ============================================================================
|
| 46 |
+
|
| 47 |
+
/**
|
| 48 |
+
* GETリクエストハンドラ(ヘルスチェック用)
|
| 49 |
+
*
|
| 50 |
+
* @param {Object} e - イベントオブジェクト
|
| 51 |
+
* @returns {TextOutput} - JSON形式のレスポンス
|
| 52 |
+
*/
|
| 53 |
+
function doGet(e) {
|
| 54 |
+
try {
|
| 55 |
+
return createJsonResponse({
|
| 56 |
+
success: true,
|
| 57 |
+
message: "超天才クイズAPI is running",
|
| 58 |
+
version: "1.0.0",
|
| 59 |
+
timestamp: new Date().toISOString()
|
| 60 |
+
});
|
| 61 |
+
} catch (error) {
|
| 62 |
+
return createErrorResponse('Internal Server Error', error.toString());
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/**
|
| 67 |
+
* POSTリクエストハンドラ(APIエンドポイントルーター)
|
| 68 |
+
*
|
| 69 |
+
* @param {Object} e - イベントオブジェクト
|
| 70 |
+
* @returns {TextOutput} - JSON形式のレスポンス
|
| 71 |
+
*/
|
| 72 |
+
function doPost(e) {
|
| 73 |
+
try {
|
| 74 |
+
// リクエストボディのパース
|
| 75 |
+
const requestBody = JSON.parse(e.postData.contents);
|
| 76 |
+
|
| 77 |
+
// 新形式(action)と旧形式(endpoint)の両方をサポート
|
| 78 |
+
const action = requestBody.action || requestBody.endpoint || '';
|
| 79 |
+
|
| 80 |
+
// 新形式: パラメータはフラット、旧形式: params内にネスト
|
| 81 |
+
const params = requestBody.params || requestBody;
|
| 82 |
+
|
| 83 |
+
Logger.log('Received request - Action: ' + action);
|
| 84 |
+
Logger.log('Request params: ' + JSON.stringify(params));
|
| 85 |
+
|
| 86 |
+
// ==================== QuestionDatabase関連のアクション (v1.6.3) ====================
|
| 87 |
+
const questionDbActions = [
|
| 88 |
+
'get_random_answers',
|
| 89 |
+
'get_random_questions', // Python互換: get_questions_by_configにマッピング
|
| 90 |
+
'get_questions_by_config', // v1.6.3: ジャンル構成固定化
|
| 91 |
+
'update_usage_count',
|
| 92 |
+
'bulk_insert_answers',
|
| 93 |
+
'get_stats',
|
| 94 |
+
'reset_usage',
|
| 95 |
+
'delete_by_subject',
|
| 96 |
+
'add_header'
|
| 97 |
+
];
|
| 98 |
+
|
| 99 |
+
if (questionDbActions.includes(action)) {
|
| 100 |
+
const result = handleQuestionDatabaseAction(action, params);
|
| 101 |
+
return ContentService.createTextOutput(JSON.stringify(result))
|
| 102 |
+
.setMimeType(ContentService.MimeType.JSON);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// アクションによる振り分け(snake_case と camelCase の両方をサポート)
|
| 106 |
+
switch (action) {
|
| 107 |
+
case 'login':
|
| 108 |
+
return handleLogin(params);
|
| 109 |
+
|
| 110 |
+
case 'register_user':
|
| 111 |
+
case 'registerUser':
|
| 112 |
+
return handleRegisterUser(params);
|
| 113 |
+
|
| 114 |
+
case 'start_session':
|
| 115 |
+
case 'startSession':
|
| 116 |
+
return handleStartSession(params);
|
| 117 |
+
|
| 118 |
+
case 'generate_question':
|
| 119 |
+
case 'generateQuestion':
|
| 120 |
+
return handleGenerateQuestion(params);
|
| 121 |
+
|
| 122 |
+
case 'generate_questions':
|
| 123 |
+
case 'generateQuestions':
|
| 124 |
+
return handleGenerateQuestions(params);
|
| 125 |
+
|
| 126 |
+
case 'submit_answers':
|
| 127 |
+
case 'submitAnswers':
|
| 128 |
+
return handleSubmitAnswers(params);
|
| 129 |
+
|
| 130 |
+
case 'get_statistics':
|
| 131 |
+
case 'getStatistics':
|
| 132 |
+
return handleGetStatistics(params);
|
| 133 |
+
|
| 134 |
+
case 'get_evaluation':
|
| 135 |
+
case 'getEvaluation':
|
| 136 |
+
return handleGetEvaluation(params);
|
| 137 |
+
|
| 138 |
+
case 'save_questions':
|
| 139 |
+
return handleSaveQuestions(params);
|
| 140 |
+
|
| 141 |
+
case 'save_summary':
|
| 142 |
+
return handleSaveSummary(params);
|
| 143 |
+
|
| 144 |
+
case 'get_session_results':
|
| 145 |
+
return handleGetSessionResults(params);
|
| 146 |
+
|
| 147 |
+
case 'check_summary_status':
|
| 148 |
+
return handleCheckSummaryStatus(params);
|
| 149 |
+
|
| 150 |
+
case 'save_evaluations':
|
| 151 |
+
return handleSaveEvaluations(params);
|
| 152 |
+
|
| 153 |
+
default:
|
| 154 |
+
return createErrorResponse('Invalid Action', 'Unknown action: ' + action);
|
| 155 |
+
}
|
| 156 |
+
} catch (error) {
|
| 157 |
+
Logger.log('Error in doPost: ' + error.toString());
|
| 158 |
+
return createErrorResponse('Bad Request', error.toString());
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
/**
|
| 163 |
+
* OPTIONSリクエストハンドラ(CORS preflight対応)
|
| 164 |
+
*
|
| 165 |
+
* @returns {TextOutput} - 空のレスポンス
|
| 166 |
+
*/
|
| 167 |
+
function doOptions() {
|
| 168 |
+
return ContentService
|
| 169 |
+
.createTextOutput('')
|
| 170 |
+
.setMimeType(ContentService.MimeType.TEXT)
|
| 171 |
+
.setHeader('Access-Control-Allow-Origin', '*')
|
| 172 |
+
.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
| 173 |
+
.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
// ============================================================================
|
| 177 |
+
// エンドポイント実装(Phase 1: ダミーレスポンス)
|
| 178 |
+
// ============================================================================
|
| 179 |
+
|
| 180 |
+
/**
|
| 181 |
+
* ユーザー登録エンドポイント(本実装)
|
| 182 |
+
* Usersシートに新規ユーザーを追加
|
| 183 |
+
*
|
| 184 |
+
* @param {Object} params - リクエストパラメータ { username: string }
|
| 185 |
+
* @returns {TextOutput} - JSON形式のレスポンス
|
| 186 |
+
*/
|
| 187 |
+
function handleRegisterUser(params) {
|
| 188 |
+
try {
|
| 189 |
+
// 1. バリデーション
|
| 190 |
+
const username = params.username;
|
| 191 |
+
|
| 192 |
+
if (!username) {
|
| 193 |
+
return createJsonResponse({
|
| 194 |
+
status: "error",
|
| 195 |
+
message: "Username is required"
|
| 196 |
+
});
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
if (username.length > 20) {
|
| 200 |
+
return createJsonResponse({
|
| 201 |
+
status: "error",
|
| 202 |
+
message: "Username must be 20 characters or less"
|
| 203 |
+
});
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
// 2. Usersシート取得
|
| 207 |
+
const sheet = getSheet(SHEET_NAMES.USERS);
|
| 208 |
+
|
| 209 |
+
// 3. 既存ユーザーチェック(存在すればログインとして扱う)
|
| 210 |
+
const data = sheet.getDataRange().getValues();
|
| 211 |
+
// ヘッダー行をスキップ(data[0])
|
| 212 |
+
for (let i = 1; i < data.length; i++) {
|
| 213 |
+
if (data[i][1] === username) { // 列1 = username
|
| 214 |
+
// 既存ユーザーが見つかった場合、ログインとして成功を返す
|
| 215 |
+
const existingUserId = data[i][0]; // 列0 = user_id
|
| 216 |
+
const existingCreatedAt = data[i][3]; // 列3 = created_at(列2はpassword_hash)
|
| 217 |
+
|
| 218 |
+
// last_loginを更新(列4)
|
| 219 |
+
const loginTime = getTimestamp();
|
| 220 |
+
sheet.getRange(i + 1, 5).setValue(loginTime); // 行番号は1始まり、列5 = last_login
|
| 221 |
+
|
| 222 |
+
Logger.log('Existing user logged in: ' + username + ' (user_id: ' + existingUserId + ')');
|
| 223 |
+
|
| 224 |
+
return createJsonResponse({
|
| 225 |
+
status: "success",
|
| 226 |
+
message: "User logged in successfully",
|
| 227 |
+
data: {
|
| 228 |
+
user_id: existingUserId,
|
| 229 |
+
username: username,
|
| 230 |
+
created_at: existingCreatedAt,
|
| 231 |
+
last_login: loginTime,
|
| 232 |
+
is_new_user: false
|
| 233 |
+
}
|
| 234 |
+
});
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
// 4. 新規ユーザーデータ作成
|
| 239 |
+
const userId = generateUUID();
|
| 240 |
+
const createdAt = getTimestamp();
|
| 241 |
+
const passwordHash = params.password_hash || ""; // パスワードハッシュ(オプション)
|
| 242 |
+
const newRow = [
|
| 243 |
+
userId, // user_id (列0)
|
| 244 |
+
username, // username (列1)
|
| 245 |
+
passwordHash, // password_hash (列2) ← 追加
|
| 246 |
+
createdAt, // created_at (列3)
|
| 247 |
+
"", // last_login (列4, 初回は空)
|
| 248 |
+
0, // total_sessions (列5)
|
| 249 |
+
0, // total_questions (列6)
|
| 250 |
+
"{}" // settings (列7)
|
| 251 |
+
];
|
| 252 |
+
|
| 253 |
+
// 5. Sheetsに書き込み
|
| 254 |
+
sheet.appendRow(newRow);
|
| 255 |
+
|
| 256 |
+
// 6. 成功レスポンス
|
| 257 |
+
Logger.log('New user registered: ' + username + ' (user_id: ' + userId + ')');
|
| 258 |
+
|
| 259 |
+
return createJsonResponse({
|
| 260 |
+
status: "success",
|
| 261 |
+
message: "User registered successfully",
|
| 262 |
+
data: {
|
| 263 |
+
user_id: userId,
|
| 264 |
+
username: username,
|
| 265 |
+
created_at: createdAt,
|
| 266 |
+
is_new_user: true
|
| 267 |
+
}
|
| 268 |
+
});
|
| 269 |
+
|
| 270 |
+
} catch (error) {
|
| 271 |
+
Logger.log('Error in handleRegisterUser: ' + error.toString());
|
| 272 |
+
return createErrorResponse('Registration Failed', error.toString());
|
| 273 |
+
}
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
/**
|
| 277 |
+
* ログインエンドポイント(パスワードハッシュ取得)
|
| 278 |
+
* ユーザー名でUsersシートを検索し、password_hashを返す
|
| 279 |
+
*
|
| 280 |
+
* @param {Object} params - リクエストパラメータ { username: string }
|
| 281 |
+
* @returns {TextOutput} - JSON形式のレスポンス
|
| 282 |
+
*/
|
| 283 |
+
function handleLogin(params) {
|
| 284 |
+
try {
|
| 285 |
+
const username = params.username;
|
| 286 |
+
|
| 287 |
+
if (!username) {
|
| 288 |
+
return createJsonResponse({
|
| 289 |
+
success: false,
|
| 290 |
+
error: "Username is required"
|
| 291 |
+
});
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
// Usersシート取得
|
| 295 |
+
const sheet = getSheet(SHEET_NAMES.USERS);
|
| 296 |
+
const data = sheet.getDataRange().getValues();
|
| 297 |
+
|
| 298 |
+
// ヘッダー行をスキップして検索
|
| 299 |
+
// 列構造: 0=user_id, 1=username, 2=password_hash, 3=created_at, 4=last_login, ...
|
| 300 |
+
for (let i = 1; i < data.length; i++) {
|
| 301 |
+
if (data[i][1] === username) { // 列1 = username
|
| 302 |
+
const userId = data[i][0]; // 列0 = user_id
|
| 303 |
+
const passwordHash = data[i][2]; // 列2 = password_hash
|
| 304 |
+
|
| 305 |
+
// last_loginを更新(列4)
|
| 306 |
+
const loginTime = getTimestamp();
|
| 307 |
+
sheet.getRange(i + 1, 5).setValue(loginTime); // 行番号は1始まり、列5(index 4) = last_login
|
| 308 |
+
|
| 309 |
+
Logger.log('User found for login: ' + username + ', password_hash exists: ' + (!!passwordHash));
|
| 310 |
+
|
| 311 |
+
return createJsonResponse({
|
| 312 |
+
success: true,
|
| 313 |
+
found: true,
|
| 314 |
+
data: {
|
| 315 |
+
user_id: userId,
|
| 316 |
+
username: username,
|
| 317 |
+
password_hash: passwordHash
|
| 318 |
+
}
|
| 319 |
+
});
|
| 320 |
+
}
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
// ユーザーが見つからない場合
|
| 324 |
+
Logger.log('User not found for login: ' + username);
|
| 325 |
+
return createJsonResponse({
|
| 326 |
+
success: true,
|
| 327 |
+
found: false,
|
| 328 |
+
message: "User not found"
|
| 329 |
+
});
|
| 330 |
+
|
| 331 |
+
} catch (error) {
|
| 332 |
+
Logger.log('Error in handleLogin: ' + error.toString());
|
| 333 |
+
return createErrorResponse('Login Failed', error.toString());
|
| 334 |
+
}
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
/**
|
| 338 |
+
* セッション開始エンドポイント(本実装)
|
| 339 |
+
*
|
| 340 |
+
* @param {Object} params - リクエストパラメータ { user_id: string, subjects: string[] }
|
| 341 |
+
* @returns {TextOutput} - JSON形式のレスポンス
|
| 342 |
+
*/
|
| 343 |
+
function handleStartSession(params) {
|
| 344 |
+
try {
|
| 345 |
+
Logger.log('handleStartSession called with params: ' + JSON.stringify(params));
|
| 346 |
+
|
| 347 |
+
const userId = params.user_id;
|
| 348 |
+
const subjects = params.subjects || [];
|
| 349 |
+
|
| 350 |
+
if (!userId) {
|
| 351 |
+
return createJsonResponse({
|
| 352 |
+
success: false,
|
| 353 |
+
error: 'user_id is required'
|
| 354 |
+
});
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
// セッションデータ作成
|
| 358 |
+
const sessionId = generateUUID();
|
| 359 |
+
const startTime = getTimestamp();
|
| 360 |
+
|
| 361 |
+
const sessionsSheet = getSheet(SHEET_NAMES.SESSIONS);
|
| 362 |
+
const row = [
|
| 363 |
+
sessionId, // session_id
|
| 364 |
+
userId, // user_id
|
| 365 |
+
startTime, // start_time
|
| 366 |
+
'', // end_time (セッション終了時に更新)
|
| 367 |
+
JSON.stringify(subjects), // subjects
|
| 368 |
+
0, // total_score (セッション終了時に計算)
|
| 369 |
+
false // completed
|
| 370 |
+
];
|
| 371 |
+
sessionsSheet.appendRow(row);
|
| 372 |
+
|
| 373 |
+
return createJsonResponse({
|
| 374 |
+
success: true,
|
| 375 |
+
data: {
|
| 376 |
+
session_id: sessionId,
|
| 377 |
+
user_id: userId,
|
| 378 |
+
subjects: subjects,
|
| 379 |
+
start_time: startTime
|
| 380 |
+
}
|
| 381 |
+
});
|
| 382 |
+
|
| 383 |
+
} catch (error) {
|
| 384 |
+
Logger.log('Error in handleStartSession: ' + error.toString());
|
| 385 |
+
return createErrorResponse('Session Start Failed', error.toString());
|
| 386 |
+
}
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
/**
|
| 390 |
+
* 問題生成エンドポイント(旧版、互換性のため残す)
|
| 391 |
+
* Phase 1: ダミー問題返却(Gemini API統合はPhase 2)
|
| 392 |
+
*
|
| 393 |
+
* @param {Object} params - リクエストパラメータ { session_id: string, subjects: string[] }
|
| 394 |
+
* @returns {TextOutput} - JSON形式のレスポンス
|
| 395 |
+
*/
|
| 396 |
+
function handleGenerateQuestion(params) {
|
| 397 |
+
try {
|
| 398 |
+
Logger.log('handleGenerateQuestion called with params: ' + JSON.stringify(params));
|
| 399 |
+
|
| 400 |
+
// Phase 1: ダミー問題データ
|
| 401 |
+
const dummyQuestion = {
|
| 402 |
+
question_id: 'dummy-question-001',
|
| 403 |
+
session_id: params.session_id || 'dummy-session-001',
|
| 404 |
+
subject: '国語',
|
| 405 |
+
topic: '漢字の読み',
|
| 406 |
+
difficulty: 3,
|
| 407 |
+
question_text: '次の漢字の読みを選びなさい:「薔薇」',
|
| 408 |
+
choices: ['ばら', 'しょうび', 'そうび', 'ばいか'],
|
| 409 |
+
correct_answer: 0,
|
| 410 |
+
created_at: new Date().toISOString()
|
| 411 |
+
};
|
| 412 |
+
|
| 413 |
+
const dummyResponse = {
|
| 414 |
+
success: true,
|
| 415 |
+
message: 'Question generated (dummy)',
|
| 416 |
+
data: {
|
| 417 |
+
question: dummyQuestion
|
| 418 |
+
}
|
| 419 |
+
};
|
| 420 |
+
|
| 421 |
+
return createJsonResponse(dummyResponse);
|
| 422 |
+
} catch (error) {
|
| 423 |
+
Logger.log('Error in handleGenerateQuestion: ' + error.toString());
|
| 424 |
+
return createErrorResponse('Question Generation Failed', error.toString());
|
| 425 |
+
}
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
/**
|
| 429 |
+
* 問題生成エンドポイント(Dify統合版・並列処理対応)
|
| 430 |
+
*
|
| 431 |
+
* @param {Object} params - { session_id: string, subjects: string[] }
|
| 432 |
+
* @returns {TextOutput} - JSON形式のレスポンス
|
| 433 |
+
*/
|
| 434 |
+
function handleGenerateQuestions(params) {
|
| 435 |
+
try {
|
| 436 |
+
Logger.log('handleGenerateQuestions called with params: ' + JSON.stringify(params));
|
| 437 |
+
|
| 438 |
+
const sessionId = params.session_id;
|
| 439 |
+
const subjects = params.subjects || [];
|
| 440 |
+
|
| 441 |
+
if (!sessionId) {
|
| 442 |
+
return createJsonResponse({
|
| 443 |
+
success: false,
|
| 444 |
+
error: 'session_id is required'
|
| 445 |
+
});
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
if (subjects.length === 0) {
|
| 449 |
+
return createJsonResponse({
|
| 450 |
+
success: false,
|
| 451 |
+
error: 'subjects array is required'
|
| 452 |
+
});
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
const allQuestions = [];
|
| 456 |
+
const questionsSheet = getSheet(SHEET_NAMES.QUESTIONS);
|
| 457 |
+
|
| 458 |
+
// === 並列処理: 全教科のDify APIリクエストを同時実行 ===
|
| 459 |
+
Logger.log('Generating questions for ' + subjects.length + ' subjects in PARALLEL');
|
| 460 |
+
const startTime = new Date().getTime();
|
| 461 |
+
|
| 462 |
+
// 全教科のリクエスト設定を準備
|
| 463 |
+
const requestConfigs = subjects.map(function(subject) {
|
| 464 |
+
return {
|
| 465 |
+
action: 'generate_questions',
|
| 466 |
+
subject: subject,
|
| 467 |
+
additionalInputs: {
|
| 468 |
+
statistics: JSON.stringify({}),
|
| 469 |
+
recent_questions: JSON.stringify([])
|
| 470 |
+
}
|
| 471 |
+
};
|
| 472 |
+
});
|
| 473 |
+
|
| 474 |
+
// 並列でDify APIを呼び出し
|
| 475 |
+
const difyResponses = callDifyWorkflowBatch(requestConfigs);
|
| 476 |
+
|
| 477 |
+
const endTime = new Date().getTime();
|
| 478 |
+
Logger.log('Parallel API calls completed in ' + ((endTime - startTime) / 1000) + ' seconds');
|
| 479 |
+
|
| 480 |
+
// === レスポンス処理: 各教科の問題を保存 ===
|
| 481 |
+
for (var i = 0; i < subjects.length; i++) {
|
| 482 |
+
var subject = subjects[i];
|
| 483 |
+
var response = difyResponses[i];
|
| 484 |
+
|
| 485 |
+
try {
|
| 486 |
+
// エラーチェック
|
| 487 |
+
if (response.error) {
|
| 488 |
+
Logger.log('Error in response for ' + subject + ': ' + response.message);
|
| 489 |
+
continue;
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
// Difyレスポンスから問題データを抽出
|
| 493 |
+
var questions = parseQuestionsFromDifyResponse(subject, response);
|
| 494 |
+
|
| 495 |
+
// 各問題をシートに保存し、レスポンス用配列に追加
|
| 496 |
+
for (var j = 0; j < questions.length; j++) {
|
| 497 |
+
var q = questions[j];
|
| 498 |
+
var questionId = generateUUID();
|
| 499 |
+
var createdAt = getTimestamp();
|
| 500 |
+
|
| 501 |
+
// Questionsシートに保存(10列: data-model.md v2.0準拠)
|
| 502 |
+
// Difyは correct_answer を返すが、互換性のため correct もチェック
|
| 503 |
+
var correctAnswer = q.correct_answer !== undefined ? q.correct_answer : q.correct;
|
| 504 |
+
|
| 505 |
+
var row = [
|
| 506 |
+
questionId, // question_id
|
| 507 |
+
sessionId, // session_id
|
| 508 |
+
subject, // subject
|
| 509 |
+
q.category || '', // category
|
| 510 |
+
q.difficulty || '標準', // difficulty (基本/標準/応用)
|
| 511 |
+
q.question, // question_text
|
| 512 |
+
JSON.stringify(q.choices), // choices (JSON)
|
| 513 |
+
correctAnswer, // correct_answer
|
| 514 |
+
q.explanation || '', // explanation
|
| 515 |
+
createdAt // created_at
|
| 516 |
+
];
|
| 517 |
+
questionsSheet.appendRow(row);
|
| 518 |
+
|
| 519 |
+
// レスポンス用に問題データを追加
|
| 520 |
+
allQuestions.push({
|
| 521 |
+
question_id: questionId,
|
| 522 |
+
subject: subject,
|
| 523 |
+
subject_name: SUBJECT_MAP[subject] ? SUBJECT_MAP[subject].name : subject,
|
| 524 |
+
category: q.category || '',
|
| 525 |
+
question: q.question,
|
| 526 |
+
choices: q.choices,
|
| 527 |
+
correct: correctAnswer,
|
| 528 |
+
difficulty: q.difficulty || 3
|
| 529 |
+
});
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
Logger.log('Generated ' + questions.length + ' questions for ' + subject);
|
| 533 |
+
|
| 534 |
+
} catch (subjectError) {
|
| 535 |
+
Logger.log('Error processing questions for ' + subject + ': ' + subjectError.toString());
|
| 536 |
+
// 個別教科のエラーは続行(他の教科は処理する)
|
| 537 |
+
}
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
if (allQuestions.length === 0) {
|
| 541 |
+
return createJsonResponse({
|
| 542 |
+
success: false,
|
| 543 |
+
error: 'Failed to generate questions for any subject'
|
| 544 |
+
});
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
var totalTime = (new Date().getTime() - startTime) / 1000;
|
| 548 |
+
Logger.log('Total question generation time: ' + totalTime + ' seconds for ' + allQuestions.length + ' questions');
|
| 549 |
+
|
| 550 |
+
return createJsonResponse({
|
| 551 |
+
success: true,
|
| 552 |
+
data: {
|
| 553 |
+
session_id: sessionId,
|
| 554 |
+
questions: allQuestions,
|
| 555 |
+
total_count: allQuestions.length
|
| 556 |
+
}
|
| 557 |
+
});
|
| 558 |
+
|
| 559 |
+
} catch (error) {
|
| 560 |
+
Logger.log('Error in handleGenerateQuestions: ' + error.toString());
|
| 561 |
+
return createErrorResponse('Question Generation Failed', error.toString());
|
| 562 |
+
}
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
/**
|
| 566 |
+
* Difyレスポンスから問題データをパース(並列処理用ヘルパー)
|
| 567 |
+
*
|
| 568 |
+
* @param {string} subject - 教科ID
|
| 569 |
+
* @param {Object} response - Dify APIレスポンス
|
| 570 |
+
* @returns {Array} - 問題配列
|
| 571 |
+
*/
|
| 572 |
+
function parseQuestionsFromDifyResponse(subject, response) {
|
| 573 |
+
if (response.data && response.data.outputs) {
|
| 574 |
+
var outputs = response.data.outputs;
|
| 575 |
+
|
| 576 |
+
// エラーチェック
|
| 577 |
+
if (outputs.action_error) {
|
| 578 |
+
throw new Error('Dify action error: ' + outputs.action_error);
|
| 579 |
+
}
|
| 580 |
+
if (outputs.subject_error_questions) {
|
| 581 |
+
throw new Error('Dify subject error: ' + outputs.subject_error_questions);
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
// 教科別の出力変数名を取得(DifyService.jsで定義)
|
| 585 |
+
var outputKeyMap = {
|
| 586 |
+
'jp': 'kokugo_questions',
|
| 587 |
+
'math': 'sansu_questions',
|
| 588 |
+
'sci': 'rika_questions',
|
| 589 |
+
'soc': 'shakai_questions'
|
| 590 |
+
};
|
| 591 |
+
|
| 592 |
+
var outputKey = outputKeyMap[subject];
|
| 593 |
+
if (!outputKey || !outputs[outputKey]) {
|
| 594 |
+
throw new Error('Output key not found: ' + outputKey + ', available: ' + Object.keys(outputs).join(', '));
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
var questionsData = outputs[outputKey];
|
| 598 |
+
|
| 599 |
+
// JSON文字列の場合はパース
|
| 600 |
+
if (typeof questionsData === 'string') {
|
| 601 |
+
var parsed = JSON.parse(questionsData);
|
| 602 |
+
return parsed.questions || parsed;
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
// 既にオブジェクト/配列の場合
|
| 606 |
+
return questionsData.questions || questionsData;
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
throw new Error('Invalid response format from Dify');
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
/**
|
| 613 |
+
* 解答送信エンドポイント(本実装)
|
| 614 |
+
*
|
| 615 |
+
* v1.6.5以降: フロントエンドから送られるcorrect_answerを使用してスコアリング
|
| 616 |
+
*
|
| 617 |
+
* @param {Object} params - {
|
| 618 |
+
* session_id: string,
|
| 619 |
+
* answers: [{
|
| 620 |
+
* question_id,
|
| 621 |
+
* selected_answer,
|
| 622 |
+
* user_answer,
|
| 623 |
+
* correct_answer,
|
| 624 |
+
* subject,
|
| 625 |
+
* category,
|
| 626 |
+
* time_taken_seconds
|
| 627 |
+
* }]
|
| 628 |
+
* }
|
| 629 |
+
* @returns {TextOutput} - JSON形式のレスポンス
|
| 630 |
+
*/
|
| 631 |
+
function handleSubmitAnswers(params) {
|
| 632 |
+
try {
|
| 633 |
+
Logger.log('handleSubmitAnswers called with params: ' + JSON.stringify(params));
|
| 634 |
+
|
| 635 |
+
const sessionId = params.session_id;
|
| 636 |
+
const answers = params.answers || [];
|
| 637 |
+
|
| 638 |
+
if (!sessionId) {
|
| 639 |
+
return createJsonResponse({
|
| 640 |
+
success: false,
|
| 641 |
+
error: 'session_id is required'
|
| 642 |
+
});
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
if (!Array.isArray(answers) || answers.length === 0) {
|
| 646 |
+
return createJsonResponse({
|
| 647 |
+
success: false,
|
| 648 |
+
error: 'answers array is required and must not be empty'
|
| 649 |
+
});
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
const answersSheet = getSheet(SHEET_NAMES.ANSWERS);
|
| 653 |
+
|
| 654 |
+
// 統計用のデータ構造(教科・分野別に集計)
|
| 655 |
+
const statsBySubjectCategory = {};
|
| 656 |
+
let correctCount = 0;
|
| 657 |
+
let totalCount = answers.length;
|
| 658 |
+
|
| 659 |
+
// 各回答を処理
|
| 660 |
+
for (let i = 0; i < answers.length; i++) {
|
| 661 |
+
const answer = answers[i];
|
| 662 |
+
const questionId = answer.question_id;
|
| 663 |
+
const userAnswer = answer.user_answer || answer.selected_answer; // user_answerを優先、なければselected_answer
|
| 664 |
+
const correctAnswer = answer.correct_answer; // フロントエンドから送られる正答
|
| 665 |
+
const subject = answer.subject;
|
| 666 |
+
const category = answer.category;
|
| 667 |
+
const timeTaken = answer.time_taken_seconds || 0;
|
| 668 |
+
|
| 669 |
+
// フロントエンドからcorrect_answerが送られていない場合はエラー
|
| 670 |
+
if (correctAnswer === undefined || correctAnswer === null) {
|
| 671 |
+
Logger.log('Warning: correct_answer missing for question_id: ' + questionId);
|
| 672 |
+
continue;
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
// 正誤判定(フロントエンドから送られるcorrect_answerを使用)
|
| 676 |
+
const isCorrect = (userAnswer === correctAnswer);
|
| 677 |
+
if (isCorrect) {
|
| 678 |
+
correctCount++;
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
// Answersシートに書き込み
|
| 682 |
+
const answerId = generateUUID();
|
| 683 |
+
const answeredAt = getTimestamp();
|
| 684 |
+
const answerRow = [
|
| 685 |
+
answerId, // answer_id
|
| 686 |
+
sessionId, // session_id
|
| 687 |
+
questionId, // question_id
|
| 688 |
+
subject || '', // subject
|
| 689 |
+
category || '', // category
|
| 690 |
+
userAnswer, // user_answer
|
| 691 |
+
correctAnswer, // correct_answer
|
| 692 |
+
isCorrect, // is_correct
|
| 693 |
+
timeTaken, // time_spent
|
| 694 |
+
answeredAt // submitted_at
|
| 695 |
+
];
|
| 696 |
+
answersSheet.appendRow(answerRow);
|
| 697 |
+
|
| 698 |
+
// 統計用データ集計(subject/categoryがある場合のみ)
|
| 699 |
+
if (subject && category) {
|
| 700 |
+
const key = subject + '|' + category;
|
| 701 |
+
if (!statsBySubjectCategory[key]) {
|
| 702 |
+
statsBySubjectCategory[key] = {
|
| 703 |
+
subject: subject,
|
| 704 |
+
category: category,
|
| 705 |
+
attempted: 0,
|
| 706 |
+
correct: 0
|
| 707 |
+
};
|
| 708 |
+
}
|
| 709 |
+
statsBySubjectCategory[key].attempted++;
|
| 710 |
+
if (isCorrect) {
|
| 711 |
+
statsBySubjectCategory[key].correct++;
|
| 712 |
+
}
|
| 713 |
+
}
|
| 714 |
+
}
|
| 715 |
+
|
| 716 |
+
// スコア計算(0-100)
|
| 717 |
+
const score = Math.round((correctCount / totalCount) * 100);
|
| 718 |
+
|
| 719 |
+
// Sessionsシート更新(total_score, end_time, completed)
|
| 720 |
+
const sessionsSheet = getSheet(SHEET_NAMES.SESSIONS);
|
| 721 |
+
const sessionsData = sessionsSheet.getDataRange().getValues();
|
| 722 |
+
for (let i = 1; i < sessionsData.length; i++) {
|
| 723 |
+
if (sessionsData[i][0] === sessionId) {
|
| 724 |
+
const endTime = getTimestamp();
|
| 725 |
+
sessionsSheet.getRange(i + 1, 4).setValue(endTime); // end_time
|
| 726 |
+
sessionsSheet.getRange(i + 1, 6).setValue(score); // total_score
|
| 727 |
+
sessionsSheet.getRange(i + 1, 7).setValue(true); // completed
|
| 728 |
+
break;
|
| 729 |
+
}
|
| 730 |
+
}
|
| 731 |
+
|
| 732 |
+
// Statisticsシート更新
|
| 733 |
+
const userId = getUserIdFromSession(sessionId);
|
| 734 |
+
if (userId) {
|
| 735 |
+
updateStatistics(userId, statsBySubjectCategory);
|
| 736 |
+
|
| 737 |
+
// Usersシートのtotal_sessionsとtotal_questionsを更新
|
| 738 |
+
updateUserStats(userId, 1, totalCount);
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
// デバッグログ (v1.6.12)
|
| 742 |
+
console.log('[handleSubmitAnswers] Response data:', JSON.stringify({
|
| 743 |
+
session_id: sessionId,
|
| 744 |
+
total: totalCount,
|
| 745 |
+
correct: correctCount,
|
| 746 |
+
accuracy: score,
|
| 747 |
+
answers_saved: answers.length
|
| 748 |
+
}));
|
| 749 |
+
|
| 750 |
+
return createJsonResponse({
|
| 751 |
+
success: true,
|
| 752 |
+
data: {
|
| 753 |
+
session_id: sessionId,
|
| 754 |
+
total: totalCount,
|
| 755 |
+
correct: correctCount,
|
| 756 |
+
accuracy: score,
|
| 757 |
+
answers_saved: answers.length
|
| 758 |
+
}
|
| 759 |
+
});
|
| 760 |
+
|
| 761 |
+
} catch (error) {
|
| 762 |
+
Logger.log('Error in handleSubmitAnswers: ' + error.toString());
|
| 763 |
+
return createErrorResponse('Submit Answers Failed', error.toString());
|
| 764 |
+
}
|
| 765 |
+
}
|
| 766 |
+
|
| 767 |
+
/**
|
| 768 |
+
* 統計取得エンドポイント(ジャンル別累積集計対応版)
|
| 769 |
+
*
|
| 770 |
+
* @param {Object} params - { user_id: string, subjects?: string[] }
|
| 771 |
+
* @returns {TextOutput} - JSON形式のレスポンス
|
| 772 |
+
*
|
| 773 |
+
* レスポンス形式:
|
| 774 |
+
* {
|
| 775 |
+
* success: true,
|
| 776 |
+
* data: {
|
| 777 |
+
* user_id: string,
|
| 778 |
+
* subjects: [
|
| 779 |
+
* {
|
| 780 |
+
* subject: string,
|
| 781 |
+
* subject_name: string,
|
| 782 |
+
* total_attempted: number,
|
| 783 |
+
* total_correct: number,
|
| 784 |
+
* overall_accuracy: number,
|
| 785 |
+
* genres: [
|
| 786 |
+
* { genre_id: string, genre_name: string, attempted: number, correct: number, accuracy: number },
|
| 787 |
+
* ...
|
| 788 |
+
* ]
|
| 789 |
+
* },
|
| 790 |
+
* ...
|
| 791 |
+
* ],
|
| 792 |
+
* cumulative: {
|
| 793 |
+
* total_sessions: number,
|
| 794 |
+
* total_questions: number,
|
| 795 |
+
* total_correct: number,
|
| 796 |
+
* overall_accuracy: number
|
| 797 |
+
* }
|
| 798 |
+
* }
|
| 799 |
+
* }
|
| 800 |
+
*/
|
| 801 |
+
function handleGetStatistics(params) {
|
| 802 |
+
try {
|
| 803 |
+
Logger.log('handleGetStatistics called with params: ' + JSON.stringify(params));
|
| 804 |
+
|
| 805 |
+
const userId = params.user_id;
|
| 806 |
+
const subjects = params.subjects || null;
|
| 807 |
+
|
| 808 |
+
if (!userId) {
|
| 809 |
+
return createJsonResponse({
|
| 810 |
+
success: false,
|
| 811 |
+
error: 'user_id is required'
|
| 812 |
+
});
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
const statisticsSheet = getSheet(SHEET_NAMES.STATISTICS);
|
| 816 |
+
const data = statisticsSheet.getDataRange().getValues();
|
| 817 |
+
|
| 818 |
+
// ヘッダー行をスキップして該当ユーザーの統計を取得
|
| 819 |
+
const userStats = [];
|
| 820 |
+
for (let i = 1; i < data.length; i++) {
|
| 821 |
+
const row = data[i];
|
| 822 |
+
const rowUserId = row[1]; // user_id
|
| 823 |
+
const rowSubject = row[2]; // subject
|
| 824 |
+
|
| 825 |
+
// user_idが一致し、subjects指定がある場合はそれも確認
|
| 826 |
+
if (rowUserId === userId) {
|
| 827 |
+
if (!subjects || subjects.includes(rowSubject)) {
|
| 828 |
+
userStats.push({
|
| 829 |
+
stat_id: row[0], // stat_id
|
| 830 |
+
user_id: row[1], // user_id
|
| 831 |
+
subject: row[2], // subject
|
| 832 |
+
category: row[3], // category(ジャンルID)
|
| 833 |
+
total_attempted: row[4], // total_attempted
|
| 834 |
+
correct_count: row[5], // correct_count
|
| 835 |
+
accuracy_rate: row[6], // accuracy_rate
|
| 836 |
+
last_updated: row[7] // last_updated
|
| 837 |
+
});
|
| 838 |
+
}
|
| 839 |
+
}
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
// 教科別・ジャンル別にグループ化
|
| 843 |
+
const bySubject = {};
|
| 844 |
+
let cumulativeTotalAttempted = 0;
|
| 845 |
+
let cumulativeTotalCorrect = 0;
|
| 846 |
+
|
| 847 |
+
for (let i = 0; i < userStats.length; i++) {
|
| 848 |
+
const stat = userStats[i];
|
| 849 |
+
const subject = stat.subject;
|
| 850 |
+
const genreId = stat.category; // categoryフィールドをジャンルIDとして扱う
|
| 851 |
+
|
| 852 |
+
if (!bySubject[subject]) {
|
| 853 |
+
bySubject[subject] = {
|
| 854 |
+
subject: subject,
|
| 855 |
+
subject_name: SUBJECT_MAP[subject] ? SUBJECT_MAP[subject].name : subject,
|
| 856 |
+
genres: [],
|
| 857 |
+
total_attempted: 0,
|
| 858 |
+
total_correct: 0,
|
| 859 |
+
overall_accuracy: 0
|
| 860 |
+
};
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
// GenreMaster.jsからジャンル情報を取得
|
| 864 |
+
const genreInfo = getGenreById(genreId);
|
| 865 |
+
const genreName = genreInfo ? genreInfo.name : '不明';
|
| 866 |
+
|
| 867 |
+
bySubject[subject].genres.push({
|
| 868 |
+
genre_id: genreId,
|
| 869 |
+
genre_name: genreName,
|
| 870 |
+
attempted: stat.total_attempted,
|
| 871 |
+
correct: stat.correct_count,
|
| 872 |
+
accuracy: stat.accuracy_rate
|
| 873 |
+
});
|
| 874 |
+
|
| 875 |
+
bySubject[subject].total_attempted += stat.total_attempted;
|
| 876 |
+
bySubject[subject].total_correct += stat.correct_count;
|
| 877 |
+
|
| 878 |
+
// 累積統計用
|
| 879 |
+
cumulativeTotalAttempted += stat.total_attempted;
|
| 880 |
+
cumulativeTotalCorrect += stat.correct_count;
|
| 881 |
+
}
|
| 882 |
+
|
| 883 |
+
// 教科別の正答率を計算
|
| 884 |
+
const subjectsArray = [];
|
| 885 |
+
for (const subjectId in bySubject) {
|
| 886 |
+
const subjectData = bySubject[subjectId];
|
| 887 |
+
if (subjectData.total_attempted > 0) {
|
| 888 |
+
subjectData.overall_accuracy = subjectData.total_correct / subjectData.total_attempted;
|
| 889 |
+
}
|
| 890 |
+
|
| 891 |
+
// ジャンルを正答率の低い順にソート(苦手なジャンル順)
|
| 892 |
+
subjectData.genres = sortByWeakness(subjectData.genres);
|
| 893 |
+
|
| 894 |
+
subjectsArray.push(subjectData);
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
// レーダーチャート用データ(教科別正答率)
|
| 898 |
+
const radarChartData = {
|
| 899 |
+
labels: [],
|
| 900 |
+
values: []
|
| 901 |
+
};
|
| 902 |
+
for (let i = 0; i < subjectsArray.length; i++) {
|
| 903 |
+
radarChartData.labels.push(subjectsArray[i].subject_name);
|
| 904 |
+
radarChartData.values.push(Math.round(subjectsArray[i].overall_accuracy * 100));
|
| 905 |
+
}
|
| 906 |
+
|
| 907 |
+
// セッション数を取得
|
| 908 |
+
const totalSessions = getUserSessionCount(userId);
|
| 909 |
+
|
| 910 |
+
// 累積統計
|
| 911 |
+
const cumulativeStats = {
|
| 912 |
+
total_sessions: totalSessions,
|
| 913 |
+
total_questions: cumulativeTotalAttempted,
|
| 914 |
+
total_correct: cumulativeTotalCorrect,
|
| 915 |
+
overall_accuracy: cumulativeTotalAttempted > 0 ? cumulativeTotalCorrect / cumulativeTotalAttempted : 0
|
| 916 |
+
};
|
| 917 |
+
|
| 918 |
+
return createJsonResponse({
|
| 919 |
+
success: true,
|
| 920 |
+
data: {
|
| 921 |
+
user_id: userId,
|
| 922 |
+
subjects: subjectsArray,
|
| 923 |
+
radar_chart: radarChartData,
|
| 924 |
+
cumulative: cumulativeStats
|
| 925 |
+
}
|
| 926 |
+
});
|
| 927 |
+
|
| 928 |
+
} catch (error) {
|
| 929 |
+
Logger.log('Error in handleGetStatistics: ' + error.toString());
|
| 930 |
+
return createErrorResponse('Get Statistics Failed', error.toString());
|
| 931 |
+
}
|
| 932 |
+
}
|
| 933 |
+
|
| 934 |
+
/**
|
| 935 |
+
* 評価取得エンドポイント(本実装)
|
| 936 |
+
*
|
| 937 |
+
* @param {Object} params - { session_id: string, subjects?: string[] }
|
| 938 |
+
* @returns {TextOutput} - JSON形式のレスポンス
|
| 939 |
+
*
|
| 940 |
+
* 処理フロー:
|
| 941 |
+
* 1. Statisticsシートから統計データを取得
|
| 942 |
+
* 2. Answers/Questionsシートから解答結果を取得
|
| 943 |
+
* 3. 各教科に対してDify評価生成を呼び出し
|
| 944 |
+
* 4. 全体評価も生成(すべての教科の結果を統合)
|
| 945 |
+
* 5. Evaluationsシートへ書き込み
|
| 946 |
+
*/
|
| 947 |
+
function handleGetEvaluation(params) {
|
| 948 |
+
try {
|
| 949 |
+
Logger.log('handleGetEvaluation called with params: ' + JSON.stringify(params));
|
| 950 |
+
|
| 951 |
+
const sessionId = params.session_id;
|
| 952 |
+
const requestedSubjects = params.subjects || null;
|
| 953 |
+
|
| 954 |
+
if (!sessionId) {
|
| 955 |
+
return createJsonResponse({
|
| 956 |
+
success: false,
|
| 957 |
+
error: 'session_id is required'
|
| 958 |
+
});
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
// セッション情報取得
|
| 962 |
+
const userId = getUserIdFromSession(sessionId);
|
| 963 |
+
if (!userId) {
|
| 964 |
+
return createJsonResponse({
|
| 965 |
+
success: false,
|
| 966 |
+
error: 'Session not found'
|
| 967 |
+
});
|
| 968 |
+
}
|
| 969 |
+
|
| 970 |
+
// 統計データ取得
|
| 971 |
+
const statisticsSheet = getSheet(SHEET_NAMES.STATISTICS);
|
| 972 |
+
const statsData = statisticsSheet.getDataRange().getValues();
|
| 973 |
+
const userStatistics = {};
|
| 974 |
+
|
| 975 |
+
for (let i = 1; i < statsData.length; i++) {
|
| 976 |
+
const row = statsData[i];
|
| 977 |
+
const statUserId = row[1];
|
| 978 |
+
const statSubject = row[2];
|
| 979 |
+
|
| 980 |
+
if (statUserId === userId) {
|
| 981 |
+
if (!userStatistics[statSubject]) {
|
| 982 |
+
userStatistics[statSubject] = {
|
| 983 |
+
subject: statSubject,
|
| 984 |
+
categories: []
|
| 985 |
+
};
|
| 986 |
+
}
|
| 987 |
+
userStatistics[statSubject].categories.push({
|
| 988 |
+
category: row[3],
|
| 989 |
+
total_attempted: row[4],
|
| 990 |
+
correct_count: row[5],
|
| 991 |
+
accuracy_rate: row[6]
|
| 992 |
+
});
|
| 993 |
+
}
|
| 994 |
+
}
|
| 995 |
+
|
| 996 |
+
// セッションの解答結果を取得
|
| 997 |
+
const sessionResults = getSessionResults(sessionId);
|
| 998 |
+
|
| 999 |
+
if (sessionResults.length === 0) {
|
| 1000 |
+
return createJsonResponse({
|
| 1001 |
+
success: false,
|
| 1002 |
+
error: 'No answers found for this session'
|
| 1003 |
+
});
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
// 教科別にグループ化
|
| 1007 |
+
const resultsBySubject = {};
|
| 1008 |
+
for (let i = 0; i < sessionResults.length; i++) {
|
| 1009 |
+
const result = sessionResults[i];
|
| 1010 |
+
const subject = result.subject;
|
| 1011 |
+
|
| 1012 |
+
if (!resultsBySubject[subject]) {
|
| 1013 |
+
resultsBySubject[subject] = [];
|
| 1014 |
+
}
|
| 1015 |
+
resultsBySubject[subject].push(result);
|
| 1016 |
+
}
|
| 1017 |
+
|
| 1018 |
+
const evaluations = [];
|
| 1019 |
+
const evaluationsSheet = getSheet(SHEET_NAMES.EVALUATIONS);
|
| 1020 |
+
|
| 1021 |
+
// 教科別評価を収集するオブジェクト(全体評価用)
|
| 1022 |
+
const subjectEvaluations = {};
|
| 1023 |
+
|
| 1024 |
+
// 各教科の評価を生成
|
| 1025 |
+
const subjects = requestedSubjects || Object.keys(resultsBySubject);
|
| 1026 |
+
for (let i = 0; i < subjects.length; i++) {
|
| 1027 |
+
const subject = subjects[i];
|
| 1028 |
+
const results = resultsBySubject[subject] || [];
|
| 1029 |
+
|
| 1030 |
+
if (results.length === 0) {
|
| 1031 |
+
Logger.log('No results found for subject: ' + subject);
|
| 1032 |
+
continue;
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
try {
|
| 1036 |
+
// Dify APIで評価生成
|
| 1037 |
+
const statistics = userStatistics[subject] || { subject: subject, categories: [] };
|
| 1038 |
+
const evaluation = generateEvaluationFromDify(subject, statistics, results);
|
| 1039 |
+
|
| 1040 |
+
// 教科別評価を収集(全体評価生成時に使用)
|
| 1041 |
+
subjectEvaluations[subject] = {
|
| 1042 |
+
subject: subject,
|
| 1043 |
+
subject_name: SUBJECT_MAP[subject] ? SUBJECT_MAP[subject].name : subject,
|
| 1044 |
+
advice: evaluation.advice || '',
|
| 1045 |
+
strengths: evaluation.strengths || [],
|
| 1046 |
+
weaknesses: evaluation.weaknesses || [],
|
| 1047 |
+
recommended_topics: evaluation.recommended_topics || []
|
| 1048 |
+
};
|
| 1049 |
+
|
| 1050 |
+
// Evaluationsシートに書き込み
|
| 1051 |
+
const evaluationId = generateUUID();
|
| 1052 |
+
const createdAt = getTimestamp();
|
| 1053 |
+
|
| 1054 |
+
// 教科別評価: evaluation_id, session_id, subject, advice, strengths, weaknesses, recommended_topics, created_at
|
| 1055 |
+
const strengthsJson = JSON.stringify(evaluation.strengths || []);
|
| 1056 |
+
const weaknessesJson = JSON.stringify(evaluation.weaknesses || []);
|
| 1057 |
+
const recommendedTopicsJson = JSON.stringify(evaluation.recommended_topics || []);
|
| 1058 |
+
|
| 1059 |
+
const row = [
|
| 1060 |
+
evaluationId,
|
| 1061 |
+
sessionId,
|
| 1062 |
+
subject,
|
| 1063 |
+
evaluation.advice || '',
|
| 1064 |
+
strengthsJson,
|
| 1065 |
+
weaknessesJson,
|
| 1066 |
+
recommendedTopicsJson,
|
| 1067 |
+
createdAt
|
| 1068 |
+
];
|
| 1069 |
+
evaluationsSheet.appendRow(row);
|
| 1070 |
+
|
| 1071 |
+
evaluations.push({
|
| 1072 |
+
evaluation_id: evaluationId,
|
| 1073 |
+
subject: subject,
|
| 1074 |
+
subject_name: SUBJECT_MAP[subject] ? SUBJECT_MAP[subject].name : subject,
|
| 1075 |
+
advice: evaluation.advice || '',
|
| 1076 |
+
strengths: evaluation.strengths || [],
|
| 1077 |
+
weaknesses: evaluation.weaknesses || [],
|
| 1078 |
+
recommended_topics: evaluation.recommended_topics || [],
|
| 1079 |
+
created_at: createdAt
|
| 1080 |
+
});
|
| 1081 |
+
|
| 1082 |
+
Logger.log('Evaluation generated for ' + subject);
|
| 1083 |
+
|
| 1084 |
+
} catch (subjectError) {
|
| 1085 |
+
Logger.log('Error generating evaluation for ' + subject + ': ' + subjectError.toString());
|
| 1086 |
+
// 個別教科のエラーは続行
|
| 1087 |
+
}
|
| 1088 |
+
}
|
| 1089 |
+
|
| 1090 |
+
// 全体評価を生成(複数教科の場合)
|
| 1091 |
+
if (subjects.length > 1) {
|
| 1092 |
+
try {
|
| 1093 |
+
// 全統計・全結果を統合
|
| 1094 |
+
const overallStatistics = {
|
| 1095 |
+
subjects: userStatistics
|
| 1096 |
+
};
|
| 1097 |
+
const overallResults = sessionResults;
|
| 1098 |
+
|
| 1099 |
+
// 教科別評価も渡す(第4引数)
|
| 1100 |
+
const overallEvaluation = generateEvaluationFromDify('overall', overallStatistics, overallResults, subjectEvaluations);
|
| 1101 |
+
|
| 1102 |
+
// Evaluationsシートに書き込み(subject = 'overall')
|
| 1103 |
+
const evaluationId = generateUUID();
|
| 1104 |
+
const createdAt = getTimestamp();
|
| 1105 |
+
|
| 1106 |
+
const strengthsJson = JSON.stringify(overallEvaluation.strengths || []);
|
| 1107 |
+
const weaknessesJson = JSON.stringify(overallEvaluation.weaknesses || []);
|
| 1108 |
+
const nextStepsJson = JSON.stringify(overallEvaluation.next_steps || []);
|
| 1109 |
+
|
| 1110 |
+
const row = [
|
| 1111 |
+
evaluationId,
|
| 1112 |
+
sessionId,
|
| 1113 |
+
'overall',
|
| 1114 |
+
overallEvaluation.overall_advice || '',
|
| 1115 |
+
strengthsJson,
|
| 1116 |
+
weaknessesJson,
|
| 1117 |
+
nextStepsJson,
|
| 1118 |
+
createdAt
|
| 1119 |
+
];
|
| 1120 |
+
evaluationsSheet.appendRow(row);
|
| 1121 |
+
|
| 1122 |
+
evaluations.push({
|
| 1123 |
+
evaluation_id: evaluationId,
|
| 1124 |
+
subject: 'overall',
|
| 1125 |
+
subject_name: '全体評価',
|
| 1126 |
+
advice: overallEvaluation.overall_advice || '',
|
| 1127 |
+
strengths: overallEvaluation.strengths || [],
|
| 1128 |
+
weaknesses: overallEvaluation.weaknesses || [],
|
| 1129 |
+
next_steps: overallEvaluation.next_steps || [],
|
| 1130 |
+
created_at: createdAt
|
| 1131 |
+
});
|
| 1132 |
+
|
| 1133 |
+
Logger.log('Overall evaluation generated');
|
| 1134 |
+
|
| 1135 |
+
} catch (overallError) {
|
| 1136 |
+
Logger.log('Error generating overall evaluation: ' + overallError.toString());
|
| 1137 |
+
// 全体評価のエラーは無視(教科別評価は返す)
|
| 1138 |
+
}
|
| 1139 |
+
}
|
| 1140 |
+
|
| 1141 |
+
return createJsonResponse({
|
| 1142 |
+
success: true,
|
| 1143 |
+
data: {
|
| 1144 |
+
session_id: sessionId,
|
| 1145 |
+
evaluations: evaluations,
|
| 1146 |
+
total_count: evaluations.length
|
| 1147 |
+
}
|
| 1148 |
+
});
|
| 1149 |
+
|
| 1150 |
+
} catch (error) {
|
| 1151 |
+
Logger.log('Error in handleGetEvaluation: ' + error.toString());
|
| 1152 |
+
return createErrorResponse('Get Evaluation Failed', error.toString());
|
| 1153 |
+
}
|
| 1154 |
+
}
|
| 1155 |
+
|
| 1156 |
+
/**
|
| 1157 |
+
* 問題保存エンドポイント(v1.6.5新規)
|
| 1158 |
+
* 生成された問題をGeneratedQuestionsシートに保存
|
| 1159 |
+
*
|
| 1160 |
+
* @param {Object} params - { session_id: string, subject: string, questions: Array }
|
| 1161 |
+
* @returns {TextOutput} - JSON形式のレスポンス
|
| 1162 |
+
*/
|
| 1163 |
+
function handleSaveQuestions(params) {
|
| 1164 |
+
try {
|
| 1165 |
+
Logger.log('handleSaveQuestions called with params: ' + JSON.stringify(params));
|
| 1166 |
+
|
| 1167 |
+
const sessionId = params.session_id;
|
| 1168 |
+
const subject = params.subject;
|
| 1169 |
+
const questions = params.questions || [];
|
| 1170 |
+
|
| 1171 |
+
if (!sessionId) {
|
| 1172 |
+
return createJsonResponse({
|
| 1173 |
+
success: false,
|
| 1174 |
+
error: 'session_id is required'
|
| 1175 |
+
});
|
| 1176 |
+
}
|
| 1177 |
+
|
| 1178 |
+
if (!subject) {
|
| 1179 |
+
return createJsonResponse({
|
| 1180 |
+
success: false,
|
| 1181 |
+
error: 'subject is required'
|
| 1182 |
+
});
|
| 1183 |
+
}
|
| 1184 |
+
|
| 1185 |
+
if (!Array.isArray(questions) || questions.length === 0) {
|
| 1186 |
+
return createJsonResponse({
|
| 1187 |
+
success: false,
|
| 1188 |
+
error: 'questions array is required and must not be empty'
|
| 1189 |
+
});
|
| 1190 |
+
}
|
| 1191 |
+
|
| 1192 |
+
// GeneratedQuestionsシート取得(なければ作成)
|
| 1193 |
+
const sheet = getOrCreateSheet(SHEET_NAMES.GENERATED_QUESTIONS, [
|
| 1194 |
+
'question_id',
|
| 1195 |
+
'session_id',
|
| 1196 |
+
'subject',
|
| 1197 |
+
'genre_id',
|
| 1198 |
+
'answer',
|
| 1199 |
+
'question_text',
|
| 1200 |
+
'choices',
|
| 1201 |
+
'correct_answer',
|
| 1202 |
+
'difficulty',
|
| 1203 |
+
'created_at'
|
| 1204 |
+
]);
|
| 1205 |
+
|
| 1206 |
+
const timestamp = getTimestamp();
|
| 1207 |
+
const savedQuestions = [];
|
| 1208 |
+
|
| 1209 |
+
// 各問題を保存
|
| 1210 |
+
for (let i = 0; i < questions.length; i++) {
|
| 1211 |
+
const q = questions[i];
|
| 1212 |
+
const questionId = q.question_id || generateUUID();
|
| 1213 |
+
|
| 1214 |
+
// 行データ作成
|
| 1215 |
+
const row = [
|
| 1216 |
+
questionId, // question_id
|
| 1217 |
+
sessionId, // session_id
|
| 1218 |
+
subject, // subject
|
| 1219 |
+
q.genre_id || q.category || '', // genre_id
|
| 1220 |
+
q.answer || '', // answer
|
| 1221 |
+
q.question || q.question_text || '', // question_text
|
| 1222 |
+
JSON.stringify(q.choices || []), // choices (JSON)
|
| 1223 |
+
q.correct_answer !== undefined ? q.correct_answer : q.correct, // correct_answer
|
| 1224 |
+
q.difficulty || '標準', // difficulty
|
| 1225 |
+
timestamp // created_at
|
| 1226 |
+
];
|
| 1227 |
+
|
| 1228 |
+
sheet.appendRow(row);
|
| 1229 |
+
|
| 1230 |
+
// レスポンス用データ
|
| 1231 |
+
savedQuestions.push({
|
| 1232 |
+
question_id: questionId,
|
| 1233 |
+
subject: subject,
|
| 1234 |
+
genre_id: q.genre_id || q.category || '',
|
| 1235 |
+
question: q.question || q.question_text || ''
|
| 1236 |
+
});
|
| 1237 |
+
|
| 1238 |
+
Logger.log('Saved question: ' + questionId + ' for subject: ' + subject);
|
| 1239 |
+
}
|
| 1240 |
+
|
| 1241 |
+
return createJsonResponse({
|
| 1242 |
+
success: true,
|
| 1243 |
+
data: {
|
| 1244 |
+
questions: savedQuestions,
|
| 1245 |
+
saved_count: savedQuestions.length
|
| 1246 |
+
}
|
| 1247 |
+
});
|
| 1248 |
+
|
| 1249 |
+
} catch (error) {
|
| 1250 |
+
Logger.log('Error in handleSaveQuestions: ' + error.toString());
|
| 1251 |
+
return createErrorResponse('Save Questions Failed', error.toString());
|
| 1252 |
+
}
|
| 1253 |
+
}
|
| 1254 |
+
|
| 1255 |
+
/**
|
| 1256 |
+
* サマリー保存エンドポイント(v1.6.5新規)
|
| 1257 |
+
* 問題要約をSummariesシートに保存
|
| 1258 |
+
*
|
| 1259 |
+
* @param {Object} params - { session_id: string, subject: string, summary_data: Object }
|
| 1260 |
+
* @returns {TextOutput} - JSON形式のレスポンス
|
| 1261 |
+
*/
|
| 1262 |
+
function handleSaveSummary(params) {
|
| 1263 |
+
try {
|
| 1264 |
+
Logger.log('handleSaveSummary called with params: ' + JSON.stringify(params));
|
| 1265 |
+
|
| 1266 |
+
const sessionId = params.session_id;
|
| 1267 |
+
const subject = params.subject;
|
| 1268 |
+
const summaryData = params.summary_data || {};
|
| 1269 |
+
|
| 1270 |
+
if (!sessionId) {
|
| 1271 |
+
return createJsonResponse({
|
| 1272 |
+
success: false,
|
| 1273 |
+
error: 'session_id is required'
|
| 1274 |
+
});
|
| 1275 |
+
}
|
| 1276 |
+
|
| 1277 |
+
if (!subject) {
|
| 1278 |
+
return createJsonResponse({
|
| 1279 |
+
success: false,
|
| 1280 |
+
error: 'subject is required'
|
| 1281 |
+
});
|
| 1282 |
+
}
|
| 1283 |
+
|
| 1284 |
+
// Summariesシート取得(なければ作成)
|
| 1285 |
+
const sheet = getOrCreateSheet(SHEET_NAMES.SUMMARIES, [
|
| 1286 |
+
'summary_id',
|
| 1287 |
+
'session_id',
|
| 1288 |
+
'subject',
|
| 1289 |
+
'keywords',
|
| 1290 |
+
'topics',
|
| 1291 |
+
'summary',
|
| 1292 |
+
'created_at'
|
| 1293 |
+
]);
|
| 1294 |
+
|
| 1295 |
+
const summaryId = generateUUID();
|
| 1296 |
+
const timestamp = getTimestamp();
|
| 1297 |
+
|
| 1298 |
+
// 行データ作成
|
| 1299 |
+
const row = [
|
| 1300 |
+
summaryId, // summary_id
|
| 1301 |
+
sessionId, // session_id
|
| 1302 |
+
subject, // subject
|
| 1303 |
+
JSON.stringify(summaryData.keywords || []), // keywords (JSON)
|
| 1304 |
+
JSON.stringify(summaryData.topics || []), // topics (JSON)
|
| 1305 |
+
summaryData.summary || '', // summary
|
| 1306 |
+
timestamp // created_at
|
| 1307 |
+
];
|
| 1308 |
+
|
| 1309 |
+
sheet.appendRow(row);
|
| 1310 |
+
|
| 1311 |
+
Logger.log('Saved summary: ' + summaryId + ' for session: ' + sessionId + ', subject: ' + subject);
|
| 1312 |
+
|
| 1313 |
+
return createJsonResponse({
|
| 1314 |
+
success: true,
|
| 1315 |
+
data: {
|
| 1316 |
+
summary_id: summaryId,
|
| 1317 |
+
session_id: sessionId,
|
| 1318 |
+
subject: subject
|
| 1319 |
+
}
|
| 1320 |
+
});
|
| 1321 |
+
|
| 1322 |
+
} catch (error) {
|
| 1323 |
+
Logger.log('Error in handleSaveSummary: ' + error.toString());
|
| 1324 |
+
return createErrorResponse('Save Summary Failed', error.toString());
|
| 1325 |
+
}
|
| 1326 |
+
}
|
| 1327 |
+
|
| 1328 |
+
// ============================================================================
|
| 1329 |
+
// ユーティリティ関数
|
| 1330 |
+
// ============================================================================
|
| 1331 |
+
|
| 1332 |
+
/**
|
| 1333 |
+
* UsersシートのユーザーStatistics(total_sessions, total_questions)を更新
|
| 1334 |
+
*
|
| 1335 |
+
* @param {string} userId - ユーザーID
|
| 1336 |
+
* @param {number} sessionsIncrement - セッション数の増分(通常1)
|
| 1337 |
+
* @param {number} questionsIncrement - 問題数の増分
|
| 1338 |
+
*/
|
| 1339 |
+
function updateUserStats(userId, sessionsIncrement, questionsIncrement) {
|
| 1340 |
+
try {
|
| 1341 |
+
const usersSheet = getSheet(SHEET_NAMES.USERS);
|
| 1342 |
+
const data = usersSheet.getDataRange().getValues();
|
| 1343 |
+
|
| 1344 |
+
// ユーザーを検索して更新
|
| 1345 |
+
for (let i = 1; i < data.length; i++) {
|
| 1346 |
+
if (data[i][0] === userId) { // user_id (列0)
|
| 1347 |
+
const currentSessions = data[i][4] || 0; // total_sessions (列4 = E列)
|
| 1348 |
+
const currentQuestions = data[i][5] || 0; // total_questions (列5 = F列)
|
| 1349 |
+
|
| 1350 |
+
const newSessions = currentSessions + sessionsIncrement;
|
| 1351 |
+
const newQuestions = currentQuestions + questionsIncrement;
|
| 1352 |
+
|
| 1353 |
+
// E列(total_sessions)とF列(total_questions)を更新
|
| 1354 |
+
usersSheet.getRange(i + 1, 5).setValue(newSessions); // 列5 = E列
|
| 1355 |
+
usersSheet.getRange(i + 1, 6).setValue(newQuestions); // 列6 = F列
|
| 1356 |
+
|
| 1357 |
+
Logger.log('Updated user stats for ' + userId + ': sessions=' + newSessions + ', questions=' + newQuestions);
|
| 1358 |
+
return;
|
| 1359 |
+
}
|
| 1360 |
+
}
|
| 1361 |
+
|
| 1362 |
+
Logger.log('User not found for stats update: ' + userId);
|
| 1363 |
+
|
| 1364 |
+
} catch (error) {
|
| 1365 |
+
Logger.log('Error in updateUserStats: ' + error.toString());
|
| 1366 |
+
}
|
| 1367 |
+
}
|
| 1368 |
+
|
| 1369 |
+
/**
|
| 1370 |
+
* セッションIDからユーザーIDを取得
|
| 1371 |
+
*
|
| 1372 |
+
* @param {string} sessionId - セッションID
|
| 1373 |
+
* @returns {string|null} - ユーザーID(見つからない場合null)
|
| 1374 |
+
*/
|
| 1375 |
+
function getUserIdFromSession(sessionId) {
|
| 1376 |
+
try {
|
| 1377 |
+
const sessionsSheet = getSheet(SHEET_NAMES.SESSIONS);
|
| 1378 |
+
const data = sessionsSheet.getDataRange().getValues();
|
| 1379 |
+
|
| 1380 |
+
for (let i = 1; i < data.length; i++) {
|
| 1381 |
+
if (data[i][0] === sessionId) { // session_id
|
| 1382 |
+
return data[i][1]; // user_id
|
| 1383 |
+
}
|
| 1384 |
+
}
|
| 1385 |
+
return null;
|
| 1386 |
+
} catch (error) {
|
| 1387 |
+
Logger.log('Error in getUserIdFromSession: ' + error.toString());
|
| 1388 |
+
return null;
|
| 1389 |
+
}
|
| 1390 |
+
}
|
| 1391 |
+
|
| 1392 |
+
/**
|
| 1393 |
+
* ユーザーのセッション総数を取得
|
| 1394 |
+
*
|
| 1395 |
+
* @param {string} userId - ユーザーID
|
| 1396 |
+
* @returns {number} - セッション総数
|
| 1397 |
+
*/
|
| 1398 |
+
function getUserSessionCount(userId) {
|
| 1399 |
+
try {
|
| 1400 |
+
const sessionsSheet = getSheet(SHEET_NAMES.SESSIONS);
|
| 1401 |
+
const data = sessionsSheet.getDataRange().getValues();
|
| 1402 |
+
|
| 1403 |
+
let count = 0;
|
| 1404 |
+
for (let i = 1; i < data.length; i++) {
|
| 1405 |
+
if (data[i][1] === userId) { // user_id
|
| 1406 |
+
count++;
|
| 1407 |
+
}
|
| 1408 |
+
}
|
| 1409 |
+
return count;
|
| 1410 |
+
} catch (error) {
|
| 1411 |
+
Logger.log('Error in getUserSessionCount: ' + error.toString());
|
| 1412 |
+
return 0;
|
| 1413 |
+
}
|
| 1414 |
+
}
|
| 1415 |
+
|
| 1416 |
+
/**
|
| 1417 |
+
* ジャンル統計を苦手順(正答率昇順)にソート
|
| 1418 |
+
*
|
| 1419 |
+
* @param {Array} genreStats - ジャンル統計配列 [{ genre_id, genre_name, attempted, correct, accuracy }]
|
| 1420 |
+
* @returns {Array} - 正答率昇順でソートされた配列
|
| 1421 |
+
*/
|
| 1422 |
+
function sortByWeakness(genreStats) {
|
| 1423 |
+
if (!Array.isArray(genreStats)) {
|
| 1424 |
+
return [];
|
| 1425 |
+
}
|
| 1426 |
+
|
| 1427 |
+
return genreStats.sort(function(a, b) {
|
| 1428 |
+
return a.accuracy - b.accuracy; // 正答率昇順(苦手なジャンルが上位)
|
| 1429 |
+
});
|
| 1430 |
+
}
|
| 1431 |
+
|
| 1432 |
+
/**
|
| 1433 |
+
* セッションの解答結果を取得
|
| 1434 |
+
*
|
| 1435 |
+
* @param {string} sessionId - セッションID
|
| 1436 |
+
* @returns {Array} - 解答結果配列
|
| 1437 |
+
*
|
| 1438 |
+
* 結果配列の各要素:
|
| 1439 |
+
* {
|
| 1440 |
+
* question_id: string,
|
| 1441 |
+
* subject: string,
|
| 1442 |
+
* category: string,
|
| 1443 |
+
* question_text: string,
|
| 1444 |
+
* user_answer: number,
|
| 1445 |
+
* correct_answer: number,
|
| 1446 |
+
* is_correct: boolean,
|
| 1447 |
+
* time_taken: number
|
| 1448 |
+
* }
|
| 1449 |
+
*/
|
| 1450 |
+
function getSessionResults(sessionId) {
|
| 1451 |
+
try {
|
| 1452 |
+
const answersSheet = getSheet(SHEET_NAMES.ANSWERS);
|
| 1453 |
+
const questionsSheet = getSheet(SHEET_NAMES.GENERATED_QUESTIONS);
|
| 1454 |
+
const answersData = answersSheet.getDataRange().getValues();
|
| 1455 |
+
const questionsData = questionsSheet.getDataRange().getValues();
|
| 1456 |
+
|
| 1457 |
+
// Questionsデータをマップ化(高速検索用)
|
| 1458 |
+
const questionsMap = {};
|
| 1459 |
+
for (let i = 1; i < questionsData.length; i++) {
|
| 1460 |
+
const questionId = questionsData[i][0];
|
| 1461 |
+
questionsMap[questionId] = {
|
| 1462 |
+
session_id: questionsData[i][1],
|
| 1463 |
+
subject: questionsData[i][2],
|
| 1464 |
+
category: questionsData[i][3],
|
| 1465 |
+
difficulty: questionsData[i][8],
|
| 1466 |
+
question_text: questionsData[i][5],
|
| 1467 |
+
choices: questionsData[i][6],
|
| 1468 |
+
correct_answer: questionsData[i][7],
|
| 1469 |
+
explanation: '',
|
| 1470 |
+
created_at: questionsData[i][9]
|
| 1471 |
+
};
|
| 1472 |
+
}
|
| 1473 |
+
|
| 1474 |
+
const results = [];
|
| 1475 |
+
|
| 1476 |
+
// Answersシートから該当セッションの解答を取得
|
| 1477 |
+
for (let i = 1; i < answersData.length; i++) {
|
| 1478 |
+
const row = answersData[i];
|
| 1479 |
+
const answerSessionId = row[1]; // session_id
|
| 1480 |
+
|
| 1481 |
+
if (answerSessionId === sessionId) {
|
| 1482 |
+
const questionId = row[2]; // question_id
|
| 1483 |
+
const userAnswer = row[5]; // user_answer (F列)
|
| 1484 |
+
const isCorrect = row[7]; // is_correct (H列)
|
| 1485 |
+
const timeTaken = row[8]; // time_spent (I列)
|
| 1486 |
+
|
| 1487 |
+
// 対応する問題データを取得
|
| 1488 |
+
const question = questionsMap[questionId];
|
| 1489 |
+
if (question) {
|
| 1490 |
+
results.push({
|
| 1491 |
+
question_id: questionId,
|
| 1492 |
+
subject: question.subject,
|
| 1493 |
+
category: question.category,
|
| 1494 |
+
difficulty: question.difficulty,
|
| 1495 |
+
question_text: question.question_text,
|
| 1496 |
+
user_answer: userAnswer,
|
| 1497 |
+
correct_answer: question.correct_answer,
|
| 1498 |
+
is_correct: isCorrect,
|
| 1499 |
+
time_taken: timeTaken
|
| 1500 |
+
});
|
| 1501 |
+
}
|
| 1502 |
+
}
|
| 1503 |
+
}
|
| 1504 |
+
|
| 1505 |
+
Logger.log('getSessionResults: Found ' + results.length + ' results for session ' + sessionId);
|
| 1506 |
+
return results;
|
| 1507 |
+
|
| 1508 |
+
} catch (error) {
|
| 1509 |
+
Logger.log('Error in getSessionResults: ' + error.toString());
|
| 1510 |
+
return [];
|
| 1511 |
+
}
|
| 1512 |
+
}
|
| 1513 |
+
|
| 1514 |
+
/**
|
| 1515 |
+
* セッション結果取得エンドポイント(評価生成用)
|
| 1516 |
+
*
|
| 1517 |
+
* @param {Object} params - { session_id: string }
|
| 1518 |
+
* @returns {TextOutput} - JSON形式のレスポンス
|
| 1519 |
+
*
|
| 1520 |
+
* レスポンス形式:
|
| 1521 |
+
* {
|
| 1522 |
+
* success: true,
|
| 1523 |
+
* data: {
|
| 1524 |
+
* session_id: string,
|
| 1525 |
+
* results: [
|
| 1526 |
+
* {
|
| 1527 |
+
* question_id: string,
|
| 1528 |
+
* subject: string,
|
| 1529 |
+
* category: string,
|
| 1530 |
+
* difficulty: string,
|
| 1531 |
+
* question_text: string,
|
| 1532 |
+
* user_answer: number,
|
| 1533 |
+
* correct_answer: number,
|
| 1534 |
+
* is_correct: boolean,
|
| 1535 |
+
* time_taken: number
|
| 1536 |
+
* },
|
| 1537 |
+
* ...
|
| 1538 |
+
* ],
|
| 1539 |
+
* summary: {
|
| 1540 |
+
* total_score: number,
|
| 1541 |
+
* completed: boolean,
|
| 1542 |
+
* start_time: string,
|
| 1543 |
+
* end_time: string
|
| 1544 |
+
* }
|
| 1545 |
+
* }
|
| 1546 |
+
* }
|
| 1547 |
+
*/
|
| 1548 |
+
function handleGetSessionResults(params) {
|
| 1549 |
+
try {
|
| 1550 |
+
Logger.log('handleGetSessionResults called with params: ' + JSON.stringify(params));
|
| 1551 |
+
|
| 1552 |
+
const sessionId = params.session_id;
|
| 1553 |
+
|
| 1554 |
+
if (!sessionId) {
|
| 1555 |
+
return createJsonResponse({
|
| 1556 |
+
success: false,
|
| 1557 |
+
error: 'session_id is required'
|
| 1558 |
+
});
|
| 1559 |
+
}
|
| 1560 |
+
|
| 1561 |
+
// Sessionsシートからセッション情報を取得
|
| 1562 |
+
const sessionsSheet = getSheet(SHEET_NAMES.SESSIONS);
|
| 1563 |
+
const sessionsData = sessionsSheet.getDataRange().getValues();
|
| 1564 |
+
|
| 1565 |
+
let sessionInfo = null;
|
| 1566 |
+
for (let i = 1; i < sessionsData.length; i++) {
|
| 1567 |
+
if (sessionsData[i][0] === sessionId) {
|
| 1568 |
+
sessionInfo = {
|
| 1569 |
+
session_id: sessionsData[i][0],
|
| 1570 |
+
user_id: sessionsData[i][1],
|
| 1571 |
+
start_time: sessionsData[i][2],
|
| 1572 |
+
end_time: sessionsData[i][3],
|
| 1573 |
+
subjects: sessionsData[i][4],
|
| 1574 |
+
total_score: sessionsData[i][5],
|
| 1575 |
+
completed: sessionsData[i][6]
|
| 1576 |
+
};
|
| 1577 |
+
break;
|
| 1578 |
+
}
|
| 1579 |
+
}
|
| 1580 |
+
|
| 1581 |
+
if (!sessionInfo) {
|
| 1582 |
+
return createJsonResponse({
|
| 1583 |
+
success: false,
|
| 1584 |
+
error: 'Session not found'
|
| 1585 |
+
});
|
| 1586 |
+
}
|
| 1587 |
+
|
| 1588 |
+
// 解答結果を取得(既存のgetSessionResults関数を使用)
|
| 1589 |
+
const results = getSessionResults(sessionId);
|
| 1590 |
+
|
| 1591 |
+
Logger.log('handleGetSessionResults: Found ' + results.length + ' results');
|
| 1592 |
+
|
| 1593 |
+
return createJsonResponse({
|
| 1594 |
+
success: true,
|
| 1595 |
+
data: {
|
| 1596 |
+
session_id: sessionId,
|
| 1597 |
+
results: results,
|
| 1598 |
+
summary: {
|
| 1599 |
+
total_score: sessionInfo.total_score,
|
| 1600 |
+
completed: sessionInfo.completed,
|
| 1601 |
+
start_time: sessionInfo.start_time,
|
| 1602 |
+
end_time: sessionInfo.end_time
|
| 1603 |
+
}
|
| 1604 |
+
}
|
| 1605 |
+
});
|
| 1606 |
+
|
| 1607 |
+
} catch (error) {
|
| 1608 |
+
Logger.log('Error in handleGetSessionResults: ' + error.toString());
|
| 1609 |
+
return createErrorResponse('Get Session Results Failed', error.toString());
|
| 1610 |
+
}
|
| 1611 |
+
}
|
| 1612 |
+
|
| 1613 |
+
/**
|
| 1614 |
+
* check_summary_statusエンドポイント
|
| 1615 |
+
* session_idに紐づくサマリー完了状態を確認
|
| 1616 |
+
*
|
| 1617 |
+
* @param {Object} params - { session_id: string }
|
| 1618 |
+
* @returns {TextOutput} - JSON形式のレスポンス
|
| 1619 |
+
*/
|
| 1620 |
+
function handleCheckSummaryStatus(params) {
|
| 1621 |
+
try {
|
| 1622 |
+
Logger.log('handleCheckSummaryStatus called with params: ' + JSON.stringify(params));
|
| 1623 |
+
|
| 1624 |
+
const sessionId = params.session_id;
|
| 1625 |
+
|
| 1626 |
+
if (!sessionId) {
|
| 1627 |
+
return createJsonResponse({
|
| 1628 |
+
success: false,
|
| 1629 |
+
error: 'session_id is required'
|
| 1630 |
+
});
|
| 1631 |
+
}
|
| 1632 |
+
|
| 1633 |
+
// Summariesシートからsession_idに一致するレコードを取得
|
| 1634 |
+
const sheet = getSheet(SHEET_NAMES.SUMMARIES);
|
| 1635 |
+
const data = sheet.getDataRange().getValues();
|
| 1636 |
+
|
| 1637 |
+
// ヘッダー行を確認(デバッグ用)
|
| 1638 |
+
const headers = data[0];
|
| 1639 |
+
Logger.log('Summaries sheet headers: ' + JSON.stringify(headers));
|
| 1640 |
+
|
| 1641 |
+
const summaries = [];
|
| 1642 |
+
|
| 1643 |
+
// ヘッダー行をスキップして検索
|
| 1644 |
+
for (let i = 1; i < data.length; i++) {
|
| 1645 |
+
const row = data[i];
|
| 1646 |
+
const rowSessionId = row[1]; // session_id列(0-indexed)
|
| 1647 |
+
|
| 1648 |
+
if (rowSessionId === sessionId) {
|
| 1649 |
+
const subject = row[2]; // subject
|
| 1650 |
+
const keywordsJson = row[3]; // keywords (JSON文字列)
|
| 1651 |
+
const summary = row[5]; // summary
|
| 1652 |
+
|
| 1653 |
+
// JSONをパース(空の場合は空配列)
|
| 1654 |
+
let keywords = [];
|
| 1655 |
+
try {
|
| 1656 |
+
keywords = keywordsJson ? JSON.parse(keywordsJson) : [];
|
| 1657 |
+
} catch (parseError) {
|
| 1658 |
+
Logger.log('Failed to parse keywords: ' + parseError.toString());
|
| 1659 |
+
keywords = [];
|
| 1660 |
+
}
|
| 1661 |
+
|
| 1662 |
+
summaries.push({
|
| 1663 |
+
subject: subject,
|
| 1664 |
+
summary: summary || '',
|
| 1665 |
+
keywords: keywords
|
| 1666 |
+
});
|
| 1667 |
+
}
|
| 1668 |
+
}
|
| 1669 |
+
|
| 1670 |
+
const completed = summaries.length > 0;
|
| 1671 |
+
const count = summaries.length;
|
| 1672 |
+
|
| 1673 |
+
Logger.log('handleCheckSummaryStatus: Found ' + count + ' summaries for session: ' + sessionId);
|
| 1674 |
+
|
| 1675 |
+
return createJsonResponse({
|
| 1676 |
+
success: true,
|
| 1677 |
+
data: {
|
| 1678 |
+
session_id: sessionId,
|
| 1679 |
+
completed: completed,
|
| 1680 |
+
count: count,
|
| 1681 |
+
summaries: summaries
|
| 1682 |
+
}
|
| 1683 |
+
});
|
| 1684 |
+
|
| 1685 |
+
} catch (error) {
|
| 1686 |
+
Logger.log('Error in handleCheckSummaryStatus: ' + error.toString());
|
| 1687 |
+
return createErrorResponse('Check Summary Status Failed', error.toString());
|
| 1688 |
+
}
|
| 1689 |
+
}
|
| 1690 |
+
|
| 1691 |
+
/**
|
| 1692 |
+
* 評価保存エンドポイント
|
| 1693 |
+
* Python側のget_evaluationから呼び出される
|
| 1694 |
+
*
|
| 1695 |
+
* @param {Object} params - { session_id: string, evaluations: Array }
|
| 1696 |
+
* @returns {TextOutput} - JSON形式のレスポンス
|
| 1697 |
+
*/
|
| 1698 |
+
function handleSaveEvaluations(params) {
|
| 1699 |
+
try {
|
| 1700 |
+
Logger.log('handleSaveEvaluations called with params: ' + JSON.stringify(params));
|
| 1701 |
+
|
| 1702 |
+
const sessionId = params.session_id;
|
| 1703 |
+
const evaluations = params.evaluations || [];
|
| 1704 |
+
|
| 1705 |
+
if (!sessionId) {
|
| 1706 |
+
return createJsonResponse({
|
| 1707 |
+
success: false,
|
| 1708 |
+
error: 'session_id is required'
|
| 1709 |
+
});
|
| 1710 |
+
}
|
| 1711 |
+
|
| 1712 |
+
if (!Array.isArray(evaluations) || evaluations.length === 0) {
|
| 1713 |
+
return createJsonResponse({
|
| 1714 |
+
success: false,
|
| 1715 |
+
error: 'evaluations array is required and must not be empty'
|
| 1716 |
+
});
|
| 1717 |
+
}
|
| 1718 |
+
|
| 1719 |
+
// Evaluationsシート取得(なければ作成)
|
| 1720 |
+
const sheet = getOrCreateSheet(SHEET_NAMES.EVALUATIONS, [
|
| 1721 |
+
'evaluation_id',
|
| 1722 |
+
'session_id',
|
| 1723 |
+
'subject',
|
| 1724 |
+
'advice',
|
| 1725 |
+
'strengths',
|
| 1726 |
+
'weaknesses',
|
| 1727 |
+
'recommended_topics',
|
| 1728 |
+
'created_at'
|
| 1729 |
+
]);
|
| 1730 |
+
|
| 1731 |
+
const timestamp = getTimestamp();
|
| 1732 |
+
const savedEvaluations = [];
|
| 1733 |
+
|
| 1734 |
+
// 各評価を保存
|
| 1735 |
+
for (let i = 0; i < evaluations.length; i++) {
|
| 1736 |
+
const evaluation = evaluations[i];
|
| 1737 |
+
const evaluationId = generateUUID();
|
| 1738 |
+
|
| 1739 |
+
const row = [
|
| 1740 |
+
evaluationId,
|
| 1741 |
+
sessionId,
|
| 1742 |
+
evaluation.subject || 'overall',
|
| 1743 |
+
evaluation.advice || '',
|
| 1744 |
+
JSON.stringify(evaluation.strengths || []),
|
| 1745 |
+
JSON.stringify(evaluation.weaknesses || []),
|
| 1746 |
+
JSON.stringify(evaluation.recommended_topics || evaluation.next_steps || []),
|
| 1747 |
+
timestamp
|
| 1748 |
+
];
|
| 1749 |
+
|
| 1750 |
+
sheet.appendRow(row);
|
| 1751 |
+
|
| 1752 |
+
savedEvaluations.push({
|
| 1753 |
+
evaluation_id: evaluationId,
|
| 1754 |
+
subject: evaluation.subject || 'overall'
|
| 1755 |
+
});
|
| 1756 |
+
|
| 1757 |
+
Logger.log('Saved evaluation: ' + evaluationId + ' for subject: ' + (evaluation.subject || 'overall'));
|
| 1758 |
+
}
|
| 1759 |
+
|
| 1760 |
+
return createJsonResponse({
|
| 1761 |
+
success: true,
|
| 1762 |
+
data: {
|
| 1763 |
+
session_id: sessionId,
|
| 1764 |
+
saved_count: savedEvaluations.length,
|
| 1765 |
+
evaluations: savedEvaluations
|
| 1766 |
+
}
|
| 1767 |
+
});
|
| 1768 |
+
|
| 1769 |
+
} catch (error) {
|
| 1770 |
+
Logger.log('Error in handleSaveEvaluations: ' + error.toString());
|
| 1771 |
+
return createErrorResponse('Save Evaluations Failed', error.toString());
|
| 1772 |
+
}
|
| 1773 |
+
}
|
| 1774 |
+
|
| 1775 |
+
/**
|
| 1776 |
+
* Statisticsシートを更新
|
| 1777 |
+
*
|
| 1778 |
+
* @param {string} userId - ユーザーID
|
| 1779 |
+
* @param {Object} statsBySubjectCategory - 教科・分野別の統計データ
|
| 1780 |
+
*/
|
| 1781 |
+
function updateStatistics(userId, statsBySubjectCategory) {
|
| 1782 |
+
try {
|
| 1783 |
+
const statisticsSheet = getSheet(SHEET_NAMES.STATISTICS);
|
| 1784 |
+
const data = statisticsSheet.getDataRange().getValues();
|
| 1785 |
+
const timestamp = getTimestamp();
|
| 1786 |
+
|
| 1787 |
+
// 各教科・分野について統計を更新
|
| 1788 |
+
for (const key in statsBySubjectCategory) {
|
| 1789 |
+
const stat = statsBySubjectCategory[key];
|
| 1790 |
+
const subject = stat.subject;
|
| 1791 |
+
const category = stat.category;
|
| 1792 |
+
const attempted = stat.attempted;
|
| 1793 |
+
const correct = stat.correct;
|
| 1794 |
+
|
| 1795 |
+
// 既存の統計レコードを検索
|
| 1796 |
+
let existingRowIndex = -1;
|
| 1797 |
+
for (let i = 1; i < data.length; i++) {
|
| 1798 |
+
if (data[i][1] === userId && data[i][2] === subject && data[i][3] === category) {
|
| 1799 |
+
existingRowIndex = i;
|
| 1800 |
+
break;
|
| 1801 |
+
}
|
| 1802 |
+
}
|
| 1803 |
+
|
| 1804 |
+
if (existingRowIndex !== -1) {
|
| 1805 |
+
// 既存レコードを更新
|
| 1806 |
+
const oldAttempted = data[existingRowIndex][4] || 0;
|
| 1807 |
+
const oldCorrect = data[existingRowIndex][5] || 0;
|
| 1808 |
+
const newAttempted = oldAttempted + attempted;
|
| 1809 |
+
const newCorrect = oldCorrect + correct;
|
| 1810 |
+
const newAccuracyRate = newAttempted > 0 ? newCorrect / newAttempted : 0;
|
| 1811 |
+
|
| 1812 |
+
statisticsSheet.getRange(existingRowIndex + 1, 5).setValue(newAttempted); // total_attempted
|
| 1813 |
+
statisticsSheet.getRange(existingRowIndex + 1, 6).setValue(newCorrect); // correct_count
|
| 1814 |
+
statisticsSheet.getRange(existingRowIndex + 1, 7).setValue(newAccuracyRate); // accuracy_rate
|
| 1815 |
+
statisticsSheet.getRange(existingRowIndex + 1, 8).setValue(timestamp); // last_updated
|
| 1816 |
+
|
| 1817 |
+
Logger.log('Updated statistics: ' + subject + '/' + category + ' - ' + newCorrect + '/' + newAttempted);
|
| 1818 |
+
} else {
|
| 1819 |
+
// 新規レコードを作成
|
| 1820 |
+
const statId = generateUUID();
|
| 1821 |
+
const accuracyRate = attempted > 0 ? correct / attempted : 0;
|
| 1822 |
+
const newRow = [
|
| 1823 |
+
statId, // stat_id
|
| 1824 |
+
userId, // user_id
|
| 1825 |
+
subject, // subject
|
| 1826 |
+
category, // category
|
| 1827 |
+
attempted, // total_attempted
|
| 1828 |
+
correct, // correct_count
|
| 1829 |
+
accuracyRate, // accuracy_rate
|
| 1830 |
+
timestamp // last_updated
|
| 1831 |
+
];
|
| 1832 |
+
statisticsSheet.appendRow(newRow);
|
| 1833 |
+
|
| 1834 |
+
Logger.log('Created new statistics: ' + subject + '/' + category + ' - ' + correct + '/' + attempted);
|
| 1835 |
+
}
|
| 1836 |
+
}
|
| 1837 |
+
|
| 1838 |
+
} catch (error) {
|
| 1839 |
+
Logger.log('Error in updateStatistics: ' + error.toString());
|
| 1840 |
+
}
|
| 1841 |
+
}
|
| 1842 |
+
|
| 1843 |
+
/**
|
| 1844 |
+
* JSON形式のレスポンスを作成
|
| 1845 |
+
*
|
| 1846 |
+
* @param {Object} data - レスポンスデータ
|
| 1847 |
+
* @returns {TextOutput} - JSON形式のレスポンス
|
| 1848 |
+
*/
|
| 1849 |
+
function createJsonResponse(data) {
|
| 1850 |
+
return ContentService
|
| 1851 |
+
.createTextOutput(JSON.stringify(data))
|
| 1852 |
+
.setMimeType(ContentService.MimeType.JSON);
|
| 1853 |
+
}
|
| 1854 |
+
|
| 1855 |
+
/**
|
| 1856 |
+
* エラーレスポンスを作成
|
| 1857 |
+
*
|
| 1858 |
+
* @param {string} message - エラーメッセージ
|
| 1859 |
+
* @param {string} details - エラー詳細
|
| 1860 |
+
* @returns {TextOutput} - JSON形式のエラーレスポンス
|
| 1861 |
+
*/
|
| 1862 |
+
function createErrorResponse(message, details) {
|
| 1863 |
+
const errorData = {
|
| 1864 |
+
success: false,
|
| 1865 |
+
error: message,
|
| 1866 |
+
details: details,
|
| 1867 |
+
timestamp: new Date().toISOString()
|
| 1868 |
+
};
|
| 1869 |
+
|
| 1870 |
+
return ContentService
|
| 1871 |
+
.createTextOutput(JSON.stringify(errorData))
|
| 1872 |
+
.setMimeType(ContentService.MimeType.JSON);
|
| 1873 |
+
}
|
| 1874 |
+
|
| 1875 |
+
/**
|
| 1876 |
+
* スプレッドシートオ��ジェクトを取得
|
| 1877 |
+
*
|
| 1878 |
+
* @returns {Spreadsheet} - スプレッドシートオブジェクト
|
| 1879 |
+
*/
|
| 1880 |
+
function getSpreadsheet() {
|
| 1881 |
+
return SpreadsheetApp.openById(SPREADSHEET_ID);
|
| 1882 |
+
}
|
| 1883 |
+
|
| 1884 |
+
/**
|
| 1885 |
+
* シートオブジェクトを取得
|
| 1886 |
+
*
|
| 1887 |
+
* @param {string} sheetName - シート名
|
| 1888 |
+
* @returns {Sheet} - シートオブジェクト
|
| 1889 |
+
*/
|
| 1890 |
+
function getSheet(sheetName) {
|
| 1891 |
+
const spreadsheet = getSpreadsheet();
|
| 1892 |
+
const sheet = spreadsheet.getSheetByName(sheetName);
|
| 1893 |
+
|
| 1894 |
+
if (!sheet) {
|
| 1895 |
+
throw new Error('Sheet not found: ' + sheetName);
|
| 1896 |
+
}
|
| 1897 |
+
|
| 1898 |
+
return sheet;
|
| 1899 |
+
}
|
| 1900 |
+
|
| 1901 |
+
/**
|
| 1902 |
+
* シートオブジェクトを取得(存在しない場合は作成)
|
| 1903 |
+
*
|
| 1904 |
+
* @param {string} sheetName - シート名
|
| 1905 |
+
* @param {Array} headers - ヘッダー行の配列(新規作成時のみ使用)
|
| 1906 |
+
* @returns {Sheet} - シートオブジェクト
|
| 1907 |
+
*/
|
| 1908 |
+
function getOrCreateSheet(sheetName, headers) {
|
| 1909 |
+
const spreadsheet = getSpreadsheet();
|
| 1910 |
+
let sheet = spreadsheet.getSheetByName(sheetName);
|
| 1911 |
+
|
| 1912 |
+
if (!sheet) {
|
| 1913 |
+
Logger.log('Creating new sheet: ' + sheetName);
|
| 1914 |
+
sheet = spreadsheet.insertSheet(sheetName);
|
| 1915 |
+
|
| 1916 |
+
// ヘッダー行を設定
|
| 1917 |
+
if (headers && headers.length > 0) {
|
| 1918 |
+
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
| 1919 |
+
Logger.log('Set headers for ' + sheetName + ': ' + headers.join(', '));
|
| 1920 |
+
}
|
| 1921 |
+
}
|
| 1922 |
+
|
| 1923 |
+
return sheet;
|
| 1924 |
+
}
|
| 1925 |
+
|
| 1926 |
+
/**
|
| 1927 |
+
* UUIDを生成(簡易版)
|
| 1928 |
+
*
|
| 1929 |
+
* @returns {string} - UUID文字列
|
| 1930 |
+
*/
|
| 1931 |
+
function generateUUID() {
|
| 1932 |
+
return Utilities.getUuid();
|
| 1933 |
+
}
|
| 1934 |
+
|
| 1935 |
+
/**
|
| 1936 |
+
* 現在のタイムスタンプ(ISO 8601形式)を取得
|
| 1937 |
+
*
|
| 1938 |
+
* @returns {string} - ISO 8601形式の日時文字列
|
| 1939 |
+
*/
|
| 1940 |
+
function getTimestamp() {
|
| 1941 |
+
return new Date().toISOString();
|
| 1942 |
+
}
|
| 1943 |
+
|
| 1944 |
+
// ============================================================================
|
| 1945 |
+
// デバッグ用関数(開発時のみ使用)
|
| 1946 |
+
// ============================================================================
|
| 1947 |
+
|
| 1948 |
+
/**
|
| 1949 |
+
* デバッグ用:ユーザー登録APIテスト
|
| 1950 |
+
* Apps Scriptエディタから実行可能
|
| 1951 |
+
* ランダムなusernameを生成して重複を回避
|
| 1952 |
+
*/
|
| 1953 |
+
function testRegisterUser() {
|
| 1954 |
+
// ランダムなusernameを生成(重複回避)
|
| 1955 |
+
const randomSuffix = Math.floor(Math.random() * 10000);
|
| 1956 |
+
const e = {
|
| 1957 |
+
postData: {
|
| 1958 |
+
contents: JSON.stringify({
|
| 1959 |
+
endpoint: 'registerUser',
|
| 1960 |
+
params: {
|
| 1961 |
+
username: 'テストユーザー' + randomSuffix
|
| 1962 |
+
}
|
| 1963 |
+
})
|
| 1964 |
+
}
|
| 1965 |
+
};
|
| 1966 |
+
|
| 1967 |
+
const response = doPost(e);
|
| 1968 |
+
Logger.log('Test Response: ' + response.getContent());
|
| 1969 |
+
}
|
| 1970 |
+
|
| 1971 |
+
function testStartSession() {
|
| 1972 |
+
const e = {
|
| 1973 |
+
postData: {
|
| 1974 |
+
contents: JSON.stringify({
|
| 1975 |
+
endpoint: 'startSession',
|
| 1976 |
+
params: {
|
| 1977 |
+
user_id: 'dummy-user-001',
|
| 1978 |
+
subjects: ['国語', '算数', '理科']
|
| 1979 |
+
}
|
| 1980 |
+
})
|
| 1981 |
+
}
|
| 1982 |
+
};
|
| 1983 |
+
|
| 1984 |
+
const response = doPost(e);
|
| 1985 |
+
Logger.log('Test Response: ' + response.getContent());
|
| 1986 |
+
}
|
| 1987 |
+
|
| 1988 |
+
function testGenerateQuestion() {
|
| 1989 |
+
const e = {
|
| 1990 |
+
postData: {
|
| 1991 |
+
contents: JSON.stringify({
|
| 1992 |
+
endpoint: 'generateQuestion',
|
| 1993 |
+
params: {
|
| 1994 |
+
session_id: 'dummy-session-001',
|
| 1995 |
+
subjects: ['国語']
|
| 1996 |
+
}
|
| 1997 |
+
})
|
| 1998 |
+
}
|
| 1999 |
+
};
|
| 2000 |
+
|
| 2001 |
+
const response = doPost(e);
|
| 2002 |
+
Logger.log('Test Response: ' + response.getContent());
|
| 2003 |
+
}
|
| 2004 |
+
|
| 2005 |
+
/**
|
| 2006 |
+
* デバッグ用:重複usernameでのユーザー登録テスト
|
| 2007 |
+
* 既存のusernameを使用してエラーレスポンスを確認
|
| 2008 |
+
*/
|
| 2009 |
+
function testDuplicateUsername() {
|
| 2010 |
+
const e = {
|
| 2011 |
+
postData: {
|
| 2012 |
+
contents: JSON.stringify({
|
| 2013 |
+
endpoint: 'registerUser',
|
| 2014 |
+
params: {
|
| 2015 |
+
username: 'テストユーザー9526' // 既存のusername
|
| 2016 |
+
}
|
| 2017 |
+
})
|
| 2018 |
+
}
|
| 2019 |
+
};
|
| 2020 |
+
|
| 2021 |
+
const response = doPost(e);
|
| 2022 |
+
Logger.log('Test Response: ' + response.getContent());
|
| 2023 |
+
}
|
| 2024 |
+
|
| 2025 |
+
/**
|
| 2026 |
+
* 問題生成APIテスト(Dify統合)
|
| 2027 |
+
*/
|
| 2028 |
+
function testGenerateQuestions() {
|
| 2029 |
+
const e = {
|
| 2030 |
+
postData: {
|
| 2031 |
+
contents: JSON.stringify({
|
| 2032 |
+
endpoint: 'generateQuestions',
|
| 2033 |
+
params: {
|
| 2034 |
+
session_id: 'test-session-' + Date.now(),
|
| 2035 |
+
subjects: ['jp'] // まず国語だけでテスト
|
| 2036 |
+
}
|
| 2037 |
+
})
|
| 2038 |
+
}
|
| 2039 |
+
};
|
| 2040 |
+
|
| 2041 |
+
const response = doPost(e);
|
| 2042 |
+
Logger.log('Test Response: ' + response.getContent());
|
| 2043 |
+
}
|
| 2044 |
+
|
| 2045 |
+
/**
|
| 2046 |
+
* 解答送信APIテスト
|
| 2047 |
+
* 注: 事前にtestStartSession、testGenerateQuestionsを実行し、
|
| 2048 |
+
* 実際のsession_idとquestion_idを取得してから実行すること
|
| 2049 |
+
*/
|
| 2050 |
+
function testSubmitAnswers() {
|
| 2051 |
+
// テスト用: 実際のsession_idとquestion_idに置き換える必要あり
|
| 2052 |
+
const e = {
|
| 2053 |
+
postData: {
|
| 2054 |
+
contents: JSON.stringify({
|
| 2055 |
+
endpoint: 'submitAnswers',
|
| 2056 |
+
params: {
|
| 2057 |
+
session_id: 'test-session-001', // 実際のsession_idに置き換え
|
| 2058 |
+
answers: [
|
| 2059 |
+
{
|
| 2060 |
+
question_id: 'test-question-001', // 実際のquestion_idに置き換え
|
| 2061 |
+
selected_answer: 0,
|
| 2062 |
+
time_taken_seconds: 12
|
| 2063 |
+
},
|
| 2064 |
+
{
|
| 2065 |
+
question_id: 'test-question-002', // 実際のquestion_idに置き換え
|
| 2066 |
+
selected_answer: 2,
|
| 2067 |
+
time_taken_seconds: 18
|
| 2068 |
+
}
|
| 2069 |
+
]
|
| 2070 |
+
}
|
| 2071 |
+
})
|
| 2072 |
+
}
|
| 2073 |
+
};
|
| 2074 |
+
|
| 2075 |
+
const response = doPost(e);
|
| 2076 |
+
Logger.log('Test Response: ' + response.getContent());
|
| 2077 |
+
}
|
| 2078 |
+
|
| 2079 |
+
/**
|
| 2080 |
+
* 統計取得APIテスト(ジャンル別累積集計対応版)
|
| 2081 |
+
* 注: 事前にtestRegisterUserを実行し、実際のuser_idを取得してから実行すること
|
| 2082 |
+
*
|
| 2083 |
+
* テスト項目:
|
| 2084 |
+
* 1. ジャンル別統計が取得できること
|
| 2085 |
+
* 2. GenreMaster.jsからジャンル名が正しく取得できること
|
| 2086 |
+
* 3. 苦手順(正答率昇順)にソートされていること
|
| 2087 |
+
* 4. 累積統計(total_sessions, total_questions, total_correct, overall_accuracy)が返却されること
|
| 2088 |
+
*/
|
| 2089 |
+
function testGetStatistics() {
|
| 2090 |
+
const e = {
|
| 2091 |
+
postData: {
|
| 2092 |
+
contents: JSON.stringify({
|
| 2093 |
+
endpoint: 'getStatistics',
|
| 2094 |
+
params: {
|
| 2095 |
+
user_id: 'test-user-001', // 実際のuser_idに置き換え
|
| 2096 |
+
subjects: ['jp', 'math'] // オプション: 指定教科のみ取得
|
| 2097 |
+
}
|
| 2098 |
+
})
|
| 2099 |
+
}
|
| 2100 |
+
};
|
| 2101 |
+
|
| 2102 |
+
const response = doPost(e);
|
| 2103 |
+
const responseText = response.getContent();
|
| 2104 |
+
Logger.log('=== testGetStatistics 結果 ===');
|
| 2105 |
+
Logger.log(responseText);
|
| 2106 |
+
|
| 2107 |
+
// レスポンスをパースして検証
|
| 2108 |
+
try {
|
| 2109 |
+
const data = JSON.parse(responseText);
|
| 2110 |
+
if (data.success && data.data) {
|
| 2111 |
+
Logger.log('✅ success: true');
|
| 2112 |
+
Logger.log('user_id: ' + data.data.user_id);
|
| 2113 |
+
|
| 2114 |
+
if (data.data.subjects && data.data.subjects.length > 0) {
|
| 2115 |
+
Logger.log('✅ subjects配列が存在(' + data.data.subjects.length + '教科)');
|
| 2116 |
+
|
| 2117 |
+
// 最初の教科の詳細を確認
|
| 2118 |
+
const firstSubject = data.data.subjects[0];
|
| 2119 |
+
Logger.log('教科例: ' + firstSubject.subject_name);
|
| 2120 |
+
Logger.log(' - 総問題数: ' + firstSubject.total_attempted);
|
| 2121 |
+
Logger.log(' - 正解数: ' + firstSubject.total_correct);
|
| 2122 |
+
Logger.log(' - 正答率: ' + (firstSubject.overall_accuracy * 100).toFixed(1) + '%');
|
| 2123 |
+
|
| 2124 |
+
if (firstSubject.genres && firstSubject.genres.length > 0) {
|
| 2125 |
+
Logger.log('✅ ジャンル配列が存在(' + firstSubject.genres.length + 'ジャンル)');
|
| 2126 |
+
Logger.log('ジャンル例(苦手順):');
|
| 2127 |
+
for (let i = 0; i < Math.min(3, firstSubject.genres.length); i++) {
|
| 2128 |
+
const genre = firstSubject.genres[i];
|
| 2129 |
+
Logger.log(' ' + (i + 1) + '. ' + genre.genre_name + '(' + genre.genre_id + '): ' +
|
| 2130 |
+
genre.correct + '/' + genre.attempted + ' = ' +
|
| 2131 |
+
(genre.accuracy * 100).toFixed(1) + '%');
|
| 2132 |
+
}
|
| 2133 |
+
} else {
|
| 2134 |
+
Logger.log('⚠️ ジャンル配列が空です');
|
| 2135 |
+
}
|
| 2136 |
+
}
|
| 2137 |
+
|
| 2138 |
+
if (data.data.cumulative) {
|
| 2139 |
+
Logger.log('✅ 累積統計が存在');
|
| 2140 |
+
Logger.log(' - 総セッション数: ' + data.data.cumulative.total_sessions);
|
| 2141 |
+
Logger.log(' - 総問題数: ' + data.data.cumulative.total_questions);
|
| 2142 |
+
Logger.log(' - 総正解数: ' + data.data.cumulative.total_correct);
|
| 2143 |
+
Logger.log(' - 全体正答率: ' + (data.data.cumulative.overall_accuracy * 100).toFixed(1) + '%');
|
| 2144 |
+
} else {
|
| 2145 |
+
Logger.log('⚠️ 累積統計が存在しません');
|
| 2146 |
+
}
|
| 2147 |
+
} else {
|
| 2148 |
+
Logger.log('❌ エラー: ' + (data.error || 'unknown'));
|
| 2149 |
+
}
|
| 2150 |
+
} catch (parseError) {
|
| 2151 |
+
Logger.log('❌ JSON解析エラー: ' + parseError.toString());
|
| 2152 |
+
}
|
| 2153 |
+
|
| 2154 |
+
Logger.log('=== テスト完了 ===');
|
| 2155 |
+
}
|
| 2156 |
+
|
| 2157 |
+
/**
|
| 2158 |
+
* 評価取得APIテスト
|
| 2159 |
+
* 注: 事前にtestStartSession、testGenerateQuestions、testSubmitAnswersを実行し、
|
| 2160 |
+
* 実際のsession_idを取得してから実行すること
|
| 2161 |
+
*
|
| 2162 |
+
* 使用方法:
|
| 2163 |
+
* 1. testStartSession() → session_idを取得
|
| 2164 |
+
* 2. testGenerateQuestions() → question_idを取得
|
| 2165 |
+
* 3. testSubmitAnswers() → 解答を送信
|
| 2166 |
+
* 4. testGetEvaluation() → 評価を取得
|
| 2167 |
+
*/
|
| 2168 |
+
function testGetEvaluation() {
|
| 2169 |
+
const e = {
|
| 2170 |
+
postData: {
|
| 2171 |
+
contents: JSON.stringify({
|
| 2172 |
+
endpoint: 'getEvaluation',
|
| 2173 |
+
params: {
|
| 2174 |
+
session_id: 'test-session-001', // 実際のsession_idに置き換え
|
| 2175 |
+
subjects: ['jp', 'math'] // オプション: 指定教科のみ評価
|
| 2176 |
+
}
|
| 2177 |
+
})
|
| 2178 |
+
}
|
| 2179 |
+
};
|
| 2180 |
+
|
| 2181 |
+
const response = doPost(e);
|
| 2182 |
+
Logger.log('Test Response: ' + response.getContent());
|
| 2183 |
+
}
|
| 2184 |
+
|
| 2185 |
+
/**
|
| 2186 |
+
* save_questions APIテスト
|
| 2187 |
+
* GeneratedQuestionsシートへの保存をテスト
|
| 2188 |
+
*/
|
| 2189 |
+
function testSaveQuestions() {
|
| 2190 |
+
const e = {
|
| 2191 |
+
postData: {
|
| 2192 |
+
contents: JSON.stringify({
|
| 2193 |
+
action: 'save_questions',
|
| 2194 |
+
session_id: 'test-session-' + Date.now(),
|
| 2195 |
+
subject: 'jp',
|
| 2196 |
+
questions: [
|
| 2197 |
+
{
|
| 2198 |
+
question_id: 'test-q-001',
|
| 2199 |
+
genre_id: 'JP01',
|
| 2200 |
+
answer: '憂鬱',
|
| 2201 |
+
question: '次の漢字の読みを選びなさい:「憂鬱」',
|
| 2202 |
+
choices: ['ゆううつ', 'ゆうげき', 'ゆうめい', 'ゆうべつ'],
|
| 2203 |
+
correct_answer: 0,
|
| 2204 |
+
difficulty: '標準'
|
| 2205 |
+
},
|
| 2206 |
+
{
|
| 2207 |
+
question_id: 'test-q-002',
|
| 2208 |
+
genre_id: 'JP01',
|
| 2209 |
+
answer: '慈悲',
|
| 2210 |
+
question: '次の熟語の読みを選びなさい:「慈悲」',
|
| 2211 |
+
choices: ['じひ', 'じしん', 'じゅひ', 'しひ'],
|
| 2212 |
+
correct_answer: 0,
|
| 2213 |
+
difficulty: '基本'
|
| 2214 |
+
}
|
| 2215 |
+
]
|
| 2216 |
+
})
|
| 2217 |
+
}
|
| 2218 |
+
};
|
| 2219 |
+
|
| 2220 |
+
const response = doPost(e);
|
| 2221 |
+
Logger.log('Test Response: ' + response.getContent());
|
| 2222 |
+
}
|
| 2223 |
+
|
| 2224 |
+
/**
|
| 2225 |
+
* save_summary APIテスト
|
| 2226 |
+
* Summariesシートへの保存をテスト
|
| 2227 |
+
*/
|
| 2228 |
+
function testSaveSummary() {
|
| 2229 |
+
const e = {
|
| 2230 |
+
postData: {
|
| 2231 |
+
contents: JSON.stringify({
|
| 2232 |
+
action: 'save_summary',
|
| 2233 |
+
session_id: 'test-session-' + Date.now(),
|
| 2234 |
+
subject: 'jp',
|
| 2235 |
+
summary_data: {
|
| 2236 |
+
keywords: ['漢字', '語彙', '読み'],
|
| 2237 |
+
topics: ['漢字の読み', '熟語'],
|
| 2238 |
+
summary: '今回は漢字と熟語の読みに関する問題でした。難読漢字が2問出題されました。'
|
| 2239 |
+
}
|
| 2240 |
+
})
|
| 2241 |
+
}
|
| 2242 |
+
};
|
| 2243 |
+
|
| 2244 |
+
const response = doPost(e);
|
| 2245 |
+
Logger.log('Test Response: ' + response.getContent());
|
| 2246 |
+
}
|
V1.7.1/gas/DifyService.js
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Dify APIサービス
|
| 3 |
+
*
|
| 4 |
+
* Dify Cloudのワークフロー実行APIを呼び出すラッパー
|
| 5 |
+
*
|
| 6 |
+
* @version 1.0.0
|
| 7 |
+
* @date 2025-12-04
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
// ============================================================================
|
| 11 |
+
// 定数定義
|
| 12 |
+
// ============================================================================
|
| 13 |
+
|
| 14 |
+
const DIFY_CONFIG = {
|
| 15 |
+
TIMEOUT: 150000, // 150秒(RAG Knowledge Base利用による処理時間増加対応)
|
| 16 |
+
RESPONSE_MODE: 'blocking',
|
| 17 |
+
USER: 'gas-orchestrator'
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
// 教科マッピング: GAS内部コード → Dify期待値(日本語)
|
| 21 |
+
const SUBJECT_TO_JAPANESE = {
|
| 22 |
+
'jp': '国語',
|
| 23 |
+
'math': '算数',
|
| 24 |
+
'sci': '理科',
|
| 25 |
+
'soc': '社会'
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
// 教科コード → Dify出力変数名のマッピング
|
| 29 |
+
const SUBJECT_TO_OUTPUT_KEY = {
|
| 30 |
+
'jp': 'kokugo_questions',
|
| 31 |
+
'math': 'sansu_questions',
|
| 32 |
+
'sci': 'rika_questions',
|
| 33 |
+
'soc': 'shakai_questions'
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
// 教科コード → Dify評価出力変数名のマッピング
|
| 37 |
+
const SUBJECT_TO_EVALUATION_KEY = {
|
| 38 |
+
'jp': 'kokugo_evaluation',
|
| 39 |
+
'math': 'sansu_evaluation',
|
| 40 |
+
'sci': 'rika_evaluation',
|
| 41 |
+
'soc': 'shakai_evaluation'
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
/**
|
| 45 |
+
* Difyの評価出力形式をGAS期待形式に正規化
|
| 46 |
+
*
|
| 47 |
+
* Dify出力形式: {"評価": ["コメント1", "コメント2", ...]}
|
| 48 |
+
* GAS期待形式: {advice, strengths[], weaknesses[], recommended_topics[]}
|
| 49 |
+
*
|
| 50 |
+
* @param {Object} difyEvaluation - Difyから返された評価データ
|
| 51 |
+
* @param {string} subject - 教科ID
|
| 52 |
+
* @returns {Object} - 正規化された評価データ
|
| 53 |
+
*/
|
| 54 |
+
function normalizeEvaluationFormat(difyEvaluation, subject) {
|
| 55 |
+
// 既にGAS期待形式の場合はそのまま返す
|
| 56 |
+
if (difyEvaluation.advice !== undefined) {
|
| 57 |
+
Logger.log('[DifyService] Evaluation already in expected format');
|
| 58 |
+
return difyEvaluation;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Dify形式 {"評価": [...]} の場合
|
| 62 |
+
if (difyEvaluation['評価'] && Array.isArray(difyEvaluation['評価'])) {
|
| 63 |
+
const comments = difyEvaluation['評価'];
|
| 64 |
+
Logger.log('[DifyService] Converting Dify format to GAS format: ' + comments.length + ' comments');
|
| 65 |
+
|
| 66 |
+
// コメントを構造化
|
| 67 |
+
// 最初のコメントを総合アドバイス、残りを分類
|
| 68 |
+
const advice = comments[0] || '';
|
| 69 |
+
const strengths = [];
|
| 70 |
+
const weaknesses = [];
|
| 71 |
+
const recommended_topics = [];
|
| 72 |
+
|
| 73 |
+
// 2番目以降のコメントを内容で分類
|
| 74 |
+
for (var i = 1; i < comments.length; i++) {
|
| 75 |
+
var comment = comments[i];
|
| 76 |
+
// 改善・弱点を示すキーワードがあればweaknesses
|
| 77 |
+
if (comment.includes('課題') || comment.includes('弱') || comment.includes('不足') ||
|
| 78 |
+
comment.includes('低い') || comment.includes('0%') || comment.includes('改善')) {
|
| 79 |
+
weaknesses.push(comment);
|
| 80 |
+
}
|
| 81 |
+
// 強み・良い点を示すキーワードがあればstrengths
|
| 82 |
+
else if (comment.includes('強') || comment.includes('得意') || comment.includes('高い') ||
|
| 83 |
+
comment.includes('安定') || comment.includes('良')) {
|
| 84 |
+
strengths.push(comment);
|
| 85 |
+
}
|
| 86 |
+
// それ以外はrecommended_topics
|
| 87 |
+
else {
|
| 88 |
+
recommended_topics.push(comment);
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
return {
|
| 93 |
+
advice: advice,
|
| 94 |
+
strengths: strengths,
|
| 95 |
+
weaknesses: weaknesses,
|
| 96 |
+
recommended_topics: recommended_topics
|
| 97 |
+
};
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// 不明な形式の場合はそのまま返す(ログ出力)
|
| 101 |
+
Logger.log('[DifyService] Unknown evaluation format: ' + JSON.stringify(Object.keys(difyEvaluation)));
|
| 102 |
+
return {
|
| 103 |
+
advice: JSON.stringify(difyEvaluation),
|
| 104 |
+
strengths: [],
|
| 105 |
+
weaknesses: [],
|
| 106 |
+
recommended_topics: []
|
| 107 |
+
};
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// ============================================================================
|
| 111 |
+
// Dify API呼び出し関数
|
| 112 |
+
// ============================================================================
|
| 113 |
+
|
| 114 |
+
/**
|
| 115 |
+
* Dify APIの環境変数を取得
|
| 116 |
+
*/
|
| 117 |
+
function getDifyCredentials() {
|
| 118 |
+
const properties = PropertiesService.getScriptProperties();
|
| 119 |
+
const apiUrl = properties.getProperty('DIFY_API_URL');
|
| 120 |
+
const apiKey = properties.getProperty('DIFY_API_KEY');
|
| 121 |
+
|
| 122 |
+
if (!apiUrl || !apiKey) {
|
| 123 |
+
throw new Error('Dify API credentials not configured. Run setDifyProperties() first.');
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
return { apiUrl, apiKey };
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
/**
|
| 130 |
+
* 複数のDify APIリクエストを並列実行
|
| 131 |
+
*
|
| 132 |
+
* @param {Array} requestConfigs - リクエスト設定の配列
|
| 133 |
+
* 各要素: { action: string, subject: string, additionalInputs: Object }
|
| 134 |
+
* @returns {Array} - Dify APIレスポンスの配列(リクエスト順序を保持)
|
| 135 |
+
*/
|
| 136 |
+
function callDifyWorkflowBatch(requestConfigs) {
|
| 137 |
+
try {
|
| 138 |
+
const { apiUrl, apiKey } = getDifyCredentials();
|
| 139 |
+
|
| 140 |
+
Logger.log('[DifyService] callDifyWorkflowBatch: Preparing ' + requestConfigs.length + ' parallel requests');
|
| 141 |
+
|
| 142 |
+
// 各リクエストの設定を作成
|
| 143 |
+
const requests = requestConfigs.map((config, index) => {
|
| 144 |
+
const japaneseSubject = SUBJECT_TO_JAPANESE[config.subject] || config.subject;
|
| 145 |
+
|
| 146 |
+
Logger.log('[DifyService] Request ' + index + ': action=' + config.action + ', subject=' + config.subject + ' (' + japaneseSubject + ')');
|
| 147 |
+
|
| 148 |
+
return {
|
| 149 |
+
url: apiUrl,
|
| 150 |
+
method: 'post',
|
| 151 |
+
contentType: 'application/json',
|
| 152 |
+
headers: {
|
| 153 |
+
'Authorization': 'Bearer ' + apiKey
|
| 154 |
+
},
|
| 155 |
+
payload: JSON.stringify({
|
| 156 |
+
inputs: {
|
| 157 |
+
action: config.action,
|
| 158 |
+
subject: japaneseSubject,
|
| 159 |
+
...config.additionalInputs
|
| 160 |
+
},
|
| 161 |
+
response_mode: DIFY_CONFIG.RESPONSE_MODE,
|
| 162 |
+
user: DIFY_CONFIG.USER
|
| 163 |
+
}),
|
| 164 |
+
muteHttpExceptions: true
|
| 165 |
+
};
|
| 166 |
+
});
|
| 167 |
+
|
| 168 |
+
Logger.log('[DifyService] Sending ' + requests.length + ' parallel requests to Dify API...');
|
| 169 |
+
const startTime = new Date().getTime();
|
| 170 |
+
|
| 171 |
+
// 並列実行
|
| 172 |
+
const responses = UrlFetchApp.fetchAll(requests);
|
| 173 |
+
|
| 174 |
+
const endTime = new Date().getTime();
|
| 175 |
+
Logger.log('[DifyService] All requests completed in ' + ((endTime - startTime) / 1000) + ' seconds');
|
| 176 |
+
|
| 177 |
+
// 結果をパースして返却
|
| 178 |
+
return responses.map((response, index) => {
|
| 179 |
+
const responseCode = response.getResponseCode();
|
| 180 |
+
const responseText = response.getContentText();
|
| 181 |
+
|
| 182 |
+
Logger.log('[DifyService] Response ' + index + ': code=' + responseCode);
|
| 183 |
+
|
| 184 |
+
if (responseCode !== 200) {
|
| 185 |
+
Logger.log('[DifyService] Batch request ' + index + ' failed: ' + responseText.substring(0, 200));
|
| 186 |
+
return {
|
| 187 |
+
error: true,
|
| 188 |
+
code: responseCode,
|
| 189 |
+
message: responseText,
|
| 190 |
+
subject: requestConfigs[index].subject
|
| 191 |
+
};
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
try {
|
| 195 |
+
const parsed = JSON.parse(responseText);
|
| 196 |
+
parsed._subject = requestConfigs[index].subject; // 後で識別するため
|
| 197 |
+
return parsed;
|
| 198 |
+
} catch (parseError) {
|
| 199 |
+
Logger.log('[DifyService] JSON parse error for request ' + index + ': ' + parseError.toString());
|
| 200 |
+
return {
|
| 201 |
+
error: true,
|
| 202 |
+
code: responseCode,
|
| 203 |
+
message: 'JSON parse error: ' + parseError.toString(),
|
| 204 |
+
subject: requestConfigs[index].subject
|
| 205 |
+
};
|
| 206 |
+
}
|
| 207 |
+
});
|
| 208 |
+
|
| 209 |
+
} catch (error) {
|
| 210 |
+
Logger.log('[DifyService] callDifyWorkflowBatch error: ' + error.toString());
|
| 211 |
+
throw error;
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
/**
|
| 216 |
+
* Dify Workflowを実行
|
| 217 |
+
*
|
| 218 |
+
* @param {string} action - アクション(generate_questions, generate_evaluation)
|
| 219 |
+
* @param {string} subject - 教科ID(jp, math, sci, soc)
|
| 220 |
+
* @param {Object} additionalInputs - 追加の入力パラメータ(統計情報など)
|
| 221 |
+
* @returns {Object} - Dify APIレスポンス
|
| 222 |
+
*/
|
| 223 |
+
function callDifyWorkflow(action, subject, additionalInputs = {}) {
|
| 224 |
+
try {
|
| 225 |
+
const { apiUrl, apiKey } = getDifyCredentials();
|
| 226 |
+
|
| 227 |
+
// GASの教科コード(jp, math, sci, soc)を日本語に変換
|
| 228 |
+
const japaneseSubject = SUBJECT_TO_JAPANESE[subject] || subject;
|
| 229 |
+
|
| 230 |
+
Logger.log('[DifyService] callDifyWorkflow: action=' + action + ', subject=' + subject + ' (' + japaneseSubject + ')');
|
| 231 |
+
|
| 232 |
+
// リクエストペイロード(subjectは日本語に変換して送信)
|
| 233 |
+
const payload = {
|
| 234 |
+
inputs: {
|
| 235 |
+
action: action,
|
| 236 |
+
subject: japaneseSubject,
|
| 237 |
+
...additionalInputs
|
| 238 |
+
},
|
| 239 |
+
response_mode: DIFY_CONFIG.RESPONSE_MODE,
|
| 240 |
+
user: DIFY_CONFIG.USER
|
| 241 |
+
};
|
| 242 |
+
|
| 243 |
+
// HTTPリクエストオプション
|
| 244 |
+
const options = {
|
| 245 |
+
method: 'post',
|
| 246 |
+
contentType: 'application/json',
|
| 247 |
+
headers: {
|
| 248 |
+
'Authorization': 'Bearer ' + apiKey
|
| 249 |
+
},
|
| 250 |
+
payload: JSON.stringify(payload),
|
| 251 |
+
muteHttpExceptions: true
|
| 252 |
+
};
|
| 253 |
+
|
| 254 |
+
Logger.log('[DifyService] Sending request to Dify API...');
|
| 255 |
+
const response = UrlFetchApp.fetch(apiUrl, options);
|
| 256 |
+
const responseCode = response.getResponseCode();
|
| 257 |
+
const responseText = response.getContentText();
|
| 258 |
+
|
| 259 |
+
Logger.log('[DifyService] Response code: ' + responseCode);
|
| 260 |
+
|
| 261 |
+
if (responseCode !== 200) {
|
| 262 |
+
Logger.log('[DifyService] Error response: ' + responseText);
|
| 263 |
+
throw new Error('Dify API error: ' + responseCode + ' - ' + responseText);
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
const responseData = JSON.parse(responseText);
|
| 267 |
+
Logger.log('[DifyService] Success: workflow_run_id=' + (responseData.workflow_run_id || 'N/A'));
|
| 268 |
+
|
| 269 |
+
return responseData;
|
| 270 |
+
|
| 271 |
+
} catch (error) {
|
| 272 |
+
Logger.log('[DifyService] Error: ' + error.toString());
|
| 273 |
+
throw error;
|
| 274 |
+
}
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
/**
|
| 278 |
+
* 問題生成APIを呼び出し
|
| 279 |
+
*
|
| 280 |
+
* @param {string} subject - 教科ID
|
| 281 |
+
* @param {Object} statistics - ユーザー統計(オプション)
|
| 282 |
+
* @param {Array} recentQuestions - 直近の問題リスト(オプション)
|
| 283 |
+
* @returns {Array} - 生成された問題配列
|
| 284 |
+
*/
|
| 285 |
+
function generateQuestionsFromDify(subject, statistics = {}, recentQuestions = []) {
|
| 286 |
+
try {
|
| 287 |
+
Logger.log('[DifyService] generateQuestionsFromDify: subject=' + subject);
|
| 288 |
+
|
| 289 |
+
const response = callDifyWorkflow('generate_questions', subject, {
|
| 290 |
+
statistics: JSON.stringify(statistics),
|
| 291 |
+
recent_questions: JSON.stringify(recentQuestions)
|
| 292 |
+
});
|
| 293 |
+
|
| 294 |
+
// Difyレスポンスから問題データを抽出
|
| 295 |
+
// response.data.outputs から教科別の出力変数を取得
|
| 296 |
+
if (response.data && response.data.outputs) {
|
| 297 |
+
const outputs = response.data.outputs;
|
| 298 |
+
|
| 299 |
+
// エラーレスポンスチェック
|
| 300 |
+
if (outputs.action_error) {
|
| 301 |
+
throw new Error('Dify action error: ' + outputs.action_error);
|
| 302 |
+
}
|
| 303 |
+
if (outputs.subject_error_questions) {
|
| 304 |
+
throw new Error('Dify subject error: ' + outputs.subject_error_questions);
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
// 教科別の出力変数名を取得(例: kokugo_questions, sansu_questions)
|
| 308 |
+
const outputKey = SUBJECT_TO_OUTPUT_KEY[subject];
|
| 309 |
+
if (!outputKey) {
|
| 310 |
+
throw new Error('Invalid subject code: ' + subject);
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
Logger.log('[DifyService] Looking for output key: ' + outputKey);
|
| 314 |
+
|
| 315 |
+
// 教科別の出力変数から問題データを取得
|
| 316 |
+
if (outputs[outputKey]) {
|
| 317 |
+
const questionsData = outputs[outputKey];
|
| 318 |
+
|
| 319 |
+
// JSON文字列の場合はパース
|
| 320 |
+
if (typeof questionsData === 'string') {
|
| 321 |
+
Logger.log('[DifyService] Parsing JSON string from ' + outputKey);
|
| 322 |
+
const parsed = JSON.parse(questionsData);
|
| 323 |
+
|
| 324 |
+
// { questions: [...] } 形式の場合
|
| 325 |
+
if (parsed.questions && Array.isArray(parsed.questions)) {
|
| 326 |
+
Logger.log('[DifyService] Successfully parsed ' + parsed.questions.length + ' questions');
|
| 327 |
+
return parsed.questions;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
// 直接配列の場合
|
| 331 |
+
if (Array.isArray(parsed)) {
|
| 332 |
+
Logger.log('[DifyService] Successfully parsed ' + parsed.length + ' questions (array format)');
|
| 333 |
+
return parsed;
|
| 334 |
+
}
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
// 既にオブジェクト/配列の場合
|
| 338 |
+
if (Array.isArray(questionsData)) {
|
| 339 |
+
Logger.log('[DifyService] Got ' + questionsData.length + ' questions (array format)');
|
| 340 |
+
return questionsData;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
if (questionsData.questions && Array.isArray(questionsData.questions)) {
|
| 344 |
+
Logger.log('[DifyService] Got ' + questionsData.questions.length + ' questions (object format)');
|
| 345 |
+
return questionsData.questions;
|
| 346 |
+
}
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
Logger.log('[DifyService] Output key "' + outputKey + '" not found in outputs: ' + JSON.stringify(Object.keys(outputs)));
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
throw new Error('Invalid response format from Dify: questions data not found');
|
| 353 |
+
|
| 354 |
+
} catch (error) {
|
| 355 |
+
Logger.log('[DifyService] generateQuestionsFromDify error: ' + error.toString());
|
| 356 |
+
throw error;
|
| 357 |
+
}
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
/**
|
| 361 |
+
* 評価生成APIを呼び出し
|
| 362 |
+
*
|
| 363 |
+
* @param {string} subject - 教科ID(jp, math, sci, soc, または 'overall')
|
| 364 |
+
* @param {Object} statistics - ユーザー統計(教科別正答率など)
|
| 365 |
+
* @param {Array} results - 解答結果配列(各問題の正誤結果)
|
| 366 |
+
* @param {Object} subjectEvaluations - 教科別評価(overallの場合のみ使用、オプション)
|
| 367 |
+
* @returns {Object} - 生成された評価
|
| 368 |
+
*
|
| 369 |
+
* レスポンスフォーマット:
|
| 370 |
+
* 教科別評価: { subject, advice, strengths[], weaknesses[], recommended_topics[] }
|
| 371 |
+
* 全体評価: { overall_advice, strengths[], weaknesses[], next_steps[] }
|
| 372 |
+
*/
|
| 373 |
+
function generateEvaluationFromDify(subject, statistics, results, subjectEvaluations = null) {
|
| 374 |
+
try {
|
| 375 |
+
Logger.log('[DifyService] generateEvaluationFromDify: subject=' + subject);
|
| 376 |
+
|
| 377 |
+
// 追加の入力パラメータ
|
| 378 |
+
const additionalInputs = {
|
| 379 |
+
statistics: JSON.stringify(statistics),
|
| 380 |
+
results: JSON.stringify(results)
|
| 381 |
+
};
|
| 382 |
+
|
| 383 |
+
// 全体評価の場合、教科別評価も渡す
|
| 384 |
+
if (subject === 'overall' && subjectEvaluations) {
|
| 385 |
+
Logger.log('[DifyService] Adding subject_evaluations to overall evaluation');
|
| 386 |
+
additionalInputs.subject_evaluations = JSON.stringify(subjectEvaluations);
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
const response = callDifyWorkflow('generate_evaluation', subject, additionalInputs);
|
| 390 |
+
|
| 391 |
+
// Difyレスポンスから評価データを抽出
|
| 392 |
+
if (response.data && response.data.outputs) {
|
| 393 |
+
const outputs = response.data.outputs;
|
| 394 |
+
|
| 395 |
+
// エラーレスポンスチェック
|
| 396 |
+
if (outputs.action_error) {
|
| 397 |
+
throw new Error('Dify action error: ' + outputs.action_error);
|
| 398 |
+
}
|
| 399 |
+
if (outputs.subject_error_evaluation) {
|
| 400 |
+
throw new Error('Dify subject error: ' + outputs.subject_error_evaluation);
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
// 全体評価の場合
|
| 404 |
+
if (subject === 'overall') {
|
| 405 |
+
if (outputs.overall_evaluation) {
|
| 406 |
+
const evaluationData = outputs.overall_evaluation;
|
| 407 |
+
|
| 408 |
+
// JSON文字列の場合はパース
|
| 409 |
+
if (typeof evaluationData === 'string') {
|
| 410 |
+
Logger.log('[DifyService] Parsing overall_evaluation JSON string');
|
| 411 |
+
const parsed = JSON.parse(evaluationData);
|
| 412 |
+
Logger.log('[DifyService] Overall evaluation generated successfully');
|
| 413 |
+
return parsed;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
// 既にオブジェクトの場合
|
| 417 |
+
Logger.log('[DifyService] Overall evaluation generated successfully (object format)');
|
| 418 |
+
return evaluationData;
|
| 419 |
+
}
|
| 420 |
+
} else {
|
| 421 |
+
// 教科別評価の場合
|
| 422 |
+
const evaluationKey = SUBJECT_TO_EVALUATION_KEY[subject];
|
| 423 |
+
if (!evaluationKey) {
|
| 424 |
+
throw new Error('Invalid subject code for evaluation: ' + subject);
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
Logger.log('[DifyService] Looking for evaluation key: ' + evaluationKey);
|
| 428 |
+
|
| 429 |
+
if (outputs[evaluationKey]) {
|
| 430 |
+
const evaluationData = outputs[evaluationKey];
|
| 431 |
+
let parsed;
|
| 432 |
+
|
| 433 |
+
// JSON文字列の場合はパース
|
| 434 |
+
if (typeof evaluationData === 'string') {
|
| 435 |
+
Logger.log('[DifyService] Parsing JSON string from ' + evaluationKey);
|
| 436 |
+
parsed = JSON.parse(evaluationData);
|
| 437 |
+
} else {
|
| 438 |
+
// 既にオブジェクトの場合
|
| 439 |
+
parsed = evaluationData;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
// Difyの出力形式 {"評価": [...]} をGAS期待形式に変換
|
| 443 |
+
const normalized = normalizeEvaluationFormat(parsed, subject);
|
| 444 |
+
Logger.log('[DifyService] Evaluation generated successfully for ' + subject);
|
| 445 |
+
return normalized;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
Logger.log('[DifyService] Evaluation key "' + evaluationKey + '" not found in outputs: ' + JSON.stringify(Object.keys(outputs)));
|
| 449 |
+
}
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
throw new Error('Invalid response format from Dify: evaluation data not found');
|
| 453 |
+
|
| 454 |
+
} catch (error) {
|
| 455 |
+
Logger.log('[DifyService] generateEvaluationFromDify error: ' + error.toString());
|
| 456 |
+
throw error;
|
| 457 |
+
}
|
| 458 |
+
}
|
V1.7.1/gas/GenreMaster.js
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* ジャンルマスターデータ
|
| 3 |
+
*
|
| 4 |
+
* 中学受験4教科のジャンル分類を定義
|
| 5 |
+
*
|
| 6 |
+
* @version 1.0.0
|
| 7 |
+
* @date 2025-12-07
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
// ============================================================================
|
| 11 |
+
// ジャンルマスターデータ
|
| 12 |
+
// ============================================================================
|
| 13 |
+
|
| 14 |
+
/**
|
| 15 |
+
* 中学受験4教科のジャンル分類マスターデータ
|
| 16 |
+
* @constant {Object}
|
| 17 |
+
*/
|
| 18 |
+
var GENRE_MASTER = {
|
| 19 |
+
// 国語(8ジャンル)
|
| 20 |
+
jp: {
|
| 21 |
+
subject_id: 'jp',
|
| 22 |
+
subject_name: '国語',
|
| 23 |
+
genres: [
|
| 24 |
+
{
|
| 25 |
+
id: 'JP01',
|
| 26 |
+
name: '漢字・語彙',
|
| 27 |
+
description: '読み書き、四字熟語、慣用句、ことわざ'
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
id: 'JP02',
|
| 31 |
+
name: '文法・言葉のきまり',
|
| 32 |
+
description: '品詞、敬語、文の成分、修飾関係'
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
id: 'JP03',
|
| 36 |
+
name: '物語文読解',
|
| 37 |
+
description: '心情理解、場面把握、人物関係'
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
id: 'JP04',
|
| 41 |
+
name: '説明文・論説文読解',
|
| 42 |
+
description: '要旨、段落構成、筆者の主張'
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
id: 'JP05',
|
| 46 |
+
name: '随筆文読解',
|
| 47 |
+
description: '筆者の体験・感想の読み取り'
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
id: 'JP06',
|
| 51 |
+
name: '詩・韻文',
|
| 52 |
+
description: '詩、短歌、俳句、表現技法'
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
id: 'JP07',
|
| 56 |
+
name: '記述問題',
|
| 57 |
+
description: '理由説明、要約、意見記述'
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
id: 'JP08',
|
| 61 |
+
name: '知識・文学史',
|
| 62 |
+
description: '作家、作品名、文学的常識'
|
| 63 |
+
}
|
| 64 |
+
]
|
| 65 |
+
},
|
| 66 |
+
|
| 67 |
+
// 算数(10ジャンル)
|
| 68 |
+
math: {
|
| 69 |
+
subject_id: 'math',
|
| 70 |
+
subject_name: '算数',
|
| 71 |
+
genres: [
|
| 72 |
+
{
|
| 73 |
+
id: 'MA01',
|
| 74 |
+
name: '計算',
|
| 75 |
+
description: '四則演算、分数・小数、逆算'
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
id: 'MA02',
|
| 79 |
+
name: '数の性質',
|
| 80 |
+
description: '約数・倍数、素因数分解、規則性'
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
id: 'MA03',
|
| 84 |
+
name: '割合・比',
|
| 85 |
+
description: '割合、比、百分率、歩合'
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
id: 'MA04',
|
| 89 |
+
name: '速さ',
|
| 90 |
+
description: '旅人算、通過算、流水算、時計算'
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
id: 'MA05',
|
| 94 |
+
name: '文章題(その他)',
|
| 95 |
+
description: '濃度、仕事算、ニュートン算、差集め算'
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
id: 'MA06',
|
| 99 |
+
name: '平面図形',
|
| 100 |
+
description: '面積、角度、相似、合同'
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
id: 'MA07',
|
| 104 |
+
name: '立体図形',
|
| 105 |
+
description: '体積、表面積、展開図、切断'
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
id: 'MA08',
|
| 109 |
+
name: '場合の数・確率',
|
| 110 |
+
description: '順列、組み合わせ、確率'
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
id: 'MA09',
|
| 114 |
+
name: 'グラフ・表',
|
| 115 |
+
description: '統計、変化のグラフ、ダイヤグラム'
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
id: 'MA10',
|
| 119 |
+
name: '特殊算',
|
| 120 |
+
description: 'つるかめ算、消去算、過不足算'
|
| 121 |
+
}
|
| 122 |
+
]
|
| 123 |
+
},
|
| 124 |
+
|
| 125 |
+
// 理科(12ジャンル)
|
| 126 |
+
sci: {
|
| 127 |
+
subject_id: 'sci',
|
| 128 |
+
subject_name: '理科',
|
| 129 |
+
genres: [
|
| 130 |
+
{
|
| 131 |
+
id: 'SC01',
|
| 132 |
+
name: '力・運動',
|
| 133 |
+
description: 'てこ、滑車、ばね、浮力、振り子'
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
id: 'SC02',
|
| 137 |
+
name: '電気',
|
| 138 |
+
description: '回路、抵抗、電磁石、発熱'
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
id: 'SC03',
|
| 142 |
+
name: '光・音・熱',
|
| 143 |
+
description: '反射、屈折、レンズ、音の性質'
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
id: 'SC04',
|
| 147 |
+
name: '物質の性質',
|
| 148 |
+
description: '金属、気体、密度、状態変化'
|
| 149 |
+
},
|
| 150 |
+
{
|
| 151 |
+
id: 'SC05',
|
| 152 |
+
name: '水溶液',
|
| 153 |
+
description: '酸・アルカリ、中和、溶解度'
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
id: 'SC06',
|
| 157 |
+
name: '燃焼・化学変化',
|
| 158 |
+
description: '燃焼、酸化還元、化合'
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
id: 'SC07',
|
| 162 |
+
name: '植物',
|
| 163 |
+
description: 'つくり、光合成、蒸散、分類'
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
id: 'SC08',
|
| 167 |
+
name: '動物',
|
| 168 |
+
description: 'からだのつくり、行動、分類'
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
id: 'SC09',
|
| 172 |
+
name: '人体',
|
| 173 |
+
description: '消化、呼吸、血液循環、感覚器官'
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
id: 'SC10',
|
| 177 |
+
name: '天体',
|
| 178 |
+
description: '太陽、月、星座、地球の運動'
|
| 179 |
+
},
|
| 180 |
+
{
|
| 181 |
+
id: 'SC11',
|
| 182 |
+
name: '気象',
|
| 183 |
+
description: '天気、気温、湿度、雲、季節風'
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
id: 'SC12',
|
| 187 |
+
name: '地学',
|
| 188 |
+
description: '地層、岩石、火山、地震'
|
| 189 |
+
}
|
| 190 |
+
]
|
| 191 |
+
},
|
| 192 |
+
|
| 193 |
+
// 社会(10ジャンル)
|
| 194 |
+
soc: {
|
| 195 |
+
subject_id: 'soc',
|
| 196 |
+
subject_name: '社会',
|
| 197 |
+
genres: [
|
| 198 |
+
{
|
| 199 |
+
id: 'SO01',
|
| 200 |
+
name: '日本地理(国土・自然)',
|
| 201 |
+
description: '地形、気候、都道府県'
|
| 202 |
+
},
|
| 203 |
+
{
|
| 204 |
+
id: 'SO02',
|
| 205 |
+
name: '日本地理(産業)',
|
| 206 |
+
description: '農業、工業、水産業、商業'
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
id: 'SO03',
|
| 210 |
+
name: '世界地理',
|
| 211 |
+
description: '大陸、国、貿易、環境問題'
|
| 212 |
+
},
|
| 213 |
+
{
|
| 214 |
+
id: 'SO04',
|
| 215 |
+
name: '歴史(古代〜平安)',
|
| 216 |
+
description: '旧石器〜平安時代'
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
id: 'SO05',
|
| 220 |
+
name: '歴史(鎌倉〜室町)',
|
| 221 |
+
description: '武士の台頭、文化'
|
| 222 |
+
},
|
| 223 |
+
{
|
| 224 |
+
id: 'SO06',
|
| 225 |
+
name: '歴史(安土桃山〜江戸)',
|
| 226 |
+
description: '統一、鎖国、元禄・化政文化'
|
| 227 |
+
},
|
| 228 |
+
{
|
| 229 |
+
id: 'SO07',
|
| 230 |
+
name: '歴史(明治〜現代)',
|
| 231 |
+
description: '近代化、戦争、戦後'
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
id: 'SO08',
|
| 235 |
+
name: '公民(政治・憲法)',
|
| 236 |
+
description: '三権分立、選挙、人権'
|
| 237 |
+
},
|
| 238 |
+
{
|
| 239 |
+
id: 'SO09',
|
| 240 |
+
name: '公民(経済・国際)',
|
| 241 |
+
description: '経済の仕組み、国際機関、SDGs'
|
| 242 |
+
},
|
| 243 |
+
{
|
| 244 |
+
id: 'SO10',
|
| 245 |
+
name: '時事問題',
|
| 246 |
+
description: '直近1〜2年のニュース'
|
| 247 |
+
}
|
| 248 |
+
]
|
| 249 |
+
}
|
| 250 |
+
};
|
| 251 |
+
|
| 252 |
+
// ============================================================================
|
| 253 |
+
// ヘルパー関数
|
| 254 |
+
// ============================================================================
|
| 255 |
+
|
| 256 |
+
/**
|
| 257 |
+
* ジャンルIDからジャンル情報を取得
|
| 258 |
+
*
|
| 259 |
+
* @param {string} genreId - ジャンルID(例: 'JP01', 'MA05')
|
| 260 |
+
* @returns {Object|null} - ジャンル情報 { id, name, description, subject_id, subject_name } または null
|
| 261 |
+
*
|
| 262 |
+
* @example
|
| 263 |
+
* var genre = getGenreById('JP01');
|
| 264 |
+
* // => { id: 'JP01', name: '漢字・語彙', description: '...', subject_id: 'jp', subject_name: '国語' }
|
| 265 |
+
*/
|
| 266 |
+
function getGenreById(genreId) {
|
| 267 |
+
if (!genreId || typeof genreId !== 'string') {
|
| 268 |
+
return null;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
var upperGenreId = genreId.toUpperCase();
|
| 272 |
+
|
| 273 |
+
// 全教科を検索
|
| 274 |
+
var subjects = ['jp', 'math', 'sci', 'soc'];
|
| 275 |
+
for (var i = 0; i < subjects.length; i++) {
|
| 276 |
+
var subjectId = subjects[i];
|
| 277 |
+
var subjectData = GENRE_MASTER[subjectId];
|
| 278 |
+
var genres = subjectData.genres;
|
| 279 |
+
|
| 280 |
+
for (var j = 0; j < genres.length; j++) {
|
| 281 |
+
var genre = genres[j];
|
| 282 |
+
if (genre.id === upperGenreId) {
|
| 283 |
+
// ジャンル情報に教科情報を追加して返却
|
| 284 |
+
return {
|
| 285 |
+
id: genre.id,
|
| 286 |
+
name: genre.name,
|
| 287 |
+
description: genre.description,
|
| 288 |
+
subject_id: subjectData.subject_id,
|
| 289 |
+
subject_name: subjectData.subject_name
|
| 290 |
+
};
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
return null;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
/**
|
| 299 |
+
* 教科のジャンル一覧を取得
|
| 300 |
+
*
|
| 301 |
+
* @param {string} subjectId - 教科ID(jp, math, sci, soc)
|
| 302 |
+
* @returns {Array} - ジャンル配列(見つからない場合は空配列)
|
| 303 |
+
*
|
| 304 |
+
* @example
|
| 305 |
+
* var genres = getGenresBySubject('jp');
|
| 306 |
+
* // => [{ id: 'JP01', name: '漢字・語彙', ... }, ...]
|
| 307 |
+
*/
|
| 308 |
+
function getGenresBySubject(subjectId) {
|
| 309 |
+
if (!subjectId || typeof subjectId !== 'string') {
|
| 310 |
+
return [];
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
var subjectData = GENRE_MASTER[subjectId.toLowerCase()];
|
| 314 |
+
if (!subjectData) {
|
| 315 |
+
return [];
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
return subjectData.genres;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
/**
|
| 322 |
+
* 全ジャンル数を取得
|
| 323 |
+
*
|
| 324 |
+
* @returns {number} - 総ジャンル数(40)
|
| 325 |
+
*
|
| 326 |
+
* @example
|
| 327 |
+
* var total = getTotalGenreCount();
|
| 328 |
+
* // => 40
|
| 329 |
+
*/
|
| 330 |
+
function getTotalGenreCount() {
|
| 331 |
+
var count = 0;
|
| 332 |
+
var subjects = ['jp', 'math', 'sci', 'soc'];
|
| 333 |
+
|
| 334 |
+
for (var i = 0; i < subjects.length; i++) {
|
| 335 |
+
var subjectId = subjects[i];
|
| 336 |
+
count += GENRE_MASTER[subjectId].genres.length;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
return count;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
/**
|
| 343 |
+
* ジャンルIDの検証
|
| 344 |
+
*
|
| 345 |
+
* @param {string} genreId - ジャンルID
|
| 346 |
+
* @returns {boolean} - 有効なジャンルIDかどうか
|
| 347 |
+
*
|
| 348 |
+
* @example
|
| 349 |
+
* isValidGenreId('JP01'); // => true
|
| 350 |
+
* isValidGenreId('XX99'); // => false
|
| 351 |
+
*/
|
| 352 |
+
function isValidGenreId(genreId) {
|
| 353 |
+
return getGenreById(genreId) !== null;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
/**
|
| 357 |
+
* Dify用のジャンルリストを生成(問題生成プロンプト用)
|
| 358 |
+
*
|
| 359 |
+
* @param {string} subjectId - 教科ID(jp, math, sci, soc)
|
| 360 |
+
* @returns {string} - ジャンルリストの文字列(改行区切り)
|
| 361 |
+
*
|
| 362 |
+
* @example
|
| 363 |
+
* var list = getGenreListForDify('jp');
|
| 364 |
+
* // => "JP01: 漢字・語彙(読み書き、四字熟語、慣用句、ことわざ)\n..."
|
| 365 |
+
*/
|
| 366 |
+
function getGenreListForDify(subjectId) {
|
| 367 |
+
var genres = getGenresBySubject(subjectId);
|
| 368 |
+
|
| 369 |
+
if (genres.length === 0) {
|
| 370 |
+
return '';
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
var lines = [];
|
| 374 |
+
for (var i = 0; i < genres.length; i++) {
|
| 375 |
+
var genre = genres[i];
|
| 376 |
+
var line = genre.id + ': ' + genre.name + '(' + genre.description + ')';
|
| 377 |
+
lines.push(line);
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
return lines.join('\n');
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
// ============================================================================
|
| 384 |
+
// テスト関数(GASエディタで実行可能)
|
| 385 |
+
// ============================================================================
|
| 386 |
+
|
| 387 |
+
/**
|
| 388 |
+
* GenreMaster.jsのテスト関数
|
| 389 |
+
* GASエディタで実行して動作確認
|
| 390 |
+
*/
|
| 391 |
+
function testGenreMaster() {
|
| 392 |
+
Logger.log('=== GenreMaster.js テスト開始 ===');
|
| 393 |
+
|
| 394 |
+
// テスト1: ジャンル総数
|
| 395 |
+
var totalCount = getTotalGenreCount();
|
| 396 |
+
Logger.log('テスト1: 総ジャンル数 = ' + totalCount + ' (期待値: 40)');
|
| 397 |
+
|
| 398 |
+
// テスト2: ジャンルID検索
|
| 399 |
+
var genre1 = getGenreById('JP01');
|
| 400 |
+
Logger.log('テスト2: getGenreById("JP01") = ' + JSON.stringify(genre1));
|
| 401 |
+
|
| 402 |
+
var genre2 = getGenreById('MA05');
|
| 403 |
+
Logger.log('テスト2: getGenreById("MA05") = ' + JSON.stringify(genre2));
|
| 404 |
+
|
| 405 |
+
// テスト3: 教科別ジャンル取得
|
| 406 |
+
var jpGenres = getGenresBySubject('jp');
|
| 407 |
+
Logger.log('テスト3: 国語ジャンル数 = ' + jpGenres.length + ' (期待値: 8)');
|
| 408 |
+
|
| 409 |
+
var mathGenres = getGenresBySubject('math');
|
| 410 |
+
Logger.log('テスト3: 算数ジャンル数 = ' + mathGenres.length + ' (期待値: 10)');
|
| 411 |
+
|
| 412 |
+
var sciGenres = getGenresBySubject('sci');
|
| 413 |
+
Logger.log('テスト3: 理科ジャンル数 = ' + sciGenres.length + ' (期待値: 12)');
|
| 414 |
+
|
| 415 |
+
var socGenres = getGenresBySubject('soc');
|
| 416 |
+
Logger.log('テスト3: 社会ジャンル数 = ' + socGenres.length + ' (期待値: 10)');
|
| 417 |
+
|
| 418 |
+
// テスト4: ジャンルID検証
|
| 419 |
+
var valid1 = isValidGenreId('JP01');
|
| 420 |
+
Logger.log('テスト4: isValidGenreId("JP01") = ' + valid1 + ' (期待値: true)');
|
| 421 |
+
|
| 422 |
+
var valid2 = isValidGenreId('XX99');
|
| 423 |
+
Logger.log('テスト4: isValidGenreId("XX99") = ' + valid2 + ' (期待値: false)');
|
| 424 |
+
|
| 425 |
+
// テスト5: Dify用ジャンルリスト生成
|
| 426 |
+
var jpList = getGenreListForDify('jp');
|
| 427 |
+
Logger.log('テスト5: 国語ジャンルリスト(最初の100文字):');
|
| 428 |
+
Logger.log(jpList.substring(0, 100) + '...');
|
| 429 |
+
|
| 430 |
+
Logger.log('=== GenreMaster.js テスト完了 ===');
|
| 431 |
+
}
|
V1.7.1/gas/appsscript.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"timeZone": "Asia/Tokyo",
|
| 3 |
+
"dependencies": {
|
| 4 |
+
},
|
| 5 |
+
"exceptionLogging": "STACKDRIVER",
|
| 6 |
+
"runtimeVersion": "V8",
|
| 7 |
+
"oauthScopes": [
|
| 8 |
+
"https://www.googleapis.com/auth/script.external_request",
|
| 9 |
+
"https://www.googleapis.com/auth/spreadsheets"
|
| 10 |
+
],
|
| 11 |
+
"webapp": {
|
| 12 |
+
"executeAs": "USER_DEPLOYING",
|
| 13 |
+
"access": "ANYONE_ANONYMOUS"
|
| 14 |
+
}
|
| 15 |
+
}
|
V1.7.1/gas/questiondb_functions.js
ADDED
|
@@ -0,0 +1,816 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* QuestionDatabase機能
|
| 3 |
+
*
|
| 4 |
+
* 機能:
|
| 5 |
+
* - 事前生成された「回答させたい答え」をGoogle Sheetsで管理
|
| 6 |
+
* - 問題生成時に使用回数が少ないものからランダムに選択
|
| 7 |
+
*
|
| 8 |
+
* シート名: QuestionDatabase
|
| 9 |
+
*
|
| 10 |
+
* スキーマ(8カラム):
|
| 11 |
+
* A: answer_id (string) - 一意ID (例: jp_001, math_042)
|
| 12 |
+
* B: subject (string) - 教科コード (jp, math, sci, soc)
|
| 13 |
+
* C: category (string) - ジャンルID (例: JP01, MA03, SC07)
|
| 14 |
+
* D: answer (string) - 回答させたい答え
|
| 15 |
+
* E: question_hint (string) - 設問方針
|
| 16 |
+
* F: difficulty (string) - 基本/標準/応用
|
| 17 |
+
* G: source_context (string) - 教材での使用例
|
| 18 |
+
* H: usage_count (number) - 使用回数(初期値0)
|
| 19 |
+
*/
|
| 20 |
+
|
| 21 |
+
// ==================== 定数定義 ====================
|
| 22 |
+
|
| 23 |
+
const QUESTIONDB_SHEET_NAME = 'QuestionDatabase';
|
| 24 |
+
|
| 25 |
+
const QUESTIONDB_COLUMNS = {
|
| 26 |
+
ANSWER_ID: 1, // A列
|
| 27 |
+
SUBJECT: 2, // B列
|
| 28 |
+
CATEGORY: 3, // C列
|
| 29 |
+
ANSWER: 4, // D列
|
| 30 |
+
QUESTION_HINT: 5, // E列
|
| 31 |
+
DIFFICULTY: 6, // F列
|
| 32 |
+
SOURCE_CONTEXT: 7, // G列
|
| 33 |
+
USAGE_COUNT: 8 // H列
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
const QUESTIONDB_HEADER = [
|
| 37 |
+
'answer_id',
|
| 38 |
+
'subject',
|
| 39 |
+
'category',
|
| 40 |
+
'answer',
|
| 41 |
+
'question_hint',
|
| 42 |
+
'difficulty',
|
| 43 |
+
'source_context',
|
| 44 |
+
'usage_count'
|
| 45 |
+
];
|
| 46 |
+
|
| 47 |
+
/**
|
| 48 |
+
* ジャンル構成定義(各教科10問)
|
| 49 |
+
*
|
| 50 |
+
* 国語: JP01(4), JP02(2), JP03-06各1 = 10問
|
| 51 |
+
* 算数: MA01-MA10 各1問 = 10問
|
| 52 |
+
* 理科: SC01-SC10 各1問 = 10問
|
| 53 |
+
* 社会: SO01-SO10 各1問 = 10問
|
| 54 |
+
*/
|
| 55 |
+
const GENRE_CONFIG = {
|
| 56 |
+
"jp": {"JP01": 4, "JP02": 2, "JP03": 1, "JP04": 1, "JP05": 1, "JP06": 1},
|
| 57 |
+
"math": {"MA01": 1, "MA02": 1, "MA03": 1, "MA04": 1, "MA05": 1, "MA06": 1, "MA07": 1, "MA08": 1, "MA09": 1, "MA10": 1},
|
| 58 |
+
"sci": {"SC01": 1, "SC02": 1, "SC03": 1, "SC04": 1, "SC05": 1, "SC06": 1, "SC07": 1, "SC08": 1, "SC09": 1, "SC10": 1},
|
| 59 |
+
"soc": {"SO01": 1, "SO02": 1, "SO03": 1, "SO04": 1, "SO05": 1, "SO06": 1, "SO07": 1, "SO08": 1, "SO09": 1, "SO10": 1}
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
// ==================== ユーティリティ関数 ====================
|
| 63 |
+
|
| 64 |
+
/**
|
| 65 |
+
* QuestionDatabaseシートを取得(なければ作成)
|
| 66 |
+
* @returns {GoogleAppsScript.Spreadsheet.Sheet}
|
| 67 |
+
*/
|
| 68 |
+
function getQuestionDatabaseSheet_() {
|
| 69 |
+
const ss = SpreadsheetApp.getActiveSpreadsheet();
|
| 70 |
+
let sheet = ss.getSheetByName(QUESTIONDB_SHEET_NAME);
|
| 71 |
+
|
| 72 |
+
if (!sheet) {
|
| 73 |
+
sheet = ss.insertSheet(QUESTIONDB_SHEET_NAME);
|
| 74 |
+
// ヘッダー行を設定
|
| 75 |
+
sheet.getRange(1, 1, 1, QUESTIONDB_HEADER.length).setValues([QUESTIONDB_HEADER]);
|
| 76 |
+
sheet.getRange(1, 1, 1, QUESTIONDB_HEADER.length).setFontWeight('bold');
|
| 77 |
+
sheet.setFrozenRows(1);
|
| 78 |
+
|
| 79 |
+
Logger.log('QuestionDatabaseシートを新規作成しました');
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
return sheet;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/**
|
| 86 |
+
* 行データをオブジェクトに変換
|
| 87 |
+
* @param {Array} row - スプレッドシートの行データ
|
| 88 |
+
* @returns {Object}
|
| 89 |
+
*/
|
| 90 |
+
function rowToQuestionObject_(row) {
|
| 91 |
+
return {
|
| 92 |
+
answer_id: row[0] || '',
|
| 93 |
+
subject: row[1] || '',
|
| 94 |
+
category: row[2] || '',
|
| 95 |
+
answer: row[3] || '',
|
| 96 |
+
question_hint: row[4] || '',
|
| 97 |
+
difficulty: row[5] || '',
|
| 98 |
+
source_context: row[6] || '',
|
| 99 |
+
usage_count: row[7] || 0
|
| 100 |
+
};
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
/**
|
| 104 |
+
* オブジェクトを行データに変換
|
| 105 |
+
* @param {Object} obj - 問題データオブジェクト
|
| 106 |
+
* @returns {Array}
|
| 107 |
+
*/
|
| 108 |
+
function questionObjectToRow_(obj) {
|
| 109 |
+
return [
|
| 110 |
+
obj.answer_id || '',
|
| 111 |
+
obj.subject || '',
|
| 112 |
+
obj.category || '',
|
| 113 |
+
obj.answer || '',
|
| 114 |
+
obj.question_hint || '',
|
| 115 |
+
obj.difficulty || '',
|
| 116 |
+
obj.source_context || '',
|
| 117 |
+
obj.usage_count || 0
|
| 118 |
+
];
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// ==================== メインAPI ====================
|
| 122 |
+
|
| 123 |
+
/**
|
| 124 |
+
* 教科を指定してランダムに答えを取得
|
| 125 |
+
*
|
| 126 |
+
* アルゴリズム:
|
| 127 |
+
* 1. subject で絞り込み
|
| 128 |
+
* 2. usage_count の最小値を取得
|
| 129 |
+
* 3. 最小値のレコードに絞り込み
|
| 130 |
+
* 4. その中からランダムに count 件抽出
|
| 131 |
+
*
|
| 132 |
+
* @param {string} subject - 教科コード (jp, math, sci, soc)
|
| 133 |
+
* @param {number} count - 取得件数
|
| 134 |
+
* @returns {Array<Object>} 取得した答えデータの配列
|
| 135 |
+
*
|
| 136 |
+
* @example
|
| 137 |
+
* // 国語から3件取得
|
| 138 |
+
* const answers = get_random_answers('jp', 3);
|
| 139 |
+
* console.log(answers);
|
| 140 |
+
* // [
|
| 141 |
+
* // { answer_id: 'jp_001', subject: 'jp', category: 'JP01', answer: '漢字', ... },
|
| 142 |
+
* // { answer_id: 'jp_042', subject: 'jp', category: 'JP02', answer: '文法', ... },
|
| 143 |
+
* // ...
|
| 144 |
+
* // ]
|
| 145 |
+
*/
|
| 146 |
+
function get_random_answers(subject, count) {
|
| 147 |
+
try {
|
| 148 |
+
const sheet = getQuestionDatabaseSheet_();
|
| 149 |
+
const lastRow = sheet.getLastRow();
|
| 150 |
+
|
| 151 |
+
if (lastRow <= 1) {
|
| 152 |
+
Logger.log('QuestionDatabaseにデータがありません');
|
| 153 |
+
return [];
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// 全データを取得(ヘッダー行を除く)
|
| 157 |
+
const dataRange = sheet.getRange(2, 1, lastRow - 1, QUESTIONDB_HEADER.length);
|
| 158 |
+
const allData = dataRange.getValues();
|
| 159 |
+
|
| 160 |
+
// subjectで絞り込み
|
| 161 |
+
const filteredData = allData
|
| 162 |
+
.map((row, index) => ({ row, originalIndex: index + 2 })) // 行番号を保持(2行目から)
|
| 163 |
+
.filter(item => item.row[QUESTIONDB_COLUMNS.SUBJECT - 1] === subject);
|
| 164 |
+
|
| 165 |
+
if (filteredData.length === 0) {
|
| 166 |
+
Logger.log(`教科コード '${subject}' のデータが見つかりませんでした`);
|
| 167 |
+
return [];
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
// usage_countの最小値を取得
|
| 171 |
+
const usageCounts = filteredData.map(item => item.row[QUESTIONDB_COLUMNS.USAGE_COUNT - 1] || 0);
|
| 172 |
+
const minUsageCount = Math.min(...usageCounts);
|
| 173 |
+
|
| 174 |
+
Logger.log(`教科 '${subject}' の最小使用回数: ${minUsageCount}`);
|
| 175 |
+
|
| 176 |
+
// 最小値のレコードに絞り込み
|
| 177 |
+
const minUsageData = filteredData.filter(
|
| 178 |
+
item => (item.row[QUESTIONDB_COLUMNS.USAGE_COUNT - 1] || 0) === minUsageCount
|
| 179 |
+
);
|
| 180 |
+
|
| 181 |
+
Logger.log(`最小使用回数のレコード数: ${minUsageData.length}`);
|
| 182 |
+
|
| 183 |
+
// ランダムに count 件抽出
|
| 184 |
+
const selectedCount = Math.min(count, minUsageData.length);
|
| 185 |
+
const shuffled = minUsageData.sort(() => Math.random() - 0.5);
|
| 186 |
+
const selected = shuffled.slice(0, selectedCount);
|
| 187 |
+
|
| 188 |
+
// オブジェクト形式に変換
|
| 189 |
+
const results = selected.map(item => rowToQuestionObject_(item.row));
|
| 190 |
+
|
| 191 |
+
Logger.log(`${selectedCount}件の答えを取得しました`);
|
| 192 |
+
return results;
|
| 193 |
+
|
| 194 |
+
} catch (error) {
|
| 195 |
+
Logger.log(`get_random_answers エラー: ${error.message}`);
|
| 196 |
+
throw error;
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/**
|
| 201 |
+
* ジャンル構成に基づいて問題を取得(v1.6.3新規)
|
| 202 |
+
*
|
| 203 |
+
* 各ジャンル内でusage_countが最小のレコードからランダムに指定数を抽出
|
| 204 |
+
* これにより全ジャンルから均等に出題され、同じ問題の繰り返しを防ぐ
|
| 205 |
+
*
|
| 206 |
+
* @param {Array<string>} subjects - 教科コードの配列 (例: ["jp", "math", "sci", "soc"])
|
| 207 |
+
* @returns {Array<Object>} 取得した問題データの配列(全教科分を結合)
|
| 208 |
+
*
|
| 209 |
+
* @example
|
| 210 |
+
* const questions = get_questions_by_genre_config(["jp", "math"]);
|
| 211 |
+
* // 国語10問 + 算数10問 = 20問が返る
|
| 212 |
+
*/
|
| 213 |
+
function get_questions_by_genre_config(subjects) {
|
| 214 |
+
try {
|
| 215 |
+
const sheet = getQuestionDatabaseSheet_();
|
| 216 |
+
const lastRow = sheet.getLastRow();
|
| 217 |
+
|
| 218 |
+
if (lastRow <= 1) {
|
| 219 |
+
Logger.log('QuestionDatabaseにデータがありません');
|
| 220 |
+
return [];
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
// 全データを取得(ヘッダー行を除く)
|
| 224 |
+
const dataRange = sheet.getRange(2, 1, lastRow - 1, QUESTIONDB_HEADER.length);
|
| 225 |
+
const allData = dataRange.getValues();
|
| 226 |
+
|
| 227 |
+
const results = [];
|
| 228 |
+
|
| 229 |
+
// 各教科を処理
|
| 230 |
+
for (const subject of subjects) {
|
| 231 |
+
const genreConfig = GENRE_CONFIG[subject];
|
| 232 |
+
|
| 233 |
+
if (!genreConfig) {
|
| 234 |
+
Logger.log(`未知の教科コード: ${subject} - スキップ`);
|
| 235 |
+
continue;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
Logger.log(`教科 '${subject}' を処理中...`);
|
| 239 |
+
|
| 240 |
+
// 各ジャンルを処理
|
| 241 |
+
for (const [genre, count] of Object.entries(genreConfig)) {
|
| 242 |
+
// このジャンルのレコードを抽出
|
| 243 |
+
const genreData = allData
|
| 244 |
+
.map((row, index) => ({ row, originalIndex: index + 2 }))
|
| 245 |
+
.filter(item =>
|
| 246 |
+
item.row[QUESTIONDB_COLUMNS.SUBJECT - 1] === subject &&
|
| 247 |
+
item.row[QUESTIONDB_COLUMNS.CATEGORY - 1] === genre
|
| 248 |
+
);
|
| 249 |
+
|
| 250 |
+
if (genreData.length === 0) {
|
| 251 |
+
Logger.log(`ジャンル '${genre}' のデータがありません`);
|
| 252 |
+
continue;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
// v1.6.14: usage_count最小優先で抽出、不足分は次のusage_countから補充
|
| 256 |
+
// ユニークなusage_count値をソート
|
| 257 |
+
const usageCounts = genreData.map(item => item.row[QUESTIONDB_COLUMNS.USAGE_COUNT - 1] || 0);
|
| 258 |
+
const uniqueUsageCounts = [...new Set(usageCounts)].sort((a, b) => a - b);
|
| 259 |
+
|
| 260 |
+
const selected = [];
|
| 261 |
+
let remaining = count;
|
| 262 |
+
|
| 263 |
+
// usage_countが小さい順に必要数を満たすまで抽出
|
| 264 |
+
for (const usageCount of uniqueUsageCounts) {
|
| 265 |
+
if (remaining <= 0) break;
|
| 266 |
+
|
| 267 |
+
// この usage_count のレコードを取得
|
| 268 |
+
const usageData = genreData.filter(
|
| 269 |
+
item => (item.row[QUESTIONDB_COLUMNS.USAGE_COUNT - 1] || 0) === usageCount
|
| 270 |
+
);
|
| 271 |
+
|
| 272 |
+
// ランダムにシャッフルして不足分だけ抽出
|
| 273 |
+
const shuffled = usageData.sort(() => Math.random() - 0.5);
|
| 274 |
+
const toTake = Math.min(remaining, shuffled.length);
|
| 275 |
+
|
| 276 |
+
for (let i = 0; i < toTake; i++) {
|
| 277 |
+
selected.push(shuffled[i]);
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
remaining -= toTake;
|
| 281 |
+
|
| 282 |
+
if (toTake > 0) {
|
| 283 |
+
Logger.log(` usage_count=${usageCount}: ${toTake}問取得`);
|
| 284 |
+
}
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
// v1.6.15: 抽出時にUSAGE_COUNTを+1(抽出と同時に更新)
|
| 288 |
+
for (const item of selected) {
|
| 289 |
+
const currentUsage = item.row[QUESTIONDB_COLUMNS.USAGE_COUNT - 1] || 0;
|
| 290 |
+
const newUsage = currentUsage + 1;
|
| 291 |
+
sheet.getRange(item.originalIndex, QUESTIONDB_COLUMNS.USAGE_COUNT).setValue(newUsage);
|
| 292 |
+
// メモリ上のデータも更新(後続のジャンルで正しいusage_countを参照するため)
|
| 293 |
+
item.row[QUESTIONDB_COLUMNS.USAGE_COUNT - 1] = newUsage;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
// オブジェクト形式に変換して結果に追加
|
| 297 |
+
for (const item of selected) {
|
| 298 |
+
results.push(rowToQuestionObject_(item.row));
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
Logger.log(`ジャンル '${genre}': ${selected.length}/${count}問取得(USAGE_COUNT更新済)`);
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
Logger.log(`合計 ${results.length} 問を取得しました`);
|
| 306 |
+
return results;
|
| 307 |
+
|
| 308 |
+
} catch (error) {
|
| 309 |
+
Logger.log(`get_questions_by_genre_config エラー: ${error.message}`);
|
| 310 |
+
throw error;
|
| 311 |
+
}
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
/**
|
| 315 |
+
* 指定した答えIDの使用回数を+1する
|
| 316 |
+
*
|
| 317 |
+
* @param {Array<string>} answerIds - 答えIDの配列
|
| 318 |
+
* @returns {Object} 更新結果 { success: true, updated_count: number }
|
| 319 |
+
*
|
| 320 |
+
* @example
|
| 321 |
+
* // 2件の使用回数を更新
|
| 322 |
+
* const result = update_usage_count(['jp_001', 'jp_042']);
|
| 323 |
+
* console.log(result); // { success: true, updated_count: 2 }
|
| 324 |
+
*/
|
| 325 |
+
function update_usage_count(answerIds) {
|
| 326 |
+
try {
|
| 327 |
+
if (!Array.isArray(answerIds) || answerIds.length === 0) {
|
| 328 |
+
Logger.log('answer_ids が空または無効です');
|
| 329 |
+
return { success: false, updated_count: 0, error: 'Invalid answer_ids' };
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
const sheet = getQuestionDatabaseSheet_();
|
| 333 |
+
const lastRow = sheet.getLastRow();
|
| 334 |
+
|
| 335 |
+
if (lastRow <= 1) {
|
| 336 |
+
Logger.log('QuestionDatabaseにデータがありません');
|
| 337 |
+
return { success: false, updated_count: 0, error: 'No data' };
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
// 全データを取得
|
| 341 |
+
const dataRange = sheet.getRange(2, 1, lastRow - 1, QUESTIONDB_HEADER.length);
|
| 342 |
+
const allData = dataRange.getValues();
|
| 343 |
+
|
| 344 |
+
let updatedCount = 0;
|
| 345 |
+
|
| 346 |
+
// answer_id でマッチする行を探して usage_count を +1
|
| 347 |
+
allData.forEach((row, index) => {
|
| 348 |
+
const answerId = row[QUESTIONDB_COLUMNS.ANSWER_ID - 1];
|
| 349 |
+
|
| 350 |
+
if (answerIds.includes(answerId)) {
|
| 351 |
+
const currentUsage = row[QUESTIONDB_COLUMNS.USAGE_COUNT - 1] || 0;
|
| 352 |
+
const newUsage = currentUsage + 1;
|
| 353 |
+
|
| 354 |
+
// usage_count セルを更新
|
| 355 |
+
const rowNumber = index + 2; // ヘッダー行を考慮
|
| 356 |
+
sheet.getRange(rowNumber, QUESTIONDB_COLUMNS.USAGE_COUNT).setValue(newUsage);
|
| 357 |
+
|
| 358 |
+
Logger.log(`${answerId} の使用回数を ${currentUsage} → ${newUsage} に更新`);
|
| 359 |
+
updatedCount++;
|
| 360 |
+
}
|
| 361 |
+
});
|
| 362 |
+
|
| 363 |
+
Logger.log(`合計 ${updatedCount} 件の使用回数を更新しました`);
|
| 364 |
+
|
| 365 |
+
return {
|
| 366 |
+
success: true,
|
| 367 |
+
updated_count: updatedCount
|
| 368 |
+
};
|
| 369 |
+
|
| 370 |
+
} catch (error) {
|
| 371 |
+
Logger.log(`update_usage_count エラー: ${error.message}`);
|
| 372 |
+
return {
|
| 373 |
+
success: false,
|
| 374 |
+
updated_count: 0,
|
| 375 |
+
error: error.message
|
| 376 |
+
};
|
| 377 |
+
}
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
/**
|
| 381 |
+
* 答えデータを一括挿入(初期データ投入用)
|
| 382 |
+
*
|
| 383 |
+
* @param {Array<Object>} answers - 答えデータの配列
|
| 384 |
+
* @returns {Object} 挿入結果 { success: true, inserted_count: number }
|
| 385 |
+
*
|
| 386 |
+
* @example
|
| 387 |
+
* const answers = [
|
| 388 |
+
* {
|
| 389 |
+
* answer_id: 'jp_001',
|
| 390 |
+
* subject: 'jp',
|
| 391 |
+
* category: 'JP01',
|
| 392 |
+
* answer: '漢字',
|
| 393 |
+
* question_hint: '部首を手がかりに意味を推測させる',
|
| 394 |
+
* difficulty: '基本',
|
| 395 |
+
* source_context: '小学3年生の漢字学習',
|
| 396 |
+
* usage_count: 0
|
| 397 |
+
* },
|
| 398 |
+
* ...
|
| 399 |
+
* ];
|
| 400 |
+
* const result = bulk_insert_answers(answers);
|
| 401 |
+
*/
|
| 402 |
+
function bulk_insert_answers(answers) {
|
| 403 |
+
try {
|
| 404 |
+
if (!Array.isArray(answers) || answers.length === 0) {
|
| 405 |
+
Logger.log('answers が空または無効です');
|
| 406 |
+
return { success: false, inserted_count: 0, error: 'Invalid answers' };
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
const sheet = getQuestionDatabaseSheet_();
|
| 410 |
+
const lastRow = sheet.getLastRow();
|
| 411 |
+
|
| 412 |
+
// オブジェクト配列を行データに変換
|
| 413 |
+
const rows = answers.map(obj => questionObjectToRow_(obj));
|
| 414 |
+
|
| 415 |
+
// データを一括挿入
|
| 416 |
+
const startRow = lastRow + 1;
|
| 417 |
+
const range = sheet.getRange(startRow, 1, rows.length, QUESTIONDB_HEADER.length);
|
| 418 |
+
range.setValues(rows);
|
| 419 |
+
|
| 420 |
+
Logger.log(`${rows.length} 件のデータを挿入しました(行 ${startRow} ~ ${startRow + rows.length - 1})`);
|
| 421 |
+
|
| 422 |
+
return {
|
| 423 |
+
success: true,
|
| 424 |
+
inserted_count: rows.length
|
| 425 |
+
};
|
| 426 |
+
|
| 427 |
+
} catch (error) {
|
| 428 |
+
Logger.log(`bulk_insert_answers エラー: ${error.message}`);
|
| 429 |
+
return {
|
| 430 |
+
success: false,
|
| 431 |
+
inserted_count: 0,
|
| 432 |
+
error: error.message
|
| 433 |
+
};
|
| 434 |
+
}
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
// ==================== 管理用ユーティリティ ====================
|
| 438 |
+
|
| 439 |
+
/**
|
| 440 |
+
* QuestionDatabaseの統計情報を取得
|
| 441 |
+
*
|
| 442 |
+
* @returns {Object} 統計情報
|
| 443 |
+
*/
|
| 444 |
+
function getQuestionDatabaseStats() {
|
| 445 |
+
const sheet = getQuestionDatabaseSheet_();
|
| 446 |
+
const lastRow = sheet.getLastRow();
|
| 447 |
+
|
| 448 |
+
if (lastRow <= 1) {
|
| 449 |
+
return {
|
| 450 |
+
total_count: 0,
|
| 451 |
+
subjects: {}
|
| 452 |
+
};
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
const dataRange = sheet.getRange(2, 1, lastRow - 1, QUESTIONDB_HEADER.length);
|
| 456 |
+
const allData = dataRange.getValues();
|
| 457 |
+
|
| 458 |
+
const stats = {
|
| 459 |
+
total_count: allData.length,
|
| 460 |
+
subjects: {}
|
| 461 |
+
};
|
| 462 |
+
|
| 463 |
+
allData.forEach(row => {
|
| 464 |
+
const subject = row[QUESTIONDB_COLUMNS.SUBJECT - 1];
|
| 465 |
+
const category = row[QUESTIONDB_COLUMNS.CATEGORY - 1];
|
| 466 |
+
const usageCount = row[QUESTIONDB_COLUMNS.USAGE_COUNT - 1] || 0;
|
| 467 |
+
|
| 468 |
+
if (!stats.subjects[subject]) {
|
| 469 |
+
stats.subjects[subject] = {
|
| 470 |
+
count: 0,
|
| 471 |
+
categories: {},
|
| 472 |
+
total_usage: 0,
|
| 473 |
+
min_usage: Infinity,
|
| 474 |
+
max_usage: 0
|
| 475 |
+
};
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
stats.subjects[subject].count++;
|
| 479 |
+
stats.subjects[subject].total_usage += usageCount;
|
| 480 |
+
stats.subjects[subject].min_usage = Math.min(stats.subjects[subject].min_usage, usageCount);
|
| 481 |
+
stats.subjects[subject].max_usage = Math.max(stats.subjects[subject].max_usage, usageCount);
|
| 482 |
+
|
| 483 |
+
if (!stats.subjects[subject].categories[category]) {
|
| 484 |
+
stats.subjects[subject].categories[category] = 0;
|
| 485 |
+
}
|
| 486 |
+
stats.subjects[subject].categories[category]++;
|
| 487 |
+
});
|
| 488 |
+
|
| 489 |
+
Logger.log('QuestionDatabase統計情報:');
|
| 490 |
+
Logger.log(JSON.stringify(stats, null, 2));
|
| 491 |
+
|
| 492 |
+
return stats;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
/**
|
| 496 |
+
* 指定した教科のデータを全削除
|
| 497 |
+
*
|
| 498 |
+
* @param {string} subject - 教科コード (jp, math, sci, soc)
|
| 499 |
+
* @returns {Object} 削除結果 { success: true, deleted_count: number }
|
| 500 |
+
*/
|
| 501 |
+
function deleteBySubject(subject) {
|
| 502 |
+
try {
|
| 503 |
+
if (!subject) {
|
| 504 |
+
Logger.log('subject が指定されていません');
|
| 505 |
+
return { success: false, deleted_count: 0, error: 'subject required' };
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
const sheet = getQuestionDatabaseSheet_();
|
| 509 |
+
const lastRow = sheet.getLastRow();
|
| 510 |
+
|
| 511 |
+
if (lastRow <= 1) {
|
| 512 |
+
Logger.log('QuestionDatabaseにデータがありません');
|
| 513 |
+
return { success: true, deleted_count: 0 };
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
// 全データを取得
|
| 517 |
+
const dataRange = sheet.getRange(2, 1, lastRow - 1, QUESTIONDB_HEADER.length);
|
| 518 |
+
const allData = dataRange.getValues();
|
| 519 |
+
|
| 520 |
+
// 削除対象の行番号を特定(後ろから削除するため逆順)
|
| 521 |
+
const rowsToDelete = [];
|
| 522 |
+
allData.forEach((row, index) => {
|
| 523 |
+
if (row[QUESTIONDB_COLUMNS.SUBJECT - 1] === subject) {
|
| 524 |
+
rowsToDelete.push(index + 2); // ヘッダー行を考慮
|
| 525 |
+
}
|
| 526 |
+
});
|
| 527 |
+
|
| 528 |
+
// 後ろから削除(行番号がずれないように)
|
| 529 |
+
rowsToDelete.reverse().forEach(rowNum => {
|
| 530 |
+
sheet.deleteRow(rowNum);
|
| 531 |
+
});
|
| 532 |
+
|
| 533 |
+
Logger.log(`教科 '${subject}' の ${rowsToDelete.length} 件を削除しました`);
|
| 534 |
+
|
| 535 |
+
return {
|
| 536 |
+
success: true,
|
| 537 |
+
deleted_count: rowsToDelete.length
|
| 538 |
+
};
|
| 539 |
+
|
| 540 |
+
} catch (error) {
|
| 541 |
+
Logger.log(`deleteBySubject エラー: ${error.message}`);
|
| 542 |
+
return {
|
| 543 |
+
success: false,
|
| 544 |
+
deleted_count: 0,
|
| 545 |
+
error: error.message
|
| 546 |
+
};
|
| 547 |
+
}
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
/**
|
| 551 |
+
* QuestionDatabaseシートにヘッダ行を追加
|
| 552 |
+
*
|
| 553 |
+
* @returns {Object} 結果 { success: true, message: string }
|
| 554 |
+
*/
|
| 555 |
+
function addQuestionDatabaseHeader() {
|
| 556 |
+
try {
|
| 557 |
+
const ss = SpreadsheetApp.getActiveSpreadsheet();
|
| 558 |
+
let sheet = ss.getSheetByName(QUESTIONDB_SHEET_NAME);
|
| 559 |
+
|
| 560 |
+
if (!sheet) {
|
| 561 |
+
// シートがない場合は作成
|
| 562 |
+
sheet = ss.insertSheet(QUESTIONDB_SHEET_NAME);
|
| 563 |
+
Logger.log('QuestionDatabaseシートを新規作成しました');
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
// ヘッダー行を設定(既存データがある場合は行1に挿入)
|
| 567 |
+
const firstRow = sheet.getRange(1, 1, 1, QUESTIONDB_HEADER.length);
|
| 568 |
+
const currentFirstRow = firstRow.getValues()[0];
|
| 569 |
+
|
| 570 |
+
// 既にヘッダーがあるかチェック
|
| 571 |
+
if (currentFirstRow[0] === 'answer_id') {
|
| 572 |
+
return {
|
| 573 |
+
success: true,
|
| 574 |
+
message: 'Header already exists'
|
| 575 |
+
};
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
// ヘッダーがない場合、行1に挿入
|
| 579 |
+
if (currentFirstRow[0] !== '' && currentFirstRow[0] !== 'answer_id') {
|
| 580 |
+
// データがある場合は行を挿入
|
| 581 |
+
sheet.insertRowBefore(1);
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
// ヘッダーを設定
|
| 585 |
+
sheet.getRange(1, 1, 1, QUESTIONDB_HEADER.length).setValues([QUESTIONDB_HEADER]);
|
| 586 |
+
sheet.getRange(1, 1, 1, QUESTIONDB_HEADER.length).setFontWeight('bold');
|
| 587 |
+
sheet.setFrozenRows(1);
|
| 588 |
+
|
| 589 |
+
Logger.log('QuestionDatabaseヘッダ行を追加しました');
|
| 590 |
+
|
| 591 |
+
return {
|
| 592 |
+
success: true,
|
| 593 |
+
message: 'Header added successfully'
|
| 594 |
+
};
|
| 595 |
+
|
| 596 |
+
} catch (error) {
|
| 597 |
+
Logger.log(`addQuestionDatabaseHeader エラー: ${error.message}`);
|
| 598 |
+
return {
|
| 599 |
+
success: false,
|
| 600 |
+
error: error.message
|
| 601 |
+
};
|
| 602 |
+
}
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
/**
|
| 606 |
+
* 全データのusage_countをリセット(開発/テスト用)
|
| 607 |
+
*
|
| 608 |
+
* @returns {Object} リセット結果
|
| 609 |
+
*/
|
| 610 |
+
function resetAllUsageCount() {
|
| 611 |
+
const sheet = getQuestionDatabaseSheet_();
|
| 612 |
+
const lastRow = sheet.getLastRow();
|
| 613 |
+
|
| 614 |
+
if (lastRow <= 1) {
|
| 615 |
+
Logger.log('QuestionDatabaseにデータがありません');
|
| 616 |
+
return { success: false, message: 'No data' };
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
const usageCountRange = sheet.getRange(2, QUESTIONDB_COLUMNS.USAGE_COUNT, lastRow - 1, 1);
|
| 620 |
+
const zeros = Array(lastRow - 1).fill([0]);
|
| 621 |
+
usageCountRange.setValues(zeros);
|
| 622 |
+
|
| 623 |
+
Logger.log(`${lastRow - 1} 件の usage_count を 0 にリセットしました`);
|
| 624 |
+
|
| 625 |
+
return {
|
| 626 |
+
success: true,
|
| 627 |
+
reset_count: lastRow - 1
|
| 628 |
+
};
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
// ==================== Web API エンドポイント ====================
|
| 632 |
+
|
| 633 |
+
/**
|
| 634 |
+
* doPost エンドポイント(既存のコードに統合用)
|
| 635 |
+
*
|
| 636 |
+
* アクション一覧:
|
| 637 |
+
* - get_random_answers: ランダムに答えを取得
|
| 638 |
+
* - update_usage_count: 使用回数を更新
|
| 639 |
+
* - bulk_insert_answers: 一括挿入
|
| 640 |
+
*
|
| 641 |
+
* 使用例:
|
| 642 |
+
*
|
| 643 |
+
* // get_random_answers
|
| 644 |
+
* POST https://script.google.com/macros/s/.../exec
|
| 645 |
+
* {
|
| 646 |
+
* "action": "get_random_answers",
|
| 647 |
+
* "subject": "jp",
|
| 648 |
+
* "count": 3
|
| 649 |
+
* }
|
| 650 |
+
*
|
| 651 |
+
* // update_usage_count
|
| 652 |
+
* POST https://script.google.com/macros/s/.../exec
|
| 653 |
+
* {
|
| 654 |
+
* "action": "update_usage_count",
|
| 655 |
+
* "answer_ids": ["jp_001", "jp_042"]
|
| 656 |
+
* }
|
| 657 |
+
*
|
| 658 |
+
* // bulk_insert_answers
|
| 659 |
+
* POST https://script.google.com/macros/s/.../exec
|
| 660 |
+
* {
|
| 661 |
+
* "action": "bulk_insert_answers",
|
| 662 |
+
* "answers": [
|
| 663 |
+
* {
|
| 664 |
+
* "answer_id": "jp_001",
|
| 665 |
+
* "subject": "jp",
|
| 666 |
+
* "category": "JP01",
|
| 667 |
+
* "answer": "漢字",
|
| 668 |
+
* "question_hint": "部首を手がかりに",
|
| 669 |
+
* "difficulty": "基本",
|
| 670 |
+
* "source_context": "小学3年生",
|
| 671 |
+
* "usage_count": 0
|
| 672 |
+
* }
|
| 673 |
+
* ]
|
| 674 |
+
* }
|
| 675 |
+
*/
|
| 676 |
+
function handleQuestionDatabaseAction(action, params) {
|
| 677 |
+
switch (action) {
|
| 678 |
+
case 'get_random_answers':
|
| 679 |
+
return get_random_answers(params.subject, params.count || 1);
|
| 680 |
+
|
| 681 |
+
case 'get_random_questions':
|
| 682 |
+
// Python互換: get_questions_by_configと同じだが、レスポンス形式を調整
|
| 683 |
+
try {
|
| 684 |
+
const questions = get_questions_by_genre_config(params.subjects || []);
|
| 685 |
+
return {
|
| 686 |
+
success: true,
|
| 687 |
+
data: {
|
| 688 |
+
questions: questions
|
| 689 |
+
}
|
| 690 |
+
};
|
| 691 |
+
} catch (error) {
|
| 692 |
+
return {
|
| 693 |
+
success: false,
|
| 694 |
+
error: error.message
|
| 695 |
+
};
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
case 'get_questions_by_config':
|
| 699 |
+
// v1.6.3: ジャンル構成に基づく問題取得
|
| 700 |
+
return get_questions_by_genre_config(params.subjects || []);
|
| 701 |
+
|
| 702 |
+
case 'update_usage_count':
|
| 703 |
+
return update_usage_count(params.answer_ids || []);
|
| 704 |
+
|
| 705 |
+
case 'bulk_insert_answers':
|
| 706 |
+
return bulk_insert_answers(params.answers || []);
|
| 707 |
+
|
| 708 |
+
case 'get_stats':
|
| 709 |
+
return getQuestionDatabaseStats();
|
| 710 |
+
|
| 711 |
+
case 'reset_usage':
|
| 712 |
+
return resetAllUsageCount();
|
| 713 |
+
|
| 714 |
+
case 'delete_by_subject':
|
| 715 |
+
return deleteBySubject(params.subject);
|
| 716 |
+
|
| 717 |
+
case 'add_header':
|
| 718 |
+
return addQuestionDatabaseHeader();
|
| 719 |
+
|
| 720 |
+
default:
|
| 721 |
+
return {
|
| 722 |
+
success: false,
|
| 723 |
+
error: `Unknown action: ${action}`
|
| 724 |
+
};
|
| 725 |
+
}
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
// ==================== テスト用関数 ====================
|
| 729 |
+
|
| 730 |
+
/**
|
| 731 |
+
* テスト用: サンプルデータを挿入
|
| 732 |
+
*/
|
| 733 |
+
function testInsertSampleData() {
|
| 734 |
+
const sampleData = [
|
| 735 |
+
{
|
| 736 |
+
answer_id: 'jp_001',
|
| 737 |
+
subject: 'jp',
|
| 738 |
+
category: 'JP01',
|
| 739 |
+
answer: '憂鬱',
|
| 740 |
+
question_hint: '気分が晴れない様子を表す漢字',
|
| 741 |
+
difficulty: '標準',
|
| 742 |
+
source_context: '中学国語教科書',
|
| 743 |
+
usage_count: 0
|
| 744 |
+
},
|
| 745 |
+
{
|
| 746 |
+
answer_id: 'jp_002',
|
| 747 |
+
subject: 'jp',
|
| 748 |
+
category: 'JP01',
|
| 749 |
+
answer: '慈悲',
|
| 750 |
+
question_hint: '思いやりの心を表す熟語',
|
| 751 |
+
difficulty: '基本',
|
| 752 |
+
source_context: '小学6年生',
|
| 753 |
+
usage_count: 0
|
| 754 |
+
},
|
| 755 |
+
{
|
| 756 |
+
answer_id: 'jp_003',
|
| 757 |
+
subject: 'jp',
|
| 758 |
+
category: 'JP02',
|
| 759 |
+
answer: 'である',
|
| 760 |
+
question_hint: '断定の助動詞',
|
| 761 |
+
difficulty: '基本',
|
| 762 |
+
source_context: '中学文法',
|
| 763 |
+
usage_count: 0
|
| 764 |
+
},
|
| 765 |
+
{
|
| 766 |
+
answer_id: 'math_001',
|
| 767 |
+
subject: 'math',
|
| 768 |
+
category: 'MA01',
|
| 769 |
+
answer: '平方根',
|
| 770 |
+
question_hint: '2乗してその数になる数',
|
| 771 |
+
difficulty: '標準',
|
| 772 |
+
source_context: '中学3年数学',
|
| 773 |
+
usage_count: 0
|
| 774 |
+
}
|
| 775 |
+
];
|
| 776 |
+
|
| 777 |
+
const result = bulk_insert_answers(sampleData);
|
| 778 |
+
Logger.log('サンプルデータ挿入結果:');
|
| 779 |
+
Logger.log(JSON.stringify(result, null, 2));
|
| 780 |
+
}
|
| 781 |
+
|
| 782 |
+
/**
|
| 783 |
+
* テスト用: get_random_answers のテスト
|
| 784 |
+
*/
|
| 785 |
+
function testGetRandomAnswers() {
|
| 786 |
+
const answers = get_random_answers('jp', 2);
|
| 787 |
+
Logger.log('取得した答え:');
|
| 788 |
+
Logger.log(JSON.stringify(answers, null, 2));
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
/**
|
| 792 |
+
* テスト用: update_usage_count のテスト
|
| 793 |
+
*/
|
| 794 |
+
function testUpdateUsageCount() {
|
| 795 |
+
const result = update_usage_count(['jp_001', 'jp_002']);
|
| 796 |
+
Logger.log('更新結果:');
|
| 797 |
+
Logger.log(JSON.stringify(result, null, 2));
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
/**
|
| 801 |
+
* テスト用: get_questions_by_genre_config のテスト(v1.6.3)
|
| 802 |
+
*/
|
| 803 |
+
function testGetQuestionsByGenreConfig() {
|
| 804 |
+
const questions = get_questions_by_genre_config(['jp', 'math']);
|
| 805 |
+
Logger.log(`取得した問題数: ${questions.length}`);
|
| 806 |
+
|
| 807 |
+
// ジャンル別の内訳を表示
|
| 808 |
+
const genreCounts = {};
|
| 809 |
+
for (const q of questions) {
|
| 810 |
+
const key = `${q.subject}/${q.category}`;
|
| 811 |
+
genreCounts[key] = (genreCounts[key] || 0) + 1;
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
Logger.log('ジャンル別内訳:');
|
| 815 |
+
Logger.log(JSON.stringify(genreCounts, null, 2));
|
| 816 |
+
}
|
V1.7.1/gas/setup_dify_properties.js
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Dify環境変数設定スクリプト
|
| 3 |
+
*
|
| 4 |
+
* Google Apps ScriptのScriptPropertiesに、Dify API統合に必要な環境変数を設定・管理します。
|
| 5 |
+
*
|
| 6 |
+
* 主要機能:
|
| 7 |
+
* - Dify API URL・API Keyの設定
|
| 8 |
+
* - 設定値の取得・確認
|
| 9 |
+
* - Dify APIへの接続テスト
|
| 10 |
+
* - 環境変数のクリア(デバッグ用)
|
| 11 |
+
*
|
| 12 |
+
* @version 1.0.0
|
| 13 |
+
* @date 2025-11-10
|
| 14 |
+
*/
|
| 15 |
+
|
| 16 |
+
// ============================================================================
|
| 17 |
+
// 定数定義
|
| 18 |
+
// ============================================================================
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* 環境変数のキー名
|
| 22 |
+
*/
|
| 23 |
+
const PROPERTY_KEYS = {
|
| 24 |
+
DIFY_API_URL: 'DIFY_API_URL',
|
| 25 |
+
DIFY_API_KEY: 'DIFY_API_KEY',
|
| 26 |
+
SPREADSHEET_ID: 'SPREADSHEET_ID'
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
// ============================================================================
|
| 30 |
+
// 環境変数設定関数
|
| 31 |
+
// ============================================================================
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* Dify関連の環境変数をScriptPropertiesに設定します
|
| 35 |
+
*
|
| 36 |
+
* 設定される環境変数:
|
| 37 |
+
* - DIFY_API_URL: Difyワークフロー実行エンドポイント
|
| 38 |
+
* - DIFY_API_KEY: Dify APIキー
|
| 39 |
+
* - SPREADSHEET_ID: Google SheetsのスプレッドシートID(確認用)
|
| 40 |
+
*
|
| 41 |
+
* 実行方法:
|
| 42 |
+
* 1. Apps Scriptエディタでこのファイルを開く
|
| 43 |
+
* 2. 関数 setDifyProperties を選択して実行
|
| 44 |
+
* 3. 実行ログで結果を確認
|
| 45 |
+
*
|
| 46 |
+
* 注意:
|
| 47 |
+
* - 実際の値を設定する際は、以下のダミー値を実際の値に置き換えてください
|
| 48 |
+
* - API Keyは機密情報のため、取り扱いに注意してください
|
| 49 |
+
*
|
| 50 |
+
* @returns {void}
|
| 51 |
+
*
|
| 52 |
+
* @example
|
| 53 |
+
* // 実行例(Apps Scriptエディタで実行)
|
| 54 |
+
* setDifyProperties();
|
| 55 |
+
* // ログ出力: 「✅ Dify環境変数の設定が完了しました」
|
| 56 |
+
*/
|
| 57 |
+
function setDifyProperties() {
|
| 58 |
+
try {
|
| 59 |
+
const properties = PropertiesService.getScriptProperties();
|
| 60 |
+
|
| 61 |
+
// ========================================================================
|
| 62 |
+
// 【重要】実際の値に置き換えてください
|
| 63 |
+
// ========================================================================
|
| 64 |
+
|
| 65 |
+
// Dify Cloud のワークフロー実行エンドポイント
|
| 66 |
+
// 例: 'https://api.dify.ai/v1/workflows/run'
|
| 67 |
+
const DIFY_API_URL = 'https://api.dify.ai/v1/workflows/run';
|
| 68 |
+
|
| 69 |
+
// Dify Cloud のAPI Key(ワークフロー作成後に取得)
|
| 70 |
+
// 例: 'app-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
| 71 |
+
const DIFY_API_KEY = 'app-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
|
| 72 |
+
|
| 73 |
+
// Google SheetsのスプレッドシートID(既存、確認用)
|
| 74 |
+
// 既にCode.jsで定義されている値
|
| 75 |
+
const SPREADSHEET_ID = '1xlyaDpCrg4M-epbQ-KqemjpDeabxBjZ7aVCvRgRLazE';
|
| 76 |
+
|
| 77 |
+
// ========================================================================
|
| 78 |
+
// 環境変数の設定
|
| 79 |
+
// ========================================================================
|
| 80 |
+
|
| 81 |
+
properties.setProperty(PROPERTY_KEYS.DIFY_API_URL, DIFY_API_URL);
|
| 82 |
+
properties.setProperty(PROPERTY_KEYS.DIFY_API_KEY, DIFY_API_KEY);
|
| 83 |
+
properties.setProperty(PROPERTY_KEYS.SPREADSHEET_ID, SPREADSHEET_ID);
|
| 84 |
+
|
| 85 |
+
Logger.log('✅ Dify環境変数の設定が完了しました');
|
| 86 |
+
Logger.log('');
|
| 87 |
+
Logger.log('設定内容:');
|
| 88 |
+
Logger.log(' DIFY_API_URL: ' + DIFY_API_URL);
|
| 89 |
+
Logger.log(' DIFY_API_KEY: ' + maskApiKey(DIFY_API_KEY));
|
| 90 |
+
Logger.log(' SPREADSHEET_ID: ' + SPREADSHEET_ID);
|
| 91 |
+
Logger.log('');
|
| 92 |
+
Logger.log('次のステップ: getDifyProperties() を実行して設定を確認してください');
|
| 93 |
+
|
| 94 |
+
} catch (error) {
|
| 95 |
+
Logger.log('❌ エラー: Dify環境変数の設定に失敗しました');
|
| 96 |
+
Logger.log('詳細: ' + error.toString());
|
| 97 |
+
throw new Error('環境変数の設定に失敗しました: ' + error.toString());
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// ============================================================================
|
| 102 |
+
// 環境変数取得関数
|
| 103 |
+
// ============================================================================
|
| 104 |
+
|
| 105 |
+
/**
|
| 106 |
+
* 設定されたDify環境変数を取得・確認します
|
| 107 |
+
*
|
| 108 |
+
* 取得する環境変数:
|
| 109 |
+
* - DIFY_API_URL: Difyワークフロー実行エンドポイント
|
| 110 |
+
* - DIFY_API_KEY: Dify APIキー(マスク表示)
|
| 111 |
+
* - SPREADSHEET_ID: Google SheetsのスプレッドシートID
|
| 112 |
+
*
|
| 113 |
+
* 実行方法:
|
| 114 |
+
* 1. Apps Scriptエディタでこのファイルを開く
|
| 115 |
+
* 2. 関数 getDifyProperties を選択して実行
|
| 116 |
+
* 3. 実行ログで設定内容を確認
|
| 117 |
+
*
|
| 118 |
+
* @returns {Object} 環境変数のオブジェクト
|
| 119 |
+
* @returns {string} returns.DIFY_API_URL - Dify API URL
|
| 120 |
+
* @returns {string} returns.DIFY_API_KEY - Dify API Key
|
| 121 |
+
* @returns {string} returns.SPREADSHEET_ID - スプレッドシートID
|
| 122 |
+
*
|
| 123 |
+
* @example
|
| 124 |
+
* // 実行例(Apps Scriptエディタで実行)
|
| 125 |
+
* const props = getDifyProperties();
|
| 126 |
+
* // ログ出力: 各環境変数の値
|
| 127 |
+
*
|
| 128 |
+
* @example
|
| 129 |
+
* // コード内での使用例
|
| 130 |
+
* const props = getDifyProperties();
|
| 131 |
+
* const apiUrl = props.DIFY_API_URL;
|
| 132 |
+
* const apiKey = props.DIFY_API_KEY;
|
| 133 |
+
*/
|
| 134 |
+
function getDifyProperties() {
|
| 135 |
+
try {
|
| 136 |
+
const properties = PropertiesService.getScriptProperties();
|
| 137 |
+
|
| 138 |
+
const difyApiUrl = properties.getProperty(PROPERTY_KEYS.DIFY_API_URL);
|
| 139 |
+
const difyApiKey = properties.getProperty(PROPERTY_KEYS.DIFY_API_KEY);
|
| 140 |
+
const spreadsheetId = properties.getProperty(PROPERTY_KEYS.SPREADSHEET_ID);
|
| 141 |
+
|
| 142 |
+
Logger.log('📋 Dify環境変数の取得結果:');
|
| 143 |
+
Logger.log('');
|
| 144 |
+
Logger.log(' DIFY_API_URL: ' + (difyApiUrl || '(未設定)'));
|
| 145 |
+
Logger.log(' DIFY_API_KEY: ' + (difyApiKey ? maskApiKey(difyApiKey) : '(未設定)'));
|
| 146 |
+
Logger.log(' SPREADSHEET_ID: ' + (spreadsheetId || '(未設定)'));
|
| 147 |
+
Logger.log('');
|
| 148 |
+
|
| 149 |
+
// 未設定の項目をチェック
|
| 150 |
+
const missingKeys = [];
|
| 151 |
+
if (!difyApiUrl) missingKeys.push('DIFY_API_URL');
|
| 152 |
+
if (!difyApiKey) missingKeys.push('DIFY_API_KEY');
|
| 153 |
+
if (!spreadsheetId) missingKeys.push('SPREADSHEET_ID');
|
| 154 |
+
|
| 155 |
+
if (missingKeys.length > 0) {
|
| 156 |
+
Logger.log('⚠️ 未設定の環境変数があります: ' + missingKeys.join(', '));
|
| 157 |
+
Logger.log('setDifyProperties() を実行して設定してください');
|
| 158 |
+
} else {
|
| 159 |
+
Logger.log('✅ すべての環境変数が設定されています');
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
return {
|
| 163 |
+
DIFY_API_URL: difyApiUrl,
|
| 164 |
+
DIFY_API_KEY: difyApiKey,
|
| 165 |
+
SPREADSHEET_ID: spreadsheetId
|
| 166 |
+
};
|
| 167 |
+
|
| 168 |
+
} catch (error) {
|
| 169 |
+
Logger.log('❌ エラー: Dify環境変数の取得に失敗しました');
|
| 170 |
+
Logger.log('詳細: ' + error.toString());
|
| 171 |
+
throw new Error('環境変数の取得に失敗しました: ' + error.toString());
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// ============================================================================
|
| 176 |
+
// Dify接続テスト関数
|
| 177 |
+
// ============================================================================
|
| 178 |
+
|
| 179 |
+
/**
|
| 180 |
+
* Dify APIへの接続テストを実行します
|
| 181 |
+
*
|
| 182 |
+
* テスト内容:
|
| 183 |
+
* - 環境変数の存在確認
|
| 184 |
+
* - Dify API URLへのHTTPリクエスト送信
|
| 185 |
+
* - レスポンスステータスの確認
|
| 186 |
+
*
|
| 187 |
+
* 実行方法:
|
| 188 |
+
* 1. setDifyProperties() を実行して環境変数を設定
|
| 189 |
+
* 2. Apps Scriptエディタでこのファイルを開く
|
| 190 |
+
* 3. 関数 testDifyConnection を選択して実行
|
| 191 |
+
* 4. 実行ログで接続テスト結果を確認
|
| 192 |
+
*
|
| 193 |
+
* 注意:
|
| 194 |
+
* - 実際のDify Cloudアカウントとワークフロー作成が必要です
|
| 195 |
+
* - API Keyが正しく設定されていることを確認してください
|
| 196 |
+
*
|
| 197 |
+
* @returns {Object} テスト結果のオブジェクト
|
| 198 |
+
* @returns {boolean} returns.success - テスト成功フラグ
|
| 199 |
+
* @returns {string} returns.message - テスト結果メッセージ
|
| 200 |
+
* @returns {Object} returns.response - APIレスポンス(成功時)
|
| 201 |
+
*
|
| 202 |
+
* @example
|
| 203 |
+
* // 実行例(Apps Scriptエディタで実行)
|
| 204 |
+
* testDifyConnection();
|
| 205 |
+
* // ログ出力: 「✅ Dify API接続テスト成功」または「❌ 接続テスト失敗」
|
| 206 |
+
*/
|
| 207 |
+
function testDifyConnection() {
|
| 208 |
+
try {
|
| 209 |
+
Logger.log('🔄 Dify API接続テストを開始します...');
|
| 210 |
+
Logger.log('');
|
| 211 |
+
|
| 212 |
+
// 環境変数の取得
|
| 213 |
+
const properties = PropertiesService.getScriptProperties();
|
| 214 |
+
const difyApiUrl = properties.getProperty(PROPERTY_KEYS.DIFY_API_URL);
|
| 215 |
+
const difyApiKey = properties.getProperty(PROPERTY_KEYS.DIFY_API_KEY);
|
| 216 |
+
|
| 217 |
+
// 環境変数の存在確認
|
| 218 |
+
if (!difyApiUrl || !difyApiKey) {
|
| 219 |
+
Logger.log('❌ エラー: 環境変数が未設定です');
|
| 220 |
+
Logger.log('setDifyProperties() を実行して環境変数を設定してください');
|
| 221 |
+
return {
|
| 222 |
+
success: false,
|
| 223 |
+
message: '環境変数が未設定です'
|
| 224 |
+
};
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
Logger.log('環境変数の確認: ✅');
|
| 228 |
+
Logger.log(' DIFY_API_URL: ' + difyApiUrl);
|
| 229 |
+
Logger.log(' DIFY_API_KEY: ' + maskApiKey(difyApiKey));
|
| 230 |
+
Logger.log('');
|
| 231 |
+
|
| 232 |
+
// テスト用のリクエストペイロード
|
| 233 |
+
const testPayload = {
|
| 234 |
+
inputs: {
|
| 235 |
+
subject: '算数',
|
| 236 |
+
category: '四則計算',
|
| 237 |
+
test: true
|
| 238 |
+
},
|
| 239 |
+
response_mode: 'blocking',
|
| 240 |
+
user: 'gas-test-user'
|
| 241 |
+
};
|
| 242 |
+
|
| 243 |
+
// HTTPリクエストオプション
|
| 244 |
+
const options = {
|
| 245 |
+
method: 'post',
|
| 246 |
+
contentType: 'application/json',
|
| 247 |
+
headers: {
|
| 248 |
+
'Authorization': 'Bearer ' + difyApiKey
|
| 249 |
+
},
|
| 250 |
+
payload: JSON.stringify(testPayload),
|
| 251 |
+
muteHttpExceptions: true // エラーレスポンスも取得するため
|
| 252 |
+
};
|
| 253 |
+
|
| 254 |
+
Logger.log('🌐 Dify APIへリクエストを送信中...');
|
| 255 |
+
Logger.log('URL: ' + difyApiUrl);
|
| 256 |
+
Logger.log('');
|
| 257 |
+
|
| 258 |
+
// Dify APIへのリクエスト送信
|
| 259 |
+
const response = UrlFetchApp.fetch(difyApiUrl, options);
|
| 260 |
+
const responseCode = response.getResponseCode();
|
| 261 |
+
const responseText = response.getContentText();
|
| 262 |
+
|
| 263 |
+
Logger.log('📥 レスポンスを受信しました');
|
| 264 |
+
Logger.log('ステータスコード: ' + responseCode);
|
| 265 |
+
Logger.log('');
|
| 266 |
+
|
| 267 |
+
// レスポンスの解析
|
| 268 |
+
if (responseCode === 200) {
|
| 269 |
+
const responseData = JSON.parse(responseText);
|
| 270 |
+
Logger.log('✅ Dify API接続テスト成功');
|
| 271 |
+
Logger.log('');
|
| 272 |
+
Logger.log('レスポンス内容:');
|
| 273 |
+
Logger.log(JSON.stringify(responseData, null, 2));
|
| 274 |
+
|
| 275 |
+
return {
|
| 276 |
+
success: true,
|
| 277 |
+
message: '接続テスト成功',
|
| 278 |
+
response: responseData
|
| 279 |
+
};
|
| 280 |
+
|
| 281 |
+
} else {
|
| 282 |
+
Logger.log('❌ Dify API接続テスト失敗');
|
| 283 |
+
Logger.log('ステータスコード: ' + responseCode);
|
| 284 |
+
Logger.log('エラー内容: ' + responseText);
|
| 285 |
+
Logger.log('');
|
| 286 |
+
Logger.log('考えられる原因:');
|
| 287 |
+
Logger.log(' 1. DIFY_API_URL が正しくない');
|
| 288 |
+
Logger.log(' 2. DIFY_API_KEY が正しくない');
|
| 289 |
+
Logger.log(' 3. Difyワークフローがまだ作成されていない');
|
| 290 |
+
Logger.log(' 4. Difyワークフローが公開されていない');
|
| 291 |
+
|
| 292 |
+
return {
|
| 293 |
+
success: false,
|
| 294 |
+
message: '接続テスト失敗(ステータスコード: ' + responseCode + ')',
|
| 295 |
+
error: responseText
|
| 296 |
+
};
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
} catch (error) {
|
| 300 |
+
Logger.log('❌ エラー: Dify API接続テストでエラーが発生しました');
|
| 301 |
+
Logger.log('詳細: ' + error.toString());
|
| 302 |
+
Logger.log('');
|
| 303 |
+
Logger.log('考えられる原因:');
|
| 304 |
+
Logger.log(' 1. ネットワーク接続エラー');
|
| 305 |
+
Logger.log(' 2. URLが不正(例: プロトコル未指定)');
|
| 306 |
+
Logger.log(' 3. Apps Scriptの外部API接続権限が不足');
|
| 307 |
+
|
| 308 |
+
return {
|
| 309 |
+
success: false,
|
| 310 |
+
message: '接続テスト中にエラーが発生しました: ' + error.toString()
|
| 311 |
+
};
|
| 312 |
+
}
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
// ============================================================================
|
| 316 |
+
// 環境変数クリア関数(デバッグ用)
|
| 317 |
+
// ============================================================================
|
| 318 |
+
|
| 319 |
+
/**
|
| 320 |
+
* 設定されたDify環境変数をクリアします(デバッグ用)
|
| 321 |
+
*
|
| 322 |
+
* 削除される環境変数:
|
| 323 |
+
* - DIFY_API_URL
|
| 324 |
+
* - DIFY_API_KEY
|
| 325 |
+
* - SPREADSHEET_ID
|
| 326 |
+
*
|
| 327 |
+
* 実行方法:
|
| 328 |
+
* 1. Apps Scriptエディタでこのファイルを開く
|
| 329 |
+
* 2. 関数 clearDifyProperties を選択して実行
|
| 330 |
+
* 3. 実行ログで削除結果を確認
|
| 331 |
+
*
|
| 332 |
+
* 注意:
|
| 333 |
+
* - この操作は元に戻せません
|
| 334 |
+
* - 誤って実行しないように注意してください
|
| 335 |
+
* - 本番環境では使用しないでください
|
| 336 |
+
*
|
| 337 |
+
* @returns {void}
|
| 338 |
+
*
|
| 339 |
+
* @example
|
| 340 |
+
* // 実行例(Apps Scriptエディタで実行)
|
| 341 |
+
* clearDifyProperties();
|
| 342 |
+
* // ログ出力: 「✅ Dify環境変数のクリアが完了しました」
|
| 343 |
+
*/
|
| 344 |
+
function clearDifyProperties() {
|
| 345 |
+
try {
|
| 346 |
+
const properties = PropertiesService.getScriptProperties();
|
| 347 |
+
|
| 348 |
+
Logger.log('🔄 Dify環境変数のクリアを開始します...');
|
| 349 |
+
Logger.log('');
|
| 350 |
+
|
| 351 |
+
// 削除前の確認
|
| 352 |
+
const before = getDifyPropertiesInternal(properties);
|
| 353 |
+
Logger.log('削除前の設定:');
|
| 354 |
+
Logger.log(' DIFY_API_URL: ' + (before.DIFY_API_URL ? '設定あり' : '設定なし'));
|
| 355 |
+
Logger.log(' DIFY_API_KEY: ' + (before.DIFY_API_KEY ? '設定あり' : '設定なし'));
|
| 356 |
+
Logger.log(' SPREADSHEET_ID: ' + (before.SPREADSHEET_ID ? '設定あり' : '設定なし'));
|
| 357 |
+
Logger.log('');
|
| 358 |
+
|
| 359 |
+
// 環境変数の削除
|
| 360 |
+
properties.deleteProperty(PROPERTY_KEYS.DIFY_API_URL);
|
| 361 |
+
properties.deleteProperty(PROPERTY_KEYS.DIFY_API_KEY);
|
| 362 |
+
properties.deleteProperty(PROPERTY_KEYS.SPREADSHEET_ID);
|
| 363 |
+
|
| 364 |
+
Logger.log('✅ Dify環境変数のクリアが完了しました');
|
| 365 |
+
Logger.log('');
|
| 366 |
+
Logger.log('削除された環境変数:');
|
| 367 |
+
Logger.log(' - DIFY_API_URL');
|
| 368 |
+
Logger.log(' - DIFY_API_KEY');
|
| 369 |
+
Logger.log(' - SPREADSHEET_ID');
|
| 370 |
+
Logger.log('');
|
| 371 |
+
Logger.log('再設定する場合: setDifyProperties() を実行してください');
|
| 372 |
+
|
| 373 |
+
} catch (error) {
|
| 374 |
+
Logger.log('❌ エラー: Dify環境変数のクリアに失敗しました');
|
| 375 |
+
Logger.log('詳細: ' + error.toString());
|
| 376 |
+
throw new Error('環境変数のクリアに失敗しました: ' + error.toString());
|
| 377 |
+
}
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
// ============================================================================
|
| 381 |
+
// ユーティリティ関数
|
| 382 |
+
// ============================================================================
|
| 383 |
+
|
| 384 |
+
/**
|
| 385 |
+
* API Keyをマスク表示します(セキュリティ対策)
|
| 386 |
+
*
|
| 387 |
+
* 例: 'app-abc123xyz789' → 'app-abc***xyz789'
|
| 388 |
+
*
|
| 389 |
+
* @param {string} apiKey - API Key
|
| 390 |
+
* @returns {string} マスク表示されたAPI Key
|
| 391 |
+
* @private
|
| 392 |
+
*/
|
| 393 |
+
function maskApiKey(apiKey) {
|
| 394 |
+
if (!apiKey || apiKey.length < 10) {
|
| 395 |
+
return '***';
|
| 396 |
+
}
|
| 397 |
+
const prefix = apiKey.substring(0, 7);
|
| 398 |
+
const suffix = apiKey.substring(apiKey.length - 6);
|
| 399 |
+
return prefix + '***' + suffix;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
/**
|
| 403 |
+
* 環境変数を取得します(内部用、ログ出力なし)
|
| 404 |
+
*
|
| 405 |
+
* @param {PropertiesService.Properties} properties - Propertiesオブジェクト
|
| 406 |
+
* @returns {Object} 環境変数のオブジェクト
|
| 407 |
+
* @private
|
| 408 |
+
*/
|
| 409 |
+
function getDifyPropertiesInternal(properties) {
|
| 410 |
+
return {
|
| 411 |
+
DIFY_API_URL: properties.getProperty(PROPERTY_KEYS.DIFY_API_URL),
|
| 412 |
+
DIFY_API_KEY: properties.getProperty(PROPERTY_KEYS.DIFY_API_KEY),
|
| 413 |
+
SPREADSHEET_ID: properties.getProperty(PROPERTY_KEYS.SPREADSHEET_ID)
|
| 414 |
+
};
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
// ============================================================================
|
| 418 |
+
// 使用例・実行手順
|
| 419 |
+
// ============================================================================
|
| 420 |
+
|
| 421 |
+
/**
|
| 422 |
+
* 【実行手順】
|
| 423 |
+
*
|
| 424 |
+
* 1. 環境変数の設定
|
| 425 |
+
* - setDifyProperties() を実行
|
| 426 |
+
* - 事前に関数内のダミー値を実際の値に置き換えてください
|
| 427 |
+
*
|
| 428 |
+
* 2. 設定の確認
|
| 429 |
+
* - getDifyProperties() を実行
|
| 430 |
+
* - 実行ログで設定内容を確認してください
|
| 431 |
+
*
|
| 432 |
+
* 3. 接続テスト
|
| 433 |
+
* - testDifyConnection() を実行
|
| 434 |
+
* - Dify APIへの接続が成功するか確認してください
|
| 435 |
+
*
|
| 436 |
+
* 4. 環境変数のクリア(必要な場合のみ)
|
| 437 |
+
* - clearDifyProperties() を実行
|
| 438 |
+
* - 誤って実行しないように注意してください
|
| 439 |
+
*
|
| 440 |
+
* 【Code.jsでの使用例】
|
| 441 |
+
*
|
| 442 |
+
* ```javascript
|
| 443 |
+
* // 環境変数の取得
|
| 444 |
+
* const properties = PropertiesService.getScriptProperties();
|
| 445 |
+
* const DIFY_API_URL = properties.getProperty('DIFY_API_URL');
|
| 446 |
+
* const DIFY_API_KEY = properties.getProperty('DIFY_API_KEY');
|
| 447 |
+
*
|
| 448 |
+
* // Dify APIへのリクエスト送信
|
| 449 |
+
* function callDifyAPI(inputs) {
|
| 450 |
+
* const options = {
|
| 451 |
+
* method: 'post',
|
| 452 |
+
* contentType: 'application/json',
|
| 453 |
+
* headers: {
|
| 454 |
+
* 'Authorization': 'Bearer ' + DIFY_API_KEY
|
| 455 |
+
* },
|
| 456 |
+
* payload: JSON.stringify({
|
| 457 |
+
* inputs: inputs,
|
| 458 |
+
* response_mode: 'blocking',
|
| 459 |
+
* user: 'gas-orchestrator'
|
| 460 |
+
* }),
|
| 461 |
+
* muteHttpExceptions: true
|
| 462 |
+
* };
|
| 463 |
+
*
|
| 464 |
+
* const response = UrlFetchApp.fetch(DIFY_API_URL, options);
|
| 465 |
+
* return JSON.parse(response.getContentText());
|
| 466 |
+
* }
|
| 467 |
+
* ```
|
| 468 |
+
*/
|
V1.7.1/gas/setup_sheets_v2.js
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* サイクル1.2.5: Google Sheets拡張(7シート構成への移行)
|
| 3 |
+
*
|
| 4 |
+
* このスクリプトは「超天才クイズDB」を4シート構成から7シート構成に自動拡張します。
|
| 5 |
+
*
|
| 6 |
+
* 実行方法:
|
| 7 |
+
* 1. Apps Scriptエディタで本ファイルを開く
|
| 8 |
+
* 2. setupSheetsV2関数を選択
|
| 9 |
+
* 3. 実行ボタンをクリック
|
| 10 |
+
* 4. ログで結果を確認
|
| 11 |
+
*
|
| 12 |
+
* 仕様:
|
| 13 |
+
* - 新規シート作成: Answers, Statistics, Evaluations(各ヘッダー + サンプルデータ)
|
| 14 |
+
* - 既存シート更新: Questions, Knowledge_Baseに`category`列追加
|
| 15 |
+
* - 安全機能: 既存シートチェック、重複チェック、エラーハンドリング
|
| 16 |
+
*
|
| 17 |
+
* 参照:
|
| 18 |
+
* - docs/data-model.md v2.0
|
| 19 |
+
* - docs/sheets-setup-manual-v2.md
|
| 20 |
+
*
|
| 21 |
+
* バージョン: 1.0
|
| 22 |
+
* 作成日: 2025-11-10
|
| 23 |
+
*/
|
| 24 |
+
|
| 25 |
+
// Note: SPREADSHEET_IDはCode.jsで定義済みのためここでは宣言しない
|
| 26 |
+
|
| 27 |
+
/**
|
| 28 |
+
* メイン関数: Google Sheetsを7シート構成に拡張
|
| 29 |
+
*/
|
| 30 |
+
function setupSheetsV2() {
|
| 31 |
+
try {
|
| 32 |
+
Logger.log('=== Google Sheets拡張開始(v2.0: 7シート構成) ===');
|
| 33 |
+
|
| 34 |
+
// スプレッドシート取得(Code.jsで定義されたSPREADSHEET_IDを使用)
|
| 35 |
+
var spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
|
| 36 |
+
Logger.log('スプレッドシート取得成功: ' + spreadsheet.getName());
|
| 37 |
+
|
| 38 |
+
// 既存シート確認
|
| 39 |
+
Logger.log('\n--- 既存シート確認 ---');
|
| 40 |
+
checkExistingSheets(spreadsheet);
|
| 41 |
+
|
| 42 |
+
// 新規シート作成(3シート)
|
| 43 |
+
Logger.log('\n--- 新規シート作成 ---');
|
| 44 |
+
createAnswersSheet(spreadsheet);
|
| 45 |
+
createStatisticsSheet(spreadsheet);
|
| 46 |
+
createEvaluationsSheet(spreadsheet);
|
| 47 |
+
|
| 48 |
+
// 既存シート更新(2シート)
|
| 49 |
+
Logger.log('\n--- 既存シート更新 ---');
|
| 50 |
+
updateQuestionsSheet(spreadsheet);
|
| 51 |
+
updateKnowledgeBaseSheet(spreadsheet);
|
| 52 |
+
|
| 53 |
+
Logger.log('\n=== Google Sheets拡張完了! ===');
|
| 54 |
+
Logger.log('結果: 7シート構成に拡張成功');
|
| 55 |
+
Logger.log('次のステップ: Google Sheetsを開いて7シート全て存在することを確認してください');
|
| 56 |
+
|
| 57 |
+
} catch (error) {
|
| 58 |
+
Logger.log('\n❌ エラー発生: ' + error.message);
|
| 59 |
+
Logger.log('スタックトレース: ' + error.stack);
|
| 60 |
+
throw error;
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
/**
|
| 65 |
+
* 既存シート確認
|
| 66 |
+
*/
|
| 67 |
+
function checkExistingSheets(spreadsheet) {
|
| 68 |
+
var sheets = spreadsheet.getSheets();
|
| 69 |
+
Logger.log('現在のシート数: ' + sheets.length);
|
| 70 |
+
|
| 71 |
+
for (var i = 0; i < sheets.length; i++) {
|
| 72 |
+
Logger.log(' - ' + sheets[i].getName());
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// 必須シート確認(Users, Sessions, Questions, Knowledge_Base)
|
| 76 |
+
var requiredSheets = ['Users', 'Sessions', 'Questions', 'Knowledge_Base'];
|
| 77 |
+
for (var j = 0; j < requiredSheets.length; j++) {
|
| 78 |
+
var sheet = spreadsheet.getSheetByName(requiredSheets[j]);
|
| 79 |
+
if (!sheet) {
|
| 80 |
+
throw new Error('必須シート「' + requiredSheets[j] + '」が存在しません。サイクル1.2を先に実行してください。');
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
Logger.log('必須シート(4シート)確認完了');
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/**
|
| 87 |
+
* Answersシート作成
|
| 88 |
+
*/
|
| 89 |
+
function createAnswersSheet(spreadsheet) {
|
| 90 |
+
var sheetName = 'Answers';
|
| 91 |
+
|
| 92 |
+
// 既存チェック
|
| 93 |
+
if (spreadsheet.getSheetByName(sheetName)) {
|
| 94 |
+
Logger.log('⚠️ ' + sheetName + 'シートは既に存在します。スキップします。');
|
| 95 |
+
return;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
Logger.log(sheetName + 'シートを作成中...');
|
| 99 |
+
|
| 100 |
+
// シート作成
|
| 101 |
+
var sheet = spreadsheet.insertSheet(sheetName);
|
| 102 |
+
|
| 103 |
+
// ヘッダー行(1行目)
|
| 104 |
+
var headers = [
|
| 105 |
+
'answer_id',
|
| 106 |
+
'session_id',
|
| 107 |
+
'question_id',
|
| 108 |
+
'user_answer',
|
| 109 |
+
'is_correct',
|
| 110 |
+
'time_taken',
|
| 111 |
+
'answered_at'
|
| 112 |
+
];
|
| 113 |
+
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
| 114 |
+
|
| 115 |
+
// サンプルデータ(2行)
|
| 116 |
+
var sampleData = [
|
| 117 |
+
['ans-sample-001', 'session-sample-001', 'q-sample-001', 0, true, 12, '2025-11-04T10:31:15Z'],
|
| 118 |
+
['ans-sample-002', 'session-sample-001', 'q-sample-002', 2, false, 18, '2025-11-04T10:31:33Z']
|
| 119 |
+
];
|
| 120 |
+
sheet.getRange(2, 1, sampleData.length, headers.length).setValues(sampleData);
|
| 121 |
+
|
| 122 |
+
Logger.log('✅ ' + sheetName + 'シート作成完了(ヘッダー + サンプルデータ2行)');
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/**
|
| 126 |
+
* Statisticsシート作成
|
| 127 |
+
*/
|
| 128 |
+
function createStatisticsSheet(spreadsheet) {
|
| 129 |
+
var sheetName = 'Statistics';
|
| 130 |
+
|
| 131 |
+
// 既存チェック
|
| 132 |
+
if (spreadsheet.getSheetByName(sheetName)) {
|
| 133 |
+
Logger.log('⚠️ ' + sheetName + 'シートは既に存在します。スキップします。');
|
| 134 |
+
return;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
Logger.log(sheetName + 'シートを作成中...');
|
| 138 |
+
|
| 139 |
+
// シート作成
|
| 140 |
+
var sheet = spreadsheet.insertSheet(sheetName);
|
| 141 |
+
|
| 142 |
+
// ヘッダー行(1行目)
|
| 143 |
+
var headers = [
|
| 144 |
+
'stat_id',
|
| 145 |
+
'user_id',
|
| 146 |
+
'subject',
|
| 147 |
+
'category',
|
| 148 |
+
'total_attempted',
|
| 149 |
+
'correct_count',
|
| 150 |
+
'accuracy_rate',
|
| 151 |
+
'last_updated'
|
| 152 |
+
];
|
| 153 |
+
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
| 154 |
+
|
| 155 |
+
// サンプルデータ(3行)
|
| 156 |
+
var sampleData = [
|
| 157 |
+
['stat-001', 'sample-user-001', '国語', '漢字・語彙', 20, 16, 0.80, '2025-11-04T10:45:00Z'],
|
| 158 |
+
['stat-002', 'sample-user-001', '国語', '物語文読解', 15, 9, 0.60, '2025-11-04T10:45:00Z'],
|
| 159 |
+
['stat-003', 'sample-user-001', '算数', '平面図形', 10, 4, 0.40, '2025-11-04T10:45:00Z']
|
| 160 |
+
];
|
| 161 |
+
sheet.getRange(2, 1, sampleData.length, headers.length).setValues(sampleData);
|
| 162 |
+
|
| 163 |
+
Logger.log('✅ ' + sheetName + 'シート作成完了(ヘッダー + サンプルデータ3行)');
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/**
|
| 167 |
+
* Evaluationsシート作成
|
| 168 |
+
*/
|
| 169 |
+
function createEvaluationsSheet(spreadsheet) {
|
| 170 |
+
var sheetName = 'Evaluations';
|
| 171 |
+
|
| 172 |
+
// 既存チェック
|
| 173 |
+
if (spreadsheet.getSheetByName(sheetName)) {
|
| 174 |
+
Logger.log('⚠️ ' + sheetName + 'シートは既に存在します。スキップします。');
|
| 175 |
+
return;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
Logger.log(sheetName + 'シートを作成中...');
|
| 179 |
+
|
| 180 |
+
// シート作成
|
| 181 |
+
var sheet = spreadsheet.insertSheet(sheetName);
|
| 182 |
+
|
| 183 |
+
// ヘッダー行(1行目)
|
| 184 |
+
var headers = [
|
| 185 |
+
'eval_id',
|
| 186 |
+
'session_id',
|
| 187 |
+
'subject_evaluations',
|
| 188 |
+
'overall_evaluation',
|
| 189 |
+
'created_at'
|
| 190 |
+
];
|
| 191 |
+
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
| 192 |
+
|
| 193 |
+
// サンプルデータ(1行)
|
| 194 |
+
// JSON形式のデータは文字列として扱う(Google Sheetsは型なし)
|
| 195 |
+
var sampleData = [
|
| 196 |
+
[
|
| 197 |
+
'eval-001',
|
| 198 |
+
'session-sample-001',
|
| 199 |
+
'{"国語":["漢字・語彙は80%の正答率で安定"],"算数":["計算力が優れています"]}',
|
| 200 |
+
'["今回は合計29/40(72.5%)"]',
|
| 201 |
+
'2025-11-04T10:46:00Z'
|
| 202 |
+
]
|
| 203 |
+
];
|
| 204 |
+
sheet.getRange(2, 1, sampleData.length, headers.length).setValues(sampleData);
|
| 205 |
+
|
| 206 |
+
Logger.log('✅ ' + sheetName + 'シート作成完了(ヘッダー + サンプルデータ1行)');
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
/**
|
| 210 |
+
* Questionsシート更新(category列追加)
|
| 211 |
+
*/
|
| 212 |
+
function updateQuestionsSheet(spreadsheet) {
|
| 213 |
+
var sheetName = 'Questions';
|
| 214 |
+
|
| 215 |
+
Logger.log(sheetName + 'シートを更新中...');
|
| 216 |
+
|
| 217 |
+
// シート取得
|
| 218 |
+
var sheet = spreadsheet.getSheetByName(sheetName);
|
| 219 |
+
if (!sheet) {
|
| 220 |
+
throw new Error(sheetName + 'シートが存在しません');
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
// 現在のヘッダー行確認
|
| 224 |
+
var lastColumn = sheet.getLastColumn();
|
| 225 |
+
if (lastColumn === 0) {
|
| 226 |
+
Logger.log('⚠️ ' + sheetName + 'シートが空です。列追加をスキップします。');
|
| 227 |
+
return;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
var headers = sheet.getRange(1, 1, 1, lastColumn).getValues()[0];
|
| 231 |
+
Logger.log('現在のヘッダー: ' + headers.join(', '));
|
| 232 |
+
|
| 233 |
+
// category列が既に存在するかチェック
|
| 234 |
+
var categoryIndex = headers.indexOf('category');
|
| 235 |
+
if (categoryIndex !== -1) {
|
| 236 |
+
Logger.log('⚠️ category列は既に存在します(列' + (categoryIndex + 1) + ')。スキップします。');
|
| 237 |
+
return;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
// D列(subjectの次)に新規列を挿入
|
| 241 |
+
// 想定: A=question_id, B=session_id, C=subject, D=(新規category), E=difficulty(元のD)...
|
| 242 |
+
var insertPosition = 4; // D列(1-indexed)
|
| 243 |
+
sheet.insertColumnBefore(insertPosition);
|
| 244 |
+
|
| 245 |
+
// D1にヘッダー「category」を追加
|
| 246 |
+
sheet.getRange(1, insertPosition).setValue('category');
|
| 247 |
+
|
| 248 |
+
// D2にサンプルデータを追加(既存のサンプルデータがある場合)
|
| 249 |
+
var lastRow = sheet.getLastRow();
|
| 250 |
+
if (lastRow >= 2) {
|
| 251 |
+
sheet.getRange(2, insertPosition).setValue('漢字・語彙');
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
Logger.log('✅ ' + sheetName + 'シート更新完了(D列にcategory追加)');
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
/**
|
| 258 |
+
* Knowledge_Baseシート更新(category列追加)
|
| 259 |
+
*/
|
| 260 |
+
function updateKnowledgeBaseSheet(spreadsheet) {
|
| 261 |
+
var sheetName = 'Knowledge_Base';
|
| 262 |
+
|
| 263 |
+
Logger.log(sheetName + 'シートを更新中...');
|
| 264 |
+
|
| 265 |
+
// シート取得
|
| 266 |
+
var sheet = spreadsheet.getSheetByName(sheetName);
|
| 267 |
+
if (!sheet) {
|
| 268 |
+
throw new Error(sheetName + 'シートが存在しません');
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
// 現在のヘッダー行確認
|
| 272 |
+
var lastColumn = sheet.getLastColumn();
|
| 273 |
+
if (lastColumn === 0) {
|
| 274 |
+
Logger.log('⚠️ ' + sheetName + 'シートが空です。列追加をスキップします。');
|
| 275 |
+
return;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
var headers = sheet.getRange(1, 1, 1, lastColumn).getValues()[0];
|
| 279 |
+
Logger.log('現在のヘッダー: ' + headers.join(', '));
|
| 280 |
+
|
| 281 |
+
// category列が既に存在するかチェック
|
| 282 |
+
var categoryIndex = headers.indexOf('category');
|
| 283 |
+
if (categoryIndex !== -1) {
|
| 284 |
+
Logger.log('⚠️ category列は既に存在します(列' + (categoryIndex + 1) + ')。スキップします。');
|
| 285 |
+
return;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
// C列(subjectの次)に新規列を挿入
|
| 289 |
+
// 想定: A=kb_id, B=subject, C=(新規category), D=grade(元のC)...
|
| 290 |
+
var insertPosition = 3; // C列(1-indexed)
|
| 291 |
+
sheet.insertColumnBefore(insertPosition);
|
| 292 |
+
|
| 293 |
+
// C1にヘッダー「category」を追加
|
| 294 |
+
sheet.getRange(1, insertPosition).setValue('category');
|
| 295 |
+
|
| 296 |
+
// C2にサンプルデータを追加(既存のサンプルデータがある場合)
|
| 297 |
+
var lastRow = sheet.getLastRow();
|
| 298 |
+
if (lastRow >= 2) {
|
| 299 |
+
sheet.getRange(2, insertPosition).setValue('植物の成長');
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
Logger.log('✅ ' + sheetName + 'シート更新完了(C列にcategory追加)');
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
/**
|
| 306 |
+
* 空シート初期化関数: Sessions, Questions, Knowledge_Baseシートが空の場合に初期データを投入
|
| 307 |
+
*
|
| 308 |
+
* 実行方法:
|
| 309 |
+
* 1. Apps Scriptエディタで本ファイルを開く
|
| 310 |
+
* 2. initializeEmptySheets関数を選択
|
| 311 |
+
* 3. 実行ボタンをクリック
|
| 312 |
+
* 4. ログで結果を確認
|
| 313 |
+
*
|
| 314 |
+
* 処理内容:
|
| 315 |
+
* - Sessionsシート: ヘッダー + サンプルデータ1行
|
| 316 |
+
* - Questionsシート: ヘッダー(category列含む) + サンプルデータ1行
|
| 317 |
+
* - Knowledge_Baseシート: ヘッダー(category列含む) + サンプルデータ1行
|
| 318 |
+
*
|
| 319 |
+
* 安全機能:
|
| 320 |
+
* - シートが既にデータを持つ場合はスキップ
|
| 321 |
+
* - エラーハンドリング
|
| 322 |
+
* - 詳細ログ出力
|
| 323 |
+
*/
|
| 324 |
+
function initializeEmptySheets() {
|
| 325 |
+
try {
|
| 326 |
+
Logger.log('=== 空シート初期化開始 ===');
|
| 327 |
+
|
| 328 |
+
// スプレッドシート取得(Code.jsで定義されたSPREADSHEET_IDを使用)
|
| 329 |
+
var spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
|
| 330 |
+
Logger.log('スプレッドシート取得成功: ' + spreadsheet.getName());
|
| 331 |
+
|
| 332 |
+
// 各シートの初期化
|
| 333 |
+
Logger.log('\n--- シート初期化処理 ---');
|
| 334 |
+
initializeSessionsSheet(spreadsheet);
|
| 335 |
+
initializeQuestionsSheet(spreadsheet);
|
| 336 |
+
initializeKnowledgeBaseSheet(spreadsheet);
|
| 337 |
+
|
| 338 |
+
Logger.log('\n=== 空シート初期化完了! ===');
|
| 339 |
+
Logger.log('結果: 3シート(Sessions, Questions, Knowledge_Base)の初期化が完了しました');
|
| 340 |
+
Logger.log('次のステップ: Google Sheetsを開いて各シートにヘッダーとサンプルデータが存在することを確認してください');
|
| 341 |
+
|
| 342 |
+
} catch (error) {
|
| 343 |
+
Logger.log('\n❌ エラー発生: ' + error.message);
|
| 344 |
+
Logger.log('スタックトレース: ' + error.stack);
|
| 345 |
+
throw error;
|
| 346 |
+
}
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
/**
|
| 350 |
+
* Sessionsシート初期化
|
| 351 |
+
*/
|
| 352 |
+
function initializeSessionsSheet(spreadsheet) {
|
| 353 |
+
var sheetName = 'Sessions';
|
| 354 |
+
|
| 355 |
+
Logger.log(sheetName + 'シートを確認中...');
|
| 356 |
+
|
| 357 |
+
// シート取得
|
| 358 |
+
var sheet = spreadsheet.getSheetByName(sheetName);
|
| 359 |
+
if (!sheet) {
|
| 360 |
+
Logger.log('⚠️ ' + sheetName + 'シートが存在しません。スキップします。');
|
| 361 |
+
return;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// 既存データ確認(空シートかどうか)
|
| 365 |
+
var lastRow = sheet.getLastRow();
|
| 366 |
+
if (lastRow > 0) {
|
| 367 |
+
Logger.log('⚠️ ' + sheetName + 'シートは既にデータを持っています(' + lastRow + '行)。スキップします。');
|
| 368 |
+
return;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
Logger.log(sheetName + 'シートを初期化中...');
|
| 372 |
+
|
| 373 |
+
// ヘッダー行(1行目)
|
| 374 |
+
var headers = [
|
| 375 |
+
'session_id',
|
| 376 |
+
'user_id',
|
| 377 |
+
'subjects',
|
| 378 |
+
'start_time',
|
| 379 |
+
'end_time',
|
| 380 |
+
'total_score',
|
| 381 |
+
'completed'
|
| 382 |
+
];
|
| 383 |
+
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
| 384 |
+
|
| 385 |
+
// サンプルデータ(1行)
|
| 386 |
+
var sampleData = [
|
| 387 |
+
['session-sample-001', 'sample-user-001', '["国語","算数"]', '2025-11-04T10:30:00Z', '2025-11-04T10:45:00Z', 29, true]
|
| 388 |
+
];
|
| 389 |
+
sheet.getRange(2, 1, sampleData.length, headers.length).setValues(sampleData);
|
| 390 |
+
|
| 391 |
+
Logger.log('✅ ' + sheetName + 'シート初期化完了(ヘッダー + サンプルデータ1行)');
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
/**
|
| 395 |
+
* Questionsシート初期化(category列含む)
|
| 396 |
+
*/
|
| 397 |
+
function initializeQuestionsSheet(spreadsheet) {
|
| 398 |
+
var sheetName = 'Questions';
|
| 399 |
+
|
| 400 |
+
Logger.log(sheetName + 'シートを確認中...');
|
| 401 |
+
|
| 402 |
+
// シート取得
|
| 403 |
+
var sheet = spreadsheet.getSheetByName(sheetName);
|
| 404 |
+
if (!sheet) {
|
| 405 |
+
Logger.log('⚠️ ' + sheetName + 'シートが存在しません。スキップします。');
|
| 406 |
+
return;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
// 既存データ確認(空シートかどうか)
|
| 410 |
+
var lastRow = sheet.getLastRow();
|
| 411 |
+
if (lastRow > 0) {
|
| 412 |
+
Logger.log('⚠️ ' + sheetName + 'シートは既にデータを持っています(' + lastRow + '行)。スキップします。');
|
| 413 |
+
return;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
Logger.log(sheetName + 'シートを初期化中...');
|
| 417 |
+
|
| 418 |
+
// ヘッダー行(1行目)- category列を含む
|
| 419 |
+
var headers = [
|
| 420 |
+
'question_id',
|
| 421 |
+
'session_id',
|
| 422 |
+
'subject',
|
| 423 |
+
'category',
|
| 424 |
+
'difficulty',
|
| 425 |
+
'question_text',
|
| 426 |
+
'choices',
|
| 427 |
+
'correct_answer',
|
| 428 |
+
'explanation',
|
| 429 |
+
'created_at'
|
| 430 |
+
];
|
| 431 |
+
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
| 432 |
+
|
| 433 |
+
// サンプルデータ(1行)
|
| 434 |
+
var sampleData = [
|
| 435 |
+
[
|
| 436 |
+
'q-sample-001',
|
| 437 |
+
'session-sample-001',
|
| 438 |
+
'国語',
|
| 439 |
+
'漢字・語彙',
|
| 440 |
+
'標準',
|
| 441 |
+
'次の漢字の読みを選びなさい:「成就」',
|
| 442 |
+
'["じょうじゅ","せいしゅう","せいじゅ","じょうしゅう"]',
|
| 443 |
+
0,
|
| 444 |
+
'「成就」は「じょうじゅ」と読みます。',
|
| 445 |
+
'2025-11-04T10:31:00Z'
|
| 446 |
+
]
|
| 447 |
+
];
|
| 448 |
+
sheet.getRange(2, 1, sampleData.length, headers.length).setValues(sampleData);
|
| 449 |
+
|
| 450 |
+
Logger.log('✅ ' + sheetName + 'シート初期化完了(ヘッダー(category列含む) + サンプルデータ1行)');
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
/**
|
| 454 |
+
* Knowledge_Baseシート初期化(category列含む)
|
| 455 |
+
*/
|
| 456 |
+
function initializeKnowledgeBaseSheet(spreadsheet) {
|
| 457 |
+
var sheetName = 'Knowledge_Base';
|
| 458 |
+
|
| 459 |
+
Logger.log(sheetName + 'シートを確認中...');
|
| 460 |
+
|
| 461 |
+
// シート取得
|
| 462 |
+
var sheet = spreadsheet.getSheetByName(sheetName);
|
| 463 |
+
if (!sheet) {
|
| 464 |
+
Logger.log('⚠️ ' + sheetName + 'シートが存在しません。スキップします。');
|
| 465 |
+
return;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
// 既存データ確認(空シートかどうか)
|
| 469 |
+
var lastRow = sheet.getLastRow();
|
| 470 |
+
if (lastRow > 0) {
|
| 471 |
+
Logger.log('⚠️ ' + sheetName + 'シートは既にデータを持っています(' + lastRow + '行)。スキップします。');
|
| 472 |
+
return;
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
Logger.log(sheetName + 'シートを初期化中...');
|
| 476 |
+
|
| 477 |
+
// ヘッダー行(1行目)- category列を含む
|
| 478 |
+
var headers = [
|
| 479 |
+
'kb_id',
|
| 480 |
+
'subject',
|
| 481 |
+
'category',
|
| 482 |
+
'grade',
|
| 483 |
+
'content',
|
| 484 |
+
'difficulty',
|
| 485 |
+
'usage_count',
|
| 486 |
+
'last_used'
|
| 487 |
+
];
|
| 488 |
+
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
| 489 |
+
|
| 490 |
+
// サンプルデータ(1行)
|
| 491 |
+
var sampleData = [
|
| 492 |
+
[
|
| 493 |
+
'kb-sample-001',
|
| 494 |
+
'理科',
|
| 495 |
+
'植物の成長',
|
| 496 |
+
5,
|
| 497 |
+
'種子が発芽するためには、水・空気・適切な温度が必要である。',
|
| 498 |
+
2,
|
| 499 |
+
5,
|
| 500 |
+
'2025-11-04T09:00:00Z'
|
| 501 |
+
]
|
| 502 |
+
];
|
| 503 |
+
sheet.getRange(2, 1, sampleData.length, headers.length).setValues(sampleData);
|
| 504 |
+
|
| 505 |
+
Logger.log('✅ ' + sheetName + 'シート初期化完了(ヘッダー(category列含む) + サンプルデータ1行)');
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
/**
|
| 509 |
+
* テスト関数: setupSheetsV2の動作確認
|
| 510 |
+
*/
|
| 511 |
+
function testSetupSheetsV2() {
|
| 512 |
+
Logger.log('=== テスト実行: setupSheetsV2 ===');
|
| 513 |
+
setupSheetsV2();
|
| 514 |
+
Logger.log('=== テスト完了 ===');
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
/**
|
| 518 |
+
* テスト関数: initializeEmptySheetsの動作確認
|
| 519 |
+
*/
|
| 520 |
+
function testInitializeEmptySheets() {
|
| 521 |
+
Logger.log('=== テスト実行: initializeEmptySheets ===');
|
| 522 |
+
initializeEmptySheets();
|
| 523 |
+
Logger.log('=== テスト完了 ===');
|
| 524 |
+
}
|
V1.7.1/gas/setup_sheets_v3.js
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* 超天才クイズv3 シートリセット・初期化スクリプト v3
|
| 3 |
+
*
|
| 4 |
+
* v1.6.8用: 現行Code.jsのスキーマに合わせたシート初期化
|
| 5 |
+
*
|
| 6 |
+
* 実行方法:
|
| 7 |
+
* 1. Apps Scriptエディタで本ファイルを開く
|
| 8 |
+
* 2. 実行したい関数を選択
|
| 9 |
+
* 3. 実行ボタンをクリック
|
| 10 |
+
*
|
| 11 |
+
* 提供関数:
|
| 12 |
+
* - deleteAllSheetsExceptQuestionDB(): QuestionDatabase以外のシートを削除
|
| 13 |
+
* - initializeAllSheets(): 現行スキーマで全シートを初期化
|
| 14 |
+
* - resetAndInitialize(): 削除 + 初期化を一括実行
|
| 15 |
+
*
|
| 16 |
+
* @version 3.0
|
| 17 |
+
* @date 2025-12-20
|
| 18 |
+
*/
|
| 19 |
+
|
| 20 |
+
// Note: SPREADSHEET_IDはCode.jsで定義済み
|
| 21 |
+
|
| 22 |
+
/**
|
| 23 |
+
* QuestionDatabase以外の全シートを削除
|
| 24 |
+
*
|
| 25 |
+
* 注意: この操作は取り消せません!
|
| 26 |
+
* QuestionDatabaseシートは保護されます。
|
| 27 |
+
*/
|
| 28 |
+
function deleteAllSheetsExceptQuestionDB() {
|
| 29 |
+
try {
|
| 30 |
+
Logger.log('=== シート削除開始(QuestionDatabase以外) ===');
|
| 31 |
+
|
| 32 |
+
const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
|
| 33 |
+
const sheets = spreadsheet.getSheets();
|
| 34 |
+
|
| 35 |
+
Logger.log('現在のシート数: ' + sheets.length);
|
| 36 |
+
|
| 37 |
+
// 保護するシート
|
| 38 |
+
const protectedSheets = ['QuestionDatabase'];
|
| 39 |
+
|
| 40 |
+
// 削除対象シートを収集
|
| 41 |
+
const sheetsToDelete = [];
|
| 42 |
+
for (let i = 0; i < sheets.length; i++) {
|
| 43 |
+
const sheetName = sheets[i].getName();
|
| 44 |
+
if (!protectedSheets.includes(sheetName)) {
|
| 45 |
+
sheetsToDelete.push(sheetName);
|
| 46 |
+
} else {
|
| 47 |
+
Logger.log('✅ 保護: ' + sheetName);
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// シート削除(最低1シートは残す必要があるため、仮シートを作成)
|
| 52 |
+
if (sheetsToDelete.length === sheets.length) {
|
| 53 |
+
// 全シート削除しようとしている場合、一時シートを作成
|
| 54 |
+
spreadsheet.insertSheet('_temp_');
|
| 55 |
+
Logger.log('一時シート作成(全削除防止)');
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// 削除実行
|
| 59 |
+
for (let i = 0; i < sheetsToDelete.length; i++) {
|
| 60 |
+
const sheetName = sheetsToDelete[i];
|
| 61 |
+
const sheet = spreadsheet.getSheetByName(sheetName);
|
| 62 |
+
if (sheet) {
|
| 63 |
+
spreadsheet.deleteSheet(sheet);
|
| 64 |
+
Logger.log('❌ 削除: ' + sheetName);
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
Logger.log('\n=== シート削除完了 ===');
|
| 69 |
+
Logger.log('削除したシート: ' + sheetsToDelete.join(', '));
|
| 70 |
+
Logger.log('残りのシート数: ' + spreadsheet.getSheets().length);
|
| 71 |
+
|
| 72 |
+
return { success: true, deleted: sheetsToDelete };
|
| 73 |
+
|
| 74 |
+
} catch (error) {
|
| 75 |
+
Logger.log('\n❌ エラー発生: ' + error.message);
|
| 76 |
+
throw error;
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/**
|
| 81 |
+
* 現行スキーマで全シートを初期化
|
| 82 |
+
*
|
| 83 |
+
* Code.jsのSHEET_NAMESとgetOrCreateSheet()の定義に基づく
|
| 84 |
+
*/
|
| 85 |
+
function initializeAllSheets() {
|
| 86 |
+
try {
|
| 87 |
+
Logger.log('=== シート初期化開始(v3スキーマ) ===');
|
| 88 |
+
|
| 89 |
+
const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
|
| 90 |
+
|
| 91 |
+
// 一時シートがあれば後で削除
|
| 92 |
+
const tempSheet = spreadsheet.getSheetByName('_temp_');
|
| 93 |
+
|
| 94 |
+
// 各シートを初期化
|
| 95 |
+
initializeUsersSheet(spreadsheet);
|
| 96 |
+
initializeSessionsSheet(spreadsheet);
|
| 97 |
+
initializeAnswersSheet(spreadsheet);
|
| 98 |
+
initializeStatisticsSheet(spreadsheet);
|
| 99 |
+
initializeEvaluationsSheet(spreadsheet);
|
| 100 |
+
initializeGeneratedQuestionsSheet(spreadsheet);
|
| 101 |
+
initializeSummariesSheet(spreadsheet);
|
| 102 |
+
|
| 103 |
+
// 一時シートを削除
|
| 104 |
+
if (tempSheet) {
|
| 105 |
+
spreadsheet.deleteSheet(tempSheet);
|
| 106 |
+
Logger.log('一時シート削除');
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
Logger.log('\n=== シート初期化完了 ===');
|
| 110 |
+
Logger.log('次のステップ: Google Sheetsを開いて各シートのヘッダーを確認してください');
|
| 111 |
+
|
| 112 |
+
return { success: true };
|
| 113 |
+
|
| 114 |
+
} catch (error) {
|
| 115 |
+
Logger.log('\n❌ エラー発生: ' + error.message);
|
| 116 |
+
throw error;
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/**
|
| 121 |
+
* シート削除 + 初期化を一括実行
|
| 122 |
+
*/
|
| 123 |
+
function resetAndInitialize() {
|
| 124 |
+
Logger.log('========================================');
|
| 125 |
+
Logger.log('シートリセット&初期化 開始');
|
| 126 |
+
Logger.log('========================================\n');
|
| 127 |
+
|
| 128 |
+
deleteAllSheetsExceptQuestionDB();
|
| 129 |
+
|
| 130 |
+
Logger.log('\n');
|
| 131 |
+
|
| 132 |
+
initializeAllSheets();
|
| 133 |
+
|
| 134 |
+
Logger.log('\n========================================');
|
| 135 |
+
Logger.log('シートリセット&初期化 完了!');
|
| 136 |
+
Logger.log('========================================');
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// ============================================================================
|
| 140 |
+
// 個別シート初期化関数
|
| 141 |
+
// ============================================================================
|
| 142 |
+
|
| 143 |
+
/**
|
| 144 |
+
* Usersシート初期化
|
| 145 |
+
* スキーマ: user_id, username, created_at, total_sessions, total_questions
|
| 146 |
+
*/
|
| 147 |
+
function initializeUsersSheet(spreadsheet) {
|
| 148 |
+
const sheetName = 'Users';
|
| 149 |
+
Logger.log(sheetName + 'シートを初期化中...');
|
| 150 |
+
|
| 151 |
+
let sheet = spreadsheet.getSheetByName(sheetName);
|
| 152 |
+
if (sheet) {
|
| 153 |
+
Logger.log('⚠️ ' + sheetName + 'シートは既に存在します。スキップします。');
|
| 154 |
+
return;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
sheet = spreadsheet.insertSheet(sheetName);
|
| 158 |
+
|
| 159 |
+
const headers = [
|
| 160 |
+
'user_id',
|
| 161 |
+
'username',
|
| 162 |
+
'created_at',
|
| 163 |
+
'total_sessions',
|
| 164 |
+
'total_questions'
|
| 165 |
+
];
|
| 166 |
+
|
| 167 |
+
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
| 168 |
+
sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');
|
| 169 |
+
|
| 170 |
+
Logger.log('✅ ' + sheetName + 'シート初期化完了');
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
/**
|
| 174 |
+
* Sessionsシート初期化
|
| 175 |
+
* スキーマ: session_id, user_id, subjects, start_time, end_time, total_score, completed
|
| 176 |
+
*/
|
| 177 |
+
function initializeSessionsSheet(spreadsheet) {
|
| 178 |
+
const sheetName = 'Sessions';
|
| 179 |
+
Logger.log(sheetName + 'シートを初期化中...');
|
| 180 |
+
|
| 181 |
+
let sheet = spreadsheet.getSheetByName(sheetName);
|
| 182 |
+
if (sheet) {
|
| 183 |
+
Logger.log('⚠️ ' + sheetName + 'シートは既に存在します。スキップします。');
|
| 184 |
+
return;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
sheet = spreadsheet.insertSheet(sheetName);
|
| 188 |
+
|
| 189 |
+
const headers = [
|
| 190 |
+
'session_id',
|
| 191 |
+
'user_id',
|
| 192 |
+
'subjects',
|
| 193 |
+
'start_time',
|
| 194 |
+
'end_time',
|
| 195 |
+
'total_score',
|
| 196 |
+
'completed'
|
| 197 |
+
];
|
| 198 |
+
|
| 199 |
+
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
| 200 |
+
sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');
|
| 201 |
+
|
| 202 |
+
Logger.log('✅ ' + sheetName + 'シート初期化完了');
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
/**
|
| 206 |
+
* Answersシート初期化
|
| 207 |
+
* スキーマ: answer_id, session_id, question_id, subject, category, user_answer, correct_answer, is_correct, time_spent, submitted_at
|
| 208 |
+
*
|
| 209 |
+
* ★重要: v1.6.6で修正されたスキーマ(10列)
|
| 210 |
+
*/
|
| 211 |
+
function initializeAnswersSheet(spreadsheet) {
|
| 212 |
+
const sheetName = 'Answers';
|
| 213 |
+
Logger.log(sheetName + 'シートを初期化中...');
|
| 214 |
+
|
| 215 |
+
let sheet = spreadsheet.getSheetByName(sheetName);
|
| 216 |
+
if (sheet) {
|
| 217 |
+
Logger.log('⚠️ ' + sheetName + 'シートは既に存在します。スキップします。');
|
| 218 |
+
return;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
sheet = spreadsheet.insertSheet(sheetName);
|
| 222 |
+
|
| 223 |
+
// v1.6.6で修正されたスキーマ(10列)
|
| 224 |
+
const headers = [
|
| 225 |
+
'answer_id', // A
|
| 226 |
+
'session_id', // B
|
| 227 |
+
'question_id', // C
|
| 228 |
+
'subject', // D
|
| 229 |
+
'category', // E
|
| 230 |
+
'user_answer', // F
|
| 231 |
+
'correct_answer', // G ★追加
|
| 232 |
+
'is_correct', // H
|
| 233 |
+
'time_spent', // I
|
| 234 |
+
'submitted_at' // J
|
| 235 |
+
];
|
| 236 |
+
|
| 237 |
+
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
| 238 |
+
sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');
|
| 239 |
+
|
| 240 |
+
Logger.log('✅ ' + sheetName + 'シート初期化完了(10列スキーマ)');
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
/**
|
| 244 |
+
* Statisticsシート初期化
|
| 245 |
+
* スキーマ: stat_id, user_id, subject, category, total_attempted, correct_count, accuracy_rate, last_updated
|
| 246 |
+
*/
|
| 247 |
+
function initializeStatisticsSheet(spreadsheet) {
|
| 248 |
+
const sheetName = 'Statistics';
|
| 249 |
+
Logger.log(sheetName + 'シートを初期化中...');
|
| 250 |
+
|
| 251 |
+
let sheet = spreadsheet.getSheetByName(sheetName);
|
| 252 |
+
if (sheet) {
|
| 253 |
+
Logger.log('⚠️ ' + sheetName + 'シートは既に存在します。スキップします。');
|
| 254 |
+
return;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
sheet = spreadsheet.insertSheet(sheetName);
|
| 258 |
+
|
| 259 |
+
const headers = [
|
| 260 |
+
'stat_id',
|
| 261 |
+
'user_id',
|
| 262 |
+
'subject',
|
| 263 |
+
'category',
|
| 264 |
+
'total_attempted',
|
| 265 |
+
'correct_count',
|
| 266 |
+
'accuracy_rate',
|
| 267 |
+
'last_updated'
|
| 268 |
+
];
|
| 269 |
+
|
| 270 |
+
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
| 271 |
+
sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');
|
| 272 |
+
|
| 273 |
+
Logger.log('✅ ' + sheetName + 'シート初期化完了');
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
/**
|
| 277 |
+
* Evaluationsシート初期化
|
| 278 |
+
* スキーマ: evaluation_id, session_id, subject, advice, strengths, weaknesses, recommended_topics, created_at
|
| 279 |
+
*/
|
| 280 |
+
function initializeEvaluationsSheet(spreadsheet) {
|
| 281 |
+
const sheetName = 'Evaluations';
|
| 282 |
+
Logger.log(sheetName + 'シートを初期化中...');
|
| 283 |
+
|
| 284 |
+
let sheet = spreadsheet.getSheetByName(sheetName);
|
| 285 |
+
if (sheet) {
|
| 286 |
+
Logger.log('⚠️ ' + sheetName + 'シートは既に存在します。スキップします。');
|
| 287 |
+
return;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
sheet = spreadsheet.insertSheet(sheetName);
|
| 291 |
+
|
| 292 |
+
const headers = [
|
| 293 |
+
'evaluation_id',
|
| 294 |
+
'session_id',
|
| 295 |
+
'subject',
|
| 296 |
+
'advice',
|
| 297 |
+
'strengths',
|
| 298 |
+
'weaknesses',
|
| 299 |
+
'recommended_topics',
|
| 300 |
+
'created_at'
|
| 301 |
+
];
|
| 302 |
+
|
| 303 |
+
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
| 304 |
+
sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');
|
| 305 |
+
|
| 306 |
+
Logger.log('✅ ' + sheetName + 'シート初期化完了');
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
/**
|
| 310 |
+
* GeneratedQuestionsシート初期化
|
| 311 |
+
* スキーマ: question_id, session_id, subject, genre_id, answer, question_text, choices, correct_answer, difficulty, created_at
|
| 312 |
+
*/
|
| 313 |
+
function initializeGeneratedQuestionsSheet(spreadsheet) {
|
| 314 |
+
const sheetName = 'GeneratedQuestions';
|
| 315 |
+
Logger.log(sheetName + 'シートを初期化中...');
|
| 316 |
+
|
| 317 |
+
let sheet = spreadsheet.getSheetByName(sheetName);
|
| 318 |
+
if (sheet) {
|
| 319 |
+
Logger.log('⚠️ ' + sheetName + 'シートは既に存在します。スキップします。');
|
| 320 |
+
return;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
sheet = spreadsheet.insertSheet(sheetName);
|
| 324 |
+
|
| 325 |
+
const headers = [
|
| 326 |
+
'question_id',
|
| 327 |
+
'session_id',
|
| 328 |
+
'subject',
|
| 329 |
+
'genre_id',
|
| 330 |
+
'answer',
|
| 331 |
+
'question_text',
|
| 332 |
+
'choices',
|
| 333 |
+
'correct_answer',
|
| 334 |
+
'difficulty',
|
| 335 |
+
'created_at'
|
| 336 |
+
];
|
| 337 |
+
|
| 338 |
+
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
| 339 |
+
sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');
|
| 340 |
+
|
| 341 |
+
Logger.log('✅ ' + sheetName + 'シート初期化完了');
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
/**
|
| 345 |
+
* Summariesシート初期化
|
| 346 |
+
* スキーマ: summary_id, session_id, subject, keywords, topics, summary, created_at
|
| 347 |
+
*/
|
| 348 |
+
function initializeSummariesSheet(spreadsheet) {
|
| 349 |
+
const sheetName = 'Summaries';
|
| 350 |
+
Logger.log(sheetName + 'シートを初期化中...');
|
| 351 |
+
|
| 352 |
+
let sheet = spreadsheet.getSheetByName(sheetName);
|
| 353 |
+
if (sheet) {
|
| 354 |
+
Logger.log('⚠️ ' + sheetName + 'シートは既に存在します。スキップします。');
|
| 355 |
+
return;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
sheet = spreadsheet.insertSheet(sheetName);
|
| 359 |
+
|
| 360 |
+
const headers = [
|
| 361 |
+
'summary_id',
|
| 362 |
+
'session_id',
|
| 363 |
+
'subject',
|
| 364 |
+
'keywords',
|
| 365 |
+
'topics',
|
| 366 |
+
'summary',
|
| 367 |
+
'created_at'
|
| 368 |
+
];
|
| 369 |
+
|
| 370 |
+
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
| 371 |
+
sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');
|
| 372 |
+
|
| 373 |
+
Logger.log('✅ ' + sheetName + 'シート初期化完了');
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
// ============================================================================
|
| 377 |
+
// テスト関数
|
| 378 |
+
// ============================================================================
|
| 379 |
+
|
| 380 |
+
/**
|
| 381 |
+
* シート一覧を表示(デバッグ用)
|
| 382 |
+
*/
|
| 383 |
+
function listAllSheets() {
|
| 384 |
+
const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
|
| 385 |
+
const sheets = spreadsheet.getSheets();
|
| 386 |
+
|
| 387 |
+
Logger.log('=== シート一覧 ===');
|
| 388 |
+
for (let i = 0; i < sheets.length; i++) {
|
| 389 |
+
const sheet = sheets[i];
|
| 390 |
+
const lastRow = sheet.getLastRow();
|
| 391 |
+
const lastCol = sheet.getLastColumn();
|
| 392 |
+
Logger.log((i + 1) + '. ' + sheet.getName() + ' (' + lastRow + '行 x ' + lastCol + '列)');
|
| 393 |
+
|
| 394 |
+
// ヘッダー行を表示
|
| 395 |
+
if (lastCol > 0) {
|
| 396 |
+
const headers = sheet.getRange(1, 1, 1, lastCol).getValues()[0];
|
| 397 |
+
Logger.log(' ヘッダー: ' + headers.join(', '));
|
| 398 |
+
}
|
| 399 |
+
}
|
| 400 |
+
Logger.log('=== 合計: ' + sheets.length + 'シート ===');
|
| 401 |
+
}
|
V1.7.1/knowledge/ORIGINAL/中学入試 でる順 ポケでる国語 漢字・熟語 四訂版.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
V1.7.1/knowledge/ORIGINAL/合格物語.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
V1.7.1/knowledge/ORIGINAL/合格論説文.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
V1.7.1/knowledge/ORIGINAL/理科/改訂版 中学入試にでる順 理科 力・運動・電気・光、物質・エネルギー.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
V1.7.1/knowledge/ORIGINAL/理科/改訂版 中学入試にでる順 理科 植物・動物・人体、地球・宇宙.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
V1.7.1/knowledge/ORIGINAL/社会/中学入試にでる順 地理(改訂版).md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
V1.7.1/knowledge/ORIGINAL/社会/中学入試にでる順 社会 歴史(改訂版).md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
V1.7.1/knowledge/ORIGINAL/社会/中学受験 社会 裏ワザテクニック Wチェック問題集 歴史編.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
V1.7.1/knowledge/ORIGINAL/社会/中学受験 社会の裏ワザテクニックWチェック問題集 地理編.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
V1.7.1/knowledge/ORIGINAL/算数/つまずきやすいところが絶対つまずかない! 小学校6年間の図形の教え方.md
ADDED
|
@@ -0,0 +1,832 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 図形の教え方(小学校6年間)
|
| 2 |
+
|
| 3 |
+
[CHUNK]
|
| 4 |
+
## 図形学習のつまずき解消と本書の目的
|
| 5 |
+
本書は、小学校算数における図形問題でつまずきやすい子どもたちに向けて、苦手意識を克服し、図形問題の解き方を習得できるよう構成されている。プロ家庭教師の経験に基づき、図形問題が苦手な子どもの特徴を分析し、克服のための具体的なアプローチを提示する。図形問題はセンスや才能ではなく、正しい方法と訓練で誰でも解けるようになることを強調する。
|
| 6 |
+
|
| 7 |
+
[CHUNK]
|
| 8 |
+
## 図形問題克服の重要性と身体感覚の活用
|
| 9 |
+
センスや才能に頼らず、図形問題は正しいアプローチと訓練で克服可能である。特に、道具の正しい使い方や、図形をかく、折る、切る、ひっくり返すといった身体感覚が重要となる。低学年のうちにこれらの経験を積むことが望ましい。経験を言葉で表現することで、知識として定着を促す。
|
| 10 |
+
|
| 11 |
+
[CHUNK]
|
| 12 |
+
## 身の回りの形を通じた学習と本書の活用
|
| 13 |
+
身の回りの形を通して、お子さんと楽しくコミュニケーションをとることが重要。「折り紙をななめに折ったら三角形になったね」「きゅうりをまっすぐ切ったら、切り口はぜんぶ丸だね」などの声かけが効果的。高学年でも遅すぎることはなく、中学、高校の数学につながる。本書は教科書の応用問題が解けるようになることを目標に、図形問題に強くなるヒントを提供する。
|
| 14 |
+
|
| 15 |
+
[CHUNK]
|
| 16 |
+
## 本書を通じた親子の成長と良好な関係構築
|
| 17 |
+
本書を通して、親御さんがお子さんとともに、問題が解ける喜びや達成感を味わい、親子の良好な関係が育まれることを願っている。教科書の応用問題が解けるようになることを目標に、図形問題に強くなるヒントを随所にちりばめた。ぜひ、参考にしてください。
|
| 18 |
+
|
| 19 |
+
# 小学校6年間の図形の教え方
|
| 20 |
+
|
| 21 |
+
[CHUNK]
|
| 22 |
+
## 図形学習の導入と苦手意識克服
|
| 23 |
+
図形問題が苦手な子どもは多い。図形問題を解く力は、身の回りの図形を実際に見て、触って、手を動かす経験で伸ばせる。行き詰まったら、問題から離れ、日常生活で図形に親しむ機会を増やそう。
|
| 24 |
+
図形が得意な子は、つみきや折り紙が好きで、身の回りにある形に気づきやすい。苦手な子は、図形と自分との関わりを感じにくく、面積の公式などを暗記に頼りがちである。
|
| 25 |
+
|
| 26 |
+
[CHUNK]
|
| 27 |
+
## 図形学習における親の役割と教え方のコツ
|
| 28 |
+
図形を教える際は、子どもの中から答えを引き出すことを重視する。自分で答えが出せた経験は自信につながり、解き方の手順も身につく。どんな小さなことでも褒めて、子どものやる気を引き出す。
|
| 29 |
+
結論を一方的に説明するのではなく、「〇〇の公式は覚えてる?」「□□って何だっけ?」と優しく問いかけながら、一緒に考える姿勢が大切。コミュニケーションを取りながら進めることで、図形学習を楽しいものにし、苦手意識を克服する。
|
| 30 |
+
|
| 31 |
+
[CHUNK]
|
| 32 |
+
## 日常生活での図形との触れ合い方
|
| 33 |
+
身の回りにあるものの形を探す(例:将棋の駒は五角形、傘は八角形)。トランプのマークが線対称であることを見つけるなど、遊びの中で図形の性質に触れる。
|
| 34 |
+
自分の体を使って角度を作る(例:座って足を何度まで開けるか)。料理の手伝いを通して、かさの感覚を養う(例:鍋に1Lの水を入れてくれる?)。本書p.14の「お助けアイテム」も参考に。
|
| 35 |
+
公式や手順を丸暗記するのではなく、「なぜそうなるのか」の背景を理解することが重要。感動したことや自分で気づいたことは忘れずに、応用問題にも生かせるようにする。
|
| 36 |
+
|
| 37 |
+
[CHUNK]
|
| 38 |
+
## 作図が苦手な子へのアプローチ
|
| 39 |
+
作図ができるようになると図形全般の理解度が高まる。まずは身近な道具や身の回りのものを使って、手を動かすことから始める。
|
| 40 |
+
対称な図形の感覚を養うには、折り紙を折って切って広げるのが効果的。好きな形を簡単につくれる点、ひっくり返したり裏返したりと図形を実際に動かせる点も優れている。頭の中で図形を動かして考える練習になる。
|
| 41 |
+
|
| 42 |
+
[CHUNK]
|
| 43 |
+
## 作図練習のステップ
|
| 44 |
+
直線を引く練習:定規は目盛りのない面を上にし、利き手と反対の手でしっかり押さえる。定規が動かないように、5~10本、30cmくらいの線を引く。
|
| 45 |
+
コンパス選び:針がしっかり刺さるものを選ぶ。コンパスを使えばきれいな円が自分でかけるという経験は、自信に��ながる。
|
| 46 |
+
図形をなぞってかく:問題集などに載っている図を上からなぞらせる。三角形や四角形、円から始め、複合図形や立体の見取り図など、複雑な図を何度もなぞって自分のものにする。慣れてきたら、図を見ながらノートにかく練習もする。
|
| 47 |
+
|
| 48 |
+
[CHUNK]
|
| 49 |
+
## 親御さんの不安解消と図形学習サポート
|
| 50 |
+
「図形が苦手」「算数が苦手」「応用問題が苦手」という親御さんも大丈夫。本書には、親子で図形に親しみ、図形の問題が得意になるヒントが多数ある。
|
| 51 |
+
本書には、多くの子どもが納得できる解説が書かれているため、親御さんもすんなり理解できる。難しく感じる場合は、前の学年にもどって、基本から1つずつ理解していくとよい。
|
| 52 |
+
|
| 53 |
+
[CHUNK]
|
| 54 |
+
## 図形問題克服のためのアイテム
|
| 55 |
+
図形を身近に感じ、問題をすらすら解けるようにする道具を紹介。勉強のときだけでなく、ふだんの生活や遊びの中に取り入れる。
|
| 56 |
+
定規・メジャー:長さの単位に慣れる。子どもの身長、手の大きさ、指の長さなどを測る。
|
| 57 |
+
三角定規:平行や垂直な線を引く。直角三角形の典型的な見本として覚えておく。
|
| 58 |
+
分度器:角度を測る。家の中のいろいろなものを測ってみる。
|
| 59 |
+
|
| 60 |
+
[CHUNK]
|
| 61 |
+
## 図形学習をサポートするアイテム(続き)
|
| 62 |
+
コンパス:円をかく。作図のときにも頻繁に使用。円をたくさんかいてコンパスの扱いに慣れる。
|
| 63 |
+
計量カップ・計量スプーン:かさや重さの単位を実感。1Lのペットボトルに水を入れて、1kgの重さを体感させる。
|
| 64 |
+
折り紙:折ったり、切ったり、楽しみながら図形の性質を身につける。三角形、四角形、角度、対称な図形など、活用方法は無限。
|
| 65 |
+
空き箱:立体の感覚をつかむ。ティッシュペーパーの空き箱は、開いて展開図にできるなど活用しやすい。
|
| 66 |
+
方眼用紙:正確な図形の感覚を身につけるために便利。5mm方眼がおすすめだが、ノートのマス目を利用してもOK。
|
| 67 |
+
|
| 68 |
+
[CHUNK]
|
| 69 |
+
## 単位の理解:視覚的アプローチの重要性
|
| 70 |
+
単位は、「何倍の関係か」を視覚的に身につけることが重要。
|
| 71 |
+
牛乳パックはサイコロ1000個分:1辺の長さ1cm (1cm³=1mL) の市販のサイコロを1個用意し、1000個集めると牛乳パック1個の大きさ(1000cm³)になることを視覚的に理解させる。計量カップ (200mL)で、空の牛乳パックに水を入れ、5杯でいっぱいになることを確認するのも効果的。
|
| 72 |
+
|
| 73 |
+
[CHUNK]
|
| 74 |
+
## 水の体積と重さの関係を体感する
|
| 75 |
+
水の体積と重さの関係を実感させる:計量カップをのせた状態で0gに合わせたはかりを使って、水の重さを実際に確かめる。
|
| 76 |
+
100mL (100cm³) の水の重さが100gということが確認できたら、「実は重さの単位は水の重さを基準に決められているんだって」と声かけを。理科の勉強にも役立つ、とても大切な感覚が身につく。
|
| 77 |
+
単位の変換をするときは、0がいくつ増える・減る関係か、イメージがわくようになるとよい。
|
| 78 |
+
|
| 79 |
+
[CHUNK]
|
| 80 |
+
## 単位換算の注意点と練習問題
|
| 81 |
+
単位換算では、かくれた「0」を見逃さないように注意する。
|
| 82 |
+
例:1L50mL = (1050)mL。上下にケタをそろえて筆算を書くように伝え、かくれた100の位の「0」を見せて意識させる。方眼ノートを使ってみるのも効果的。
|
| 83 |
+
単位の変換をするときは、0がいくつ増える・減る関係か、イメージがわくようになるとよい。
|
| 84 |
+
|
| 85 |
+
# 小学校算数つまずきポイント解決ナレッジベース
|
| 86 |
+
|
| 87 |
+
[CHUNK]
|
| 88 |
+
## 2年生: くり下がりのある単位の計算 (cm, mm)
|
| 89 |
+
2年生でつまずきやすい、くり下がりのある単位の計算(例:3cm2mm - 1cm7mm)について解説します。前提として、1m=100cm、1cm=10mmといった基本的な単位の知識が定着している必要があります。本質的な理解のため、最初はmmに変換して計算し、慣れてきたらcmとmmのまま計算する方法を練習します。mmに直して計算する際は、3cm2mmを32mm、1cm7mmを17mmに変換し、32mm - 17mm = 15mm = 1cm5mmと計算します。cmとmmのまま筆算する場合は、位を揃えて書き、1cmを10mmとして繰り下げて計算します。
|
| 90 |
+
|
| 91 |
+
[CHUNK]
|
| 92 |
+
## 2年生: くり下がりのある単位の計算練習と指導のポイント
|
| 93 |
+
cmとmmのまま筆算する場合は、位を揃えて書き、1cmを10mmとして繰り下げて計算します。単位換算の練習として、「3cmは何mm?」「3cm2mmは何mm?」といった質問を投げかけ、基本的な単位間の関係を理解させることが重要です。いきなり「3cm2mmは何mm?」と聞くのではなく、「1cmは���mm?」「1mは何cm?」「1kgは何g?」といった基本的な単位間の関係を声かけしながら練習することがポイントです。練習問題として、5cm4mm-3cm6mm、1m45cm-77cm、2kg300g-1kg600g、5L3dL-3L7dL、3m7cm-89cmなどが考えられます。
|
| 94 |
+
|
| 95 |
+
[CHUNK]
|
| 96 |
+
## 2年生: 間に0が入る単位の変換 (m, cm)
|
| 97 |
+
2年生でつまずきやすい、間に0が入る単位の変換(例:3m7cm = cm)について解説します。長さ、かさ、重さの単位の変換が定着していないと、見たままの数字で答えてしまうことがあります。1m=100cmということがしっかり定着していないと、省略されている0に気づけず「3m7cm=37cm」といったミスをしてしまいます。3m7cm = 37cm ?という誤答に対して、3と7の間の0に気づいていないことを指摘します。
|
| 98 |
+
|
| 99 |
+
[CHUNK]
|
| 100 |
+
## 2年生: 単位変換の指導と練習問題
|
| 101 |
+
3m7cm = 37cm ?という誤答に対して、3と7の間の0に気づいていないことを指摘します。「3mは何cm?」の声かけとケタをそろえた書き方を習慣づけることで、あわててミスすることがなくなります。3m = 300cm、7cmをたてにそろえて書く練習をします。方眼ノートを使うと、そろえて書きやすいでしょう。「3mは300cm」と書いて、「7cm」も書いて、目で見て確認します。その中で「ケタをそろえる」を習慣にするのが最大のポイントです。練習問題として、4m8cm=( )cm、12m5cm=( )cm、4L80mL=( )mL、105cm=( )m( )cm、3075mL =( )L( )mLなどが考えられます。
|
| 102 |
+
|
| 103 |
+
[CHUNK]
|
| 104 |
+
## 6年生: 水のかさ、重さ、体積の変換 (L, mL, cm³)
|
| 105 |
+
6年生でつまずきやすい、水のかさ、重さ、体積の変換(例:水1L500mLの重さ)について解説します。ものの重さは水を基準に決まっていて、水1mLの重さを1gとしています。つまり体積の単位を正しく知っておくことで、水の重さがわかるようになります。cm³とL(リットル)、dL(デシリットル)、mL(ミリリットル) の関係がわかっていないと、まちがえてしまいます。1L500mLの重さは何gか?という問題に対して、1Lは何mLだったっけ?という疑問が生じます。
|
| 106 |
+
|
| 107 |
+
[CHUNK]
|
| 108 |
+
## 6年生: 体積と重さの関係理解と練習問題
|
| 109 |
+
1L500mLの重さは何gか?という問題に対して、1Lは何mLだったっけ?という疑問が生じます。1cm³と1mLは同じ量です。そして1Lは1000mL (1000cm³)です。だから1L500mLは1500 cm³ (mL) となることを確認します。体積と重さの変換を、くり返し質問することが重要です。かさから体積、重さから体積、体積から重さと何度もくり返し質問して、答えてもらうようにしましょう。1Lのペットボトルに水を入れて、量と重さを確認するのもよいでしょう。水1cm³ = 1g、1000cm³ = 1000g、1L = 1kgという関係を理解させます。「重さの単位は水を基準に決められている」と伝えると、多くの子どもが「へ~そうなんだ!」とおどろきます。そんなおどろきが、忘れない記憶のもとになります。練習問題として、次の量の水の重さは何gですか。980mL、2L400mL、1200 cm³、1L3dL、2.5Lなどが出題できます。
|
| 110 |
+
|
| 111 |
+
[CHUNK]
|
| 112 |
+
## 図形の性質: 分度器と角度の理解
|
| 113 |
+
図形の性質の理解におけるつまずきを解消します。分度器の性質、使い方、「1周の角=360°」を理解することが重要です。まずは「かいてみて確かめる」ことを習慣にしましょう。例として、正方形の対称の軸はいくつありますか?という問題を通して、図形を実際に描いてみることで理解を深めます。対称の軸とは、1つの図形を2つの同じ形に分ける線のことですが、まずは「かいてみてごらん」という声かけを。そしてさらに声かけで正しい考え方、気づきに導いてあげましょう。
|
| 114 |
+
|
| 115 |
+
[CHUNK]
|
| 116 |
+
## 図形の性質: 角度の測定と練習問題
|
| 117 |
+
正方形の対称の軸を求める問題を通して、図形を実際に描いてみることで理解を深めます。「ちょっとかいてみてごらん」といった声かけをすることで、子どもたちは自ら考え、試行錯誤することができます。正五角形・正六角形の対称の軸を全部かいてみましょう。角度の問題では、「平行線→同じ大きさの角ができる」ということをおさえておきましょう。また、拡大図・縮図では、「1つの点(線)を固定してかく」ことが、作図のポイントです。角度の大きさの概算を問う問題を通して、角度の感覚を養います。
|
| 118 |
+
|
| 119 |
+
[CHUNK]
|
| 120 |
+
## 4年生: 直角より大きな角の理解
|
| 121 |
+
4年生でつまずきやすい、直角より大きな角(鈍角)の理解について解説します。例として、鈍角の角度を求める、水���より大きな角度を求める問題を取り上げます。「角」というのは、とがっているもの、開き方がせまいものだけではありません。90°(直角)や180°(水平)をこえる角もあることがつかめるようにしましょう。90°をこえる角を「角」と考えられない場合、鈍角(90°より大きく開いた角)を角として考えられず、どう測ったらよいかがわからないことがあります。
|
| 122 |
+
|
| 123 |
+
[CHUNK]
|
| 124 |
+
## 4年生: 鈍角の測定と指導のポイント
|
| 125 |
+
90°をこえる角を「角」と考えられない場合、鈍角(90°より大きく開いた角)を角として考えられず、どう測ったらよいかがわからないことがあります。「とがっていなくても角だよ」と声かけをすることが重要です。とがっているものだけが角ではないことを伝えます。分度器をあてて、90°より大きい角も分度器で測れること (180°まで測れること)を、確認しましょう。180°より大きい角が測れない場合、「1周360°」 という感覚が身についていないと、大きさの測り方がわかりません。分度器よりも大きな角の測り方がわからない場合は、「ぐるんと1周で360°なんだよ」と声かけをしてあげましょう。小さいほうの角を測るということに自分で気づけるとよいですね。練習問題として、様々な角度の角の大きさを分度器で測る問題に取り組みましょう。
|
| 126 |
+
|
| 127 |
+
# 図形問題最適化ナレッジベース
|
| 128 |
+
|
| 129 |
+
[CHUNK]
|
| 130 |
+
## 角度計算の基本と交差する直線
|
| 131 |
+
角度計算の基本は、交わる直線や平行線における角の性質理解にある。一つの角の大きさがわかれば、分度器を使わずに他の角も計算可能。隣り合う角や対応する角の関係性を把握することが重要。見た目だけで判断せず、理屈に基づいた考え方を養う。角度を測る際は分度器を使用し、54°の角と隣接する角の合計が180°になることを確認する。
|
| 132 |
+
|
| 133 |
+
[CHUNK]
|
| 134 |
+
## 交差する直線における角度の求め方
|
| 135 |
+
54°の角と隣接する角の合計が180°になることを確認する。180°から54°を引いて隣接する角が126°であることを導く。対頂角は等しいという性質を利用し、向かい合う角も54°であることを確認する。平行線においては、他の直線と同じ角度で交わるため、離れた場所でも同じ大きさの角を見つけられる。
|
| 136 |
+
|
| 137 |
+
[CHUNK]
|
| 138 |
+
## 平行線と角度の関係
|
| 139 |
+
平行線においては、他の直線と同じ角度で交わるため、離れた場所でも同じ大きさの角を見つけられる。角度を求める際は、既知の角から順に全ての角を求めることで混乱を防ぐ。分度器を用いて実際に角度を測り、合わせて180°になる角を確認する。平行線が他の直線と交わる際にできる角の関係性を理解する。
|
| 140 |
+
|
| 141 |
+
[CHUNK]
|
| 142 |
+
## 平行線と角度の関係の応用
|
| 143 |
+
平行線が他の直線と交わる際にできる角の関係性を理解することで、未知の角を求める。例えば、110°の角がある場合、その隣の角は180° - 110° = 70°となる。平行線の性質から、対応する角も70°となるため、さらに隣の角は180° - 70° = 110°と計算できる。
|
| 144 |
+
|
| 145 |
+
[CHUNK]
|
| 146 |
+
## 平行と垂直な直線の作図
|
| 147 |
+
平行な直線や垂直な直線を作図する際、三角定規を適切に使用することが重要。特に、斜めになっている直線に対して作図を行う場合、三角定規の使い方が理解度を左右する。目分量で線を引くのではなく、三角定規を用いて正確に作図する習慣を身につける。平行線や垂直線を作図する際は、2つの三角定規を使用する。
|
| 148 |
+
|
| 149 |
+
[CHUNK]
|
| 150 |
+
## 三角定規を用いた平行線作図
|
| 151 |
+
平行線を作図する際は、三角定規の直角部分を基準となる直線に合わせ、もう一枚の三角定規を添える。その後、基準となる三角定規を固定したまま、もう一枚の三角定規をスライドさせることで平行線を作図する。三角定規の直角部分を上手に活用することで、平行な直線を簡単に引けることを体験させる。
|
| 152 |
+
|
| 153 |
+
[CHUNK]
|
| 154 |
+
## 三角定規を用いた垂直線作図
|
| 155 |
+
三角定規の直角部分を基準となる直線に合わせ、もう一枚の三角定規を直角に添えることで垂直線を作図する。三角定規を繰り返し練習することで、作図スキルを向上させる。平行線と垂直線の見分け方では、「見た目」だけで判断せず、三角定規を用いて実際に角度を測り、垂直であるかを確認する。
|
| 156 |
+
|
| 157 |
+
[CHUNK]
|
| 158 |
+
## 平行・垂直な直線の見分け方
|
| 159 |
+
平行・垂直な直線の見分け方では、「見た目」だけで判断せず、三角定規を用いて実���に角度を測り、垂直であるかを確認する。方眼を利用して直線の傾きを確認し、平行であるかを判断する。例えば、直線が右に1マス、下に2マス傾いている場合、別の直線も同じ傾きであれば平行である。
|
| 160 |
+
|
| 161 |
+
[CHUNK]
|
| 162 |
+
## 方眼を用いた平行の確認
|
| 163 |
+
直線が右に1マス、下に2マス傾いている場合、別の直線も同じ傾きであれば平行である。平行な直線を見つける際は、方眼のマス目を数えて傾きを比較する。垂直な直線を見つける際は、三角定規を当てて直角になっているか確認する。
|
| 164 |
+
|
| 165 |
+
[CHUNK]
|
| 166 |
+
## 拡大図・縮図の基本
|
| 167 |
+
拡大図・縮図は、元の図形と同じ形で、すべての辺の長さを一定の割合で拡大または縮小した図形。作図の際には、まず一つの頂点を固定し、そこから各頂点への距離を拡大または縮小する。拡大図や縮図を作成する際、どの頂点を固定するかを最初に決めることが重要。
|
| 168 |
+
|
| 169 |
+
[CHUNK]
|
| 170 |
+
## 拡大図・縮図の作図手順
|
| 171 |
+
拡大図や縮図を作成する際、どの頂点を固定するかを最初に決めることが重要。左下の頂点を固定し、他の頂点までの距離を測り、指定された倍率で拡大または縮小する。コンパスを用いて各頂点の位置を決定し、最後に各頂点を線で結ぶ。拡大図や縮図は、元の図形と形が同じであることが重要。
|
| 172 |
+
|
| 173 |
+
[CHUNK]
|
| 174 |
+
## 拡大図・縮図における辺と角の関係
|
| 175 |
+
拡大図・縮図では、対応する角の大きさは常に等しい。辺の長さは拡大率または縮小率に応じて変化するが、図形の形状は変わらない。拡大図・縮図の問題を解く際には、元の図形と拡大・縮小された図形が「全く同じ形」であることを意識する。
|
| 176 |
+
|
| 177 |
+
[CHUNK]
|
| 178 |
+
## 拡大図・縮図の応用問題
|
| 179 |
+
拡大図・縮図の問題では、辺の長さだけでなく、角の大きさも問われる。例えば、三角形ABCの縮図である三角形DEFにおいて、辺DEの長さが与えられた場合、対応する辺ABの長さを求める。角Eの大きさは、対応する角Bの大きさと等しくなる。
|
| 180 |
+
|
| 181 |
+
# 小学校6年間の図形の教え方
|
| 182 |
+
|
| 183 |
+
[CHUNK]
|
| 184 |
+
## 線対称・点対称な図形の理解と作図のポイント
|
| 185 |
+
小学校6年生で学ぶ線対称・点対称な図形について、特に点対称な図形が苦手な児童が多い。点対称とは何かを丁寧に説明し、理解を深める必要がある。線対称な図形は比較的容易に作図できる。点対称な図形を作図する際には、対応する点を正確にとらえることが重要となる。線対称では、対称の軸に対して同じ長さになっているかを意識することが重要。
|
| 186 |
+
|
| 187 |
+
[CHUNK]
|
| 188 |
+
## 点対称図形の作図: 対応点の特定と線での連結
|
| 189 |
+
線対称な図形は正しくかける子が多いのですが、点対称な図形になると、かけない子がグッと増えます。点対称がどういうことかをしっかり説明してあげましょう。点対称図形を作図する際は、対応する点を一つ一つ丁寧に見つけ、線で結ぶことが重要。各点に記号をふり、質問を重ねることで、対応点を見つけやすくする。すべての点を見つけたら、順番に線でつなぎ、図形を完成させる。
|
| 190 |
+
|
| 191 |
+
[CHUNK]
|
| 192 |
+
## 点対称図形の作図における注意点とアドバイス
|
| 193 |
+
点対称図形をかくのが少し手ごわいですが、「対称の中心までの長さが等しくなる点」を先にかいて、それを結ぶようアドバイスしてあげましょう。点対称では、1つ1つていねいに点をとらないと、線対称と混在してしまうところがあります。線対称みたいに、かんたんにできればいいんだけど……。落ち着いて、まず点をとってみようね。鏡に映したみたいになればいいのかな……? その通り! 長さをよく確認しながらかこうね。
|
| 194 |
+
|
| 195 |
+
[CHUNK]
|
| 196 |
+
## 正多角形と対称性の理解: 線対称と点対称の識別
|
| 197 |
+
正多角形と対称性について学習する。正三角形の対称軸の数や、点対称となる多角形を見つける問題に取り組む。形を見て考えることが大切だが、表にまとめた結果からわかることもある。自分で対称の軸が引けない場合は、正多角形の紙を用意して、折ってみせるとよい。正多角形はすべて線対称であり、辺・頂点の数が偶数のものは点対称でもある。
|
| 198 |
+
|
| 199 |
+
[CHUNK]
|
| 200 |
+
## 正多角形の対称軸の数え方と回転による確認
|
| 201 |
+
正多角形の線対称の「対称の軸」の本数がわからない。辺・頂点の数が多い図形になると混乱するので、まずは正三角形や正方形で、見て確かめることから始めましょう。正三角形は線対称な図形だね。でも対称の���は何本だろう……? 1本はわかるけど……。ほかにもありそうだね。図形を実際に回転させてみる。紙にかいて切り取ってもよいですし、本を持って上下をさかさまにしてもよいでしょう。目で見て確認することがポイントです。
|
| 202 |
+
|
| 203 |
+
[CHUNK]
|
| 204 |
+
## 正多角形の対称性のまとめと練習問題
|
| 205 |
+
180°まわしたらもとの図とぴったり重なるのが点対称だね! 正五角形は無理みたい。正六角形は重なるね! 辺・頂点の数と同じ数だけ対称の軸があることを確認する。正三角形(辺・頂点の数が3)、正方形(辺・頂点の数が4) を使って、対称の軸の数が辺・頂点の数と同じだけあることを確かめましょう。頂点の多い正多角形でも同じことになると納得できれば素晴らしいですね。
|
| 206 |
+
|
| 207 |
+
[CHUNK]
|
| 208 |
+
## 対称の中心と対応点の特定: 平行四辺形を例に
|
| 209 |
+
点対称図形には、必ず「対称の中心」 と 「対応する点」があります。自分で見つけられるようにしましょう。点対称図形の「対称の中心」を目分量で答える。対称の中心の見つけ方がわからず、「だいたい」で答えてしまうことがあります。このあたり? だいたいこのあたりかな……? わかりやすい「対応する点」を結ぶ。わかりやすい「対応する点」を結んで、その交点を調べることで「対称の中心」を見つけることができる。
|
| 210 |
+
|
| 211 |
+
[CHUNK]
|
| 212 |
+
## 対称の中心の見つけ方と対応点の関係性
|
| 213 |
+
「点Aはくるんと180°まわると、どこにくる?」と声かけをして「対応する点」を見つけられるよう促しましょう。どの点とどの点が対応してるかな? AとCかな? あとはBとD? そう! どの点も、対称の中心から等しい距離にあるんだよね? じゃあ、こう? その通り! 対応する点どうしを結んで交わったところが対称の中心だね! つねに対称の中心を探す。まずは対称の中心を見つけないと、対応する点は特定できません。点対称では対称の中心が重要だということを伝えましょう。
|
| 214 |
+
|
| 215 |
+
[CHUNK]
|
| 216 |
+
## 対応点の特定: 対称の中心からの距離と方向
|
| 217 |
+
180° 「くるん」とまわしたとき、どのあたりにくるかっていうことだよね。ちょうど反対側にくるね。何に対して反対なのかな? あ! 「対称の中心」だ! わかりやすく見つけられる「対応する点」から「対称の中心」を見つけることもできるし、「対称の中心」から「対応する点」を見つけることもできます。大切なのは「だいたい」で済まさないことです。
|
| 218 |
+
|
| 219 |
+
# 小学校算数_図形_RAG最適化
|
| 220 |
+
|
| 221 |
+
[CHUNK]
|
| 222 |
+
## 単位換算の基本と「単位のはしご」の概念
|
| 223 |
+
単位換算は、L(リットル)、mL(ミリリットル)、kg(キログラム)、g(グラム)など、体積や重さを別の単位に変換する際に重要。1kg = 1000gのような関係を利用。単位換算を容易にするツールとして「単位のはしご」を紹介。特に1000倍で単位が変わる体積や重さに有効。7マスの「はしご」を利用し、単位を視覚的に変換。
|
| 224 |
+
単位のはしごは、体積や重さの単位換算を容易にする視覚的ツール。1000倍で単位が変わる場合に特に有効。
|
| 225 |
+
|
| 226 |
+
[CHUNK]
|
| 227 |
+
## 「単位のはしご」を使った体積と重さの単位換算
|
| 228 |
+
「単位のはしご」は、体積(KL, L, mL)や重さ(t, kg, g)の単位換算に適用可能。例として、14850gをkgに変換するケースを提示。1kg=1000gの関係から、14.85kgと算出。はしご上で小数点位置を調整することで、単位換算を視覚的にサポート。1L=10dLのような関係も考慮し、必要に応じてdLを追記。長さ(mm, cm, m, km)や面積(mm², cm², m², a, ha, km²)でも同様の「単位のはしご」が作成可能。
|
| 229 |
+
長さや面積、水のかさと重さの単位換算にも「単位のはしご」は応用可能。
|
| 230 |
+
|
| 231 |
+
[CHUNK]
|
| 232 |
+
## 三角形と四角形の学習導入と作図の基本
|
| 233 |
+
part3では、身近な三角形と四角形を通して図形の知識を深める。三角形・四角形の性質や面積など、図形問題の基礎を習得。作図の基本として、コンパスと分度器の適切な使用法を解説。例として、定規、コンパス、分度器の中から、三角形作図に必要な道具を選択する問題。角度情報がない場合、定規とコンパスを使用。
|
| 234 |
+
三角形と四角形の学習では、図形問題の基礎を習得し、コンパスと分度器の適切な使用法を理解する。
|
| 235 |
+
|
| 236 |
+
[CHUNK]
|
| 237 |
+
## コンパスを用いた作図と四角形の性質理解
|
| 238 |
+
コンパスは、長さの取得に利用。10cmの辺を描いた後、コンパスで8cmと6cmの長さを設定し、交点を結んで三角形を作図。長さが既知の場合は定規とコンパス、角度が既知の���合は定規と分度器を使用。四角形の性質理解では、特徴の少ない順(台形→平行四辺形→長方形→ひし形→正方形)に確認。
|
| 239 |
+
コンパスは長さの取得に利用し、四角形の性質は特徴の少ない順に確認することで効率的に理解する。
|
| 240 |
+
|
| 241 |
+
[CHUNK]
|
| 242 |
+
## 四角形の分類と特徴、複合図形の面積計算
|
| 243 |
+
各四角形(台形、平行四辺形、長方形、ひし形、正方形)の特徴を整理。例えば、平行四辺形は2組の辺が平行で長さが等しい。長方形は4つの角が全て直角。ひし形は対角線が垂直に交わる。複合図形の面積を求める際は、足し算だけでなく引き算も利用。全体から不要部分を引くことで計算を簡略化。
|
| 244 |
+
四角形は特徴の少ない順に分類し、複合図形の面積計算は引き算も利用することで簡略化する。
|
| 245 |
+
|
| 246 |
+
[CHUNK]
|
| 247 |
+
## 複合図形の面積計算例と三角形の合同条件
|
| 248 |
+
複合図形の面積計算例として、複雑な図形を長方形と三角形に分割し、それぞれの面積を足し合わせる方法と、長方形から不要な三角形を引く方法を比較。三角形の合同条件(2辺とその間の角、2角とその間の辺、3辺)を提示。合同条件に基づき、図形が合同であるかを判断。
|
| 249 |
+
複合図形の面積計算は、分割して足す方法と全体から引く方法を比較検討。三角形の合同条件を理解し、図形の合同判断を行う。
|
| 250 |
+
|
| 251 |
+
[CHUNK]
|
| 252 |
+
## 二等辺三角形の作図と平行四辺形の作図
|
| 253 |
+
二等辺三角形の作図では、コンパスを使用。指定された辺の長さ(例:8cm、6cm、6cm)で三角形を作図。定規のみでの作図は困難。平行四辺形の作図では、4点目を試行錯誤で決定。平行四辺形になる点を複数試し、平行四辺形の性質を理解。
|
| 254 |
+
二等辺三角形の作図にはコンパスを使用し、平行四辺形の作図は試行錯誤を通して理解を深める。
|
| 255 |
+
|
| 256 |
+
[CHUNK]
|
| 257 |
+
## 平行四辺形の作図と試行錯誤の重要性
|
| 258 |
+
平行四辺形の作図では、与えられた3点から4つ目の点を決定。試行錯誤を通じて、平行四辺形を完成させる。試行錯誤力は算数の重要な要素。平行四辺形にならない場合でも、手を動かして試すことが重要。
|
| 259 |
+
平行四辺形の作図は試行錯誤を通して理解を深め、試行錯誤力を養う。
|
| 260 |
+
|
| 261 |
+
# 図形の教え方
|
| 262 |
+
|
| 263 |
+
[CHUNK]
|
| 264 |
+
## 四角形の対角線と類推
|
| 265 |
+
四角形の対角線から四角形を類推する問題。対角線の端点を結び四角形を完成させる。微妙な図形は四角形の性質から考える。対角線から四角形をイメージできない場合は、与えられた対角線の端を結んでみる。
|
| 266 |
+
|
| 267 |
+
[CHUNK]
|
| 268 |
+
## 対角線の特徴と四角形の関係
|
| 269 |
+
与えられた対角線の端を結び四角形を完成させる。四角形とその対角線をかいて確認する。対角線とあるから、それぞれの端を結ぶ。四角形は対角線から読み取れる。各四角形の特徴を整理し復習する。向きのイメージで形を覚えている場合、向きだけでなく対角線の特徴を考える。
|
| 270 |
+
|
| 271 |
+
[CHUNK]
|
| 272 |
+
## 四角形の対角線と特徴の整理
|
| 273 |
+
向きだけでなく対角線の特徴を考える。次の図のような対角線をもつ四角形はどんな四角形か考える。四角形の特徴を表にまとめる問題。実際に四角形をかいてみて、辺の長さ、角の大きさ、対角線の長さや交わり方などを思い出してみることが大切。
|
| 274 |
+
|
| 275 |
+
[CHUNK]
|
| 276 |
+
## 四角形の特徴の整理と図形
|
| 277 |
+
それぞれの四角形について、特徴がわかりやすい図がないと、うまくかけない。教科書や参考書の図を参考に、いろいろな四角形の図を子どもにかかせ、特徴を言い合ってみる。四角形の名前、特徴をまとめる。2本の対角線が垂直に交わるか、長さが等しいか、まん中で交わるか、角がすべて直角か、向かい合った辺が平行か、辺の長さがすべて等しいかなどを確認する。
|
| 278 |
+
|
| 279 |
+
[CHUNK]
|
| 280 |
+
## 四角形の特徴と図形の分類
|
| 281 |
+
四角形の特徴をまとめる。図のア〜エにあてはまる四角形を考える。四角形、1組の辺が平行、2組の辺が平行、角がすべて直角、4本の辺の長さが等しいなどの条件から図形を特定する。
|
| 282 |
+
|
| 283 |
+
[CHUNK]
|
| 284 |
+
## 長方形の面積を求める
|
| 285 |
+
L字形やコの字形の図形の面積を求める問題。「切り分けて求める」発想に加え、「いらないところを引く」発想も重要。大きな四角形からいらない部分を引く発想ができない場合、「引き算」に抵抗があると「切り分けて求める」となりがち。問題によっては手間が多くまちがいやすくなる。
|
| 286 |
+
|
| 287 |
+
[CHUNK]
|
| 288 |
+
## 面積計算の工夫
|
| 289 |
+
手間が多くまちがいやすくなる場合がある。計算し���すい形に変える。「切り分けて求める」ができたら、「いらないものを引く」という発想を促す。上と下に切り分けたり、右と左で考えたり、他の方法があるか検討する。
|
| 290 |
+
|
| 291 |
+
[CHUNK]
|
| 292 |
+
## 図形の面積計算
|
| 293 |
+
確かに「切り分けて足し算」でも正解が出せる問題は多いが、「いらないものを引く」という考え方は、ほかの問題でもよく使うので知っておくとよい。次の図の面積を求める。
|
| 294 |
+
|
| 295 |
+
[CHUNK]
|
| 296 |
+
## 三角形・平行四辺形の高さ
|
| 297 |
+
鈍角になると高さがわからなくなる場合、「底辺と同じ高さのところから、いちばん高いところまで」と考えるようにする。方眼上でどの部分を数えれば高さになるのかわかっていない場合は、平行四辺形の面積の公式は、底辺×高さ。底辺が3cmだとすると、ななめのところが高さかな?と考える。
|
| 298 |
+
|
| 299 |
+
[CHUNK]
|
| 300 |
+
## 図形の高さの考え方
|
| 301 |
+
底辺と同じ高さのところから、いちばん高いところまでが「高さ」と伝える。図形単体で考えるのではなく、底辺を延長する考え方が必要。声かけをして、気づかせてあげる。底辺と同じ高さのところ」つまり底辺の延長線上から頂点までの垂直な線が高さ。
|
| 302 |
+
|
| 303 |
+
[CHUNK]
|
| 304 |
+
## 高さの確認と面積計算
|
| 305 |
+
底辺と同じ高さのところから、いちばんてっぺんまでが「高さ」と確認する。次の図の面積を求める。底辺は4cmで高さも4cm?底辺の年と辺の交差するところを数えている場合、高さは底辺のところから垂直にいちばん高いところまでと教える。
|
| 306 |
+
|
| 307 |
+
[CHUNK]
|
| 308 |
+
## 面積から三角形の高さや辺の長さを求める
|
| 309 |
+
「底辺×高さ→面積」の1方向だけでなく、面積の公式を逆に計算して求める練習を、くり返し行う。公式は「あてはめて答えを出すもの」と思っていて、「わかっていることを書き込んで、未知数を求める」ということができない場合がある。
|
| 310 |
+
|
| 311 |
+
[CHUNK]
|
| 312 |
+
## 面積からの逆算
|
| 313 |
+
面積(答え)はわかっているんだよね?」と声かけをする。面積の公式は「面積を求めるときにだけ使うもの」と思っている子どもは多い。声かけをし、答えを書き込んだ公式を書いてみてあげる。三角形の面積を求める公式は底辺×高さ÷2=面積と教える。
|
| 314 |
+
|
| 315 |
+
[CHUNK]
|
| 316 |
+
## 公式の逆算と面積計算
|
| 317 |
+
小学生にとって、式は「=」の右の答えを出すためのもの」という認識。答えがわかっている式の逆算を一緒に練習して、考え方をマスターさせる。次の図の□にあてはまる数を求める。
|
| 318 |
+
|
| 319 |
+
# 小学校算数図形問題集
|
| 320 |
+
|
| 321 |
+
[CHUNK]
|
| 322 |
+
## 複合図形の面積:道の面積の求め方(四角形内)
|
| 323 |
+
四角形の中の道の面積を求める問題。道の部分の面積は、縦横の重なりがあるため、工夫が必要。糸口を見つけるには、図を移動させるなど、工夫して面積を楽に求める発想が重要。
|
| 324 |
+
道の端への移動による等積変形を利用し、全体から道以外の部分を引く方法が有効。例: 全体(8m×15m)から道以外の部分(6m×13m)を引く。
|
| 325 |
+
冗長な説明を削除し、簡潔な表現に修正。
|
| 326 |
+
|
| 327 |
+
[CHUNK]
|
| 328 |
+
## 複合図形の面積:平行四辺形を含む道の面積
|
| 329 |
+
前のチャンクから50トークンの重複:道の端への移動による等積変形を利用し、全体から道以外の部分を引く方法が有効。例: 全体(8m×15m)から道以外の部分(6m×13m)を引く。
|
| 330 |
+
平行四辺形と長方形が混ざった図形の面積問題。底辺と高さが同じ長方形と平行四辺形は面積が同じであることに着目。長方形の「たて」は平行四辺形の「高さ」にあたる。
|
| 331 |
+
図形を自在に動かすことで、問題を簡略化。底辺と高さを変えなければ面積は変わらない。
|
| 332 |
+
|
| 333 |
+
[CHUNK]
|
| 334 |
+
## 合同な図形:合同の定義と見つけ方
|
| 335 |
+
まったく同じ形・大きさの図形を「合同」と定義。ピッタリと重なるものが合同。裏返しの場合も含む。
|
| 336 |
+
「だいたいの形」が似ているだけで合同と判断しないよう注意。合同=同じ形という認識では不十分。
|
| 337 |
+
辺の長さや角度を書き込むことで、より正確に判断可能。2つの辺とその間の角が同じなら、同じ三角形。直角三角形がわかりやすい。
|
| 338 |
+
|
| 339 |
+
[CHUNK]
|
| 340 |
+
## 合同な図形:合同な図形の見つけ方(続き)
|
| 341 |
+
前のチャンクから50トークンの重複:辺の長さや角度を書き込むことで、より正確に判断可能。2つの辺とその間の角が同じなら、同じ三角形。直角三角形がわかりやすい。
|
| 342 |
+
方眼を活用し、長さを書き込むことで、より正確に合同な図形を見つける。
|
| 343 |
+
「だいたい」で判断せず、具体的な数値で確認することが重要。
|
| 344 |
+
|
| 345 |
+
[CHUNK]
|
| 346 |
+
## 合同な三角形の作図:作図条件の理解
|
| 347 |
+
合同な三角形を作図するには、三角形の合同条件を覚えることが不可欠。条件によって使用する道具が異なる。
|
| 348 |
+
三角形の合同条件が不明確だと、どの道具をどう使えば良いか判断できない。
|
| 349 |
+
三角形の合同条件(3辺相等、2辺夾角相等、2角夾辺相等)を理解させることが重要。
|
| 350 |
+
|
| 351 |
+
[CHUNK]
|
| 352 |
+
## 合同な三角形の作図:作図手順と道具の選択
|
| 353 |
+
前のチャンクから50トークンの重複:三角形の合同条件(3辺相等、2辺夾角相等、2角夾辺相等)を理解させることが重要。
|
| 354 |
+
合同条件に応じて、定規、コンパス、分度器を適切に使い分ける。角度がわかっている場合は分度器、長さのみの場合はコンパスを使用。
|
| 355 |
+
作図手順を具体的に解説。例:底辺を引く→分度器で角度を測る→定規で線を引く。
|
| 356 |
+
|
| 357 |
+
[CHUNK]
|
| 358 |
+
## 三角形の角度:重なりから角度を求める方針
|
| 359 |
+
三角形の重なりから角度を求める問題。不要な角まで求めてしまう誤りを防ぐため、解法の方針を定めることが重要。
|
| 360 |
+
三角形の内角の和や外角の性質など、どの知識を使うか事前に考える。
|
| 361 |
+
「わかる角度」を書き込むだけでなく、必要な角度を絞り込む。
|
| 362 |
+
|
| 363 |
+
[CHUNK]
|
| 364 |
+
## 三角形の角度:解法と知識の活用
|
| 365 |
+
前のチャンクから50トークンの重複:「わかる角度」を書き込むだけでなく、必要な角度を絞り込む。
|
| 366 |
+
必要な角度を特定し、三角形の内角の和(180°)や外角の定理を活用。
|
| 367 |
+
例:三角形DECの内角から角イを求め、角イから角アを求める。
|
| 368 |
+
|
| 369 |
+
# 小学校6年間の図形の教え方
|
| 370 |
+
|
| 371 |
+
[CHUNK]
|
| 372 |
+
## ワークの答え (p.51-75): 図形問題の解答と解説
|
| 373 |
+
|
| 374 |
+
p.51: 中点と垂直に関する問題。図形の中点を求め、垂直な線を見つける。
|
| 375 |
+
p.53: アとエの図形が該当する問題。図形の形状を識別する。
|
| 376 |
+
p.55: 図形の辺の長さを測定し、指定された図形を作成する。図形の辺の長さと形状を理解する。
|
| 377 |
+
p.57: 図形内の指定された位置に頂点を配置し、線で接続する。頂点の位置と線の接続方法を理解する。
|
| 378 |
+
p.59: 平行四辺形、長方形、正方形を識別する。四角形の特性を理解する。
|
| 379 |
+
p.61: 台形、平行四辺形、長方形、ひし形を識別する。四角形の特性を理解する。
|
| 380 |
+
p.63: 図形の面積を計算する。面積の計算方法を適用する。
|
| 381 |
+
p.65: 図形の面積を計算する。単位換算も含む。面積の計算と単位換算を理解する。
|
| 382 |
+
|
| 383 |
+
[CHUNK]
|
| 384 |
+
## 図形の面積計算と形状識別 (p.67-75): 解答と解説 (重複50トークン)
|
| 385 |
+
p.67: 図形の辺の長さを計算する。図形の辺の長さの計算方法を理解する。
|
| 386 |
+
p.69: 図形の面積を計算する。単位換算も含む。面積の計算と単位換算を理解する。
|
| 387 |
+
p.71: アとエ、イとオ、カとクの図形を識別する。図形の形状を識別する。
|
| 388 |
+
p.73: 指定された条件で図形を作成する。分度器と定規の使用方法を理解する。
|
| 389 |
+
p.75: 角度を計算する。角度の計算方法を理解する。
|
| 390 |
+
p.76: 指定された条件で図形を作成する。図形の作成方法を理解する。
|
| 391 |
+
p.77: 図形の面積を計算する。複雑な図形の面積計算方法を理解する。
|
| 392 |
+
|
| 393 |
+
[CHUNK]
|
| 394 |
+
## 三角形・四角形の復習と角の合計 (p.76-80): 図形問題の解答と解説 (重複50トークン)
|
| 395 |
+
p.76: 図形の頂点を指定された位置に配置し、線で接続する。これにより、様々な四角形を作成する。
|
| 396 |
+
p.77: 複雑な図形の面積を計算する。補助線を利用して、図形を分割し、面積を計算する。角度を計算する問題も含む。
|
| 397 |
+
p.78-79: 図形問題の解答。様々な図形の特性を理解し、問題を解決する。
|
| 398 |
+
p.80: コラム: ☆形の角の合計は常に一定であることを解説。三角形の外角の性質を利用して証明する。
|
| 399 |
+
|
| 400 |
+
[CHUNK]
|
| 401 |
+
## 円の基本要素と円周の理解 (p.82-84): 円の定義と円周率 (重複50トークン)
|
| 402 |
+
p.82: 円の基本要素(半径、直径、中心、円周、面積)を定義する。円の各部分の関係性を理解する。
|
| 403 |
+
p.83: 円を8つ折りにする方法とコンパスを使った円の描き方を説明する。円の作成方法を理解する。
|
| 404 |
+
p.84: 円周の長さを実際に測って体感する方法を説明する。円周率(約3.14)を導入する。円周と直径の関係を理解する。
|
| 405 |
+
|
| 406 |
+
[CHUNK]
|
| 407 |
+
## 円の面積とコンパスの使い方 (p.84-87): 円の面積計算と作図 (重複50トークン)
|
| 408 |
+
p.84: 円の面積を求めるために、円を細かく分割して四角形に近似する方法を説明する。円の面積の公式(半径×半径×3.14)を導出する。
|
| 409 |
+
p.85: 円周の長さと直線の長さを比較する��題。円周の概念を理解する。
|
| 410 |
+
p.86: コンパスを使って円をかく方法をステップごとに説明する。コンパスの選び方や準備についても解説する。
|
| 411 |
+
p.87: コンパスを使って実際に円をかく練習問題。指定された点を中心に、指定された半径の円をかく。
|
| 412 |
+
|
| 413 |
+
# 小学校算数図形問題 RAG最適化ナレッジベース
|
| 414 |
+
|
| 415 |
+
[CHUNK]
|
| 416 |
+
## 3年生: 円と四角形 - 半径から長方形の辺を求める
|
| 417 |
+
半径3cmの円が長方形に内接している時、長方形の辺長を求める問題。円の半径と長方形の辺の関係理解が重要。半径は円の中心からどの方向へ引いても同じ長さである点を理解させる。半径が斜めに示されている場合に、縦横の辺との関係を見つけ出すのが難しい。
|
| 418 |
+
|
| 419 |
+
[CHUNK]
|
| 420 |
+
## 3年生: 円と四角形 - 直径と補助線による辺長特定
|
| 421 |
+
半径3cmの円が内接する長方形の辺長を求める。円の直径が長方形の辺と一致することに気づかせる。補助線として、円の中心を通る縦横の線を引き、直径を視覚的に捉えやすくする。直径は半径の2倍であるため、半径から直径を計算する。
|
| 422 |
+
|
| 423 |
+
[CHUNK]
|
| 424 |
+
## 3年生: 円と四角形 - 具体例と演習問題
|
| 425 |
+
半径3cmの円が縦に2つ、横に3つ並んで長方形に内接している場合、縦の長さは直径6cmの2倍で12cm、横の長さは直径の3倍で18cmとなる。半径4cmの円が4つ内接する長方形の縦の長さ、指定された点間の距離を求める演習問題。図形問題は手を動かし、試行錯誤を通じて解法を発見させることが重要。
|
| 426 |
+
|
| 427 |
+
[CHUNK]
|
| 428 |
+
## 5年生: 円と三角形 - 半径を用いた正三角形の作図
|
| 429 |
+
半径2cmの円を利用し、一辺2cmの正三角形を作図する問題。円の半径はどこも同じ長さであるという性質を活用。コンパスを用いて円周上に点を取ることで正三角形を作図できることを体感的に理解する。定規のみで正三角形を作図しようとする誤りを防ぐため、コンパスの使用に慣れさせる。
|
| 430 |
+
|
| 431 |
+
[CHUNK]
|
| 432 |
+
## 5年生: 円と三角形 - コンパスを用いた花びら模様の作図
|
| 433 |
+
半径2cmの円を利用した正三角形作図。円周上にコンパスの針を置き、同じ半径で円を描くことで花びらのような模様を作る。模様の各交点が正三角形の頂点となることを理解する。円とコンパスの性質理解を促し、今後の図形学習への応用力を養う。
|
| 434 |
+
|
| 435 |
+
[CHUNK]
|
| 436 |
+
## 5年生: 円と三角形 - 花びら模様と正三角形の関係
|
| 437 |
+
花びら模様の中心の円に着目すると、内部に6つの正三角形が存在する。円周上の点から半径2cmで半円を描き、元の円との交点を結ぶことで正三角形を作図できる。円とコンパスを用いた作図を通して、図形の性質を視覚的に理解させることが重要。二等辺三角形を作図する演習問題。
|
| 438 |
+
|
| 439 |
+
[CHUNK]
|
| 440 |
+
## 5年生: 円周 - 円周から直径を求める
|
| 441 |
+
円周の長さが30cmの円の半径を概数で求める問題。円周率(3.14)を用いて円周から直径を求める計算。割り算よりも掛け算の関係で理解を深める。「直径×3.14=円周」という関係式を理解させ、既知の情報から未知の情報を導き出す思考力を養う。
|
| 442 |
+
|
| 443 |
+
[CHUNK]
|
| 444 |
+
## 5年生: 円周 - 関係式と質問による理解促進
|
| 445 |
+
円周から直径を求める問題。「直径×3.14=円周」の関係式に既知の円周の長さを代入し、直径を求める式を導き出す。質問形式で子供の知識を引き出し、能動的な問題解決を促す。直径を求めるために割り算が必要となることを理解させる。
|
| 446 |
+
|
| 447 |
+
[CHUNK]
|
| 448 |
+
## 5年生: 円周 - 概数計算と演習問題
|
| 449 |
+
円周から求めた直径を概数で表す。10分の1の位までの概数を求めるには、100分の1の位まで計算し四捨五入する。円周の長さが37.68cmの円の直径を求める演習問題。計算スキルだけでなく、問題文の意味を理解し、適切な公式を選択する能力も重要。
|
| 450 |
+
|
| 451 |
+
[CHUNK]
|
| 452 |
+
## 5年生: おうぎ形のまわりの長さ - 基本概念
|
| 453 |
+
中心角が90°や180°のおうぎ形の周りの長さを求める問題。おうぎ形の弧の長さと半径(または直径)を足し合わせる必要がある。90°は円周の1/4、180°は円周の1/2であるという理解が前提。周りの長さの意味を理解していないと、弧の長さのみを計算して終わってしまう。
|
| 454 |
+
|
| 455 |
+
[CHUNK]
|
| 456 |
+
## 5年生: おうぎ形のまわりの長さ - 計算手順と視覚化
|
| 457 |
+
おうぎ形の周りの長さを求める際、計算の順序を意識させることが重要。弧の長さを計算しただけで終わらないように、計算を始める前に全体の流れを確認する。弧の部分を波線、直径や半径を直線でなぞり、視覚的に区別することで、足し��れを防ぐ。
|
| 458 |
+
|
| 459 |
+
[CHUNK]
|
| 460 |
+
## 5年生: おうぎ形のまわりの長さ - 計算例と演習問題
|
| 461 |
+
中心角90°のおうぎ形の場合、弧の長さは (直径)×3.14÷4 で求められる。求めた弧の長さに半径2つ分の長さを足すことで、周りの長さを算出する。中心角60°のおうぎ形の周りの長さを求める演習問題。図形問題は、視覚的な補助と丁寧な計算が重要。
|
| 462 |
+
|
| 463 |
+
[CHUNK]
|
| 464 |
+
## 5年生: 半円組み合わせ図形の周りの長さ - 全体像把握の重要性
|
| 465 |
+
複数の半円を組み合わせた図形の周りの長さを求める問題。一度に計算しようとせず、個々の半円に分割して考える。各半円の直径と半径を正確に把握し、それぞれの弧の長さを計算する。複雑な図形を単純化し、一つずつ解決していく能力が求められる。
|
| 466 |
+
|
| 467 |
+
[CHUNK]
|
| 468 |
+
## 5年生: 半円組み合わせ図形の周りの長さ - 式の可視化と計算
|
| 469 |
+
個々の半円の周りの長さを式で表現する。各半円の直径を基に、円周の半分の長さを計算する。計算過程を可視化することで、誤りを減らす。各半円の式を縦に揃えて書き、同じ種類の数(3.14など)をまとめて計算する。
|
| 470 |
+
|
| 471 |
+
[CHUNK]
|
| 472 |
+
## 5年生: 半円組み合わせ図形の周りの長さ - 計算効率化と分配法則
|
| 473 |
+
各半円の周の長さを足し合わせる。3.14を各項に分配し、計算を効率化する。分配法則を理解することで、より複雑な計算もスムーズに行えるようになる。複数の半円を組み合わせた図形の周りの長さを求める演習問題。
|
| 474 |
+
|
| 475 |
+
# 図形の教え方
|
| 476 |
+
|
| 477 |
+
[CHUNK]
|
| 478 |
+
## 半円・おうぎ形組み合わせ問題:基本と計算ミス対策
|
| 479 |
+
小学校6年生で学習する半円やおうぎ形を組み合わせた図形の面積問題について解説。計算ミスを防ぐために、式をまとめて3.14のかけ算を1回にすること、3.14にかける数を小さくすることの重要性を説明。分配法則の逆利用で計算を効率化。
|
| 480 |
+
|
| 481 |
+
[CHUNK]
|
| 482 |
+
## 分配法則の逆利用と計算例:半円・おうぎ形組み合わせ問題
|
| 483 |
+
分配法則の逆利用(例:2×10+3×10=(2+3)×10=50円)を解説。複雑な図形の面積計算例として、12×12×3.14÷2 - 8×8×3.14÷2 + 4×4×3.14÷2 = (72-32+8)×3.14 = 48×3.14 = 150.72cm² を提示。式をまとめることで計算が容易になることを示す。
|
| 484 |
+
|
| 485 |
+
[CHUNK]
|
| 486 |
+
## 図形問題応用:半径の異なる円の面積の和
|
| 487 |
+
半径6cmの円と8cmの円の面積の和が、半径何cmの円の面積と等しいかを問う問題。6×6×3.14 + 8×8×3.14 = (36+64)×3.14 = 100×3.14 より、半径10cmの円と等しいことを解説。
|
| 488 |
+
|
| 489 |
+
[CHUNK]
|
| 490 |
+
## 正方形とおうぎ形組み合わせ問題:図形分解と線分図の活用
|
| 491 |
+
正方形とおうぎ形を組み合わせた図形の面積問題について、図を分解して考えることの重要性を解説。1辺が8cmの正方形内の特定部分の面積を求める例題を通して、線分図を使った考え方を練習する。
|
| 492 |
+
|
| 493 |
+
[CHUNK]
|
| 494 |
+
## 線分図による長さの算出:正方形とおうぎ形組み合わせ問題
|
| 495 |
+
線分図を用いて、複雑な図形内の線分の長さを求める方法を解説。AからC、DからB、CからDまでの長さをABから他の線分を引くことで算出する。例:AからCまで = AB - CB = 10 - 7 = 3cm。
|
| 496 |
+
|
| 497 |
+
[CHUNK]
|
| 498 |
+
## 線分図の応用:線分の分割と組み合わせによる問題解決
|
| 499 |
+
ABの中点をEとしたとき、CからE、EからDの長さを線分図で求める。CからDまでの長さはCE+DEで計算。わかっていることを3つの線に分け、AD + CB - AB = CD の関係からCDの長さを求める。
|
| 500 |
+
|
| 501 |
+
[CHUNK]
|
| 502 |
+
## 図形問題の解法:線分図で理解した知識の応用
|
| 503 |
+
線分図で理解した考え方を実際の図形問題に応用する。正方形、三角形、おうぎ形に図形を分割し、それぞれの面積を求める。8cmの正方形、三角形、おうぎ形を組み合わせた図形の面積を計算する例を示す。
|
| 504 |
+
|
| 505 |
+
[CHUNK]
|
| 506 |
+
## 図形面積計算:複数の解法アプローチ
|
| 507 |
+
複数の解法で同一の図形問題(正方形とおうぎ形の組み合わせ)を解く。64 - 50.24 = 13.76cm²、64 - 13.76×2 = 36.48cm²、50.24 - 32 = 18.24cm²、18.24 + 18.24 = 36.48cm²、50.24 + 50.24 - 64 = 36.48cm² など、異なるアプローチで同じ答えが得られることを示す。
|
| 508 |
+
|
| 509 |
+
[CHUNK]
|
| 510 |
+
## 図形問題演習:色を塗った部分の面積を求める
|
| 511 |
+
20cmの正方形内に配置された図形の、色を塗った部分の面積を求める演習問題。線分図で考え方を練習した後、同様の問題に応用する。
|
| 512 |
+
|
| 513 |
+
[CHUNK]
|
| 514 |
+
## 円の復習:円周と面積の計算
|
| 515 |
+
円の復習として、様々な図形の円周の長さと面積を求める問題。半径や直径が与えられた円、半円、扇形などについて、それぞれの円周の長さと面積を計算する。
|
| 516 |
+
|
| 517 |
+
[CHUNK]
|
| 518 |
+
## 図形の面積計算:複合図形
|
| 519 |
+
複合図形において、特定の部分(例:斜線部分)の面積を求める問題。正方形や扇形など、複数の図形が組み合わさった図形について、指定された部分の面積を計算する。
|
| 520 |
+
|
| 521 |
+
[CHUNK]
|
| 522 |
+
## 円の比較と応用:円周と面積の倍数計算
|
| 523 |
+
半径の異なる円(例:半径8cmの円と半径4cmの円)の円周や面積の倍数を計算する問題。文中の空欄に数字を埋める形式で、円の性質を理解する。
|
| 524 |
+
|
| 525 |
+
[CHUNK]
|
| 526 |
+
## 図形の割合:正方形と内接円
|
| 527 |
+
正方形と内接円の関係について、文中の空欄に数字を埋める形式で理解を深める。左の円の面積は、色を塗ってある正方形の面積の約何倍か、正方形の面積が80cm²のとき、中にきっちり入っている円の面積はいくらか、といった問題に取り組む。
|
| 528 |
+
|
| 529 |
+
[CHUNK]
|
| 530 |
+
## 解答:図形問題の解答
|
| 531 |
+
p.85からp.103までのワークの解答を示す。円周、面積、複合図形の面積など、様々な図形問題の解答を一覧で提示。
|
| 532 |
+
|
| 533 |
+
[CHUNK]
|
| 534 |
+
## 解答詳細:図形問題の解答と解説
|
| 535 |
+
p.104とp.105の円の復習問題の解答と解説。円周の長さ、面積、複合図形の面積などを求める計算過程を詳細に説明。図形問題の理解を深める。
|
| 536 |
+
|
| 537 |
+
[CHUNK]
|
| 538 |
+
## 解答詳細:図形問題の解答と解説(続き)
|
| 539 |
+
p.107の図形問題の解答と解説。正方形と円の複合図形における面積計算、円周の長さなどを求める過程を詳細に説明。図形問題の理解を深める。
|
| 540 |
+
|
| 541 |
+
# 小学校算数図形問題RAG最適化ナレッジベース
|
| 542 |
+
|
| 543 |
+
[CHUNK]
|
| 544 |
+
## 円の模様作りと図形観察の導入
|
| 545 |
+
コンパスを使った円の模様作りは、図形への興味を引き出す導入として効果的。同じ半径の円を重ねて描くことで、美しい模様が生まれる。模様作りを通して、図形の規則性や対称性への気づきを促す。半円を組み合わせた図形の円周を調べる活動も、図形観察の基礎となる。模様作りの手順: (1)半径2cmの円を描く。(2)コンパスの幅を変えずに円周上に針を置いて別の円を描く。(3)交点を中心にさらに円を描き、繰り返す。完成した模様から、図形的な特徴(花びらのような形)を発見させる。
|
| 546 |
+
|
| 547 |
+
[CHUNK]
|
| 548 |
+
## 図形観察と円周の関係性
|
| 549 |
+
模様作りを通して、図形の規則性や対称性への気づきを促す。半円を組み合わせた図形の円周を調べる活動も、図形観察の基礎となる。半円を組み合わせた図形の円周の長さを求める問題を通して、円周率の理解を深める。図形の周りの長さを求める際、半径の異なる半円を組み合わせることで、計算の複雑さを増し、応用力を養う。最終的に、最も大きな円の円周と等しくなるという結論を導き出す。
|
| 550 |
+
|
| 551 |
+
[CHUNK]
|
| 552 |
+
## 半円組み合わせ図形の円周計算
|
| 553 |
+
図形の周りの長さを求める際、半径の異なる半円を組み合わせることで、計算の複雑さを増し、応用力を養う。最終的に、最も大きな円の円周と等しくなるという結論を導き出す。例:半径6cm, 4cm, 2cmの半円を組み合わせた図形の周りの長さは、12×3.14で計算できる。計算過程を示すことで、理解を助ける。12×3.14+2+8×3.14+2+4×3.14+2 = (6+4+2)×3.14 = 12×3.14。
|
| 554 |
+
|
| 555 |
+
[CHUNK]
|
| 556 |
+
## 立体の見取り図の描き方とポイント
|
| 557 |
+
空間認識力を養うには、見取り図の作成が不可欠。見取り図を描く際のポイントは、平行線を意識すること。方眼紙を利用して、見取り図の描き方を段階的に習得する。手順:(1)正面の長方形を描く。(2)方眼の格子の点を数えながら平行線を引く。(3)各頂点を線で結ぶ。慣れてきたらフリーハンドで描く練習をする。
|
| 558 |
+
|
| 559 |
+
[CHUNK]
|
| 560 |
+
## 見取り図の応用と斜め線の練習
|
| 561 |
+
各頂点を線で結ぶ。慣れてきたらフリーハンドで描く練習をする。斜め線の傾きを変えることで、様々な方向から見た立体を描く練習をする。見える部分は実線、見えない部分は点線で表現する。立方体の見取り図を例に、斜め線の引き方、実線と点線の使い分けを練習する。
|
| 562 |
+
|
| 563 |
+
[CHUNK]
|
| 564 |
+
## 底面と高さの関係理解
|
| 565 |
+
立体図形の体積を理解するためには、底面と高さの関係を理解することが重要。底面と高さは見る方向によって変わることを、具体例を通して理解する。ティッシュペーパーの箱など身近な物を使って、底面を変えた場合に高さがどう変わるかを確認する。牛乳パックを例に、体積の目安を把握することも有効。
|
| 566 |
+
|
| 567 |
+
[CHUNK]
|
| 568 |
+
## 体積計算における工夫
|
| 569 |
+
牛乳パックを例に、体積の目安を把握することも有効。体積計算では、計算の順序��工夫することで、計算を簡単にすることができる。例:5×15÷2×20の計算を、5×15×(20÷2)とすることで、計算が容易になる。3つの数字のかけ算では、順序を工夫することで計算ミスを減らせる。
|
| 570 |
+
|
| 571 |
+
[CHUNK]
|
| 572 |
+
## 直方体の体積と辺の長さの関係
|
| 573 |
+
直方体の体積から辺の長さを求める問題を通して、体積の理解を深める。□を使った式を立てて計算する方法と、底面積から高さを求める方法を習得する。交換法則を利用して計算を簡単にする方法も有効。例:8×□×15=1080という式を、8×15×□=1080と変形することで、計算が容易になる。
|
| 574 |
+
|
| 575 |
+
[CHUNK]
|
| 576 |
+
## 高さから底面を見つける
|
| 577 |
+
交換法則を利用して計算を簡単にする方法も有効。例:8×□×15=1080という式を、8×15×□=1080と変形することで、計算が容易になる。高さが与えられた場合に、どの面が底面になるかを判断する練習をする。底面に色を塗ることで、視覚的に理解を助ける。体積がわかっている直方体で、辺の長さを求める練習問題に取り組む。
|
| 578 |
+
|
| 579 |
+
# 小学校6年間の図形の教え方
|
| 580 |
+
|
| 581 |
+
[CHUNK]
|
| 582 |
+
## 直方体の体積:複雑な立体の攻略
|
| 583 |
+
直方体を取り除いたり、はり合わせたりする問題では、どちらの考え方でも良いという安心感が重要。体積の基本は(たて)×(よこ)×(高さ)だが、(底面積)×(高さ)も理解することで、立体図形への理解が深まる。複雑な立体に対して抵抗感をなくすため、手順を一つずつ確認し、体感させることが重要。
|
| 584 |
+
|
| 585 |
+
[CHUNK]
|
| 586 |
+
## 複雑な立体の体積:計算手順と教え方
|
| 587 |
+
抵抗感をなくすため、手順を一つずつ確認し、体感させることが重要。「はり合わせ」「取りのぞき」の2つの解法を提示し、両方で解けることを伝える。図に直接書き込み、解く道筋を視覚的に示すことが効果的。例として、複雑な立体を分割し、各部分の体積を計算して合計する方法を示す。
|
| 588 |
+
|
| 589 |
+
[CHUNK]
|
| 590 |
+
## 複雑な立体の体積:計算例と底面積の活用
|
| 591 |
+
各部分の体積を計算して合計する方法を示す。例えば、底面積を分割して考え、それぞれの面積を計算し、合計することで全体の底面積を求める。体積は(底面積)×(高さ)で計算できることを再確認する。
|
| 592 |
+
|
| 593 |
+
[CHUNK]
|
| 594 |
+
## 体積と容積:違いの理解と問題解決
|
| 595 |
+
体積と容積はどちらも「かさ」を表すが、体積は「そのもの自体のかさ」、容積は「入れ物に入るもののかさ」を指す。厚さのある入れ物の場合、体積と容積の違いを理解することが重要。容積を求める際に体積を答えてしまう間違いを防ぐ必要がある。
|
| 596 |
+
|
| 597 |
+
[CHUNK]
|
| 598 |
+
## 体積と容積:計算と単位換算
|
| 599 |
+
容積を求める際に体積を答えてしまう間違いを防ぐ必要がある。体積と容積の問題では、「内のり」の寸法を図に書き込ませることが有効。板の厚さを考慮して内側の寸法を計算し、容積を求める。体積の単位はcm³、容積の単位はmL、Lなどを使用し、単位換算の理解も重要。
|
| 600 |
+
|
| 601 |
+
[CHUNK]
|
| 602 |
+
## 体積と容積:単位換算と板の体積
|
| 603 |
+
mL、Lなどを使用し、単位換算の理解も重要。1mL=1cm³であることを理解し、Lとcm³の相互換算ができるようにする。木の板の体積がどこを指すか理解することも重要。直方体全体の体積から容積を引くことで、木の板の体積を求められることを説明する。
|
| 604 |
+
|
| 605 |
+
[CHUNK]
|
| 606 |
+
## いろいろな立体の体積:底面の見つけ方
|
| 607 |
+
底面を見つけて体積を求める問題では、(底面積)×(高さ)=(体積)の公式を活用する。立体を水平にスライスした際に、どの高さでも切り口が同じになるかを理解させることが重要。図の下の部分が底面だと思い込んでしまうケースがあるので注意が必要。
|
| 608 |
+
|
| 609 |
+
[CHUNK]
|
| 610 |
+
## 立体の体積:底面と高さの特定
|
| 611 |
+
図の下の部分が底面だと思い込んでしまうケースがあるので注意が必要。つみきなどを用いて、実際に触れながら形を確認すると理解が深まる。底面となる面に色をつけさせ、その面を底面にして立体を立てて考える練習をする。
|
| 612 |
+
|
| 613 |
+
[CHUNK]
|
| 614 |
+
## 立体の体積:底面の確認と体積計算
|
| 615 |
+
立体を立てて考える練習をする。立てたときに、どの高さで水平に切っても底面積と同じ形・大きさになるかを確認する。底面と高さを特定できれば、(底面積)×(高さ)で体積を計算できる。
|
| 616 |
+
|
| 617 |
+
# 小学校6年間の図形の教え方
|
| 618 |
+
|
| 619 |
+
[CHUNK]
|
| 620 |
+
## 立体の理解:展開図とパターン認識
|
| 621 |
+
[CHUNK]識別子
|
| 622 |
+
立体把握の重要性:展開図のパターン認識、斜め立体の体積計算。身近な物を活用。
|
| 623 |
+
展開図の学習:ティ���シュ箱の展開で基本を理解。切る位置で形状変化。立方体の展開図は何種類?
|
| 624 |
+
6種類程度の予想に対し、実際は11種類存在することを示す。
|
| 625 |
+
50トークン重複:実際は11種類存在することを示す。
|
| 626 |
+
[CHUNK]
|
| 627 |
+
## 立方体の展開図の種類とパターン
|
| 628 |
+
[CHUNK]識別子
|
| 629 |
+
立方体の展開図の種類:[1-4-1]型(6種)、[1-3-2]型(3種)、[2-2-2]型(1種)、[3-3]型(1種)。
|
| 630 |
+
[1-4-1]型:1段目に1個、2段目に4個、3段目に1個。回転・反転で重複排除。
|
| 631 |
+
[1-3-2]型:1個、3個、2個の順。2段目と3段目の形状固定。
|
| 632 |
+
[2-2-2]型、[3-3]型:各1種類。
|
| 633 |
+
50トークン重複:各1種類。[2-2-2]型、[3-3]型は各1種類。
|
| 634 |
+
[CHUNK]
|
| 635 |
+
## 斜め立体の体積:10円硬貨の積み重ね
|
| 636 |
+
[CHUNK]識別子
|
| 637 |
+
斜め立体の体積:通常の体積計算との関連性。
|
| 638 |
+
問題提起:斜めになった立体の体積の求め方。
|
| 639 |
+
アプローチ:10円硬貨の積み重ねによる視覚的な理解。
|
| 640 |
+
50トークン重複:視覚的な理解。
|
| 641 |
+
[CHUNK]
|
| 642 |
+
## 円柱の体積と斜め変形
|
| 643 |
+
[CHUNK]識別子
|
| 644 |
+
円柱による体積計算:(底面積)×(高さ)で体積を求める。
|
| 645 |
+
斜め変形:積み重ねた硬貨をずらす。高さは変わらない。
|
| 646 |
+
結論:斜めになった立体の体積も(底面積)×(高さ)で計算可能。
|
| 647 |
+
50トークン重複:(底面積)×(高さ)で計算可能。
|
| 648 |
+
[CHUNK]
|
| 649 |
+
## 具体例:円柱と斜め円柱の体積計算
|
| 650 |
+
[CHUNK]識別子
|
| 651 |
+
円柱の体積計算例:底面積3×3=9cm²、高さ12cm、体積9×12=108cm³。
|
| 652 |
+
斜め円柱の体積計算例:底面積2×2×3.14=12.56cm²、高さ12cm、体積12.56×12=150.72cm³。
|
| 653 |
+
50トークン重複:体積12.56×12=150.72cm³。
|
| 654 |
+
[CHUNK]
|
| 655 |
+
## 図形問題演習:まとめワーク
|
| 656 |
+
[CHUNK]識別子
|
| 657 |
+
まとめワーク:part2~part5の復習問題。
|
| 658 |
+
図形学習の総仕上げ。
|
| 659 |
+
間違えた問題は該当単元に戻って復習。
|
| 660 |
+
50トークン重複:該当単元に戻って復習。
|
| 661 |
+
[CHUNK]
|
| 662 |
+
## 計算問題:単位換算と四則演算
|
| 663 |
+
[CHUNK]識別子
|
| 664 |
+
単位・図形の性質/三角形・四角形
|
| 665 |
+
計算問題:単位換算を含む四則演算。kg, g, m, cm, L, dL等の単位。
|
| 666 |
+
例:2kg300g+7kg800g, 3m22cm-1m85cm, 3L2dL-2L6dL, 4kg70g-2kg300g, 2m48cm+1m55cm
|
| 667 |
+
50トークン重複:2m48cm+1m55cm
|
| 668 |
+
[CHUNK]
|
| 669 |
+
## 単位換算と水の重さ
|
| 670 |
+
[CHUNK]識別子
|
| 671 |
+
単位換算:kg→g, m→cm, L→dL, L→mLの換算。
|
| 672 |
+
例:3kg20g = g, 6m5cm = cm, 3L7dL = dL, 4L20mL = mL, 3.7L = mL
|
| 673 |
+
水の重さ:2L (kg) = ( )kg, 1630mL (g) = ( )g, 850cm³ (kg) = ( )kg, 3L4dL (g) = ( )g, 1L480mL (kg·g) = ( )kg( )g
|
| 674 |
+
50トークン重複:1L480mL (kg·g) = ( )kg( )g
|
| 675 |
+
[CHUNK]
|
| 676 |
+
## 角の測定と図形の作図
|
| 677 |
+
[CHUNK]識別子
|
| 678 |
+
角の測定:分度器による角度測定。
|
| 679 |
+
図形の作図:平行線、垂直線の作図。
|
| 680 |
+
50トークン重複:平行線、垂直線の作図。
|
| 681 |
+
[CHUNK]
|
| 682 |
+
## 角の大きさの計算
|
| 683 |
+
[CHUNK]識別子
|
| 684 |
+
角度計算:平行線における角度計算。
|
| 685 |
+
例:ア、イ、ウの角度を求める。
|
| 686 |
+
50トークン重複:ア、イ、ウの角度を求める。
|
| 687 |
+
[CHUNK]
|
| 688 |
+
## 図形の対称性:線対称と点対称
|
| 689 |
+
[CHUNK]識別子
|
| 690 |
+
図形の対称性:線対称な直線、点対称な図形を見つける。
|
| 691 |
+
50トークン重複:点対称な図形を見つける。
|
| 692 |
+
[CHUNK]
|
| 693 |
+
## 図形の拡大と縮小
|
| 694 |
+
[CHUNK]識別子
|
| 695 |
+
図形の拡大・縮小:三角形の2倍の拡大図、1/2の縮図。
|
| 696 |
+
50トークン重複:三角形の2倍の拡大図、1/2の縮図。
|
| 697 |
+
[CHUNK]
|
| 698 |
+
## 対称軸と点対称性の識別
|
| 699 |
+
[CHUNK]識別子
|
| 700 |
+
図形の対称性:線対称の軸、点対称の中心を作図。
|
| 701 |
+
50トークン重複:点対称の中心を作図。
|
| 702 |
+
[CHUNK]
|
| 703 |
+
## 正多角形の対称性
|
| 704 |
+
[CHUNK]識別子
|
| 705 |
+
正多角形の対称性:正方形、正五角形、正六角形、正七角形、正八角形について、対称軸の本数と点対称性の有無を答える。
|
| 706 |
+
50トークン重複:対称軸の本数と点対称性の有無を答える。
|
| 707 |
+
[CHUNK]
|
| 708 |
+
## 平行四辺形の点対称性
|
| 709 |
+
[CHUNK]識別子
|
| 710 |
+
平行四辺形の点対称性:対称の中心を作図。点Pに対応する点を作図。
|
| 711 |
+
50トークン重複:点Pに対応する点を作図。
|
| 712 |
+
[CHUNK]
|
| 713 |
+
## 図形の面積計算
|
| 714 |
+
[CHUNK]識別子
|
| 715 |
+
図形の面積:複合図形の面積計算。長方形から一部を切り取った図形、道幅のある土地の面積。
|
| 716 |
+
50トークン重複:道幅のある土地の面積。
|
| 717 |
+
[CHUNK]
|
| 718 |
+
## 円周の長さと面積
|
| 719 |
+
[CHUNK]識別子
|
| 720 |
+
円:円周の長さと面積を求める。
|
| 721 |
+
50トークン重複:円周の長さと面積を求める。
|
| 722 |
+
[CHUNK]
|
| 723 |
+
## 複合図形の円周と面積
|
| 724 |
+
[CHUNK]識別子
|
| 725 |
+
複合図形:円の一部を含む複合図形の円周の長さと面積を求める。
|
| 726 |
+
50トークン重複:円の一部を含む複合図形の円周の長さと面積を求める。
|
| 727 |
+
|
| 728 |
+
# 小学校6年間の図形の教え方
|
| 729 |
+
|
| 730 |
+
[CHUNK]
|
| 731 |
+
## 立体の体積:台形底面の直方体
|
| 732 |
+
図のような立体の体積を求��る。台形ABCDを底面としたとき、高さは?体積は?台形ABCDを底面とすると、高さは3cm。体積は底面積x高さで求められる。底面積は台形の面積の公式(上底+下底)x高さ÷2で計算。
|
| 733 |
+
|
| 734 |
+
[CHUNK]
|
| 735 |
+
## 台形底面の直方体の体積計算
|
| 736 |
+
高さは何cmですか。体積はいくつになりますか。底面積は(5+8)x3÷2=19.5cm²。体積は19.5cm²x3cm=58.5cm³。したがって、高さは3cm、体積は58.5cm³。
|
| 737 |
+
|
| 738 |
+
[CHUNK]
|
| 739 |
+
## 立体の体積:高さが24cmの直方体
|
| 740 |
+
次の立体の体積を求める。高さをCK=24cmとすると、底面積は?体積は?高さをCKの24cmと考えると、底面の面積は?体積はいくつになりますか。
|
| 741 |
+
|
| 742 |
+
[CHUNK]
|
| 743 |
+
## 底面積と体積の計算:高さ24cmの直方体
|
| 744 |
+
底面積は(6+16)x4÷2=44cm²。体積は44cm²x24cm=1056cm³。したがって、底面積は44cm²、体積は1056cm³。
|
| 745 |
+
|
| 746 |
+
[CHUNK]
|
| 747 |
+
## 木の板で作った直方体の入れ物:容積と水面の高さ
|
| 748 |
+
厚さ1cmの木の板で直方体の入れ物を作る。容積は?42dLの水を入れた時の水面の高さは?入れ物の容積は何cmですか。また、何dLですか。水面の高さは、入れ物の外側の底から何cmですか。
|
| 749 |
+
|
| 750 |
+
[CHUNK]
|
| 751 |
+
## 直方体の入れ物の容積と水面の高さの計算
|
| 752 |
+
入れ物の内側の寸法は、縦37-2=35cm、横22-2=20cm、高さ21-1=20cm。容積は35cmx20cmx20cm=14000cm³=14dL。水42dL=4200cm³。水面の高さは4200cm³÷700cm²=6cm。底板の厚みを足して6+1=7cm。
|
| 753 |
+
|
| 754 |
+
[CHUNK]
|
| 755 |
+
## 体積と単位換算
|
| 756 |
+
次の□にあてはまる数を入れましょう。1L=□dL=□cm³、34000cm³=□dL=□L、0.37L=□dL=□cm³。
|
| 757 |
+
|
| 758 |
+
[CHUNK]
|
| 759 |
+
## L, dL, cm³の単位換算
|
| 760 |
+
1L=10dL=1000cm³、34000cm³=3400dL=34L、0.37L=3.7dL=370cm³。単位換算は、1L=10dL=1000cm³の関係を利用する。
|
| 761 |
+
|
| 762 |
+
[CHUNK]
|
| 763 |
+
## 図形の対称の中心
|
| 764 |
+
まず、対角線の交点から対称の中心を求めます。対称の中心を通って、向かい側の辺と交差するところが、対応する点です。
|
| 765 |
+
|
| 766 |
+
[CHUNK]
|
| 767 |
+
## 図形のまわりの長さと面積の計算
|
| 768 |
+
(まわりの長さ) 37.68cm。半円を組み合わせた図形のまわりの長さは、いちばん大きな半径の円の円周と等しくなります。12×3.14=37.68。
|
| 769 |
+
|
| 770 |
+
[CHUNK]
|
| 771 |
+
## 半径2cmの円周の計算
|
| 772 |
+
(まわりの長さ)25.12cm。半径2cmの円の円周2つ分。4×3.14×2=25.12。
|
| 773 |
+
|
| 774 |
+
[CHUNK]
|
| 775 |
+
## 1辺4cmの正方形の面積
|
| 776 |
+
(面積)16cm²。1辺4cmの正方形と同じ面積。4×4=16。
|
| 777 |
+
|
| 778 |
+
[CHUNK]
|
| 779 |
+
## 図形の面積計算:円と正方形の組み合わせ
|
| 780 |
+
(面積)75.36cm²。6×6×3.14÷2+4×4×3.14÷2-2×2×3.14÷2=18×3.14+8×3.14-2×3.14=(18+8-2)×3.14=24×3.14=75.36。
|
| 781 |
+
|
| 782 |
+
[CHUNK]
|
| 783 |
+
## 立体の体積計算:複合図形
|
| 784 |
+
①472cm²。(12+16)×24-(12×6+16×8)=472。②180m²。(12-2)×(20-2)=180。
|
| 785 |
+
|
| 786 |
+
[CHUNK]
|
| 787 |
+
## 図形の周りの長さの計算
|
| 788 |
+
① (まわりの長さ) 35.7cm。20×3.14÷4+10×2=15.7+20=35.7。
|
| 789 |
+
|
| 790 |
+
[CHUNK]
|
| 791 |
+
## 図形の面積の計算
|
| 792 |
+
(面積)78.5cm²。10×10×3.14÷4=78.5。
|
| 793 |
+
|
| 794 |
+
[CHUNK]
|
| 795 |
+
## 図形の周りの長さの計算:円と線分の組み合わせ
|
| 796 |
+
①24.84。6×3.14÷2+12×3.14÷4+6=3×3.14+3×3.14+6=6×3.14+6=24.84。
|
| 797 |
+
|
| 798 |
+
[CHUNK]
|
| 799 |
+
## 図形の周りの長さの計算:円と線分の組み合わせ
|
| 800 |
+
②33.12。8×3.14+4×2=25.12+8=33.12。
|
| 801 |
+
|
| 802 |
+
[CHUNK]
|
| 803 |
+
## 拡大図と縮図
|
| 804 |
+
2倍の拡大図、1/2の縮図。
|
| 805 |
+
|
| 806 |
+
[CHUNK]
|
| 807 |
+
## 立体の体積計算
|
| 808 |
+
①8。(3+5)×3+2×8=8×3+2×8=96。②112。8×16-4×4=128-16=112。
|
| 809 |
+
|
| 810 |
+
[CHUNK]
|
| 811 |
+
## 立体の体積計算:直方体
|
| 812 |
+
①14000、140。(37-2)×(22-2)×(21-1)=35×20×20=14000。
|
| 813 |
+
|
| 814 |
+
[CHUNK]
|
| 815 |
+
## 立体の体積計算:直方体(内側)
|
| 816 |
+
②7。内のりの底面積:(37-2)×(22-2)=35×20=700cm²。容積は42dLだから、4200cm³。4200÷700=6cm。これに底の板の厚みを加えると、6+1=7。
|
| 817 |
+
|
| 818 |
+
[CHUNK]
|
| 819 |
+
## 単位換算:Lとcm³
|
| 820 |
+
①10、1000。②340、34。③3.7、370。
|
| 821 |
+
|
| 822 |
+
[CHUNK]
|
| 823 |
+
## 平行な直線と垂直な直線
|
| 824 |
+
(平行な直線)と、と。(垂直な直線) おとか。
|
| 825 |
+
|
| 826 |
+
[CHUNK]
|
| 827 |
+
## 正多角形の対称の軸
|
| 828 |
+
正多角形の対称の軸は、辺(角)の数と同じだけあります。ア 4本、イ 5本、ウ 6本、エ 7本、オ 8本。
|
| 829 |
+
|
| 830 |
+
[CHUNK]
|
| 831 |
+
## 点対称な図形
|
| 832 |
+
点対称な図形は、辺(角)の数が偶数のものです。②ア、ウ、オ。
|
V1.7.1/knowledge/ORIGINAL/算数/中学入試 割合と比:食塩水、割合、相当算.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
V1.7.1/knowledge/ORIGINAL/算数/中学入試攻略 下克上算数ドリル【速さ編】.md
ADDED
|
@@ -0,0 +1,808 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 中学入試攻略 下克上算数ドリル【速さ編】
|
| 2 |
+
|
| 3 |
+
[CHUNK]
|
| 4 |
+
## はじめに: 単元特化型問題集の紹介
|
| 5 |
+
この本は、中学受験算数の速さに特化した問題集です。一般的な問題集と異なり、特定の単元(速さ、旅人算、流水算、列車算、時計算)に焦点を当てています。ご家族と一緒に、自分1人で問題を解く力を身につけることを目指します。
|
| 6 |
+
|
| 7 |
+
[CHUNK]
|
| 8 |
+
## 他の問題集との違い: 手書きメモと解法ステップ
|
| 9 |
+
自分1人で問題を解く力を身につける。本書の特色は、手書きの計算メモをそのまま掲載し、先生が目の前で解説しているような安心感を提供します。計算メモをSTEP1からSTEP4で表し、解く順番を分かりやすく解説。関連する言葉や数字を同じ色で表現し、理解を促進します。
|
| 10 |
+
|
| 11 |
+
[CHUNK]
|
| 12 |
+
## 解説の理解度向上と本書の構成
|
| 13 |
+
解く順番を分かりやすく解説。関連する言葉や数字を同じ色で表現し、理解を促進します。本書は、まず問題に挑戦し、わからなかったポイントを後半で確認する構成を採用。定着力とスピードを重視しています。対象者は、中学受験生、速さを学び直したい中学生、保護者です。
|
| 14 |
+
|
| 15 |
+
[CHUNK]
|
| 16 |
+
## 著者紹介と中学受験の経験
|
| 17 |
+
定着力とスピードを重視しています。対象者は、中学受験生、速さを学び直したい中学生、保護者です。著者は山下晃平。中学受験に全敗した経験から、小学生向けの解説ノートを作成し、早稲田大学の附属高校に合格。現在はロジカルスクールの講師を務めています。
|
| 18 |
+
|
| 19 |
+
[CHUNK]
|
| 20 |
+
## 目次: 本書の構成要素
|
| 21 |
+
現在はロジカルスクールの講師を務めています。本書は問題集(p4-18)、解説(p19-74)、算数が得意になるポイント(p74-86)で構成されています。基本問題に特化しており、レベルの高い問題は「さんすうがく」のサイトで無料で提供しています。
|
| 22 |
+
|
| 23 |
+
[CHUNK]
|
| 24 |
+
## 追加問題と問題1: 旅人算
|
| 25 |
+
基本問題に特化しており、レベルの高い問題は「さんすうがく」のサイトで無料で提供しています。
|
| 26 |
+
**問題1**: 8km離れたA町とB町から太郎くんと次郎くんがそれぞれ分速50m、分速100mの速さで向かい合って同時に出発。出会うのは何分後か。
|
| 27 |
+
|
| 28 |
+
[CHUNK]
|
| 29 |
+
## 問題2と3: 反対方向へ進む場合と追いつき問題
|
| 30 |
+
**問題1**: 8km離れたA町とB町から太郎くんと次郎くんがそれぞれ分速50m、分速100mの速さで向かい合って同時に出発。出会うのは何分後か。
|
| 31 |
+
**問題2**: 周囲480mの池を兄弟が反対方向に進む。兄は分速80m、弟は分速40m。何分後に出会うか。
|
| 32 |
+
**問題3**: 弟が家を出て10分後、兄が自転車で追う。弟は毎分70m、兄は分速170m。兄は何分後に追いつくか。
|
| 33 |
+
|
| 34 |
+
[CHUNK]
|
| 35 |
+
## 問題4と5: 池の周回と追いつき問題
|
| 36 |
+
**問題3**: 弟が家を出て10分後、兄が自転車で追う。弟は毎分70m、兄は分速170m。兄は何分後に追いつくか。
|
| 37 |
+
**問題4**: 周囲3600mの池をA,B,Cが周回。Aは毎分120m、Bは毎分90m、Cは毎分60m。AがCと出会い引き返すとBと何分後に出会うか。
|
| 38 |
+
**問題5**: 弟が毎分80mで出発後、25分後に兄が毎分180mで追う。兄は何分後に追いつくか。
|
| 39 |
+
|
| 40 |
+
[CHUNK]
|
| 41 |
+
## 問題6と7: 池の周回と速さの比
|
| 42 |
+
**問題5**: 弟が毎分80mで出発後、25分後に兄が毎分180mで追う。兄は何分後に追いつくか。
|
| 43 |
+
**問題6**: 周囲500mの池をAとBが反対/同じ方向に回る。反対方向では50秒、同じ方向では125秒で出会う。それぞれの速さは。
|
| 44 |
+
**問題7**: 64km離れたP,Q間をA,Bが往復。4時間後に出会い、Aは2時間24分後にQに着く。速さの比とBの速さを求めよ。
|
| 45 |
+
|
| 46 |
+
[CHUNK]
|
| 47 |
+
## 問題8と9: 家からの出発と追いつき問題
|
| 48 |
+
**問題7**: 64km離れたP,Q間をA,Bが往復。4時間後に出会い、Aは2時間24分後にQに着く。速さの比とBの速さを求めよ。
|
| 49 |
+
**問題8**: 3.2km離れたA,Bの家。Bが7時に出発し、Aが7時2分に出発。Aは毎分50m、Bは毎分100m。何分後に出会うか。
|
| 50 |
+
**問題9**: 兄は毎分75m、弟は毎分55m。弟は9時に出発、兄は9時4分に出発。兄は何分後に追いつくか。
|
| 51 |
+
|
| 52 |
+
[CHUNK]
|
| 53 |
+
## 問題10と11: 池の周回と出会い時間
|
| 54 |
+
**問題9**: 兄は毎分75m、弟は毎分55m。弟は9時に出発、兄は9時4分に出発。兄は何分後に追いつくか。
|
| 55 |
+
**問題10**: 周囲1800mの池をA,Bが周回。同じ方向に3時間後、反対方向に30分後に出会う。A,Bそれぞれの速さは。
|
| 56 |
+
**問題11**: Aは30分、Bは20分で池を一周。反対方向に進むと何分後に出会うか。
|
| 57 |
+
|
| 58 |
+
[CHUNK]
|
| 59 |
+
## 問題12と13: ロケットと新幹線
|
| 60 |
+
**問題11**: Aは30分、Bは20分で池を一周。反対方向に進むと何分後に出会うか。
|
| 61 |
+
**問題12**: 秒速25.2kmのロケットが1分20秒で進む距離を、時速168kmの新幹線は何時間かかるか。
|
| 62 |
+
**問題13**: 6kmの坂道、行きは毎時6km、帰りは毎時12km。往復の平均速度は。
|
| 63 |
+
|
| 64 |
+
[CHUNK]
|
| 65 |
+
## 問題14と15: 速さの変化と出会い
|
| 66 |
+
**問題13**: 6kmの坂道、行きは毎時6km、帰りは毎時12km。往復の平均速度は。
|
| 67 |
+
**問題14**: 2.1kmの道のりを、最初は毎分50m、途中から毎分70mで歩き34分。速さを変えたのは何分後か。
|
| 68 |
+
**問題15**: 1km離れたA,Bを太郎は分速45m、次郎は分速80mで出発。どこで出会うか。
|
| 69 |
+
|
| 70 |
+
[CHUNK]
|
| 71 |
+
## 問題16と17: 追いつきと往復運動
|
| 72 |
+
**問題15**: 1km離れたA,Bを太郎は分速45m、次郎は分速80mで出発。どこで出会うか。
|
| 73 |
+
**問題16**: Aが分速64mで出発後、Bが分速80mで追う。何分後に追いつくか。
|
| 74 |
+
**問題17**: 1.2km離れたA,B間をA君は分速70m、B君は分速50mで往復。2回目に出会うのは何分後か。
|
| 75 |
+
|
| 76 |
+
[CHUNK]
|
| 77 |
+
## 問題18と19: 遅れて出発と競争
|
| 78 |
+
**問題17**: 1.2km離れたA,B間をA君は分速70m、B君は分速50mで往復。2回目に出会うのは何分後か。
|
| 79 |
+
**問題18**: Aは分速150mで出発後、忘れ物に気づいたBが車で追う。Bが出発して9分後に出会う。Bは何分後に出発したか。
|
| 80 |
+
**問題19**: 100m走で兄は18秒、妹は20秒。50m走で兄がゴール時、妹は何m後ろか。
|
| 81 |
+
|
| 82 |
+
[CHUNK]
|
| 83 |
+
## 問題20: 一定速度と出会い
|
| 84 |
+
**問題19**: 100m走で兄は18秒、妹は20秒。50m走で兄がゴール時、妹は何m後ろか。
|
| 85 |
+
**問題20**: Q,R間でA,Bが一定速度で出発。20分後に出会い、Aは16分後にRに着く。Bは出発から何分後にQに着くか。
|
| 86 |
+
|
| 87 |
+
# 速さに関する問題集
|
| 88 |
+
|
| 89 |
+
[CHUNK]
|
| 90 |
+
## A君とB君の速さの比
|
| 91 |
+
|
| 92 |
+
A君が4歩あるく間にB君は7歩あるきます。またA君が3歩であるくきょりをB君は6歩で歩きます。A君とB君の速さの比を求めなさい。A君の歩数比と距離比から速さの比を算出。A君の4歩はB君の7歩に相当し、A君の3歩の距離はB君の6歩に相当。速さの比は歩数比と距離比の積で計算。
|
| 93 |
+
|
| 94 |
+
[CHUNK]
|
| 95 |
+
## 水夫の川下り・川上り問題
|
| 96 |
+
|
| 97 |
+
ある水夫が 35km あるところを5時間で下りました。同じところを上るのに7時間かかりました。次の問いに答えなさい。(1) 上りの速さは毎時何kmですか。(2) 下りの速さは毎時何kmですか。下りと上りの時間と距離からそれぞれの速さを計算。下りは5時間で35kmなので、速さは35km/5h。上りは7時間で35kmなので、速さは35km/7h。
|
| 98 |
+
|
| 99 |
+
[CHUNK]
|
| 100 |
+
## 川を往復する船の問題
|
| 101 |
+
|
| 102 |
+
ある川に沿って33km はなれたP町、Q町があります。この間をA、Bの2せきの船が往復しています。Aの船は上りに11時間かかり、下りに3時間かかります。またBの船は6時間かかって上ります。(1)Aの船の上り、下りの速さはそれぞれ毎時何km ですか。(2) 川の速さは毎時何kmですか。(3)Bの船の上りの速さは、毎時何km ですか。(4)Bの船は PQ 町を下るのに何時間かかりますか。A船の上り下りの時間と距離から速さを計算し、川の速さを求める。B船の上り時間から速さを計算し、川の速さを用いて下り時間を求める。
|
| 103 |
+
|
| 104 |
+
[CHUNK]
|
| 105 |
+
## 水夫の川往復問題と流速の計算
|
| 106 |
+
|
| 107 |
+
A、Bの2人の水夫がいます。長さが90kmの川を往復するのにAは上りに15時間かかり、下りに9時間かかりました。Bは上りに18時間かかりました。(1) この川の流速は毎時何km ですか。(2)Aの静水時の速さは毎時何kmですか。(3)Bの下りの速さは毎時何kmですか。(4) この川をBが下るには何時間かかりますか。Aの上り下りの時間と距離から、流速と静水時の速さを計算。Bの上り時間と流速から、Bの静水時の速さと下り時間を計算。
|
| 108 |
+
|
| 109 |
+
[CHUNK]
|
| 110 |
+
## 船の川上り・川下り問題と流速・静水時の速さの計算
|
| 111 |
+
|
| 112 |
+
ある船が川を 144km 上るのに12時間かかり、下るのに8時間かかりました。以下の問いに答えなさい。(1) この川の流れの速さは毎時何kmですか。(2) 船の静水時の速さは毎時何kmですか。上りと下りの時間と距離から、それぞれの速さを計算。流速と静水時の速さを連立方程式で求める。上りの速さ=静水時の速さ-流速、下りの速さ=静水時の速さ+流速。
|
| 113 |
+
|
| 114 |
+
[CHUNK]
|
| 115 |
+
## 静水をこぐ人の速さと川の流れの問題
|
| 116 |
+
|
| 117 |
+
静水をこぐ速さが毎時10.5kmの人が3時間で36km こぎおりました。このとき、以下の問いに答えなさい。(1) 流れの速さは、毎時何kmですか。(2) 上りの速さは、毎時何km ですか。(3)この川を5時間かけて上ると何km進みますか。下りの速さと時間から、流れの速さを計算。静水時の速さから流れの速��を引いて、上りの速さを計算。上りの速さと時間から、上る距離を計算。
|
| 118 |
+
|
| 119 |
+
[CHUNK]
|
| 120 |
+
## 川の流れと船の速度に関する問題
|
| 121 |
+
|
| 122 |
+
流れの速さが毎時3kmの川を、60kmこぎ上るのに10時間かかりました。この川を 24km こぎ下りるのに何時間かかりますか。上りの速さと流れの速さから、静水時の速さを計算。静水時の速さと流れの速さから、下りの速さを計算。下りの速さと距離から、下る時間を計算。
|
| 123 |
+
|
| 124 |
+
[CHUNK]
|
| 125 |
+
## 川の流れと船の速度に関する問題2
|
| 126 |
+
|
| 127 |
+
ある船が70kmの川を上るのに7時間かかり、同じところを下るのに2時間20分かかりました。以下の問いに答えなさい。(1) この船の下りの速さは毎時何kmですか。(2)この川の流速は船の静水時の速さの何倍ですか。上りと下りの時間と距離から、それぞれの速さを計算。流速と静水時の速さを求め、流速が静水時の速さの何倍かを計算。
|
| 128 |
+
|
| 129 |
+
[CHUNK]
|
| 130 |
+
## プールの流水問題
|
| 131 |
+
|
| 132 |
+
1周280mのプールでAは流れと同じ向きに泳いで1周すると3分30秒かかり、流れと反対方向に泳いで1周すると4分40秒かかります。このときAが静水時に進む速さは毎分何mですか。流れに沿った時と逆らった時の時間から、Aの速さと流れの速さを計算。Aの静水時の速さを求める。
|
| 133 |
+
|
| 134 |
+
[CHUNK]
|
| 135 |
+
## 静水時の船の速さと川の流れの問題
|
| 136 |
+
|
| 137 |
+
静水では毎時12km で走る船Aと、毎時16kmで走る船Bがあります。川の下流から上流まで走るとき、Aは10時間、Bは6時間40分かかります。川の流れは毎時何kmですか。AとBの上りの時間と静水時の速さから、川の流れの速さを計算。AとBの速さの差と時間の差から、川の流れを求める。
|
| 138 |
+
|
| 139 |
+
[CHUNK]
|
| 140 |
+
## プールでの流水とうき輪の問題
|
| 141 |
+
|
| 142 |
+
一定の速さで水が流れている1周240mのプールがあります。Aはスタートの位置から水の流れに乗って泳ぎ始めます。それと同時にスタートした位置からうき輪を流したところ、ちょうどAが3周進んだときにうき輪に追いつきました。またAが流れに逆らって泳いだところ、1周泳ぐのに12分かかりました。このとき、以下の問いに答えなさい。ただしAの静水時の速さは一定であるとします。(1) プールの水の流れる速さは毎分何mですか。(2)Aが流れに乗って泳いだとき、1周するのにかかる時間は何分ですか。Aが流れに逆らって泳いだ時間から、Aの静水時の速さを計算。Aが3周泳いだ時間とうき輪が流れた時間が等しいことから、水の流れる速さを計算。Aが流れに乗って泳いだ時の速さから、1周する時間を計算。
|
| 143 |
+
|
| 144 |
+
[CHUNK]
|
| 145 |
+
## 川下りと上りの時間変化問題
|
| 146 |
+
|
| 147 |
+
ある船が 105kmの川を下るのに7時間かかりました。元のところへもどるとき、流れの速さが2倍になったので、11時間40分かかりました。(1) この船の下りの速さは毎時何kmですか。(2) 流速が2倍になった時の上りの速さは毎時何kmですか。(3) この船の静水時のときの速さは毎時何km ですか。下りの時間と距離から、下りの速さを計算。流れの速さが2倍になった時の上りの時間から、上りの速さを計算。静水時の速さと流れの速さを求める。
|
| 148 |
+
|
| 149 |
+
[CHUNK]
|
| 150 |
+
## 川の上り下り問題と流水変化
|
| 151 |
+
|
| 152 |
+
川に沿って 27km はなれてP町、Q町があります。ある船がQ町を出発してP町まで上るのに4時間半かかり、下る時は流水が2倍になったので1時間で下りました。この船の静水時の速さは毎時何km ですか。上りと下りの時間と距離から、上りと下りの速さを計算。流水が2倍になった時の下りの速さから、流水の速さを計算。静水時の速さを求める。
|
| 153 |
+
|
| 154 |
+
[CHUNK]
|
| 155 |
+
## 川の流れと船の速度変化問題
|
| 156 |
+
|
| 157 |
+
ある船が川を 48km 下ったところ8時間かかりました。その後、流速が下るときの2倍になったため同じところを上るのに下った時の2倍の時間がかかりました。この船の静水時の速さは毎時何kmですか。下りの時間と距離から、下りの速さを計算。流速が2倍になった時の上りの時間から、上りの速さを計算。静水時の速さを求める。
|
| 158 |
+
|
| 159 |
+
[CHUNK]
|
| 160 |
+
## 川の上り下り問題と流速変化
|
| 161 |
+
|
| 162 |
+
川にそって39km はなれたP町とQ町があります。ある船がQ町を出発してP町まで上るのに13時間かかり、下る時は流速が上りのときの4倍になったので3時間で下ることができました。この船の静水時の速さは毎時何kmですか。上りの時間と距離から、上りの速さを計算。流速が4倍になった時の下りの時間から、下りの速さを計算。静水時の速さを求める。
|
| 163 |
+
|
| 164 |
+
[CHUNK]
|
| 165 |
+
## 川の往復問���
|
| 166 |
+
|
| 167 |
+
ある船が川上にあるP町とそこから 45km はなれたQ町を往復するのに、上りは9時間、下りは5時間かかりました。(1) 上りの速さは毎時何kmですか。(2) 下りの速さは毎時何kmですか。(3) 船が静水を進むときの速さは毎時何kmですか。上りと下りの時間と距離から、それぞれの速さを計算。静水時の速さを求める。
|
| 168 |
+
|
| 169 |
+
[CHUNK]
|
| 170 |
+
## 川の上り下り問題と静水時の速さ
|
| 171 |
+
|
| 172 |
+
ある船が川上にあるA町とそこから 135km はなれたB町の間を往復するのに、上りは15時間、下りは5時間かかります。(1) 上りの速さは毎時何kmですか。(2) 下りの速さは毎時何km ですか。(3) 船の静水時のときの速さは毎時何km ですか。上りと下りの時間と距離から、それぞれの速さを計算。静水時の速さを求める。
|
| 173 |
+
|
| 174 |
+
[CHUNK]
|
| 175 |
+
## 船の出会い問題
|
| 176 |
+
|
| 177 |
+
静水時の速さが毎時4kmの船A、毎時7kmの船Bがあります。55km はなれた川上のP町をA船が、川下のQ町をB船が向かい合って同時に進みます。A、B両船は出発してから何時間何分後に出会いますか。AとBの速さの和から、出会うまでの時間を計算。
|
| 178 |
+
|
| 179 |
+
[CHUNK]
|
| 180 |
+
## 船の追いつき問題
|
| 181 |
+
|
| 182 |
+
A、Bの2せきの船があります。静水を進む速さはAは毎時40km、Bは毎時20kmです。今 240kmの川を同時にAは川上から、Bは下流の川下から向かい合って進みます。Bはと中で出会った後、16時間たってAの出発地点にとう着することができました。この川の流速は毎時何kmですか。AとBの速さの和から出会うまでの距離を計算。Aが出発地点に戻るまでの時間と距離から、川の流速を計算。
|
| 183 |
+
|
| 184 |
+
[CHUNK]
|
| 185 |
+
## 川の流れと船の速度問題
|
| 186 |
+
|
| 187 |
+
流水の速さが一定の川があります。この川を静水時の速さが一定の船が往復するとき、上りの速さは毎時20km、下りの速さが28km でした。(1) この川の流れの速さは毎時何kmですか。(2) この船の静水時の速さは毎時何kmですか。上りと下りの速さから、川の流れの速さと船の静水時の速さを計算。
|
| 188 |
+
|
| 189 |
+
[CHUNK]
|
| 190 |
+
## 流水の速さが変化する川の問題
|
| 191 |
+
|
| 192 |
+
流水の速さが一定の川があります。この川を静水時の速さが一定の船が往復するとき、上りの速さは毎時24km、下るときは川の速さが5倍になったので、速さが毎時42kmになりました。(1) この川の速さは毎時何kmですか。(2) この船の静水時の速さは、毎時何kmですか。上りと下りの速さから、川の流れの速さと船の静水時の速さを計算。
|
| 193 |
+
|
| 194 |
+
[CHUNK]
|
| 195 |
+
## プールでの速度問題
|
| 196 |
+
|
| 197 |
+
(1) プールで毎分 75mの速さで泳ぐ人が、毎分15mの速さで流れるプールの上流に向かって120m泳ぐには、何分かかりますか。(2) 20m先のプールサイドを歩いているBくんが毎分30mで歩いているとき、Aくんが泳いでBくんに追いつくのは何秒後ですか。ただし、A君はプールと反対向きで進むとします。Aが上流に向かって泳ぐ速さから、120m泳ぐ時間を計算。AとBの速さの差から、追いつくまでの時間を計算。
|
| 198 |
+
|
| 199 |
+
[CHUNK]
|
| 200 |
+
## 静水時の速さが一定の船の問題
|
| 201 |
+
|
| 202 |
+
ある川を静水時の速さが一定の船で80km 下るのに5時間、20km 上るのに2時間半かかりました。この川の流れの速さは毎時何kmですか。下りと上りの時間と距離から、それぞれの速さを計算。川の流れの速さを計算。
|
| 203 |
+
|
| 204 |
+
[CHUNK]
|
| 205 |
+
## 川下り問題
|
| 206 |
+
|
| 207 |
+
ある船が105kmの川を下るのに7時間かかりました。元のところへもどるとき、流れの速さが2倍になったので、11時間40分かかりました。(1) この船の下りの速さは毎時何kmですか。(2) 流速が2倍になった時の上りの速さは毎時何kmですか。(3) この船の静水時の速さは毎時何kmですか。下りの時間と距離から、下りの速さを計算。流れの速さが2倍になった時の上りの時間から、上りの速さを計算。静水時の速さと流れの速さを求める。
|
| 208 |
+
|
| 209 |
+
[CHUNK]
|
| 210 |
+
## 列車のトンネル通過問題
|
| 211 |
+
|
| 212 |
+
ある列車は 840mのトンネルをぬけるのに60秒かかり、466mの橋をわたるのに38秒かかります。列車の長さは何mですか。トンネル通過と橋の通過の時間差から、列車の速さを計算。列車の速さと時間から、列車の長さを計算。
|
| 213 |
+
|
| 214 |
+
[CHUNK]
|
| 215 |
+
## 列車の速度問題
|
| 216 |
+
|
| 217 |
+
長さ450m の列車が、線路ぎわに立っている人の前を9秒で通過しました。この列車の速さは毎時何kmですか。列車の長さと時間から、列車の速さを計算。単位を変換して、毎時何kmで表す。
|
| 218 |
+
|
| 219 |
+
[CHUNK]
|
| 220 |
+
## 列車のトンネル通過問題2
|
| 221 |
+
|
| 222 |
+
一定の速さで走る列車が長さ700mのトンネルを32秒で、電柱の前を4秒で、それぞれ通過しまし��。この列車の長さは何mですか。電柱の前を通過する時間から、列車の速さを計算。列車の速さとトンネル通過時間から、列車の長さを計算。
|
| 223 |
+
|
| 224 |
+
[CHUNK]
|
| 225 |
+
## 列車のトンネル通過問題3
|
| 226 |
+
|
| 227 |
+
長さ120m、時速48.6kmの列車がトンネルに入ってから、列車は40秒間トンネルに完全にかくれていました。このトンネルの長さは何mですか。列車の速さを秒速に変換。トンネルにかくれている時間と列車の速さから、トンネルの長さを計算。
|
| 228 |
+
|
| 229 |
+
[CHUNK]
|
| 230 |
+
## 列車のすれ違い問題
|
| 231 |
+
|
| 232 |
+
長さ120m、時速180kmのA列車と、長さ210m、時速216kmのB列車が、出会ってからすれちがい終わるまでに何秒かかりますか。AとBの速さの和を計算。AとBの長さの和を計算。速さと距離から、すれ違いにかかる時間を計算。
|
| 233 |
+
|
| 234 |
+
[CHUNK]
|
| 235 |
+
## 列車の追い越し問題
|
| 236 |
+
|
| 237 |
+
時速108kmと時速72km で走る電車があります。この列車がそれぞれ反対方向に走るとすれちがうのに10秒かかります。この列車が同じ向きに走ったとき、早い方の列車がおそい方の列車を追いぬくのに何秒かかりますか。すれ違う時間と速さから、列車の長さを計算。同じ向きに走る時の速さの差から、追い抜く時間を計算。
|
| 238 |
+
|
| 239 |
+
[CHUNK]
|
| 240 |
+
## 列車の追い越し問題2
|
| 241 |
+
|
| 242 |
+
長さ150m、時速90kmのA列車が、長さ120mのB列車に追いついてから追いこし終わるまでに30秒かかりました。B列車の速さは毎時何kmですか。AとBの長さの和を計算。AとBの速さの差を計算。速さの差と時間から、Bの速さを計算。
|
| 243 |
+
|
| 244 |
+
[CHUNK]
|
| 245 |
+
## 列車のトンネル通過と追い越し問題
|
| 246 |
+
|
| 247 |
+
速さが19秒で長さが300mの長きょり列車A、速さ毎秒16m で長さ 360mの長きょり列車Bがあります。以下の問いに答えなさい。(1) 列車Aがトンネルを通り抜けるのに12分かかりました。トンネルの長さは何m ですか。(2) 列車Aが列車Bに追いついてから追いこすまでに何秒かかりますか。Aの速さと時間から、トンネルの長さを計算。AとBの速さの差から、追い越す時間を計算。
|
| 248 |
+
|
| 249 |
+
[CHUNK]
|
| 250 |
+
## 電車の通過問題
|
| 251 |
+
|
| 252 |
+
長さ 500m の電車が秒速20mで走っています。以下の問いに答えなさい。(1) この電車がふみきりで待っている太郎くんの前を通過し始めてから通過し終わるまでに何秒かかりますか。(2) この電車が長さ300m の橋をわたり始めてからわたり終わるまでに何秒かかりますか。電車の長さと速さから、太郎くんの前を通過する時間を計算。電車の長さと橋の長さを足し、速さから通過時間を計算。
|
| 253 |
+
|
| 254 |
+
[CHUNK]
|
| 255 |
+
## 時計の角度問題
|
| 256 |
+
|
| 257 |
+
7時51分のとき、時計の長針と短針がつくる小さい方の角度の大きさは何度ですか。7時51分における長針と短針の位置を計算し、角度を求める。
|
| 258 |
+
|
| 259 |
+
[CHUNK]
|
| 260 |
+
## 時計の重なり問題
|
| 261 |
+
|
| 262 |
+
4時と5時の間で、時計の長針と短針が重なる時刻は4時何分ですか。長針と短針の速さの差から、重なる時間を計算。
|
| 263 |
+
|
| 264 |
+
[CHUNK]
|
| 265 |
+
## 時計の角度問題2
|
| 266 |
+
|
| 267 |
+
3時と4時の間で、長針と短針の作る角度が30度になる時はそれぞれ3時何分ですか。長針と短針の速さの差から、30度になる時間を計算。
|
| 268 |
+
|
| 269 |
+
[CHUNK]
|
| 270 |
+
## 特殊な時計の問題
|
| 271 |
+
|
| 272 |
+
ある特別な時計は長針が30秒で1周、短針が2分で1周します。今、長身と短針が反対方向をさして一直線になっています。時計の長針と短針が最初に重なるのは今から何秒後ですか。長針と短針の速さの差から、重なる時間を計算。
|
| 273 |
+
|
| 274 |
+
[CHUNK]
|
| 275 |
+
## 時計の直角問題
|
| 276 |
+
|
| 277 |
+
10時と11時の間で、時計の長針と短針のつくる角が直角になることがあります。2回目に直角になるのは10時何分ですか。長針と短針の速さの差から、直角になる時間を計算。
|
| 278 |
+
|
| 279 |
+
[CHUNK]
|
| 280 |
+
## 時計の重なり問題3
|
| 281 |
+
|
| 282 |
+
時計の長針と短針が2時と3時の間でちょうど重なっています。このとき、以下の問いに答えなさい。(1) このときの時間は2時何分ですか。(2) その後長針と短針が文字ばんのめもりの6をはさんで等しい角度になっている時刻は2時何分ですか。長針と短針の速さの差から、重なる時間を計算。長針と短針が反対側になる時間を計算。
|
| 283 |
+
|
| 284 |
+
[CHUNK]
|
| 285 |
+
## 時計の角度問題4
|
| 286 |
+
|
| 287 |
+
時計の針が5時10分をさしています。このとき、以下の問いに答えなさい。(1) 長針と短針が作る角度で小さいものは何度ですか。(2) 長針が短針を追いこしてから作る角度が90度となるのは5時何分ですか。5時10分における長針と短針の位置を計算し、角度を求める。長針が短針を追い越してから90度になる時間を計算。
|
| 288 |
+
|
| 289 |
+
# 速さ編ドリル
|
| 290 |
+
|
| 291 |
+
[CHUNK]
|
| 292 |
+
## 太郎と次郎の出会い:8kmを分速50mと100mで向かい合う
|
| 293 |
+
8km離れたA町とB町から太郎と次郎がそれぞれ分速50m、100mで向かい合って出発。出会うまでの時間を求める。単位をmに統一し、8km=8100mとする。2人は1分で150m近づくため、8100m÷150m/分=54分後に出会う。面積図による解法も参照。
|
| 294 |
+
|
| 295 |
+
[CHUNK]
|
| 296 |
+
## 池の周回:480mの池を反対方向に進む兄弟の出会い
|
| 297 |
+
周囲480mの池を兄弟が反対方向に進む。兄は分速80m、弟は分速40m。出会うまでの時間を求める。2人は1分で120m近づくため、480m÷120m/分=4分後に出会う。池を円で図示し、反対方向の矢印で示す。面積図による解法も参照。
|
| 298 |
+
|
| 299 |
+
[CHUNK]
|
| 300 |
+
## 弟を追う兄:10分後に自転車で出発、追いつくまでの時間
|
| 301 |
+
弟が家を出て10分後、兄が自転車で追いかける。弟は分速70m、兄は分速170m。兄が出発して追いつくまでの時間を求める。弟は10分で700m進む。兄は毎分100mずつ近づくので、700m÷100m/分=7分後。線分図で距離と速さを視覚化。
|
| 302 |
+
|
| 303 |
+
[CHUNK]
|
| 304 |
+
## 周回池:A,B,Cが反対方向に進む、AとCが出会った後AがBに出会う時間
|
| 305 |
+
周囲3600mの池をA,B,Cが周回。Aは毎分120m、Bは毎分90m、Cは毎分60m。AがCと出会い、その後AがBに出会うまでの時間を求める。AとCは20分後に出会う。AとBは20分で600m離れる。AがBに向かうと毎分210m近づくので、600m÷210m/分=20/7分後。円で図示し、各人物の速度と方向を示す。
|
| 306 |
+
|
| 307 |
+
[CHUNK]
|
| 308 |
+
## 兄が弟を追う:25分後に出発、追いつくまでの時間
|
| 309 |
+
弟が毎分80mで出発後、25分後に兄が毎分180mで追いかける。兄が追いつくまでの時間を求める。弟は25分で2000m進む。兄は毎分100mずつ近づくので、2000m÷100m/分=20分後。線分図で距離と速さを視覚化。
|
| 310 |
+
|
| 311 |
+
[CHUNK]
|
| 312 |
+
## 池の周回:AとBが反対/同じ方向に進む速さを求める
|
| 313 |
+
1周500mの池をAとBが周回。反対方向では50秒、同じ方向では125秒で出会う。AとBの速さを求める。反対方向ではA+B、同じ方向ではA-Bの速さで計算。A+B=10m/秒、A-B=4m/秒。A=7m/秒、B=3m/秒。円で図示し、各人物の速度と方向を示す。
|
| 314 |
+
|
| 315 |
+
[CHUNK]
|
| 316 |
+
## 2地点間の移動:AとBの速さの比とBの速さを求める
|
| 317 |
+
64km離れたP,Q間をAとBが移動。AはPからQへ、BはQからPへ。4時間後に出会い、Aはその後2時間24分でQに到着。AとBの速さの比とBの速さを求める。A:B=5:3、B=6km/時。線分図で距離と時間を視覚化。
|
| 318 |
+
|
| 319 |
+
[CHUNK]
|
| 320 |
+
## 家から家へ:AとBが出発、出会うまでの時間を求める
|
| 321 |
+
AとBの家は3.2km離れている。Bは7時に出発、Aは7時2分に出発。Aは毎分50m、Bは毎分100m。出会うまでの時間を求める。Bは2分で200m進む。残りは3000m。2人は毎分150m近づくので、3000m÷150m/分=20分後。線分図で距離と時間を視覚化。
|
| 322 |
+
|
| 323 |
+
[CHUNK]
|
| 324 |
+
## 弟を追う兄:4分後に出発、追いつくまでの時間を求める
|
| 325 |
+
弟は毎分75m、兄は毎分55mで歩く。弟は9時に出発、兄は9時4分に出発。兄が出発して追いつくまでの時間を求める。弟は4分で220m進む。兄は毎分20mずつ近づくので、220m÷20m/分=11分後。線分図で距離と時間を視覚化。
|
| 326 |
+
|
| 327 |
+
[CHUNK]
|
| 328 |
+
## 池の周回:AとBが同じ/反対方向に進む速さを求める
|
| 329 |
+
周囲1800mの池をAとBが周回。同じ方向では3時間、反対方向では30分で出会う。AとBの速さを求める。同じ方向ではA-B、反対方向ではA+Bの速さで計算。A-B=10m/分、A+B=60m/分。A=35m/分、B=25m/分。円で図示し、各人物の速度と方向を示す。
|
| 330 |
+
|
| 331 |
+
# 速さ編ドリル - RAG最適化
|
| 332 |
+
|
| 333 |
+
[CHUNK]
|
| 334 |
+
## 池の周回:反対方向への進行と出会い
|
| 335 |
+
ある池をAは30分、Bは20分で一周する。AとBが反対方向に進むとき、出会うまでの時間は?解答は12分。池の周りの長さもAとBの速さも不明なため、「仮定」を利用。池の周りを30と20の最小公倍数60と仮定。Aの速さは60÷30=2/分、Bは60÷20=3/分。反対方向なので、近づく速さは2+3=5/分。出会う時間は60÷5=12分。
|
| 336 |
+
|
| 337 |
+
[CHUNK]
|
| 338 |
+
## 池の周回:反対方向への進行と出会い(重複)
|
| 339 |
+
解答は12分。池の周りの長さもAとBの速さも不明なため、「仮定」を利用。池の周りを30と20の最小公倍数60と仮定。Aの速さは60÷30=2/分、Bは60÷20=3/分。反対方向なので、近づく速さは2+3=5/分。出会う時間は60÷5=12分。最小公倍数で仮定すると計算が容易。
|
| 340 |
+
|
| 341 |
+
[CHUNK]
|
| 342 |
+
## ロケットと新幹線:時間計算
|
| 343 |
+
秒速25.2kmのロケットが1分20秒で進む距離を、時速168kmの新幹線で移動するのにかかる時間は?解答は12時間。秒速25.2kmは1秒で25.2km進む速さ。1分20秒は80秒。ロケットは80秒で25.2km/秒×80秒=2016km進む。新幹線は時速168kmなの��、2016km÷168km/時=12時間。
|
| 344 |
+
|
| 345 |
+
[CHUNK]
|
| 346 |
+
## ロケットと新幹線:時間計算(重複)
|
| 347 |
+
解答は12時間。秒速25.2kmは1秒で25.2km進む速さ。1分20秒は80秒。ロケットは80秒で25.2km/秒×80秒=2016km進む。新幹線は時速168kmなので、2016km÷168km/時=12時間。計算ミスを防ぐため単位を秒に統一。面積図で視覚的に理解。
|
| 348 |
+
|
| 349 |
+
[CHUNK]
|
| 350 |
+
## 坂道の往復:平均速度の計算
|
| 351 |
+
6kmの坂道、行きは毎時6km、帰りは毎時12kmの速さで往復。往復の平均速度は何km/時?解答は8km/時。往復の平均速度を求めるには、合計距離と合計時間が必要。距離の合計は6km×2=12km。行きは6km÷6km/時=1時間、帰りは6km÷12km/時=0.5時間。合計時間は1+0.5=1.5時間。平均速度は12km÷1.5時間=8km/時。
|
| 352 |
+
|
| 353 |
+
[CHUNK]
|
| 354 |
+
## 坂道の往復:平均速度の計算(重複)
|
| 355 |
+
解答は8km/時。往復の平均速度を求めるには、合計距離と合計時間が必要。距離の合計は6km×2=12km。行きは6km÷6km/時=1時間、帰りは6km÷12km/時=0.5時間。合計時間は1+0.5=1.5時間。平均速度は12km÷1.5時間=8km/時。単位を時間で統一。面積図で視覚的に理解。
|
| 356 |
+
|
| 357 |
+
[CHUNK]
|
| 358 |
+
## 距離と速さ:速度変化時の時間計算
|
| 359 |
+
2.1kmの道のり、最初は毎分50m、途中から毎分70mで歩き、合計34分。速さを変えたのは何分後?解答は14分後。50m/分で進む時間を○分、70m/分で進む時間を□分と仮定。合計距離は2100m。緑の四角形は70m/分で34分進んだ距離=2380m。青の四角形の面積は2380m-2100m=280m。
|
| 360 |
+
|
| 361 |
+
[CHUNK]
|
| 362 |
+
## 距離と速さ:速度変化時の時間計算(重複)
|
| 363 |
+
解答は14分後。50m/分で進む時間を○分、70m/分で進む時間を□分と仮定。合計距離は2100m。緑の四角形は70m/分で34分進んだ距離=2380m。青の四角形の面積は2380m-2100m=280m。青の四角形の縦は70m/分-50m/分=20m/分。横(時間)は280m÷20m/分=14分。
|
| 364 |
+
|
| 365 |
+
[CHUNK]
|
| 366 |
+
## 向かい合う移動:出会う場所の特定
|
| 367 |
+
1km離れたA,B地点間を、太郎は分速45mでAから、次郎は分速80mでBから同時に出発。出会うのはAから何m?解答は360m。二人の距離と速さを線分図で示す。単位をmに統一。二人が近づく速さは45m/分+80m/分=125m/分。出会うまでの時間は1000m÷125m/分=8分。
|
| 368 |
+
|
| 369 |
+
[CHUNK]
|
| 370 |
+
## 向かい合う移動:出会う場所の特定(重複)
|
| 371 |
+
解答は360m。二人の距離と速さを線分図で示す。単位をmに統一。二人が近づく速さは45m/分+80m/分=125m/分。出会うまでの時間は1000m÷125m/分=8分。太郎はAから45m/分×8分=360m進んだ地点で出会う。速さの単位を揃えることが重要。
|
| 372 |
+
|
| 373 |
+
[CHUNK]
|
| 374 |
+
## 追いかけ:追いつく時間
|
| 375 |
+
Aが分速64mで出発してから8分後、Bが分速80mでAを追跡。Aは出発から何分後に追いつかれる?解答は40分後。AとBの出発時間のずれを考慮。Aが8分で進んだ距離は64m/分×8分=512m。BがAに近づく速さは80m/分-64m/分=16m/分。Bが追いつくまでの時間は512m÷16m/分=32分。
|
| 376 |
+
|
| 377 |
+
[CHUNK]
|
| 378 |
+
## 追いかけ:追いつく時間(重複)
|
| 379 |
+
解答は40分後。Aが8分で進んだ距離は64m/分×8分=512m。BがAに近づく速さは80m/分-64m/分=16m/分。Bが追いつくまでの時間は512m÷16m/分=32分。Aが出発してから追いつかれるまでの時間は32分+8分=40分。問題文をよく読み、問われていることを正確に把握。
|
| 380 |
+
|
| 381 |
+
[CHUNK]
|
| 382 |
+
## 往復運動:2回目の出会い
|
| 383 |
+
1.2km離れたA,B間をA君は分速70mでPから、B君は分速50mでQから向かい合って出発しPQ間を往復。2回目の出会いは何分後?解答は30分後。1.2km=1200m。1回目に出会うまでの時間は1200m÷(70m/分+50m/分)=10分。
|
| 384 |
+
|
| 385 |
+
[CHUNK]
|
| 386 |
+
## 往復運動:2回目の出会い(重複)
|
| 387 |
+
解答は30分後。1.2km=1200m。1回目に出会うまでの時間は1200m÷(70m/分+50m/分)=10分。2回目に出会うまでに2人は合計1200m×2=2400m多く進む必要。2400m÷120m/分=20分。2回目の出会いは10分+20分=30分後。
|
| 388 |
+
|
| 389 |
+
[CHUNK]
|
| 390 |
+
## 自転車と車:追いつく時間
|
| 391 |
+
Aは分速150mで自転車、Bは時速30kmの車でAを追う。BはAの何分後に出発?B出発9分後に追いつく。解答は21分後。速さの単位を統一。30km/時=500m/分。Bが9分で進む距離は500m/分×9分=4500m。
|
| 392 |
+
|
| 393 |
+
[CHUNK]
|
| 394 |
+
## 自転車と車:追いつく時間(重複)
|
| 395 |
+
解答は21分後。速さの単位を統一。30km/時=500m/分。Bが9分で進む距離は500m/分×9分=4500m。Aが4500m進む時間は4500m÷150m/分=30分。BはAより30分-9分=21分遅れて出発。線分図で視覚的に理解。
|
| 396 |
+
|
| 397 |
+
[CHUNK]
|
| 398 |
+
## 短距離走:ゴール時の距離差
|
| 399 |
+
100m走、兄18秒、妹20秒。50m走で兄がゴール時、妹は何m後方?解答は5m。距離を半分にすると時間も半分。兄は50mを9秒、妹は10秒。兄がゴール時、妹はあと1秒。妹は20秒で100m、1秒で5m。
|
| 400 |
+
|
| 401 |
+
[CHUNK]
|
| 402 |
+
## 短距離走:���ール時の距離差(重複)
|
| 403 |
+
解答は5m。距離を半分にすると時間も半分。兄は50mを9秒、妹は10秒。兄がゴール時、妹はあと1秒。妹は20秒で100m、1秒で5m。兄ゴール時、妹は5m後方。工夫して計算量を削減。
|
| 404 |
+
|
| 405 |
+
[CHUNK]
|
| 406 |
+
## 向かい合う移動:出会いと到達
|
| 407 |
+
Q,R間でA,Bが向かい合い出発、20分後に出会い、Aは16分後にR着。Bは出発から何分後にQ着?解答は25分後。Bが20分で進む距離=Aが16分で進む距離。Bの速さを4/分と仮定。Aの速さは(4/分*20分)/16分=5/分。
|
| 408 |
+
|
| 409 |
+
[CHUNK]
|
| 410 |
+
## 向かい合う移動:出会いと到達(重複)
|
| 411 |
+
解答は25分後。Bが20分で進む距離=Aが16分で進む距離。Bの速さを4/分と仮定。Aの速さは(4/分*20分)/16分=5/分。Aが20分で進む距離は5/分*20分=100。BがQに着くまでの時間は100/(4/分)=25分。
|
| 412 |
+
|
| 413 |
+
# 速さに関する問題集
|
| 414 |
+
|
| 415 |
+
[CHUNK]
|
| 416 |
+
## A君とB君の速さの比を求める問題
|
| 417 |
+
A君が4歩あるく間にB君は7歩あるきます。またA君が3歩である距離をB君は6歩で歩きます。A君とB君の速さの比を求めます。A君が4歩あるく時間を28と仮定すると、A君、B君それぞれの1歩あるく時間の比は7:4。ふたりの進む距離を6として、線分図を書くとふたりの歩幅の比は、2:1。A君とB君の速さの比は7×2:4×1=14:4=7:2。
|
| 418 |
+
|
| 419 |
+
[CHUNK]
|
| 420 |
+
## A君とB君の速さの比
|
| 421 |
+
A君とB君の速さの比は1歩あるく時間の比×歩幅の比で求める。A君が4歩あるく時間を28と仮定すると、A君、B君それぞれの1歩あるく時間の比は7:4。ふたりの進む距離を6として、線分図を書くとふたりの歩幅の比は、2:1。A君とB君の速さの比は7×2:4×1=14:4=7:2。
|
| 422 |
+
|
| 423 |
+
[CHUNK]
|
| 424 |
+
## 川の流れに乗る船の速さに関する問題
|
| 425 |
+
ある水夫が35km あるところを5時間で下りました。同じところを上るのに7時間かかりました。上りの速さ、下りの速さ、川の速さ、船の静水時の速さを求めます。上りは7時間かかるので、35km÷7時間=5km/時。下りは5時間なので、35km÷5時間=7km/時。速さの線分図より、川の速さは1km/時、船の静水時の速さは6km/時。
|
| 426 |
+
|
| 427 |
+
[CHUNK]
|
| 428 |
+
## 川の流れに乗る船の速さ
|
| 429 |
+
川の流れに乗る船の速さは、川の速さについても考える必要。川上に進むときはゆっくり、川下に進むときは早くなる。35kmの道のりを7時間かけて進むので、上るときの速さは35km÷7時間=5km/時。同じように川を下るときの速さを計算すると、35km÷5時間=7km/時。速さの線分図より、川の速さは1km/時、船の静水時の速さは6km/時。
|
| 430 |
+
|
| 431 |
+
[CHUNK]
|
| 432 |
+
## 川を往復する船の速さに関する問題
|
| 433 |
+
ある川に沿って33km離れたP町、Q町があります。この間をA、Bの2隻の船が往復。Aの船は上りに11時間、下りに3時間。Bの船は6時間かかって上ります。Aの船の上り、下りの速さ、川の速さ、Bの船の上りの速さ、Bの船の下りの速さを求めます。Aの上り3km/時、下り11km/時。川の速さは4km/時。Bの上り5.5km/時、下り13.5km/時。
|
| 434 |
+
|
| 435 |
+
[CHUNK]
|
| 436 |
+
## 川を往復する船の速さの計算
|
| 437 |
+
33kmの川を上るまでに11時間かかっているので、33km÷11時間=3km/時。下るときは3時間なので、33km÷3時間=11km/時。川を上ったり下ったりする問題では川の速さを考える。川の速さを①とすると、②=8km/時。川の速さ×2=8km/時なので、川の速さは4km/時。Bの上るときに進む速さは33km÷6時間=5.5km/時。下りの速さは、5.5km/時+8km/時=13.5km/時。
|
| 438 |
+
|
| 439 |
+
[CHUNK]
|
| 440 |
+
## 川を往復する水夫の速さに関する問題
|
| 441 |
+
A、Bの2人の水夫がいます。長さが90kmの川を往復するのにAは上りに15時間、下りに9時間。Bは上りに18時間かかりました。川の流速、Bの静水時の速さ、Bの下りの速さ、Bが下る時間を求めます。川の流速は2km/時、Bの静水時の速さは7km/時、Bの下りの速さは9km/時、Bが下る時間は10時間。
|
| 442 |
+
|
| 443 |
+
[CHUNK]
|
| 444 |
+
## 川を往復する水夫の速さの計算
|
| 445 |
+
90kmの川を上るまでに15時間かかっているので、90km÷15時間=6km/時。下るときは9時間なので、90km÷9時間=10km/時。川を上ったり下ったりする問題では川の速さを考える。川の速さ×2=4km/時なので、川の速さは2km/時。Bが上るときの速さは90km÷18時間=5km/時。Bの静水時の速さは5km/時+2km/時=7km/時。Bの下るときの速さは7km/時+2km/時=9km/時。Bが下る時間は90km÷9km/時=10時間。
|
| 446 |
+
|
| 447 |
+
[CHUNK]
|
| 448 |
+
## 川を上り下りする船の問題
|
| 449 |
+
ある船が川を144km上るのに12時間かかり、下るのに8時間かかりました。川の流れの速さと船の静水時の速さを求めます。川の流れの速さは3km/時、船の静水時の速さは15km/時。
|
| 450 |
+
|
| 451 |
+
[CHUNK]
|
| 452 |
+
## 川を上り下りする船の速さの計算
|
| 453 |
+
川の長さ、上���までの時間、下るまでの時間が分かっているので、上るときの速さと下るときの速さを求める。144km÷12時間=12km/時(上るときの速さ)、144km÷8時間=14km/時(下るときの速さ)。上るときの速さと下るときの速さが分かったら、「速さの線分図」を書く。川の速さを1として、計算する。川の速さを1と仮定すると2=4km/時を計算。よって川の速さの1は4km/時÷2=2km/時。下に進むときの速さ=静水時の速さ+川の速さなので、14km/時=静水時の速さ+2km/時。静水時の速さ=14km/時-2km/時となり、12km/時。
|
| 454 |
+
|
| 455 |
+
[CHUNK]
|
| 456 |
+
## 川をこぎおりる人の速さに関する問題
|
| 457 |
+
静水をこぐ速さが毎時10.5kmの人が3時間で36kmこぎおりました。流れの速さ、上りの速さ、5時間かけて上ると何km進むかを求めます。流れの速さは1.5km/時、上りの速さは9km/時、5時間かけて上ると45km進む。
|
| 458 |
+
|
| 459 |
+
[CHUNK]
|
| 460 |
+
## 川をこぎおりる人の速さの計算
|
| 461 |
+
静水時の速さが10.5km/時、3時間で36kmを下ったことが分かっているので、36km÷3時間=12km/時と下りの速さが12km/時とわかる。下に進むときの速さ=静水時の速さ+川の速さなので、下に進むときの速さ一静水時の速さ=川の速さと考えられる。よって川の速さは、12km/時-10.5km/時=1.5km/時。上に進むときの速さ=静水時の速さー川の速さなので、上に進むときの速さ=10.5km/時-1.5km/時となり、9km/時。上に進むときの速さ(9km/時)で5時間進むので進む距離は、9km/時×5時間=45km。
|
| 462 |
+
|
| 463 |
+
[CHUNK]
|
| 464 |
+
## 川をこぎ上る時間の問題
|
| 465 |
+
流れの速さが毎時3kmの川を、60kmこぎ上るのに10時間かかりました。この川を24kmこぎ下りるのに何時間かかりますか。2時間。
|
| 466 |
+
|
| 467 |
+
[CHUNK]
|
| 468 |
+
## 川をこぎ上る時間の計算
|
| 469 |
+
60kmの距離を10時間かけて上るので、60km÷10時間=6km/時。静水時の速さ=上るときの速さ+川の速さと考えることができるので、静水時の速さ=6km/時+3km/時で9km/時。下るときの速さ=静水時の速さ(9km/時)+川の速さ(3km/時)で12km/時と分かります。24km下るときの時間は、24km÷下るときの速さで求めることができるので、24km÷12km/時=2時間。
|
| 470 |
+
|
| 471 |
+
[CHUNK]
|
| 472 |
+
## 川を上る船の問題
|
| 473 |
+
ある船が70kmの川を上るのに7時間かかり、同じところを下るのに2時間20分かかりました。船の下りの速さと川の流速は船の静水時の速さの何倍かを求めます。下りの速さは30km/時、川の流速は船の静水時の速さの1/2倍。
|
| 474 |
+
|
| 475 |
+
[CHUNK]
|
| 476 |
+
## 川を上る船の速さの計算
|
| 477 |
+
70kmの道のりを2時間20分かけて下るので、70km÷7/3時間=70km×3/7時間で30km/時と計算。この船は70kmの道のりを上るのに7時間かけているので、70km÷7時間=10km/時。船が上に進むときの速さ=静水時の速さ―川の速さ、下に進むときの速さ=静水時の速さ+川の速さ。川の速さを①とすると、下に進むときの速さー上に進むときの速さ=2(川の速さ×2)となる。30km/時-10km/時=20km/時、20km/時=2(川の速さ×2)なので川の速さ=10km/時と分かります。静水時の速さは10km/時+10km/時=20km/時なので、川の速さ(10km/時)は静水時の速さ(20km/時)の1/2倍と求める。
|
| 478 |
+
|
| 479 |
+
[CHUNK]
|
| 480 |
+
## プールで泳ぐ速さの問題
|
| 481 |
+
1周280mのプールでAは流れと同じ向きに泳いで1周すると3分30秒かかり、流れと反対方向に泳いで1周すると4分40秒かかります。Aが静水時に進む速さは毎分何mですか。70m/分。
|
| 482 |
+
|
| 483 |
+
[CHUNK]
|
| 484 |
+
## プールで泳ぐ速さの計算
|
| 485 |
+
30秒=1/2分なので、3分30秒=3.5分となり、7/2分と書きかえる。280m÷7/2分=80m/分。40秒=2/3分となり、4分40秒=4と2/3分、14/3分と考える。280m÷14/3分=60m/分。流れと反対方向に進む速さ=静水時の速さーブールの速さ、流れと同じ方法に進む速さ=静水時の速さ+ブールの速さなので、プールの速さを1とすると、流れと同じ方法に進む速さー流れと反対方向に進む速さ=2(ブールの速さ×2)となる。80m/分-60m/分=2(ブールの速さ×2)なので2=20m/分。プールの速さは20m/分÷2=10m/分と計算。静水時の速さ=流れと反対方向に進む速さ+プールの速さで60m/分+10m/分=70m/分。
|
| 486 |
+
|
| 487 |
+
[CHUNK]
|
| 488 |
+
## 川を下流から上流へ走る船の問題
|
| 489 |
+
静水では毎時12kmで走る船Aと、毎時16kmで走る船Bがあります。川の下流から上流まで走るとき、Aは10時間、Bは6時間40分かかります。川の流れは毎時何kmですか。4km/時。
|
| 490 |
+
|
| 491 |
+
[CHUNK]
|
| 492 |
+
## 川を下流から上流へ走る船の速さの計算
|
| 493 |
+
40分=2/3時間と考えることができるので、上に上るときにかかる時間の比は10時間:6と2/3時間となり、10:20/3、両辺を×3して、30:20=3:2と求める。走る時間が短ければ短いほど、走る速さが���やいと考えることができる。静水時の速さが12km/時、16km/時と分かっているので、上るときの速さ=静水時の速さ―川の速さと仮定)なので、12km/時= 2+1、16km/時= 3+1と考える。3 - 2 = 1 がAとBの静水時の差の4km/時であることが分かります。16km/時(Bの静水時の速さ)-12km時の静水時の速さ)=4km/時。12km/時= 2 +1なので12km/時=8km/時+1となり、川の速さの1=4km/時。
|
| 494 |
+
|
| 495 |
+
# 速さ編ドリル - 流水算
|
| 496 |
+
|
| 497 |
+
[CHUNK]
|
| 498 |
+
## 流水算の基本:一定速度のプールでのAと浮き輪の出会い
|
| 499 |
+
一定速度で水が流れる1周240mのプールで、Aが流れに乗って泳ぎ始め、同時に浮き輪を流した。Aが3周したとき浮き輪に追いつき、流れに逆らうと1周12分かかる。プールの流速は毎分40mで、Aが流れに乗ると1周2分24秒。Aが3周進む時間とうき輪が2周する時間が同じであることに着目し、速さの比から流速を求める。
|
| 500 |
+
|
| 501 |
+
[CHUNK]
|
| 502 |
+
## プールの流速計算とAのラップタイム:流水算の応用
|
| 503 |
+
Aが3周したとき浮き輪に追いつき、流れに逆らうと1周12分かかる。プールの流速は毎分40m。Aが流れに乗ると1周2分24秒。Aが3周進む時間とうき輪が2周する時間が同じであることに着目し、Aの静水時速さを計算。流れに乗った時の速さ(静水時+流速)で240mを割ることでラップタイムを算出。
|
| 504 |
+
|
| 505 |
+
[CHUNK]
|
| 506 |
+
## 川下りと上り:流速変化時の船の速さ計算
|
| 507 |
+
ある船が105kmの川を下るのに7時間、元の場所に戻るのに流速が2倍になり11時間40分かかった。下りの速さは毎時15km、流速2倍時の上り速度は毎時9km、船の静水時速さは毎時13km。下りと上りの速さの差から流速を逆算し、静水時速さを求める。
|
| 508 |
+
|
| 509 |
+
[CHUNK]
|
| 510 |
+
## 流速変化時の船の速さ計算:上り下りの時間変化
|
| 511 |
+
ある船が105kmの川を下るのに7時間かかりました。元のところへもどるとき、流れの速さが2倍になったので、11時間40分かかりました。下りの速さは毎時15km。上りの速さは毎時9km。静水時の速さは毎時13km。上りと下りの速さの差から流速の変化を捉え、静水時の速さを算出する。
|
| 512 |
+
|
| 513 |
+
[CHUNK]
|
| 514 |
+
## 川の上り下り:流速が変化するケース
|
| 515 |
+
川に沿って27km離れたP町とQ町があり、ある船がQ町からP町へ上るのに4時間半、下る時は流速が2倍になり1時間で下った。船の静水時速さは毎時13km。上りと下りの時間からそれぞれの速さを計算し、流速の変化を考慮して静水時速さを求める。
|
| 516 |
+
|
| 517 |
+
[CHUNK]
|
| 518 |
+
## 流速変化時の静水時速さ:流水算の応用
|
| 519 |
+
川に沿って27km離れたP町とQ町があり、ある船がQ町からP町へ上るのに4時間半、下る時は流速が2倍になり1時間で下った。船の静水時速さは毎時13km。上りと下りの速さの差から流速を計算し、静水時速さを求める。流速の変化を線分図で視覚化し、計算を簡略化。
|
| 520 |
+
|
| 521 |
+
[CHUNK]
|
| 522 |
+
## 川下り:流速が2倍になった場合の上り時間
|
| 523 |
+
ある船が川を48km下るのに8時間、その後流速が2倍になり同じ距離を上るのに下りの2倍の時間がかかった。船の静水時速さは毎時5km。下りと上りの時間からそれぞれの速さを計算し、流速の変化を考慮して静水時速さを求める。
|
| 524 |
+
|
| 525 |
+
[CHUNK]
|
| 526 |
+
## 流速変化時の静水時速さ:上り下りの時間と速さ
|
| 527 |
+
ある船が川を48km下るのに8時間、その後流速が2倍になり同じ距離を上るのに下りの2倍の時間がかかった。船の静水時速さは毎時5km。下りと上りの速さの差から流速を計算し、静水時速さを求める。流速の変化を線分図で視覚化し、計算を簡略化。
|
| 528 |
+
|
| 529 |
+
[CHUNK]
|
| 530 |
+
## 川の上り下り:流速が4倍になるケース
|
| 531 |
+
川に沿って39km離れたP町とQ町があり、ある船がQ町からP町へ上るのに13時間、下る時は流速が上りの4倍になり3時間で下った。船の静水時速さは毎時5km。上りと下りの時間からそれぞれの速さを計算し、流速の変化を考慮して静水時速さを求める。
|
| 532 |
+
|
| 533 |
+
[CHUNK]
|
| 534 |
+
## 流速変化時の静水時速さ:流速4倍のケース
|
| 535 |
+
川に沿って39km離れたP町とQ町があり、ある船がQ町からP町へ上るのに13時間、下る時は流速が上りの4倍になり3時間で下った。船の静水時速さは毎時5km。上りと下りの速さの差から流速を計算し、静水時速さを求める。流速の変化を線分図で視覚化し、計算を簡略化。
|
| 536 |
+
|
| 537 |
+
[CHUNK]
|
| 538 |
+
## 川の往復:上り下りの時間と速さから静水時速さを求める
|
| 539 |
+
ある船が川上にあるP町とそこから45km離れたQ町を往復するのに、上りは9時間、下りは5時間かかった。上りの速さは毎時5km、下りの速さは毎時9km、船の静水時速さは毎時7km。上��と下りの時間からそれぞれの速さを計算し、静水時速さを求める。
|
| 540 |
+
|
| 541 |
+
[CHUNK]
|
| 542 |
+
## 上り下りの時間と速さ:静水時速さの計算
|
| 543 |
+
ある船が川上にあるP町とそこから45km離れたQ町を往復するのに、上りは9時間、下りは5時間かかった。上りの速さは毎時5km、下りの速さは毎時9km、船の静水時速さは毎時7km。上りと下りの速さの差から流速を計算し、静水時速さを求める。流速の変化を線分図で視覚化し、計算を簡略化。
|
| 544 |
+
|
| 545 |
+
[CHUNK]
|
| 546 |
+
## 川の往復:上り下りの時間から静水時速さを算出
|
| 547 |
+
ある船が川上にあるA町とそこから135km離れたB町の間を往復するのに、上りは15時間、下りは5時間かかった。上りの速さは毎時9km、下りの速さは毎時27km、船の静水時速さは毎時18km。上りと下りの時間からそれぞれの速さを計算し、静水時速さを求める。
|
| 548 |
+
|
| 549 |
+
[CHUNK]
|
| 550 |
+
## 上り下りの時間と速さ:静水時速さの計算と線分図
|
| 551 |
+
ある船が川上にあるA町とそこから135km離れたB町の間を往復するのに、上りは15時間、下りは5時間かかった。上りの速さは毎時9km、下りの速さは毎時27km、船の静水時速さは毎時18km。上りと下りの速さの差から流速を計算し、静水時速さを求める。線分図で視覚化。
|
| 552 |
+
|
| 553 |
+
[CHUNK]
|
| 554 |
+
## 川での出会い:静水時速さが異なる2隻の船
|
| 555 |
+
静水時速さが毎時4kmの船Aと毎時7kmの船Bが、55km離れた川上のP町と川下のQ町から向かい合って同時に進む。A、B両船は出発してから5時間後に出会う。2隻の船が近づく速さを計算し、出会うまでの時間を求める。
|
| 556 |
+
|
| 557 |
+
[CHUNK]
|
| 558 |
+
## 2隻の船の出会い:流水算と旅人算の融合
|
| 559 |
+
静水時速さが毎時4kmの船Aと毎時7kmの船Bが、55km離れた川上のP町と川下のQ町から向かい合って同時に進む。A、B両船は出発してから5時間後に出会う。2隻の船の相対速度を計算し、出会うまでの時間を求める。川の流れを考慮した旅人算。
|
| 560 |
+
|
| 561 |
+
[CHUNK]
|
| 562 |
+
## 川での出会い:出会い後の時間経過
|
| 563 |
+
静水時速さが毎時40kmの船Aと毎時20kmの船Bが、240kmの川を向かい合って進む。Bは出会った後、16時間たってAの出発地点に到着。川の流速は毎時8km。出会うまでの時間とBの速さから流速を計算。
|
| 564 |
+
|
| 565 |
+
[CHUNK]
|
| 566 |
+
## 出会い後の時間経過:流速の計算
|
| 567 |
+
静水時速さが毎時40kmの船Aと毎時20kmの船Bが、240kmの川を向かい合って進む。Bは出会った後、16時間たってAの出発地点に到着。川の流速は毎時8km。出会うまでの時間とBの速さから流速を計算。線分図で視覚化。
|
| 568 |
+
|
| 569 |
+
[CHUNK]
|
| 570 |
+
## 流水の速さが一定の川:上り下りの速さから流速と静水時速さを求める
|
| 571 |
+
流水の速さが一定の川で、静水時の速さが一定の船が往復する。上りの速さは毎時20km、下りの速さは毎時28km。川の流れの速さは毎時4km、船の静水時の速さは毎時24km。上りと下りの速さの差から流速を計算。
|
| 572 |
+
|
| 573 |
+
[CHUNK]
|
| 574 |
+
## 流水算:上り下りの速さから流速と静水時速さを計算
|
| 575 |
+
流水の速さが一定の川で、静水時の速さが一定の船が往復する。上りの速さは毎時20km、下りの速さは毎時28km。川の流れの速さは毎時4km、船の静水時の速さは毎時24km。線分図で視覚化。
|
| 576 |
+
|
| 577 |
+
# 下克上算数ドリル【速さ編】
|
| 578 |
+
|
| 579 |
+
[CHUNK]
|
| 580 |
+
## 流水算の基本:上りと下りの速さ
|
| 581 |
+
流水算では、川の流れがある状況での船の動きを扱います。船が川を上るとき、船の速度は静水時の速度から川の流れの速度を引いたものになります。逆に、船が川を下るときは、静水時の速度に川の流れの速度を加えたものが実際の速度となります。上りと下りの速さの違いを理解することが、流水算を解く上で重要です。問題文を読み解き、上りと下りの状況を正確に把握しましょう。
|
| 582 |
+
|
| 583 |
+
[CHUNK]
|
| 584 |
+
## 流水の速さが変化する場合の解法
|
| 585 |
+
川の流れの速さが一定でない場合、問題は複雑になります。例えば、川の流れの速さが変化する場合には、それぞれの区間における速さを個別に計算する必要があります。川の流れの速さが変化するタイミングや、それぞれの速度を正確に把握し、線分図やグラフを用いて視覚的に整理することが有効です。変化する速さを考慮して、上り下りの速さを計算しましょう。
|
| 586 |
+
|
| 587 |
+
[CHUNK]
|
| 588 |
+
## 流水の速さ変化と線分図の活用
|
| 589 |
+
川の流れの速さが変化する問題では、線分図が非常に役立ちます。線分図を用いることで、船の速度、川の流れの速度、そしてそれらの関係性を視覚的に捉えることができます。特に、川の流れの速さが変化する場合には、��れぞれの区間ごとに線分図を作成し、変化を明確にすることが重要です。線分図を活用して、問題の状況を整理しましょう。
|
| 590 |
+
|
| 591 |
+
[CHUNK]
|
| 592 |
+
## 静水時の速さの計算
|
| 593 |
+
流水算において、静水時の速さを求めることは基本的なスキルです。静水時の速さは、上りの速さと下りの速さの平均値として計算できます。例えば、上りの速さが毎時24km、下りの速さが毎時42kmの場合、静水時の速さは(24 + 42) / 2 = 33km/時となります。静水時の速さを正確に計算し、問題解決に役立てましょう。
|
| 594 |
+
|
| 595 |
+
[CHUNK]
|
| 596 |
+
## 静水時の速さ計算と応用
|
| 597 |
+
静水時の速さを求める問題では、上りと下りの速さの関係を理解することが重要です。上りの速さは「静水時の速さ - 川の流れの速さ」、下りの速さは「静水時の速さ + 川の流れの速さ」で表されます。これらの関係式を用いることで、静水時の速さや川の流れの速さを求めることができます。問題文から必要な情報を抽出し、関係式を適切に適用しましょう。
|
| 598 |
+
|
| 599 |
+
[CHUNK]
|
| 600 |
+
## プールでの流水算:反対方向への移動
|
| 601 |
+
プールでの流水算は、川での流水算と同様に考えることができます。プールの場合、流れの速さを考慮して、人が反対方向に泳ぐ場合を考えます。人が流れに逆らって泳ぐ場合、その人の速度は、静水時の速度から流れの速度を引いたものになります。反対方向への移動距離と時間を計算する際には、この速度を考慮する必要があります。
|
| 602 |
+
|
| 603 |
+
[CHUNK]
|
| 604 |
+
## プールでの流水算:追いつき問題
|
| 605 |
+
プールでの流水算では、追いつき問題も頻出です。例えば、A君が泳いでB君に追いつく場合、A君の速度からB君の速度を引いたものが、A君がB君に近づく相対速度となります。この相対速度を用いて、追いつくまでの時間を計算します。追いつき問題では、相対速度の概念を理解し、正確に計算することが重要です。
|
| 606 |
+
|
| 607 |
+
[CHUNK]
|
| 608 |
+
## 列車算の基本:トンネル通過問題
|
| 609 |
+
列車算では、列車がトンネルや橋を通過する問題を扱います。列車がトンネルを通過する場合、列車が完全にトンネルを通過するためには、列車の長さとトンネルの長さを合わせた距離を移動する必要があります。この距離を列車の速度で割ることで、通過にかかる時間を計算できます。図を描いて状況を整理し、正確に計算しましょう。
|
| 610 |
+
|
| 611 |
+
[CHUNK]
|
| 612 |
+
## 列車算:トンネル通過と橋の通過
|
| 613 |
+
列車算では、トンネル通過問題と橋の通過問題を組み合わせた問題も出題されます。これらの問題を解くためには、それぞれの状況における列車の移動距離を正確に把握する必要があります。トンネル通過時には「トンネルの長さ + 列車の長さ」、橋の通過時には「橋の長さ + 列車の長さ」が移動距離となります。それぞれの距離を正確に計算し、問題解決に役立てましょう。
|
| 614 |
+
|
| 615 |
+
[CHUNK]
|
| 616 |
+
## 列車算:すれ違い問題
|
| 617 |
+
列車算では、列車同士がすれ違う問題を扱います。列車がすれ違う場合、それぞれの列車の速度を足し合わせたものが、相対速度となります。すれ違いにかかる時間は、それぞれの列車の長さを足し合わせた距離を、相対速度で割ることで計算できます。すれ違い問題では、相対速度の概念を理解し、正確に計算することが重要です。
|
| 618 |
+
|
| 619 |
+
# 中学受験算数ドリル:速さ編
|
| 620 |
+
|
| 621 |
+
[CHUNK]
|
| 622 |
+
## 51. 追いつき追い越し問題の解法
|
| 623 |
+
長さ150m、時速90kmのA列車が、長さ120mのB列車に追いついてから追いこし終わるまでに30秒かかった。B列車の速さを求める。A列車の秒速は25m/秒。A列車がB列車を追い越すには、A列車の先頭がB列車の後尾にきてから、A列車の後尾がB列車の先頭に来るまで、つまりA列車がB列車よりも270m多く進む必要がある。A列車は30秒で270m多く進むので、A列車はB列車より9m/秒早い。B列車の秒速は25m/秒-9m/秒=16m/秒。B列車の時速は57.6km/時。
|
| 624 |
+
|
| 625 |
+
[CHUNK]
|
| 626 |
+
## 51. 追いつき追い越し問題の計算と単位換算
|
| 627 |
+
A列車は30秒で270mだけ多くB列車よりも早く走ることが分かりますので、270m÷30秒=9m/秒でA列車はB列車よりも9m/秒早いことが計算できますね。B列車の秒速は25m/秒-9m/秒=16m/秒となります。問題で聞かれていることは時速なので、16m/秒を時速になおしましょう。1分=60秒なので、16m/秒×60秒=960m/分。1時間=60分なので、960m/分×60分=57600m/時(57.6km/時)と求められました。
|
| 628 |
+
|
| 629 |
+
[CHUNK]
|
| 630 |
+
## 52. トンネル通過と追いつき追い越し問題
|
| 631 |
+
速さ19m/秒、長さ300mのA列車が、速さ16m/秒、長さ360mのB列車について。(1)A列車がトンネルを通り抜けるのに12分かかった。トンネルの長さは?(2)A列車がB列車に追いついてから追いこすまでの時間は?(1)13380m (2)220秒。A列車がトンネルに入るからぬけるまでに12分かかっています。12分=720秒なので列車Aは720秒で、19m/秒×720秒=13680m進む。13680mはトンネルの長さ+列車Aの長さなので、トンネルの長さは13680m-300m=13380m。
|
| 632 |
+
|
| 633 |
+
[CHUNK]
|
| 634 |
+
## 52. 追いつき追い越し計算と図解
|
| 635 |
+
秒速19mの列車Aが16m/秒の列車Bに追いついてから追いぬくまでに、660m近づく必要がある。2つの列車は19m/秒と16m/秒なので、同じ方向に進むと1秒間で3mずつ近づきます。列車Aは追いぬくまでに、660m近づく必要があり、2つの列車は1秒間で3mずつ近づきますので、660m÷3m/秒=220秒かかることが求められました!
|
| 636 |
+
|
| 637 |
+
[CHUNK]
|
| 638 |
+
## 53. 電車の通過時間計算
|
| 639 |
+
長さ500mの電車が秒速20mで走る。(1)踏切で待つ太郎くんの前を通過し始めてから通過し終わるまでの時間。(2)長さ300mの橋を渡り始めてから渡り終わるまでの時間。(1)25秒 (2)40秒。太郎くんの前を通り過ぎるまでにこの列車が何m進まないといけないのかを考えてきましょう。踏切を通り過ぎるまでに、この列車は500mだけ前に進む必要がある。
|
| 640 |
+
|
| 641 |
+
[CHUNK]
|
| 642 |
+
## 53. 通過距離と時間の計算
|
| 643 |
+
踏切を通り過ぎるまで列車の先頭が踏切の前を通ってから、列車の後尾が踏切を通るまで、となります。つまり、500mを20m/秒の速さで進む必要があるので500m÷20m/秒=25秒となります。今度は300mの橋を渡るときの様子を考えてみましょう。300mの橋を渡る列車の頭が橋を通ってから列車の後ろが橋を通り過ぎるまで、となります。図を書いていくと、この列車が300m+500m=800m進んでいく必要があります。
|
| 644 |
+
|
| 645 |
+
[CHUNK]
|
| 646 |
+
## 53. 橋の通過時間計算
|
| 647 |
+
800m÷20m/秒=40秒なので、列車が橋を通り過ぎるまでに40秒かかることが計算できました。
|
| 648 |
+
|
| 649 |
+
[CHUNK]
|
| 650 |
+
## 54. 時計の角度計算:7時51分
|
| 651 |
+
7時51分のとき、時計の長針と短針がつくる小さい方の角度の大きさは何度ですか。解答:70.5°。時計算の問題では長針(長い針) と短針 (短い針) がそれぞれ進む速さをとらえることが大切です。まずは長針の進む速さを考えてみましょう。長針は1時間 (60分)で360° (1回転) することが分かります。よって1分間で進む角度は360°÷60分=6°/分と計算できますね。
|
| 652 |
+
|
| 653 |
+
[CHUNK]
|
| 654 |
+
## 54. 長針と短針の角度計算
|
| 655 |
+
同じように短針の進む速さを考えてみましょう。短針は12時間(720分)で360° (1回転) しますので、1時間で進む角度は30℃。360+12時間=30/。よって1分間で進む角度は30°÷60分=0.5 /分と計算できます。7時51分のときの角度を計算する前に7時のときの長針と短針の角度を考えてみましょう。長針は12の位置にあり、ここを0°と考えると短針は30/時×7時間=210° 進んでいます。なので7時の時点では2つの針は210 はなれていることが分かります。
|
| 656 |
+
|
| 657 |
+
[CHUNK]
|
| 658 |
+
## 54. 時計の角度計算:7時51分の詳細
|
| 659 |
+
最後に、7時51分のときをイメージしてみます。長針と短針はそれぞれ7時の場所から51分分だけ進みます。短針は0.5°/分なので、0.5°/分×51分=25.5° 進みます。長針は6°/分なので、6°/分×51分=306° 進みます。時計の図を書きながら計算していくと、306 -25.5-210 =70.5" と求めることができました。
|
| 660 |
+
|
| 661 |
+
[CHUNK]
|
| 662 |
+
## 55. 長針と短針が重なる時刻
|
| 663 |
+
4時と5時の間で、時計の長針と短針が重なる時刻は4時何分ですか。解答:4時21分。今回は時計の長針と短針が重なる時間を求める問題でした。旅人算のようにはなれた場所にいる長針と短針が重なるときと考えてみましょう。時計の問題では2つの針のきょりは角度で表すことができます。つまり長針と短針が重なる時間=長針と短針の間が0になるということです。長針と短針がそれぞれ進む速さを計算してみます。
|
| 664 |
+
|
| 665 |
+
[CHUNK]
|
| 666 |
+
## 55. 長針と短針の速度と角度
|
| 667 |
+
長針は1時間(60分)で360° (1回転) するので6°/分。短針は12時間(720分)で360° (1回転)しますので0.5°/分。長針との進む速さについては、1つ前の問題から解いてみるとより分かりやすいですよ!4時から5時の間で2つの針が重なる時間なので、4時の時点での長針と短針の間の角度を計算しましょう。長針は12の位置にあり、ここを0と考えると短針は30°/時×4時間=120°進んでいます。
|
| 668 |
+
|
| 669 |
+
[CHUNK]
|
| 670 |
+
## 55. 長針と短針が重なる時間の計算
|
| 671 |
+
4時の時点で120° はなれていることが分かりま��。長針と短針が同じ方向に進むとき、2つの針は1分間で6° -0.5 =5.5° 近づきます。4時の時点で120° はなれているので、120°÷5.5°/分=21分と計算出来ました!
|
| 672 |
+
|
| 673 |
+
[CHUNK]
|
| 674 |
+
## 56. 長針と短針の角度が30度になる時刻
|
| 675 |
+
3時と4時の間で、長針と短針の作る角度が30度になる時はそれぞれ3時何分ですか。解答:3時10分、3時21分。今回の問題、実は答えが二つあることに気づきましたか? 問題をときながら気づいてもOKですし、問題文にも「それぞれ」 何時何分ですか?と聞いているのでよく読んでいれば分かりますね。3時から4時までの間ですので、まずは3時の時点を考えましょう。このとき、長針と短針は90℃はなれています。
|
| 676 |
+
|
| 677 |
+
[CHUNK]
|
| 678 |
+
## 56. 長針と短針の速度と角度変化
|
| 679 |
+
長針は6°/分。短針は0.5°/分の速さでしたね!この2つの針は1分間で6° -0.5 =5.5° 近づきます。線分図を書いてみて、長針と短針のはなれているきょりと速さを書きこんでみましょう。まず1回目に30° ができるときを考えてみましょう。線分図では長針と短針は90° はなれていますが、長針が短針よりも60° だけ多く進むと、2つの針の間の角度が30° になることがわかると思います。
|
| 680 |
+
|
| 681 |
+
[CHUNK]
|
| 682 |
+
## 56. 長針と短針の角度が30度になる時間の計算
|
| 683 |
+
よって60° ÷5.5°/分=10 ・/分=10分と計算することができます。次に2回目に30°ができるときについて考えてみましょう。2回目に30°になるときは、長針と短針を追いこして、30℃先に進んだときになります。(線分図にイメージ図を書いています。)つまり、3時の時点から長針は短針よりも120° 多く進めば2回目に30°を作ることができます。よって120° +5.5°/分=21 分と求めることができました。
|
| 684 |
+
|
| 685 |
+
[CHUNK]
|
| 686 |
+
## 57. 特殊な時計の針の重なり
|
| 687 |
+
ある特別な時計は長針が30秒で1周、短針が2分で1周する。長針と短針が反対方向をさして一直線になっている時、長針と短針が最初に重なるのは今から何秒後ですか。解答:20秒。この問題は普通の時計算と比べて、長針と短針の進む速さがちがいますので気をつけましょう!まずは問題文から長針と短針がそれぞれ進む速さを計算していきましょう。長針は30秒かけて1周しますので、30秒で360° 進むことが分かります。よって長針は360°÷30秒=12°/秒と計算できますね。
|
| 688 |
+
|
| 689 |
+
[CHUNK]
|
| 690 |
+
## 57. 特殊な時計の針の速度と角度
|
| 691 |
+
短針は2分で1周しますので、2分で360° 進むことになります。2分=120秒なので、360°÷120秒=3秒と分かりますね。長針と短針が反対方向を指しているので、2つの針の間の角度は360°の半分の180°となります。この2つの針は時計のように動きますので、2つの針は同じ方向に進みます。長針と短針を線分図になおして、出会うまでの時間を計算しましょう。
|
| 692 |
+
|
| 693 |
+
[CHUNK]
|
| 694 |
+
## 57. 特殊な時計の針が重なる時間の計算
|
| 695 |
+
線分図に、長針 (12°/秒) と短針 (3°/秒)そして180° を書きこみましょう。長針と短針は1秒で12° -3° 9° となりますので、9°/秒の速さで近づきます。面積図を書いて計算すると、180°÷9°/秒=20秒で20秒後に重なると分かります!
|
| 696 |
+
|
| 697 |
+
[CHUNK]
|
| 698 |
+
## 58. 長針と短針が直角になる時刻
|
| 699 |
+
10時と11時の間で、時計の長針と短針のつくる角が直角になることがあります。2回目に直角になるのは10時何分ですか。解答:10時38分。56 を解いてからこの解説を読むとよりわかりやすいと思います!まずは10時のときの2つの長針と短針のはなれている角度を計算しましょう。10時のとき、2つの針は60℃はなれています。ここから2つの針の角度が90°になるときを計算しましょう。
|
| 700 |
+
|
| 701 |
+
[CHUNK]
|
| 702 |
+
## 58. 長針と短針の速度と角度変化
|
| 703 |
+
長針は6°/分。短針は0.5°/分の速さになります。よって、この2つの針は1分間で6° -0.5 =5.5° 近づきます。線分図を書いてみて、長針と短針のはなれている角度と速さを書きこんでみましょう。まず1回目に90° ができるときを考えてみましょう。線分図では長針と短針は60° はなれていますが、長針が短針よりも30° だけ多く進むと、2つの針の間の角度が90° になることがわかると思います。
|
| 704 |
+
|
| 705 |
+
[CHUNK]
|
| 706 |
+
## 58. 長針と短針が直角になる時間の計算
|
| 707 |
+
よって30° ÷5.5°/分=5分と計算することができます。次に2回目に90° ができるときについて考えてみましょう。2回目に90°になるときは、長針と短針を追いこして、90°先に進んだときになります。(線分図にイメージ図を書いています。)つまり、10時の時���から長針は短針よりも210°多く進めば2回目に90°を作ることができます。よって210° ÷5.5°/分=38 分と求めることができました。
|
| 708 |
+
|
| 709 |
+
[CHUNK]
|
| 710 |
+
## 59. 長針と短針が重なる時刻と等しい角度になる時刻
|
| 711 |
+
時計の長針と短針が2時と3時の間でちょうど重なっている。このとき、以下の問いに答えなさい。(1) このときの時間は2時何分ですか。(2)その後長針と短針が文字ばんのめもりの6をはさんで等しい角度になっている時刻は2時何分ですか。解答:(1)2時10分 (2)2時46分。かなりむずかしい問題でした。特に (2) はよく MEMO と一緒によく読んでみてください!
|
| 712 |
+
|
| 713 |
+
[CHUNK]
|
| 714 |
+
## 59. 長針と短針の重なる時間の計算
|
| 715 |
+
(1) 2時のときの長針と短針のはなれている角度は60°です。また長針は6/分、短針は0.5分の速さで進みますのでこれを線分図に書きこんで、針が重なる時間を計算しましょう。MEMO のように線分図を書いて、2つの針が重なる時間を計算します。2つの針は1分間で6-0.5 =5.5 近づきます。重なる=2つの針の間の角度が0になるときなので、60° +5.5°/分=10分の時間に重なることが分かりました。(ここまではいつもの問題と同じでした。)
|
| 716 |
+
|
| 717 |
+
[CHUNK]
|
| 718 |
+
## 59. 等しい角度になる時刻の計算
|
| 719 |
+
(2) 問題文で書かれている状況について考えてみましょう。6のめもりは時計の真ん中にあるので、時計を半分にしたときに、長針と短針の2つの角度(MEMOの青と紫)が同じになる時間を求めることができればOKです。時間を求めたいので口分として計算していきます。短針の角度は2時の時点で120 でどんどん小さくなるので、120-0.5/分×口分と考えることができます。同じように長針の角度は2時の時点では0なので、6/分×分から180引いた、6/分×分~180℃になります!
|
| 720 |
+
|
| 721 |
+
[CHUNK]
|
| 722 |
+
## 59. 等しい角度になる時刻の計算:詳細
|
| 723 |
+
短針の角度=長針の角度になる時間を知りたいので、120-0.5/分×口分=6/分×口分-180 を計算すれば答えが出せます。線分図を書いてこの式を表してみました。線分図を書いて「差」に注目してみましょう。すると、180°-0.5/分×口分=6°/分×口分-120°と式をかき変えることができます。この式を工夫して整理してみます。
|
| 724 |
+
300°=6°/分×口分+0.5分×分
|
| 725 |
+
180°=6°/分×口分-120° +0.5分×口分
|
| 726 |
+
२
|
| 727 |
+
300(6/分+0.5/分)×分
|
| 728 |
+
?
|
| 729 |
+
両辺に+120
|
| 730 |
+
よって300÷6.5°/分=46分と計算することができました。
|
| 731 |
+
|
| 732 |
+
[CHUNK]
|
| 733 |
+
## 60. 長針と短針が作る角度と追い越し後の角度
|
| 734 |
+
時計の針が5時10分をさしています。このとき、以下の問いに答えなさい。(1) 長針と短針が作る角度で小さいものは何度ですか。(2) 長針が短針を追いこしてから作る角度が90度となるのは5時何分ですか。解答:(1) 95° (2)5時43分。5時10分のときの長針と短針の2つの角度を求めるために、まずは5時のときの角度を計算しましょう。(長針は6/分、短針は0.5/分の速さで進みます。)
|
| 735 |
+
|
| 736 |
+
[CHUNK]
|
| 737 |
+
## 60. 長針と短針の角度計算
|
| 738 |
+
5時のとき、長針は12の位置にいるので角度を0°と考えてみましょう。すると、短針は5の位置にあるので30°/時×5時間=150° はなれていることが分かります。5時10分の角度を求めたいので、5時から10分で長針と短針が進む角度を計算しましょう。長針は6°/分×10分=60°、短針は0.5/分×10分=5 なので、時計の図を書いて2つの針の間の角度を、150°+5 -60° =95° と解いていきましょう。
|
| 739 |
+
|
| 740 |
+
[CHUNK]
|
| 741 |
+
## 60. 追い越し後の角度計算
|
| 742 |
+
線分図を書いて、問題文で聞かれている2つの針が90°になるときを考えてみましょう。長針は6°/分。短針は0.5/分の速さで進むので、2つの針は1分間で6° -0.5 =5.5° 近づきます。線分図を書いてみて、長針と短針のはなれている角度 (150°) と速さを書いていきます。長針が短針を追いこして90°になるとき、長針は短針よりも240° (はなれている 150°+90°) 多く進む必要があります。2つの針は1分間で5.5 近づくので、長針が短針を追いこして90°240°÷5.5°/分=43分となり、5時43分と求められることができます。
|
| 743 |
+
|
| 744 |
+
# 下克上算数ドリル【速さ編】
|
| 745 |
+
|
| 746 |
+
[CHUNK]
|
| 747 |
+
## 面積図の活用:四角形の性質で問題を視覚的に解く
|
| 748 |
+
面積図は、算数の解法の一つで、四角形の面積(たて×横)の性質を利用する。速さの問題では、縦を速さ、横を時間、面積を進んだ距離として表現する。例えば、1分で30m進む人が5分で進む距離は、30m/分×5分=150mで計算できる。面積図はケアレスミスを減らし、問題を正確に解くために有効。特に算数が苦手な場合や学習初期段階では、図を描くことで見直しが容易になり、理解が深まる。速さ、時間、距離の関係を視覚的に捉え、複雑な問題への対応力を高める。面積図の利用は、問題解決の基礎を固める上で重要。
|
| 749 |
+
|
| 750 |
+
[CHUNK]
|
| 751 |
+
## 面積図の種類と活用:速さ、割合、個数問題への応用
|
| 752 |
+
面積図は、速さだけでなく、割合や個数に関する問題にも応用可能。速さの問題では、1分あたりの進む距離と時間を掛けて総距離を求める。例えば、1分で60m進む人が5分で進む距離は、60m/分×5分=300mとなる。割合の問題では、全体を四角形で表し、その一部を塗りつぶすことで割合を視覚化する。個数の問題では、1個あたりの価格と個数を掛けて合計金額を求める。例えば、1個200円の商品を3個購入した場合、200円/個×3個=600円となる。面積図は、問題の種類に応じて柔軟に活用できる。
|
| 753 |
+
|
| 754 |
+
[CHUNK]
|
| 755 |
+
## 線分図の基本:問題文の情報を整理し視覚化
|
| 756 |
+
線分図は、問題文中の数値や数量関係を線で表すことで、問題を解きやすくする算数の解法。例えば、600円の商品と300円の商品を購入した場合、それぞれの金額を線で表現し、合計金額を視覚的に示す。線分図は、比の性質を理解する上でも役立ち、問題解決の糸口を見つけやすくする。面積図と並び、算数の問題で頻繁に使用される。線分図は、問題文の情報を整理し、数量関係を明確にするための強力なツール。
|
| 757 |
+
|
| 758 |
+
[CHUNK]
|
| 759 |
+
## 線分図の種類と活用:和差算、旅人算への応用
|
| 760 |
+
線分図は、和差算や旅人算など、様々な問題に応用可能。和差算では、りんごとバナナの値段を線で表し、それぞれの値段と合計金額を比較することで、それぞれの値段を求める。旅人算では、2人が同じ方向または反対方向に進む状況を線で表し、それぞれの進む距離や速さの関係を視覚的に捉える。特に、2人が出会うまでの時間や距離を求める問題で有効。線分図は、複雑な状況を整理し、問題解決を容易にする。
|
| 761 |
+
|
| 762 |
+
[CHUNK]
|
| 763 |
+
## 比の基本:数量や速さの比較
|
| 764 |
+
比は、2つの数量を比較するための表現方法で、○:□のように表す。例えば、2cmと4cmの長さを比較する場合、2:4と表現できる。比は、前項と後項で構成され、それぞれ比較される数量を表す。比を用いることで、異なる単位の数量を相対的に比較することが可能。比は、算数における重要な概念であり、様々な問題解決に役立つ。
|
| 765 |
+
|
| 766 |
+
[CHUNK]
|
| 767 |
+
## 比と線分図:視覚的な表現で理解を深める
|
| 768 |
+
比は、線分図と組み合わせて使用することで、視覚的に理解を深めることができる。例えば、500円と1000円の金額を比較する場合、それぞれの金額を線分の長さで表し、比率を視覚的に示す。線分図を用いることで、比の関係を直感的に捉えることが可能。比の値を線分図上に表現する際は、実際の数量と区別するために記号を用いることが重要。比と線分図の組み合わせは、問題解決の精度を高める。
|
| 769 |
+
|
| 770 |
+
[CHUNK]
|
| 771 |
+
## 逆比の理解:面積一定条件下の縦横比
|
| 772 |
+
逆比は、面積が一定の場合に、縦と横の長さの比が逆になる関係。例えば、面積が15の長方形において、縦の長さが3の場合、横の長さは5となる。一方、縦の長さが5の場合、横の長さは3となる。このとき、縦の長さの比が3:5であるのに対し、横の長さの比は5:3となる。このように、ある条件が一定の場合に、2つの要素の比が逆になる関係を逆比と呼ぶ。逆比の理解は、複雑な問題を解く上で重要。
|
| 773 |
+
|
| 774 |
+
[CHUNK]
|
| 775 |
+
## 逆比の応用:速さの問題への適用
|
| 776 |
+
逆比は、速さの問題に応用できる。例えば、同じ距離を進む場合、速さの比と時間の比は逆比の関係になる。時速3kmの人が15km進むのに5時間かかるのに対し、時速5kmの人が15km進むのに3時間かかる。このとき、速さの比が3:5であるのに対し、時間の比は5:3となる。逆比の関係を理解することで、速さの問題を効率的に解くことができる。面積図を丁寧に書くことで、逆比の関係を視覚的に捉えることも可能。
|
| 777 |
+
|
| 778 |
+
[CHUNK]
|
| 779 |
+
## 仮定法の活用:問題解決の糸口を見つける
|
| 780 |
+
仮定法は、問題を解く際に、ある条件を仮定して考える方法。例えば、「もし○○が□□だったら〜」というように、具体的な数値を仮定して問題を解き進める。AくんがBくんより3倍速く走る場合、Aくんが15分で走る距離をBくんが何分で走れるかを考える際、Aくんの速さを①/分と仮定する��、Bくんの速さは③/分となる。仮定法を用いることで、複雑な問題も簡略化し、解決の糸口を見つけやすくなる。
|
| 781 |
+
|
| 782 |
+
[CHUNK]
|
| 783 |
+
## 仮定法の応用:仕事算への展開
|
| 784 |
+
仮定法は、仕事算にも応用できる。例えば、ある仕事量を1人が何時間で終わらせるかを考える際、1時間あたりの仕事量を①と仮定する。仮定法を用いることで、複数人が共同で仕事をする場合や、仕事の進捗状況を把握する際に役立つ。仕事算では、全体の仕事量を1と仮定し、各人の仕事量を分数で表すことが多い。仮定法は、問題解決の柔軟性を高め、様々な算数問題に対応できる。
|
| 785 |
+
|
| 786 |
+
[CHUNK]
|
| 787 |
+
## 流水算の基本:川の流れを考慮した速さの計算
|
| 788 |
+
流水算は、川の流れがある状況下での船の速さを扱う問題。川の流れに逆らって進む場合、船の速さは静水時の速さから川の流れの速さを引いたものになる。一方、川の流れに乗って進む場合、船の速さは静水時の速さに川の流れの速さを足したものになる。流水算では、川の流れの向きと速さを考慮して、船の実際の速さを計算する必要がある。流水算は、速さの問題の中でも特に複雑で、注意が必要。
|
| 789 |
+
|
| 790 |
+
[CHUNK]
|
| 791 |
+
## 流水算の解法:線分図の活用
|
| 792 |
+
流水算を解く上で、線分図の活用が有効。川上に向かう場合と川下に向かう場合で、船の速さが異なるため、それぞれの状況を線分図で表現する。例えば、静水時の速さが75m/分、川の速さが15m/分の場合、川上に向かう速さは60m/分、川下に向かう速さは90m/分となる。線分図を用いることで、速さの関係を視覚的に捉え、計算ミスを防ぐことができる。流水算は、線分図を丁寧に書くことが重要。
|
| 793 |
+
|
| 794 |
+
[CHUNK]
|
| 795 |
+
## 列車算の解法:問題文の正確な理解
|
| 796 |
+
列車算は、列車が通過する時間や距離を計算する問題。問題文を注意深く読み、何が問われているかを正確に理解することが重要。列車が黄色い小人の前を通過する状況を例に、通過開始から通過終了までの時間や、列車が見えなくなってから出始めるまでの時間など、問われる内容によって計算方法が異なる。問題文の理解不足は、誤答につながるため、注意が必要。
|
| 797 |
+
|
| 798 |
+
[CHUNK]
|
| 799 |
+
## 列車算の応用:すれ違いと追い越しの計算
|
| 800 |
+
列車算では、列車同士がすれ違う場合や追い越す場合の計算も重要。列車がすれ違う場合、それぞれの列車の長さの合計が、すれ違いに要する距離となる。一方、列車が追い越す場合、速い列車が遅い列車よりもどれだけ長く進む必要があるかを考慮する。列車算では、列車の長さや速さ、進行方向などを正確に把握し、適切な計算を行う必要がある。図を描くことで、状況を整理しやすくなる。
|
| 801 |
+
|
| 802 |
+
[CHUNK]
|
| 803 |
+
## 時計算の基本:長針と短針の速さの理解
|
| 804 |
+
時計算は、時計の針の角度や位置関係を扱う問題。長針と短針は、それぞれ異なる速さで回転しており、長針は1時間で360度、短針は12時間で360度回転する。時計算では、長針と短針のそれぞれの速さを正確に把握し、角度や時間の計算を行う必要がある。長針と短針の速さの差を理解することが、時計算の問題を解く上で重要。
|
| 805 |
+
|
| 806 |
+
[CHUNK]
|
| 807 |
+
## 時計算の応用:角度と時間の計算
|
| 808 |
+
時計算では、特定の時刻における長針と短針の角度や、針が重なる時刻などを計算する。例えば、4時の場合、長針は12の位置、短針は4の位置にあり、その間の角度は120度となる。長針と短針が重なる時刻を求めるには、それぞれの針の速さの差を利用する。時計算は、線分図を用いることで、針の動きを視覚的に捉え、問題を解きやすくなる。時計算は、算数の応用問題として頻出。
|
V1.7.1/knowledge/ORIGINAL/算数/四天王寺対策算数.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
V1.7.1/knowledge/ORIGINAL/算数/算数出る順文章題.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
V1.7.1/knowledge/japanese.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
V1.7.1/knowledge/math.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
V1.7.1/knowledge/science.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
V1.7.1/knowledge/social.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
V1.7.1/requirements.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Web Framework
|
| 2 |
+
fastapi==0.115.6
|
| 3 |
+
uvicorn[standard]==0.34.0
|
| 4 |
+
|
| 5 |
+
# HTTP Client
|
| 6 |
+
httpx==0.28.1
|
| 7 |
+
|
| 8 |
+
# Google Gemini API (新SDK: google-genai - thinking_budget対応)
|
| 9 |
+
google-genai>=1.0.0
|
| 10 |
+
|
| 11 |
+
# Environment Variables
|
| 12 |
+
python-dotenv==1.0.1
|
| 13 |
+
|
| 14 |
+
# JSON Processing
|
| 15 |
+
orjson==3.10.12
|
| 16 |
+
|
| 17 |
+
# Password Hashing
|
| 18 |
+
bcrypt==4.2.1
|
V1.7.1/src/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# 超天才クイズ v3
|
V1.7.1/src/models/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# src/models/__init__.py
|
V1.7.1/src/models/gemini_schemas.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Gemini Structured Output Schemas
|
| 3 |
+
|
| 4 |
+
Pydanticモデルを使用してGeminiのJSON出力スキーマを定義。
|
| 5 |
+
response_json_schemaパラメータで使用することで、構造化出力を保証する。
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from typing import List, Optional, Literal, Dict, Any
|
| 9 |
+
from pydantic import BaseModel, Field
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# =============================================================================
|
| 13 |
+
# Helper Functions (スキーマ生成ヘルパー)
|
| 14 |
+
# =============================================================================
|
| 15 |
+
|
| 16 |
+
def get_list_schema(item_schema: type) -> Dict[str, Any]:
|
| 17 |
+
"""
|
| 18 |
+
Pydanticモデルのリストスキーマを生成
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
item_schema: リスト要素のPydanticモデルクラス
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
JSON Schema (配列型)
|
| 25 |
+
"""
|
| 26 |
+
item_json_schema = item_schema.model_json_schema()
|
| 27 |
+
# $defsを展開してフラット化
|
| 28 |
+
defs = item_json_schema.pop("$defs", {})
|
| 29 |
+
|
| 30 |
+
return {
|
| 31 |
+
"type": "array",
|
| 32 |
+
"items": item_json_schema,
|
| 33 |
+
"$defs": defs
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# =============================================================================
|
| 38 |
+
# Summary Schema (サマリー生成用)
|
| 39 |
+
# =============================================================================
|
| 40 |
+
|
| 41 |
+
class SummarySchema(BaseModel):
|
| 42 |
+
"""問題群のサマリー生成スキーマ"""
|
| 43 |
+
keywords: List[str] = Field(
|
| 44 |
+
description="問題の具体的なキーワード(答え、題材)のリスト"
|
| 45 |
+
)
|
| 46 |
+
topics: List[str] = Field(
|
| 47 |
+
description="問題タイプ + 具体的な題材を組み合わせたトピックのリスト"
|
| 48 |
+
)
|
| 49 |
+
summary: str = Field(
|
| 50 |
+
description="問題群の概要(50文字以内)"
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# =============================================================================
|
| 55 |
+
# Question Schema (問題生成用)
|
| 56 |
+
# =============================================================================
|
| 57 |
+
|
| 58 |
+
class QuestionSchema(BaseModel):
|
| 59 |
+
"""生成される問題のスキーマ"""
|
| 60 |
+
subject: str = Field(
|
| 61 |
+
description="教科コード (jp, math, sci, soc)"
|
| 62 |
+
)
|
| 63 |
+
category: str = Field(
|
| 64 |
+
description="カテゴリコード (例: JP01, MA05)"
|
| 65 |
+
)
|
| 66 |
+
difficulty: Literal["easy", "medium", "hard"] = Field(
|
| 67 |
+
description="難易度"
|
| 68 |
+
)
|
| 69 |
+
question: str = Field(
|
| 70 |
+
description="問題文"
|
| 71 |
+
)
|
| 72 |
+
choices: List[str] = Field(
|
| 73 |
+
description="選択肢のリスト(4つ)",
|
| 74 |
+
min_length=4,
|
| 75 |
+
max_length=4
|
| 76 |
+
)
|
| 77 |
+
correct_answer: int = Field(
|
| 78 |
+
description="正解の選択肢インデックス(0-3)",
|
| 79 |
+
ge=0,
|
| 80 |
+
le=3
|
| 81 |
+
)
|
| 82 |
+
explanation: str = Field(
|
| 83 |
+
description="解説文"
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# QuestionsListSchemaはget_list_schema(QuestionSchema)で代替
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# =============================================================================
|
| 91 |
+
# Evaluation Schema (評価生成用)
|
| 92 |
+
# =============================================================================
|
| 93 |
+
|
| 94 |
+
class SubjectEvaluationSchema(BaseModel):
|
| 95 |
+
"""教科別評価スキーマ"""
|
| 96 |
+
advice: str = Field(
|
| 97 |
+
description="教科別アドバイス(2-3文)"
|
| 98 |
+
)
|
| 99 |
+
strengths: List[str] = Field(
|
| 100 |
+
description="強み・得意分野のリスト",
|
| 101 |
+
min_length=1,
|
| 102 |
+
max_length=5
|
| 103 |
+
)
|
| 104 |
+
weaknesses: List[str] = Field(
|
| 105 |
+
description="改善点・苦手分野のリスト",
|
| 106 |
+
min_length=1,
|
| 107 |
+
max_length=5
|
| 108 |
+
)
|
| 109 |
+
recommended_topics: List[str] = Field(
|
| 110 |
+
description="おすすめの学習トピックのリスト",
|
| 111 |
+
min_length=1,
|
| 112 |
+
max_length=5
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
class OverallEvaluationSchema(BaseModel):
|
| 117 |
+
"""全体評価スキーマ"""
|
| 118 |
+
overall_advice: str = Field(
|
| 119 |
+
description="全体的なアドバイス(2-3文)"
|
| 120 |
+
)
|
| 121 |
+
strengths: List[str] = Field(
|
| 122 |
+
description="教科横断的な強みのリスト"
|
| 123 |
+
)
|
| 124 |
+
weaknesses: List[str] = Field(
|
| 125 |
+
description="教科横断的な改善点のリスト"
|
| 126 |
+
)
|
| 127 |
+
next_steps: List[str] = Field(
|
| 128 |
+
description="次のステップのリスト"
|
| 129 |
+
)
|
| 130 |
+
balance_comment: str = Field(
|
| 131 |
+
description="教科バランスと全体的な傾向についてのコメント"
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class BatchEvaluationSchema(BaseModel):
|
| 136 |
+
"""複数教科一括評価スキーマ"""
|
| 137 |
+
subject_evaluations: Dict[str, SubjectEvaluationSchema] = Field(
|
| 138 |
+
description="教科別評価 {subject_code: SubjectEvaluationSchema}"
|
| 139 |
+
)
|
| 140 |
+
overall_evaluation: OverallEvaluationSchema = Field(
|
| 141 |
+
description="全体評価"
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
# =============================================================================
|
| 146 |
+
# Validation Schema (検証用)
|
| 147 |
+
# =============================================================================
|
| 148 |
+
|
| 149 |
+
class ValidationResultSchema(BaseModel):
|
| 150 |
+
"""問題検証結果スキーマ"""
|
| 151 |
+
subject: str = Field(
|
| 152 |
+
description="教科コード"
|
| 153 |
+
)
|
| 154 |
+
index: int = Field(
|
| 155 |
+
description="問題のインデックス"
|
| 156 |
+
)
|
| 157 |
+
modified: bool = Field(
|
| 158 |
+
description="修正されたかどうか"
|
| 159 |
+
)
|
| 160 |
+
modification_reason: Optional[str] = Field(
|
| 161 |
+
default=None,
|
| 162 |
+
description="修正理由(修正された場合のみ)"
|
| 163 |
+
)
|
| 164 |
+
question: QuestionSchema = Field(
|
| 165 |
+
description="検証後の問題(修正された場合は修正後)"
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
# ValidationResultsListSchemaはget_list_schema(ValidationResultSchema)で代替
|
V1.7.1/src/prompts/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Prompts
|
V1.7.1/src/prompts/evaluation_prompts.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
評価生成プロンプト定義
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from typing import Dict, List
|
| 7 |
+
|
| 8 |
+
SUBJECT_NAMES = {
|
| 9 |
+
"jp": "国語",
|
| 10 |
+
"math": "算数",
|
| 11 |
+
"sci": "理科",
|
| 12 |
+
"soc": "社会"
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def get_evaluation_prompt(
|
| 17 |
+
subject: str,
|
| 18 |
+
results: List[Dict],
|
| 19 |
+
statistics: Dict = None
|
| 20 |
+
) -> str:
|
| 21 |
+
"""
|
| 22 |
+
評価生成プロンプトを構築
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
subject: 教科コード
|
| 26 |
+
results: 解答結果リスト
|
| 27 |
+
statistics: ユーザー統計
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
完成したプロンプト
|
| 31 |
+
"""
|
| 32 |
+
subject_name = SUBJECT_NAMES.get(subject, subject)
|
| 33 |
+
|
| 34 |
+
# 結果サマリー計算
|
| 35 |
+
total = len(results)
|
| 36 |
+
correct = sum(1 for r in results if r.get("is_correct", False))
|
| 37 |
+
accuracy = (correct / total * 100) if total > 0 else 0
|
| 38 |
+
|
| 39 |
+
# カテゴリ別集計
|
| 40 |
+
by_category = {}
|
| 41 |
+
for r in results:
|
| 42 |
+
cat = r.get("category", "unknown")
|
| 43 |
+
if cat not in by_category:
|
| 44 |
+
by_category[cat] = {"total": 0, "correct": 0}
|
| 45 |
+
by_category[cat]["total"] += 1
|
| 46 |
+
if r.get("is_correct", False):
|
| 47 |
+
by_category[cat]["correct"] += 1
|
| 48 |
+
|
| 49 |
+
prompt = f"""あなたは中学受験の{subject_name}指導の専門家です。
|
| 50 |
+
以下の解答結果を分析し、学習アドバイスを生成してください。
|
| 51 |
+
|
| 52 |
+
## 今回の結果
|
| 53 |
+
- 教科: {subject_name}
|
| 54 |
+
- 問題数: {total}問
|
| 55 |
+
- 正解数: {correct}問
|
| 56 |
+
- 正答率: {accuracy:.1f}%
|
| 57 |
+
|
| 58 |
+
## カテゴリ別結果
|
| 59 |
+
{json.dumps(by_category, ensure_ascii=False, indent=2)}
|
| 60 |
+
|
| 61 |
+
## 解答詳細
|
| 62 |
+
{json.dumps(results[:10], ensure_ascii=False, indent=2)}
|
| 63 |
+
(最大10問まで表示)
|
| 64 |
+
|
| 65 |
+
## 出力形式
|
| 66 |
+
以下のJSON形式で出力してください。
|
| 67 |
+
|
| 68 |
+
```json
|
| 69 |
+
{{
|
| 70 |
+
"advice": "総合的なアドバイス(2-3文)",
|
| 71 |
+
"strengths": ["強み・得意分野1", "強み・得意分野2"],
|
| 72 |
+
"weaknesses": ["改善点・苦手分野1", "改善点・苦手分野2"],
|
| 73 |
+
"recommended_topics": ["おすすめの学習トピック1", "おすすめの学習トピック2"]
|
| 74 |
+
}}
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
**注意**:
|
| 78 |
+
- 具体的で建設的なアドバイスを心がけてください
|
| 79 |
+
- 小学生に分かりやすい表現を使ってください
|
| 80 |
+
- 励ましの言葉を含めてください
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
# 累積統計があれば追加
|
| 84 |
+
if statistics:
|
| 85 |
+
prompt += f"""
|
| 86 |
+
|
| 87 |
+
## 累積統計(参考)
|
| 88 |
+
{json.dumps(statistics, ensure_ascii=False, indent=2)}
|
| 89 |
+
"""
|
| 90 |
+
|
| 91 |
+
return prompt
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def get_batch_evaluation_prompt(
|
| 95 |
+
results_by_subject: Dict[str, List[Dict]],
|
| 96 |
+
statistics: Dict = None
|
| 97 |
+
) -> str:
|
| 98 |
+
"""
|
| 99 |
+
複数教科の評価を一括生成するプロンプト
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
results_by_subject: {"jp": [...], "math": [...], ...} 形式の教科別結果
|
| 103 |
+
statistics: ユーザー統計情報(教科別)
|
| 104 |
+
|
| 105 |
+
Returns:
|
| 106 |
+
評価生成用プロンプト文字列
|
| 107 |
+
"""
|
| 108 |
+
# 全教科のサマリーを構築
|
| 109 |
+
subject_summaries = []
|
| 110 |
+
|
| 111 |
+
for subject, results in results_by_subject.items():
|
| 112 |
+
subject_name = SUBJECT_NAMES.get(subject, subject)
|
| 113 |
+
|
| 114 |
+
# 結果サマリー計算
|
| 115 |
+
total = len(results)
|
| 116 |
+
correct = sum(1 for r in results if r.get("is_correct", False))
|
| 117 |
+
accuracy = (correct / total * 100) if total > 0 else 0
|
| 118 |
+
|
| 119 |
+
# カテゴリ別集計
|
| 120 |
+
by_category = {}
|
| 121 |
+
for r in results:
|
| 122 |
+
cat = r.get("category", "unknown")
|
| 123 |
+
if cat not in by_category:
|
| 124 |
+
by_category[cat] = {"total": 0, "correct": 0}
|
| 125 |
+
by_category[cat]["total"] += 1
|
| 126 |
+
if r.get("is_correct", False):
|
| 127 |
+
by_category[cat]["correct"] += 1
|
| 128 |
+
|
| 129 |
+
subject_summaries.append({
|
| 130 |
+
"subject": subject,
|
| 131 |
+
"subject_name": subject_name,
|
| 132 |
+
"total": total,
|
| 133 |
+
"correct": correct,
|
| 134 |
+
"accuracy": accuracy,
|
| 135 |
+
"by_category": by_category,
|
| 136 |
+
"details": results[:10] # 最大10問まで
|
| 137 |
+
})
|
| 138 |
+
|
| 139 |
+
# プロンプト構築
|
| 140 |
+
prompt = f"""あなたは中学受験の指導専門家です。
|
| 141 |
+
以下の複数教科の解答結果を分析し、**教科別評価と全体評価**を生成してください。
|
| 142 |
+
|
| 143 |
+
## 受験した教科
|
| 144 |
+
{", ".join([s["subject_name"] for s in subject_summaries])}
|
| 145 |
+
|
| 146 |
+
## 教科別結果詳細
|
| 147 |
+
"""
|
| 148 |
+
|
| 149 |
+
# 各教科の詳細を追加
|
| 150 |
+
for summary in subject_summaries:
|
| 151 |
+
prompt += f"""
|
| 152 |
+
### {summary["subject_name"]}
|
| 153 |
+
- 問題数: {summary["total"]}問
|
| 154 |
+
- 正解数: {summary["correct"]}問
|
| 155 |
+
- 正答率: {summary["accuracy"]:.1f}%
|
| 156 |
+
|
| 157 |
+
#### カテゴリ別結果
|
| 158 |
+
{json.dumps(summary["by_category"], ensure_ascii=False, indent=2)}
|
| 159 |
+
|
| 160 |
+
#### 解答詳細(最大10問)
|
| 161 |
+
{json.dumps(summary["details"], ensure_ascii=False, indent=2)}
|
| 162 |
+
"""
|
| 163 |
+
|
| 164 |
+
# 累積統計があれば追加
|
| 165 |
+
if statistics:
|
| 166 |
+
prompt += f"""
|
| 167 |
+
## 累積統計(参考)
|
| 168 |
+
{json.dumps(statistics, ensure_ascii=False, indent=2)}
|
| 169 |
+
"""
|
| 170 |
+
|
| 171 |
+
# 出力形式を指定
|
| 172 |
+
prompt += """
|
| 173 |
+
## 出力形式
|
| 174 |
+
以下のJSON形式で、**教科別評価**と**全体評価**を生成してください。
|
| 175 |
+
|
| 176 |
+
```json
|
| 177 |
+
{
|
| 178 |
+
"subject_evaluations": {
|
| 179 |
+
"""
|
| 180 |
+
|
| 181 |
+
# 各教科の出力フォーマットを追加
|
| 182 |
+
for i, summary in enumerate(subject_summaries):
|
| 183 |
+
comma = "," if i < len(subject_summaries) - 1 else ""
|
| 184 |
+
prompt += f""" "{summary['subject']}": {{
|
| 185 |
+
"advice": "<subject-specific advice in 2-3 sentences>",
|
| 186 |
+
"strengths": ["<strength1>", "<strength2>"],
|
| 187 |
+
"weaknesses": ["<weakness1>", "<weakness2>"],
|
| 188 |
+
"recommended_topics": ["<topic1>", "<topic2>"]
|
| 189 |
+
}}{comma}
|
| 190 |
+
"""
|
| 191 |
+
|
| 192 |
+
prompt += """ },
|
| 193 |
+
"overall_evaluation": {
|
| 194 |
+
"overall_advice": "<comprehensive advice across all subjects in 2-3 sentences>",
|
| 195 |
+
"strengths": ["<cross-subject strength1>", "<strength2>"],
|
| 196 |
+
"weaknesses": ["<cross-subject weakness1>", "<weakness2>"],
|
| 197 |
+
"next_steps": ["<next learning step1>", "<step2>"],
|
| 198 |
+
"balance_comment": "<comment on subject balance and overall trends>"
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
**注意事項**:
|
| 204 |
+
- 各教科の特性を踏まえた具体的なアドバイスを生成してください
|
| 205 |
+
- 小学生に分かりやすい表現を使ってください
|
| 206 |
+
- 励ましと前向きな指摘を心がけてください
|
| 207 |
+
- 全体評価では、教科間のバランスや全体的な学習傾向も分析してください
|
| 208 |
+
"""
|
| 209 |
+
|
| 210 |
+
return prompt
|
V1.7.1/src/prompts/question_prompts.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
問題生成プロンプト定義
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from typing import Dict, List, Optional
|
| 7 |
+
|
| 8 |
+
# ジャンル定義(詳細分析用カテゴリID)
|
| 9 |
+
GENRES = {
|
| 10 |
+
"jp": {
|
| 11 |
+
"JP01": "漢字・語彙(読み書き、四字熟語、慣用句、ことわざ)",
|
| 12 |
+
"JP02": "文法・言葉のきまり(品詞、敬語、文の成分、修飾関係)",
|
| 13 |
+
"JP03": "物語文読解(心情理解、場面把握、人物関係)",
|
| 14 |
+
"JP04": "説明文・論説文読解(要旨、段落構成、筆者の主張)",
|
| 15 |
+
"JP05": "随筆文読解(筆者の体験・感想の読み取り)",
|
| 16 |
+
"JP06": "詩・韻文(詩、短歌、俳句、表現技法)",
|
| 17 |
+
"JP07": "記述問題(理由説明、要約、意見記述)",
|
| 18 |
+
"JP08": "知識・文学史(作家、作品名、文学的常識)"
|
| 19 |
+
},
|
| 20 |
+
"math": {
|
| 21 |
+
"MA01": "計算",
|
| 22 |
+
"MA02": "数の性質",
|
| 23 |
+
"MA03": "割合・比",
|
| 24 |
+
"MA04": "速さ",
|
| 25 |
+
"MA05": "文章題(その他)",
|
| 26 |
+
"MA06": "平面図形",
|
| 27 |
+
"MA07": "立体図形",
|
| 28 |
+
"MA08": "場合の数・確率",
|
| 29 |
+
"MA09": "グラフ・表",
|
| 30 |
+
"MA10": "特殊算"
|
| 31 |
+
},
|
| 32 |
+
"sci": {
|
| 33 |
+
"SC01": "力・運動",
|
| 34 |
+
"SC02": "電気",
|
| 35 |
+
"SC03": "光・音・熱",
|
| 36 |
+
"SC04": "物質の性質",
|
| 37 |
+
"SC05": "水溶液",
|
| 38 |
+
"SC06": "燃焼・化学変化",
|
| 39 |
+
"SC07": "植物",
|
| 40 |
+
"SC08": "動物",
|
| 41 |
+
"SC09": "人体",
|
| 42 |
+
"SC10": "天体",
|
| 43 |
+
"SC11": "気象",
|
| 44 |
+
"SC12": "地学"
|
| 45 |
+
},
|
| 46 |
+
"soc": {
|
| 47 |
+
"SO01": "日本地理(国土・自然)",
|
| 48 |
+
"SO02": "日本地理(産業)",
|
| 49 |
+
"SO03": "世界地理",
|
| 50 |
+
"SO04": "歴史(古代〜平安)",
|
| 51 |
+
"SO05": "歴史(鎌倉〜室町)",
|
| 52 |
+
"SO06": "歴史(安土桃山〜江戸)",
|
| 53 |
+
"SO07": "歴史(明治〜現代)",
|
| 54 |
+
"SO08": "公民(政治・憲法)",
|
| 55 |
+
"SO09": "公民(経済・国際)",
|
| 56 |
+
"SO10": "時事問題"
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
SUBJECT_NAMES = {
|
| 61 |
+
"jp": "国語",
|
| 62 |
+
"math": "算数",
|
| 63 |
+
"sci": "理科",
|
| 64 |
+
"soc": "社会"
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def get_genre_list(subject: str) -> str:
|
| 69 |
+
"""ジャンルリストを文字列で取得"""
|
| 70 |
+
genres = GENRES.get(subject, {})
|
| 71 |
+
return "\n".join([f"- {gid}: {name}" for gid, name in genres.items()])
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def get_question_prompt(
|
| 75 |
+
subject: str,
|
| 76 |
+
knowledge_base: str,
|
| 77 |
+
statistics: Dict = None,
|
| 78 |
+
recent_questions: List = None
|
| 79 |
+
) -> str:
|
| 80 |
+
"""
|
| 81 |
+
問題生成プロンプトを構築
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
subject: 教科コード
|
| 85 |
+
knowledge_base: Knowledge Base内容
|
| 86 |
+
statistics: ユーザー統計
|
| 87 |
+
recent_questions: 直近の問題リスト
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
完成したプロンプト
|
| 91 |
+
"""
|
| 92 |
+
subject_name = SUBJECT_NAMES.get(subject, subject)
|
| 93 |
+
genre_list = get_genre_list(subject)
|
| 94 |
+
|
| 95 |
+
prompt = f"""あなたは中学受験対策の{subject_name}問題作成の専門家です。
|
| 96 |
+
|
| 97 |
+
## タスク
|
| 98 |
+
以下の教材を参考に、{subject_name}の4択問題を10問生成してください。
|
| 99 |
+
|
| 100 |
+
## 教材(Knowledge Base)
|
| 101 |
+
<knowledge_base>
|
| 102 |
+
{knowledge_base}
|
| 103 |
+
</knowledge_base>
|
| 104 |
+
|
| 105 |
+
## ジャンル一覧(必ず以下のジャンルIDを使用)
|
| 106 |
+
{genre_list}
|
| 107 |
+
|
| 108 |
+
## 条件
|
| 109 |
+
1. **教材活用**: 上記の教材に基づいた問題を出題すること
|
| 110 |
+
2. **分野構成**: 上記ジャンルから幅広く出題(各1〜3問)
|
| 111 |
+
3. **難易度配分**: 基本4問、標準4問、応用2問(計10問)
|
| 112 |
+
- 基本: 知識の確認、即答できるレベル(30秒以内で解答可能)
|
| 113 |
+
- 標準: 少し考えれば解ける問題(1分程度で解答可能)
|
| 114 |
+
- 応用: 知識の応用や複合的な思考が必要(1-2分で解答可能)
|
| 115 |
+
4. **問題の質**: 素早く解けるが、考えさせる要素を残す
|
| 116 |
+
5. **問題形式**: 問題文、4つの選択肢、正解番号(0〜3)、解説を含む
|
| 117 |
+
6. **ジャンルID使用**: categoryフィールドには必ずジャンルID({list(GENRES.get(subject, {}).keys())[0]}など)を使用すること
|
| 118 |
+
7. **正解位置の分散**: correct_answerは0〜3の間で均等に分散させること(特定の位置に偏らないよう注意)
|
| 119 |
+
|
| 120 |
+
## 出力形式
|
| 121 |
+
JSON配列形式で出力してください。
|
| 122 |
+
|
| 123 |
+
```json
|
| 124 |
+
[
|
| 125 |
+
{{
|
| 126 |
+
"category": "{list(GENRES.get(subject, {}).keys())[0]}",
|
| 127 |
+
"difficulty": "基本",
|
| 128 |
+
"question": "問題文",
|
| 129 |
+
"choices": ["選択肢1", "選択肢2", "選択肢3", "選択肢4"],
|
| 130 |
+
"correct_answer": 0,
|
| 131 |
+
"explanation": "解説文"
|
| 132 |
+
}},
|
| 133 |
+
...
|
| 134 |
+
]
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
**重要**:
|
| 138 |
+
- categoryには必ずジャンルID(例: {list(GENRES.get(subject, {}).keys())[0]})を使用
|
| 139 |
+
- correct_answerは0〜3の数値(選択肢の0-indexedインデックス)
|
| 140 |
+
- 10問生成すること(基本4問、標準4問、応用2問)
|
| 141 |
+
- 難���度フィールドには「基本」「標準」「応用」のいずれかを使用
|
| 142 |
+
- 正解位置は0〜3の間で均等に分散させること
|
| 143 |
+
"""
|
| 144 |
+
|
| 145 |
+
# 統計情報があれば追加
|
| 146 |
+
if statistics:
|
| 147 |
+
prompt += f"""
|
| 148 |
+
|
| 149 |
+
## ユーザーの分野別正答率
|
| 150 |
+
{json.dumps(statistics, ensure_ascii=False, indent=2)}
|
| 151 |
+
|
| 152 |
+
正答率60%未満の分野は「基本」レベル中心、60%以上は「標準」「応用」をミックスしてください。
|
| 153 |
+
"""
|
| 154 |
+
|
| 155 |
+
# 直近問題があれば追加
|
| 156 |
+
if recent_questions:
|
| 157 |
+
prompt += f"""
|
| 158 |
+
|
| 159 |
+
## 直近20問の履歴(重複回避用)
|
| 160 |
+
{json.dumps(recent_questions[:20], ensure_ascii=False)}
|
| 161 |
+
|
| 162 |
+
上記と類似した問題は避けてください。
|
| 163 |
+
"""
|
| 164 |
+
|
| 165 |
+
return prompt
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def get_batch_question_prompt(
|
| 169 |
+
subjects: List[str],
|
| 170 |
+
knowledge_bases: Dict[str, str],
|
| 171 |
+
priority_genres: Optional[Dict[str, List[str]]] = None,
|
| 172 |
+
exclude_keywords: Optional[List[str]] = None
|
| 173 |
+
) -> str:
|
| 174 |
+
"""複数教科の問題を一括生成するプロンプト
|
| 175 |
+
|
| 176 |
+
Args:
|
| 177 |
+
subjects: 教科コードのリスト (例: ["jp", "math", "sci", "soc"])
|
| 178 |
+
knowledge_bases: 教科別Knowledge Base {"jp": "...", "math": "...", ...}
|
| 179 |
+
priority_genres: 教科別優先ジャンル {"jp": ["JP05", "JP06"], ...}
|
| 180 |
+
exclude_keywords: 除外キーワードリスト ["いたわる", "四字熟語", ...]
|
| 181 |
+
|
| 182 |
+
Returns:
|
| 183 |
+
一括生成用プロンプト文字列
|
| 184 |
+
"""
|
| 185 |
+
# システムインストラクション
|
| 186 |
+
subject_names_str = "、".join([SUBJECT_NAMES.get(s, s) for s in subjects])
|
| 187 |
+
|
| 188 |
+
prompt = f"""あなたは中学受験対策の複数教科問題作成の専門家です。
|
| 189 |
+
|
| 190 |
+
## タスク
|
| 191 |
+
以下の教材を参考に、{subject_names_str}の4択問題を各教科10問ずつ生成してください。
|
| 192 |
+
|
| 193 |
+
"""
|
| 194 |
+
|
| 195 |
+
# 各教科のKnowledge Baseをセクション分け
|
| 196 |
+
for subject in subjects:
|
| 197 |
+
subject_name = SUBJECT_NAMES.get(subject, subject)
|
| 198 |
+
kb = knowledge_bases.get(subject, "")
|
| 199 |
+
|
| 200 |
+
prompt += f"""## {subject_name}の教材(Knowledge Base)
|
| 201 |
+
<knowledge_base_{subject}>
|
| 202 |
+
{kb}
|
| 203 |
+
</knowledge_base_{subject}>
|
| 204 |
+
|
| 205 |
+
"""
|
| 206 |
+
|
| 207 |
+
# 各教科のジャンル一覧
|
| 208 |
+
prompt += "## ジャンル一覧\n"
|
| 209 |
+
for subject in subjects:
|
| 210 |
+
subject_name = SUBJECT_NAMES.get(subject, subject)
|
| 211 |
+
genre_list = get_genre_list(subject)
|
| 212 |
+
prompt += f"""
|
| 213 |
+
### {subject_name}のジャンル(必ず以下のジャンルIDを使用)
|
| 214 |
+
{genre_list}
|
| 215 |
+
|
| 216 |
+
"""
|
| 217 |
+
|
| 218 |
+
# 条件
|
| 219 |
+
prompt += """## 条件
|
| 220 |
+
1. **教材活用**: 各教科の教材に基づいた問題を出題すること
|
| 221 |
+
2. **分野構成**: 各教科のジャンルから幅広く出題(各1〜3問)
|
| 222 |
+
3. **難易度配分**: 各教科ごとに基本4問、標準4問、応用2問(計10問)
|
| 223 |
+
- 基本: 知識の確認、即答できるレベル(30秒以内で解答可能)
|
| 224 |
+
- 標準: 少し考えれば解ける問題(1分程度で解答可能)
|
| 225 |
+
- 応用: 知識の応用や複合的な思考が必要(1-2分で解答可能)
|
| 226 |
+
4. **問題の質**: 素早く解けるが、考えさせる要素を残す
|
| 227 |
+
5. **問題形式**: 問題文、4つの選択肢、正解番号(0〜3)、解説を含む
|
| 228 |
+
6. **ジャンルID使用**: categoryフィールドには必ずジャンルID(JP01, MA01など)を使用すること
|
| 229 |
+
7. **正解位置の分散**: correct_answerは0〜3の間で均等に分散させること(各教科内で偏らないよう注意)
|
| 230 |
+
|
| 231 |
+
## 出力形式
|
| 232 |
+
JSON配列形式で、教科別に識別可能な形式で出力してください。
|
| 233 |
+
|
| 234 |
+
```json
|
| 235 |
+
[
|
| 236 |
+
"""
|
| 237 |
+
|
| 238 |
+
# 各教科の出力例を1問ずつ示す
|
| 239 |
+
example_questions = []
|
| 240 |
+
for subject in subjects:
|
| 241 |
+
first_genre = list(GENRES.get(subject, {}).keys())[0]
|
| 242 |
+
subject_name = SUBJECT_NAMES.get(subject, subject)
|
| 243 |
+
example_questions.append(f""" {{
|
| 244 |
+
"subject": "{subject}",
|
| 245 |
+
"category": "{first_genre}",
|
| 246 |
+
"difficulty": "基本",
|
| 247 |
+
"question": "{subject_name}の問題文(教材に基づく)",
|
| 248 |
+
"choices": ["選択肢1", "選択肢2", "選択肢3", "選択肢4"],
|
| 249 |
+
"correct_answer": 0,
|
| 250 |
+
"explanation": "解説文"
|
| 251 |
+
}}""")
|
| 252 |
+
|
| 253 |
+
prompt += ",\n".join(example_questions)
|
| 254 |
+
prompt += """,
|
| 255 |
+
...
|
| 256 |
+
]
|
| 257 |
+
```
|
| 258 |
+
|
| 259 |
+
**重要**:
|
| 260 |
+
- **必須フィールド**: subjectフィールドは必須(教科識別のため)
|
| 261 |
+
"""
|
| 262 |
+
|
| 263 |
+
# 各教科のジャンルIDリストを明示
|
| 264 |
+
for subject in subjects:
|
| 265 |
+
genre_ids = ", ".join(GENRES.get(subject, {}).keys())
|
| 266 |
+
subject_name = SUBJECT_NAMES.get(subject, subject)
|
| 267 |
+
prompt += f"- {subject_name}のcategoryには {{{genre_ids}}} のいずれかを使用\n"
|
| 268 |
+
|
| 269 |
+
prompt += """- correct_answerは0〜3の数値(選択肢の0-indexedインデックス)
|
| 270 |
+
- 各教科10問生成すること(基本4問、標準4問、応用2問)
|
| 271 |
+
- 難易度フィールドには「基本」「標準」「応用」のいずれかを使用
|
| 272 |
+
- 教科の順序は問いません(ランダムでOK)
|
| 273 |
+
- **重要**: 正解位置は0〜3の間で均等に分散させること(同じ位置に偏らないよう注意)
|
| 274 |
+
"""
|
| 275 |
+
|
| 276 |
+
# 優先ジャンル指定を追加
|
| 277 |
+
if priority_genres:
|
| 278 |
+
prompt += "\n## 出題優先ジャンル\n"
|
| 279 |
+
prompt += "以下のジャンルからの出題を優先してください(出題が少ない分野):\n"
|
| 280 |
+
for subject in subjects:
|
| 281 |
+
if subject in priority_genres and priority_genres[subject]:
|
| 282 |
+
subject_name = SUBJECT_NAMES.get(subject, subject)
|
| 283 |
+
genres = priority_genres[subject][:5] # 上位5ジャンル
|
| 284 |
+
prompt += f"- {subject_name}: {', '.join(genres)}\n"
|
| 285 |
+
|
| 286 |
+
# 除外キーワード指定を追加
|
| 287 |
+
if exclude_keywords and len(exclude_keywords) > 0:
|
| 288 |
+
prompt += "\n## 除外キーワード\n"
|
| 289 |
+
prompt += "以下のキーワード・トピックに関する問題は避けてください(直近で出題済み):\n"
|
| 290 |
+
prompt += f"{', '.join(exclude_keywords[:30])}\n"
|
| 291 |
+
|
| 292 |
+
return prompt
|
V1.7.1/src/prompts/validation_prompts.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
問題検証プロンプト定義
|
| 3 |
+
生成された問題の品質を検証し、必要に応じて修正する
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
from typing import List, Dict
|
| 8 |
+
|
| 9 |
+
SUBJECT_NAMES = {
|
| 10 |
+
"jp": "国語",
|
| 11 |
+
"math": "算数",
|
| 12 |
+
"sci": "理科",
|
| 13 |
+
"soc": "社会"
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def get_validation_prompt(subject: str, questions: List[Dict]) -> str:
|
| 18 |
+
"""
|
| 19 |
+
問題検証プロンプトを構築
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
subject: 教科コード
|
| 23 |
+
questions: 検証対象の問題リスト
|
| 24 |
+
|
| 25 |
+
Returns:
|
| 26 |
+
完成したプロンプト
|
| 27 |
+
"""
|
| 28 |
+
subject_name = SUBJECT_NAMES.get(subject, subject)
|
| 29 |
+
|
| 30 |
+
prompt = f"""あなたは中学受験問題の品質検証専門家です。
|
| 31 |
+
|
| 32 |
+
## タスク
|
| 33 |
+
以下の{subject_name}の問題を検証し、問題があれば修正してください。
|
| 34 |
+
|
| 35 |
+
## 検証観点
|
| 36 |
+
1. **正確性**: 正解が本当に正解か(学術的・事実として正しいか)
|
| 37 |
+
2. **整合性**: 問題文・選択肢・解説に矛盾がないか
|
| 38 |
+
3. **明確性**: 問題文が曖昧でなく、一意に解答を特定できるか
|
| 39 |
+
4. **選択肢品質**: 明らかに間違いとわかる選択肢や、紛らわしすぎる選択肢がないか
|
| 40 |
+
5. **適切性**: 中学受験レベルとして適切か(小学生に理解可能か)
|
| 41 |
+
6. **解説の正確性**: 解説が正解を正しく説明しているか
|
| 42 |
+
7. **正解位置の分散**: correct_answerが0〜3の間で偏っていないか確認し、偏りがある場合は選択肢の順序を入れ替えてcorrect_answerを分散させること
|
| 43 |
+
|
| 44 |
+
## 入力(検証対象の問題)
|
| 45 |
+
```json
|
| 46 |
+
{json.dumps(questions, ensure_ascii=False, indent=2)}
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## 出力形式
|
| 50 |
+
各問題について、修正が必要かどうかを判定し、以下のJSON形式で出力してください。
|
| 51 |
+
|
| 52 |
+
- 問題に問題がなければ `"modified": false` でそのまま返す
|
| 53 |
+
- 修正が必要な場合は `"modified": true` で修正版を返す
|
| 54 |
+
|
| 55 |
+
```json
|
| 56 |
+
[
|
| 57 |
+
{{
|
| 58 |
+
"index": 0,
|
| 59 |
+
"modified": false,
|
| 60 |
+
"question": {{問題をそのまま含める}}
|
| 61 |
+
}},
|
| 62 |
+
{{
|
| 63 |
+
"index": 1,
|
| 64 |
+
"modified": true,
|
| 65 |
+
"modification_reason": "正解番号が誤っていたため修正(正解は選択肢2の「...」)",
|
| 66 |
+
"question": {{修正後の問題}}
|
| 67 |
+
}}
|
| 68 |
+
]
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
## 重要
|
| 72 |
+
- すべての問題を検証し、結果を出力すること
|
| 73 |
+
- 修正は必要最小限にとどめること(過剰な修正は避ける)
|
| 74 |
+
- 修正理由は具体的に記述すること
|
| 75 |
+
- 元の問題の意図やカテゴリは変更しないこと
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
return prompt
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def get_batch_validation_prompt(questions_by_subject: Dict[str, List[Dict]]) -> str:
|
| 82 |
+
"""
|
| 83 |
+
複数教科の問題を一括検証するプロンプトを構築
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
questions_by_subject: 教科別問題辞書 {"jp": [...], "math": [...], ...}
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
複数教科一括検証用プロンプト
|
| 90 |
+
"""
|
| 91 |
+
# 全問題を教科名とインデックスで識別できる形式に整形
|
| 92 |
+
all_questions = []
|
| 93 |
+
for subject, questions in questions_by_subject.items():
|
| 94 |
+
subject_name = SUBJECT_NAMES.get(subject, subject)
|
| 95 |
+
for idx, question in enumerate(questions):
|
| 96 |
+
all_questions.append({
|
| 97 |
+
"subject": subject,
|
| 98 |
+
"subject_name": subject_name,
|
| 99 |
+
"index": idx,
|
| 100 |
+
"question": question
|
| 101 |
+
})
|
| 102 |
+
|
| 103 |
+
total_count = len(all_questions)
|
| 104 |
+
subject_summary = ", ".join([
|
| 105 |
+
f"{SUBJECT_NAMES.get(subj, subj)}{len(questions)}問"
|
| 106 |
+
for subj, questions in questions_by_subject.items()
|
| 107 |
+
])
|
| 108 |
+
|
| 109 |
+
prompt = f"""あなたは中学受験問題の品質検証専門家です。
|
| 110 |
+
|
| 111 |
+
## タスク
|
| 112 |
+
複数教科の問題(合計{total_count}問)を一括で検証し、問題があれば修正してください。
|
| 113 |
+
|
| 114 |
+
**対象**: {subject_summary}
|
| 115 |
+
|
| 116 |
+
## 検証観点
|
| 117 |
+
1. **正確性**: 正解が本当に正解か(学術的・事実として正しいか)
|
| 118 |
+
2. **整合性**: 問題文・選択肢・解説に矛盾がないか
|
| 119 |
+
3. **明確性**: 問題文が曖昧でなく、一意に解答を特定できるか
|
| 120 |
+
4. **選択肢品質**: 明らかに間違いとわかる選択肢や、紛らわしすぎる選択肢がないか
|
| 121 |
+
5. **適切性**: 中学受験レベルとして適切か(小学生に理解可能か)
|
| 122 |
+
6. **解説の正確性**: 解説が正解を正しく説明しているか
|
| 123 |
+
7. **正解位置の分散**: correct_answerが0〜3の間で偏っていないか確認し、偏りがある場合は選択肢の順序を入れ替えてcorrect_answerを分散させること
|
| 124 |
+
|
| 125 |
+
## 入力(検証対象の問題)
|
| 126 |
+
```json
|
| 127 |
+
{json.dumps(all_questions, ensure_ascii=False, indent=2)}
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
## 出力形式
|
| 131 |
+
各問題について、修正が必要かどうかを判定し、以下のJSON形式で出力してください。
|
| 132 |
+
|
| 133 |
+
**重要**: 各問題には必ず `subject` と `index` を含め、どの教科の何番��の問題かを明示すること。
|
| 134 |
+
|
| 135 |
+
- 問題に問題がなければ `"modified": false` でそのまま返す
|
| 136 |
+
- 修正が必要な場合は `"modified": true` で修正版を返す
|
| 137 |
+
|
| 138 |
+
```json
|
| 139 |
+
[
|
| 140 |
+
{{
|
| 141 |
+
"subject": "jp",
|
| 142 |
+
"index": 0,
|
| 143 |
+
"modified": false,
|
| 144 |
+
"question": {{問題をそのまま含める}}
|
| 145 |
+
}},
|
| 146 |
+
{{
|
| 147 |
+
"subject": "math",
|
| 148 |
+
"index": 3,
|
| 149 |
+
"modified": true,
|
| 150 |
+
"modification_reason": "正解番号が誤っていたため修正(正解は選択肢2の「...」)",
|
| 151 |
+
"question": {{修正後の問題}}
|
| 152 |
+
}}
|
| 153 |
+
]
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
## 重要
|
| 157 |
+
- **すべての問題**(合計{total_count}問)を検証し、結果を出力すること
|
| 158 |
+
- 各問題に `subject` と `index` を必ず含めること
|
| 159 |
+
- 修正は必要最小限にとどめること(過剰な修正は避ける)
|
| 160 |
+
- 修正理由は具体的に記述すること
|
| 161 |
+
- 元の問題の意図やカテゴリは変更しないこと
|
| 162 |
+
"""
|
| 163 |
+
|
| 164 |
+
return prompt
|
V1.7.1/src/services/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Services
|
V1.7.1/src/services/auth_service.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
認証サービス - パスワードハッシュ化・検証
|
| 3 |
+
|
| 4 |
+
bcryptを使用した安全なパスワード管理機能を提供
|
| 5 |
+
"""
|
| 6 |
+
import bcrypt
|
| 7 |
+
import logging
|
| 8 |
+
from typing import Optional
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class AuthService:
|
| 14 |
+
"""認証関連のサービス - パスワードハッシュ化と検証"""
|
| 15 |
+
|
| 16 |
+
@staticmethod
|
| 17 |
+
def hash_password(password: str) -> str:
|
| 18 |
+
"""
|
| 19 |
+
パスワードをbcryptでハッシュ化
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
password: 平文パスワード
|
| 23 |
+
|
| 24 |
+
Returns:
|
| 25 |
+
str: ハッシュ化されたパスワード(UTF-8文字列)
|
| 26 |
+
|
| 27 |
+
Raises:
|
| 28 |
+
ValueError: パスワードが空文字列の場合
|
| 29 |
+
"""
|
| 30 |
+
if not password or not password.strip():
|
| 31 |
+
raise ValueError("Password cannot be empty")
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
# ソルト生成とハッシュ化
|
| 35 |
+
salt = bcrypt.gensalt()
|
| 36 |
+
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
|
| 37 |
+
return hashed.decode('utf-8')
|
| 38 |
+
except Exception as e:
|
| 39 |
+
logger.error(f"Password hashing error: {e}")
|
| 40 |
+
raise
|
| 41 |
+
|
| 42 |
+
@staticmethod
|
| 43 |
+
def verify_password(password: str, hashed: str) -> bool:
|
| 44 |
+
"""
|
| 45 |
+
パスワードを検証
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
password: 平文パスワード
|
| 49 |
+
hashed: ハッシュ化されたパスワード
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
bool: パスワードが一致すればTrue、それ以外はFalse
|
| 53 |
+
"""
|
| 54 |
+
if not password or not hashed:
|
| 55 |
+
logger.warning("verify_password: Empty password or hash provided")
|
| 56 |
+
return False
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
return bcrypt.checkpw(
|
| 60 |
+
password.encode('utf-8'),
|
| 61 |
+
hashed.encode('utf-8')
|
| 62 |
+
)
|
| 63 |
+
except Exception as e:
|
| 64 |
+
logger.error(f"Password verification error: {e}")
|
| 65 |
+
return False
|
| 66 |
+
|
| 67 |
+
@staticmethod
|
| 68 |
+
def validate_password_strength(password: str) -> tuple[bool, Optional[str]]:
|
| 69 |
+
"""
|
| 70 |
+
パスワード強度を検証(オプション)
|
| 71 |
+
|
| 72 |
+
Args:
|
| 73 |
+
password: 検証するパスワード
|
| 74 |
+
|
| 75 |
+
Returns:
|
| 76 |
+
tuple[bool, Optional[str]]: (検証結果, エラーメッセージ)
|
| 77 |
+
"""
|
| 78 |
+
if not password:
|
| 79 |
+
return False, "パスワードが空です"
|
| 80 |
+
|
| 81 |
+
if len(password) < 8:
|
| 82 |
+
return False, "パスワードは8文字以上必要です"
|
| 83 |
+
|
| 84 |
+
# 追加の強度チェック(必要に応じて)
|
| 85 |
+
# has_upper = any(c.isupper() for c in password)
|
| 86 |
+
# has_lower = any(c.islower() for c in password)
|
| 87 |
+
# has_digit = any(c.isdigit() for c in password)
|
| 88 |
+
|
| 89 |
+
return True, None
|