Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .claude/rules/scratch-gui/e2e-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ expect(await isActiveByTestId(driver, 'ruby-toolbar-mode-dncl')).toBe(true);
| `classroom-assignment-name-input` | input | 課題名入力 |
| `classroom-create-submit` | button | クラス作成実行(クラス名・人数・課題名 3 つすべて必須) |
| `classroom-sidebar-item-{classroomId}` | li | サイドバーのクラス項目(クリックで選択・teacher-class-detail へ) |
| `classroom-post-assignment` | button | Google Classroom に共有(連携した先生以外は既定で disabled) |
| `classroom-post-assignment-blocked` | div | 共有できない理由(誰に依頼すればよいか)#1184 |
| `classroom-post-assignment-verify` | button | GC 教師であることを自分で確認して共有を解禁する #1184 |

**先生フロー自動化のコツ**:
- `classroom-create-submit` 後は `teacher-class-detail` には**自動遷移しない**。`teacher-dashboard` に戻ってから `classroom-sidebar-item-{id}` を明示的にクリックする必要がある
Expand Down
40 changes: 40 additions & 0 deletions docs/classroom/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ One Tap が表示できない環境(Windows や Playwright では毎回)で
| Method | Path | 説明 |
|--------|------|------|
| `GET` | `/classrooms/google-courses` | Google Classroom のコース一覧 |
| `GET` | `/classroom-groups/{groupId}/google-course-access` | **押す先生自身が**紐付け先コースの GC 教師かの判定(`{courseId, status, linkedByEmail, linkedByMe}`。status = `teacher` / `notTeacher` / `notLinked` / `unknown`) |
| `POST` | `/classrooms/google-import` | コースをインポートして課題を作成(`Classrooms` に書き込む) |
| `POST` | `/classrooms/{id}/google-assignment` | Google Classroom に課題を投稿 |

Expand All @@ -260,11 +261,49 @@ Google Classroom API の失敗は、Google が返した `error.status` / `error.
| 401 | 401 | `googleTokenExpired` | Google のアクセストークンが失効 |
| 403 | 403 | `googlePermissionDenied` | 権限(スコープ)不足 / そのコースの先生ではない |
| 404 | 404 | `googleCourseNotFound` | ひもづけたコースが削除された / そのコースにアクセスできない |
| 404(課題配信時・押した先生が連携者でない) | 404 | `googleNotCourseTeacher` | 押した先生がそのコースの GC 教師ではない(Google は未参加のコースを 404 で隠すため) |
| 404(課題配信時・押した先生が連携者) | 404 | `googleCourseNotFound` | 連携者はそのコースの教師なので、404 はコース消失/到達不能を意味する(「連携し直す」案内にする) |
| 400 | 400 | `googleInvalidRequest` | Google がリクエストを拒否(`FAILED_PRECONDITION` 等を含む) |
| その他 | 502 | `googleApiError` | レート超過・Google 側障害など |

レスポンス本文: `{error, reason, googleStatus?, googleMessage?}`。

#### 共有できる先生の判定(GC 教師かどうか)

課題配信は **押した先生自身の Google アクセストークン**で `courseWork` を作成するため、
Google はその先生がそのコースの教師であることを要求する。Smalruby のクラス共同担当は
Smalruby 上の権限であって GC の教師とは別物なので、両者が食い違うと Google 側で失敗する。

**既定はブロック**で、共有できるのは次のどちらかに当てはまる先生だけ:

1. **そのコースを連携した先生**(`googleClassroomLinkedBy`。既存データはクラス作成者
`teacherSub` にフォールバック)。サーバーが真偽値 `linkedByMe` を返し、クライアントは
メール文字列を突き合わせない(大小文字・別名でぶれるため)。
`GET /classroom-groups` も `googleClassroomLinkedByMe` を返す。
2. **GC 教師であることを実際に確認できた先生**。
`GET /classroom-groups/{groupId}/google-course-access` が `courses/{courseId}/teachers/me` を
caller のトークンで引き、200 なら `teacher`、404 なら `notTeacher`。401/403/5xx などは
`unknown`(判定不能)。

既定を「押させて失敗させる」から「ブロック」に反転したのは、**別の Google ワークスペース
(Workspace 契約の無い個人アカウント等)の共同担当は、運用でコースの教師になることが
できない**ため。この先生はそもそも Classroom のトークンを取得できず、判定は常に `unknown`
になる。判定不能を「押させる」側に倒すと、この先生だけが救われないまま残る。

正当な GC 共同教師の救済は**明示的なオプトイン**:
UI のブロック表示に「自分もこの Google Classroom のクラスの先生です(確認する)」を置き、
押されたときだけ同意(`requestClassroomAccessToken()`)→ 上記の判定を実行し、`teacher` なら
共有を解禁する。**自動では同意ポップアップを出さない**方針は維持する(クラスを開いた時点の
判定は、すでに取得済みのトークンがあるときだけ走る)。同意が得られない / `notTeacher` の
ときはエラーバナーを出さず、ブロック表示のまま「確認できませんでした」に差し替える。

連携した先生本人は判定を一切経ずに共有でき、404 になったときの文言も「依頼してください」では
なく `googleCourseNotFound`(連携し直す案内)になる — 依頼先は本人自身だからである。

メールが分からないとき(既存データ等)は「連携した先生」と一般名詞で表示する
(推測で名前を出さない)。**メールドメインの一致は判定条件にしない** — Workspace の設定次第で
外部ドメインの先生もコースの教師になれるため。

クライアントが取得するアクセストークンは `classroom.coursework.students` を含む 3 スコープを要求するが、
Google の同意画面はスコープごとにチェックボックスを出すため**一部だけ許可される**ことがある。
クライアントは `hasGrantedAllScopes` で検証し、不足していれば `prompt: 'consent'` で再要求する。
Expand Down Expand Up @@ -428,6 +467,7 @@ erDiagram
number schemaVersion "データモデル版 (v2 = クラス→課題モデル)"
string_array topics "課題のトピック一覧 (最大20件)"
string googleClassroomCourseId "GC コース紐づけ (クラス単位, 任意)"
map googleClassroomLinkedBy "GC を紐付けた先生 {sub, email} (任意)"
number studentCount "人数 (課題の座席数の真実, 任意)"
string_array coTeacherEmails "クラス単位の共同管理者 (任意)"
string createdAt "ISO8601"
Expand Down
2 changes: 2 additions & 0 deletions docs/classroom/source-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ smalruby3-editor/
│ │ │ ├── classroom-api.js ← API クライアント (リトライ付き)
│ │ │ ├── classroom-assignment-utils.js ← 課題エディタの純粋ヘルパー
│ │ │ ├── classroom-group-utils.js ← サイドバー組階層の純粋ヘルパー
│ │ │ ├── classroom-google-share.js ← GC 共有の既定ブロック判定 + 解除導線の文言(純粋)#1184
│ │ │ ├── classroom-evaluation/ ← sb3-analyzer(静的解析)+ evaluation-utils(総合評価/CSV)
│ │ │ ├── classroom-kick-request-storage.js ← 退室依頼の localStorage 永続化 (#692)
│ │ │ ├── google-classroom-auth.js ← Google Classroom OAuth
Expand Down Expand Up @@ -115,6 +116,7 @@ CDK スタック定義。以下のリソースを作成:
| `handleCreateGroup()` / `handleListGroups()` / `handleUpdateGroup()` | 組(グループ)の作成・一覧・更新/アーカイブ |
| `handleDuplicateClassroom()` | 課題の複製(課題コンテンツの S3 オブジェクトコピー含む) |
| `handleListGoogleCourses()` | Google Classroom コース一覧 |
| `handleGetGoogleCourseAccess()` | 押した先生が紐付け先コースの GC 教師か・連携者本人か(`linkedByMe`)の判定 |
| `handleImportGoogleClassroom()` | Google Classroom コースインポート |
| `handlePostAssignment()` | Google Classroom に課題投稿 |
| `verifyTeacherToken()` | Google ID Token 検証 |
Expand Down
2 changes: 2 additions & 0 deletions docs/classroom/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ Playwright MCP および Selenium integration tests で使用する `data-testid
| data-testid | 要素 | 説明 |
|------------|------|------|
| `classroom-post-assignment` | button | 課題配信ボタン (詳細画面) |
| `classroom-post-assignment-blocked` | div | 共有できない理由 (連携した先生以外) |
| `classroom-post-assignment-verify` | button | 「自分もこの Google Classroom のクラスの先生です(確認する)」 |
| `classroom-post-assignment-title` | input | タイトル |
| `classroom-post-assignment-description` | textarea | 説明 |
| `classroom-post-assignment-submit` | button | 配信実行 |
Expand Down
17 changes: 17 additions & 0 deletions docs/classroom/ui-ux.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ Google または Microsoft アカウントでサインインする画面。先
| クラス(学級)名 | 「技術 2026年度 / 2年1組」 | `classroom-detail-name` | 課題が属するクラスの表示ラベル(`formatClassLabel(group)`。v2 以前のデータでは `Classrooms.className`) |
| 課題名入力 | 課題名(編集可) | `classroom-detail-assignment-name` | blur で保存 |
| 課題配信ボタン | 「課題を配信」 | `classroom-post-assignment` | → teacher-post-assignment。**未配信のとき**表示 |
| 共有できない理由 | 「このクラスの Google Classroom は ◯◯ 先生が連携しています。…」 | `classroom-post-assignment-blocked` | 連携した先生以外に表示(配信ボタンは `disabled`) |
| GC 教師の確認ボタン | 「自分もこの Google Classroom のクラスの先生です(確認する)」 | `classroom-post-assignment-verify` | 上の理由表示の中。押すと同意を求めて判定し、GC 教師なら共有が解禁される |
| 課題確認リンク | 「課題を確認」 | `classroom-view-assignment` | 配信済みのとき表示(新しいタブ) |
| 参加コード表示 | 「参加コード: 3cexm5」 | `classroom-detail-join-code` | 大きなフォントで中央表示 |
| コード拡大ボタン | ⛶ アイコン(ツールチップ: 「全画面表示」) | `classroom-detail-expand-code` | 全画面コード表示 |
Expand All @@ -258,6 +260,21 @@ Google または Microsoft アカウントでサインインする画面。先

**課題配信ボタンの表示条件:** Google Classroom 連携はクラス(group)単位に移行したため、配信ボタン(`classroom-post-assignment`)は **クラスが GC 連携済み(`group.googleClassroomCourseId`)** であれば、その課題自体に courseId が無くても表示されます(課題の投稿先はクラスのコース)。課題単位の `googleClassroomCourseId`(v2 以前のフォールバック)が有る場合も表示されます。どちらの courseId も無い(非連携クラス)ときは表示されません。配信済み(課題に `googleClassroomAlternateLink` が保存済み)になると「Google Classroom で確認」リンク(`classroom-view-assignment`)に切り替わります。

**課題配信ボタンを押せる条件 (#1184):** 配信は**押した先生自身の Google アカウント**で行われるため、
紐付け先の Google Classroom コースの教師でなければ Google に拒否されます。**連携した先生以外は
既定で `disabled`** にし、`classroom-post-assignment-blocked` に誰に依頼すればよいかを表示します
(別ワークスペースの個人アカウントの共同担当は、そのコースの教師に**なれない**ため、押させてから
失敗を説明しても出口がありません)。

同じコースの GC 共同教師は、ブロック表示の中の「自分もこの Google Classroom のクラスの先生です
(確認する)」(`classroom-post-assignment-verify`)を 1 回押せば共有できます。押したときだけ
Google の同意を求め、`teacher` と確認できればボタンが有効になります(クラスを開いただけでは
同意ポップアップは出ません)。確認できなかったときはエラーバナーではなく、ブロック表示が
「…先生であることを確認できませんでした。共有は ◯◯ 先生に依頼してください。」に変わります。

**連携した先生本人**は確認導線を経ずに従来どおり共有でき、コースが削除・到達不能で失敗した
ときの文言も「連携し直してください」になります(自分に依頼させない)。

**座席グリッド:**

座席番号が格子状に並び(1行 10列)、各セルの背景色で状態を表します。
Expand Down
Loading
Loading