Initial commit: Claude Code Gantt Chart Generator
- 対話型Ganttチャート自動生成システム
- Claude Code スキル定義 (/gantt, /gantt-update)
- Google Apps Script連携
- Todoist・Discord統合機能
- 完全なセットアップドキュメント
🤖 Generated with Claude Code
This commit is contained in:
commit
a892a3c87c
21 changed files with 9496 additions and 0 deletions
278
.claude/commands/gantt-update.md
Normal file
278
.claude/commands/gantt-update.md
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
# Ganttチャート更新 - 対話型プロジェクト更新
|
||||
|
||||
あなたは対話型プロジェクト管理アシスタントです。ユーザーとの対話を通じて既存プロジェクトの情報を更新し、Ganttチャート用のJSONファイルと対話履歴を更新します。
|
||||
|
||||
## 重要なルール
|
||||
|
||||
1. **一問一答形式を厳守**:必ず1つの質問のみを行い、ユーザーの回答を待つこと
|
||||
2. **段階的確認**:各フェーズ完了時に確認を取る
|
||||
3. **深掘り対話**:変更内容について何度も壁打ちして詳細を引き出す
|
||||
|
||||
## 対話の進行順序
|
||||
|
||||
以下の順序で情報を更新してください:
|
||||
|
||||
### Phase 1: プロジェクト選択
|
||||
|
||||
1. **プロジェクトID入力**
|
||||
- 質問例:「更新したいプロジェクトIDを入力してください(例: 192)」
|
||||
- 入力されたIDで`/output/プロジェクトID_*.json`を検索
|
||||
|
||||
2. **プロジェクトファイル読み込み**
|
||||
- JSONファイルを読み込んで現在の情報を表示:
|
||||
```
|
||||
以下のプロジェクトが見つかりました:
|
||||
|
||||
プロジェクトID: {project_id}
|
||||
プロジェクト名: {project_name}
|
||||
目的: {project_purpose}
|
||||
ジャンル: {project_type}
|
||||
期日: {project_deadline}
|
||||
|
||||
タスク数: {tasks.length}件
|
||||
- 親タスク: {親タスク数}件
|
||||
- 子タスク: {子タスク数}件
|
||||
|
||||
このプロジェクトを更新しますか?(はい/いいえ)
|
||||
```
|
||||
- 「いいえ」の場合:プロジェクトID入力に戻る
|
||||
- ファイルが見つからない場合:「プロジェクトが見つかりません。IDを確認してください。」
|
||||
|
||||
### Phase 2: 更新内容の選択
|
||||
|
||||
3. **更新項目の選択**
|
||||
- 質問例:
|
||||
```
|
||||
何を更新しますか?(番号で選択)
|
||||
|
||||
1. プロジェクト基本情報(名前、目的、ジャンル、期日)
|
||||
2. 新しいPhaseの追加
|
||||
3. 既存Phaseの編集
|
||||
4. 既存タスクの編集
|
||||
5. 新しいタスクの追加
|
||||
6. タスクの削除
|
||||
7. 複数項目を更新
|
||||
```
|
||||
|
||||
### Phase 3: 更新実行
|
||||
|
||||
#### 3-1. プロジェクト基本情報の更新
|
||||
|
||||
4. **変更する項目の選択**
|
||||
- 現在の情報を表示
|
||||
- 変更したい項目のみ選択
|
||||
- 質問例:
|
||||
```
|
||||
どの項目を変更しますか?(複数選択可、カンマ区切り)
|
||||
|
||||
1. プロジェクト名(現在: {project_name})
|
||||
2. 目的(現在: {project_purpose})
|
||||
3. ジャンル(現在: {project_type})
|
||||
4. 期日(現在: {project_deadline})
|
||||
```
|
||||
|
||||
5. **新しい値の入力**
|
||||
- 選択された項目ごとに新しい値を入力
|
||||
- 変更前後を確認して承認を取る
|
||||
|
||||
#### 3-2. 新しいPhaseの追加
|
||||
|
||||
6. **Phase情報の収集**
|
||||
- Phase名の入力
|
||||
- 開始日・終了日の入力
|
||||
- 依存関係の確認
|
||||
- 優先度、タグ、工数見積もりの設定
|
||||
- Phase内のタスク定義
|
||||
|
||||
7. **Phase IDの自動採番**
|
||||
- 既存のPhase IDを確認して次の番号を採番(例: P005が最後なら P006)
|
||||
|
||||
#### 3-3. 既存Phaseの編集
|
||||
|
||||
8. **Phase選択**
|
||||
- 既存のPhaseリストを表示
|
||||
- 質問例:
|
||||
```
|
||||
編集するPhaseを選択してください:
|
||||
|
||||
P001: Phase 1: 企画・設計
|
||||
P002: Phase 2: 基盤構築
|
||||
P003: Phase 3: 実績整理
|
||||
P004: Phase 4: カード作成
|
||||
P005: Phase 5: 仕上げ
|
||||
```
|
||||
|
||||
9. **Phase更新内容の選択**
|
||||
- 質問例:
|
||||
```
|
||||
何を変更しますか?(複数選択可、カンマ区切り)
|
||||
|
||||
1. Phase名
|
||||
2. 開始日・終了日
|
||||
3. 依存関係
|
||||
4. 優先度
|
||||
5. タグ
|
||||
6. 工数見積もり
|
||||
```
|
||||
|
||||
#### 3-4. 既存タスクの編集
|
||||
|
||||
10. **タスク選択**
|
||||
- Phase内のタスクリストを表示
|
||||
- または全タスクリストを表示
|
||||
- タスクIDで選択
|
||||
|
||||
11. **タスク更新内容の選択**
|
||||
- 質問例:
|
||||
```
|
||||
タスク「{task_name}」の何を変更しますか?
|
||||
|
||||
1. タスク名
|
||||
2. 開始日・終了日
|
||||
3. 担当者
|
||||
4. 依存関係
|
||||
5. 進捗率
|
||||
6. 優先度
|
||||
7. 親タスクID
|
||||
8. タグ
|
||||
9. 工数見積もり
|
||||
10. マイルストーン設定
|
||||
```
|
||||
|
||||
#### 3-5. 新しいタスクの追加
|
||||
|
||||
12. **追加先Phase選択**
|
||||
- 既存Phaseリストから選択
|
||||
- または「新しいPhaseを作成」を選択
|
||||
|
||||
13. **タスク情報の収集**
|
||||
- `/gantt`スキルと同様の対話フロー
|
||||
- タスクIDは既存の最大番号+1で自動採番
|
||||
|
||||
#### 3-6. タスクの削除
|
||||
|
||||
14. **削除対象の選択**
|
||||
- 削除したいタスクIDを入力
|
||||
- 確認:「本当に削除しますか?子タスクも削除されます。」
|
||||
|
||||
15. **依存関係の調整**
|
||||
- 削除するタスクに依存している他タスクがある場合、警告を表示
|
||||
- 依存関係の再設定を提案
|
||||
|
||||
### Phase 4: 確認とファイル生成
|
||||
|
||||
16. **変更内容の確認**
|
||||
- 変更前後の差分を表示
|
||||
- 質問例:
|
||||
```
|
||||
以下の変更を適用しますか?
|
||||
|
||||
【変更サマリー】
|
||||
- プロジェクト名: 旧 → 新
|
||||
- 追加されたPhase: 1件
|
||||
- 追加されたタスク: 3件
|
||||
- 編集されたタスク: 2件
|
||||
- 削除されたタスク: 0件
|
||||
|
||||
適用しますか?(はい/いいえ/修正したい)
|
||||
```
|
||||
|
||||
17. **JSON生成とファイル出力**
|
||||
- 既存のJSONファイルを上書き
|
||||
- 同じスキーマで生成:
|
||||
```json
|
||||
{
|
||||
"project_id": "プロジェクトID",
|
||||
"project_name": "プロジェクト名",
|
||||
"project_purpose": "目的(個人事業/HRteam)",
|
||||
"project_type": "プロジェクトジャンル",
|
||||
"project_deadline": "プロジェクト期日 (YYYY-MM-DD)",
|
||||
"tasks": [
|
||||
{
|
||||
"task_id": "タスクID",
|
||||
"task_name": "タスク名",
|
||||
"start_date": "開始日 (YYYY-MM-DD)",
|
||||
"end_date": "終了日 (YYYY-MM-DD)",
|
||||
"assignee": "担当者名",
|
||||
"dependencies": ["前提タスクID"],
|
||||
"progress": 0,
|
||||
"priority": "優先度(★の数)",
|
||||
"parent_task_id": "親タスクID",
|
||||
"tags": ["タグ1", "タグ2"],
|
||||
"estimated_hours": "工数見積もり(時間)",
|
||||
"is_milestone": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **保存先**: `/output/プロジェクトID_プロジェクト名.json`(上書き)
|
||||
|
||||
- **対話履歴の追記**
|
||||
- **ファイル名**: `プロジェクトID_プロジェクト名.md`
|
||||
- **保存先**: `/docs` ディレクトリ
|
||||
- **内容**: この更新対話の履歴を既存ファイルに追記(タイムスタンプ付き)
|
||||
|
||||
18. **Google Drive手動アップロードの案内**
|
||||
- 完了後、以下のメッセージを表示:
|
||||
```
|
||||
✅ プロジェクトを更新しました
|
||||
|
||||
ローカル保存先: /output/プロジェクトID_プロジェクト名.json
|
||||
対話履歴: /docs/プロジェクトID_プロジェクト名.md
|
||||
|
||||
📂 次のステップ:
|
||||
Google Driveの「Ganttチャート自動生成」フォルダに
|
||||
更新したJSONファイルを手動でアップロードしてください。
|
||||
|
||||
アップロード後、Google Apps Scriptが自動的に
|
||||
スプレッドシートを更新します。
|
||||
|
||||
または、スプレッドシートのメニューから
|
||||
「📊 ガントチャート > 📂 Driveから再読み込み」を実行してください。
|
||||
```
|
||||
|
||||
## 自動判定のガイドライン
|
||||
|
||||
### 依存関係の判定
|
||||
- 既存タスクとの関連性を分析
|
||||
- 新規タスクの場合、同じPhase内の前後関係を推測
|
||||
- 必ずユーザーに確認を取る
|
||||
|
||||
### 工数見積もりの推定
|
||||
- タスクの複雑さ、範囲から推定
|
||||
- 既存の類似タスクの工数を参考
|
||||
- 必ずユーザーに確認を取る
|
||||
|
||||
### 優先度の判定
|
||||
- 重要性(プロジェクトへの影響度)
|
||||
- 緊急性(期日までの余裕)
|
||||
- 依存関係(他タスクへの影響)
|
||||
- 既存タスクの優先度傾向を参考
|
||||
- 必ずユーザーに確認を取る
|
||||
|
||||
### タグの自動設定
|
||||
- タスク名や説明から業務内容を判断
|
||||
- 同じPhase内の既存タスクのタグを参考
|
||||
- 複数タグ設定可能
|
||||
- 必ずユーザーに確認を取る
|
||||
|
||||
## 出力形式の注意点
|
||||
|
||||
- タスクIDの採番は既存の最大値+1から
|
||||
- Phase IDの採番は既存の最大値+1から
|
||||
- 日付はすべてYYYY-MM-DD形式
|
||||
- 親タスクのparent_task_idはnull
|
||||
- マイルストーンは作業時間なし(開始日=終了日)
|
||||
|
||||
## 対話のトーン
|
||||
|
||||
- プロフェッショナルで簡潔
|
||||
- 一問一答を厳守
|
||||
- 必要に応じて提案や助言を行う
|
||||
- ユーザーの意図を確認しながら進める
|
||||
- 既存の構造を尊重しつつ、改善提案も行う
|
||||
|
||||
---
|
||||
|
||||
それでは、プロジェクト更新を開始します。まず最初の質問をしてください。
|
||||
186
.claude/commands/gantt.md
Normal file
186
.claude/commands/gantt.md
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
# Ganttチャート自動生成 - 対話型プロジェクト作成
|
||||
|
||||
あなたは対話型プロジェクト管理アシスタントです。ユーザーとの対話を通じてプロジェクト情報を収集し、Ganttチャート用のJSONファイルと対話履歴を生成します。
|
||||
|
||||
## 重要なルール
|
||||
|
||||
1. **一問一答形式を厳守**:必ず1つの質問のみを行い、ユーザーの回答を待つこと
|
||||
2. **段階的確認**:各フェーズ完了時に確認を取る
|
||||
3. **深掘り対話**:プロジェクト概要とタスク分割では何度も壁打ちして詳細を引き出す
|
||||
|
||||
## 対話の進行順序
|
||||
|
||||
以下の順序で情報を収集してください:
|
||||
|
||||
### Phase 1: 基本情報収集(Notion連携)
|
||||
|
||||
1. **NotionページURL入力**
|
||||
- 質問例:「NotionページのURLを入力してください(Beelzebub_ideaデータベースのページURL)」
|
||||
- 例: `https://www.notion.so/29dacd79ea7d8106adb5ff4ea997f28b`
|
||||
- URLを入力してもらう(スキップして手動入力も可能)
|
||||
|
||||
2. **Notionページ取得**
|
||||
- `mcp__notionMCP__notion-fetch` ツールを使用してページ情報を取得
|
||||
- 取得成功した場合:
|
||||
```
|
||||
Notionから以下の情報を取得しました:
|
||||
|
||||
- プロジェクトID: {Project ID}
|
||||
- プロジェクト名: {Name}
|
||||
- 内容: {Content}
|
||||
- 理由: {Reason}
|
||||
- 用途: {Usage}
|
||||
- ステータス: {Status}
|
||||
- リポジトリURL: {Repo URL}(あれば)
|
||||
|
||||
この情報でプロジェクト作成を開始しますか?(はい/いいえ/修正したい)
|
||||
```
|
||||
- 取得失敗またはスキップ:手動入力フローに進む
|
||||
|
||||
3. **Notion情報確認**
|
||||
- 「はい」:Notionの情報を使用してPhase 2へ
|
||||
- 「修正したい」:該当項目のみ手動入力
|
||||
- 「いいえ」:すべて手動入力(Phase 1-4へ)
|
||||
|
||||
4. **手動入力フロー(Notionに情報がない場合)**
|
||||
- **プロジェクトID入力**:数値4桁(例: 0033)
|
||||
- **プロジェクト名入力**
|
||||
- **目的選択**:個人事業 / HRteam
|
||||
- **プロジェクトジャンル選択**:
|
||||
1. 開発プロジェクト
|
||||
2. マーケティング
|
||||
3. イベント企画
|
||||
4. 業務改善
|
||||
5. 汎用プロジェクト
|
||||
|
||||
### Phase 2: プロジェクト詳細化
|
||||
|
||||
5. **プロジェクト概要説明**
|
||||
- 何度も質問を重ねて深掘りする
|
||||
- プロジェクトの背景、目的、ゴール、制約条件などを引き出す
|
||||
- 十分に情報が集まったら次へ進む確認を取る
|
||||
|
||||
6. **プロジェクト期日入力**
|
||||
- 質問例:「プロジェクト全体の期日を入力してください(YYYY-MM-DD形式)」
|
||||
|
||||
### Phase 3: タスク設計
|
||||
|
||||
7. **タスク分割**
|
||||
- 何度も質問を重ねて詳細なタスクに分割
|
||||
- 親タスクと小タスクの階層構造を意識
|
||||
- マイルストーンの設定も提案
|
||||
- 十分にタスクが洗い出せたら確認:「タスク分割完了しました。期日設定に進みますか?」
|
||||
|
||||
8. **各タスクの期日設定**
|
||||
- タスクごとに開始日と終了日を設定
|
||||
- プロジェクト期日との整合性を確認
|
||||
|
||||
### Phase 4: 詳細設定
|
||||
|
||||
9. **詳細設定**
|
||||
- **担当者**:自由入力(タスクごとに質問)
|
||||
- **依存関係**:AIが対話内容から自動判定(ユーザーに確認)
|
||||
- **タグ**:業務分類タグを自動設定(ユーザーに確認)
|
||||
- 企画・計画、設計、開発・実装、テスト・検証、リリース・デプロイ、運用・保守、マーケティング、営業・商談、事務・管理、その他
|
||||
- **工数見積もり**:AIが自動推定(時間単位、ユーザーに確認)
|
||||
- **優先度**:AIが自動判定(ユーザーに確認)
|
||||
- ★★★★★、★★★★☆、★★★☆☆、★★☆☆☆、★☆☆☆☆、☆☆☆☆☆
|
||||
|
||||
すべて設定完了したら確認:「すべて完了しました。JSON生成しますか?」
|
||||
|
||||
### Phase 5: ファイル生成
|
||||
|
||||
10. **JSON生成とファイル出力**
|
||||
- 以下のJSONスキーマで生成:
|
||||
|
||||
```json
|
||||
{
|
||||
"project_id": "プロジェクトID",
|
||||
"project_name": "プロジェクト名",
|
||||
"project_purpose": "目的(個人事業/HRteam)",
|
||||
"project_type": "プロジェクトジャンル",
|
||||
"project_deadline": "プロジェクト期日 (YYYY-MM-DD)",
|
||||
"tasks": [
|
||||
{
|
||||
"task_id": "タスクID(T001から自動連番)",
|
||||
"task_name": "タスク名",
|
||||
"start_date": "開始日 (YYYY-MM-DD)",
|
||||
"end_date": "終了日 (YYYY-MM-DD)",
|
||||
"assignee": "担当者名",
|
||||
"dependencies": ["前提タスクID"],
|
||||
"progress": 0,
|
||||
"priority": "優先度(★の数)",
|
||||
"parent_task_id": "親タスクID (nullの場合は親タスク)",
|
||||
"tags": ["タグ1", "タグ2"],
|
||||
"estimated_hours": "工数見積もり(時間)",
|
||||
"is_milestone": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **JSONファイル名**: `プロジェクトID_プロジェクト名.json`
|
||||
- **保存先**: `/output` ディレクトリ
|
||||
|
||||
- **対話履歴ファイル名**: `プロジェクトID_プロジェクト名.md`
|
||||
- **保存先**: `/docs` ディレクトリ
|
||||
- **内容**: この対話の全履歴を時系列でMarkdown形式で記録
|
||||
|
||||
両ファイルを生成後、JSONをGoogle Driveに自動アップロード(環境変数 `GOOGLE_DRIVE_FOLDER_ID` で指定されたフォルダ)
|
||||
|
||||
**Google Driveアップロード後の出力**:
|
||||
- アップロード成功時、以下の情報を表示:
|
||||
```
|
||||
✅ Google Driveにアップロード完了
|
||||
|
||||
ファイル名: プロジェクトID_プロジェクト名.json
|
||||
ファイルID: {Google Drive File ID}
|
||||
URL: https://drive.google.com/file/d/{File ID}/view
|
||||
フォルダID: {GOOGLE_DRIVE_FOLDER_ID}
|
||||
|
||||
ローカル保存先: /output/プロジェクトID_プロジェクト名.json
|
||||
対話履歴: /docs/プロジェクトID_プロジェクト名.md
|
||||
```
|
||||
|
||||
## 自動判定のガイドライン
|
||||
|
||||
### 依存関係の判定
|
||||
- タスクの説明から「〜の後に」「〜が完了してから」などの表現を検出
|
||||
- 技術的・論理的な依存関係を推測(例:設計→開発→テスト)
|
||||
- 必ずユーザーに確認を取る
|
||||
|
||||
### 工数見積もりの推定
|
||||
- タスクの複雑さ、範囲から推定
|
||||
- 類似プロジェクトの経験則を参考
|
||||
- 必ずユーザーに確認を取る
|
||||
|
||||
### 優先度の判定
|
||||
- 重要性(プロジェクトへの影響度)
|
||||
- 緊急性(期日までの余裕)
|
||||
- 依存関係(他タスクへの影響)
|
||||
- を総合的に判断して6段階で設定
|
||||
- 必ずユーザーに確認を取る
|
||||
|
||||
### タグの自動設定
|
||||
- タスク名や説明から業務内容を判断
|
||||
- 複数タグ設定可能
|
||||
- 必ずユーザーに確認を取る
|
||||
|
||||
## 出力形式の注意点
|
||||
|
||||
- タスクIDは必ずT001から連番で採番
|
||||
- 日付はすべてYYYY-MM-DD形式
|
||||
- 進捗率の初期値は0
|
||||
- 親タスクのparent_task_idはnull
|
||||
- マイルストーンは作業時間なし(開始日=終了日)
|
||||
|
||||
## 対話のトーン
|
||||
|
||||
- プロフェッショナルで簡潔
|
||||
- 一問一答を厳守
|
||||
- 必要に応じて提案や助言を行う
|
||||
- ユーザーの意図を確認しながら進める
|
||||
|
||||
---
|
||||
|
||||
それでは、プロジェクト作成を開始します。まず最初の質問をしてください。
|
||||
8
.env.example
Normal file
8
.env.example
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Google Drive API設定
|
||||
GOOGLE_DRIVE_FOLDER_ID=your_folder_id_here
|
||||
GOOGLE_CLIENT_ID=your_client_id_here
|
||||
GOOGLE_CLIENT_SECRET=your_client_secret_here
|
||||
GOOGLE_REDIRECT_URI=http://localhost:3000/oauth2callback
|
||||
|
||||
# プロジェクト設定
|
||||
PROJECT_ROOT=/Users/takashishibata/Desktop/個人事業/185-automatic-gantt-chart-creation
|
||||
119
.gitignore
vendored
Normal file
119
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
*.sqlite
|
||||
|
||||
# Gantt Chart Project - Sensitive files
|
||||
dist/
|
||||
.google-token.json
|
||||
.upload-history.json
|
||||
client_secret.json
|
||||
.clasp.json
|
||||
conversation_history/
|
||||
output/
|
||||
|
||||
# Gantt Chart Project - Claude settings
|
||||
.claude/settings.local.json
|
||||
.claude/.DS_Store
|
||||
.claude/agents/
|
||||
|
||||
# Gantt Chart Project - JSON files
|
||||
*.json
|
||||
!package.json
|
||||
!tsconfig.json
|
||||
!gas/appsscript.json
|
||||
!.mcp.json
|
||||
92
.mcp.json
Normal file
92
.mcp.json
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"adobe-xd": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"/Users/takashishibata/Desktop/BEELZEBUB/adobe-xd-mcp/dist/index.js"
|
||||
]
|
||||
},
|
||||
"serena": {
|
||||
"type": "stdio",
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"--from",
|
||||
"git+https://github.com/oraios/serena",
|
||||
"serena",
|
||||
"start-mcp-server",
|
||||
"--context",
|
||||
"ide-assistant",
|
||||
"--project",
|
||||
"/Users/takashishibata/Desktop/BEELZEBUB"
|
||||
],
|
||||
"env": {}
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"@playwright/mcp@latest"
|
||||
]
|
||||
},
|
||||
"chrome-devtools": {
|
||||
"command": "npx",
|
||||
"args": ["chrome-devtools-mcp@latest"]
|
||||
},
|
||||
"context7": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"--yes",
|
||||
"@upstash/context7-mcp"
|
||||
],
|
||||
"env": {
|
||||
"CONTEXT7_API_KEY": "ctx7sk-d154076d-c508-42e7-b5cd-6c9c481b3954"
|
||||
}
|
||||
},
|
||||
"codex": {
|
||||
"type": "stdio",
|
||||
"command": "codex",
|
||||
"args": ["mcp"]
|
||||
},
|
||||
"mcp-gemini-cli": {
|
||||
"command": "npx",
|
||||
"args": ["mcp-gemini-cli", "--allow-npx"]
|
||||
},
|
||||
"file-system": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-filesystem",
|
||||
"/Users/takashishibata/Desktop/BEELZEBUB"
|
||||
]
|
||||
},
|
||||
"github": {
|
||||
"type": "http",
|
||||
"url": "https://api.githubcopilot.com/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
||||
}
|
||||
},
|
||||
"supabase": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@supabase/mcp-server-supabase@latest"
|
||||
],
|
||||
"env": {
|
||||
"SUPABASE_ACCESS_TOKEN": "${SUPABASE_ACCESS_TOKEN}",
|
||||
"SUPABASE_PROJECT_REF": "${SUPABASE_PROJECT_REF}"
|
||||
}
|
||||
},
|
||||
"todoist-mcp": {
|
||||
"command": "node",
|
||||
"args": ["/Users/takashishibata/.claude/todoist-mcp/build/index.js"],
|
||||
"env": {
|
||||
"TODOIST_API_KEY": "dcbc9cfc8aa52fce62a50a09e3ccecd0200faf15"
|
||||
}
|
||||
},
|
||||
"notionMCP": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote", "https://mcp.notion.com/mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
324
DESIGN.md
Normal file
324
DESIGN.md
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
# 自動Ganttチャート生成システム 設計書
|
||||
|
||||
## 1. システム概要
|
||||
|
||||
Claude Codeを使った対話型プロジェクト管理システム。ユーザーとの対話を通じてプロジェクト情報を収集し、Google Apps Script (GAS)を使ってスプレッドシートに自動でGanttチャートを生成する。
|
||||
|
||||
## 2. システム構成
|
||||
|
||||
### 2.1 コンポーネント
|
||||
|
||||
1. **Claude Code対話システム**
|
||||
- スラッシュコマンド: `/gantt`
|
||||
- メタプロンプトファイル
|
||||
- JSON生成機能
|
||||
|
||||
2. **データ連携層**
|
||||
- Google Drive API(OAuth認証)
|
||||
- 自動アップロード機能
|
||||
|
||||
3. **GASバックエンド**
|
||||
- ドライブ監視トリガー
|
||||
- JSON読み込み・解析
|
||||
- Ganttチャート生成エンジン
|
||||
|
||||
4. **スプレッドシート出力**
|
||||
- プロジェクト一覧シート
|
||||
- 各プロジェクトのGanttチャートシート
|
||||
|
||||
### 2.2 データフロー
|
||||
|
||||
```
|
||||
1. ユーザー入力
|
||||
└─> Claude Code対話
|
||||
└─> JSON生成
|
||||
└─> Google Drive APIで指定フォルダにアップロード
|
||||
└─> GASがドライブ監視(自動トリガー)
|
||||
└─> JSON読み込み・解析
|
||||
└─> 指定スプレッドシートに新規シート追加
|
||||
└─> Ganttチャート生成
|
||||
```
|
||||
|
||||
## 3. データ仕様
|
||||
|
||||
### 3.1 JSONスキーマ(予定)
|
||||
|
||||
```json
|
||||
{
|
||||
"project_id": "プロジェクトID",
|
||||
"project_name": "プロジェクト名",
|
||||
"project_purpose": "目的(個人事業/HRteam)",
|
||||
"project_type": "プロジェクトジャンル",
|
||||
"project_deadline": "プロジェクト期日 (YYYY-MM-DD)",
|
||||
"tasks": [
|
||||
{
|
||||
"task_id": "タスクID(T001から自動連番)",
|
||||
"task_name": "タスク名",
|
||||
"start_date": "開始日 (YYYY-MM-DD)",
|
||||
"end_date": "終了日 (YYYY-MM-DD)",
|
||||
"assignee": "担当者名",
|
||||
"dependencies": ["前提タスクID"],
|
||||
"progress": "進捗率 (0-100)",
|
||||
"priority": "優先度",
|
||||
"parent_task_id": "親タスクID (nullの場合は親タスク)",
|
||||
"tags": ["タグ1", "タグ2"],
|
||||
"estimated_hours": "工数見積もり(時間)",
|
||||
"is_milestone": "マイルストーンフラグ (true/false)"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**注**: 最終的なスキーマはGASとの相性を考慮して調整
|
||||
|
||||
### 3.2 プロジェクトジャンル
|
||||
|
||||
1. 開発プロジェクト
|
||||
2. マーケティング
|
||||
3. イベント企画
|
||||
4. 業務改善
|
||||
5. 汎用プロジェクト
|
||||
|
||||
**注**: テンプレートは作成せず、ジャンル分け用の情報として使用
|
||||
|
||||
### 3.3 業務分類タグ(10種類)
|
||||
|
||||
| タグ名 | 説明 | 色 | カラーコード |
|
||||
|--------|------|-----|-------------|
|
||||
| **企画** | 構想・アイデア出し・要件定義 | 青 | #4285F4 |
|
||||
| **設計** | 仕様書作成・アーキテクチャ設計 | 紫 | #9C27B0 |
|
||||
| **開発** | コーディング・実装作業 | 緑 | #34A853 |
|
||||
| **デザイン** | UI/UXデザイン・ビジュアル制作 | ピンク | #E91E63 |
|
||||
| **テスト** | 動作確認・品質検証 | オレンジ | #FF9800 |
|
||||
| **レビュー** | コードレビュー・成果物確認 | 黄色 | #FBBC04 |
|
||||
| **資料作成** | ドキュメント・マニュアル作成 | 水色 | #00BCD4 |
|
||||
| **調整** | 会議・調整・承認取得 | グレー | #9E9E9E |
|
||||
| **運用** | デプロイ・保守・監視 | 茶色 | #795548 |
|
||||
| **その他** | 上記に該当しない作業 | ライトグレー | #E0E0E0 |
|
||||
|
||||
**注**: タスクには複数タグを設定可能。Ganttチャート上でタグごとに色分けして視覚化(複数タグの場合は最初のタグの色を使用)
|
||||
|
||||
### 3.4 優先度表現(6段階)
|
||||
|
||||
- ★★★★★(最高優先度)
|
||||
- ★★★★☆
|
||||
- ★★★☆☆
|
||||
- ★★☆☆☆
|
||||
- ★☆☆☆☆
|
||||
- ☆☆☆☆☆(最低優先度)
|
||||
|
||||
AIが自動判定する際は、タスクの重要性・緊急性・依存関係を考慮して設定
|
||||
|
||||
### 3.5 プロジェクトステータス(5段階)
|
||||
|
||||
- **未着手** - プロジェクト開始前
|
||||
- **進行中** - 実行中
|
||||
- **完了** - プロジェクト終了
|
||||
- **保留** - 一時停止中
|
||||
- **中止** - プロジェクト中止
|
||||
|
||||
初期値は「未着手」、スプレッドシート上で手動更新
|
||||
|
||||
## 4. 対話フロー設計
|
||||
|
||||
### 4.1 対話方式
|
||||
|
||||
- **基本方式**: 順次型(案A)とテンプレート型(案C)のハイブリッド
|
||||
- **コミュニケーション**: 一問一答形式(必須)
|
||||
- **段階的確認**: 各フェーズ完了時に確認を行う
|
||||
|
||||
### 4.2 対話の進行順序
|
||||
|
||||
```
|
||||
1. プロジェクトID入力
|
||||
↓
|
||||
2. プロジェクト名入力
|
||||
↓
|
||||
3. 目的選択(個人事業/HRteam)
|
||||
↓
|
||||
4. プロジェクトジャンル選択(5種類から)
|
||||
↓
|
||||
5. プロジェクト概要説明
|
||||
※何度も壁打ちして深掘り
|
||||
↓
|
||||
6. プロジェクト期日入力
|
||||
↓
|
||||
7. タスク分割
|
||||
※何度も壁打ちして深掘り
|
||||
【確認】「タスク分割完了しました。期日設定に進みますか?」
|
||||
↓
|
||||
8. 各タスクの期日設定
|
||||
↓
|
||||
9. 詳細設定(担当者・依存関係・タグ・工数など)
|
||||
【確認】「すべて完了しました。JSON生成しますか?」
|
||||
↓
|
||||
10. JSON生成 → Google Driveに自動アップロード
|
||||
- **JSONファイル名**: `プロジェクトID_プロジェクト名.json` → Google Driveにアップロード
|
||||
- **対話履歴ファイル名**: `プロジェクトID_プロジェクト名.md` → ローカルの`/docs`ディレクトリに保存
|
||||
```
|
||||
|
||||
### 4.3 自動判定項目
|
||||
|
||||
- **依存関係**: AIが対話内容から自動判定
|
||||
- **工数見積もり**: AIが自動推定
|
||||
- **優先度**: AIが自動判定
|
||||
|
||||
### 4.4 手動入力項目
|
||||
|
||||
- **担当者**: 自由入力
|
||||
- **小タスクの完了状態**: スプレッドシート上のステータス列で手動更新
|
||||
|
||||
## 5. スプレッドシート仕様
|
||||
|
||||
### 5.1 シート構成
|
||||
|
||||
1. **プロジェクト一覧シート**
|
||||
- プロジェクトID
|
||||
- プロジェクト名
|
||||
- プロジェクトジャンル
|
||||
- 目的(個人事業/HRteam ※将来的に増える可能性あり)
|
||||
- 作成日
|
||||
- 期日(プロジェクト全体の終了予定日)
|
||||
- ステータス
|
||||
|
||||
2. **全プロジェクトタスクシート**
|
||||
- 全プロジェクトのタスクを統合表示
|
||||
- **表示項目**: プロジェクト名 + 各プロジェクトシート左側の全項目
|
||||
- プロジェクト名、タスクID、タスク名、開始日、終了日、進捗率、ステータス、担当者、優先度、タグ、工数
|
||||
- **デフォルト並び順**: プロジェクトごとにまとめて表示
|
||||
- **更新方法**(3種類実装):
|
||||
1. 新規プロジェクト作成時に全タスクを自動追加
|
||||
2. 各プロジェクトシートの変更を監視して自動同期(onEdit トリガー)
|
||||
3. 手動更新ボタン(シート上部に配置)
|
||||
- **機能**: スプレッドシートのフィルタ/並び替え機能で期日順・優先度順など自由に並び替え可能
|
||||
|
||||
3. **各プロジェクトのGanttチャートシート**
|
||||
- シート名: `プロジェクトID_プロジェクト名`
|
||||
- 上部: 基本情報(プロジェクトID、プロジェクト名、ジャンル等)
|
||||
- メイン: Ganttチャート
|
||||
|
||||
### 5.2 Ganttチャートスタイル(標準構成)
|
||||
|
||||
**レイアウト:**
|
||||
- 左側: タスクリスト(全項目表示)
|
||||
- **列の並び順**: タスクID → タスク名 → 開始日 → 終了日 → 進捗率 → ステータス → [折りたたみ]担当者 → 優先度 → タグ → 工数
|
||||
- **行の並び順**: タスクIDの昇順(T001 → T002 → T003...)
|
||||
- **親子関係の表示**: 小タスクをインデント表示(親タスクの下に字下げ)
|
||||
- **常に表示**: タスクID、タスク名、開始日、終了日、進捗率、ステータス
|
||||
- **デフォルト折りたたみ**: 担当者、優先度、タグ、工数見積もり
|
||||
- GASで列グループ化機能を使用して折りたたみ設定
|
||||
- 右側: タイムライン(日付軸に沿った棒グラフ)
|
||||
- **日付軸の単位**: 日単位
|
||||
|
||||
**視覚要素:**
|
||||
1. タグによる色分け(業務種別の視覚化)
|
||||
2. 週末のグレー背景(スケジュール感覚)
|
||||
3. 進捗率は棒の塗りつぶし(例: 50%完了なら棒の半分を濃色)
|
||||
4. 依存関係は灰色の実線矢印(#666666)で前提タスクからの関係を可視化
|
||||
5. マイルストーンは赤色の◆マーク(#EA4335)+ テキストラベル付き
|
||||
|
||||
### 5.3 進捗管理
|
||||
|
||||
- **親タスク**: 配下の小タスクの完了状況から自動計算
|
||||
- **小タスク**: ステータス列(ドロップダウン: 未着手/進行中/完了)を手動更新
|
||||
- 親タスクの進捗率 = 完了した小タスク数 / 全小タスク数 × 100
|
||||
- **計算ルール**: 「完了」のみカウント(「進行中」は0%扱い)
|
||||
|
||||
## 6. 技術実装
|
||||
|
||||
### 6.0 環境変数管理
|
||||
|
||||
- **Claude Code側**: `.env`ファイル(推奨)
|
||||
- `GOOGLE_DRIVE_FOLDER_ID`: アップロード先フォルダID
|
||||
- **GAS側**: スクリプトプロパティ
|
||||
- `SPREADSHEET_ID`: 出力先スプレッドシートID
|
||||
- `DISCORD_WEBHOOK_URL`: エラー通知用Discord Webhook URL
|
||||
|
||||
### 6.1 Google Drive連携
|
||||
|
||||
- **方式**: Google Drive API使用
|
||||
- **認証**: OAuth認証(初回のみ設定)
|
||||
- **アップロード先**: 指定フォルダ
|
||||
- **指定方法**: 環境変数(`.env`)でフォルダIDを設定
|
||||
|
||||
### 6.2 GASトリガー
|
||||
|
||||
- **トリガータイプ**: ドライブ監視(ファイルアップロード検知)
|
||||
- **実行内容**:
|
||||
1. 新規JSONファイル検知
|
||||
2. JSON読み込み・解析
|
||||
3. 指定スプレッドシートに新規Ganttチャートシート追加
|
||||
4. Ganttチャート生成
|
||||
5. プロジェクト一覧シートに自動追加(プロジェクトID、プロジェクト名、目的、ジャンル、作成日、期日、ステータス)
|
||||
6. 全プロジェクトタスクシートに全タスクを自動追加
|
||||
|
||||
### 6.3 スプレッドシート操作
|
||||
|
||||
- **対象**: 指定された1つのスプレッドシート
|
||||
- **指定方法**: 環境変数でスプレッドシートURLまたはIDを設定
|
||||
- **操作**: 新規シート追加(既存シートは保持)
|
||||
- **命名規則**: `プロジェクトID_プロジェクト名`
|
||||
|
||||
## 7. 開発タスク
|
||||
|
||||
### 7.1 Phase 1: Claude Code側の実装
|
||||
- [ ] メタプロンプトファイル作成
|
||||
- [ ] スラッシュコマンド `/gantt` 実装
|
||||
- [ ] JSON生成ロジック実装
|
||||
- [ ] Google Drive API連携実装
|
||||
|
||||
### 7.2 Phase 2: GAS側の実装
|
||||
- [ ] ドライブ監視トリガー設定
|
||||
- [ ] JSON読み込み・解析ロジック
|
||||
- [ ] スプレッドシート操作ロジック
|
||||
- [ ] Ganttチャート生成エンジン実装
|
||||
- [ ] 進捗率自動計算ロジック
|
||||
- [ ] 全プロジェクトタスクシート機能
|
||||
- 新規プロジェクト作成時の自動追加
|
||||
- onEditトリガーによる自動同期
|
||||
- 手動更新ボタン実装
|
||||
- [ ] エラーハンドリング・Discord通知機能
|
||||
- Discord Webhook URLは環境変数で設定
|
||||
|
||||
### 7.3 Phase 3: 統合とテスト
|
||||
- [ ] エンドツーエンドテスト
|
||||
- [ ] エラーハンドリング実装
|
||||
- [ ] ドキュメント整備
|
||||
|
||||
## 10. 次のステップ
|
||||
|
||||
設計仕様が確定しました。次は以下の実装に進みます:
|
||||
|
||||
1. **メタプロンプトファイル作成** - 対話フローを実装
|
||||
2. **スラッシュコマンド実装** - `/gantt`コマンドの作成
|
||||
3. **JSON生成ロジック** - 対話情報からJSONを生成
|
||||
4. **Google Drive API連携** - 自動アップロード機能
|
||||
5. **GASスクリプト実装** - Ganttチャート生成エンジン
|
||||
|
||||
**ステータス**: 設計完了 ✓ (最終更新: 2025-12-17)
|
||||
|
||||
**主要機能まとめ:**
|
||||
- 対話型プロジェクト作成(10ステップ)
|
||||
- JSON自動生成 + Google Drive自動アップロード
|
||||
- GAS自動トリガー + Ganttチャート生成
|
||||
- プロジェクト一覧シート
|
||||
- 全プロジェクトタスクシート(3種類の更新方法)
|
||||
- 各プロジェクトGanttチャートシート
|
||||
- Discord通知 + エラーハンドリング
|
||||
|
||||
## 8. 保留事項
|
||||
|
||||
### 8.1 全体管理シートの仕様
|
||||
- プロジェクト一覧シートの詳細機能
|
||||
- 複数プロジェクト間の依存関係管理
|
||||
- リソース配分の可視化
|
||||
- ダッシュボード機能
|
||||
|
||||
**→ 後で仕様を検討・実装**
|
||||
|
||||
## 9. 補足
|
||||
|
||||
- 対話は常に一問一答形式で進行
|
||||
- 各フェーズで段階的確認を実施
|
||||
- AIが自動判定する項目と手動入力項目を明確に分離
|
||||
- スプレッドシート上での手動更新を前提とした進捗管理
|
||||
268
INSTALL.md
Normal file
268
INSTALL.md
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
# インストールガイド
|
||||
|
||||
Claude Code対話型Ganttチャート自動生成システムのセットアップ手順を説明します。
|
||||
|
||||
## 📋 前提条件
|
||||
|
||||
- **Node.js**: v20.x以上
|
||||
- **npm**: v10.x以上
|
||||
- **Claude Code CLI**: インストール済み
|
||||
- **Google Cloud Platform**: アカウント作成済み
|
||||
- **Google Apps Script**: 基本的な知識
|
||||
|
||||
## 🚀 セットアップ手順
|
||||
|
||||
### 1. リポジトリのクローン
|
||||
|
||||
```bash
|
||||
git clone https://github.com/{your-username}/claude-gantt-chart.git
|
||||
cd claude-gantt-chart
|
||||
```
|
||||
|
||||
### 2. 依存関係のインストール
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. 環境変数の設定
|
||||
|
||||
`.env.example`をコピーして`.env`を作成:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
`.env`ファイルを編集:
|
||||
|
||||
```env
|
||||
# Google Drive設定
|
||||
GOOGLE_DRIVE_FOLDER_ID=your_folder_id_here
|
||||
GOOGLE_CLIENT_ID=your_client_id_here
|
||||
GOOGLE_CLIENT_SECRET=your_client_secret_here
|
||||
GOOGLE_REDIRECT_URI=http://localhost:19204/oauth2callback
|
||||
|
||||
# スプレッドシートID(Discord通知用、オプション)
|
||||
GOOGLE_SPREADSHEET_ID=your_spreadsheet_id_here
|
||||
|
||||
# Discord通知設定(オプション)
|
||||
DISCORD_WEBHOOK_URL=your_webhook_url_here
|
||||
|
||||
# Todoist API設定(オプション)
|
||||
TODOIST_API_KEY=your_todoist_api_token_here
|
||||
```
|
||||
|
||||
### 4. Google Cloud Platformの設定
|
||||
|
||||
#### 4.1 プロジェクト作成
|
||||
|
||||
1. [Google Cloud Console](https://console.cloud.google.com/)にアクセス
|
||||
2. 新規プロジェクトを作成
|
||||
|
||||
#### 4.2 Google Drive API有効化
|
||||
|
||||
1. 「APIとサービス」→「ライブラリ」
|
||||
2. 「Google Drive API」を検索して有効化
|
||||
- URL: https://console.cloud.google.com/apis/library/drive.googleapis.com
|
||||
|
||||
#### 4.3 OAuth 2.0認証情報の作成
|
||||
|
||||
1. 「APIとサービス」→「認証情報」
|
||||
2. 「認証情報を作成」→「OAuth 2.0 クライアントID」
|
||||
3. アプリケーションの種類: **デスクトップアプリ**
|
||||
4. リダイレクトURI: `http://localhost:19204/oauth2callback`
|
||||
5. クライアントIDとシークレットを`.env`に設定
|
||||
|
||||
#### 4.4 Google Driveフォルダ作成
|
||||
|
||||
1. Google Driveで新規フォルダを作成(例: `Gantt-JSON-Files`)
|
||||
2. フォルダURLからフォルダIDを取得:
|
||||
- URL形式: `https://drive.google.com/drive/folders/{FOLDER_ID}`
|
||||
- `{FOLDER_ID}`の部分を`.env`の`GOOGLE_DRIVE_FOLDER_ID`に設定
|
||||
|
||||
### 5. Google OAuth認証
|
||||
|
||||
初回認証トークンを取得:
|
||||
|
||||
```bash
|
||||
npm run gantt:auth
|
||||
```
|
||||
|
||||
ブラウザが開き、Googleアカウントでログイン・認証します。
|
||||
認証完了後、`.google-token.json`が自動生成されます(gitignoreに含まれます)。
|
||||
|
||||
### 6. TypeScriptのビルド
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 7. Google Apps Scriptの設定
|
||||
|
||||
#### 7.1 スプレッドシート作成
|
||||
|
||||
1. [Google Sheets](https://sheets.google.com/)で新規スプレッドシートを作成
|
||||
2. スプレッドシートのURLからIDを取得:
|
||||
- URL形式: `https://docs.google.com/spreadsheets/d/{SPREADSHEET_ID}/edit`
|
||||
- `{SPREADSHEET_ID}`を`.env`の`GOOGLE_SPREADSHEET_ID`に設定(オプション)
|
||||
|
||||
#### 7.2 Apps Scriptプロジェクト作成
|
||||
|
||||
1. スプレッドシートを開く
|
||||
2. 「拡張機能」→「Apps Script」
|
||||
3. 新規プロジェクトが作成されます
|
||||
|
||||
#### 7.3 GASコードのコピー
|
||||
|
||||
`gas/`フォルダ内の全ファイルをApps Scriptエディタにコピー:
|
||||
|
||||
- `Config.gs`
|
||||
- `SheetManager.gs`
|
||||
- `GanttRenderer.gs`
|
||||
- `Code.gs`
|
||||
|
||||
#### 7.4 Config.gsの設定
|
||||
|
||||
`Config.gs`を開き、以下を設定:
|
||||
|
||||
```javascript
|
||||
const CONFIG = {
|
||||
DRIVE_FOLDER_ID: 'あなたのフォルダID', // .envと同じ値
|
||||
DISCORD_WEBHOOK_URL: 'あなたのWebhook URL', // オプション
|
||||
TODOIST_API_KEY: 'あなたのTodoist APIトークン', // オプション
|
||||
// ...その他の設定
|
||||
};
|
||||
```
|
||||
|
||||
#### 7.5 時間トリガーの設定
|
||||
|
||||
1. Apps Scriptエディタで「トリガー」アイコン(⏰)をクリック
|
||||
2. 「トリガーを追加」をクリック
|
||||
3. 以下のように設定:
|
||||
- **実行する関数**: `checkForNewJsonFiles`
|
||||
- **イベントのソース**: `時間主導型`
|
||||
- **時間ベースのトリガーのタイプ**: `分ベースのタイマー`
|
||||
- **時間の間隔**: `1分おき`
|
||||
4. 「保存」をクリック
|
||||
|
||||
### 8. Todoist統合(オプション)
|
||||
|
||||
Todoistと連携する場合:
|
||||
|
||||
#### 8.1 Todoist APIトークン取得
|
||||
|
||||
1. [Todoist設定](https://todoist.com/app/settings/integrations/developer)にアクセス
|
||||
2. 「開発者」タブ→「APIトークン」をコピー
|
||||
3. `.env`の`TODOIST_API_KEY`に設定
|
||||
4. `Config.gs`の`TODOIST_API_KEY`にも設定
|
||||
|
||||
#### 8.2 Todoistトリガー設定
|
||||
|
||||
Apps Scriptで以下のトリガーを追加:
|
||||
|
||||
1. **Todoistタスク同期**:
|
||||
- 実行する関数: `syncTodoistTasks`
|
||||
- イベントのソース: `時間主導型`
|
||||
- 時間の間隔: `30分おき`
|
||||
|
||||
2. **期日切れタスク通知**:
|
||||
- 実行する関数: `sendOverdueTasksNotification`
|
||||
- イベントのソース: `時間主導型`
|
||||
- 時間ベースのトリガーのタイプ: `日タイマー`
|
||||
- 時刻: `午前9時~10時`
|
||||
|
||||
### 9. Discord通知設定(オプション)
|
||||
|
||||
Discord通知を有効にする場合:
|
||||
|
||||
1. Discordサーバーで「サーバー設定」→「連携サービス」→「ウェブフック」
|
||||
2. 新しいウェブフックを作成
|
||||
3. ウェブフックURLをコピー
|
||||
4. `.env`の`DISCORD_WEBHOOK_URL`に設定
|
||||
5. `Config.gs`の`DISCORD_WEBHOOK_URL`にも設定
|
||||
|
||||
## ✅ 動作確認
|
||||
|
||||
### 1. Claude Codeで動作確認
|
||||
|
||||
```bash
|
||||
# プロジェクトディレクトリで
|
||||
claude code
|
||||
```
|
||||
|
||||
Claude Codeのプロンプトで:
|
||||
|
||||
```
|
||||
/gantt
|
||||
```
|
||||
|
||||
スキルが正常に認識されれば成功です。
|
||||
|
||||
### 2. JSONファイル生成テスト
|
||||
|
||||
1. `/gantt`コマンドで対話形式でプロジェクト情報を入力
|
||||
2. 完了後、以下を実行:
|
||||
|
||||
```bash
|
||||
npm run gantt:save
|
||||
```
|
||||
|
||||
3. `output/`フォルダにJSONファイルが生成されることを確認
|
||||
4. Google Driveにアップロードされることを確認
|
||||
|
||||
### 3. Ganttチャート生成テスト
|
||||
|
||||
1. JSONファイルがGoogle Driveにアップロードされた後、1分以内に自動処理される
|
||||
2. スプレッドシートに以下のシートが作成されることを確認:
|
||||
- プロジェクト一覧
|
||||
- 全プロジェクトタスク
|
||||
- 期日切れタスク
|
||||
- 個別Ganttチャート
|
||||
|
||||
## 🛠️ トラブルシューティング
|
||||
|
||||
### Q1. OAuth認証がうまくいかない
|
||||
|
||||
**A**: 以下を確認:
|
||||
1. Google Drive APIが有効化されているか
|
||||
2. `.env`のクライアントID・シークレットが正確か
|
||||
3. リダイレクトURIが正確に設定されているか
|
||||
|
||||
### Q2. JSONファイルが生成されない
|
||||
|
||||
**A**: 以下を確認:
|
||||
1. `npm run build`を実行したか
|
||||
2. TypeScriptのコンパイルエラーがないか
|
||||
3. `npm run gantt:save`を実行したか
|
||||
|
||||
### Q3. Ganttチャートが自動生成されない
|
||||
|
||||
**A**: 以下を確認:
|
||||
1. 時間トリガーが正しく設定されているか
|
||||
2. `Config.gs`の`DRIVE_FOLDER_ID`が正しいか
|
||||
3. Apps Scriptの実行ログでエラーがないか
|
||||
4. JSONファイルが正しいフォルダにアップロードされているか
|
||||
|
||||
### Q4. Todoistタスクが同期されない
|
||||
|
||||
**A**: 以下を確認:
|
||||
1. `TODOIST_API_KEY`が正しく設定されているか
|
||||
2. Todoistトリガーが正しく設定されているか
|
||||
3. `testTodoistIntegration()`関数で手動テストする
|
||||
|
||||
## 📚 次のステップ
|
||||
|
||||
- [README.md](README.md) - 詳細な使い方
|
||||
- [gas/README.md](gas/README.md) - GAS側の詳細ドキュメント
|
||||
- [DESIGN.md](DESIGN.md) - システム設計ドキュメント
|
||||
|
||||
## 🆘 サポート
|
||||
|
||||
問題が発生した場合:
|
||||
1. [Issues](https://github.com/{your-username}/claude-gantt-chart/issues)で報告
|
||||
2. トラブルシューティングセクションを確認
|
||||
3. Apps Scriptの実行ログを確認
|
||||
|
||||
## 📝 ライセンス
|
||||
|
||||
MIT License
|
||||
502
README.md
Normal file
502
README.md
Normal file
|
|
@ -0,0 +1,502 @@
|
|||
# Claude Code対話型Ganttチャート自動生成システム
|
||||
|
||||
Claude Codeとの対話でプロジェクトを設計し、JSONファイルを自動生成して、Google Apps Scriptでスプレッドシートに美しいGanttチャートを自動作成するシステムです。
|
||||
|
||||
## 📋 目次
|
||||
|
||||
- [機能概要](#機能概要)
|
||||
- [システム構成](#システム構成)
|
||||
- [セットアップ](#セットアップ)
|
||||
- [使い方](#使い方)
|
||||
- [データ仕様](#データ仕様)
|
||||
- [GAS統合](#gas統合)
|
||||
- [トラブルシューティング](#トラブルシューティング)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 機能概要
|
||||
|
||||
### Phase 1: Claude Code側(このリポジトリ)
|
||||
|
||||
- **対話型プロジェクト設計**: `/gantt` スラッシュコマンドで段階的にプロジェクト情報を入力
|
||||
- **JSON自動生成**: プロジェクト基本情報とタスク一覧を構造化JSONで出力
|
||||
- **対話履歴保存**: プロジェクト作成プロセスをMarkdown形式で記録
|
||||
- **Google Driveアップロード**: 生成したJSONファイルを自動アップロード
|
||||
|
||||
### Phase 2: GAS側(✅ 実装完了)
|
||||
|
||||
- **自動Ganttチャート生成**: JSONファイルからスプレッドシートに視覚的なGanttチャートを作成
|
||||
- **4シート構成**: プロジェクト一覧、全プロジェクトタスク、期日切れタスク、Todoistタスク、個別Ganttチャート
|
||||
- **タスク管理機能**: 進捗率、依存関係、担当者、優先度の管理
|
||||
- **Todoist統合**: TodoistのInboxタスクを自動同期してスプレッドシートで一元管理
|
||||
- **期日切れタスク通知**: 期日を過ぎたタスクを自動検出してDiscord通知
|
||||
- **Discord通知**: 成功/エラー通知をDiscordに自動送信
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ システム構成
|
||||
|
||||
```
|
||||
185-automatic-gantt-chart-creation/
|
||||
├── .claude/
|
||||
│ └── commands/
|
||||
│ └── gantt.md # /gantt スラッシュコマンド定義
|
||||
├── src/
|
||||
│ └── gantt-helper.ts # ヘルパースクリプト(状態管理・JSON生成・Discord通知)
|
||||
├── gas/ # Google Apps Script ファイル ✨ NEW
|
||||
│ ├── Config.gs # 設定管理(スプレッドシートID、Discord、色設定)
|
||||
│ ├── SheetManager.gs # シート操作(作成、更新、データ投入)
|
||||
│ ├── GanttRenderer.gs # Ganttチャート描画(バー、ヘッダー、スタイル)
|
||||
│ ├── Code.gs # メインスクリプト(実行エントリーポイント)
|
||||
│ └── README.md # GAS導入手順・使い方
|
||||
├── outputs/ # JSONファイル保存先
|
||||
├── docs/ # 対話履歴ファイル保存先
|
||||
├── package.json # 依存関係
|
||||
├── tsconfig.json # TypeScript設定
|
||||
├── .env.example # 環境変数テンプレート
|
||||
├── .gitignore
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 セットアップ
|
||||
|
||||
### 1. 依存関係のインストール
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. 環境変数の設定
|
||||
|
||||
`.env.example` をコピーして `.env` を作成:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
`.env` ファイルを編集して以下の値を設定:
|
||||
|
||||
```env
|
||||
# Google Drive設定
|
||||
GOOGLE_DRIVE_FOLDER_ID=your_folder_id_here
|
||||
GOOGLE_CLIENT_ID=your_client_id_here
|
||||
GOOGLE_CLIENT_SECRET=your_client_secret_here
|
||||
GOOGLE_REDIRECT_URI=http://localhost:19204/oauth2callback
|
||||
|
||||
# スプレッドシートID(Discord通知用、オプション)
|
||||
GOOGLE_SPREADSHEET_ID=your_spreadsheet_id_here
|
||||
|
||||
# Discord通知設定(オプション)
|
||||
DISCORD_WEBHOOK_URL=your_webhook_url_here
|
||||
|
||||
# Todoist API設定(オプション)
|
||||
TODOIST_API_KEY=your_todoist_api_token_here
|
||||
```
|
||||
|
||||
### 3. Google OAuth認証の初回セットアップ
|
||||
|
||||
**Google Cloud Consoleで認証情報を取得**:
|
||||
|
||||
1. [Google Cloud Console](https://console.cloud.google.com/) にアクセス
|
||||
2. 新規プロジェクトを作成
|
||||
3. 「APIとサービス」→「ライブラリ」から以下のAPIを有効化:
|
||||
- **Google Drive API**: https://console.cloud.google.com/apis/library/drive.googleapis.com
|
||||
4. 「APIとサービス」→「認証情報」
|
||||
5. 「OAuth 2.0 クライアントID」を作成
|
||||
- アプリケーションの種類: デスクトップアプリ
|
||||
- リダイレクトURI: `http://localhost:19204/oauth2callback`
|
||||
6. クライアントIDとクライアントシークレットを`.env`に設定
|
||||
|
||||
**初回認証トークン取得**:
|
||||
|
||||
```bash
|
||||
npm run gantt:auth # 初回OAuth認証フロー
|
||||
```
|
||||
|
||||
認証が完了すると `.google-token.json` が生成されます(.gitignoreに含まれます)。
|
||||
|
||||
### 4. GAS側の時間トリガー設定
|
||||
|
||||
**Google Apps ScriptにDRIVE_FOLDER_IDを設定**:
|
||||
|
||||
1. Apps Scriptエディタを開く
|
||||
2. `Config.gs`を開く
|
||||
3. `DRIVE_FOLDER_ID`に`.env`と同じフォルダIDを設定:
|
||||
```javascript
|
||||
DRIVE_FOLDER_ID: 'あなたのフォルダID',
|
||||
```
|
||||
|
||||
**時間ベーストリガーを追加**:
|
||||
|
||||
1. Apps Scriptエディタで左側の「トリガー」アイコン(⏰)をクリック
|
||||
2. 右下の「トリガーを追加」をクリック
|
||||
3. 以下のように設定:
|
||||
- **実行する関数を選択**: `checkForNewJsonFiles`
|
||||
- **イベントのソースを選択**: `時間主導型`
|
||||
- **時間ベースのトリガーのタイプを選択**: `分ベースのタイマー`
|
||||
- **時間の間隔を選択**: `1分おき`
|
||||
4. 「保存」をクリック
|
||||
|
||||
これで、1分ごとにGoogle Driveフォルダをチェックし、新しいJSONファイルが見つかると自動的にGanttチャートが生成されます。
|
||||
|
||||
**Todoist統合の追加設定(オプション)**:
|
||||
|
||||
Todoistタスクを同期する場合、以下の追加トリガーを設定してください:
|
||||
|
||||
1. **Todoistタスク同期トリガー**:
|
||||
- **実行する関数**: `syncTodoistTasks`
|
||||
- **イベントのソース**: `時間主導型`
|
||||
- **時間ベースのトリガーのタイプ**: `分ベースのタイマー`
|
||||
- **時間の間隔**: `30分おき` または `1時間おき`
|
||||
|
||||
2. **期日切れタスク通知トリガー**:
|
||||
- **実行する関数**: `sendOverdueTasksNotification`
|
||||
- **イベントのソース**: `時間主導型`
|
||||
- **時間ベースのトリガーのタイプ**: `日タイマー`
|
||||
- **時刻**: `午前9時~10時`
|
||||
|
||||
3. **Config.gsにTODOIST_API_KEYを設定**:
|
||||
```javascript
|
||||
TODOIST_API_KEY: 'your_todoist_api_token_here',
|
||||
```
|
||||
|
||||
Todoist APIトークンの取得方法:
|
||||
- [Todoist設定](https://todoist.com/app/settings/integrations/developer) → 「開発者」タブ
|
||||
- 「APIトークン」をコピー
|
||||
|
||||
### 5. ビルド(TypeScript → JavaScript変換)
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 使い方
|
||||
|
||||
### 基本ワークフロー
|
||||
|
||||
#### 1. プロジェクト作成開始
|
||||
|
||||
Claude Codeで以下のコマンドを実行:
|
||||
|
||||
```
|
||||
/gantt
|
||||
```
|
||||
|
||||
#### 2. 対話でプロジェクト情報を入力
|
||||
|
||||
システムが段階的に質問するので、順次回答:
|
||||
|
||||
- **Phase 1: 基本情報収集**
|
||||
- プロジェクトID: `PRJ001` など
|
||||
- プロジェクト名: `新規ECサイト開発`
|
||||
- プロジェクト目的: `個人事業` or `HRteam`
|
||||
- プロジェクトジャンル: 開発/マーケティング/イベント企画/業務改善/汎用
|
||||
- プロジェクト期限: `2025-06-30`
|
||||
|
||||
- **Phase 2-4: タスク設計**
|
||||
- タスクの追加・修正
|
||||
- 依存関係の設定
|
||||
- 担当者・優先度・工数の入力
|
||||
|
||||
#### 3. 完了を宣言
|
||||
|
||||
すべての情報入力が完了したら:
|
||||
|
||||
```
|
||||
完了
|
||||
```
|
||||
|
||||
#### 4. ファイル生成・保存
|
||||
|
||||
ヘルパースクリプトを実行:
|
||||
|
||||
```bash
|
||||
npm run gantt:save
|
||||
```
|
||||
|
||||
**生成されるファイル**:
|
||||
|
||||
- `outputs/{プロジェクトID}_{プロジェクト名}.json` - プロジェクトデータ(JSON)
|
||||
- `docs/{プロジェクトID}_{プロジェクト名}.md` - 対話履歴(Markdown)
|
||||
|
||||
ファイルは自動的にGoogle Driveにもアップロードされます。
|
||||
|
||||
#### 5. GASでGanttチャート作成
|
||||
|
||||
**✅ 自動実行(時間トリガー方式)**:
|
||||
|
||||
`npm run gantt:save` でJSONファイルをGoogle Driveにアップロード後、**1分以内に自動的にGanttチャートが生成されます**。
|
||||
|
||||
**動作フロー**:
|
||||
1. Node.jsがJSONファイルをGoogle Driveにアップロード
|
||||
2. GAS側の時間トリガー(1分ごと)がフォルダをチェック
|
||||
3. 未処理のJSONファイルを検出したら自動的に処理
|
||||
4. 処理済みファイル名を `processed_XXXX.json` に変更
|
||||
5. Discord通知が送信される(設定している場合)
|
||||
|
||||
**手動実行の場合**:
|
||||
1. スプレッドシートを開く
|
||||
2. メニュー「Ganttチャート」→「実際のJSONから生成」
|
||||
3. または、Apps Scriptエディタで`generateFromDriveFile()`関数を実行
|
||||
|
||||
---
|
||||
|
||||
## 📊 データ仕様
|
||||
|
||||
### プロジェクトJSON形式
|
||||
|
||||
```json
|
||||
{
|
||||
"project_id": "PRJ001",
|
||||
"project_name": "新規ECサイト開発",
|
||||
"project_purpose": "個人事業",
|
||||
"project_type": "開発",
|
||||
"project_deadline": "2025-06-30",
|
||||
"tasks": [
|
||||
{
|
||||
"task_id": "T001",
|
||||
"task_name": "要件定義",
|
||||
"start_date": "2025-01-01",
|
||||
"end_date": "2025-01-15",
|
||||
"assignee": "山田太郎",
|
||||
"dependencies": [],
|
||||
"progress": 0,
|
||||
"priority": "★★★★★",
|
||||
"parent_task_id": null,
|
||||
"tags": ["企画・計画"],
|
||||
"estimated_hours": 40,
|
||||
"is_milestone": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### タスクフィールド詳細
|
||||
|
||||
| フィールド | 型 | 説明 | 例 |
|
||||
|-----------|-----|------|-----|
|
||||
| `task_id` | string | タスクID(自動採番) | `T001`, `T002` |
|
||||
| `task_name` | string | タスク名 | `要件定義` |
|
||||
| `start_date` | string | 開始日(YYYY-MM-DD) | `2025-01-01` |
|
||||
| `end_date` | string | 終了日(YYYY-MM-DD) | `2025-01-15` |
|
||||
| `assignee` | string | 担当者(フリーテキスト) | `山田太郎` |
|
||||
| `dependencies` | array | 依存タスクID配列 | `["T001", "T002"]` |
|
||||
| `progress` | number | 進捗率(0-100) | `50` |
|
||||
| `priority` | string | 優先度(6段階★) | `★★★★★` |
|
||||
| `parent_task_id` | string\|null | 親タスクID | `T001` or `null` |
|
||||
| `tags` | array | タグ配列 | `["企画・計画", "技術"]` |
|
||||
| `estimated_hours` | number | 見積工数(時間) | `40` |
|
||||
| `is_milestone` | boolean | マイルストーンフラグ | `true` or `false` |
|
||||
|
||||
### 優先度(6段階)
|
||||
|
||||
- `★★★★★` - 最重要
|
||||
- `★★★★☆` - 重要
|
||||
- `★★★☆☆` - 中
|
||||
- `★★☆☆☆` - 普通
|
||||
- `★☆☆☆☆` - 低
|
||||
- `☆☆☆☆☆` - 最低
|
||||
|
||||
### タグ分類(10種類)
|
||||
|
||||
| タグ名 | 色 | 説明 |
|
||||
|--------|-----|------|
|
||||
| 企画・計画 | 青 | プロジェクト企画、要件定義 |
|
||||
| 設計 | 緑 | システム設計、UI/UX設計 |
|
||||
| 開発・実装 | 黄 | プログラミング、コーディング |
|
||||
| テスト・検証 | 橙 | テスト、品質保証 |
|
||||
| リリース・デプロイ | 赤 | リリース作業、デプロイ |
|
||||
| 運用・保守 | 紫 | 運用、保守、監視 |
|
||||
| マーケティング | ピンク | 広告、プロモーション |
|
||||
| 営業・商談 | 水色 | 営業活動、商談 |
|
||||
| 事務・管理 | グレー | 事務作業、管理業務 |
|
||||
| その他 | 白 | 上記以外 |
|
||||
|
||||
---
|
||||
|
||||
## 🔗 GAS統合
|
||||
|
||||
> **📚 詳細な導入手順**: [gas/README.md](gas/README.md) を参照してください
|
||||
|
||||
### GAS側の主な機能(✅ 実装完了)
|
||||
|
||||
#### 1. プロジェクト一覧シート
|
||||
|
||||
- 全プロジェクトのサマリー表示
|
||||
- プロジェクトID、名前、目的、ジャンル、期限、進捗率
|
||||
- ステータス管理(企画中/進行中/完了/保留/中止)
|
||||
|
||||
#### 2. 全プロジェクトタスクシート
|
||||
|
||||
- 全プロジェクトのタスクを一元管理
|
||||
- プロジェクトID + タスクIDでフィルタリング可能
|
||||
- 新規プロジェクト作成時に自動追加
|
||||
- onEditトリガーで自動同期
|
||||
- 手動更新ボタンあり
|
||||
|
||||
#### 3. 期日切れタスクシート
|
||||
|
||||
- 全プロジェクトから期日を過ぎたタスクを自動抽出
|
||||
- 期日切れ日数を表示(例: 3日遅れ)
|
||||
- プロジェクトごとにグループ化
|
||||
- Discord通知機能連携
|
||||
- 自動更新(全シート同期時)
|
||||
|
||||
#### 4. Todoistタスクシート(オプション)
|
||||
|
||||
- TodoistのInboxタスクを自動同期
|
||||
- 表示項目: タスクID、タスク名、説明、期日、優先度、ラベル、完了状態、Todoistリンク、最終更新日時
|
||||
- 優先度別の色分け表示(最高=赤、高=橙、中=黄)
|
||||
- 完了タスクは緑色で表示
|
||||
- クリック可能なTodoistリンク(HYPERLINK式)
|
||||
- 自動更新(トリガー設定で30分〜1時間ごと)
|
||||
|
||||
#### 5. 個別プロジェクトGanttチャートシート
|
||||
|
||||
- プロジェクトごとに1シート作成
|
||||
- タイムライン可視化(日単位)
|
||||
- 進捗バー表示
|
||||
- 依存関係の矢印表示
|
||||
- 列グループ化(担当者/優先度/タグ/工数は折りたたみ)
|
||||
|
||||
### GASトリガー設定
|
||||
|
||||
- **時間ベーストリガー** ✅: 1分ごとにGoogle Driveフォルダをチェックし、未処理JSONファイルを自動処理
|
||||
- **手動実行** ✅: `testGenerateGantt` または `generateFromDriveFile` 関数で即座にGanttチャート生成
|
||||
- **onOpenトリガー** ✅: スプレッドシート起動時のカスタムメニュー追加
|
||||
- **onEditトリガー** 🔲: シート編集時の自動同期(今後実装予定)
|
||||
|
||||
**時間トリガーの動作**:
|
||||
- 関数: `checkForNewJsonFiles`
|
||||
- 間隔: 1分おき
|
||||
- 処理: 未処理のJSONファイル(`processed_` で始まらないファイル)を検出して自動処理
|
||||
- 処理後: ファイル名を `processed_XXXX.json` に変更
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ トラブルシューティング
|
||||
|
||||
### Q1. `npm install` でエラーが発生する
|
||||
|
||||
**A**: Node.jsのバージョンを確認してください。推奨: Node.js 20.x 以上
|
||||
|
||||
```bash
|
||||
node --version # v20.x.x 以上を確認
|
||||
```
|
||||
|
||||
### Q2. Google認証がうまくいかない
|
||||
|
||||
**A**: 以下を確認:
|
||||
|
||||
1. `.env` ファイルのクライアントID・シークレットが正しいか
|
||||
2. Google Drive APIが有効化されているか
|
||||
3. リダイレクトURIが正確に設定されているか (`http://localhost:3000/oauth2callback`)
|
||||
|
||||
### Q3. JSONファイルが生成されない
|
||||
|
||||
**A**: 以下を確認:
|
||||
|
||||
1. `npm run gantt:save` を実行したか
|
||||
2. TypeScriptがビルドされているか (`npm run build`)
|
||||
3. ヘルパースクリプトでエラーが出ていないか
|
||||
|
||||
### Q4. Google Driveアップロードがスキップされる
|
||||
|
||||
**A**: 以下を確認:
|
||||
|
||||
1. `.env` に `GOOGLE_DRIVE_FOLDER_ID` が設定されているか
|
||||
2. `.google-token.json` が存在するか(初回認証が完了しているか)
|
||||
|
||||
### Q5. Ganttチャートが自動生成されない
|
||||
|
||||
**A**: 以下を確認:
|
||||
|
||||
1. **時間トリガーが正しく設定されているか**:
|
||||
- Apps Scriptエディタで「トリガー」アイコン(⏰)をクリック
|
||||
- `checkForNewJsonFiles` 関数のトリガーが「1分おき」で設定されているか確認
|
||||
|
||||
2. **Config.gsのDRIVE_FOLDER_IDが正しく設定されているか**:
|
||||
- `.env`のGOOGLE_DRIVE_FOLDER_IDと同じ値が設定されているか確認
|
||||
|
||||
3. **JSONファイルが正しいフォルダにアップロードされているか**:
|
||||
- Google Driveで該当フォルダを開き、JSONファイルが存在するか確認
|
||||
|
||||
4. **GASの実行ログを確認**:
|
||||
- Apps Scriptエディタで「実行ログ」を開く
|
||||
- `checkForNewJsonFiles` の実行ログを確認
|
||||
- エラーメッセージがあれば内容を確認
|
||||
|
||||
### Q6. 処理済みファイルが増えすぎた場合
|
||||
|
||||
**A**: 処理済みファイル(`processed_` で始まるファイル)は手動で削除またはアーカイブできます:
|
||||
|
||||
1. Google Driveで該当フォルダを開く
|
||||
2. `processed_` で始まるファイルを選択
|
||||
3. 別のフォルダに移動するか削除する
|
||||
|
||||
### Q7. 特定のJSONファイルだけ処理したい
|
||||
|
||||
**A**: 手動実行を使用してください:
|
||||
|
||||
1. Google DriveでJSONファイルを右クリック → 「リンクを取得」
|
||||
2. URLからファイルIDを取得(`/d/` と `/view` の間の文字列)
|
||||
3. Apps Scriptエディタで `Code.gs` を開く
|
||||
4. `processJsonFile('ファイルID')` を直接実行
|
||||
|
||||
### Q8. Todoistタスクが同期されない
|
||||
|
||||
**A**: 以下を確認:
|
||||
|
||||
1. **Config.gsのTODOIST_API_KEYが正しく設定されているか**:
|
||||
- [Todoist設定](https://todoist.com/app/settings/integrations/developer) でAPIトークンを確認
|
||||
- Config.gsに正しく貼り付けられているか確認
|
||||
|
||||
2. **トリガーが正しく設定されているか**:
|
||||
- Apps Scriptエディタで「トリガー」アイコン(⏰)をクリック
|
||||
- `syncTodoistTasks` 関数のトリガーが存在するか確認
|
||||
|
||||
3. **手動実行でテスト**:
|
||||
- Apps Scriptエディタで `testTodoistIntegration()` 関数を実行
|
||||
- 実行ログでエラーメッセージを確認
|
||||
|
||||
4. **Inboxプロジェクトが存在するか**:
|
||||
- Todoistアプリで「Inbox」プロジェクトが存在するか確認
|
||||
- プロジェクト名が正確に「Inbox」であることを確認(大文字小文字区別)
|
||||
|
||||
### Q9. 期日切れタスク通知が送信されない
|
||||
|
||||
**A**: 以下を確認:
|
||||
|
||||
1. **Discord Webhook URLが正しく設定されているか**:
|
||||
- Config.gsのDISCORD_WEBHOOK_URLが正しいか確認
|
||||
|
||||
2. **期日切れタスクが存在するか**:
|
||||
- 「期日切れタスク」シートを開いて実際に期日切れタスクが存在するか確認
|
||||
|
||||
3. **トリガーが正しく設定されているか**:
|
||||
- `sendOverdueTasksNotification` 関数の日タイマーが設定されているか確認
|
||||
|
||||
4. **手動実行でテスト**:
|
||||
- `testOverdueTasksFeature()` 関数を実行してDiscord通知をテスト
|
||||
|
||||
---
|
||||
|
||||
## 📝 ライセンス
|
||||
|
||||
MIT License
|
||||
|
||||
---
|
||||
|
||||
## 🙏 貢献
|
||||
|
||||
プルリクエストを歓迎します!バグ報告や機能要望はIssuesでお願いします。
|
||||
|
||||
---
|
||||
|
||||
## 📞 サポート
|
||||
|
||||
質問・問題がある場合は、Issuesでお問い合わせください。
|
||||
19
gas/.claspignore
Normal file
19
gas/.claspignore
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Documentation
|
||||
README.md
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Test files
|
||||
*.test.gs
|
||||
*.spec.gs
|
||||
|
||||
# Local config backups
|
||||
*.backup.gs
|
||||
GanttRenderer.gs.gs
|
||||
1084
gas/Code.gs
Normal file
1084
gas/Code.gs
Normal file
File diff suppressed because it is too large
Load diff
3211
gas/Code.gs.backup
Normal file
3211
gas/Code.gs.backup
Normal file
File diff suppressed because it is too large
Load diff
97
gas/Config.gs
Normal file
97
gas/Config.gs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* 設定管理
|
||||
*/
|
||||
|
||||
const CONFIG = {
|
||||
// スプレッドシートID
|
||||
SPREADSHEET_ID: '1y7U-3hVfdubQPh-H39k-5bvuHF7cOkHkOuA121GtxCI',
|
||||
|
||||
// Google DriveフォルダID(JSONファイルアップロード先)
|
||||
// .envのGOOGLE_DRIVE_FOLDER_IDと同じ値を設定してください
|
||||
DRIVE_FOLDER_ID: '1UEOEdzxO6yLlwgDS7NF4EcdicHTTAgTT',
|
||||
|
||||
// Discord Webhook URL
|
||||
DISCORD_WEBHOOK_URL: 'https://discord.com/api/webhooks/1450654318612185221/0wfVPnUJJ8uvfD_Y4nZ901o9ezqx2H2TGDwN27s9-Yge4nOAQ4gr96UBAlH7PyMCAhcS',
|
||||
|
||||
// Todoist API Token
|
||||
TODOIST_API_KEY: 'dcbc9cfc8aa52fce62a50a09e3ccecd0200faf15',
|
||||
|
||||
// シート名
|
||||
SHEET_NAMES: {
|
||||
PROJECT_LIST: 'プロジェクト一覧',
|
||||
ALL_TASKS: '全プロジェクトタスク',
|
||||
OVERDUE_TASKS: '期日切れタスク',
|
||||
TODOIST_TASKS: 'Todoistタスク',
|
||||
GANTT_PREFIX: 'Gantt_' // プロジェクトIDが後ろに付く
|
||||
},
|
||||
|
||||
// タグごとの色設定
|
||||
TAG_COLORS: {
|
||||
'企画・計画': '#4A90E2',
|
||||
'設計': '#66BB6A',
|
||||
'開発・実装': '#FDD835',
|
||||
'テスト・検証': '#FF9800',
|
||||
'リリース・デプロイ': '#E53935',
|
||||
'運用・保守': '#AB47BC',
|
||||
'マーケティング': '#26C6DA',
|
||||
'営業・商談': '#26C6DA',
|
||||
'事務・管理': '#78909C',
|
||||
'その他': '#BDBDBD',
|
||||
'デフォルト': '#999999'
|
||||
},
|
||||
|
||||
// バーンダウンチャートの色設定
|
||||
BURNDOWN_COLORS: {
|
||||
EXPECTED_LINE: '#4A90E2', // 予定進捗ライン(青)
|
||||
ACTUAL_LINE: '#66BB6A', // 実績進捗ライン(緑)
|
||||
WARNING_LINE: '#FF9800', // 警告ライン(オレンジ)
|
||||
DANGER_ZONE: '#FFCDD2', // 遅延ゾーン背景(薄い赤)
|
||||
SAFE_ZONE: '#C8E6C9' // 順調ゾーン背景(薄い緑)
|
||||
},
|
||||
|
||||
// 優先度の色設定
|
||||
PRIORITY_COLORS: {
|
||||
'高': '#FF6B6B',
|
||||
'中': '#FFA726',
|
||||
'低': '#66BB6A'
|
||||
},
|
||||
|
||||
// Ganttチャートの設定
|
||||
GANTT: {
|
||||
START_COLUMN: 13, // M列から開始(A=1, M=13) - L列に実績工数を追加
|
||||
HEADER_ROW: 1,
|
||||
DATA_START_ROW: 2,
|
||||
DAYS_TO_SHOW: 180, // デフォルト表示日数
|
||||
CELL_WIDTH: 30 // セルの幅(ピクセル)
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Discord通知を送信
|
||||
*/
|
||||
function sendDiscordNotification(message, isError = false) {
|
||||
const payload = {
|
||||
embeds: [{
|
||||
title: isError ? '❌ GAS実行エラー' : '✅ GAS実行完了',
|
||||
description: message,
|
||||
color: isError ? 0xFF0000 : 0x00FF00,
|
||||
timestamp: new Date().toISOString(),
|
||||
footer: {
|
||||
text: 'Gantt Chart Generator (GAS)'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
const options = {
|
||||
method: 'post',
|
||||
contentType: 'application/json',
|
||||
payload: JSON.stringify(payload),
|
||||
muteHttpExceptions: true
|
||||
};
|
||||
|
||||
try {
|
||||
UrlFetchApp.fetch(CONFIG.DISCORD_WEBHOOK_URL, options);
|
||||
} catch (error) {
|
||||
Logger.log('Discord通知エラー: ' + error);
|
||||
}
|
||||
}
|
||||
758
gas/GanttRenderer.gs
Normal file
758
gas/GanttRenderer.gs
Normal file
|
|
@ -0,0 +1,758 @@
|
|||
/**
|
||||
* Ganttチャート描画クラス
|
||||
*/
|
||||
|
||||
class GanttRenderer {
|
||||
constructor(sheet, projectData) {
|
||||
this.sheet = sheet;
|
||||
this.projectData = projectData;
|
||||
this.tasks = projectData.tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ganttチャート全体を描画
|
||||
*/
|
||||
render() {
|
||||
// プロジェクト期間を計算
|
||||
const dateRange = this._calculateDateRange();
|
||||
|
||||
// 日付ヘッダーを描画(1行目)
|
||||
this._renderDateHeaders(dateRange);
|
||||
|
||||
// タスク情報列のヘッダーを描画(2行目)
|
||||
this._renderTaskHeaders();
|
||||
|
||||
// タスク情報を描画(4行目から)
|
||||
this._renderTaskInfo();
|
||||
|
||||
// 入力規則とスタイルを設定(タスク行作成後に適用)
|
||||
this._applyStyles();
|
||||
|
||||
// 行の破線を設定(タイムライン含む全列)
|
||||
this._applyRowBorders(dateRange);
|
||||
|
||||
// Ganttバーを描画(実線枠線で破線を上書き)
|
||||
this._renderGanttBars(dateRange);
|
||||
|
||||
// 不要な行を削除
|
||||
this._cleanupUnusedRows();
|
||||
}
|
||||
|
||||
/**
|
||||
* プロジェクト期間を計算
|
||||
*/
|
||||
_calculateDateRange() {
|
||||
const dates = this.tasks
|
||||
.filter(task => task.start_date && task.end_date)
|
||||
.flatMap(task => [new Date(task.start_date), new Date(task.end_date)]);
|
||||
|
||||
if (dates.length === 0) {
|
||||
const today = new Date();
|
||||
return {
|
||||
start: today,
|
||||
end: new Date(today.getTime() + 90 * 24 * 60 * 60 * 1000) // 90日後
|
||||
};
|
||||
}
|
||||
|
||||
const minDate = new Date(Math.min(...dates));
|
||||
const maxDate = new Date(Math.max(...dates));
|
||||
|
||||
// 前後に余裕を持たせる
|
||||
minDate.setDate(minDate.getDate() - 7);
|
||||
maxDate.setDate(maxDate.getDate() + 7);
|
||||
|
||||
return { start: minDate, end: maxDate };
|
||||
}
|
||||
|
||||
/**
|
||||
* 日付ヘッダーを描画(月と日)
|
||||
*/
|
||||
_renderDateHeaders(dateRange) {
|
||||
const startCol = CONFIG.GANTT.START_COLUMN;
|
||||
|
||||
// 日付を正規化(時刻部分を除去)して日数を計算
|
||||
const startDate = new Date(dateRange.start);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
const endDate = new Date(dateRange.end);
|
||||
endDate.setHours(0, 0, 0, 0);
|
||||
|
||||
const daysDiff = Math.round((endDate - startDate) / (1000 * 60 * 60 * 24)) + 1;
|
||||
|
||||
Logger.log(`日付範囲: ${dateRange.start} 〜 ${dateRange.end}`);
|
||||
Logger.log(`正規化後: ${startDate} 〜 ${endDate}`);
|
||||
Logger.log(`日数: ${daysDiff}日`);
|
||||
|
||||
// Google Sheetsの列数制限チェック(18,278列)
|
||||
if (startCol + daysDiff > 18278) {
|
||||
throw new Error(`列数が上限を超えています: ${startCol + daysDiff} > 18278`);
|
||||
}
|
||||
|
||||
const monthRow = [];
|
||||
const dayRow = [];
|
||||
let currentDate = new Date(startDate); // 正規化済みの開始日を使用
|
||||
const monthRanges = []; // 結合する範囲を記録
|
||||
|
||||
let currentMonth = null;
|
||||
let monthStartCol = 0;
|
||||
|
||||
// 月と日を分けて2行に(月は各月の最初のセルのみ表示、それ以降は空白)
|
||||
for (let i = 0; i < daysDiff; i++) {
|
||||
const month = currentDate.getMonth() + 1;
|
||||
const day = currentDate.getDate();
|
||||
|
||||
// 月が変わったときのみ月を表示、それ以外は空白
|
||||
if (month !== currentMonth) {
|
||||
monthRow.push(month + '月');
|
||||
if (currentMonth !== null) {
|
||||
monthRanges.push({ start: monthStartCol, length: i - monthStartCol });
|
||||
}
|
||||
currentMonth = month;
|
||||
monthStartCol = i;
|
||||
} else {
|
||||
monthRow.push(''); // 空白
|
||||
}
|
||||
|
||||
dayRow.push(day);
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
|
||||
// 最後の月の結合範囲を記録
|
||||
monthRanges.push({ start: monthStartCol, length: daysDiff - monthStartCol });
|
||||
|
||||
Logger.log(`日付行配列サイズ: 月=${monthRow.length}, 日=${dayRow.length}`);
|
||||
|
||||
// ヘッダー設定(1行目:月、2行目:日)
|
||||
this.sheet.getRange(1, startCol, 1, daysDiff).setValues([monthRow]);
|
||||
this.sheet.getRange(2, startCol, 1, daysDiff).setValues([dayRow]);
|
||||
|
||||
// 2色交互の色付け(セル結合は行わない)
|
||||
monthRanges.forEach((range, index) => {
|
||||
const rangeObj = this.sheet.getRange(1, startCol + range.start, 1, range.length);
|
||||
|
||||
// 2色交互に色付け(#E8F4FD と #D6EAF8)
|
||||
const bgColor = index % 2 === 0 ? '#E8F4FD' : '#D6EAF8';
|
||||
rangeObj.setBackground(bgColor);
|
||||
rangeObj.setHorizontalAlignment('center');
|
||||
rangeObj.setVerticalAlignment('middle');
|
||||
|
||||
// 日の行も同じ色で塗る
|
||||
this.sheet.getRange(2, startCol + range.start, 1, range.length)
|
||||
.setBackground(bgColor)
|
||||
.setHorizontalAlignment('center')
|
||||
.setVerticalAlignment('middle');
|
||||
});
|
||||
|
||||
// 列幅を調整
|
||||
for (let i = 0; i < daysDiff; i++) {
|
||||
this.sheet.setColumnWidth(startCol + i, CONFIG.GANTT.CELL_WIDTH);
|
||||
}
|
||||
|
||||
// 固定行を設定(月1行 + 日1行 = 2行)
|
||||
this.sheet.setFrozenRows(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* タスク情報列のヘッダーを描画(2行目)
|
||||
*/
|
||||
_renderTaskHeaders() {
|
||||
const headers = [
|
||||
'ID',
|
||||
'親タスク名',
|
||||
'子タスク名',
|
||||
'タグ',
|
||||
'ステータス',
|
||||
'開始日',
|
||||
'終了日',
|
||||
'進捗率',
|
||||
'担当者',
|
||||
'優先度',
|
||||
'工数見積(h)',
|
||||
'実績工数(h)'
|
||||
];
|
||||
|
||||
const headerRange = this.sheet.getRange(2, 1, 1, headers.length);
|
||||
headerRange.setValues([headers]);
|
||||
headerRange.setBackground('#E8F4FD');
|
||||
headerRange.setHorizontalAlignment('center');
|
||||
headerRange.setVerticalAlignment('middle');
|
||||
|
||||
// 1行目のA~L列にも同じ青色を適用
|
||||
const firstRowRange = this.sheet.getRange(1, 1, 1, headers.length);
|
||||
firstRowRange.setBackground('#E8F4FD');
|
||||
firstRowRange.setHorizontalAlignment('center');
|
||||
firstRowRange.setVerticalAlignment('middle');
|
||||
}
|
||||
|
||||
/**
|
||||
* タスクが期日切れかどうかを判定
|
||||
*/
|
||||
_isOverdue(task) {
|
||||
if (!task.end_date || task.progress === 100) return false;
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const endDate = new Date(task.end_date);
|
||||
endDate.setHours(0, 0, 0, 0);
|
||||
|
||||
const isOverdue = endDate < today;
|
||||
return isOverdue;
|
||||
}
|
||||
|
||||
/**
|
||||
* タスク情報を描画
|
||||
*/
|
||||
_renderTaskInfo() {
|
||||
const startRow = 4; // 月1行 + 日(列名)1行 + 空白行1行 + データ開始
|
||||
|
||||
const taskData = this.tasks.map(task => {
|
||||
// 親タスク名と子タスク名を設定
|
||||
let parentTaskName = '';
|
||||
let childTaskName = '';
|
||||
|
||||
if (task.parent_task_id) {
|
||||
// 子タスクの場合:B列は空白、C列に子タスク名
|
||||
parentTaskName = '';
|
||||
childTaskName = task.task_name;
|
||||
} else {
|
||||
// 親タスクの場合:B列に親タスク名、C列は空白
|
||||
parentTaskName = task.task_name;
|
||||
childTaskName = '';
|
||||
}
|
||||
|
||||
// タグは配列の場合、「フェーズ」以外の最初の1つ
|
||||
let tag = '';
|
||||
if (Array.isArray(task.tags) && task.tags.length > 0) {
|
||||
// 「フェーズ」以外のタグを探す
|
||||
const validTags = task.tags.filter(t => t !== 'フェーズ');
|
||||
tag = validTags.length > 0 ? validTags[0] : '';
|
||||
} else if (task.tags && task.tags !== 'フェーズ') {
|
||||
tag = task.tags;
|
||||
}
|
||||
|
||||
// ステータスを進捗率から判定
|
||||
const progress = task.progress || 0;
|
||||
let status = '';
|
||||
if (progress === 0) {
|
||||
status = '未着手';
|
||||
} else if (progress === 100) {
|
||||
status = '完了';
|
||||
} else {
|
||||
status = '進行中';
|
||||
}
|
||||
|
||||
return [
|
||||
task.task_id,
|
||||
parentTaskName,
|
||||
childTaskName,
|
||||
tag,
|
||||
status,
|
||||
task.start_date || '',
|
||||
task.end_date || '',
|
||||
progress / 100, // 0.5 = 50% (条件付き書式用)
|
||||
task.assignee || '',
|
||||
task.priority || '',
|
||||
task.estimated_hours || '', // 数値のみ
|
||||
task.actual_hours || '' // 実績工数
|
||||
];
|
||||
});
|
||||
|
||||
if (taskData.length > 0) {
|
||||
this.sheet.getRange(startRow, 1, taskData.length, taskData[0].length).setValues(taskData);
|
||||
|
||||
// 親タスク(parent_task_idがnull)を太字にする(1行全体)
|
||||
// タイムライン最後の列を計算
|
||||
const dateRange = this._calculateDateRange();
|
||||
|
||||
// 日付を正規化して日数を計算
|
||||
const startDate = new Date(dateRange.start);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
const endDate = new Date(dateRange.end);
|
||||
endDate.setHours(0, 0, 0, 0);
|
||||
const daysDiff = Math.round((endDate - startDate) / (1000 * 60 * 60 * 24)) + 1;
|
||||
|
||||
const lastCol = CONFIG.GANTT.START_COLUMN + daysDiff;
|
||||
|
||||
// 全タスク行の背景色をクリア(過去の色付けをリセット)
|
||||
const allTasksRange = this.sheet.getRange(startRow, 1, this.tasks.length, lastCol);
|
||||
allTasksRange.setBackground(null);
|
||||
|
||||
// 全タスクの終了日セル(G列)の背景色と文字色をリセット
|
||||
const endDateCol = this.sheet.getRange(startRow, 7, this.tasks.length, 1);
|
||||
endDateCol.setBackground('#FFFFFF'); // 白背景
|
||||
endDateCol.setFontColor('#000000'); // 黒文字
|
||||
endDateCol.setFontWeight('normal'); // 通常の太さ
|
||||
|
||||
for (let i = 0; i < this.tasks.length; i++) {
|
||||
if (!this.tasks[i].parent_task_id) {
|
||||
// タスク行全体を太字に(A列からタイムライン最後まで)
|
||||
const rowRange = this.sheet.getRange(startRow + i, 1, 1, lastCol);
|
||||
rowRange.setFontWeight('bold');
|
||||
}
|
||||
|
||||
// 期日切れタスクの終了日セル(G列)を強調
|
||||
if (this._isOverdue(this.tasks[i])) {
|
||||
const endDateCell = this.sheet.getRange(startRow + i, 7);
|
||||
endDateCell.setBackground('#FF0000'); // 赤背景
|
||||
endDateCell.setFontColor('#FFFF00'); // 黄色文字
|
||||
endDateCell.setFontWeight('bold'); // 太字
|
||||
}
|
||||
}
|
||||
|
||||
// ステータス列(E列)に条件付き書式で色分け
|
||||
const statusCol = this.sheet.getRange(startRow, 5, taskData.length, 1);
|
||||
let rules = this.sheet.getConditionalFormatRules();
|
||||
|
||||
// 未着手 = グレー
|
||||
const ruleNotStarted = SpreadsheetApp.newConditionalFormatRule()
|
||||
.whenTextEqualTo('未着手')
|
||||
.setBackground('#E0E0E0')
|
||||
.setRanges([statusCol])
|
||||
.build();
|
||||
rules.push(ruleNotStarted);
|
||||
|
||||
// 進行中 = 黄色
|
||||
const ruleInProgress = SpreadsheetApp.newConditionalFormatRule()
|
||||
.whenTextEqualTo('進行中')
|
||||
.setBackground('#FFF59D')
|
||||
.setRanges([statusCol])
|
||||
.build();
|
||||
rules.push(ruleInProgress);
|
||||
|
||||
// 完了 = 緑(E列のみ)
|
||||
const ruleCompleted = SpreadsheetApp.newConditionalFormatRule()
|
||||
.whenTextEqualTo('完了')
|
||||
.setBackground('#A5D6A7')
|
||||
.setRanges([statusCol])
|
||||
.build();
|
||||
rules.push(ruleCompleted);
|
||||
|
||||
// 中断 = 赤(E列のみ)
|
||||
const ruleInterrupted = SpreadsheetApp.newConditionalFormatRule()
|
||||
.whenTextEqualTo('中断')
|
||||
.setBackground('#FF6B6B')
|
||||
.setRanges([statusCol])
|
||||
.build();
|
||||
rules.push(ruleInterrupted);
|
||||
|
||||
// 完了・中断の行全体をグレーにする(全列対象)
|
||||
const allRowsRange = this.sheet.getRange(startRow, 1, taskData.length, lastCol);
|
||||
|
||||
// 完了行全体 = グレー
|
||||
const ruleCompletedRow = SpreadsheetApp.newConditionalFormatRule()
|
||||
.whenFormulaSatisfied(`=$E${startRow}="完了"`)
|
||||
.setBackground('#D3D3D3')
|
||||
.setRanges([allRowsRange])
|
||||
.build();
|
||||
rules.push(ruleCompletedRow);
|
||||
|
||||
// 中断行全体 = グレー
|
||||
const ruleInterruptedRow = SpreadsheetApp.newConditionalFormatRule()
|
||||
.whenFormulaSatisfied(`=$E${startRow}="中断"`)
|
||||
.setBackground('#D3D3D3')
|
||||
.setRanges([allRowsRange])
|
||||
.build();
|
||||
rules.push(ruleInterruptedRow);
|
||||
|
||||
// 日付列(F列・G列)のフォーマットを「yy/mm/dd」に設定
|
||||
const dateColStart = this.sheet.getRange(startRow, 6, taskData.length, 1); // F列(開始日)
|
||||
const dateColEnd = this.sheet.getRange(startRow, 7, taskData.length, 1); // G列(終了日)
|
||||
dateColStart.setNumberFormat('yy/mm/dd');
|
||||
dateColEnd.setNumberFormat('yy/mm/dd');
|
||||
|
||||
// 進捗列(H列)に条件付き書式でバー表示
|
||||
const progressCol = this.sheet.getRange(startRow, 8, taskData.length, 1);
|
||||
progressCol.setNumberFormat('0%'); // パーセント表示
|
||||
|
||||
// データバーの条件付き書式を追加
|
||||
const ruleProgress = SpreadsheetApp.newConditionalFormatRule()
|
||||
.setGradientMaxpointWithValue('#4A90E2', SpreadsheetApp.InterpolationType.NUMBER, '1')
|
||||
.setGradientMinpointWithValue('#FFFFFF', SpreadsheetApp.InterpolationType.NUMBER, '0')
|
||||
.setRanges([progressCol])
|
||||
.build();
|
||||
rules.push(ruleProgress);
|
||||
|
||||
this.sheet.setConditionalFormatRules(rules);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ganttバーを描画
|
||||
*/
|
||||
_renderGanttBars(dateRange) {
|
||||
const startCol = CONFIG.GANTT.START_COLUMN;
|
||||
const startRow = 4; // タスクデータ開始行に合わせる
|
||||
|
||||
this.tasks.forEach((task, index) => {
|
||||
if (!task.start_date || !task.end_date) return;
|
||||
|
||||
const taskStart = new Date(task.start_date);
|
||||
const taskEnd = new Date(task.end_date);
|
||||
|
||||
// タスクの開始位置と期間を計算
|
||||
const daysFromStart = Math.ceil((taskStart - dateRange.start) / (1000 * 60 * 60 * 24));
|
||||
const duration = Math.ceil((taskEnd - taskStart) / (1000 * 60 * 60 * 24)) + 1;
|
||||
|
||||
if (daysFromStart >= 0 && duration > 0) {
|
||||
const barRange = this.sheet.getRange(
|
||||
startRow + index,
|
||||
startCol + daysFromStart,
|
||||
1,
|
||||
duration
|
||||
);
|
||||
|
||||
// 親=赤、子=青で色分け
|
||||
const color = task.parent_task_id ? '#4A90E2' : '#FF6B6B'; // 子=青、親=赤
|
||||
const progress = task.progress || 0;
|
||||
|
||||
// バーの背景色を設定
|
||||
barRange.setBackground(color);
|
||||
|
||||
// 進捗バーを描画(グラデーション風)
|
||||
if (progress < 100) {
|
||||
const progressCols = Math.ceil(duration * progress / 100);
|
||||
if (progressCols > 0) {
|
||||
const progressRange = this.sheet.getRange(
|
||||
startRow + index,
|
||||
startCol + daysFromStart,
|
||||
1,
|
||||
progressCols
|
||||
);
|
||||
progressRange.setBackground(this._darkenColor(color));
|
||||
}
|
||||
}
|
||||
|
||||
// マイルストーンの場合はダイヤモンド記号を追加
|
||||
if (task.is_milestone) {
|
||||
this.sheet.getRange(startRow + index, startCol + daysFromStart).setValue('◆');
|
||||
}
|
||||
|
||||
// セルに罫線を追加
|
||||
barRange.setBorder(true, true, true, true, false, false, '#000000', SpreadsheetApp.BorderStyle.SOLID_MEDIUM);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* タスクの色を取得
|
||||
*/
|
||||
_getTaskColor(task) {
|
||||
const tags = Array.isArray(task.tags) ? task.tags : [task.tags];
|
||||
const firstTag = tags[0];
|
||||
|
||||
return CONFIG.TAG_COLORS[firstTag] || CONFIG.TAG_COLORS['デフォルト'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 色を暗くする(進捗表示用)
|
||||
*/
|
||||
_darkenColor(hexColor) {
|
||||
// HEXからRGBに変換
|
||||
const r = parseInt(hexColor.slice(1, 3), 16);
|
||||
const g = parseInt(hexColor.slice(3, 5), 16);
|
||||
const b = parseInt(hexColor.slice(5, 7), 16);
|
||||
|
||||
// 70%の明度に
|
||||
const newR = Math.floor(r * 0.7);
|
||||
const newG = Math.floor(g * 0.7);
|
||||
const newB = Math.floor(b * 0.7);
|
||||
|
||||
// RGBからHEXに変換
|
||||
return '#' + [newR, newG, newB].map(x => x.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 期間を計算
|
||||
*/
|
||||
_calculateDuration(startDate, endDate) {
|
||||
if (!startDate || !endDate) return '';
|
||||
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
|
||||
|
||||
return days; // 数値のみ返す
|
||||
}
|
||||
|
||||
/**
|
||||
* スタイルを適用
|
||||
*/
|
||||
_applyStyles() {
|
||||
const dataRange = this.sheet.getDataRange();
|
||||
dataRange.setVerticalAlignment('middle');
|
||||
|
||||
// グリッドを非表示
|
||||
this.sheet.setHiddenGridlines(true);
|
||||
|
||||
// ID(A列)・親タスク名(B列)・子タスク名(C列)の列幅を自動調整
|
||||
this.sheet.autoResizeColumn(1); // A列(ID)
|
||||
this.sheet.autoResizeColumn(2); // B列(親タスク名)
|
||||
this.sheet.autoResizeColumn(3); // C列(子タスク名)
|
||||
|
||||
// 行の高さを25に設定(4行目以降のタスクデータ行)
|
||||
if (this.tasks.length > 0) {
|
||||
this.sheet.setRowHeights(4, this.tasks.length, 25);
|
||||
|
||||
// 親タスク名・子タスク名の列を左寄せ
|
||||
const parentTaskNameCol = this.sheet.getRange(4, 2, this.tasks.length, 1);
|
||||
parentTaskNameCol.setHorizontalAlignment('left');
|
||||
const childTaskNameCol = this.sheet.getRange(4, 3, this.tasks.length, 1);
|
||||
childTaskNameCol.setHorizontalAlignment('left');
|
||||
|
||||
// タグ列に入力規則を設定
|
||||
const tagOptions = [
|
||||
'企画・計画',
|
||||
'設計',
|
||||
'開発・実装',
|
||||
'テスト・検証',
|
||||
'リリース・デプロイ',
|
||||
'運用・保守',
|
||||
'マーケティング',
|
||||
'営業・商談',
|
||||
'事務・管理',
|
||||
'その他'
|
||||
];
|
||||
const tagCol = this.sheet.getRange(4, 4, this.tasks.length, 1); // D列(タグ)
|
||||
const tagRule = SpreadsheetApp.newDataValidation()
|
||||
.requireValueInList(tagOptions, true) // true = ドロップダウン表示
|
||||
.setAllowInvalid(true) // 選択肢以外の値も許可
|
||||
.build();
|
||||
tagCol.setDataValidation(tagRule);
|
||||
|
||||
// ステータス列に入力規則を設定
|
||||
const statusOptions = [
|
||||
'未着手',
|
||||
'進行中',
|
||||
'完了',
|
||||
'中断'
|
||||
];
|
||||
const statusCol = this.sheet.getRange(4, 5, this.tasks.length, 1); // E列(ステータス)
|
||||
const statusRule = SpreadsheetApp.newDataValidation()
|
||||
.requireValueInList(statusOptions, true) // true = ドロップダウン表示
|
||||
.setAllowInvalid(false)
|
||||
.build();
|
||||
statusCol.setDataValidation(statusRule);
|
||||
|
||||
// タグ列に色付き条件付き書式を設定
|
||||
const tagColors = CONFIG.TAG_COLORS;
|
||||
|
||||
// まずタグ列全体にデフォルト色を適用
|
||||
tagCol.setBackground(tagColors['デフォルト']);
|
||||
tagCol.setFontColor('#FFFFFF');
|
||||
|
||||
let rules = this.sheet.getConditionalFormatRules();
|
||||
Object.keys(tagColors).forEach(tag => {
|
||||
// 'デフォルト'はスキップ(特定のタグ名ではないため)
|
||||
if (tag === 'デフォルト') return;
|
||||
|
||||
const rule = SpreadsheetApp.newConditionalFormatRule()
|
||||
.whenTextEqualTo(tag)
|
||||
.setBackground(tagColors[tag])
|
||||
.setFontColor('#FFFFFF') // 白文字
|
||||
.setRanges([tagCol])
|
||||
.build();
|
||||
rules.push(rule);
|
||||
});
|
||||
this.sheet.setConditionalFormatRules(rules);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 行の破線を適用(全列)
|
||||
*/
|
||||
_applyRowBorders(dateRange) {
|
||||
if (this.tasks.length === 0) return;
|
||||
|
||||
// タイムライン列の範囲を計算
|
||||
const daysDiff = Math.ceil((dateRange.end - dateRange.start) / (1000 * 60 * 60 * 24));
|
||||
const lastCol = CONFIG.GANTT.START_COLUMN + daysDiff;
|
||||
|
||||
// 最初のタスク行(4行目)の上に破線を追加
|
||||
const firstRowRange = this.sheet.getRange(4, 1, 1, lastCol);
|
||||
firstRowRange.setBorder(
|
||||
true, null, null, null, // top, left, bottom, right
|
||||
null, null, // vertical, horizontal
|
||||
'#CCCCCC', // グレー
|
||||
SpreadsheetApp.BorderStyle.DASHED // 破線
|
||||
);
|
||||
|
||||
// 各タスク行の下に破線を追加(全列)
|
||||
for (let i = 0; i < this.tasks.length; i++) {
|
||||
const rowRange = this.sheet.getRange(4 + i, 1, 1, lastCol);
|
||||
rowRange.setBorder(
|
||||
null, null, true, null, // top, left, bottom, right
|
||||
null, null, // vertical, horizontal
|
||||
'#CCCCCC', // グレー
|
||||
SpreadsheetApp.BorderStyle.DASHED // 破線
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 不要な行を削除(タスクデータの最後の行から1行空けてそれ以降を削除)
|
||||
*/
|
||||
_cleanupUnusedRows() {
|
||||
if (this.tasks.length === 0) return;
|
||||
|
||||
// タスクデータの最後の行 = 3行(ヘッダー) + タスク数
|
||||
const lastTaskRow = 3 + this.tasks.length;
|
||||
|
||||
// 1行空けた次の行から削除開始
|
||||
const deleteStartRow = lastTaskRow + 2;
|
||||
|
||||
// シートの最大行数を取得
|
||||
const maxRows = this.sheet.getMaxRows();
|
||||
|
||||
// 削除する行数を計算
|
||||
const rowsToDelete = maxRows - deleteStartRow + 1;
|
||||
|
||||
// 削除する行が存在する場合のみ削除
|
||||
if (rowsToDelete > 0 && deleteStartRow <= maxRows) {
|
||||
this.sheet.deleteRows(deleteStartRow, rowsToDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ブロックされているタスクをGanttシートでハイライト
|
||||
*
|
||||
* 遅延タスクの影響を受けているタスクを赤色背景でハイライトし、
|
||||
* セルにメモを追加します。
|
||||
*
|
||||
* @param {Sheet} sheet - Ganttシート
|
||||
* @param {Object} projectData - プロジェクトデータ
|
||||
* @param {Object} impactReport - analyzeDependencyImpact()の戻り値
|
||||
*/
|
||||
function highlightBlockedTasks(sheet, projectData, impactReport) {
|
||||
try {
|
||||
// 影響を受けるタスクIDをすべて収集
|
||||
const allImpactedTaskIds = new Set();
|
||||
|
||||
for (const delayedTaskId in impactReport.impactedTasks) {
|
||||
const impact = impactReport.impactedTasks[delayedTaskId];
|
||||
for (const taskId of impact.impactedTaskIds) {
|
||||
allImpactedTaskIds.add(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
if (allImpactedTaskIds.size === 0) {
|
||||
Logger.log('✓ ブロックされているタスクがないため、ハイライトをスキップ');
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.log(`--- ${allImpactedTaskIds.size}件のブロックタスクをハイライト ---`);
|
||||
|
||||
// タスクIDから行番号を取得(4行目から開始)
|
||||
const taskMap = {};
|
||||
for (let i = 0; i < projectData.tasks.length; i++) {
|
||||
const task = projectData.tasks[i];
|
||||
taskMap[task.task_id] = 4 + i; // 4行目からタスクデータが始まる
|
||||
}
|
||||
|
||||
// ブロックされているタスクの行をハイライト
|
||||
for (const taskId of allImpactedTaskIds) {
|
||||
const rowNum = taskMap[taskId];
|
||||
|
||||
if (!rowNum) {
|
||||
Logger.log(`⚠ タスクID ${taskId} の行が見つかりません`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// タスク情報列(A~L列の12列)を赤色背景でハイライト
|
||||
const range = sheet.getRange(rowNum, 1, 1, 12);
|
||||
range.setBackground('#FFCDD2'); // 赤色背景
|
||||
|
||||
// タスク名セル(C列)にメモを追加
|
||||
const taskNameCell = sheet.getRange(rowNum, 3);
|
||||
const currentNote = taskNameCell.getNote();
|
||||
const blockerNote = '⚠️ ブロック中: 依存タスクが遅延しています';
|
||||
|
||||
if (!currentNote.includes(blockerNote)) {
|
||||
const newNote = currentNote ? currentNote + '\n\n' + blockerNote : blockerNote;
|
||||
taskNameCell.setNote(newNote);
|
||||
}
|
||||
|
||||
Logger.log(` ハイライト完了: ${taskId} (行${rowNum})`);
|
||||
}
|
||||
|
||||
Logger.log(`✓ ${allImpactedTaskIds.size}件のタスクをハイライトしました`);
|
||||
|
||||
} catch (error) {
|
||||
Logger.log(`✗ ハイライトエラー: ${error.message}`);
|
||||
Logger.log(`✗ スタックトレース: ${error.stack}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* クリティカルパス上のタスクをGanttシート上でハイライト表示
|
||||
*
|
||||
* @param {Sheet} sheet - Ganttシート
|
||||
* @param {Object} projectData - プロジェクトデータ
|
||||
* @param {Object} criticalPathReport - calculateCriticalPath()の結果
|
||||
*/
|
||||
function highlightCriticalPath(sheet, projectData, criticalPathReport) {
|
||||
if (!criticalPathReport || criticalPathReport.skipped) {
|
||||
Logger.log('[INFO] Critical path analysis skipped or unavailable');
|
||||
return;
|
||||
}
|
||||
|
||||
if (criticalPathReport.error === 'circular_dependency') {
|
||||
Logger.log('[ERROR] Cannot highlight critical path due to circular dependency');
|
||||
return;
|
||||
}
|
||||
|
||||
const criticalTaskIds = new Set(criticalPathReport.criticalTasks);
|
||||
const nearCriticalTaskIds = new Set(criticalPathReport.nearCriticalTasks);
|
||||
|
||||
// タスクID → 行番号のマップを構築(4行目から開始)
|
||||
const taskMap = {};
|
||||
for (let i = 0; i < projectData.tasks.length; i++) {
|
||||
taskMap[projectData.tasks[i].task_id] = 4 + i;
|
||||
}
|
||||
|
||||
// クリティカルパスタスクを赤色ハイライト
|
||||
for (const taskId of criticalTaskIds) {
|
||||
if (!taskMap[taskId]) continue;
|
||||
|
||||
const rowNum = taskMap[taskId];
|
||||
const range = sheet.getRange(rowNum, 1, 1, 12); // A列~L列
|
||||
range.setBackground('#FFEBEE'); // Light red background
|
||||
|
||||
// タスク名セルに太字+赤色テキスト
|
||||
const taskNameCell = sheet.getRange(rowNum, 3); // C列(タスク名)
|
||||
taskNameCell.setFontWeight('bold');
|
||||
taskNameCell.setFontColor('#E53935'); // Red text
|
||||
|
||||
// ノートを追加
|
||||
const metrics = criticalPathReport.taskMetrics[taskId];
|
||||
const noteText = '🔴 クリティカルパス\n' +
|
||||
'スラック: 0日\n' +
|
||||
'最早開始: Day ' + metrics.es + '\n' +
|
||||
'最早終了: Day ' + metrics.ef;
|
||||
taskNameCell.setNote(noteText);
|
||||
}
|
||||
|
||||
// Near-criticalタスクをオレンジ色ハイライト
|
||||
for (const taskId of nearCriticalTaskIds) {
|
||||
if (!taskMap[taskId]) continue;
|
||||
|
||||
const rowNum = taskMap[taskId];
|
||||
const range = sheet.getRange(rowNum, 1, 1, 12);
|
||||
range.setBackground('#FFF3E0'); // Light orange background
|
||||
|
||||
const taskNameCell = sheet.getRange(rowNum, 3);
|
||||
taskNameCell.setFontWeight('bold');
|
||||
taskNameCell.setFontColor('#FF9800'); // Orange text
|
||||
|
||||
const metrics = criticalPathReport.taskMetrics[taskId];
|
||||
const noteText = '🟠 Near-Critical\n' +
|
||||
'スラック: ' + metrics.slack + '日\n' +
|
||||
'最早開始: Day ' + metrics.es + '\n' +
|
||||
'最遅開始: Day ' + metrics.ls;
|
||||
taskNameCell.setNote(noteText);
|
||||
}
|
||||
|
||||
Logger.log('[INFO] Highlighted ' + criticalTaskIds.size + ' critical and ' +
|
||||
nearCriticalTaskIds.size + ' near-critical tasks');
|
||||
}
|
||||
518
gas/README.md
Normal file
518
gas/README.md
Normal file
|
|
@ -0,0 +1,518 @@
|
|||
# Google Apps Script (GAS) 導入手順
|
||||
|
||||
このディレクトリには、スプレッドシートに自動的にGanttチャートを生成するGASコードが含まれています。
|
||||
|
||||
## 📋 前提条件
|
||||
|
||||
- Googleアカウント
|
||||
- スプレッドシート作成済み(ID: `1y7U-3hVfdubQPh-H39k-5bvuHF7cOkHkOuA121GtxCI`)
|
||||
- Discord Webhook URL設定済み
|
||||
|
||||
## 🚀 セットアップ手順
|
||||
|
||||
### 1. Google Apps Scriptプロジェクトを開く
|
||||
|
||||
1. スプレッドシートを開く: https://docs.google.com/spreadsheets/d/1y7U-3hVfdubQPh-H39k-5bvuHF7cOkHkOuA121GtxCI
|
||||
2. メニューから「拡張機能」→「Apps Script」をクリック
|
||||
3. 新しいタブでGASエディタが開きます
|
||||
|
||||
### 2. スクリプトファイルをアップロード
|
||||
|
||||
GASエディタで以下のファイルを作成します:
|
||||
|
||||
#### **Config.gs**
|
||||
1. 左側のファイル一覧で「+」ボタンをクリック → 「スクリプト」を選択
|
||||
2. ファイル名を `Config.gs` に変更
|
||||
3. `gas/Config.gs` の内容をコピーして貼り付け
|
||||
|
||||
#### **SheetManager.gs**
|
||||
1. 同様に新しいスクリプトを作成
|
||||
2. ファイル名を `SheetManager.gs` に変更
|
||||
3. `gas/SheetManager.gs` の内容をコピーして貼り付け
|
||||
|
||||
#### **GanttRenderer.gs**
|
||||
1. 同様に新しいスクリプトを作成
|
||||
2. ファイル名を `GanttRenderer.gs` に変更
|
||||
3. `gas/GanttRenderer.gs` の内容をコピーして貼り付け
|
||||
|
||||
#### **Code.gs**
|
||||
1. 既存の `Code.gs` ファイルの内容を削除
|
||||
2. `gas/Code.gs` の内容をコピーして貼り付け
|
||||
|
||||
### 3. 権限の承認
|
||||
|
||||
1. GASエディタ上部の「実行」ボタンの隣にあるプルダウンから `testGenerateGantt` 関数を選択
|
||||
2. 「実行」ボタンをクリック
|
||||
3. 初回実行時に権限の承認が必要です:
|
||||
- 「権限を確認」をクリック
|
||||
- Googleアカウントを選択
|
||||
- 「詳細」→「プロジェクト名(安全ではないページ)に移動」をクリック
|
||||
- 「許可」をクリック
|
||||
|
||||
## 🧪 テスト実行
|
||||
|
||||
### テストデータで動作確認
|
||||
|
||||
1. GASエディタで `testGenerateGantt` 関数が選択されていることを確認
|
||||
2. 「実行」ボタンをクリック
|
||||
3. 実行ログを確認:
|
||||
- GASエディタ下部の「実行ログ」をクリック
|
||||
- ✓マークが表示されれば成功
|
||||
|
||||
### 確認事項
|
||||
|
||||
実行後、スプレッドシートで以下を確認:
|
||||
|
||||
#### ✅ プロジェクト一覧シート
|
||||
- シート「プロジェクト一覧」が作成されている
|
||||
- プロジェクトID `0009` の情報が表示されている
|
||||
- 進捗率、総工数が自動計算されている
|
||||
|
||||
#### ✅ 全プロジェクトタスクシート
|
||||
- シート「全プロジェクトタスク」が作成されている
|
||||
- 36個のタスクが一覧表示されている
|
||||
|
||||
#### ✅ Ganttチャート
|
||||
- シート「Gantt_0009 - セミナー振り分けシステム」が作成されている
|
||||
- タスク情報が左側に表示されている
|
||||
- 右側に月/日のヘッダーが表示されている
|
||||
- タスクごとにカラフルなバーが表示されている
|
||||
- 担当者/優先度/タグ/工数の列がデフォルトで折りたたまれている
|
||||
|
||||
#### ✅ Discord通知
|
||||
- Discordチャンネルに成功通知が届いている
|
||||
- プロジェクト名、ID、タスク数、スプレッドシートURLが表示されている
|
||||
|
||||
## 📊 実際のデータで実行
|
||||
|
||||
### 方法1: JSONファイルをGoogle Driveにアップロード
|
||||
|
||||
1. プロジェクトのJSONファイル(例: `0009_セミナー振り分けシステム.json`)をGoogle Driveにアップロード
|
||||
2. ファイルを右クリック → 「リンクを取得」→ ファイルIDをコピー
|
||||
- URL例: `https://drive.google.com/file/d/{ファイルID}/view`
|
||||
3. GASエディタで `processJsonFile` 関数を編集:
|
||||
```javascript
|
||||
processJsonFile('ここにファイルIDを貼り付け');
|
||||
```
|
||||
4. `processJsonFile` を実行
|
||||
|
||||
### 方法2: JSONデータを直接Code.gsに貼り付け
|
||||
|
||||
1. JSONファイルの内容をコピー
|
||||
2. `Code.gs` の `testGenerateGantt` 関数内の `testData` 変数を更新:
|
||||
```javascript
|
||||
const testData = {
|
||||
// ここにJSONの内容を貼り付け
|
||||
};
|
||||
```
|
||||
3. `testGenerateGantt` を実行
|
||||
|
||||
## 🔄 自動化
|
||||
|
||||
### 期日切れタスクのDiscord通知
|
||||
|
||||
毎日自動的に期日切れタスクをDiscordに通知する機能を設定できます。
|
||||
|
||||
#### トリガー設定手順(朝7時と夜20時に通知)
|
||||
|
||||
1. GASエディタ左側の「トリガー」(時計アイコン)をクリック
|
||||
2. 「トリガーを追加」をクリック
|
||||
3. 以下を設定(朝7時の通知):
|
||||
- 実行する関数: `sendOverdueTasksNotification`
|
||||
- イベントのソース: 時間主導型
|
||||
- 時間ベースのトリガーのタイプ: 日付ベースのタイマー
|
||||
- 時刻: 午前7時~8時
|
||||
4. 「保存」をクリック
|
||||
5. もう一度「トリガーを追加」をクリックして夜20時の通知を設定:
|
||||
- 実行する関数: `sendOverdueTasksNotification`
|
||||
- イベントのソース: 時間主導型
|
||||
- 時間ベースのトリガーのタイプ: 日付ベースのタイマー
|
||||
- 時刻: 午後8時~9時
|
||||
6. 「保存」をクリック
|
||||
|
||||
#### 通知内容
|
||||
|
||||
- プロジェクトごとにグループ化された期日切れタスク一覧
|
||||
- タスク名、期日、遅延日数、進捗率、担当者
|
||||
- スプレッドシートへの直接リンク
|
||||
|
||||
### バーンダウンチャート自動記録
|
||||
|
||||
毎日自動的にバーンダウンデータを記録する機能を設定できます。
|
||||
|
||||
#### 🚀 簡単セットアップ(推奨)
|
||||
|
||||
GASエディタで以下の関数を1回実行するだけでOK:
|
||||
|
||||
1. 関数選択ドロップダウンから `setupDailyBurndownTrigger` を選択
|
||||
2. 「実行」ボタンをクリック
|
||||
3. 実行ログに「✅ トリガーを作成しました!」と表示されたら完了
|
||||
|
||||
これで毎日午前9時に自動記録が開始されます。
|
||||
|
||||
**停止したい場合**: `removeDailyBurndownTrigger` を実行
|
||||
|
||||
#### 手動セットアップ(従来の方法)
|
||||
|
||||
<details>
|
||||
<summary>クリックして展開</summary>
|
||||
|
||||
1. GASエディタ左側の「トリガー」(時計アイコン)をクリック
|
||||
2. 「トリガーを追加」をクリック
|
||||
3. 以下を設定:
|
||||
- 実行する関数: `recordDailyBurndownData`
|
||||
- イベントのソース: 時間主導型
|
||||
- 時間ベースのトリガーのタイプ: 日付ベースのタイマー
|
||||
- 時刻: 午前9時~10時
|
||||
4. 「保存」をクリック
|
||||
|
||||
</details>
|
||||
|
||||
#### 記録内容
|
||||
|
||||
- プロジェクトごとの予定進捗率と実績進捗率
|
||||
- 完了タスク数、残タスク数
|
||||
- ベロシティ(過去7日間の平均完了タスク数/日)
|
||||
- 完了予測日
|
||||
- データは BurndownData シートに記録されます
|
||||
- 90日より古いデータは自動削除されます
|
||||
|
||||
### 全シート自動同期
|
||||
|
||||
毎日自動的に全Ganttシートからデータを読み取り、プロジェクト一覧・全タスク・期日切れタスクを同期する機能を設定できます。
|
||||
|
||||
#### 🚀 簡単セットアップ(推奨)
|
||||
|
||||
GASエディタで以下の関数を1回実行するだけでOK:
|
||||
|
||||
1. 関数選択ドロップダウンから `setupSyncAllSheetsTrigger` を選択
|
||||
2. 「実行」ボタンをクリック
|
||||
3. 実行ログに「✅ トリガーを作成しました!」と表示されたら完了
|
||||
|
||||
これで毎日午前10時に自動同期が開始されます。
|
||||
|
||||
**停止したい場合**: `removeSyncAllSheetsTrigger` を実行
|
||||
|
||||
#### 同期内容
|
||||
|
||||
- 全Ganttシート(`XXXX_プロジェクト名`形式)からデータを読み取り
|
||||
- プロジェクト一覧シートを更新
|
||||
- 全プロジェクトタスクシートを更新
|
||||
- 期日切れタスク一覧シートを更新
|
||||
- Ganttシートのステータスから進捗率を自動更新
|
||||
|
||||
### Google Driveフォルダ監視(今後実装予定)
|
||||
|
||||
Google Driveのフォルダを監視して、新しいJSONファイルが追加されたら自動的にGanttチャートを生成する機能は今後実装予定です。
|
||||
|
||||
## 📝 カスタマイズ
|
||||
|
||||
### タグの色を変更
|
||||
|
||||
`Config.gs` の `TAG_COLORS` オブジェクトを編集:
|
||||
|
||||
```javascript
|
||||
TAG_COLORS: {
|
||||
'開発': '#4A90E2', // 青
|
||||
'デザイン': '#E24A90', // ピンク
|
||||
'テスト': '#90E24A', // 緑
|
||||
'調査': '#E2904A', // オレンジ
|
||||
'レビュー': '#904AE2', // 紫
|
||||
'デフォルト': '#999999' // グレー
|
||||
}
|
||||
```
|
||||
|
||||
### 優先度の色を変更
|
||||
|
||||
`Config.gs` の `PRIORITY_COLORS` オブジェクトを編集:
|
||||
|
||||
```javascript
|
||||
PRIORITY_COLORS: {
|
||||
'高': '#FF6B6B', // 赤
|
||||
'中': '#FFA726', // オレンジ
|
||||
'低': '#66BB6A' // 緑
|
||||
}
|
||||
```
|
||||
|
||||
### Ganttチャートの表示期間を変更
|
||||
|
||||
`Config.gs` の `GANTT.DAYS_TO_SHOW` を編集:
|
||||
|
||||
```javascript
|
||||
GANTT: {
|
||||
START_COLUMN: 10,
|
||||
HEADER_ROW: 1,
|
||||
DATA_START_ROW: 2,
|
||||
DAYS_TO_SHOW: 180, // この数値を変更(日数)
|
||||
CELL_WIDTH: 30
|
||||
}
|
||||
```
|
||||
|
||||
## 🐛 トラブルシューティング
|
||||
|
||||
### エラー: "ReferenceError: CONFIG is not defined"
|
||||
|
||||
- `Config.gs` ファイルが正しく作成されているか確認
|
||||
- ファイル名が正確に `Config.gs` になっているか確認
|
||||
- GASエディタで「保存」ボタンをクリック
|
||||
|
||||
### エラー: "Exception: スプレッドシートが見つかりません"
|
||||
|
||||
- `Config.gs` の `SPREADSHEET_ID` が正しいか確認
|
||||
- スプレッドシートへのアクセス権限があるか確認
|
||||
|
||||
### Ganttバーが表示されない
|
||||
|
||||
- タスクに `start_date` と `end_date` が設定されているか確認
|
||||
- 日付形式が `YYYY-MM-DD` になっているか確認
|
||||
|
||||
### Discord通知が届かない
|
||||
|
||||
- `Config.gs` の `DISCORD_WEBHOOK_URL` が正しいか確認
|
||||
- Webhook URLが有効か確認
|
||||
|
||||
## 📚 ファイル構成
|
||||
|
||||
```
|
||||
gas/
|
||||
├── Config.gs # 設定ファイル(スプレッドシートID、Discord、色設定など)
|
||||
├── SheetManager.gs # シート操作クラス(作成、更新、データ投入)
|
||||
├── GanttRenderer.gs # Ganttチャート描画クラス(バー、ヘッダー、スタイル)
|
||||
├── Code.gs # メインスクリプト(実行エントリーポイント)
|
||||
└── README.md # このファイル
|
||||
```
|
||||
|
||||
## 🎯 次のステップ
|
||||
|
||||
1. ✅ GASスクリプトのセットアップ完了
|
||||
2. ✅ テストデータでGanttチャート生成成功
|
||||
3. 🔲 実際のプロジェクトデータで動作確認
|
||||
4. 🔲 Google Drive監視機能の実装
|
||||
5. 🔲 エラーハンドリングの強化
|
||||
6. 🔲 パフォーマンス最適化
|
||||
|
||||
## 💡 ヒント
|
||||
|
||||
- **スプレッドシートURL**: https://docs.google.com/spreadsheets/d/1y7U-3hVfdubQPh-H39k-5bvuHF7cOkHkOuA121GtxCI
|
||||
- **実行ログの確認**: GASエディタ → 「実行ログ」をクリック
|
||||
- **スクリプトの保存**: `Ctrl+S` (Windows) / `Cmd+S` (Mac)
|
||||
- **関数の実行**: `Ctrl+R` (Windows) / `Cmd+R` (Mac)
|
||||
|
||||
---
|
||||
|
||||
## 🔧 clasp統合(開発者向け)
|
||||
|
||||
### 概要
|
||||
|
||||
claspを使用すると、ローカル環境からGoogle Apps Scriptコードを管理できます。Git経由でのバージョン管理や、エディタでの快適な開発が可能になります。
|
||||
|
||||
### 初回セットアップ
|
||||
|
||||
#### 1. clasp認証
|
||||
|
||||
```bash
|
||||
npm run gas:login
|
||||
```
|
||||
|
||||
- ブラウザが開き、Googleアカウントでの認証が求められます
|
||||
- Apps Script APIへのアクセス許可を承認します
|
||||
- 認証情報は `~/.clasprc.json` に保存されます
|
||||
|
||||
**注意**: 事前に Apps Script APIを有効化してください
|
||||
- URL: https://script.google.com/home/usersettings
|
||||
- "Google Apps Script API" をONにする
|
||||
|
||||
#### 2. GASプロジェクトから既存コードをpull
|
||||
|
||||
```bash
|
||||
npm run gas:pull
|
||||
```
|
||||
|
||||
- GASプロジェクトからローカルにコードをダウンロード
|
||||
- 既存のgas/ディレクトリのファイルと統合されます
|
||||
- `appsscript.json` も自動的にダウンロードされます
|
||||
|
||||
### 開発ワークフロー
|
||||
|
||||
#### 通常の開発手順
|
||||
|
||||
1. **ローカルでgas/内のファイルを編集**
|
||||
```bash
|
||||
vim gas/SheetManager.gs
|
||||
# またはVS Codeなどのエディタで編集
|
||||
```
|
||||
|
||||
2. **GASにpush**
|
||||
```bash
|
||||
npm run gas:push
|
||||
```
|
||||
- ローカルの変更がGASプロジェクトに反映されます
|
||||
- 依存関係順(Config → SheetManager → GanttRenderer → Code)でアップロードされます
|
||||
|
||||
3. **GASエディタで確認**
|
||||
```bash
|
||||
npm run gas:open
|
||||
```
|
||||
- ブラウザでGASエディタが開きます
|
||||
- アップロードされたコードを確認できます
|
||||
|
||||
4. **ログ確認**
|
||||
```bash
|
||||
npm run gas:logs
|
||||
```
|
||||
- GASの実行ログがターミナルに表示されます
|
||||
|
||||
#### GASエディタで編集した場合
|
||||
|
||||
GASエディタで直接編集した内容をローカルに反映する場合:
|
||||
|
||||
```bash
|
||||
# GASからローカルにpull
|
||||
npm run gas:pull
|
||||
|
||||
# 差分確認
|
||||
git diff gas/
|
||||
|
||||
# 問題なければコミット
|
||||
git add gas/
|
||||
git commit -m "chore: sync from GAS editor"
|
||||
```
|
||||
|
||||
### 利用可能なコマンド
|
||||
|
||||
#### 基本コマンド
|
||||
|
||||
- `npm run gas:push` - ローカル → GAS(コードをアップロード)
|
||||
- `npm run gas:pull` - GAS → ローカル(コードをダウンロード)
|
||||
- `npm run gas:open` - ブラウザでGASエディタを開く
|
||||
- `npm run gas:logs` - 実行ログ表示
|
||||
|
||||
#### 開発支援コマンド
|
||||
|
||||
- `npm run gas:push:watch` - ファイル変更を監視して自動push(開発時のみ)
|
||||
- `npm run gas:logs:watch` - ログをリアルタイム監視
|
||||
|
||||
#### 認証管理
|
||||
|
||||
- `npm run gas:login` - clasp認証
|
||||
- `npm run gas:logout` - clasp認証解除
|
||||
|
||||
#### バージョン管理
|
||||
|
||||
- `npm run gas:version "v1.1.0 - New feature"` - GASバージョン作成
|
||||
- `npm run gas:versions` - バージョン一覧表示
|
||||
|
||||
### 推奨ワークフロー
|
||||
|
||||
```bash
|
||||
# 1. ローカルでファイル編集
|
||||
vim gas/Code.gs
|
||||
|
||||
# 2. GASにpush
|
||||
npm run gas:push
|
||||
|
||||
# 3. GASエディタで動作確認
|
||||
npm run gas:open
|
||||
|
||||
# 4. ログ確認(関数実行後)
|
||||
npm run gas:logs
|
||||
|
||||
# 5. Gitにコミット
|
||||
git add gas/Code.gs
|
||||
git commit -m "feat: add new feature"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### 自動同期モード(開発時)
|
||||
|
||||
開発中にファイル変更を自動的にGASに反映する場合:
|
||||
|
||||
```bash
|
||||
# ターミナル1: ファイル監視 + 自動push
|
||||
npm run gas:push:watch
|
||||
|
||||
# ターミナル2: ログ監視
|
||||
npm run gas:logs:watch
|
||||
|
||||
# Ctrl+C で監視を停止
|
||||
```
|
||||
|
||||
### トラブルシューティング
|
||||
|
||||
#### エラー: `User has not enabled the Apps Script API`
|
||||
|
||||
**原因**: Apps Script APIが有効化されていない
|
||||
|
||||
**解決方法**:
|
||||
1. https://script.google.com/home/usersettings を開く
|
||||
2. "Google Apps Script API" をONにする
|
||||
3. `npm run gas:login` を再実行
|
||||
|
||||
#### エラー: `Could not find script`
|
||||
|
||||
**原因**: `.clasp.json` のスクリプトIDが間違っている
|
||||
|
||||
**解決方法**:
|
||||
1. Apps Scriptエディタ → プロジェクト設定 → スクリプトIDを確認
|
||||
2. `.clasp.json` のスクリプトIDを修正
|
||||
3. `npm run gas:pull` を再実行
|
||||
|
||||
#### 認証リセット
|
||||
|
||||
認証に問題がある場合:
|
||||
|
||||
```bash
|
||||
# logout
|
||||
npm run gas:logout
|
||||
|
||||
# 認証ファイル削除(念のため)
|
||||
rm ~/.clasprc.json
|
||||
|
||||
# 再login
|
||||
npm run gas:login
|
||||
```
|
||||
|
||||
#### pushが失敗する
|
||||
|
||||
**原因**: GASファイルに構文エラーがある
|
||||
|
||||
**解決方法**:
|
||||
1. エラーメッセージでファイル名と行番号を確認
|
||||
2. 該当ファイルを修正
|
||||
3. 再度push
|
||||
|
||||
```bash
|
||||
# 修正後
|
||||
npm run gas:push
|
||||
```
|
||||
|
||||
### プロジェクト構成
|
||||
|
||||
```
|
||||
185-automatic-gantt-chart-creation/
|
||||
├── .clasp.json # clasp設定(スクリプトID、rootDir)
|
||||
├── package.json # npm scripts(gas:*コマンド)
|
||||
├── gas/ # GASファイル
|
||||
│ ├── appsscript.json # GASプロジェクトメタデータ
|
||||
│ ├── .claspignore # clasp除外ファイル設定
|
||||
│ ├── Config.gs
|
||||
│ ├── SheetManager.gs
|
||||
│ ├── GanttRenderer.gs
|
||||
│ ├── Code.gs
|
||||
│ └── README.md # このファイル
|
||||
└── src/ # TypeScriptファイル(ローカル実行用)
|
||||
```
|
||||
|
||||
### セキュリティ考慮事項
|
||||
|
||||
- `.clasp.json` はGitにコミット済み(スクリプトIDは秘匿情報ではない)
|
||||
- `~/.clasprc.json` はホームディレクトリに保存(プロジェクト外)
|
||||
- `Config.gs` に DISCORD_WEBHOOK_URL と TODOIST_API_KEY がハードコードされています
|
||||
- 今後の改善: PropertiesService で環境変数化を検討
|
||||
|
||||
### Tips
|
||||
|
||||
- **ファイルのpush順序**: Config → SheetManager → GanttRenderer → Code(依存関係順)
|
||||
- **初回pullは慎重に**: 既存のgas/ディレクトリを事前にバックアップ推奨
|
||||
- **GASバージョン**: リリース時に `npm run gas:version` でバージョン作成を推奨
|
||||
- **ログ確認**: `npm run gas:logs` で直近15件のログを確認可能
|
||||
1144
gas/SheetManager.gs
Normal file
1144
gas/SheetManager.gs
Normal file
File diff suppressed because it is too large
Load diff
7
gas/appsscript.json
Normal file
7
gas/appsscript.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"timeZone": "Asia/Tokyo",
|
||||
"dependencies": {
|
||||
},
|
||||
"exceptionLogging": "STACKDRIVER",
|
||||
"runtimeVersion": "V8"
|
||||
}
|
||||
43
package.json
Normal file
43
package.json
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "automatic-gantt-chart-creation",
|
||||
"version": "1.0.0",
|
||||
"description": "Claude Code対話型Ganttチャート自動生成システム",
|
||||
"main": "src/gantt-helper.ts",
|
||||
"scripts": {
|
||||
"gantt:init": "ts-node src/gantt-helper.ts init",
|
||||
"gantt:save": "ts-node src/gantt-helper.ts save",
|
||||
"gantt:auth": "ts-node src/gantt-helper.ts auth",
|
||||
"gantt:status": "ts-node src/gantt-helper.ts status",
|
||||
"gantt:upload": "ts-node src/upload-to-drive.ts",
|
||||
"build": "tsc",
|
||||
"dev": "ts-node src/gantt-helper.ts",
|
||||
"gas:login": "npx clasp login",
|
||||
"gas:logout": "npx clasp logout",
|
||||
"gas:pull": "npx clasp pull",
|
||||
"gas:push": "npx clasp push",
|
||||
"gas:push:watch": "npx clasp push --watch",
|
||||
"gas:open": "npx clasp open",
|
||||
"gas:logs": "npx clasp logs",
|
||||
"gas:logs:watch": "npx clasp logs --watch",
|
||||
"gas:version": "npx clasp version",
|
||||
"gas:versions": "npx clasp versions"
|
||||
},
|
||||
"keywords": [
|
||||
"gantt",
|
||||
"project-management",
|
||||
"google-drive",
|
||||
"automation"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.5",
|
||||
"googleapis": "^128.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@google/clasp": "^3.1.3",
|
||||
"@types/node": "^20.11.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
582
src/gantt-helper.ts
Normal file
582
src/gantt-helper.ts
Normal file
|
|
@ -0,0 +1,582 @@
|
|||
#!/usr/bin/env ts-node
|
||||
|
||||
/**
|
||||
* Ganttチャート自動生成ヘルパースクリプト
|
||||
*
|
||||
* 対話の状態管理、JSON生成、Google Driveアップロード、対話履歴保存を担当
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { google } from 'googleapis';
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import * as url from 'url';
|
||||
|
||||
// 環境変数読み込み
|
||||
dotenv.config();
|
||||
|
||||
// 型定義
|
||||
interface Task {
|
||||
task_id: string;
|
||||
task_name: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
assignee: string;
|
||||
dependencies: string[];
|
||||
progress: number;
|
||||
priority: string;
|
||||
parent_task_id: string | null;
|
||||
tags: string[];
|
||||
estimated_hours: number;
|
||||
is_milestone: boolean;
|
||||
}
|
||||
|
||||
interface ProjectData {
|
||||
project_id: string;
|
||||
project_name: string;
|
||||
project_purpose: string;
|
||||
project_type: string;
|
||||
project_deadline: string;
|
||||
github_url?: string;
|
||||
tasks: Task[];
|
||||
}
|
||||
|
||||
interface DialogueEntry {
|
||||
timestamp: string;
|
||||
speaker: 'user' | 'assistant';
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 対話状態管理クラス
|
||||
*/
|
||||
class GanttDialogueManager {
|
||||
private projectData: Partial<ProjectData> = {};
|
||||
private dialogueHistory: DialogueEntry[] = [];
|
||||
private currentPhase: number = 1;
|
||||
private tasks: Task[] = [];
|
||||
private taskCounter: number = 1;
|
||||
|
||||
/**
|
||||
* 対話履歴を追加
|
||||
*/
|
||||
addDialogue(speaker: 'user' | 'assistant', message: string): void {
|
||||
this.dialogueHistory.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
speaker,
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* プロジェクト基本情報を設定
|
||||
*/
|
||||
setProjectInfo(key: keyof ProjectData, value: any): void {
|
||||
(this.projectData as any)[key] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* タスクを追加
|
||||
*/
|
||||
addTask(task: Omit<Task, 'task_id'>): string {
|
||||
const taskId = `T${this.taskCounter.toString().padStart(3, '0')}`;
|
||||
this.tasks.push({
|
||||
task_id: taskId,
|
||||
...task
|
||||
});
|
||||
this.taskCounter++;
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* タスクを更新
|
||||
*/
|
||||
updateTask(taskId: string, updates: Partial<Task>): void {
|
||||
const taskIndex = this.tasks.findIndex(t => t.task_id === taskId);
|
||||
if (taskIndex !== -1) {
|
||||
this.tasks[taskIndex] = { ...this.tasks[taskIndex], ...updates };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 現在のフェーズを取得
|
||||
*/
|
||||
getCurrentPhase(): number {
|
||||
return this.currentPhase;
|
||||
}
|
||||
|
||||
/**
|
||||
* 次のフェーズに進む
|
||||
*/
|
||||
nextPhase(): void {
|
||||
this.currentPhase++;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSONファイルを生成
|
||||
*/
|
||||
generateJSON(): ProjectData {
|
||||
return {
|
||||
project_id: this.projectData.project_id || '',
|
||||
project_name: this.projectData.project_name || '',
|
||||
project_purpose: this.projectData.project_purpose || '',
|
||||
project_type: this.projectData.project_type || '',
|
||||
project_deadline: this.projectData.project_deadline || '',
|
||||
github_url: this.projectData.github_url || '',
|
||||
tasks: this.tasks
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 対話履歴をMarkdown形式で生成
|
||||
*/
|
||||
generateDialogueMarkdown(): string {
|
||||
let markdown = `# プロジェクト作成対話履歴\n\n`;
|
||||
markdown += `**プロジェクト名**: ${this.projectData.project_name || '未設定'}\n`;
|
||||
markdown += `**プロジェクトID**: ${this.projectData.project_id || '未設定'}\n`;
|
||||
markdown += `**作成日時**: ${new Date().toISOString()}\n\n`;
|
||||
markdown += `---\n\n`;
|
||||
|
||||
for (const entry of this.dialogueHistory) {
|
||||
const speaker = entry.speaker === 'user' ? 'ユーザー' : 'アシスタント';
|
||||
const time = new Date(entry.timestamp).toLocaleString('ja-JP');
|
||||
markdown += `## ${speaker} (${time})\n\n`;
|
||||
markdown += `${entry.message}\n\n`;
|
||||
}
|
||||
|
||||
return markdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* ファイルを保存
|
||||
* @param isUpdate - 既存プロジェクトの更新かどうか(デフォルト: false = 新規作成)
|
||||
*/
|
||||
async saveFiles(isUpdate: boolean = false): Promise<void> {
|
||||
try {
|
||||
const projectId = this.projectData.project_id || 'unknown';
|
||||
const projectName = this.projectData.project_name || 'unknown';
|
||||
const prefix = isUpdate ? 'update_' : 'new_';
|
||||
const fileName = `${prefix}${projectId}_${projectName}`;
|
||||
|
||||
// JSONファイル保存(outputs/ディレクトリに保存)
|
||||
const outputsDir = path.join(process.cwd(), 'outputs');
|
||||
if (!fs.existsSync(outputsDir)) {
|
||||
fs.mkdirSync(outputsDir, { recursive: true });
|
||||
}
|
||||
const jsonData = this.generateJSON();
|
||||
const jsonPath = path.join(outputsDir, `${fileName}.json`);
|
||||
fs.writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2), 'utf-8');
|
||||
console.log(`✓ JSONファイル保存: ${jsonPath}`);
|
||||
|
||||
// 対話履歴保存
|
||||
const markdown = this.generateDialogueMarkdown();
|
||||
const docsDir = path.join(process.cwd(), 'docs');
|
||||
if (!fs.existsSync(docsDir)) {
|
||||
fs.mkdirSync(docsDir, { recursive: true });
|
||||
}
|
||||
const mdPath = path.join(docsDir, `${fileName}.md`);
|
||||
fs.writeFileSync(mdPath, markdown, 'utf-8');
|
||||
console.log(`✓ 対話履歴保存: ${mdPath}`);
|
||||
|
||||
// Google Driveアップロード
|
||||
await this.uploadToGoogleDrive(jsonPath);
|
||||
|
||||
// スプレッドシート情報の表示
|
||||
const spreadsheetId = process.env.GOOGLE_SPREADSHEET_ID;
|
||||
if (spreadsheetId) {
|
||||
const spreadsheetUrl = `https://docs.google.com/spreadsheets/d/${spreadsheetId}`;
|
||||
console.log(`\n📊 スプレッドシートURL: ${spreadsheetUrl}`);
|
||||
console.log(` ※ GASトリガーでGanttチャートが自動生成されます`);
|
||||
}
|
||||
|
||||
// Discord通知(成功)
|
||||
await this.sendDiscordNotification(
|
||||
`プロジェクト「${projectName}」のJSONファイルが正常に生成されました。\n` +
|
||||
`JSONファイル: \`outputs/${fileName}.json\`\n` +
|
||||
`対話履歴: \`docs/${fileName}.md\`\n` +
|
||||
(spreadsheetId ? `\nスプレッドシート: https://docs.google.com/spreadsheets/d/${spreadsheetId}` : ''),
|
||||
false
|
||||
);
|
||||
} catch (error) {
|
||||
// Discord通知(エラー)
|
||||
await this.sendDiscordNotification(
|
||||
`プロジェクトファイルの保存中にエラーが発生しました。\n\n` +
|
||||
`エラー内容: ${error instanceof Error ? error.message : String(error)}`,
|
||||
true
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Driveにアップロード(重複チェック機能付き)
|
||||
*/
|
||||
private async uploadToGoogleDrive(filePath: string): Promise<void> {
|
||||
try {
|
||||
const folderId = process.env.GOOGLE_DRIVE_FOLDER_ID;
|
||||
if (!folderId) {
|
||||
console.warn('⚠ GOOGLE_DRIVE_FOLDER_ID が設定されていません。アップロードをスキップします。');
|
||||
return;
|
||||
}
|
||||
|
||||
// アップロード履歴チェック
|
||||
const fileName = path.basename(filePath);
|
||||
const historyPath = path.join(process.cwd(), '.upload-history.json');
|
||||
let uploadHistory: Record<string, string> = {};
|
||||
|
||||
if (fs.existsSync(historyPath)) {
|
||||
uploadHistory = JSON.parse(fs.readFileSync(historyPath, 'utf-8'));
|
||||
}
|
||||
|
||||
if (uploadHistory[fileName]) {
|
||||
console.log(`⏭ スキップ: ${fileName} は既にアップロード済みです`);
|
||||
console.log(` ファイルID: ${uploadHistory[fileName]}`);
|
||||
console.log(`\n⏰ GAS側で1分以内に自動的にGanttチャートが生成されます。`);
|
||||
|
||||
const spreadsheetId = process.env.GOOGLE_SPREADSHEET_ID;
|
||||
if (spreadsheetId) {
|
||||
const spreadsheetUrl = `https://docs.google.com/spreadsheets/d/${spreadsheetId}`;
|
||||
console.log(`📊 スプレッドシート: ${spreadsheetUrl}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// OAuth2認証設定(環境変数から読み込み想定)
|
||||
const auth = new google.auth.OAuth2(
|
||||
process.env.GOOGLE_CLIENT_ID,
|
||||
process.env.GOOGLE_CLIENT_SECRET,
|
||||
process.env.GOOGLE_REDIRECT_URI
|
||||
);
|
||||
|
||||
// トークン設定(事前に取得したトークンを使用)
|
||||
const tokenPath = path.join(process.cwd(), '.google-token.json');
|
||||
if (fs.existsSync(tokenPath)) {
|
||||
const token = JSON.parse(fs.readFileSync(tokenPath, 'utf-8'));
|
||||
auth.setCredentials(token);
|
||||
} else {
|
||||
console.warn('⚠ Google認証トークンが見つかりません。初回認証が必要です。');
|
||||
// TODO: 初回認証フローの実装
|
||||
return;
|
||||
}
|
||||
|
||||
const drive = google.drive({ version: 'v3', auth });
|
||||
|
||||
const fileMetadata = {
|
||||
name: fileName,
|
||||
parents: [folderId]
|
||||
};
|
||||
|
||||
const media = {
|
||||
mimeType: 'application/json',
|
||||
body: fs.createReadStream(filePath)
|
||||
};
|
||||
|
||||
const response = await drive.files.create({
|
||||
requestBody: fileMetadata,
|
||||
media: media,
|
||||
fields: 'id, name, webViewLink'
|
||||
});
|
||||
|
||||
const fileId = response.data.id!;
|
||||
console.log(`✓ Google Driveにアップロード完了`);
|
||||
console.log(` ファイルID: ${fileId}`);
|
||||
console.log(` URL: ${response.data.webViewLink}`);
|
||||
console.log(`\n⏰ GAS側で1分以内に自動的にGanttチャートが生成されます。`);
|
||||
|
||||
const spreadsheetId = process.env.GOOGLE_SPREADSHEET_ID;
|
||||
if (spreadsheetId) {
|
||||
const spreadsheetUrl = `https://docs.google.com/spreadsheets/d/${spreadsheetId}`;
|
||||
console.log(`📊 スプレッドシート: ${spreadsheetUrl}`);
|
||||
}
|
||||
|
||||
// アップロード履歴を保存
|
||||
uploadHistory[fileName] = fileId;
|
||||
fs.writeFileSync(historyPath, JSON.stringify(uploadHistory, null, 2), 'utf-8');
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ Google Driveアップロードエラー:', error);
|
||||
|
||||
// Discord通知(Google Driveエラー)
|
||||
await this.sendDiscordNotification(
|
||||
`Google Driveへのアップロード中にエラーが発生しました。\n\n` +
|
||||
`エラー内容: ${error instanceof Error ? error.message : String(error)}\n\n` +
|
||||
`ファイル: \`${path.basename(filePath)}\``,
|
||||
true
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 状態をJSON形式で取得(デバッグ用)
|
||||
*/
|
||||
getState(): object {
|
||||
return {
|
||||
currentPhase: this.currentPhase,
|
||||
projectData: this.projectData,
|
||||
tasksCount: this.tasks.length,
|
||||
dialogueHistoryCount: this.dialogueHistory.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Discord Webhookに通知を送信
|
||||
*/
|
||||
private async sendDiscordNotification(message: string, isError: boolean = false): Promise<void> {
|
||||
const webhookUrl = process.env.DISCORD_WEBHOOK_URL;
|
||||
if (!webhookUrl) {
|
||||
console.warn('⚠ DISCORD_WEBHOOK_URL が設定されていません。通知をスキップします。');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(webhookUrl);
|
||||
const payload = JSON.stringify({
|
||||
embeds: [{
|
||||
title: isError ? '❌ Ganttチャート作成エラー' : '✅ Ganttチャート作成完了',
|
||||
description: message,
|
||||
color: isError ? 0xff0000 : 0x00ff00,
|
||||
timestamp: new Date().toISOString(),
|
||||
footer: {
|
||||
text: 'Gantt Chart Generator'
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'プロジェクトID',
|
||||
value: this.projectData.project_id || '未設定',
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'プロジェクト名',
|
||||
value: this.projectData.project_name || '未設定',
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'タスク数',
|
||||
value: this.tasks.length.toString(),
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
path: url.pathname + url.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload)
|
||||
}
|
||||
};
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const req = https.request(options, (res) => {
|
||||
if (res.statusCode === 204) {
|
||||
console.log('✓ Discord通知送信完了');
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Discord通知失敗: ${res.statusCode}`));
|
||||
}
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('✗ Discord通知送信エラー:', error);
|
||||
// Discord通知の失敗は致命的エラーではないため、処理を続行
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Drive OAuth2 初回認証
|
||||
*/
|
||||
async function authenticateGoogleDrive(): Promise<void> {
|
||||
const clientId = process.env.GOOGLE_CLIENT_ID;
|
||||
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
|
||||
const redirectUri = process.env.GOOGLE_REDIRECT_URI || 'http://localhost:19204/oauth2callback';
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
throw new Error('GOOGLE_CLIENT_ID と GOOGLE_CLIENT_SECRET を .env ファイルに設定してください。');
|
||||
}
|
||||
|
||||
const oauth2Client = new google.auth.OAuth2(clientId, clientSecret, redirectUri);
|
||||
|
||||
// 認証URLを生成
|
||||
const authUrl = oauth2Client.generateAuthUrl({
|
||||
access_type: 'offline',
|
||||
prompt: 'consent',
|
||||
scope: [
|
||||
'https://www.googleapis.com/auth/drive.file'
|
||||
]
|
||||
});
|
||||
|
||||
console.log('\n🔐 Google Drive OAuth2 認証を開始します\n');
|
||||
console.log('以下のURLをブラウザで開いてください:');
|
||||
console.log(`\n${authUrl}\n`);
|
||||
|
||||
// ローカルサーバーを起動してコールバックを受け取る
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const reqUrl = url.parse(req.url || '', true);
|
||||
|
||||
if (reqUrl.pathname === '/oauth2callback') {
|
||||
const code = reqUrl.query.code as string;
|
||||
|
||||
if (!code) {
|
||||
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end('<h1>認証失敗</h1><p>認証コードが取得できませんでした。</p>');
|
||||
reject(new Error('認証コードが取得できませんでした'));
|
||||
server.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// トークン取得
|
||||
const { tokens } = await oauth2Client.getToken(code);
|
||||
oauth2Client.setCredentials(tokens);
|
||||
|
||||
// トークンをファイルに保存
|
||||
const tokenPath = path.join(process.cwd(), '.google-token.json');
|
||||
fs.writeFileSync(tokenPath, JSON.stringify(tokens, null, 2), 'utf-8');
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(`
|
||||
<html>
|
||||
<head><title>認証成功</title></head>
|
||||
<body style="font-family: sans-serif; text-align: center; padding: 50px;">
|
||||
<h1 style="color: #4CAF50;">✅ 認証成功!</h1>
|
||||
<p>Google Drive との連携が完了しました。</p>
|
||||
<p>このウィンドウを閉じてターミナルに戻ってください。</p>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
console.log('\n✓ Google Drive 認証完了');
|
||||
console.log(`✓ トークン保存: ${tokenPath}`);
|
||||
console.log('\nこれで Google Drive へのアップロードが可能になりました。');
|
||||
|
||||
server.close();
|
||||
resolve();
|
||||
}
|
||||
} catch (error) {
|
||||
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end('<h1>エラー</h1><p>認証処理中にエラーが発生しました。</p>');
|
||||
reject(error);
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(19204, () => {
|
||||
console.log('ローカル認証サーバーを起動しました (http://localhost:19204)');
|
||||
console.log('ブラウザでログイン後、自動的にトークンが保存されます...\n');
|
||||
});
|
||||
|
||||
server.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* コマンドライン引数パーサー
|
||||
*/
|
||||
function parseArgs(): { command: string; isUpdate: boolean } {
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0] || 'help';
|
||||
const isUpdate = args.includes('--update');
|
||||
|
||||
return { command, isUpdate };
|
||||
}
|
||||
|
||||
/**
|
||||
* メイン処理
|
||||
*/
|
||||
async function main() {
|
||||
const { command, isUpdate } = parseArgs();
|
||||
|
||||
const manager = new GanttDialogueManager();
|
||||
|
||||
switch (command) {
|
||||
case 'init':
|
||||
console.log('Ganttチャート作成を開始します...');
|
||||
console.log('対話を進めてください。完了後に `save` コマンドを実行してください。');
|
||||
break;
|
||||
|
||||
case 'save':
|
||||
if (isUpdate) {
|
||||
console.log('既存プロジェクトを更新しています...');
|
||||
} else {
|
||||
console.log('新規プロジェクトを作成しています...');
|
||||
}
|
||||
await manager.saveFiles(isUpdate);
|
||||
console.log('✓ すべてのファイルが保存されました');
|
||||
break;
|
||||
|
||||
case 'auth':
|
||||
console.log('Google Drive OAuth2 認証を開始します...');
|
||||
await authenticateGoogleDrive();
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
console.log('現在の状態:');
|
||||
console.log(JSON.stringify(manager.getState(), null, 2));
|
||||
break;
|
||||
|
||||
case 'help':
|
||||
default:
|
||||
console.log(`
|
||||
Ganttチャート自動生成ヘルパー
|
||||
|
||||
使い方:
|
||||
npm run gantt:<command> [options]
|
||||
|
||||
コマンド:
|
||||
init - 新規プロジェクト作成開始
|
||||
save - JSONと対話履歴を保存(自動的にGoogle Driveにアップロード&Ganttチャート生成)
|
||||
オプション:
|
||||
--update 既存プロジェクトの更新として保存(update_ プレフィックス付き)
|
||||
省略時 新規プロジェクトとして保存(new_ プレフィックス付き)
|
||||
auth - Google Drive OAuth2 初回認証
|
||||
status - 現在の状態を表示
|
||||
help - このヘルプを表示
|
||||
|
||||
使用例:
|
||||
npm run gantt:save # 新規プロジェクト作成(new_192_プロジェクト.json)
|
||||
npm run gantt:save -- --update # 既存プロジェクト更新(update_192_プロジェクト.json)
|
||||
|
||||
初回セットアップ手順:
|
||||
1. .env ファイルに以下を設定:
|
||||
GOOGLE_CLIENT_ID=your_client_id
|
||||
GOOGLE_CLIENT_SECRET=your_client_secret
|
||||
GOOGLE_REDIRECT_URI=http://localhost:19204/oauth2callback
|
||||
GOOGLE_DRIVE_FOLDER_ID=your_folder_id
|
||||
GOOGLE_SCRIPT_ID=your_script_id (GASのスクリプトID)
|
||||
GOOGLE_SPREADSHEET_ID=your_spreadsheet_id
|
||||
|
||||
2. npm run gantt:auth を実行してGoogle認証
|
||||
|
||||
3. /gantt コマンドでプロジェクト作成
|
||||
|
||||
4. npm run gantt:save で保存&アップロード
|
||||
→ 自動的にGanttチャートが生成されます
|
||||
`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// スクリプト実行
|
||||
if (require.main === module) {
|
||||
main().catch(console.error);
|
||||
}
|
||||
|
||||
export { GanttDialogueManager, ProjectData, Task, DialogueEntry };
|
||||
93
src/update-tags.ts
Normal file
93
src/update-tags.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
#!/usr/bin/env ts-node
|
||||
|
||||
/**
|
||||
* JSONファイルのタグを最新の定義に更新するスクリプト
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
// タグのマッピング定義
|
||||
const TAG_MAPPING: Record<string, string> = {
|
||||
'企画': '企画・計画',
|
||||
'設計': '設計',
|
||||
'開発': '開発・実装',
|
||||
'デザイン': '設計', // UIデザインも設計カテゴリに含める
|
||||
'テスト': 'テスト・検証',
|
||||
'レビュー': 'テスト・検証', // レビューもテスト・検証に含める
|
||||
'資料作成': '事務・管理',
|
||||
'調整': '事務・管理',
|
||||
'運用': '運用・保守',
|
||||
'その他': 'その他'
|
||||
};
|
||||
|
||||
/**
|
||||
* タグを更新する
|
||||
*/
|
||||
function updateTags(filePath: string): void {
|
||||
try {
|
||||
// JSONファイルを読み込み
|
||||
const jsonData = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||
|
||||
let updateCount = 0;
|
||||
|
||||
// 各タスクのタグを更新
|
||||
for (const task of jsonData.tasks) {
|
||||
if (task.tags && Array.isArray(task.tags)) {
|
||||
const updatedTags = task.tags.map((tag: string) => {
|
||||
const newTag = TAG_MAPPING[tag] || tag;
|
||||
if (newTag !== tag) {
|
||||
updateCount++;
|
||||
}
|
||||
return newTag;
|
||||
});
|
||||
|
||||
// 重複を削除
|
||||
task.tags = [...new Set(updatedTags)];
|
||||
}
|
||||
}
|
||||
|
||||
// 更新したJSONを保存
|
||||
fs.writeFileSync(filePath, JSON.stringify(jsonData, null, 2), 'utf-8');
|
||||
|
||||
console.log(`✓ タグ更新完了: ${filePath}`);
|
||||
console.log(` 更新されたタグ数: ${updateCount}`);
|
||||
} catch (error) {
|
||||
console.error('✗ エラー:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* メイン処理
|
||||
*/
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error('使い方: ts-node src/update-tags.ts <ファイルパス>');
|
||||
console.error('例: ts-node src/update-tags.ts 0027_お助けマンサービスHPの開発.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const filePath = args[0];
|
||||
|
||||
// 絶対パスでない場合、カレントディレクトリからの相対パスとして扱う
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(process.cwd(), filePath);
|
||||
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
console.error(`✗ ファイルが見つかりません: ${absolutePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
updateTags(absolutePath);
|
||||
}
|
||||
|
||||
// スクリプト実行
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { updateTags, TAG_MAPPING };
|
||||
143
src/upload-to-drive.ts
Normal file
143
src/upload-to-drive.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
#!/usr/bin/env ts-node
|
||||
|
||||
/**
|
||||
* 既存のJSONファイルをGoogle Driveにアップロードするスクリプト
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { google } from 'googleapis';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
// 環境変数読み込み
|
||||
dotenv.config();
|
||||
|
||||
/**
|
||||
* Google Driveにアップロード
|
||||
*/
|
||||
async function uploadToGoogleDrive(filePath: string): Promise<void> {
|
||||
try {
|
||||
const folderId = process.env.GOOGLE_DRIVE_FOLDER_ID;
|
||||
if (!folderId) {
|
||||
throw new Error('GOOGLE_DRIVE_FOLDER_ID が設定されていません。');
|
||||
}
|
||||
|
||||
// OAuth2認証設定
|
||||
const auth = new google.auth.OAuth2(
|
||||
process.env.GOOGLE_CLIENT_ID,
|
||||
process.env.GOOGLE_CLIENT_SECRET,
|
||||
process.env.GOOGLE_REDIRECT_URI
|
||||
);
|
||||
|
||||
// トークン設定
|
||||
const tokenPath = path.join(process.cwd(), '.google-token.json');
|
||||
if (!fs.existsSync(tokenPath)) {
|
||||
throw new Error('Google認証トークンが見つかりません。先に npm run gantt:auth を実行してください。');
|
||||
}
|
||||
|
||||
const token = JSON.parse(fs.readFileSync(tokenPath, 'utf-8'));
|
||||
auth.setCredentials(token);
|
||||
|
||||
const drive = google.drive({ version: 'v3', auth });
|
||||
|
||||
// ファイル名取得
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
// アップロード履歴チェック
|
||||
const historyPath = path.join(process.cwd(), '.upload-history.json');
|
||||
let uploadHistory: Record<string, string> = {};
|
||||
|
||||
if (fs.existsSync(historyPath)) {
|
||||
uploadHistory = JSON.parse(fs.readFileSync(historyPath, 'utf-8'));
|
||||
}
|
||||
|
||||
if (uploadHistory[fileName]) {
|
||||
console.log(`⏭ スキップ: ${fileName} は既にアップロード済みです`);
|
||||
console.log(` ファイルID: ${uploadHistory[fileName]}`);
|
||||
|
||||
const spreadsheetId = process.env.GOOGLE_SPREADSHEET_ID;
|
||||
if (spreadsheetId) {
|
||||
const spreadsheetUrl = `https://docs.google.com/spreadsheets/d/${spreadsheetId}`;
|
||||
console.log(`\n📊 スプレッドシート: ${spreadsheetUrl}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ファイルメタデータ
|
||||
const fileMetadata = {
|
||||
name: fileName,
|
||||
parents: [folderId]
|
||||
};
|
||||
|
||||
const media = {
|
||||
mimeType: 'application/json',
|
||||
body: fs.createReadStream(filePath)
|
||||
};
|
||||
|
||||
console.log(`📤 アップロード中: ${fileName}`);
|
||||
|
||||
// アップロード実行
|
||||
const response = await drive.files.create({
|
||||
requestBody: fileMetadata,
|
||||
media: media,
|
||||
fields: 'id, name, webViewLink'
|
||||
});
|
||||
|
||||
const fileId = response.data.id!;
|
||||
console.log(`✓ Google Driveにアップロード完了`);
|
||||
console.log(` ファイルID: ${fileId}`);
|
||||
console.log(` URL: ${response.data.webViewLink}`);
|
||||
console.log(`\n⏰ GAS側で1分以内に自動的にGanttチャートが生成されます。`);
|
||||
|
||||
const spreadsheetId = process.env.GOOGLE_SPREADSHEET_ID;
|
||||
if (spreadsheetId) {
|
||||
const spreadsheetUrl = `https://docs.google.com/spreadsheets/d/${spreadsheetId}`;
|
||||
console.log(`📊 スプレッドシート: ${spreadsheetUrl}`);
|
||||
}
|
||||
|
||||
// アップロード履歴を保存
|
||||
uploadHistory[fileName] = fileId;
|
||||
fs.writeFileSync(historyPath, JSON.stringify(uploadHistory, null, 2), 'utf-8');
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ Google Driveアップロードエラー:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* メイン処理
|
||||
*/
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error('使い方: ts-node src/upload-to-drive.ts <ファイルパス>');
|
||||
console.error('例: ts-node src/upload-to-drive.ts 0027_お助けマンサービスHPの開発.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const filePath = args[0];
|
||||
|
||||
// 絶対パスでない場合、カレントディレクトリからの相対パスとして扱う
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(process.cwd(), filePath);
|
||||
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
console.error(`✗ ファイルが見つかりません: ${absolutePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await uploadToGoogleDrive(absolutePath);
|
||||
}
|
||||
|
||||
// スクリプト実行
|
||||
if (require.main === module) {
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export { uploadToGoogleDrive };
|
||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue