diff --git a/.env.example b/.env.example index 6c5e8ea..7e752b1 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,13 @@ ADMIN_ROLE_ID="1234567890" # 昨年度メンバーロールのID(継続メンバー判定に使用) RETURNING_MEMBER_ROLE_ID="1234567890" +# 年度メンバーロールのID(例: Member2026)— オンボーディング完了時・バッチ付与で使用 +MEMBER_ROLE_ID="" + +# メンバー種別・学年・学部・興味分野のロールは Discord ギルドのロール名で自動マッチング。 +# Discord サーバーのロール名をプロフィール値(例: "理工学部", "Python", "学部3年", "学部生")と +# 一致させれば env var の設定なしに自動で付与される。 + # 新入生ロールのID と 付与対象の判定基準日時 # 管理者ページ「参加日時によるロール付与」のフォーム初期値として使用 # NEW_MEMBER_JOINED_AFTER: ISO 8601 形式(タイムゾーンオフセット付き) diff --git a/app/api/onboarding/route.ts b/app/api/onboarding/route.ts index bf1536f..40c7d03 100644 --- a/app/api/onboarding/route.ts +++ b/app/api/onboarding/route.ts @@ -9,6 +9,7 @@ import { buildAdminOnboardingCompletedNotification, } from "@/lib/discord-dm"; import { checkLineGroupMembership } from "@/lib/line-invite"; +import { syncMemberDiscordRoles } from "@/lib/discord-role"; export async function POST() { const session = await auth(); @@ -101,6 +102,14 @@ export async function POST() { ).catch((e) => { console.error("Failed to notify admin channel (onboarding):", e); }); + + syncMemberDiscordRoles(session.user.id, { + year: updatedMember.yearByFiscal?.[String(new Date().getFullYear())], + faculty: updatedMember.enrollments?.find((e) => e.isCurrent)?.faculty, + memberType: updatedMember.memberType, + }).catch((e) => { + console.error("Failed to sync Discord roles (onboarding):", e); + }); } return NextResponse.json({ success: true }); diff --git a/app/api/optout/rejoin/route.ts b/app/api/optout/rejoin/route.ts index 039e0bb..dcd2c11 100644 --- a/app/api/optout/rejoin/route.ts +++ b/app/api/optout/rejoin/route.ts @@ -4,6 +4,7 @@ import { deleteOptoutSubmission } from "@/lib/discord-optout"; import { getMember, markMemberRejoined } from "@/lib/members"; import { sendDiscordDm, buildRejoinCompletedMessage } from "@/lib/discord-dm"; import { fetchDiscordDisplayName } from "@/lib/discord"; +import { syncMemberDiscordRoles } from "@/lib/discord-role"; export async function POST() { const session = await auth(); @@ -24,9 +25,10 @@ export async function POST() { ); } - // 再加入歓迎 DM (失敗してもレスポンスには影響させない) + // 再加入歓迎 DM + ロール同期 (失敗してもレスポンスには影響させない) + let member: Awaited> = null; try { - const member = await getMember(discordId); + member = await getMember(discordId); const displayName = member?.discordUsername ?? member?.nickname ?? @@ -37,5 +39,16 @@ export async function POST() { console.error("Failed to send rejoin DM:", e); } + // 退会者ロール削除 + 通常ロール付与 (fire-and-forget) + if (member) { + syncMemberDiscordRoles(discordId, { + year: member.yearByFiscal?.[String(new Date().getFullYear())], + faculty: member.enrollments?.find((e) => e.isCurrent)?.faculty, + memberType: member.memberType, + }).catch((e) => { + console.error("Failed to sync Discord roles (rejoin):", e); + }); + } + return NextResponse.json({ success: true }); } diff --git a/app/internal/(protected)/admin/page.tsx b/app/internal/(protected)/admin/page.tsx index 063c4e4..94b1a02 100644 --- a/app/internal/(protected)/admin/page.tsx +++ b/app/internal/(protected)/admin/page.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import { getUnregisteredMembers } from "@/lib/admin/actions"; import { AdminNotificationPanel } from "@/components/admin/notification-panel"; +import { RoleSyncPanel } from "@/components/admin/role-sync-panel"; import { RoleAssignmentPanel } from "@/components/admin/role-assignment-panel"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; @@ -83,6 +84,14 @@ export default async function AdminPage() { )} +
+

Discordロール付与

+

+ 登録済みメンバー全員に年度メンバーロールおよびメンバー種別ロールを一括付与します。 +

+ +
+

参加日時によるロール付与

diff --git a/app/layout.tsx b/app/layout.tsx index 9283e8e..72cd907 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,5 @@ import type React from "react"; import type { Metadata } from "next"; -import Script from "next/script"; import { Noto_Sans_JP } from "next/font/google"; import "./globals.css"; import Header from "@/components/header"; @@ -70,9 +69,7 @@ export default function RootLayout({ return ( -