26.7.7 - システム更新
【学習120時間のドライバーが、7年超の現場知をsql.jsでブラウザ完結アプリに落とし込んだ話】
🛻はじめに
「クイズを1個作るのに7時間❓️」
正直、タイトルだけ見たらそう思う人もいると思います。選択肢を並べてポチポチ選ばせるだけなら、それこそ 1〜2時間 で終わる話で…
でも今回作ったのは、ただの選択式クイズではありません。 大型20t ・ 中型7t 、現役でハンドルを握っているトラックドライバーが、日々の運行の中で
「これ、事故る人のパターンだな」
と肌で感じてきたことを、【全50問・5段階 -
1問0〜2点配点】で言語化した運転性格診断テストになります。
この記事では、 「なぜクイズなのに設計に時間がかかったのか?」 を、実際のコードと一緒に振り返っていこうと思います。
(下にDPTのリンク貼っておきます。自身の運転と照らし合わせながら答えてみてください😊)
⚠️【おおよその所要時間 : 約20分】⚠️
※ システムの処理自体は非常に軽量でサクサク動作しますが、問題と解答のテキストをじっくり読み進める時間を考慮した目安です。ご自身のペースに合わせて進めてください。
🌐 デプロイおよびキャッシュ制御(Cache Busting)
本システムはサーバーレス(純粋なフロントエンドのみ)のSPA構成であるため、既存ユーザーのブラウザ側に古いアセット(app.js)が居座り、更新が反映されないリスク(キャッシュの同期ズレ)がありました。
この課題を根本から解決するため、HTML側で制御する
Cache Busting(キャッシュバスター) を導入しています。
キャッシュ制御の挙動(Before / After)
app.js(診断結果ロジックやUIテキスト)をアップデートした際は、index.html 内のスクリプト読み込みタグのクエリパラメータ(バージョン情報)を以下のように更新して出荷(デプロイ)します。
<!-- Before: パラメータなし(古いキャッシュが居座り、手動削除が必要だった状態) -->
<script src="app.js"></script>
<!-- After: 初回キャッシュバスターパラメータの追加(自動更新される状態) -->
<script src="app.js?v=20260707"></script>
💡 こだわりポイント:設問の「完全ランダム化」と安全なデータ処理
1. 現場のリアルを再現する出題ロジック
実際の道路と同じく 「予測できないイレギュラー」 を再現するため、リロードごとに 出題順(全50問)を完全シャッフル する仕様にしました。
回答の偏り(順序バイアス)を排除し、初見の緊張感で 「とっさの判断力」 を測る、事実ベースの診断を可能にしています。
2. Fisher-Yatesアルゴリズムによる出題のランダム化
ランダム化のコアには、Web開発で信頼性の高い Fisher-Yates アルゴリズムを採用し、 app.js 内に シャッフル関数 を自作して組み込みました。
高効率(計算量の最適化): データ量が増大してもブラウザに負荷をかけず 、一瞬でソートが完了します。
非破壊処理(安全運転な設計): スプレッド演算子(...)を使い、元のデータ(testData.js)のコピーを作ってからシャッフルしています。
「倉庫のマスターデータには一切傷をつけず、配送用のコピーだけを安全に捌く」 という、堅牢なリスク管理を意識したコード設計です。
3. リップルエフェクトによる直感的な操作感(UI/UXの最適化)
選択肢をタップした際、指の置かれた座標からじわっと波紋が広がる 「リップルエフェクト」 を CSSアニメーション とJavaScriptで自作しました。
ボタンに複数の処理(createRipple と handleAnswer)を綺麗にカプセル化して持たせることで、データの遷移を邪魔せず、ユーザーに 「今、確実に操作した」 という確かな手応え(視覚的フィードバック)を返します。とっさの判断を求められる診断だからこそ、誤操作を防ぎ、集中力を削がない動的な画面設計を意識しています。
⛑️開発のきっかけ
前日、「日々の日報を集めて解析したら面白いのでは?」という話をAIとしていました。
でも一晩置いて翌朝、ふと引っかかりました。
『日報は会社の資料であり、それを集めて解析するのは著作権・情報管理的にグレーなのでは❓️』
実は前作の 『puoppo』 というアプリでも、同じような『構造的な壁(ルートの閉塞)』にぶつかっていました。
当時は
特定サイトのスクレイピング ➔サイト側から尽くアクセスブロック❌️ ➔それならば RSS からの抽出へ迂回 ➔ 成功 ➔ 100件の概要データを Gemini に要約させて世論可視化
このように技術的な障害をアイデアと迂回ルートで回避する設計を行っていました。
今回もその「正面衝突のリスクを避け、最適な迂回ルートを見つける」という思考の癖が、著作権リスクを構造ごと回避する
『状況設定(フィクションのクイズ)』 という着地点に繋がりました。
グレーな道を通るくらいなら、最初からオープンでフリーな題材で同じ目的を達成した方がいい。
そこで思いついたのが 「運転性格診断クイズ」 という形でした。
日報という 個別の記録 の代わりに、 状況設定 というフィクションの形を取れば、著作権の問題を構造ごと回避できます。
📋️こちらに開発記録が残されてます👇️
🚶➡️これまでの経緯
1.AM9時に着想し骨組みが完成 README の更新(約1時間)
2.判定ロジックと評価テキストの修正 + メンテナンス向上のためファイルを HTML/CSS/JS のファイル分割を実施 + 診断結果ページにやり直し機能(リセットリンク)を追加(約1時間)
3.採点基準、クイズシステムの変更(約1時間)
4.休憩・昼食・送迎・家事 🍙🧺🚘️🧹
5.運転性格診断クイズ( DPQ )の完成ver.1(約1時間)
6.判定ロジック修正(約1時間)
7. LocalStorage のキー競合対策で独自のキーに設定(約1時間)
8.システム名の変更( DPQ → DPT に変更) quiz → test へ。それによるファイル内の quiz◯◯ 部分は一括置換(約1時間)
😮💨ここまでで 約7時間 掛かりました。
🗡️このシステムの「武器」
このテストの 核 は、問題そのものの中身にあります。
『50問』 はすべて、以下の 『5カテゴリ』 に沿って作成しました。
1. 公道での他車・歩行者との遭遇(10問)
2. 運行管理とスケジュール圧迫(10問)
3. 現場・荷役・バース着け(10問)
4. 異常気象と物理的限界(10問)
5. プロとしての精神力と他車への配慮(10問)
※なぜ5カテゴリになったのか❓️
それは、この5つのカテゴリは、
・外面的なリスクへの対応
(1. 他者、3. 現場、4. 物理環境)
・内面的なリソースの制御
(2. スケジュール・焦り、5. 精神力・プライド)
となり、それぞれが
・50問(各10問)という均等なウェイト(傾斜配点のベース)
で美しくバランスを保っています。
・どこか1つが欠けても「プロフェッショナルな運転性格診断」としては片手落ちになってしまう、私個人の経験則から逆算された ポートフォリオ設計 です。
一般的な安全運転チェックリストは 「正解」 と 「不正解」 の2値で終わることが多いですね。でも現場の実態はそう単純ではありません。
「法律上は正しいが現場では浮く判断」と「現場の空気は読めるがリスクは高い判断」の間には、グラデーションがあります。
それを表現するために、 選択肢1つ1つに5段階(2.0点〜0.0点)のスコアを割り振る設計 にしました。
🔧技術解説
ここからは実装の話。
全体の技術スタックはシンプルで、HTML / CSS / バニラJavaScript + sql.js(ブラウザ内で動くWebAssembly版SQLite)のみ。
外部フレームワークには依存していません。
1. 5段階傾斜配点のデータ構造
全50問の選択肢を、常に同じ5段階のスコア構造で統一しました。
{
category: "公道での他車・歩行者との遭遇",
question: "狭い道で対向車(乗用車)と遭遇...",
options: [
{ text: "自車が安全に退避できる...", score: 2.0 },
{ text: "ハザードを焚いて停車...", score: 1.5 },
{ text: "相手が動くか周囲の状況が変わるまで...", score: 1.0 },
{ text: "じりじりと車を前に詰めて待機...", score: 0.5 },
{ text: "強めにクラクションを鳴らす...", score: 0.0 }
]
}
※ なぜ5段階に揃えたかというと、後段の集計ロジックをシンプルに保つためです。
選択肢ごとにスコアの刻み幅がバラバラだと、後で「この問題だけ重み付けが違う」といった歪みが出て、合計点の意味が壊れる可能性が高いからです。
50問全てで対称な構造にすることで、 0点〜100点 という分かりやすいスケールに正規化できました。
2. 🖥️画面遷移(スタート/テスト/結果)の設計
最初はクイズ画面がいきなり表示される作りだったんですが、後から 「トップページ的な情報画面」 と 「過去の受診履歴」 を挟みたくなり、 3画面構成に変更しました。
function showScreen(screen) {
document.getElementById('start-screen').style.display = screen === 'start' ? 'block' : 'none';
document.getElementById('test-container').style.display = screen === 'test' ? 'block' : 'none';
document.getElementById('back-to-top-link').style.display = screen === 'test' ? 'block' : 'none';
document.getElementById('result-area').classList.toggle('show', screen === 'result');
}
最初は onclick の中で直接 style.display を書き換えていて、画面が増えるたびに条件分岐があちこちに散らばっていました。
「スタート画面を隠す処理」 「テスト画面を出す処理」 「戻るリンクを出し分ける処理」 が別々の場所に書かれていたため、一箇所直すと他の画面の表示がおかしくなる、という 積載ミス 的な事故を何度か起こしています。
実際、結果画面の display プロパティを重複して書いてしまい、 永遠に結果が表示されないバグ を踏んだこともありました。
そこで、あちこちに散らばっていた処理を showScreen(screen) という1つの関数 にまとめ、「今どの画面を表示するか」を1箇所で集中管理できるようにしました。
さらに、結果画面( result-area )に関しては、 JavaScript で直接 display を触るのをやめ、 classList.toggle('show') による クラスの付け替え制御 へとリファクタリングしています。これにより、 JavaScript と CSS の役割を分離させました。
画面を増やすたびに条件分岐を追加する手間はありますが、全体を一括管理して構造からバグを防ぐようにしたため、少なくとも 「どこかで表示が矛盾する」という事故は起きにくい構造 になったと思います。
3. 📊sql.js導入と「インメモリ→永続化」への方針転換
最初、受診履歴は sql.js のインメモリDBだけで管理していました。 ページを閉じれば消える 、軽量な設計のつもりでした。
// 最初期バージョン:メモリ上だけで完結
db = new SQL.Database();
しかし実際に使ってみると、 「ページをリロードしたら履歴が消えた」 という不便さにすぐ気づきました。puoppo で採用したデータの持ち方と同じように、こちらも 「使う人が困らない形」 を優先すべきだと判断し、DBのバイナリを LocalStorage へ退避・復元する方式に変更しました。
function saveScore(score) {
db.run(`INSERT INTO scores (score, taken_at) VALUES (?, ?);`, [score, takenAt]);
// DB全体をバイナリ化してLocalStorageに保存
const exportedData = db.export();
localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(exportedData)));
}
この方式には、INSERTのたびにデータベース全体をバイナリ化してLocalStorageに書き込むというコストがあります。
レコードが増え続ければ、いずれ書き込みのたびに重くなるはずです。
ただ、今回 DPTが管理 しているデータは 「スコア(数値)」 と 「受診日時(文字列)」 のみで、1レコードあたり数十バイト程度です。
仮に100回受診しても数キロバイトにしかならず、LocalStorageの容量制限(約5MB)や、ブラウザのメインスレッドへの負荷を考えても、体感できるほどの遅さにはならないと判断しました。
今後、1問ごとの詳細ログのような大きなデータを扱う場合は、IndexedDBへの移行を検討するつもりです。
4.命名の整理(quiz → test への統一)
開発初期は 「クイズ」 という言葉で全体を組んでいましたが、途中で 「テスト(診断テスト)」 という呼び方に統一する判断をしました。
変数名・関数名・CSSセレクタ・LocalStorageのキー名まで、影響範囲は想像より広かったです。
quizData → testData
initQuiz() → initTest()
.quiz-card → .test-card
LocalStorageのキー名も
truck_driver_quiz_db_v1
↓↓↓
truck_driver_test_db_v1
に変更しました。
地味な作業ですが、命名の一貫性は後々のメンテナンス性に直結します。
数字で見る成長(puoppoとの比較)
| 学習時間 | 開発時間 | 内容 | |
|---|---|---|---|
| puoppo | 42時間 | 10時間 | スクレイピング開始→主要サイトからブロック→RSSへ回避→RSS100件抽出→Gemini APIで要約→世論の傾向・不満を可視化するページとして表示 |
| DPT | 120時間 | 7時間 | 5段階傾斜配点+sql.js永続化+本格判定ロジック |
改めて工程を書き出してみると、puoppoも決して単純な作業ではありませんでした。
「スクレイピングがブロックされたので RSS に切り替えた」という一文だけでは伝わらないけど、実際は
「RSSから100件規模のデータを抽出し、Gemini APIに要約させ、世論の傾向や不満を可視化するページに落とし込む」
までを含めた一連のパイプラインになっています。
その上で今回の DPT と比べると、「7時間」でも中身の性質が違うことが見えてきます。
puoppoは「外部データを集めて要約・可視化する」パイプライン構築型、
DPT は 「自分の現場経験を50問の判定ロジックに落とし込み、ブラウザ内で完結させる」 設計・データ構造型。
時間の使い道の質が違います。
puoppo開発の時は 出荷(デプロイ・Git)コマンド すらもろくに覚えてなくて、知らない単語ばかりでかなり時間を浪費していました💧
しかしDPT開発の時はある程度全体の流れが分かるようになり、見たことある単語も増え、少しずつですが💻️コードに対する理解を深めることもできました。puoppoと違い、悩む事も減って割と早く進めました。
まとめ
- 現役トラックドライバーとしての現場経験を、著作権の問題を回避しながら形にする方法として 「診断テスト」 という着地点を選びました。
- 自分だけではなく 『誰か使う人がいてこそのシステム開発』 だと思います。
自分の作ったもので 誰かの役に立ちたい という目標があるから頑張れます。 - 5段階傾斜配点 というシンプルなルールを50問全部に一貫させることで、集計ロジックの複雑化を防ぎました。
- インメモリ設計から永続化への方針転換は、実際に使ってみて初めて気づいた課題でした。
- 命名の統一は地味ですが、後から見返した時のコストを減らす投資になります。
最初の問いに戻ります。
「クイズを1個作るのに7時間❓️」
その答えは、このテストが選択肢を並べただけのものではなかったからです。
- 現場の実感を 『50問・5カテゴリ』 に分解
- 法律だけでは測れない判断のグラデーションを5段階の配点で表現
- 『スタート・テスト・結果』 という画面設計を組む
- 履歴が消えないようデータの持ち方を作り直す
- 命名まで統一する。
ひとつひとつは地味な作業ですが、その積み重ねが 7時間 という数字の中身です。
最後までお読みいただきありがとうございました。もし DPT🚛 を実際に試してみて、『自分の運転を振り返る』きっかけになったら、 いいね👍️ を頂けると励みになります!
DPT(Driver-Personality-Test)
システム開発︰7時間
Qiita記事構成︰13時間









