見出し画像

ブラウザで簡単に記録:CBT試験対策のためのマークシート作成ツール - Coded by ChatGPT

近年、CBT(Computer Based Testing)形式の試験が広く普及しつつあります。従来の紙とペンを使った試験に代わり、コンピュータを利用して試験を実施するこの形式は、効率的でありながら公平性も高いことから、多くの教育機関や試験団体で採用されています。例えば「基本情報処理試験」やOracle主催の「Java bronze/silver」等もこの形式が採用されています。

この変化に伴い、CBT形式の試験対策の方法も進化を遂げています。そこで、CBT形式の試験勉強をより効率的に、そして効果的に行うための新しいアプローチを模索している中で生まれたのが、今回ご紹介する、ブラウザ上で使えるマークシート作成・記録ツールです。

このツールは、ブラウザを利用して簡単に試験勉強の記録を行えるもので、特にCBT形式の試験対策に適しています。イメージとしては、参考書等を片手に回答した選択を記録しておき、最後に採点して見直しを行う補助的なものです。
作成に際しては、9割以上ChatGPT-4oを活用しています。時間にして、おおよそ30分で完成しました。(ベースは10分、追加で実装を入れるのに20分。4oだとレスポンス早くて良いですね。)

この記事では、このツールの作成過程と使用方法を簡単にご紹介します。また、コードは全量公開しますので、ご自由にお使いいただければと思います。(改変、再配布等許可なくOKです。※ライセンスの規定はGitHubの方で細かく指定する可能性はありますが、一旦常識の範囲内でご自由にどうぞ、というご認識でお願いします。)
もの自体はシンプルなので、コード全量をAIに読み込ませて、カスタムされることもオススメします。


マークシート作成ツール

以下に自ドメイン配下でも公開しています。
ローカル環境でも動作するので、htmlファイルをダウンロードしていただき、index.htmlから読み込んでもらえればと思います。なお、入力内容は一切外部に送信されません。(この辺不安なときは、コード全量をChatGPT等のAIに連携して、聞いてみるのも手です。)

index.html

このページでは、タイトルや問題数、合格ラインを入力します。
進め方は個人様々ですが、章ごとに区切りをつけることを想定した例となります。合格ラインは試験によって異なるため、事前に確認することをお勧めします。本ツールでは、未入力の場合は次の画面で採点時、無条件に「合格」判定となります。

必須項目は問題数だけです。

※ここでの「送信」は、marksheet.htmlにパラメータで渡しているだけです。

marksheet.html?…以降に渡しています。
余談ですが、サーバ(DB等)に保存(更新)するものではないので、この辺のバリデーションチェック等は厳密にしておりません。(Count=10aに改ざんした場合、表示が変になるだけ。)

marksheet.html

「送信」ボタンを押すと、こちらのHTMLファイルが呼び出され、中身が作成されます。各項目には回答の記録と答え合わせ用のエリアがあり、自由入力欄も設けています。

AからGまでの選択肢がありますが、足りない場合はHTMLファイルを修正する必要があります。自由入力に入れても良いですが、コード全量をChatGPTに連携して、指示を出すことも可能です。

AからGまでの選択肢ですが、足りない場合HTMLファイルを修正する必要があります。
自由入力に入れても良いですが、コード全量をChatGPTに連携して、指示を出しても良いでしょう。

下部に「正解数をカウント」ボタンがあるので、これを押すことで判定処理が流れます。なお、事前に自分で答えあわせを行います。

合格としていますが、割合で出しているだけなのでご注意ください。

入力が不足していた場合、採点を促すようにエラーが表示されます。

問題番号にジャンプ機能は、ChatGPT-4oのアイデアです。
縦に長いと、ピンポイントで戻るのが手間なので、かなり良いと思います。

PCであれば、ブラウザから印刷(PDF化)できるので、それで記録を残してもよいでしょう。(本ツールでは、ブラウザに記録を残すような仕組みにはしていません。)

ChatGPT-4oとのやりとり

以下に全量を連携します。
コーディングにかかることだけではなく、この記事の書き方の相談も含んでいます。

一番最初の要件を抜粋して、この記事にも掲載します。

下記要件もとに作成をお願いします。不明点は適宜質問してください。

マークシート作成の要件定義

目的: HTML技術を用いてマークシートを作成する。 概要: ユーザーが問題数を入力すると、指定された問題数分のマークシート項目が生成されます。各項目は選択肢(A, B, C, D, E, F, G)、正解、不正解、自由入力のフィールドを含みます。 詳細要件:初期画面:
ユーザーに問題数を入力させるテキストボックスを表示。
テキストボックスの下に「問題数を入力してください」とのメッセージを表示。
送信ボタンを配置し、ユーザーが問題数を入力して送信できるようにする。
問題数入力後の画面:
ユーザーが入力した問題数に基づき、以下のフォーマットでマークシート項目を生成する:
各項目に番号を付ける (例: No.1, No.2, ... No.20)。
各番号に対し、AからGまでの選択肢を配置。
各番号に「正解」および「不正解」チェックボックスを配置。
各番号に自由入力フィールドを配置。
画面構成:
問題数入力画面とマークシート生成画面の2つの画面を構成する。
CSSを使用して、見やすく整ったデザインを提供する。 追加情報:
必要に応じて、CSSフレームワーク(例: Bootstrap)を使用してスタイルを調整。
JavaScriptを用いて、問題数入力後の動的なコンテンツ生成を行う。
フォームの入力検証を行い、ユーザーが不正な値を入力した場合のエラーメッセージを表示する。

プロンプト

あとはやりとりを継続して、ブラッシュアップしています。

断念したこと

概ねやりたいことは実装できましたが、PDF化を自前で実装しようと思いましたが、予想以上に手こずったので断念しました。
この辺フォントのエンコードが必要等、自分が指示を見逃しているのでやりとりが無駄に長引いでいます。いずれにしても、手間がかかりすぎるのと、ブラウザの印刷で代用できるので実装を見送りました。

良かったこと

やはり自然な日本語で実装が進められるのは良いですね。新しい時代のコーディングです。これからはこういった流れが主流になるのだろう、というのを強く感じます。もちろん、システムやソフトウェアの規模によって使える範囲は限られますが、30分のやりとりでここまでできれば御の字です。

また、ある程度完成したタイミングで、追加で何か実装できるアイデアはあるかと聞いて、進めることができるのも良いですね。

余談:生成AI時代の資格勉強

この時代にJava等の資格勉強をする必要があるのでしょうか。個人的には積極的にやるべきだと思います。生成AI任せでは良いものは作れません。使う側の知識も同じくらい問われます。資格を持つことは、スキルの客観的評価に有用です。実務経験が評価される場合がほとんどですが、資格勉強を通して自分の弱みが具体的に見えてくる点で有用です。

また、勉強中にわからないことを生成AIに聞けるのも大きな利点です。何度同じことを聞いてもキレられません。これがモチベーションに直結し、非常に良いです。もちろん、回答内容が正しいかどうかの確認は必要ですが、2024年6月時点では、大きな見当違いな回答は少なくなっています。人間の教師や講師が間違うレベルと同等なので、感覚的に変だと感じない限りは気にしなくて良いと思います。

余談:紙のマークシート

マークシート式のルーズリーフも売っているので、これを活用するのも手だと思います。※Amazonアソシエイトリンクです。

マークシート試験用ですが、大差ないのでこれも良いですね。実際に使っています。
使う際には、解答欄をA,B等に読み替えています。

コード全量

上記までに連携している、htmlファイルをダウンロードすれば良いだけですが、一応コード自体も貼り付けます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>マークシート生成</title>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
    <style>
        body {
            padding: 20px;
        }
        .container {
            max-width: 600px;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1 class="text-center">マークシート生成</h1>
        <div class="form-group">
            <label for="sheetTitle">タイトルを入力してください</label>
            <input type="text" class="form-control" id="sheetTitle" placeholder="例: 数学テスト">
        </div>
        <div class="form-group">
            <label for="questionCount">問題数を入力してください</label>
            <input type="number" class="form-control" id="questionCount" min="1" placeholder="例: 20">
        </div>
        <div class="form-group">
            <label for="passingScore">合格ラインを入力してください(%)</label>
            <input type="number" class="form-control" id="passingScore" min="0" max="100" placeholder="例: 60" oninput="updatePassingScore()">
            <div id="passingScoreDisplay" class="mt-2"></div>
        </div>
        <button class="btn btn-primary btn-block" onclick="generateMarkSheet()">送信</button>
        <div id="errorMessage" class="text-danger mt-2"></div>
    </div>

    <script>
        function updatePassingScore() {
            const passingScore = document.getElementById('passingScore').value;
            const passingScoreDisplay = document.getElementById('passingScoreDisplay');
            passingScoreDisplay.innerText = `合格ライン:${passingScore}%`;
        }

        function generateMarkSheet() {
            const sheetTitle = document.getElementById('sheetTitle').value;
            const questionCount = document.getElementById('questionCount').value;
            const passingScore = document.getElementById('passingScore').value;
            const errorMessage = document.getElementById('errorMessage');
            errorMessage.innerHTML = '';

            if (questionCount < 1) {
                errorMessage.innerHTML = '問題数は1以上の数を入力してください。';
                return;
            }

            if (passingScore < 0 || passingScore > 100) {
                errorMessage.innerHTML = '合格ラインは0から100の間で入力してください。';
                return;
            }

            const startTime = Date.now();
            const queryParams = new URLSearchParams({ sheetTitle, questionCount, passingScore, startTime });
            window.location.href = `marksheet.html?${queryParams.toString()}`;
        }
    </script>
</body>
</html>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>マークシート</title>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
    <style>
        body {
            padding: 20px;
        }
        .container {
            max-width: 800px;
            margin: 0 auto;
        }
        .question-item {
            margin-bottom: 20px;
        }
        .options {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
        }
        @media (max-width: 576px) {
            .container {
                padding: 10px;
            }
            .form-control {
                width: 100%;
            }
            .btn {
                width: 100%;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="d-flex justify-content-between align-items-center mb-4">
            <div id="currentDate"></div>
            <div class="form-group mb-0">
                <label for="elapsedTime">経過時間 (分):</label>
                <input type="text" class="form-control d-inline-block w-auto ml-2" id="elapsedTime" readonly>
            </div>
        </div>
        <h1 id="sheetTitle" class="text-center">マークシート</h1>
        <div id="markSheet"></div>
        <div class="form-group mt-4">
            <label for="jumpToQuestion">問題番号にジャンプ</label>
            <input type="number" class="form-control d-inline-block w-auto ml-2" id="jumpToQuestion" min="1" placeholder="例: 5">
            <button class="btn btn-secondary ml-2" onclick="jumpToQuestion()">ジャンプ</button>
        </div>
        <div class="d-flex justify-content-between mt-4">
            <button class="btn btn-primary" onclick="countCorrectAnswers()">正解数をカウント</button>
        </div>
        <!-- <div id="realTimeResult" class="mt-3"></div> -->
        <div id="result" class="mt-3 text-danger"></div>
    </div>

    <script>
        let timerInterval;

        document.addEventListener('DOMContentLoaded', function() {
            const urlParams = new URLSearchParams(window.location.search);
            const sheetTitle = urlParams.get('sheetTitle');
            const questionCount = urlParams.get('questionCount');
            const passingScore = urlParams.get('passingScore');
            const startTime = parseInt(urlParams.get('startTime'), 10);
            const markSheet = document.getElementById('markSheet');
            const currentDate = document.getElementById('currentDate');
            const titleElement = document.getElementById('sheetTitle');
            const elapsedTimeElement = document.getElementById('elapsedTime');

            // タイトルを設定
            titleElement.innerText = sheetTitle || 'マークシート';

            // 日付を自動表示
            const today = new Date();
            currentDate.innerText = `日付: ${today.getFullYear()}年${today.getMonth() + 1}月${today.getDate()}日`;

            // 経過時間を計算して表示
            timerInterval = setInterval(() => {
                const now = Date.now();
                const elapsedTime = Math.floor((now - startTime) / 60000); // 分単位
                elapsedTimeElement.value = `${elapsedTime} 分`;
            }, 1000);

            for (let i = 1; i <= questionCount; i++) {
                const questionItem = document.createElement('div');
                questionItem.className = 'question-item';
                questionItem.innerHTML = `
                    <h5>No.${i}</h5>
                    <div class="options">
                        ${['A', 'B', 'C', 'D', 'E', 'F', 'G'].map(option => `
                            <label>
                                <input type="checkbox" name="q${i}" value="${option}"> ${option}
                            </label>
                        `).join('')}
                    </div>
                    <div>
                        <label>
                            <input type="radio" name="q${i}Answer" value="correct" onchange="updateRealTimeResult()"> 正解
                        </label>
                        <label>
                            <input type="radio" name="q${i}Answer" value="incorrect" onchange="updateRealTimeResult()"> 不正解
                        </label>
                    </div>
                    <div>
                        <label>自由入力:</label>
                        <input type="text" class="form-control" name="q${i}FreeText">
                    </div>
                `;
                markSheet.appendChild(questionItem);
            }

            // リアルタイム正解数を更新
            updateRealTimeResult();
        });

        function jumpToQuestion() {
            const questionNumber = document.getElementById('jumpToQuestion').value;
            const questionItem = document.querySelector(`.question-item:nth-of-type(${questionNumber})`);
            if (questionItem) {
                questionItem.scrollIntoView({ behavior: 'smooth' });
            }
        }

        function updateRealTimeResult() {
            const questionCount = new URLSearchParams(window.location.search).get('questionCount');
            let correctCount = 0;

            for (let i = 1; i <= questionCount; i++) {
                if (document.querySelector(`input[name="q${i}Answer"][value="correct"]`).checked) {
                    correctCount++;
                }
            }

            const realTimeResult = document.getElementById('realTimeResult');
            const correctPercentage = (correctCount / questionCount) * 100;
            realTimeResult.innerText = `リアルタイム正解数: ${correctCount} / ${questionCount} (${correctPercentage.toFixed(2)}%)`;
        }

        function countCorrectAnswers() {
            const urlParams = new URLSearchParams(window.location.search);
            const questionCount = urlParams.get('questionCount');
            const passingScore = urlParams.get('passingScore');
            let correctCount = 0;
            let unansweredQuestions = [];

            for (let i = 1; i <= questionCount; i++) {
                const correctChecked = document.querySelector(`input[name="q${i}Answer"][value="correct"]`).checked;
                const incorrectChecked = document.querySelector(`input[name="q${i}Answer"][value="incorrect"]`).checked;
                
                if (!correctChecked && !incorrectChecked) {
                    unansweredQuestions.push(i);
                } else if (correctChecked) {
                    correctCount++;
                }
            }

            const result = document.getElementById('result');

            if (unansweredQuestions.length > 0) {
                result.innerText = `答え合わせが漏れている設問があります: No.${unansweredQuestions.join(", ")}`
                return;
            }

            clearInterval(timerInterval);
            const correctPercentage = (correctCount / questionCount) * 100;
            const passOrFail = correctPercentage >= passingScore ? '合格!' : '不合格';
            result.innerText = `正解数: ${correctCount} / ${questionCount} (${correctPercentage.toFixed(2)}%) - ${passOrFail}`;
        }
    </script>
</body>
</html>

この記事が気に入ったらサポートをしてみませんか?