diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..910964a --- /dev/null +++ b/SETUP.md @@ -0,0 +1,92 @@ +# セットアップ手順 + +## 1. スクリプトプロパティ(環境変数)の設定 + +GASエディタで環境変数を設定します。 + +### 設定方法 + +1. `npm run deploy` でデプロイ +2. `npm run open` でGASエディタを開く +3. 左メニューの「プロジェクトの設定」(⚙️アイコン)をクリック +4. 下部の「スクリプト プロパティ」セクションまでスクロール +5. 「スクリプト プロパティを追加」をクリック +6. 以下の3つのプロパティを追加: + +| プロパティ | 値 | 説明 | +|---------|-----|------| +| `DISCORD_WEBHOOK_URL` | `https://discord.com/api/webhooks/...` | Discord Webhook URL | +| `DISCORD_MENTION_ID` | `755410747042955294` | メンションするユーザーID | +| `APPLICATION_FORM_URL` | `https://forms.gle/...` | 申請フォームのURL | + +### Discord Webhook URLの取得方法 + +1. Discordサーバーの設定を開く +2. 「連携サービス」→「ウェブフック」 +3. 「新しいウェブフック」をクリック +4. チャンネルを選択してウェブフックURLをコピー + +### メンションIDの取得方法 + +1. Discord開発者モードを有効化(ユーザー設定→詳細設定→開発者モード) +2. ユーザーを右クリック→「IDをコピー」 + +## 2. トリガーの設定 + +GASエディタで以下の手順でトリガーを設定します。 + +1. GASエディタを開く +2. 左メニューの「トリガー」(時計アイコン)をクリック +3. 「トリガーを追加」 +4. 以下のように設定: + - 実行する関数: `onEdit` + - イベントのソース: `スプレッドシートから` + - イベントの種類: `編集時` + - 保存 + +## 3. テスト + +### 手動テスト + +1. スプレッドシートの任意のデータ行を選択 +2. GASエディタで `testNotification` 関数を実行 +3. Discordに通知が届くことを確認 + +### 実際の動作テスト + +1. スプレッドシートに新しい行を追加 +2. C列に申請者名を入力 +3. D列に申請種類を入力 +4. Discordに通知が届くことを確認 + +## トラブルシューティング + +### 通知が届かない場合 + +1. GASエディタの「実行ログ」を確認 +2. スクリプトプロパティが正しく設定されているか確認 + - 左メニュー「プロジェクトの設定」→「スクリプト プロパティ」 +3. Webhook URLが正しいか確認 + +### 環境変数が設定されているか確認 + +GASエディタで以下のスクリプトを実行: + +1. GASエディタのツールバーで関数選択を「testNotification」から「デバッグ用」に変更 +2. 以下のコードをエディタに貼り付けて実行: + +```javascript +function checkConfig() { + const props = PropertiesService.getScriptProperties(); + Logger.log('DISCORD_WEBHOOK_URL: ' + props.getProperty('DISCORD_WEBHOOK_URL')); + Logger.log('DISCORD_MENTION_ID: ' + props.getProperty('DISCORD_MENTION_ID')); + Logger.log('APPLICATION_FORM_URL: ' + props.getProperty('APPLICATION_FORM_URL')); +} +``` + +3. 「実行ログ」で値を確認 + +## 列の対応 + +- **C列**: 申請者名 +- **D列**: 申請種類 diff --git a/src/App.ts b/src/App.ts index 2de4aed..39c700d 100644 --- a/src/App.ts +++ b/src/App.ts @@ -1,14 +1,63 @@ +import { getApplicationFromRow } from './application'; +import { sendDiscordNotification } from './discord'; + /** - * メインのアプリケーション関数 + * スプレッドシートに行が追加されたときのトリガー */ -export const App = () => { - console.log('App started!'); +export const onEdit = (e: GoogleAppsScript.Events.SheetsOnEdit) => { + try { + const range = e.range; + const sheet = range.getSheet(); + const row = range.getRow(); + + // ヘッダー行(1行目)は無視 + if (row <= 1) { + return; + } + + // 申請情報を取得 + const application = getApplicationFromRow(sheet, row); + + if (!application) { + Logger.log(`行${row}: 申請情報が不完全のため通知をスキップ`); + return; + } + + // Discord通知を送信 + sendDiscordNotification(application); + Logger.log(`行${row}: 通知送信完了`); + } catch (error) { + Logger.log(`エラー: ${error}`); + throw error; + } +}; +/** + * テスト用: 手動で通知を送信 + */ +export const testNotification = () => { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getActiveSheet(); - Logger.log('スプレッドシート名: ' + ss.getName()); - Logger.log('シート名: ' + sheet.getName()); + // アクティブセルの行で通知テスト + const activeRow = sheet.getActiveCell().getRow(); + + if (activeRow <= 1) { + SpreadsheetApp.getUi().alert('データ行(2行目以降)を選択してください'); + return; + } + + const application = getApplicationFromRow(sheet, activeRow); + + if (!application) { + SpreadsheetApp.getUi().alert('申請情報が取得できませんでした'); + return; + } - SpreadsheetApp.getUi().alert('接続成功!'); + try { + sendDiscordNotification(application); + SpreadsheetApp.getUi().alert('通知送信成功!Discordを確認してください。'); + } catch (error) { + SpreadsheetApp.getUi().alert(`エラー: ${error}`); + } }; diff --git a/src/application.ts b/src/application.ts new file mode 100644 index 0000000..f44a2a5 --- /dev/null +++ b/src/application.ts @@ -0,0 +1,31 @@ +/** + * 申請情報の型定義 + */ +export interface Application { + applicantName: string; // 申請者名(C列) + applicationType: string; // 申請種類(D列) + rowNumber: number; // 行番号 +} + +/** + * 指定された行から申請情報を取得 + */ +export function getApplicationFromRow( + sheet: GoogleAppsScript.Spreadsheet.Sheet, + rowNumber: number +): Application | null { + // C列とD列のデータを取得 + const applicantName = sheet.getRange(rowNumber, 3).getValue() as string; // C列 + const applicationType = sheet.getRange(rowNumber, 4).getValue() as string; // D列 + + // 両方の値が存在する場合のみ有効な申請として扱う + if (!applicantName || !applicationType) { + return null; + } + + return { + applicantName: applicantName.toString().trim(), + applicationType: applicationType.toString().trim(), + rowNumber, + }; +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..f700a66 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,36 @@ +/** + * 環境変数の型定義 + */ +export interface Config { + DISCORD_WEBHOOK_URL: string; + DISCORD_MENTION_ID: string; + APPLICATION_FORM_URL: string; +} + +/** + * スクリプトプロパティから設定を取得 + */ +export function getConfig(): Config { + const props = PropertiesService.getScriptProperties(); + const webhookUrl = props.getProperty('DISCORD_WEBHOOK_URL'); + const mentionId = props.getProperty('DISCORD_MENTION_ID'); + const formUrl = props.getProperty('APPLICATION_FORM_URL'); + + if (!webhookUrl) { + throw new Error('DISCORD_WEBHOOK_URL が設定されていません'); + } + + if (!mentionId) { + throw new Error('DISCORD_MENTION_ID が設定されていません'); + } + + if (!formUrl) { + throw new Error('APPLICATION_FORM_URL が設定されていません'); + } + + return { + DISCORD_WEBHOOK_URL: webhookUrl, + DISCORD_MENTION_ID: mentionId, + APPLICATION_FORM_URL: formUrl, + }; +} diff --git a/src/discord.ts b/src/discord.ts new file mode 100644 index 0000000..d354b9f --- /dev/null +++ b/src/discord.ts @@ -0,0 +1,70 @@ +import { Application } from './application'; +import { getConfig } from './config'; + +/** + * Discord Webhookのペイロード型 + */ +interface DiscordWebhookPayload { + content: string; + username?: string; + avatar_url?: string; +} + +/** + * Discordに通知を送信 + */ +export function sendDiscordNotification(application: Application): void { + const config = getConfig(); + + // メッセージ内容を構築 + const message = createNotificationMessage( + config.DISCORD_MENTION_ID, + config.APPLICATION_FORM_URL, + application + ); + + const payload: DiscordWebhookPayload = { + content: message, + username: '申請通知Bot', + }; + + const options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = { + method: 'post', + contentType: 'application/json', + payload: JSON.stringify(payload), + muteHttpExceptions: true, + }; + + try { + const response = UrlFetchApp.fetch(config.DISCORD_WEBHOOK_URL, options); + const responseCode = response.getResponseCode(); + + if (responseCode !== 204 && responseCode !== 200) { + throw new Error(`Discord API error: ${responseCode} - ${response.getContentText()}`); + } + + Logger.log(`Discord通知送信成功: 行${application.rowNumber}`); + } catch (error) { + Logger.log(`Discord通知送信失敗: ${error}`); + throw error; + } +} + +/** + * 通知メッセージを作成 + */ +function createNotificationMessage( + mentionId: string, + formUrl: string, + application: Application +): string { + return `<@${mentionId}> + +📝 **新しいエクスプレッション申請が追加されました** + +**申請種類:** ${application.applicationType} +**申請者:** ${application.applicantName} +**行番号:** ${application.rowNumber} + +🔗 **【Discord】絵文字/スタンプ/サウンドボード申請フォーム:** ${formUrl}`; +} diff --git a/src/main.ts b/src/main.ts index ae7bb31..47256a9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,11 @@ -import { App } from './App'; +import { onEdit, testNotification } from './App'; interface Global { - App: typeof App; + onEdit: typeof onEdit; + testNotification: typeof testNotification; } declare const global: Global; // entryPoints -global.App = App; +global.onEdit = onEdit; +global.testNotification = testNotification;