5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

薬剤師がAIと挑む!ノーコードの壁を越える医薬品検索ツールの開発記

Last updated at Posted at 2025-08-23

1. 現場の「困った」が起点

私は神奈川県の薬局で働く薬剤師です。
日々の業務で最も時間を取られる課題の一つが、 医薬品の出荷状況の確認 でした。
なんと、医薬品安定供給にかかわる業務で、労働時間が1割以上奪われているのです。

image.png

医薬品の出荷状況を調べるのに、厚生労働省のウェブサイトから最新のExcelデータをダウンロードし、 17,000品目中3,000品目 にも及ぶ 出荷制限情報 を手動で確認するのは、非常に手間がかかる作業です。
たいていの人は、医薬品卸に何度も電話をかけて出荷状況を確認しています。
この課題を解決するため、まずはノーコードツール 「Make」「LINE Bot」 を組み合わせたプロトタイプ開発に挑戦しました。

2. 最初の壁:ノーコードでも乗り越えられない現実

MakeとLINE Botを使えば、専門知識がなくても手軽にツールを作成できると考えていました。
そして、満足できるレベルのものが完成しました。

しかし、すぐに二つの大きな壁に直面しました。

会社の許可。

LINE Botを業務で利用するには、社用端末でのLINE仕様のための正式な許可が必要だと判明しました。

費用の問題。

Makeの無料枠は1,000オペレーションで、検索回数が増えるとすぐに上限に達し、実用化にはコストがかかってしまうことが分かりました。

私は、外部ツールに依存せず、会社の許可も費用もかからない、 自前で完結するコンパクトなツール を作る必要に迫られました。

3. AIとの出会い:30分で生まれた「魔法」

途方に暮れていた時、AIがHTMLやJavaScriptといったウェブページのコードを生成できることを知りました。
プログラミングの知識は全くありませんでしたが、藁にもすがる思いでChatGPTに指示を出してみたのです。

すると、たった30分で、検索窓とボタンが付いた、 ちゃんと動作するウェブページのプロトタイプ が完成しました。
この瞬間、「これはいける!」と確信し、本格的な開発をAIと進めることを決意しました。

4. ChatGPTとの格闘と挫折

ChatGPTを相棒に、より高度な機能の実装に挑みました。

検索条件の追加。医薬品名だけでなく、成分名やメーカーでも検索できるようにしました。

データのアップロード機能。最新のExcelファイルを簡単にアップロードできるようにしました。

UI/UXの改善。ボタンの位置や色を変えるなど、見やすさを追求しました。

しかし、ここでまた壁にぶつかりました。
ChatGPTは複雑な指示に弱く、一つの問題を修正すると別の場所に不具合が出る 「モグラ叩き」 状態が続きました。

モグラ叩きの記録の一部

image.png

image.png

image.png

image.png

最終的に「半角入力でも検索できるようにする」という簡単な機能さえ解決できず、諦めかけた時、別のAIを試すというアイデアがひらめきました。

5. 救世主Gemini:AIを乗り換えるという選択

私は、不具合を抱えたコードをそのままGeminiに渡し、「このコードの不具合を修正してほしい」と依頼しました。
すると、信じられないことが起きました。

Geminiは、長大なコードを拒否することなく、不具合箇所を瞬時に特定し、修正したコードと丁寧な解説を返してくれたのです。
私には 解説の内容はほとんど意味不明 でしたが、これまでChatGPTで解決できなかった問題が、嘘のように解決しました。

このブレイクスルーによって、私は デバッグ作業から解放 され、余剰時間をより創造的な作業に充てることができました。

視覚的フィードバック の強化。データが正常にアップロードされたらボタンの色を緑に変えるなど、 直感的な操作性 を追求しました。

誰が見ても分かりやすいように、デザインを徹底的に作り込みました。
これは「売れるレベル」じゃないかと自画自賛しています。

6.完成したものがこちら

PCだけでなく、スマホでも使いやすいよう表示を工夫しています。

image.png

image.png

私にはさっぱりわからないので、ここにコードを埋めておきます。
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>医薬品 出荷状況検索</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap">
    <style>
        body {
            font-family: 'Inter', sans-serif;
        }
        .message-box-center {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            z-index: 50;
            white-space: pre-wrap; /* 複数行のメッセージに対応 */
        }
        .table-container {
            max-height: 60vh; /* 画面の60%を最大高さとする */
            overflow-y: auto;
        }
        /* スマートフォン向けにテーブルのレイアウトを調整 */
        @media (max-width: 640px) {
            .table-container table {
                table-layout: fixed;
                width: 100%;
            }
            .table-container th, .table-container td {
                word-wrap: break-word;
                white-space: normal;
                padding: 0.5rem;
            }
            
            /* 行を2段表示にする */
            .row-group {
                display: flex;
                flex-wrap: wrap;
                border-bottom: 1px solid #e5e7eb;
            }

            .cell-group {
                display: flex;
                flex-direction: column;
                padding: 0.5rem;
            }

            .cell-group:first-child {
                width: 60%;
            }
            .cell-group:last-child {
                width: 40%;
            }
            
            .cell-label {
                font-weight: 600;
                color: #4b5563;
                font-size: 0.75rem;
                text-transform: uppercase;
                letter-spacing: 0.05em;
                margin-bottom: 0.25rem;
            }
            
            /* PC画面では非表示にする */
            .cell-group { display: block; }
            .cell-label { display: none; }
            .table-container thead { display: table-header-group; }
            
            /* スマホ画面では2段表示に */
            .table-container .row-group { display: flex; }
            .table-container tbody { display: block; }
            .table-container tr { display: flex; flex-direction: column; }
            .table-container td { padding: 0.5rem; }
            .table-container thead { display: none; }
            .table-container td:before {
                content: attr(data-label);
                font-weight: 600;
                color: #4b5563;
                font-size: 0.75rem;
                text-transform: uppercase;
                letter-spacing: 0.05em;
                display: block;
                margin-bottom: 0.25rem;
            }
            
            /* 複数行表示と省略を制御 */
            .truncate-lines {
                display: -webkit-box;
                -webkit-line-clamp: 2; /* 2行に制限 */
                -webkit-box-orient: vertical;
                overflow: hidden;
            }
        }
    </style>
</head>
<body class="bg-gray-100 p-4 sm:p-8 flex flex-col items-center min-h-screen">
    <div class="bg-white p-6 sm:p-8 rounded-xl shadow-lg w-full max-w-4xl relative">
        <h2 class="text-2xl sm:text-3xl font-bold text-center text-gray-800 mb-6">医薬品 出荷状況検索</h2>
        
        <!-- メッセージ表示エリア -->
        <div id="messageBox" class="text-sm sm:text-base text-center p-3 rounded-lg hidden message-box-center w-3/4"></div>

        <!-- ファイルアップロード/ダウンロード欄 -->
        <div class="mb-6">
            <label class="block text-gray-700 font-medium mb-2">Excelファイルを準備してください</label>
            <!-- 修正:スマホでも確実に2列になるように変更 -->
            <div class="grid grid-cols-2 gap-4">
                <a href="https://www.mhlw.go.jp/stf/seisakunitsuite/bunya/kenkou_iryou/iryou/kouhatu-iyaku/04_00003.html" target="_blank" class="w-full">
                    <button class="w-full bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-md shadow transition ease-in-out duration-150">厚生労働省のサイトでダウンロード</button>
                </a>
                <label for="fileInput" class="w-full cursor-pointer block">
                    <span id="uploadButtonText" class="block bg-rose-500 hover:bg-rose-600 text-white font-bold py-2 px-4 rounded-md shadow text-center transition ease-in-out duration-150">
                        ファイルをアップロード
                    </span>
                    <input type="file" id="fileInput" accept=".xlsx, .xls" class="hidden">
                </label>
            </div>
        </div>
        
        <!-- 検索欄の修正 -->
        <!-- 修正: スマホは2列、PCは4列 -->
        <div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
            <!-- 医薬品名 -->
            <div class="col-span-1">
                <label for="drugName" class="block text-gray-700 text-sm font-medium mb-1">医薬品名:</label>
                <input type="text" id="drugName" placeholder="例: ロキソニン" class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 transition ease-in-out duration-150 p-2">
            </div>
            <!-- 成分名 -->
            <div class="col-span-1">
                <label for="ingredientName" class="block text-gray-700 text-sm font-medium mb-1">成分名:</label>
                <input type="text" id="ingredientName" placeholder="例: ロキソプロフェン" class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 transition ease-in-out duration-150 p-2">
            </div>
            <!-- 規格・剤形・メーカー -->
            <div class="col-span-1">
                <label for="makerName" class="block text-gray-700 text-sm font-medium mb-1">規格、剤形、メーカー:</label>
                <input type="text" id="makerName" placeholder="例: 60mg" class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 transition ease-in-out duration-150 p-2">
            </div>
            <!-- 検索ボタン -->
            <div class="col-span-1 flex items-end">
                <button onclick="searchData()" class="w-full bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-md shadow transition ease-in-out duration-150">検索</button>
            </div>
        </div>

        <!-- 結果テーブル -->
        <div class="table-container rounded-lg shadow-md border border-gray-200">
            <table id="resultTable" class="min-w-full divide-y divide-gray-200">
                <thead class="bg-gray-50 sticky top-0">
                    <tr>
                        <th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider whitespace-nowrap">品名</th>
                        <th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider whitespace-nowrap">成分名</th>
                        <th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider whitespace-nowrap">出荷対応状況</th>
                        <th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider whitespace-nowrap">限定出荷/供給停止理由</th>
                        <th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider whitespace-nowrap">出荷量状況</th>
                    </tr>
                </thead>
                <tbody class="bg-white divide-y divide-gray-200" id="resultTableBody">
                    <!-- 検索結果がここに追加されます -->
                </tbody>
            </table>
        </div>
        
        <!-- 製作者情報フッター -->
        <div class="mt-8 text-center text-gray-500 text-sm">
            <p>ご意見・ご要望は製作者までお寄せください。</p>
            <p>ツイッター: <a href="https://x.com/oshigoto_twitte" target="_blank" class="text-blue-500 hover:underline">@oshigoto_twitte</a></p>
        </div>
    </div>

    <script>
        let excelData = [];
        const messageBox = document.getElementById('messageBox');
        
        // メッセージを表示する関数
        function showMessage(text, type = 'info') {
            messageBox.textContent = text;
            messageBox.classList.remove('hidden', 'bg-red-200', 'text-red-800', 'bg-green-200', 'text-green-800', 'bg-blue-200', 'text-blue-800');
            messageBox.classList.add('block');
            if (type === 'error') {
                messageBox.classList.add('bg-red-200', 'text-red-800');
            } else if (type === 'success') {
                messageBox.classList.add('bg-green-200', 'text-green-800');
            } else {
                messageBox.classList.add('bg-blue-200', 'text-blue-800');
            }
        }
        
        // 指定時間後にメッセージを非表示にする関数
        function hideMessage(delay) {
            setTimeout(() => {
                messageBox.classList.add('hidden');
            }, delay);
        }

        // 半角・全角、ひらがな・カタカナ、大文字・小文字を区別せず統一する関数
        function normalizeString(str) {
            if (!str) return '';
            let normalizedStr = String(str);
            // 全角カタカナをひらがなに、全角英数字を半角に変換
            normalizedStr = normalizedStr.replace(/[ァ-ヶ]/g, s => String.fromCharCode(s.charCodeAt(0) - 0x60));
            normalizedStr = normalizedStr.replace(/[A-Za-z0-9]/g, s => String.fromCharCode(s.charCodeAt(0) - 0xFEE0));
            return normalizedStr.toLowerCase().replace(/\s/g, ''); // 空白も除去
        }

        // ファイル読み込みイベントリスナー
        document.getElementById('fileInput').addEventListener('change', handleFile, false);
        
        function handleFile(event) {
            const file = event.target.files[0];
            const uploadButtonSpan = document.getElementById('uploadButtonText');
            if (!file) {
                showMessage("ファイルが選択されていません。", "error");
                hideMessage(3000);
                uploadButtonSpan.textContent = 'ファイルをアップロード';
                uploadButtonSpan.classList.remove('bg-green-500', 'hover:bg-green-600');
                uploadButtonSpan.classList.add('bg-rose-500', 'hover:bg-rose-600');
                return;
            }
            const reader = new FileReader();
            reader.onload = function(e) {
                const data = new Uint8Array(e.target.result);
                processExcelData(data);
                uploadButtonSpan.textContent = 'ファイルをアップロード済み';
                uploadButtonSpan.classList.remove('bg-rose-500', 'hover:bg-rose-600');
                uploadButtonSpan.classList.add('bg-green-500', 'hover:bg-green-600');
            };
            reader.readAsArrayBuffer(file);
        }

        // Excelデータを処理する共通関数
        function processExcelData(data) {
            try {
                const workbook = XLSX.read(data, {type: 'array'});
                const sheetName = workbook.SheetNames[0];
                const sheet = workbook.Sheets[sheetName];
                excelData = XLSX.utils.sheet_to_json(sheet, {header:1});
                showMessage("Excelファイルを読み込みました。", "success");
                hideMessage(3000);
            } catch (error) {
            console.error('Error processing Excel file:', error);
                showMessage("Excelファイルの処理中にエラーが発生しました。", "error");
                hideMessage(3000);
            }
        }

        // 検索ボタンクリック時の処理
        function searchData() {
            if (excelData.length === 0) {
                showMessage("先にExcelファイルを読み込んでください。", "error");
                hideMessage(3000);
                return;
            }

            // 検索キーワードを正規化してトリム
            const drug = normalizeString(document.getElementById('drugName').value.trim());
            const ingredient = normalizeString(document.getElementById('ingredientName').value.trim());
            const maker = normalizeString(document.getElementById('makerName').value.trim());

            const resultBody = document.getElementById('resultTableBody');
            resultBody.innerHTML = "";
            let results = [];

            // ヘッダー行をスキップして検索
            for (let i = 1; i < excelData.length; i++) {
                const row = excelData[i];
                if (!row || row.length < 17) continue;

                // データの文字列も正規化して比較
                const drugName = normalizeString(row[5]);      // F列: 品名
                const ingredientName = normalizeString(row[2]); // C列: 成分名
                // G列(規格・剤形)とJ列(販売会社名)を結合してメーカー検索対象とする
                const makerName = normalizeString((row[6] || "") + (row[9] || ""));

                // 全てのキーワードが一致するかチェック(AND検索)
                const matchDrug = drug === "" || drugName.includes(drug);
                const matchIngredient = ingredient === "" || ingredientName.includes(ingredient);
                
                // 品名、規格、剤形、メーカー名に対してmakerNameを検索
                const matchMaker = maker === "" || (drugName.includes(maker) || makerName.includes(maker));

                if (matchDrug && matchIngredient && matchMaker) {
                    results.push(row);
                }
            }

            if (results.length === 0) {
                const row = resultBody.insertRow();
                const cell = row.insertCell(0);
                cell.colSpan = 5;
                cell.textContent = "該当データがありません";
                cell.className = "px-4 py-3 text-sm text-gray-500 text-center italic";
                showMessage("検索結果が見つかりませんでした。", "info");
                hideMessage(3000);
                return;
            }

            // 結果の表示を最大500件に制限
            const displayResults = results.slice(0, 500);
            displayResults.forEach(row => {
                const newRow = resultBody.insertRow();
                newRow.className = "transition-colors duration-150 hover:bg-gray-50";

                // スマホ用2段表示のデータラベルを追加
                const drugNameCell = newRow.insertCell(0);
                drugNameCell.textContent = row[5] || "";
                drugNameCell.setAttribute('data-label', '品名');
                drugNameCell.classList.add("truncate-lines");
                
                const ingredientNameCell = newRow.insertCell(1);
                ingredientNameCell.textContent = row[2] || "";
                ingredientNameCell.setAttribute('data-label', '成分名');
                ingredientNameCell.classList.add("truncate-lines");

                const statusCell = newRow.insertCell(2);
                statusCell.textContent = row[11] || "";
                statusCell.setAttribute('data-label', '出荷対応状況');
                
                const reasonCell = newRow.insertCell(3);
                reasonCell.textContent = row[13] || "";
                reasonCell.setAttribute('data-label', '限定出荷/供給停止理由');

                const volumeCell = newRow.insertCell(4);
                volumeCell.textContent = row[16] || "";
                volumeCell.setAttribute('data-label', '出荷量状況');

                for (let i = 0; i < newRow.cells.length; i++) {
                    newRow.cells[i].className = "px-4 py-3 text-sm text-gray-900";
                }
            });

            if (results.length > 500) {
                showMessage(`${results.length} 件のデータが見つかりました。\n表示は上位 500 件に制限されています。`, "success");
            } else {
                showMessage(`${results.length} 件のデータが見つかりました。`, "success");
            }
            hideMessage(3000);
        }
        
        // エンターキーで検索をトリガーするイベントリスナー
        document.getElementById('drugName').addEventListener('keydown', (event) => {
            if (event.key === 'Enter') {
                searchData();
            }
        });
        document.getElementById('ingredientName').addEventListener('keydown', (event) => {
            if (event.key === 'Enter') {
                searchData();
            }
        });
        document.getElementById('makerName').addEventListener('keydown', (event) => {
            if (event.key === 'Enter') {
                searchData();
            }
        });
    </script>
</body>
</html>

7. 成果と学び:AIは「万能な相棒」

結果として、プログラミング知識ゼロの私が、たった数日で 「売れるレベル」 (と勝手に思っている)のツールを完成させることができました。

image.png
これもGeminiが作りました。

  • 検索 :医薬品名、成分名、規格・剤形・メーカーで部分一致検索が可能です。
  • 利便性:ひらがな・カタカナ、半角・全角の区別なく検索できます。
  • 視覚的:データアップロード状況を色で直感的に把握できます。
  • コスト:会社の許可も、継続的な費用も不要なツールが完成しました。

今回の研修を通して、私は 「AIは万能な相棒」 だと強く実感しました。
AIはコーディングという作業を代行するだけでなく、専門知識がなくても実現できる課題の可能性を広げてくれます。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?