ソフトウェア開発 / 動くものを、確かめながら育てる
「文化祭で、見たい企画が見つからない」を、小さなアプリで解く。
完成させるのは、企画名や教室名を入力すると、文化祭の企画を絞り込めるページです。AIには仕様の整理・コード案・テスト案を手伝ってもらいます。作るときにAIを使うことと、完成したアプリがAIで動くことは別。今回の検索は普通のJavaScriptで動き、AIへの通信や課金はありません。
サンプルの企画は全て架空です。完成コードはAI ATLASの教材用実装で、動作確認の対象です。依頼文は読者が試すための例であり、特定のサービスに同じ依頼を送った実行記録ではありません。
完成例:5件から、理科室の2件を見つける
- 「理科室」と入力すると2件。
- 「ai」または全角の「AI」と入力すると、AIイラスト展の1件。
- 「水族館」と入力すると0件と案内文。
- 「すべて表示」を押すと、開始時刻順に5件へ戻ります。
データや検索結果は保存しません。リロードすると、初期表示に戻ります。この教材は単語を文字列として探す検索で、意味を理解して近い企画を推薦するAI検索ではありません。
初心者:コードを書く前に、完成の条件を決める
必要なもの:ブラウザーと、文字をそのまま保存できるテキストエディター。AIとの制作も試すなら、利用できる対話AIまたはコード支援ツール。まずは上の完成例だけを試しても大丈夫です。コードの実行に外部サービスのアカウントは不要です。
「いい感じに」では、何を確かめるか決まらない
最初から「文化祭の完璧なアプリ」を頼むと、ログイン、地図、通知などが増え、どこで壊れたのか分かりにくくなります。今回は、検索だけを完成させます。
| 仕様 | 自分で確かめること |
|---|---|
| 企画名と教室名で部分一致検索 | 「ラリー」で謎解きラリーが出る。「理科室」で2件出る。 |
| 空欄は全件表示 | 最初に5件出る。検索語を消すと5件に戻る。 |
| 開始時刻の早い順 | 09:30の謎解きラリーが最初になる。 |
| 0件の状態も説明する | 「水族館」で「見つかりませんでした」と表示される。 |
| スマートフォンとキーボードで使える | 細い画面でも横にはみ出さず、Tabキーで検索欄とボタンへ移動できる。 |
コピーして試せる、制作の依頼文
文化祭の企画を検索する、小さなWebページを作ります。 HTML・CSS・JavaScriptだけを使い、外部ライブラリやAPIは使わないでください。 目的:初めて来た人が、企画名と教室名から行き先を探せること。 データ: 謎解きラリー / 2年A組 / 09:30 星空プラネタリウム / 理科室 / 10:00 AIイラスト展 / 美術室 / 10:30 ミニライブ / 体育館 / 11:00 紙飛行機研究所 / 理科室 / 13:00 必要な動作: - 企画名と教室名の部分一致検索。 - 検索欄が空なら全件を開始時刻順に表示。 - 英字の大文字・小文字と、全角・半角の違いを吸収。 - 0件なら「見つかりませんでした」と説明。 - 入力欄に見えるラベルを付け、キーボードで操作できる。 - 入力や履歴を保存しない。外部通信しない。ログイン機能は付けない。 - 文字の表示にtextContentを使い、入力をHTMLとして解釈しない。 まず仕様と確認項目を短く整理し、その後にコードを示してください。 実行していないテストを「合格」と書かず、実行方法と期待結果を示してください。
完成コードを手元で開く
- コード一式のZIPをダウンロードして展開します。ZIPを展開する操作は端末によって異なります。
- 同じフォルダーにあるdemo.htmlをブラウザーで開きます。demo.jsとsearch-core.jsを移動しないでください。
- 5件が表示されたら、「理科室」「ai」「水族館」を順に試します。
- 変更したい場合は、search-core.jsのeventsというデータ部分を編集し、保存してブラウザーを再読み込みします。
- 新しい企画を足したら、空欄の件数と検索結果も変わります。期待する結果を書き直してから、再び確かめます。
HTMLは見出しや入力欄などの構造、CSSは見た目、JavaScriptは検索と表示の動作を担当します。分けておくと、色の変更で検索の仕組みまで直す必要がありません。
コアになる関数を読む
function normalize(text) {
return String(text).normalize('NFKC')
.toLocaleLowerCase('ja-JP').trim();
}
function searchEvents(query, items = events) {
const q = normalize(query);
return items
.filter(event =>
normalize(event.title + ' ' + event.room).includes(q))
.slice()
.sort((a, b) =>
a.start.localeCompare(b.start) || a.id.localeCompare(b.id));
}
filterは条件に合う企画だけを選びます。includesは文字列の中に検索語が含まれるかを調べます。時刻を必ず「09:30」のような同じ長さの24時間表記にしているので、この例では文字として並べても時刻順になります。「9:30」と「10:00」を混ぜたり、日をまたぐ予定を扱ったりするなら、日時の扱いを設計し直します。
中級:「動いた」と「使える」の間を埋める
前提:完成例を動かせること。次は、検索が失敗する条件を一つずつ切り分け、修正の理由を説明します。
失敗1:「ai」で見つからない
修正前の教材例
event.title.includes(query)「AIイラスト展」と「ai」では大文字・小文字が違います。検索語だけを小文字にしても、比較するデータがそのままでは一致しません。
修正後
normalize(event.title + ' ' + event.room)
.includes(normalize(query))比べる両側に同じ処理をします。さらに教室名を加えることで、「理科室」でも探せます。
NFKCは、互換性を考慮したUnicodeの正規化です。たとえば全角英字と半角英字を比較しやすくします。ただし、見た目の区別を変える場合もあります。検索用の文字列だけを変え、画面に見せる企画名は元のデータを使うのが、この例での判断です。漢字と読み仮名、同義語まで同じにする機能ではありません。MDN:normalize()
失敗2:画面が空白になり、壊れたように見える
0件は異常ではなく、普通に起こる検索結果です。件数と「短い言葉や別の場所で探す」という次の操作を表示します。「検索中」「0件」「まだ入力していない」を、利用者が区別できるようにします。今回の検索は即時処理なので、見せるだけの読み込みアニメーションは付けていません。
失敗3:検索できるのに、入力欄が何なのか伝わらない
薄い例文だけを入力欄に置くと、入力した瞬間に用途の説明が消えます。見えるlabelを入力欄に結び付け、結果の件数はrole="status"で知らせます。読み上げ対応は属性を置くだけで完了ではなく、実際の支援技術でも確かめる必要があります。W3C:入力欄のラベル / W3C:状態メッセージ
変更を小さく頼む
現在の不具合は「aiではAIイラスト展が見つからない」です。 再現手順:初期表示から検索欄にaiを入力する。 期待:AIイラスト展が1件表示される。 実際:0件になる。 デザインや他の機能は変えず、原因と最小限の修正を提案してください。 検索語とデータの両側に同じ正規化をしてください。 「AI」「ai」「AI」「 ai 」が同じ結果になるテストと、 「水族館」は0件のままになるテストも示してください。 修正した関数の全文と、まだ確かめていない点を分けてください。
この伝え方なら、「検索が変です」より原因をしぼれます。AIが大きな書き換えを提案したら、必要な変更だけかを確認します。前のファイルを別名で残すかGitで保存してから試すと、戻せます。
上級:検索の正しさと、画面の使いやすさを分けて検証する
前提:関数・配列・イベントの意味が分かること。自動テストまで動かすなら、Node.jsを利用できる環境が必要です。完成アプリの閲覧には不要です。
1. データを入れて結果を返す関数を独立させる
searchEventsは、入力と企画データから配列を返す処理です。ボタン、色、画面の要素に依存させないため、ブラウザーを開かなくても結果を検査できます。画面側のrenderは、返った結果を表示するだけにします。
ただし、関数のテストが全部通っても、入力欄のイベントがつながっていなければ画面は動きません。関数のテスト・ブラウザー操作・人が読む確認は、互いの代わりになりません。
2. 正常な例だけでなく、境界を試す
| 入力・条件 | 期待 | 何を守るか |
|---|---|---|
| 空文字・空白だけ | 全5件 | 検索前とリセット後の動作 |
| 理科室 | e1とe5 | 教室検索と複数結果 |
| ai・AI | e3だけ | 表記の違いを吸収 |
| 水族館 | 空配列 | 0件を異常扱いしない |
| 同時刻の2件 | ID順 | 並び順を曖昧にしない |
| 企画データが0件 | 空配列 | データがない状態で落ちない |
| 検索を繰り返す | 元データの並びは変わらない | 別の処理へ副作用を出さない |
node search-core.test.cjs
付属テストはNode.js v24.19.0で実行し、12件中12件の合格を確認しました。これは付属の固定データと列挙した条件に対する結果で、あらゆる入力・端末で安全なことの証明ではありません。実行したテストの結果
3. 表示する文字と、実行するコードを混ぜない
データをinnerHTMLに直接つなぐと、ただの企画名のつもりで渡した文字列がHTMLとして解釈される可能性があります。この例は、表示用の要素をつくり、タイトルや教室名をtextContentに入れます。MDN:textContent
const title = document.createElement('span');
title.textContent = event.title;
li.append(title);
これで全てのセキュリティ問題が消えるわけではありません。外部リンクを生成するならURLの扱い、サーバーに保存するなら権限や入力検証、共有するなら個人情報と公開範囲も必要です。今回はその機能を付けず、固定の架空データだけに範囲をしぼっています。
4. AIを本番動作に入れるべきか判断する
| 方式 | 向いている目的 | 増える負担 |
|---|---|---|
| 今回の部分一致検索 | 名称・教室が分かる5〜数十件の案内 | 読み仮名や同義語は、別の検索用データを足す必要がある。 |
| 検索用の別名を手動で追加 | 「音楽」で「ミニライブ」を見つけたい | 別名の管理は増えるが、結果の理由は明確にできる。 |
| 意味を使う検索や生成AI | 「静かに見られるもの」のような曖昧な相談 | 誤った推薦、通信、費用、入力の取り扱い、評価が必要。5件なら導入が割に合わないこともある。 |
APIを加える場合、秘密のAPIキーをブラウザーのHTMLやJavaScriptへ書いて公開してはいけません。通常はサーバー側で認証情報を管理し、利用制限やエラー時の動作を設計します。この教材に「キーを貼るだけ」の拡張を置かないのは、その重要な境界を飛ばさないためです。OpenAI:APIキーの安全な扱い
5. AIの修正を、テストと差分で審査する
「コードをレビューした」というAIの文章は、実際にテストが走った記録ではありません。変更前後の差分を見て、どの条件を直したかを確認し、自分の環境でテストします。GitHubも、生成されたコードを確認・テストする責任を利用者が持つと案内しています。GitHub:Copilotの責任ある利用
追加課題は、「場所の絞り込み」「検索用の読み仮名」「企画データの外部JSON化」のどれか一つにします。変更の前に期待結果を一行書き、変更後に既存12件のテストも再実行します。これが、機能を増やしながら前の動作を壊さないための回帰テストです。
公開前の、現実的な合格ライン
- 固定のサンプルで、全件・1件・複数件・0件が意図どおりになる。
- Tabキーだけで入力欄と「すべて表示」へ到達し、フォーカスの位置が見える。
- 幅375px程度の画面とデスクトップで、内容を横へスクロールしなくても読める。
- 日本語入力の変換中に表示が不自然にちらつかないか、実機でも試す。
- 企画名を変えても、文字が勝手にHTMLとして解釈されない。
- 実在の行事へ置き換えるなら、日時・教室・公開許可を主催者が確認する。
この教材では関数の自動テスト12件の合格を確認しています。ブラウザーでの表示・操作、スマートフォン、支援技術、日本語IMEによる実機試験の結果は、この実行記録には含みません。
公式資料と教材の出所
確認日:2026年10月22日(日本時間)。コード、架空の企画、図解、テストはAI ATLASの教材用制作物です。特定のAI製品の性能比較は行っていません。
- MDN:String.prototype.normalize()。Unicode正規化と互換性の注意。
- MDN:Node.textContent。文字列の表示とinnerHTMLとの違い。
- W3C WAI:Labeling Controls。フォームの見えるラベル。
- W3C WAI:Status Messages。件数など状態変化の伝達。
- GitHub:Responsible use of inline suggestions。生成コードの確認・テスト。