IndexedDBをわかりやすく解説
IndexedDBは、ブラウザの中にデータベースを作って、データを保存できるJavaScriptの仕組みです。
たとえば、Webページで入力した社員情報、買い物リスト、学習記録などを、サーバーに送信せずにブラウザ内へ保存できます。
1. そもそも何に使うの?
- ブラウザにデータを保存する
たとえば、社員名簿やメモを保存。ページを閉じても、データは基本的に残ります。
- オフラインでも使えるWebアプリ
一度保存したデータなら、ネット接続がない状態でも読み書きできるアプリを作れます。
- 大量のデータを管理する
検索用のインデックスを作り、IDや名前などを使ってデータを効率よく探せます。
2. localStorageとの違い
JavaScriptでデータを保存するなら、まずこの2つを区別すると理解しやすいです。
| 比較 | localStorage | IndexedDB |
|---|---|---|
| 保存形式 | 文字列 | オブジェクトなどの構造化データ |
| データ量 | 小規模向け | 比較的大規模なデータ向け |
| 検索 | 基本的に自分で処理 | インデックスで検索可能 |
| 処理方法 | 同期処理 | 非同期処理 |
| 主な用途 | 設定、テーマ | 名簿、記録、アプリのデータ |
イメージとしては、localStorageが小さなメモ帳、IndexedDBがブラウザ内のデータベースです。
3. IndexedDBの構造
たとえば、社員証アプリを作るとします。
データベース:EmployeeApp
オブジェクトストア:employees
社員データをまとめて保存する場所
レコード1
ID: 1 / 名前: 田中 / 部署: 営業
レコード2
ID: 2 / 名前: 佐藤 / 部署: 開発
用語は3つだけ覚えれば大丈夫です。
- Database(データベース):データ全体をまとめる入れ物。
- Object Store(オブジェクトストア):種類ごとにレコードを格納する場所。SQLデータベースのテーブルに少し似ています。
- Record(レコード):実際に保存する1件のデータ。
さらに、key(キー)でレコードを識別し、index(インデックス)で名前や部署などから検索しやすくします。
4. 実際にJavaScriptで使ってみる
ここからが実践です。メモを保存する簡単な例で、IndexedDBの基本を体験してみましょう。
手順① データベースを開く
const request = indexedDB.open("MyDatabase", 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore("memos", {
keyPath: "id",
autoIncrement: true
});
};
request.onsuccess = (event) => {
const db = event.target.result;
console.log("データベースを開きました", db);
};
request.onerror = () => {
console.error("データベースを開けませんでした");
};
重要なのは次の3つです。
-
indexedDB.open():データベースを開く。存在しなければ作成する。 -
onupgradeneeded:新規作成時やデータベースのバージョン更新時に、保存場所などを用意する。 -
onsuccess:データベースを開く処理が成功したときに実行する。
keyPath: "id" は id をレコードのキーにする指定で、autoIncrement: true はキーを自動採番する指定です。
手順② データを保存する
データベースを開いたあとの db を使って、メモを追加します。
const transaction = db.transaction("memos", "readwrite");
const store = transaction.objectStore("memos");
store.add({
title: "IndexedDBの勉強",
content: "データを保存する",
createdAt: new Date().toISOString()
});
ここでの処理は次のとおりです。
-
transaction()でデータ操作を開始する。 -
objectStore()で保存先を取得する。 -
add()で新しいデータを登録する。
"readwrite" は読み書き可能という意味です。読み取りだけなら "readonly" を使います。
手順③ データを読み出す
const transaction = db.transaction("memos", "readonly");
const store = transaction.objectStore("memos");
const request = store.get(1);
request.onsuccess = () => {
console.log(request.result);
};
get(1) は、キーが 1 のレコードを取得します。保存したメモが存在すれば、そのオブジェクトが request.result に入ります。
なお、上のコードは手順①の onsuccess 内など、db が利用できる場所で実行してください。
5. 全体を整理すると
1. データベースを開く
indexedDB.open()
2. 操作する場所と方法を決める
transaction() → objectStore()
3. データを操作する
add()、get()、put()、delete()
6. 実際の開発では何に使う?
これまでのWeb開発の学習に当てはめると、IndexedDBは次のような場面で役立ちます。
- 社員証アプリ:社員情報の一部や表示用データを端末にキャッシュする。
- 業務記録アプリ:通信できない間の入力内容を一時保存し、あとでサーバーと同期する。
- 学習アプリ:問題の回答履歴や進捗を保存する。
- 画像を扱うアプリ:画像やファイルのデータをブラウザ内に保存する。
ただし、IndexedDBはブラウザごとの保存領域です。別の端末や別のブラウザへ自動的にデータが同期されるわけではありません。また、ユーザーの操作やブラウザのストレージ管理によってデータが消える可能性もあります。重要な業務データを扱う場合は、サーバーへの保存やバックアップも検討します。
7.実際の社員名簿アプリを作ってみる
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>社員名簿 | IndexedDB CRUD</title>
<style>
:root{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;color:#172033;background:#f4f6fa}
*{box-sizing:border-box}body{margin:0}header{background:#172b4d;color:white;padding:24px max(20px,calc((100% - 1100px)/2))}
header h1{margin:0 0 6px;font-size:1.6rem}header p{margin:0;color:#d5def0}
main{max-width:1100px;margin:24px auto;padding:0 16px 48px}.panel{background:white;border:1px solid #e0e5ee;border-radius:14px;padding:18px;margin-bottom:16px;box-shadow:0 3px 12px #1b31500a}
h2{font-size:1.1rem;margin:0 0 14px}.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
label{display:block;font-size:.88rem;font-weight:650;margin-bottom:5px}input,select,textarea,button{font:inherit}input,select,textarea{width:100%;padding:10px;border:1px solid #cbd3e1;border-radius:8px;background:white;color:inherit}
.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}button,.filelabel{border:0;border-radius:8px;padding:10px 14px;background:#2458a6;color:white;cursor:pointer;font-weight:650}
button.secondary,.filelabel.secondary{background:#e9eef7;color:#243956}button.danger{background:#a52d38}button:disabled{opacity:.5;cursor:not-allowed}
.toolbar{display:grid;grid-template-columns:2fr 1fr 1fr auto;gap:10px;align-items:end}.muted{color:#66738a;font-size:.9rem}.status{padding:10px 12px;border-radius:8px;background:#eef5ff;color:#234a83;margin-bottom:16px;white-space:pre-wrap}
.tablewrap{overflow:auto}table{width:100%;border-collapse:collapse;min-width:850px}th,td{text-align:left;padding:11px 10px;border-bottom:1px solid #e7ebf2;vertical-align:top}th{font-size:.82rem;color:#526079;background:#f8faff}td small{display:block;color:#657188;margin-top:3px}.rowactions{display:flex;gap:6px}.rowactions button{padding:6px 9px;font-size:.85rem}
.badge{display:inline-block;background:#e8f5ee;color:#216644;padding:3px 8px;border-radius:99px;font-size:.8rem}
footer{font-size:.88rem;color:#637087;line-height:1.6}.hidden{display:none!important}
@media(max-width:760px){.grid{grid-template-columns:1fr 1fr}.toolbar{grid-template-columns:1fr 1fr}.toolbar .wide{grid-column:1/-1}}
@media(max-width:480px){.grid{grid-template-columns:1fr}.toolbar{grid-template-columns:1fr}}
</style>
</head>
<body>
<header><h1>社員名簿</h1><p>IndexedDBで動作するローカルCRUDアプリ</p></header>
<main>
<div id="status" class="status" role="status">データベースを準備しています…</div>
<section class="panel">
<h2 id="formTitle">社員を登録</h2>
<form id="employeeForm">
<input id="employeeId" type="hidden">
<div class="grid">
<div><label for="name">氏名 *</label><input id="name" required maxlength="100" placeholder="例:山田 花子"></div>
<div><label for="department">部署 *</label><select id="department" required><option value="">選択してください</option><option>営業</option><option>開発</option><option>総務</option><option>経理</option><option>人事</option><option>企画</option><option>その他</option></select></div>
<div><label for="email">メールアドレス</label><input id="email" type="email" maxlength="254" placeholder="hanako@example.com"></div>
<div><label for="phone">電話番号</label><input id="phone" type="tel" maxlength="40" placeholder="090-0000-0000"></div>
<div><label for="joined">入社日</label><input id="joined" type="date"></div>
<div><label for="employment">雇用区分</label><select id="employment"><option>正社員</option><option>契約社員</option><option>パート・アルバイト</option><option>業務委託</option></select></div>
</div>
<div class="actions"><button type="submit" id="saveButton">社員を登録</button><button type="button" class="secondary hidden" id="cancelEdit">編集をキャンセル</button><button type="button" class="secondary" id="sampleButton">サンプルデータを追加</button></div>
</form>
</section>
<section class="panel">
<h2>社員を検索・絞り込み</h2>
<div class="toolbar">
<div class="wide"><label for="search">キーワード(氏名・メール・電話)</label><input id="search" placeholder="名前やメールの一部を入力"></div>
<div><label for="filterDepartment">部署</label><select id="filterDepartment"><option value="">すべて</option><option>営業</option><option>開発</option><option>総務</option><option>経理</option><option>人事</option><option>企画</option><option>その他</option></select></div>
<div><label for="sort">並び順</label><select id="sort"><option value="name">氏名(昇順)</option><option value="nameDesc">氏名(降順)</option><option value="newest">登録が新しい順</option><option value="oldest">登録が古い順</option></select></div>
<button id="refreshButton" type="button" class="secondary">再表示</button>
</div>
<p class="muted" id="count">社員 0 人</p>
<div class="tablewrap"><table>
<thead><tr><th>ID</th><th>氏名</th><th>部署・雇用区分</th><th>連絡先</th><th>入社日</th><th>操作</th></tr></thead>
<tbody id="employeeRows"><tr><td colspan="6">読み込み中…</td></tr></tbody>
</table></div>
<div class="actions"><button type="button" id="cursorButton" class="secondary">カーソルで一覧を取得</button><button type="button" id="exportButton" class="secondary">JSONバックアップを書き出す</button><label class="filelabel secondary" for="importFile">JSONを読み込む</label><input id="importFile" type="file" accept="application/json,.json" class="hidden"><button type="button" id="clearButton" class="danger">全社員を削除</button></div>
<p class="muted">検索はインデックス(部署)と文字列の部分一致を組み合わせています。社員数が非常に多い場合は、サーバー側検索なども検討してください。</p>
</section>
<section class="panel">
<h2>このアプリのIndexedDB機能</h2>
<ul>
<li>データベースの作成・バージョン管理(<code>open</code>、<code>onupgradeneeded</code>)</li>
<li>オブジェクトストアと自動採番キー(<code>createObjectStore</code>、<code>keyPath</code>、<code>autoIncrement</code>)</li>
<li>ユニーク/通常インデックス(<code>createIndex</code>)</li>
<li>CRUD(<code>add</code>、<code>get</code>、<code>put</code>、<code>delete</code>、<code>clear</code>)</li>
<li>トランザクションの成功・失敗・中断、DB接続のバージョン変更・ブロック処理</li>
<li>インデックス検索、キー範囲、カーソル走査(<code>IDBKeyRange</code>、<code>openCursor</code>)</li>
<li>JSON形式のバックアップ/復元</li>
</ul>
</section>
<footer>注意:データはこのブラウザのこのサイト領域に保存されます。別の端末とは同期されず、ブラウザのデータ削除などで失われることがあります。実在する社員の個人情報を扱う本番用途では、このローカル版だけに頼らず、アクセス制御・暗号化・監査ログ・バックアップ等を設計してください。</footer>
</main>
<script>
"use strict";
const DB_NAME = "EmployeeDirectoryDB";
const DB_VERSION = 1;
const STORE = "employees";
let db;
const $ = (id) => document.getElementById(id);
const statusEl = $("status");
const form = $("employeeForm");
const fields = ["name", "department", "email", "phone", "joined", "employment"];
function showStatus(message, error = false) {
statusEl.textContent = message;
statusEl.style.background = error ? "#fff0f0" : "#eef5ff";
statusEl.style.color = error ? "#8a2028" : "#234a83";
}
function openDatabase() {
if (!("indexedDB" in window)) {
showStatus("このブラウザではIndexedDBを利用できません。", true);
return;
}
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = (event) => {
const database = event.target.result;
let store;
if (!database.objectStoreNames.contains(STORE)) {
store = database.createObjectStore(STORE, {
keyPath: "id",
autoIncrement: true
});
store.createIndex("name", "name", { unique: false });
store.createIndex("department", "department", { unique: false });
store.createIndex("email", "email", { unique: false });
store.createIndex("joined", "joined", { unique: false });
store.createIndex("createdAt", "createdAt", { unique: false });
} else {
store = event.target.transaction.objectStore(STORE);
}
// スキーマ変更はこのversionchangeトランザクション内で行う。
};
request.onsuccess = (event) => {
db = event.target.result;
db.onversionchange = () => {
db.close();
showStatus("別タブなどからデータベースの更新が要求されました。ページを再読み込みしてください。");
};
showStatus("データベース接続済み。データはこのブラウザ内に保存されます。");
renderEmployees();
};
request.onerror = () => showStatus("DBを開けませんでした:" + (request.error?.message || "不明なエラー"), true);
request.onblocked = () => showStatus("DB更新がブロックされています。他のタブでこのアプリを閉じてください。", true);
}
function transaction(mode = "readonly") {
if (!db) throw new Error("データベースがまだ準備できていません。");
const tx = db.transaction(STORE, mode);
tx.onerror = () => showStatus("トランザクションエラー:" + (tx.error?.message || "操作に失敗しました"), true);
tx.onabort = () => showStatus("トランザクションが中断されました。変更は確定していません。", true);
return tx;
}
function readAll() {
return new Promise((resolve, reject) => {
const tx = transaction("readonly");
const req = tx.objectStore(STORE).getAll();
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
function addEmployee(record) {
return new Promise((resolve, reject) => {
const tx = transaction("readwrite");
const req = tx.objectStore(STORE).add(record);
let key;
req.onsuccess = () => { key = req.result; };
tx.oncomplete = () => resolve(key);
tx.onerror = () => reject(tx.error || req.error);
tx.onabort = () => reject(tx.error || new Error("登録が中断されました"));
});
}
function updateEmployee(record) {
return new Promise((resolve, reject) => {
const tx = transaction("readwrite");
const req = tx.objectStore(STORE).put(record);
req.onerror = () => reject(req.error);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error || req.error);
tx.onabort = () => reject(tx.error || new Error("更新が中断されました"));
});
}
function getEmployee(id) {
return new Promise((resolve, reject) => {
const tx = transaction("readonly");
const req = tx.objectStore(STORE).get(Number(id));
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
function deleteEmployee(id) {
return new Promise((resolve, reject) => {
const tx = transaction("readwrite");
const req = tx.objectStore(STORE).delete(Number(id));
req.onerror = () => reject(req.error);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error || req.error);
tx.onabort = () => reject(tx.error || new Error("削除が中断されました"));
});
}
function clearEmployees() {
return new Promise((resolve, reject) => {
const tx = transaction("readwrite");
const req = tx.objectStore(STORE).clear();
req.onerror = () => reject(req.error);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error || req.error);
tx.onabort = () => reject(tx.error || new Error("全件削除が中断されました"));
});
}
function escapeHTML(value) {
return String(value ?? "").replace(/[&<>"']/g, c => ({
"&":"&", "<":"<", ">":">", '"':""", "'":"'"
})[c]);
}
function formatDate(value) {
if (!value) return "—";
return escapeHTML(value);
}
async function renderEmployees() {
if (!db) return;
try {
let employees = await readAll();
const keyword = $("search").value.trim().toLocaleLowerCase("ja");
const department = $("filterDepartment").value;
const sort = $("sort").value;
if (department) {
// 部署インデックスから取得する。IDBKeyRange.onlyで完全一致。
employees = await new Promise((resolve, reject) => {
const tx = transaction("readonly");
const index = tx.objectStore(STORE).index("department");
const req = index.getAll(IDBKeyRange.only(department));
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
if (keyword) {
employees = employees.filter(e =>
[e.name, e.email, e.phone].some(v => String(v || "").toLocaleLowerCase("ja").includes(keyword))
);
}
employees.sort((a,b) => {
if (sort === "nameDesc") return b.name.localeCompare(a.name, "ja");
if (sort === "newest") return b.createdAt.localeCompare(a.createdAt);
if (sort === "oldest") return a.createdAt.localeCompare(b.createdAt);
return a.name.localeCompare(b.name, "ja");
});
$("count").textContent = `社員 ${employees.length} 人`;
const tbody = $("employeeRows");
if (!employees.length) {
tbody.innerHTML = '<tr><td colspan="6" class="muted">該当する社員はいません。</td></tr>';
return;
}
tbody.innerHTML = employees.map(e => `<tr>
<td>${e.id}</td>
<td><strong>${escapeHTML(e.name)}</strong><small>${escapeHTML(e.email || "")}</small></td>
<td>${escapeHTML(e.department)}<small>${escapeHTML(e.employment || "")}</small></td>
<td>${escapeHTML(e.phone || "—")}</td>
<td>${formatDate(e.joined)}</td>
<td><div class="rowactions"><button type="button" data-edit="${e.id}" class="secondary">編集</button><button type="button" data-delete="${e.id}" class="danger">削除</button></div></td>
</tr>`).join("");
} catch (error) {
showStatus("一覧の取得に失敗しました:" + error.message, true);
}
}
function resetForm() {
form.reset();
$("employeeId").value = "";
$("formTitle").textContent = "社員を登録";
$("saveButton").textContent = "社員を登録";
$("cancelEdit").classList.add("hidden");
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
const id = $("employeeId").value;
const record = {
name: $("name").value.trim(),
department: $("department").value,
email: $("email").value.trim().toLowerCase(),
phone: $("phone").value.trim(),
joined: $("joined").value,
employment: $("employment").value,
updatedAt: new Date().toISOString()
};
if (!record.name || !record.department) {
showStatus("氏名と部署は必須です。", true); return;
}
try {
if (id) {
const old = await getEmployee(id);
if (!old) throw new Error("更新対象の社員が見つかりません。");
await updateEmployee({...old, ...record, id: Number(id), createdAt: old.createdAt});
showStatus(`社員ID ${id} を更新しました。`);
} else {
record.createdAt = new Date().toISOString();
const key = await addEmployee(record);
showStatus(`社員を登録しました(ID: ${key})。`);
}
resetForm();
await renderEmployees();
} catch (error) {
const message = error?.name === "ConstraintError"
? "データの制約エラーが発生しました。入力内容を確認してください。"
: (error.message || "保存に失敗しました。");
showStatus(message, true);
}
});
$("employeeRows").addEventListener("click", async (event) => {
const editButton = event.target.closest("[data-edit]");
const deleteButton = event.target.closest("[data-delete]");
if (editButton) {
try {
const employee = await getEmployee(editButton.dataset.edit);
if (!employee) throw new Error("社員が見つかりません。");
$("employeeId").value = employee.id;
fields.forEach(key => $(key).value = employee[key] || "");
$("formTitle").textContent = `社員ID ${employee.id} を編集`;
$("saveButton").textContent = "変更を保存";
$("cancelEdit").classList.remove("hidden");
$("name").focus();
window.scrollTo({top: 0, behavior: "smooth"});
} catch (error) { showStatus(error.message, true); }
}
if (deleteButton) {
const id = deleteButton.dataset.delete;
if (!confirm(`社員ID ${id} のデータを削除しますか?この操作は取り消せません。`)) return;
try {
await deleteEmployee(id);
if ($("employeeId").value === id) resetForm();
showStatus(`社員ID ${id} を削除しました。`);
await renderEmployees();
} catch (error) { showStatus("削除に失敗しました:" + error.message, true); }
}
});
$("cancelEdit").addEventListener("click", resetForm);
["search", "filterDepartment", "sort"].forEach(id => $(id).addEventListener(id === "search" ? "input" : "change", renderEmployees));
$("refreshButton").addEventListener("click", renderEmployees);
$("sampleButton").addEventListener("click", async () => {
const samples = [
{name:"山田 花子",department:"営業",email:"hanako.yamada@example.com",phone:"090-1111-2222",joined:"2022-04-01",employment:"正社員"},
{name:"佐藤 健",department:"開発",email:"ken.sato@example.com",phone:"090-2222-3333",joined:"2023-10-01",employment:"正社員"},
{name:"鈴木 美咲",department:"総務",email:"misaki.suzuki@example.com",phone:"052-333-4444",joined:"2024-04-01",employment:"契約社員"}
];
try {
for (const s of samples) await addEmployee({...s, createdAt:new Date().toISOString(), updatedAt:new Date().toISOString()});
showStatus("サンプルデータを3件追加しました。");
await renderEmployees();
} catch (error) { showStatus("サンプル追加が途中で失敗しました:" + (error.message || error.name), true); await renderEmployees(); }
});
// カーソルは全件を順番に処理したい場合や、メモリ使用量を抑えたい場合に役立つ。
$("cursorButton").addEventListener("click", () => {
try {
const tx = transaction("readonly");
const store = tx.objectStore(STORE);
const request = store.openCursor(null, "next");
let total = 0;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
total++;
cursor.continue();
} else {
showStatus(`カーソル走査完了:${total}件を順に読み取りました。`);
}
};
request.onerror = () => showStatus("カーソル走査に失敗しました。", true);
} catch (error) { showStatus(error.message, true); }
});
$("exportButton").addEventListener("click", async () => {
try {
const records = await readAll();
const blob = new Blob([JSON.stringify({database:DB_NAME, exportedAt:new Date().toISOString(), employees:records}, null, 2)], {type:"application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = "employee-directory-backup.json"; a.click();
URL.revokeObjectURL(url);
showStatus(`${records.length}件をJSONバックアップとして書き出しました。`);
} catch (error) { showStatus("バックアップに失敗しました:" + error.message, true); }
});
$("importFile").addEventListener("change", async (event) => {
const file = event.target.files[0];
if (!file) return;
try {
const parsed = JSON.parse(await file.text());
if (!Array.isArray(parsed.employees)) throw new Error("employees配列が見つかりません。");
if (!confirm(`${parsed.employees.length}件のデータを読み込みます。既存IDと重なるレコードは上書きされます。続行しますか?`)) return;
const tx = transaction("readwrite");
const store = tx.objectStore(STORE);
for (const raw of parsed.employees) {
if (!raw || typeof raw.name !== "string" || typeof raw.department !== "string") {
tx.abort(); throw new Error("データ形式が不正です。処理を中断しました。");
}
const record = {
name: raw.name, department: raw.department,
email: typeof raw.email === "string" ? raw.email : "",
phone: typeof raw.phone === "string" ? raw.phone : "",
joined: typeof raw.joined === "string" ? raw.joined : "",
employment: typeof raw.employment === "string" ? raw.employment : "正社員",
createdAt: raw.createdAt || new Date().toISOString(),
updatedAt: new Date().toISOString()
};
if (Number.isSafeInteger(raw.id) && raw.id > 0) record.id = raw.id;
store.put(record);
}
tx.oncomplete = async () => { showStatus("JSONデータを読み込みました。"); await renderEmployees(); };
tx.onerror = () => showStatus("インポートに失敗しました:" + (tx.error?.message || "不明なエラー"), true);
tx.onabort = () => showStatus("インポートを中断しました。データは確定していません。", true);
} catch (error) { showStatus("JSON読み込みエラー:" + error.message, true); }
finally { event.target.value = ""; }
});
$("clearButton").addEventListener("click", async () => {
if (!confirm("本当に全社員データを削除しますか?バックアップがない場合、元に戻せません。")) return;
if (!confirm("最終確認:全件削除を実行しますか?")) return;
try {
await clearEmployees();
resetForm();
showStatus("全社員データを削除しました。");
await renderEmployees();
} catch (error) { showStatus("全件削除に失敗しました:" + error.message, true); }
});
openDatabase();
</script>
</body>
</html>