マキモノ
Web開発無料✅ 公式検証済みv1.0.0 / 更新

社内アプリの不具合報告に「対応したら本人の Discord へ返信」を付ける(Next.js + Discord Bot)

報告者を名簿から選ばせて Discord ユーザーIDを確定し、対応完了時に Bot が本人へ DM を返す。二重送信防止・DM不可の可視化・特権インテントの有効化と本番検証手順まで。

出品者: nishi@orgiast.jp📖 読込 約4,421トークン (約7円)💰 コスパ 9倍
トークン節約メーター85%節約
ゼロからAIに作らせた場合約4.8万トークン
このMDを読ませた場合約7,000トークン

約4.1万トークンの節約 (API料金換算で約62円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。

この巻物について

「社内アプリの不具合報告に「対応したら本人の Discord へ返信」を付ける(Next.js + Discord Bot)」は、Web開発カテゴリのAI指示書(MDファイル)です。報告者を名簿から選ばせて Discord ユーザーIDを確定し、対応完了時に Bot が本人へ DM を返す。二重送信防止・DM不可の可視化・特権インテントの有効化と本番検証手順まで。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約4.1万トークン(API料金換算で約62円)・85%のトークンを節約できます。

カテゴリ
Web開発
対応AI
claude-code、cursor、codex-cli
ライセンス
商用利用可 (再販不可)
価格
無料
ゼロから開発時
約4.8万トークン
この巻物使用時
約7,000トークン
節約量
約4.1万トークン (約62円)
更新日
2026-10-12

使い方 (AIに渡す3つの方法)

いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。

⬇ .md をダウンロード
claude "https://makimono-md.vercel.app/api/v1/files/discord-next-js-discord-bot/raw を読み込んで、この指示書どおりに実装して"
claude-codecursorcodex-cliライセンス: 商用利用可 (再販不可)

中身

社内アプリの不具合・要望フォームで「対応したら報告者本人の Discord に返信」を実装する(Next.js App Router + Discord Bot)

社内アプリに不具合報告フォームを付けると、報告した人は「あれどうなった?」を自分からは追えない。 報告が増えるほど「対応済みを伝える」手作業が増え、やがて誰も報告しなくなる。

この指示書は、報告者を特定し、対応完了時にその人の Discord へ Bot が DM を返すまでを、 Next.js App Router + 任意の DB(ここでは PostgREST 互換を例示)+ Discord Bot で作る手順をまとめる。 AI エージェントにそのまま読ませて実装させられる粒度で書いてある。

前提

  • Next.js App Router(app/ ディレクトリ)
  • Discord Bot を1つ作成済み(DISCORD_BOT_TOKEN がある)。Bot は対象サーバーに参加済み
  • 投稿を保存するテーブルが1つある(列は後述)
  • 管理画面が Cookie などで認証されている

設計の要点(ここを外すと動かない)

  1. 報告者の Discord ユーザーIDを投稿時に確定する。 名前の文字列だけでは DM を送れない。 フォームで「お名前」をサーバーのメンバー一覧から選ばせて、裏で ID を hidden に積む。
  2. 名簿が引けない環境でも壊れない。 メンバー一覧 API が失敗したら自由入力に落とし、 ID が無い投稿は「DM不可」と管理画面に表示する。沈黙しない。
  3. 二重送信を防ぐ列を持つ。 状態を何度 done にしても DM は1回だけ。
  4. 通知の失敗で本処理を失敗にしない。 状態更新は成功させ、DM 失敗は管理者へ別途知らせる。

1. テーブルに3列足す(冪等)

alter table public.app_feedback add column if not exists submitter_discord_id text;
alter table public.app_feedback add column if not exists reply_message text;
alter table public.app_feedback add column if not exists replied_at timestamptz;

既存列は id / kind / title / body / page_path / submitter / status / priority / admin_note / created_at / updated_at を想定。 status は new / triaged / in_progress / done / rejected。

2. Discord 送信ヘルパー lib/discord-dm.ts

const API = "https://discord.com/api/v10";
const H = () => ({ Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}` });

export function isDiscordUserId(v: string): boolean {
  return /^\d{17,20}$/.test(v);
}

export async function sendDirectMessage(userId: string, content: string): Promise<boolean> {
  if (!process.env.DISCORD_BOT_TOKEN || !isDiscordUserId(userId)) return false;
  try {
    // 1) 相手との DM チャンネルを開く(既にあれば同じ ID が返る)
    const ch = await fetch(`${API}/users/@me/channels`, {
      method: "POST",
      headers: { ...H(), "Content-Type": "application/json" },
      body: JSON.stringify({ recipient_id: userId }),
    });
    if (!ch.ok) return false;
    const { id } = await ch.json();
    // 2) そこへ送る。allowed_mentions を空にして意図しないメンションを防ぐ
    const msg = await fetch(`${API}/channels/${id}/messages`, {
      method: "POST",
      headers: { ...H(), "Content-Type": "application/json" },
      body: JSON.stringify({ content, allowed_mentions: { parse: [] } }),
    });
    return msg.ok;
  } catch {
    return false;   // 通知の失敗を外へ投げない
  }
}

let cache: { at: number; rows: { id: string; name: string }[] } | null = null;

export async function fetchGuildMembers(): Promise<{ id: string; name: string }[]> {
  if (cache && Date.now() - cache.at < 60 * 60 * 1000) return cache.rows;
  try {
    const g = await fetch(`${API}/users/@me/guilds`, { headers: H() });
    if (!g.ok) return [];
    const map = new Map<string, string>();
    for (const guild of await g.json()) {
      const m = await fetch(`${API}/guilds/${guild.id}/members?limit=1000`, { headers: H() });
      if (!m.ok) continue;                      // 権限が無いサーバーは黙って飛ばす
      for (const row of await m.json()) {
        if (row.user?.bot) continue;
        map.set(row.user.id, row.nick || row.user.global_name || row.user.username);
      }
    }
    const rows = [...map].map(([id, name]) => ({ id, name }))
      .sort((a, b) => a.name.localeCompare(b.name, "ja"));
    cache = { at: Date.now(), rows };
    return rows;
  } catch {
    return [];
  }
}

メンバー一覧には特権インテントが要る。 無効のままだと 403 {"code":50001,"message":"Missing Access"} になる。 開発者ポータルの Bot ページで SERVER MEMBERS INTENT をオンにするか、API で有効化する(後述)。

3. 名簿 API app/api/feedback/members/route.ts

import { NextResponse } from "next/server";
import { fetchGuildMembers } from "@/lib/discord-dm";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export async function GET() {
  const members = await fetchGuildMembers();
  // 取れなくても 200 + 空配列。フォーム側は自由入力に落ちる
  return NextResponse.json({ ok: true, members }, { headers: { "Cache-Control": "no-store" } });
}

4. フォーム側(クライアント)

  • モーダルを開いた最初の1回だけ /api/feedback/members を取得する(開くたびに叩かない)
  • 1件以上あれば <select>、末尾に「一覧にない(自分で入力)」を置く。0件ならテキスト入力だけ
  • 選んだ {name, id} を localStorage に保存し、次回の初期選択にする。読み書きは必ず try/catch (プライベートウィンドウで例外になる)。保存値が今の名簿に無ければ無視する
  • 送信する FormData に submitter(表示名)と submitter_discord_id(選択時のみ)を入れる

5. 投稿 API(抜粋)

const submitterId = String(form.get("submitter_discord_id") || "");
const discordId = isDiscordUserId(submitterId) ? submitterId : null;
// ... DB へ insert(discordId も保存)

// 管理者への通知は DM を第1経路にし、失敗時だけ従来のチャンネル通知へ落とす
let via = "dm";
let ok = await sendDirectMessage(process.env.ADMIN_DISCORD_USER_ID!, content);
if (!ok) { ok = await notifyChannel(content); via = "bot"; }
return NextResponse.json({ ok: true, id, sinks: { db: true, discord: ok, via } });

sinks.via を返すと、本番で1回叩くだけでどの経路を通ったか分かる。検証がとても楽になる。

6. 状態更新 API(ここが本題)

// 1) 先に現在の行を読む(旧 status と replied_at が要る)
const before = await getRow(id);

// 2) 更新する(reply_message も保存)
await patchRow(id, { status, priority, reply_message: replyMessage || null, updated_at: now });

// 3) 条件を全部満たす時だけ DM
const closing = status === "done" || status === "rejected";
if (closing && before.status !== status && !before.replied_at
    && isDiscordUserId(before.submitter_discord_id || "")) {
  const body = [
    `✅ ${APP_NAME} — ご報告の対応が完了しました`,
    `件名: ${before.title}`,
    status === "rejected" ? "今回は対応を見送らせていただきました。" : "",
    "",
    replyMessage || "対応しました。ご報告ありがとうございました。",
    "",
    "── ご報告いただいた内容",
    String(before.body).slice(0, 200),
  ].filter(Boolean).join("\n");

  if (await sendDirectMessage(before.submitter_discord_id, body)) {
    await patchRow(id, { replied_at: new Date().toISOString() });  // 二重送信を防ぐ印
  } else {
    await sendDirectMessage(ADMIN_ID, `⚠️ ${APP_NAME} — 完了報告を報告者へ返せませんでした\n件名: ${before.title}`);
  }
}
// 4) 通知の成否に関わらず一覧へリダイレクト

管理画面側には 返信メッセージ の textarea と、行ごとの DM可 / DM不可(名前が一覧から選ばれていません) / 返信済み <日時> を出す。DM不可が見えないと、届いていないことに誰も気づかない。

7. 特権インテントを API で有効にする

開発者ポータルを開かずに済ませたい場合、アプリ所有者の Bot トークンで次を1回叩く。

curl -X PATCH "https://discord.com/api/v10/applications/@me" \
  -H "Authorization: Bot $DISCORD_BOT_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary '{"flags":32768}'

32768 = 1 << 15(GATEWAY_GUILD_MEMBERS_LIMITED)。

注意: flags は置き換えなので、元から立っていたビットが消える。 先に現在値を読んでから OR する。

curl -s -H "Authorization: Bot $DISCORD_BOT_TOKEN" \
  "https://discord.com/api/v10/applications/@me" | grep -o '"flags":[0-9]*'

成功判定は「設定が変わったこと」ではなく 実際に名簿が引けることで行う。

curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bot $DISCORD_BOT_TOKEN" \
  "https://discord.com/api/v10/guilds/<GUILD_ID>/members?limit=5"   # 200 なら成功

8. 本番での検証手順(これを通すまで「完成」と呼ばない)

# ① 名簿が返るか(0件ならインテント未設定)
curl -s "https://<app>/api/feedback/members?cb=$(date +%s)"

# ② 投稿 → 管理者へ DM。via が dm なら第1経路を通っている
curl -s -X POST -F "kind=bug" -F "title=動作確認" -F "body=テストです" \
  -F "submitter=動作確認" -F "submitter_discord_id=<自分のID>" \
  "https://<app>/api/feedback"

# ③ 完了にする(管理者 Cookie が要る)
curl -s -X POST -H "Cookie: <認証Cookie>" \
  -F "id=<①で返った id>" -F "status=done" -F "priority=normal" \
  -F "reply_message=動作確認です" "https://<app>/api/feedback/update"

# ④ replied_at が入っていれば DM 送信成功(成功時だけ記録される設計なので、これが証拠になる)

最後にテスト行を削除する。実在のチャンネルや他人へ DM を飛ばすテストはしない。 宛先は自分か、もともとその通知を受け取っている管理者だけにする。

つまずきやすい点

症状原因と対処
名簿が常に空SERVER MEMBERS INTENT 未設定。403 code 50001 が出ているはず
設定変更 API が拒否される複数フラグをまとめて送っていないか。変更したい1つだけに絞ると通ることがある
DM が届かない相手が DM を拒否している/共通サーバーに居ない。sendDirectMessage が false を返すので管理者へ通知して気づけるようにする
完了にするたび DM が飛ぶreplied_at を見ていない。旧 status との比較だけでは再発する
日本語が DB で化けるWindows の curl -F が CP932 で送っている。検証は Node の FormData + fetch で行う
PostgREST が 401 を返すPowerShell の Invoke-RestMethod は新形式キーで 401 になることがある。curl.exe なら 200

この設計を選ぶ理由

報告者の特定を「名前の自由入力」にすると、対応完了時に誰へ返すか決められず、結局人が仲介する。 逆に「全員に Discord ID を調べて入力させる」のは初回の手間が大きい。 名簿から1回選ぶだけにして ID を裏で確定させるのが、報告側の手間と確実性のつり合いが最も良い。

よくある質問

+「社内アプリの不具合報告に「対応したら本人の Discord へ返信」を付ける(Next.js + Discord Bot)」とは何ですか?

報告者を名簿から選ばせて Discord ユーザーIDを確定し、対応完了時に Bot が本人へ DM を返す。二重送信防止・DM不可の可視化・特権インテントの有効化と本番検証手順まで。

+どれくらいトークン(費用)を節約できますか?

ゼロから開発すると約4.8万トークンかかりますが、この巻物を使えば約7,000トークンで済みます。差し引き約4.1万トークン(API料金換算で約62円)・85%の節約です。

+どうやって使いますか?

無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。

+どのAIツールに対応していますか?

claude-code、cursor、codex-cli に対応しています。

+商用利用できますか?

ライセンスは「商用利用可 (再販不可)」です。

🤝 自分でAIを動かすのは、まだ不安…という方へ

この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。

AI代行堂を見る →

関連する巻物

この巻物、誰かのトークンも救えます

𝕏 で節約レシートをシェア