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:
柴田貴司 2026-01-01 17:23:40 +09:00
commit a892a3c87c
21 changed files with 9496 additions and 0 deletions

View 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
View 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": "タスクIDT001から自動連番",
"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
View 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
View 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
View 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
View 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 APIOAuth認証
- 自動アップロード機能
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": "タスクIDT001から自動連番",
"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
View 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
# スプレッドシートIDDiscord通知用、オプション
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
View 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
# スプレッドシートIDDiscord通知用、オプション
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
View 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

File diff suppressed because it is too large Load diff

3211
gas/Code.gs.backup Normal file

File diff suppressed because it is too large Load diff

97
gas/Config.gs Normal file
View file

@ -0,0 +1,97 @@
/**
* 設定管理
*/
const CONFIG = {
// スプレッドシートID
SPREADSHEET_ID: '1y7U-3hVfdubQPh-H39k-5bvuHF7cOkHkOuA121GtxCI',
// Google DriveフォルダIDJSONファイルアップロード先
// .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
View 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);
// IDA列・親タスク名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
View 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 scriptsgas:*コマンド)
├── 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

File diff suppressed because it is too large Load diff

7
gas/appsscript.json Normal file
View file

@ -0,0 +1,7 @@
{
"timeZone": "Asia/Tokyo",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}

43
package.json Normal file
View 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
View 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
View 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
View 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
View 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"]
}