1. 現場の「困った」が起点
私は神奈川県の薬局で働く薬剤師です。
日々の業務で最も時間を取られる課題の一つが、 医薬品の出荷状況の確認 でした。
なんと、医薬品安定供給にかかわる業務で、労働時間が1割以上奪われているのです。
医薬品の出荷状況を調べるのに、厚生労働省のウェブサイトから最新の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は複雑な指示に弱く、一つの問題を修正すると別の場所に不具合が出る 「モグラ叩き」 状態が続きました。
最終的に「半角入力でも検索できるようにする」という簡単な機能さえ解決できず、諦めかけた時、別のAIを試すというアイデアがひらめきました。
5. 救世主Gemini:AIを乗り換えるという選択
私は、不具合を抱えたコードをそのままGeminiに渡し、「このコードの不具合を修正してほしい」と依頼しました。
すると、信じられないことが起きました。
Geminiは、長大なコードを拒否することなく、不具合箇所を瞬時に特定し、修正したコードと丁寧な解説を返してくれたのです。
私には 解説の内容はほとんど意味不明 でしたが、これまでChatGPTで解決できなかった問題が、嘘のように解決しました。
このブレイクスルーによって、私は デバッグ作業から解放 され、余剰時間をより創造的な作業に充てることができました。
視覚的フィードバック の強化。データが正常にアップロードされたらボタンの色を緑に変えるなど、 直感的な操作性 を追求しました。
誰が見ても分かりやすいように、デザインを徹底的に作り込みました。
これは「売れるレベル」じゃないかと自画自賛しています。
6.完成したものがこちら
さらに、PCの表示は変えず、横幅の狭い画面(スマートフォン等)のために表示を(Geminiが)調整してみました。


私にはさっぱりわからないので、ここにコードを埋めておきます。
<!DOCTYPE ahtml>
<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;
}
/* Mobile-first approach for responsive table */
@media (max-width: 767px) {
#resultTable thead {
display: none; /* Hide table headers on small screens */
}
#resultTable tbody tr {
display: block; /* Make each table row a block element */
border: 1px solid #e5e7eb;
margin-bottom: 1rem;
padding: 1rem;
border-radius: 0.5rem;
}
#resultTable tbody td {
display: flex; /* Display cells as flex items */
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #e5e7eb;
}
#resultTable tbody td:last-child {
border-bottom: none;
}
#resultTable tbody td::before {
content: attr(data-label); /* Use data-label attribute for pseudo-element content */
font-weight: 600;
text-transform: uppercase;
color: #6b7280;
font-size: 0.75rem;
flex: 1;
margin-right: 1rem;
}
}
</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 mb-4 hidden absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-3/4"></div>
<!-- ファイルアップロード/ダウンロード欄 -->
<div class="mb-6">
<label class="block text-gray-700 font-medium mb-2">Excelファイルを準備してください</label>
<div class="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4">
<a href="https://www.mhlw.go.jp/stf/seisakunitsuite/bunya/kenkou_iryou/iryou/kouhatu-iyaku/04_00003.html" target="_blank" class="w-full sm:w-1/2">
<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 sm:w-1/2 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>
<!-- 検索欄 -->
<div class="space-y-4 mb-6">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<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>
<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>
<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>
<div class="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="overflow-x-auto 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">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">品名</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">成分名</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">出荷対応状況</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">限定出荷/供給停止理由</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">出荷量状況</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200" id="resultTableBody">
<!-- 検索結果がここに追加されます -->
</tbody>
</table>
</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 katakanaToHiragana(str) {
return str.replace(/[\u30a1-\u30f6]/g, function(match) {
const chr = match.charCodeAt(0) - 0x60;
return String.fromCharCode(chr);
});
}
// 半角・全角、ひらがな・カタカナを区別せず、小文字に統一して検索する関数
function normalizeString(str) {
if (!str) return '';
// カタカナをひらがなに変換
let normalizedStr = katakanaToHiragana(String(str));
// 小文字に変換
normalizedStr = normalizedStr.toLowerCase();
// 全角英数字を半角に変換
return normalizedStr.replace(/[A-Za-z0-9]/g, function(s) {
return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
});
}
// ファイル読み込みイベントリスナー
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(2000); // 2秒後にメッセージを消す
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(2000); // 2秒後にメッセージを消す
} catch (error) {
console.error('Error processing Excel file:', error);
showMessage("Excelファイルの処理中にエラーが発生しました。", "error");
hideMessage(2000); // 2秒後にメッセージを消す
}
}
// 検索ボタンクリック時の処理
function searchData() {
if (excelData.length === 0) {
showMessage("先にExcelファイルを読み込んでください。", "error");
hideMessage(2000); // 2秒後にメッセージを消す
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 = excelData.filter((row, index) => {
if (index === 0) return false; // ヘッダー行を除外
// データ側の文字列も正規化して比較
const drugName = normalizeString(row[5]); // F列: 品名
const ingredientName = normalizeString(row[2]); // C列: 成分名
const makerName = normalizeString((row[5] || "") + " " + (row[6] || "")); // F+G列
const matchDrug = drug === "" || drugName.includes(drug);
const matchIngredient = ingredient === "" || ingredientName.includes(ingredient);
const matchMaker = maker === "" || makerName.includes(maker);
return matchDrug && matchIngredient && matchMaker;
});
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(2000); // 2秒後にメッセージを消す
return;
}
results.forEach(row => {
const newRow = resultBody.insertRow();
newRow.className = "transition-colors duration-150 hover:bg-gray-50";
// Set data-label attributes for mobile view
newRow.insertCell(0).textContent = row[5] || "";
newRow.cells[0].setAttribute('data-label', '品名');
newRow.insertCell(1).textContent = row[2] || "";
newRow.cells[1].setAttribute('data-label', '成分名');
newRow.insertCell(2).textContent = row[11] || "";
newRow.cells[2].setAttribute('data-label', '出荷対応状況');
newRow.insertCell(3).textContent = row[13] || "";
newRow.cells[3].setAttribute('data-label', '限定出荷/供給停止理由');
newRow.insertCell(4).textContent = row[16] || "";
newRow.cells[4].setAttribute('data-label', '出荷量状況');
// Add classes for styling
for (let i = 0; i < newRow.cells.length; i++) {
newRow.cells[i].className = "px-4 py-3 text-sm text-gray-900";
}
});
showMessage(`${results.length} 件のデータが見つかりました。`, "success");
hideMessage(2000); // 2秒後にメッセージを消す
}
// エンターキーで検索をトリガーするイベントリスナーを追加
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は「万能な相棒」
結果として、プログラミング知識ゼロの私が、たった数日で 「売れるレベル」 (と勝手に思っている)のツールを完成させることができました。
- 検索 :医薬品名、成分名、規格・剤形・メーカーで部分一致検索が可能です。
- 利便性:ひらがな・カタカナ、半角・全角の区別なく検索できます。
- 視覚的:データアップロード状況を色で直感的に把握できます。
- コスト:会社の許可も、継続的な費用も不要な内製ツールが完成しました。
今回の研修を通して、私は 「AIは万能な相棒」 だと強く実感しました。
AIはコーディングという作業を代行するだけでなく、専門知識がなくても実現できる課題の可能性を広げてくれます。