Skip to content
Merged
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
18 changes: 15 additions & 3 deletions .claude/rules/scratch-gui/e2e-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,21 @@ expect(await isActiveByTestId(driver, 'ruby-toolbar-mode-dncl')).toBe(true);

| data-testid | 要素 | 値の内容 |
|------------|------|----------|
| `classroom-menu-label` | span | メニューバーのクラス表示テキスト |
| `classroom-menu-class-name` | span | 課題名(またはクラス名) |
| `classroom-menu-seat-number` | span | 出席番号(0埋め2桁) |
| `classroom-menu-label` | span | メニューバーのクラス表示テキスト全体(参加中は「クラス:出席番号NN」、未参加は「クラス」) |
| `classroom-menu-seat-number` | span | 出席番号(0埋め2桁、参加中のみレンダリング) |
| `classroom-kicked-banner` | div | 先生に kick されたときに seat 画面で表示される警告バナー (#692) |
| `classroom-kicked-banner-dismiss` | button | バナーの × |
| `kick-request-confirm-dialog` | div | 「使用中の席」をタップすると現れる退室依頼ダイアログ (#692) |
| `kick-request-reason-input` | textarea | ひと言入力欄 (任意、最大 200 字) |
| `kick-request-submit` | button | 依頼を送る |
| `kick-request-cancel` | button | キャンセル |
| `kick-request-error` | div | 依頼送信エラー表示 |
| `kick-request-pending-banner` | div | 依頼後に表示される「先生に依頼中です…」バナー |
| `classroom-seat-kick-request-{N}` | span | 先生クラス詳細の座席に表示される赤いバッジ |
| `classroom-member-kick-request-panel` | div | メンバー詳細パネル内の依頼セクション |
| `classroom-kick-request-row-{requestId}` | div | 1 依頼の行 |
| `classroom-kick-request-approve-{requestId}` | button | 承認 = kick + リクエスト削除 |
| `classroom-kick-request-reject-{requestId}` | button | 却下 = リクエストのみ削除 |
| `classroom-list` | ul | クラス一覧 |
| `classroom-item-{id}` | li | クラス一覧の各項目 |
| `classroom-item-name-{id}` | span | クラス名 |
Expand Down
6 changes: 6 additions & 0 deletions .claude/rules/scratch-gui/smalruby-prettier-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ upstream (Scratch) ファイルは対象外。

**個別ファイル:**
- `src/containers/block-display-modal.jsx`
- `src/containers/classroom-classcode-utils.js`
- `src/containers/classroom-error-utils.js`
- `src/containers/classroom-modal.jsx`
- `src/containers/classroom-teacher-modal.jsx`
Expand Down Expand Up @@ -99,6 +100,7 @@ upstream (Scratch) ファイルは対象外。
- `src/lib/auto-correct.js`
- `src/lib/backpack-mesh-v1-migration.js`
- `src/lib/classroom-api.js`
- `src/lib/classroom-kick-request-storage.js`
- `src/lib/deck-setup.js`
- `src/lib/google-classroom-auth.js`
- `src/lib/block-utils.js`
Expand Down Expand Up @@ -217,6 +219,8 @@ upstream (Scratch) ファイルは対象外。
- `test/unit/components/student-join-form.test.js`
- `test/unit/containers/backpack.test.jsx`
- `test/unit/containers/cards.test.jsx`
- `test/unit/containers/classroom-classcode-utils.test.js`
- `test/unit/containers/classroom-error-utils.test.js`
- `test/unit/containers/connection-modal-connected-message.test.jsx`
- `test/unit/containers/connection-modal-smalrubot-s1.test.jsx`
- `test/unit/containers/connection-modal.test.jsx`
Expand All @@ -233,6 +237,8 @@ upstream (Scratch) ファイルは対象外。
- `test/unit/lib/block-display-initialization.test.js`
- `test/unit/lib/blockly-private-api.test.js`
- `test/unit/lib/blocks-gesture-recovery.test.js`
- `test/unit/lib/classroom-api.test.js`
- `test/unit/lib/classroom-kick-request-storage.test.js`
- `test/unit/lib/deck-setup.test.js`
- `test/unit/lib/blocks-screenshot.test.js`
- `test/unit/lib/calculate-popup-position.test.js`
Expand Down
7 changes: 7 additions & 0 deletions docs/classroom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ Smalruby Classroom は、日本の学校の授業で Smalruby を使うための
3. 「生徒」→ 参加コード入力 → 席番号を選択
4. プロジェクトを作成し「提出」ボタンで提出

## 強制退室通知と退室リクエスト (Issue #692)

- 先生がメンバーを Remove すると、対象の生徒は次にクラスモーダルを開いたタイミングで「先生によってクラスから退室させられました」と通知され、自動で出席番号選択画面に戻ります。同じクラスの席を即座に選び直して再参加できます。
- 生徒は seat 選択画面で「使用中」の席をタップして、先生に退室を依頼できます。先生はクラス詳細の座席バッジ + メンバー詳細パネルで「承認 (kick)」「却下」を選べます。
- 生徒のクラス参加状態は localStorage に永続化されており、ブラウザ再起動・新タブ・classcode URL の再オープンでも保持されます (旧仕様: sessionStorage)。
- 異なる classcode URL を開くと、古い席はサーバー側でも自動で leave されます。

## Mesh v2 ドメインの自動連動

クラスに参加中(生徒)または先生がクラス管理でクラスを選択中の間、Mesh v2 のドメインは**そのクラスの参加コードに自動で固定**される。これにより同じクラスの先生・生徒が自動で同じ Mesh ドメインに集まる。詳細は [`docs/mesh-v2/README.md`](../mesh-v2/README.md#クラス管理との連動) を参照。
Expand Down
38 changes: 35 additions & 3 deletions docs/classroom/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,11 @@ sequenceDiagram
| `GET` | `/classrooms/{id}` | クラス詳細 |
| `PATCH` | `/classrooms/{id}` | クラス更新 |
| `DELETE` | `/classrooms/{id}` | クラス削除 (アーカイブ) |
| `GET` | `/classrooms/{id}/members` | メンバー一覧 |
| `DELETE` | `/classrooms/{id}/members/{memberId}` | メンバー削除 |
| `GET` | `/classrooms/{id}/members` | メンバー一覧 (kicked 行は除外) |
| `DELETE` | `/classrooms/{id}/members/{memberId}` | メンバー削除 = ソフト kick (1h tombstone を残し、`kicked: true` をマーク)。verify-session が 410 reason='kicked' を返せるようにするための仕様。lookup の takenSeats も即座に空く |
| `GET` | `/classrooms/{id}/kick-requests` | 退室リクエスト一覧 (Issue #692) |
| `POST` | `/classrooms/{id}/kick-requests/{requestId}/approve` | 承認 = `handleDeleteMember` (= kick) + 同席への全リクエスト削除 |
| `DELETE` | `/classrooms/{id}/kick-requests/{requestId}` | 却下 = リクエストのみ削除、メンバーは残る |
| `GET` | `/classrooms/{id}/submissions` | 提出一覧 (ダウンロード URL 付き) |
| `PATCH` | `/classrooms/{id}/submissions/{subId}` | 提出の返却・コメント |

Expand All @@ -174,8 +177,9 @@ sequenceDiagram
| Method | Path | 認証 | 説明 |
|--------|------|------|------|
| `POST` | `/classrooms/lookup` | 不要 | 参加コードでクラス検索 |
| `POST` | `/classrooms/lookup/kick-request` | 不要 | 「使用中の席」を空けてもらう依頼を先生に送信 (Issue #692) |
| `POST` | `/classrooms/join` | 不要 | クラスに参加 (→ sessionToken 取得) |
| `POST` | `/classrooms/verify-session` | Session Token | セッション検証 + 提出状況取得 |
| `POST` | `/classrooms/verify-session` | Session Token | セッション検証 + 提出状況取得。kick された生徒には 410 + `{reason: 'kicked', joinCode, className, seatNumber}` を返す |
| `POST` | `/classrooms/{id}/submissions` | Session Token | 提出 (Presigned URL 取得) |
| `DELETE` | `/classrooms/{id}/members/me` | Session Token | 自主退出 |

Expand Down Expand Up @@ -230,6 +234,34 @@ erDiagram
**GSI:**
- `sessionToken-index` — セッショントークンでの認証

### ClassroomKickRequests テーブル (Issue #692)

```mermaid
erDiagram
ClassroomKickRequests {
string classroomId PK "UUID"
string requestId SK "UUID"
number seatNumber "1-50"
string reason "任意・最大200文字"
string sourceIpHash "abuse trace用 (sha256 16桁)"
string createdAt "ISO8601"
number ttl "Unix timestamp (1h)"
}
```

**GSI:**
- `classroomId-seatNumber-index` — 承認時に同席への全リクエストを batch-delete するため

短命 (TTL 1 時間) のレコード。生徒が「使用中の席を空けてください」と先生に依頼するときに作成される。同一席への複数依頼を許容する仕様 (規制なし)。

### Memberships の kick tombstone (Issue #692)

`handleDeleteMember` (= 教師 kick) は **行を物理削除しない**:
- `kicked: true, kickedAt, kickJoinCode, kickClassName, kickSeatNumber, ttl: now+1h` をセット
- これにより、kick された生徒の次回 `verify-session` 呼出で 410 reason='kicked' を返せる
- `listMembers` / `lookup takenSeats` は `FilterExpression: attribute_not_exists(kicked) OR kicked <> :true` で tombstone を除外
- `joinClassroom` の `ConditionExpression: attribute_not_exists(memberId) OR kicked = :true` で新生徒が kicked 行を上書き可能 (tombstone はその時点で消滅)

### ClassroomSubmissions テーブル

```mermaid
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 5 additions & 2 deletions docs/classroom/source-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ smalruby3-editor/
│ │ │ │ ├── classroom-modal.css
│ │ │ │ ├── class-code-display.jsx ← 参加コード表示(全画面対応)
│ │ │ │ ├── google-course-list.jsx ← GC コースタイルグリッド
│ │ │ │ ├── teacher-class-detail.jsx ← 先生クラス詳細
│ │ │ │ ├── kick-request-confirm-dialog.jsx ← 退室依頼ダイアログ (#692)
│ │ │ │ ├── teacher-class-detail.jsx ← 先生クラス詳細 (退室リクエストバッジ + 退室通知バナー)
│ │ │ │ ├── teacher-member-detail.jsx ← メンバー詳細パネル (承認/却下ボタン)
│ │ │ │ ├── teacher-create-form.jsx ← クラス作成フォーム
│ │ │ │ └── teacher-post-assignment.jsx ← 課題配信
│ │ │ ├── classroom-teacher-modal/ ← 先生用フルスクリーンモーダル
Expand All @@ -43,7 +45,8 @@ smalruby3-editor/
│ │ │ ├── classroom-error-utils.js ← エラーメッセージ変換
│ │ │ └── alert.jsx ← Alert コンテナ(参加しなおす対応)
│ │ ├── lib/
│ │ │ ├── classroom-api.js ← API クライアント (20メソッド, リトライ付き)
│ │ │ ├── classroom-api.js ← API クライアント (24メソッド, リトライ付き)
│ │ │ ├── classroom-kick-request-storage.js ← 退室依頼の localStorage 永続化 (#692)
│ │ │ ├── google-classroom-auth.js ← Google Classroom OAuth
│ │ │ └── alerts/index.jsx ← Alert 定義(classroomSessionExpired 追加)
│ │ ├── reducers/
Expand Down
25 changes: 22 additions & 3 deletions docs/classroom/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,28 @@ Playwright MCP および Selenium integration tests で使用する `data-testid
| data-testid | 要素 | 説明 |
|------------|------|------|
| `classroom-menu-button` | div | クラスボタン(コンテナ) |
| `classroom-menu-label` | span | メニューバーのクラス表示テキスト |
| `classroom-menu-class-name` | span | 課題名(またはクラス名) |
| `classroom-menu-seat-number` | span | 出席番号(0埋め2桁) |
| `classroom-menu-label` | span | メニューバーのクラス表示テキスト全体(参加中は「クラス:出席番号NN」、未参加時は「クラス」)|
| `classroom-menu-seat-number` | span | 出席番号(0埋め2桁、参加中のみレンダリングされる) |

### 強制退室通知 / 退室リクエスト (Issue #692)

| data-testid | 要素 | 説明 |
|------------|------|------|
| `classroom-kicked-banner` | div | 生徒の seat 画面に表示する「先生によって退室させられました」バナー |
| `classroom-kicked-banner-dismiss` | button | バナーの × |
| `kick-request-confirm-dialog` | div | 「使用中の席」をタップしたとき表示される退室依頼ダイアログ |
| `kick-request-reason-input` | textarea | 任意のひと言入力欄(200 字制限)|
| `kick-request-submit` | button | 依頼を送信 |
| `kick-request-cancel` | button | ダイアログを閉じる |
| `kick-request-error` | div | 依頼送信エラー表示 |
| `kick-request-pending-banner` | div | 「先生に依頼中です…」バナー(5 秒ごとに lookupClassroom を polling)|
| `kick-request-rejected-banner` | div | 「依頼は受理されませんでした」バナー (却下 / TTL 期限切れ検出時に pending と差し替え) |
| `kick-request-rejected-banner-dismiss` | button | × ボタン |
| `classroom-seat-kick-request-{seatNumber}` | span | 先生クラス詳細の座席グリッドに表示する赤いバッジ「!」|
| `classroom-member-kick-request-panel` | div | 先生メンバー詳細パネルに表示される依頼一覧 |
| `classroom-kick-request-row-{requestId}` | div | 1 リクエストの行 |
| `classroom-kick-request-approve-{requestId}` | button | 承認(kick + リクエスト削除)|
| `classroom-kick-request-reject-{requestId}` | button | 却下(リクエストのみ削除)|

### 汎用

Expand Down
18 changes: 11 additions & 7 deletions docs/classroom/ui-ux.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,14 @@ stateDiagram-v2
| クラスボタン | 「クラス」(未参加時) | `classroom-menu-button` |
| ラベル | — | `classroom-menu-label` |

生徒がクラスに参加中の場合、ボタンのテキストが課題名と出席番号に変わります:
生徒がクラスに参加中の場合、ボタンのテキストは固定の「クラス:出席番号NN」表記に変わります (NN は 0 埋め 2 桁):

| 要素 | テキスト/内容 | data-testid |
|------|-------------|-------------|
| 課題名(またはクラス名) | 例: 「第1回チャットアプリを作ろう」 | `classroom-menu-class-name` |
| 出席番号 | 例: 「/ 03」(0埋め2桁) | `classroom-menu-seat-number` |
| ラベル全体 | 例: 「クラス:出席番号03」 | `classroom-menu-label` |
| 出席番号 (内側 span) | 例: 「03」 | `classroom-menu-seat-number` |

未参加時はラベルが「クラス」のみになる。 課題名 / クラス名はメニューに表示せず、モーダル内でのみ確認する設計。

---

Expand Down Expand Up @@ -219,10 +221,12 @@ Google Classroom からインポートした場合は「インポート元: {コ

| セルの色 | 状態 | テキスト |
|---------|------|---------|
| グレー (`#e0e0e0`) | 空席 | 出席番号のみ (例: 「5」) |
| 青 (`#4285f4`) | 着席(未提出) | 出席番号(下線付き) |
| 緑 (`#34a853`) | 提出済み | 「✓」+ 出席番号 (例: 「✓5」) |
| オレンジ (`#ff9800`) | 返却済み | 出席番号 |
| 青 (`#4c97ff`) | 空席 | 出席番号のみ (例: 「5」) |
| グレー (`#d9d9d9`) | 着席(未提出) | 出席番号(下線付き) |
| 緑 (`#0fbd8c`) | 提出済み | 「✓」+ 出席番号 (例: 「✓5」) |
| オレンジ (`#ff8c1a`) | 返却済み | 出席番号 |

色は生徒側の出席番号選択画面と統一されている: 青 = 「空き / 選択可能 (生徒視点では選べる席、先生視点では未参加)」、灰色 = 「使用中」。

セルをクリックすると右カラムに詳細パネルが表示されます。

Expand Down
18 changes: 18 additions & 0 deletions docs/classroom/user-stories.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ sequenceDiagram

授業が終わったクラスは削除(アーカイブ)できます。生徒のセッションは無効化されます。

### 6. 退室リクエストに対応する (Issue #692)

生徒が「使用中の席を空けてほしい」と依頼を送ると、先生のクラス詳細画面では:
- 該当席に赤い「!」バッジが表示される
- その席をクリックすると、メンバー詳細パネルに依頼一覧(生徒からのひと言付き)と「承認(この生徒を退室させる)」「却下」の 2 ボタンが現れる

「承認」を押すと、その席の生徒が退室させられ (5. と同じ kick 処理) + すべての同席依頼が一括で消える。「却下」を押すと、対象の依頼だけが消え、座っている生徒はそのまま。

---

## 生徒ロール
Expand Down Expand Up @@ -201,6 +209,16 @@ URL に `classcode` パラメータがあると、モーダルが自動で開き

生徒はいつでもクラスから退出できます。退出するとセッションが無効化され、提出データは保持されます。

### 5. 間違った席の人に退室を依頼する (Issue #692)

クラスに参加しようとしたら自分の出席番号がすでに他の生徒に取られていた場合、生徒は「使用中」になっている席をタップして先生に退室を依頼できます。任意で先生へひと言(最大 200 文字)も添えられます。

依頼を送ると seat 選択画面に「先生に退室を依頼中です…」バナーが表示され、5 秒ごとに席が空いたかチェックされます。先生が承認すると席が空き、生徒は再びその席を選べるようになります。先生が却下した場合は、依頼は消えますが席は埋まったままです (TTL 1h で自動消滅)。

### 6. 先生に退室させられたときの挙動 (Issue #692)

先生がクラス管理画面で生徒を Remove したり、生徒が送った退室リクエストを承認したりすると、対象の生徒は次にクラスモーダルを開いたタイミングで「先生によってクラスから退室させられました」とバナーで通知され、自動的に出席番号選択画面に戻ります。元と同じクラスがプリロードされているので、ワンクリックで席を選び直して再参加できます。

---

## 典型的な授業の流れ
Expand Down
Loading
Loading