0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

IndexedDB ブラウザの中にデータベースを作って、データを保存できるJavaScriptの仕組み

0
Posted at

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()
});

ここでの処理は次のとおりです。

  1. transaction() でデータ操作を開始する。
  2. objectStore() で保存先を取得する。
  3. 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 => ({
    "&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;", "'":"&#39;"
  })[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>
0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?