セットアップ手順書(通知+追加6機能)

endai-system / 第14回日本地域理学療法学会学術大会 — 2026-02-15作成・更新

1
コードのデプロイ 完了済み
通知機能のコードはすべてコミット・プッシュ済みです。Vercelの自動デプロイで本番に反映されます。
コミット c832808 で22ファイル、+1,418行を追加しました。
2
Vercel環境変数の設定 完了済み
Web Push通知に必要なVAPID鍵をVercelのProduction環境に設定済みです。
変数名 用途
NEXT_PUBLIC_VAPID_PUBLIC_KEY BLzmavhlbb3A1SHc2Kae1iwT4-mtaPFCxtuJ-RCh_3LTaqdug2QLz4Ch36lzPCoy7yf0gWbwF1EAEnplwV-eb-s VAPID公開鍵(クライアント用)
VAPID_PRIVATE_KEY (秘密鍵・設定済み) VAPID秘密鍵(サーバー用)
WEB_PUSH_EMAIL mailto:kawaguchi.ns.reha033@gmail.com VAPID連絡先
3
Supabase DBマイグレーション実行 要手動実行
8つのSQLを Supabase Dashboard の SQL Editor で順番に実行してください。
順番を守ることが重要です(外部キー制約のため)。
022〜024: 通知機能 / 025〜029: 追加6機能
実行前に必ずダッシュボードの Database > Backups で最新のバックアップがあることを確認してください。
  1. Supabase Dashboard にログイン
  2. プロジェクト vlyhbrnjriqiyvuufgid を選択
  3. 左メニューの SQL Editor を開く
  4. 以下の3つのSQLを 1つずつ順番に 貼り付けて「Run」で実行
3-1. batch_emails テーブル(022_batch_emails.sql)
-- batch_emails テーブル(予約送信・一斉メール管理)
CREATE TABLE IF NOT EXISTS public.batch_emails (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  conference_id UUID REFERENCES public.conferences(id) ON DELETE SET NULL,
  subject TEXT NOT NULL,
  body TEXT NOT NULL,
  target_type TEXT NOT NULL CHECK (target_type IN (
    'all_registrations', 'paid_registrations', 'unpaid_registrations',
    'abstract_submitters', 'custom'
  )),
  target_filter JSONB DEFAULT NULL,
  total_count INTEGER NOT NULL DEFAULT 0,
  sent_count INTEGER NOT NULL DEFAULT 0,
  failed_count INTEGER NOT NULL DEFAULT 0,
  status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'sending', 'completed', 'cancelled')),
  scheduled_at TIMESTAMPTZ DEFAULT NULL,
  started_at TIMESTAMPTZ DEFAULT NULL,
  completed_at TIMESTAMPTZ DEFAULT NULL,
  created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- インデックス
CREATE INDEX IF NOT EXISTS idx_batch_emails_conference ON public.batch_emails(conference_id);
CREATE INDEX IF NOT EXISTS idx_batch_emails_status ON public.batch_emails(status);
CREATE INDEX IF NOT EXISTS idx_batch_emails_scheduled ON public.batch_emails(scheduled_at) WHERE scheduled_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_batch_emails_created_by ON public.batch_emails(created_by);

-- updated_at自動更新トリガー
CREATE TRIGGER update_batch_emails_updated_at
  BEFORE UPDATE ON public.batch_emails
  FOR EACH ROW EXECUTE FUNCTION update_updated_at();

-- RLS有効化
ALTER TABLE public.batch_emails ENABLE ROW LEVEL SECURITY;

-- 管理者・運営者のみ全操作
CREATE POLICY "batch_emails_admin_all" ON public.batch_emails
  FOR ALL USING (
    EXISTS (
      SELECT 1 FROM public.profiles
      WHERE profiles.id = auth.uid()
      AND profiles.role IN ('admin', 'organizer')
    )
  )
  WITH CHECK (
    EXISTS (
      SELECT 1 FROM public.profiles
      WHERE profiles.id = auth.uid()
      AND profiles.role IN ('admin', 'organizer')
    )
  );

-- サービスロール(Cron用)
CREATE POLICY "batch_emails_service_role" ON public.batch_emails
  FOR ALL USING (auth.role() = 'service_role')
  WITH CHECK (auth.role() = 'service_role');
3-2. notifications テーブル(023_notifications.sql)
-- アプリ内通知テーブル
CREATE TABLE IF NOT EXISTS public.notifications (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
  type TEXT NOT NULL CHECK (type IN (
    'review_request',
    'decision_result',
    'session_assignment',
    'announcement',
    'batch_email_completed',
    'revision_request',
    'system'
  )),
  title TEXT NOT NULL,
  body TEXT NOT NULL,
  link TEXT DEFAULT NULL,
  is_read BOOLEAN NOT NULL DEFAULT FALSE,
  related_id UUID DEFAULT NULL,
  related_type TEXT DEFAULT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- インデックス
CREATE INDEX IF NOT EXISTS idx_notifications_user_unread ON public.notifications(user_id, is_read) WHERE is_read = FALSE;
CREATE INDEX IF NOT EXISTS idx_notifications_user_created ON public.notifications(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_notifications_type ON public.notifications(type);

-- RLS有効化
ALTER TABLE public.notifications ENABLE ROW LEVEL SECURITY;

-- 本人の通知のみ閲覧
CREATE POLICY "notifications_select_own" ON public.notifications
  FOR SELECT USING (user_id = auth.uid());

-- 本人の通知のみ更新(既読化)
CREATE POLICY "notifications_update_own" ON public.notifications
  FOR UPDATE USING (user_id = auth.uid())
  WITH CHECK (user_id = auth.uid());

-- 本人の通知のみ削除
CREATE POLICY "notifications_delete_own" ON public.notifications
  FOR DELETE USING (user_id = auth.uid());

-- 管理者・運営者がINSERT可能
CREATE POLICY "notifications_insert_admin" ON public.notifications
  FOR INSERT WITH CHECK (
    EXISTS (
      SELECT 1 FROM public.profiles
      WHERE profiles.id = auth.uid()
      AND profiles.role IN ('admin', 'organizer')
    )
  );

-- サービスロール(Server Actions用)
CREATE POLICY "notifications_service_role" ON public.notifications
  FOR ALL USING (auth.role() = 'service_role')
  WITH CHECK (auth.role() = 'service_role');
3-3. push_subscriptions テーブル(024_push_subscriptions.sql)
-- Web Push購読管理テーブル
CREATE TABLE IF NOT EXISTS public.push_subscriptions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
  endpoint TEXT NOT NULL,
  p256dh TEXT NOT NULL,
  auth TEXT NOT NULL,
  user_agent TEXT DEFAULT NULL,
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE(user_id, endpoint)
);

-- インデックス
CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user_active ON public.push_subscriptions(user_id, is_active) WHERE is_active = TRUE;

-- updated_at自動更新トリガー
CREATE TRIGGER update_push_subscriptions_updated_at
  BEFORE UPDATE ON public.push_subscriptions
  FOR EACH ROW EXECUTE FUNCTION update_updated_at();

-- RLS有効化
ALTER TABLE public.push_subscriptions ENABLE ROW LEVEL SECURITY;

-- 本人のみ操作
CREATE POLICY "push_subscriptions_own" ON public.push_subscriptions
  FOR ALL USING (user_id = auth.uid())
  WITH CHECK (user_id = auth.uid());

-- サービスロール(Push送信時の購読取得用)
CREATE POLICY "push_subscriptions_service_role" ON public.push_subscriptions
  FOR ALL USING (auth.role() = 'service_role')
  WITH CHECK (auth.role() = 'service_role');

以下は追加6機能用のマイグレーションです(025〜029)。通知機能のSQL(022〜024)を先に実行してください。
3-4. セッション配信カラム追加(025_session_streaming.sql)
-- セッションにライブ配信・アーカイブURL・ハイブリッドフラグを追加
ALTER TABLE public.sessions
  ADD COLUMN IF NOT EXISTS streaming_url TEXT DEFAULT NULL,
  ADD COLUMN IF NOT EXISTS recording_url TEXT DEFAULT NULL,
  ADD COLUMN IF NOT EXISTS is_hybrid BOOLEAN NOT NULL DEFAULT FALSE;
3-5. プロフィール公開設定(026_participant_visibility.sql)
-- プロフィール公開設定を追加
ALTER TABLE public.profiles
  ADD COLUMN IF NOT EXISTS is_profile_public BOOLEAN NOT NULL DEFAULT FALSE;
3-6. セッションQ&A(027_session_qa.sql)
-- セッションQ&A: 質問テーブル
CREATE TABLE IF NOT EXISTS public.session_questions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  session_id UUID NOT NULL REFERENCES public.sessions(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  body TEXT NOT NULL,
  is_anonymous BOOLEAN NOT NULL DEFAULT FALSE,
  is_selected BOOLEAN NOT NULL DEFAULT FALSE,
  is_answered BOOLEAN NOT NULL DEFAULT FALSE,
  vote_count INTEGER NOT NULL DEFAULT 0,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- セッションQ&A: 投票テーブル
CREATE TABLE IF NOT EXISTS public.session_question_votes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  question_id UUID NOT NULL REFERENCES public.session_questions(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE(question_id, user_id)
);

-- インデックス
CREATE INDEX IF NOT EXISTS idx_session_questions_session_id ON public.session_questions(session_id);
CREATE INDEX IF NOT EXISTS idx_session_question_votes_question_id ON public.session_question_votes(question_id);

-- RLS
ALTER TABLE public.session_questions ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.session_question_votes ENABLE ROW LEVEL SECURITY;

CREATE POLICY "認証ユーザーが質問を閲覧" ON public.session_questions
  FOR SELECT USING (auth.uid() IS NOT NULL);

CREATE POLICY "認証ユーザーが質問を投稿" ON public.session_questions
  FOR INSERT WITH CHECK (auth.uid() = user_id);

CREATE POLICY "本人が質問を削除" ON public.session_questions
  FOR DELETE USING (auth.uid() = user_id);

CREATE POLICY "管理者が質問を更新" ON public.session_questions
  FOR UPDATE USING (
    EXISTS (
      SELECT 1 FROM public.profiles
      WHERE id = auth.uid() AND role IN ('admin', 'organizer')
    )
  );

CREATE POLICY "認証ユーザーが投票を閲覧" ON public.session_question_votes
  FOR SELECT USING (auth.uid() IS NOT NULL);

CREATE POLICY "認証ユーザーが投票" ON public.session_question_votes
  FOR INSERT WITH CHECK (auth.uid() = user_id);

CREATE POLICY "本人が投票を削除" ON public.session_question_votes
  FOR DELETE USING (auth.uid() = user_id);
3-7. アンケート(028_surveys.sql)
-- アンケートテーブル
CREATE TABLE IF NOT EXISTS public.surveys (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  conference_id UUID NOT NULL REFERENCES public.conferences(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  description TEXT,
  target_type TEXT NOT NULL DEFAULT 'all',
  is_active BOOLEAN NOT NULL DEFAULT FALSE,
  start_at TIMESTAMPTZ,
  end_at TIMESTAMPTZ,
  created_by UUID NOT NULL REFERENCES auth.users(id),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- アンケート設問テーブル
CREATE TABLE IF NOT EXISTS public.survey_questions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  survey_id UUID NOT NULL REFERENCES public.surveys(id) ON DELETE CASCADE,
  question_text TEXT NOT NULL,
  question_type TEXT NOT NULL DEFAULT 'rating',
  options JSONB DEFAULT '[]',
  sort_order INTEGER NOT NULL DEFAULT 0,
  is_required BOOLEAN NOT NULL DEFAULT TRUE
);

-- 回答テーブル
CREATE TABLE IF NOT EXISTS public.survey_responses (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  survey_id UUID NOT NULL REFERENCES public.surveys(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES auth.users(id),
  completed_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE(survey_id, user_id)
);

-- 回答詳細テーブル
CREATE TABLE IF NOT EXISTS public.survey_answers (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  response_id UUID NOT NULL REFERENCES public.survey_responses(id) ON DELETE CASCADE,
  question_id UUID NOT NULL REFERENCES public.survey_questions(id) ON DELETE CASCADE,
  answer_text TEXT,
  answer_rating INTEGER,
  answer_choices JSONB DEFAULT '[]'
);

-- インデックス
CREATE INDEX IF NOT EXISTS idx_surveys_conference_id ON public.surveys(conference_id);
CREATE INDEX IF NOT EXISTS idx_survey_questions_survey_id ON public.survey_questions(survey_id);
CREATE INDEX IF NOT EXISTS idx_survey_responses_survey_id ON public.survey_responses(survey_id);
CREATE INDEX IF NOT EXISTS idx_survey_answers_response_id ON public.survey_answers(response_id);

-- RLS
ALTER TABLE public.surveys ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.survey_questions ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.survey_responses ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.survey_answers ENABLE ROW LEVEL SECURITY;

CREATE POLICY "認証ユーザーがアンケート閲覧" ON public.surveys
  FOR SELECT USING (auth.uid() IS NOT NULL);
CREATE POLICY "管理者がアンケート管理" ON public.surveys
  FOR ALL USING (
    EXISTS (SELECT 1 FROM public.profiles WHERE id = auth.uid() AND role IN ('admin', 'organizer'))
  );

CREATE POLICY "認証ユーザーが設問閲覧" ON public.survey_questions
  FOR SELECT USING (auth.uid() IS NOT NULL);
CREATE POLICY "管理者が設問管理" ON public.survey_questions
  FOR ALL USING (
    EXISTS (SELECT 1 FROM public.profiles WHERE id = auth.uid() AND role IN ('admin', 'organizer'))
  );

CREATE POLICY "本人の回答を閲覧" ON public.survey_responses
  FOR SELECT USING (auth.uid() = user_id OR EXISTS (
    SELECT 1 FROM public.profiles WHERE id = auth.uid() AND role IN ('admin', 'organizer')
  ));
CREATE POLICY "認証ユーザーが回答作成" ON public.survey_responses
  FOR INSERT WITH CHECK (auth.uid() = user_id);

CREATE POLICY "回答詳細の閲覧" ON public.survey_answers
  FOR SELECT USING (
    EXISTS (
      SELECT 1 FROM public.survey_responses sr
      WHERE sr.id = response_id AND (sr.user_id = auth.uid() OR EXISTS (
        SELECT 1 FROM public.profiles WHERE id = auth.uid() AND role IN ('admin', 'organizer')
      ))
    )
  );
CREATE POLICY "回答詳細の作成" ON public.survey_answers
  FOR INSERT WITH CHECK (
    EXISTS (
      SELECT 1 FROM public.survey_responses sr
      WHERE sr.id = response_id AND sr.user_id = auth.uid()
    )
  );
3-8. ポスターファイル+コメント(029_poster_files.sql)
-- ポスターファイルテーブル
CREATE TABLE IF NOT EXISTS public.poster_files (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  abstract_id UUID NOT NULL REFERENCES public.abstracts(id) ON DELETE CASCADE,
  file_url TEXT NOT NULL,
  file_type TEXT NOT NULL DEFAULT 'pdf',
  uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- ポスターコメントテーブル
CREATE TABLE IF NOT EXISTS public.poster_comments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  abstract_id UUID NOT NULL REFERENCES public.abstracts(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  body TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- インデックス
CREATE INDEX IF NOT EXISTS idx_poster_files_abstract_id ON public.poster_files(abstract_id);
CREATE INDEX IF NOT EXISTS idx_poster_comments_abstract_id ON public.poster_comments(abstract_id);

-- RLS
ALTER TABLE public.poster_files ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.poster_comments ENABLE ROW LEVEL SECURITY;

CREATE POLICY "認証ユーザーがポスター閲覧" ON public.poster_files
  FOR SELECT USING (auth.uid() IS NOT NULL);

CREATE POLICY "投稿者がポスターアップロード" ON public.poster_files
  FOR INSERT WITH CHECK (
    EXISTS (
      SELECT 1 FROM public.abstracts a
      WHERE a.id = abstract_id AND (a.submitter_id = auth.uid() OR EXISTS (
        SELECT 1 FROM public.profiles WHERE id = auth.uid() AND role IN ('admin', 'organizer')
      ))
    )
  );

CREATE POLICY "投稿者がポスター削除" ON public.poster_files
  FOR DELETE USING (
    EXISTS (
      SELECT 1 FROM public.abstracts a
      WHERE a.id = abstract_id AND (a.submitter_id = auth.uid() OR EXISTS (
        SELECT 1 FROM public.profiles WHERE id = auth.uid() AND role IN ('admin', 'organizer')
      ))
    )
  );

CREATE POLICY "認証ユーザーがコメント閲覧" ON public.poster_comments
  FOR SELECT USING (auth.uid() IS NOT NULL);
CREATE POLICY "認証ユーザーがコメント投稿" ON public.poster_comments
  FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "本人がコメント削除" ON public.poster_comments
  FOR DELETE USING (auth.uid() = user_id);
各SQLの実行後、「Success. No rows returned」と表示されれば成功です。
4
動作確認 要手動確認
マイグレーション実行後、以下の手順で通知機能が正常に動作するか確認してください。

4-1. テーブル作成の確認

  1. Supabase Dashboard > Table Editor を開く
  2. 以下のテーブルが存在することを確認:
    • batch_emails(通知)
    • notifications(通知)
    • push_subscriptions(通知)
    • session_questionssession_question_votes(Q&A)
    • surveyssurvey_questionssurvey_responsessurvey_answers(アンケート)
    • poster_filesposter_comments(ポスター)

4-2. アプリ内通知 (ベルアイコン)

  1. https://endai-system.vercel.app/login にアクセス
  2. 管理者アカウントでログイン
  3. ヘッダー右側にベルアイコンが表示されていることを確認
  4. ベルアイコンをクリックし、通知ドロワーが開くことを確認
  5. 「通知はありません」と表示されればOK(まだ通知データがないため)

4-3. Web Push通知

  1. 通知ドロワー内の下部に「Push通知」トグルが表示されていることを確認
  2. トグルをONにすると、ブラウザの通知許可ダイアログが表示される
  3. 「許可」をクリックするとトグルがON状態になる
  4. Supabase の push_subscriptions テーブルにレコードが追加されていることを確認

4-4. 通知トリガーのテスト(任意)

管理画面から以下の操作を行うと、アプリ内通知が自動生成されます:
操作 通知タイプ 通知先
査読依頼の一括送信 review_request 査読者
採否結果の一括通知 decision_result 演題投稿者
セッション割当通知 session_assignment 発表者
一斉配信メール完了 batch_email_completed 配信作成者
修正申請の作成 revision_request 管理者
修正申請の承認/却下 revision_request 申請者
5
手動Push送信のテスト 任意
管理者がAPIから手動でPush通知を送信できます。curlまたはブラウザのDevToolsで確認可能です。
# 全ユーザーにPush通知を送信(管理者の認証Cookieが必要)
curl -X POST https://endai-system.vercel.app/api/push/send \
  -H "Content-Type: application/json" \
  -H "Cookie: (認証Cookie)" \
  -d '{
    "title": "テスト通知",
    "message": "Push通知のテストです。",
    "link": "/dashboard"
  }'
このAPIは管理者 (admin/organizer) のみ実行可能です。一般ユーザーは403エラーになります。

チェックリスト
状態 項目 担当
コードをGitHubにプッシュ 自動完了
Vercel環境変数(VAPID鍵3つ)を設定 自動完了
ローカル .env.local にVAPID鍵を追加 自動完了
Supabase SQL Editor で8つのSQLを実行 手動
テーブル作成の確認(Table Editor) 手動
ベルアイコン・通知ドロワーの表示確認 手動
Push通知トグルの動作確認 手動
追加6機能
Feature A: ライブ配信連携(コードデプロイ済み) 自動完了
Feature B: 参加者名簿(コードデプロイ済み) 自動完了
Feature C: リアルタイムQ&A(コードデプロイ済み) 自動完了
Feature D: アンケート(コードデプロイ済み) 自動完了
Feature E: ポスター閲覧(コードデプロイ済み) 自動完了
Feature F: 座長パネル(コードデプロイ済み) 自動完了
025〜029のSQLを実行 手動
追加テーブルの存在確認(Table Editor) 手動