語源英単語コースの手触りを直した一日 - 音の鳴り方から、設定のユーザー別DB保存まで
語源英単語コースの挿絵洗い直しを完走した記録の翌日である。前日までは絵を作る側の話だった。今日は、できあがったものを自分でめくって出てきた注文を、片っ端から潰す一日になった。
朝いちばんは前日の後始末で、残骸のwebp 1,696枚を消し、1件だけ残っていた画像名の不一致をOpus 5のサブエージェントに委任して恒久修正させた。真因は「次回の再実行で再発する構造」ではなく、コンテンツ側のJSONが7/19のエクスポートのまま古かっただけだった。ここまでで前日の仕掛かりは尽き、そこから先が今日の本題になる。
単語だけ聞きたい日と、例文まで聞きたい日がある
構造ビューアーを矢印キーで送ると音声が鳴る。ただ、鳴り方が固定だった。語だけ確認したいときに例文まで読み上げられると邪魔だし、逆に例文を耳で流したいときもある。四つのパターン(無音/単語だけ/例文だけ/両方)を、その場で切り替えられるようにしたかった。
「自動再生 □単語 □例文」のチェックボックス2つに落とせば四通りが表現できる。既存構造を確認させたところ、ページ本体はビューアーのローダーに委譲されていて、音声は data-gogen-audio ボタンへのクリック委譲で鳴っていた。鳴らす経路はすでにあるので、ページ移動のフックからその経路を叩けばいい。
実装後、dev で四通りとも実機確認させた。ページを送ると単語(words/47)→ 例文(examples/47-1)の順に鳴る。この手の追加でこわいのは、チェックボックスにフォーカスが乗った瞬間に矢印キーが横取りされて、ページ送りが効かなくなることである。フォーカスをチェックボックスに置いたまま矢印を叩かせて、移動と単語のみ再生が両方生きていることを確かめてもらった。
380、400、500
同じ流れで、差分ビューアーの画像表示幅も気になった。小さい。400pxに広げさせたが、リロードしても実測はまだ380pxのままだった。
セレクタが当たっていなかった。画像は .theater-text の配下にいるつもりで書いたルールが、実際には .article-body(articleモード)の配下だったので空振りしていた。祖先チェーンを確認させて追加すると、実測が400pxに変わった。数字で診断していなければ「反映されたことにして」進んでいた場面である。
そして400pxを見て、これでも足りないと思った。500pxに変えてデプロイし直した。幅は議論しても仕方がなく、出して、見て、決める。
この日はデプロイを4回打っている。1回7〜10分かかるので、投げてから戻るまでの間に次の実装を進めることになる。完了通知が来たら本番の実物を見に行き、変更が乗っているかを確かめてから次に移る、という往復を繰り返した。
間違えるということは、イメージがついていない
英単語のトレーナーには、日本語を見て英語を答える形式がある。間違えると、正解と語源の解説が出る。ここに例文と情景画像も出してほしい、と注文を出した。ただし間違えたときだけ。
理由ははっきりしていた。間違えるということは、その語のイメージがついていないということだ。挿絵を1,576枚も作り直したのは、まさにそのイメージを絵で持たせるためだった。正解した語にまで絵を出せば画面が重くなるだけだが、外した語には絵が要る。誤答は「この語だけ絵が足りていない」という申告として使える。
word データの例文構造と既存のハイライトユーティリティを確認させてから、不正解時のフィードバックに例文と例文画像を差し込ませた。lintクリーン、テスト39件通過。dev でわざと誤答してみせてもらい、正解・語源・例文・情景画像が縦に並ぶところまで見た。
トレーナー側には、この直前に単語の自動再生も入れている。こちらは mountSuspended + happy-dom でコンポーネントテストまで書かせた。日→英の形式では出題時に音を鳴らさない(答えが音で漏れる)という条件があるので、鳴らない側も含めて固定した。
設定はブラウザの中にしかなかった
ここまでで、自動再生の可否、出題形式、出題数と、設定が一日で増えた。そして全部 localStorage に置いてあった。ログインの仕組みはとうに動いているのに、設定だけブラウザに閉じ込めたままだったわけだ。端末を変えれば全部リセットされるし、そもそも「この人がどう解いているか」がサーバー側から一切見えない。
コンテキストが残り少なかったので、実装は次に回して計画だけ立てさせることにした。ログイン済みユーザーの設定をDBに持ち、未ログインでも従来どおり使える形にする。
計画書はCodexにレビューさせたが、これが一度で通らなかった。往復は4回になった。
1回目は同期の順序。サーバーから初期値を取り終わる前にローカル値を書き戻すと設定が消えるし、PUTが失敗したまま黙ると巻き戻る。初期GETの完了まで書き戻し禁止、dirty/pending を持ってPUT成功まで再送、サーバー値を反映するのは「まだ触っていないキー」だけ、という状態遷移を計画に書き足した。
// サーバー値で上書きしていいのは、ユーザーがまだ触っていないキーだけ
const applyServer = (server, dirtyKeys) =>
Object.fromEntries(
Object.entries(server).filter(([key]) => !dirtyKeys.has(key)),
)
2回目は、リロードを挟むと未送信の変更が消える点(永続outboxにする)、localStorage をユーザー単位に分けないと別アカウントの設定が混ざる点(旧キーからの移行フォールバック付き)、そして非同期応答が順序を追い越したときのための世代ガード。3回目は同一キーへの並行PUTの順序逆転。ここまで潰して4回目で通過した。
引き継ぎプロンプトをターミナルに出し、計画書をコミットして、そのセッションは閉じた。
別ブラウザから入っても同じ設定が戻る
セッションを切り直して、その計画書をそのまま実装に入らせた。マイグレーション0008で設定テーブルを作り、GET/PUTのAPIを足し、設定の読み書きを共通のコンポーザブル経由に寄せる。未ログインは anon スコープでブラウザ内に持ち、ログインするとその値がDBへ持ち上がってマージされる。
実機確認でつまずいたのは、確かめる相手がいないことだった。ログインはふだんGoogle経由なので、実D1でのラウンドトリップを試す手段がない。dev モードのときだけメール登録フォームが生きていたので、テスト用アカウントを作って通した。設定画面をリロードすると、未ログイン時に触っていた値(出題数33、形式、出題順、自動再生)が丸ごとDBへPUTされ、ログイン後の初回マージが計画どおりに走った。
続けて、localStorage を空にした状態から同じアカウントで入り直させた。DBから出題数55が戻り、ユーザースコープのキャッシュに書き込まれる。「別ブラウザで同じ設定が復元される」がこれで実証になった。終わったらテストユーザーは消してログアウトさせた。
途中、ユニットテストが12ファイル落ちた。原因は今回の変更ではなく、Vitest 4 に上げたときに maxWorkers: 1 の指定が消えていたことだった。前から壊れていた設定を、今日たまたま踏んで直した形になる。最終的に2929件が通った。
コミットの前には学習ゲートが立つ。今回は既存概念に「ユーザー別の設定同期」に当たるものがなかったので、新規5概念で出題させて5問とも答えた。順序の話ばかり詰めた直後だったので、聞かれる側もそこに寄っていて、答えながら計画書の内容を一度なぞり直す形になった。デプロイ履歴CSVへの追記だけは機械的な記録なので、理由を書いてスキップした。
本番は、D1に0008を適用してからデプロイ。直後は設定APIだけ404を返した。デプロイの失敗を疑いかけたが、5秒待って叩き直したら401(未認証)が返ってきた。Cloudflare側が新しいルートを認識するまでの時間差だった。切り分けに走る前に、もう一度叩く。
「不正解」の3文字を消す
本番反映まで終わってから、トレーナーで間違えたときの表示が気になった。
× 不正解 正解:観点、遠近法
これが1行に詰まっている。まず「不正解」の文字が要らない。×が赤い時点で外したことは分かっているので、同じことを二度言われている。そして本当に見たいのは正解のほうなのに、それが行末で小さく潰れている。
×だけを1行目に置き、改行して、次の行に正解を黒の太字で大きく出すように直させた。足すのではなく削る修正である。dev でわざと誤答して描画を確認させたら、選択直後にQ2へ進んでしまって、スクロールで戻って見ることになった。×が単独行、次行に「正解 側面、局面」。これで読める。
学び
- 実測しないと「反映されたつもり」が通る。幅の指定が効かなかったのは、描画幅を数字で測らせたから見つかった
- 誤答は情報として使える。間違えた語にだけ絵と例文を出すのは、画面を増やさずに手当てを厚くする方向の変更だった
- 表示の改善は、要素を足すより減らすほうが早いことがある。「不正解」の3文字を消して改行しただけで読めるようになった
- 設定の同期は、書き込みの順序で壊れる。レビューで出てきた指摘4件は全部「いつ書くか」「どのキーを上書きするか」の話だった
- デプロイ直後の404は待つと消えることがある。新しいルートを本番が認識するまでの数秒を、実装の失敗と読み違えかけた
- テストの失敗が全部自分の変更のせいとは限らない。今回落ちた12ファイルは、並列実行の設定が消えたまま放置されていた分だった
一つ残った。地図側のビューアーの自動再生設定だけ、まだブラウザの中にいる。今日のDB同期から漏れているのを実装の途中で見つけたので、次の計画書に書き残してある。あわせて、間違えた語だけを集めて解き直すページの計画も立てた。間違える=イメージがついていない、という今日の仮説が本当かどうかは、その一覧が動き出してから確かめることになる。