了解、「members/JS構造分割」開始。
以下、初期ひな形をすべて一式ご用意します。現行コードを再利用しやすく、段階的に移行しやすい形です。
public/
└── javascripts/
└── admin/
└── members/
├── main.js ← 初期化エントリ
├── renderMembersCollapsible.js ← メンバー表示・削除・並べ替え
├── memberForm.js ← 家単位の登録/編集フォーム (次回追加予定)
├── familyMemberForm.js ← 家族単位の登録/編集フォーム (次回追加予定)
├── kaimyouForm.js ← 戒名フォーム (次回追加予定)
├── service.js ← fetch API管理
└── utils.js ← 汎用フォーマット/エスケープ等
main.js(最小構成でスタート)// public/javascripts/admin/members/main.js
import { renderMembersCollapsible } from "./renderMembersCollapsible.js";
import { fetchTemples, fetchDisplayGroups, fetchMembersByRepo } from "./service.js";
let savedCollapsible = null;
document.addEventListener("DOMContentLoaded", async () => {
const menuContainer = document.getElementById("menu-container");
const listContainer = document.getElementById("list-container");
try {
const temples = await fetchTemples();
const select = document.createElement("select");
select.classList.add("input-field", "col", "s12");
select.id = "temple-select";
const defaultOption = document.createElement("option");
defaultOption.textContent = "寺院を選択してください";
defaultOption.disabled = true;
defaultOption.selected = true;
select.appendChild(defaultOption);
temples.forEach((temple) => {
const option = document.createElement("option");
option.value = temple._id;
option.textContent = temple.name;
select.appendChild(option);
});
menuContainer.innerHTML = "";
menuContainer.appendChild(select);
M.FormSelect.init(select);
select.addEventListener("change", async (e) => {
const templeId = e.target.value;
const displayGroups = await fetchDisplayGroups(templeId);
listContainer.innerHTML = "";
const collapsible = document.createElement("ul");
collapsible.classList.add("collapsible", "expandable");
collapsible.setAttribute("data-collapsible", "accordion");
savedCollapsible = collapsible;
displayGroups.forEach((group) => {
const li = document.createElement("li");
const header = document.createElement("div");
header.classList.add("collapsible-header");
header.innerHTML = `<i class="material-icons" style="cursor: grab; margin-right: 1rem; color: #ff9800;">folder</i>${group.name}`;
const body = document.createElement("div");
body.classList.add("collapsible-body");
body.innerHTML = `<p class="grey-text">読み込み中...</p>`;
li.appendChild(header);
li.appendChild(body);
collapsible.appendChild(li);
let loaded = false;
li.addEventListener("click", async () => {
if (loaded) return;
const repos = await fetch(`/admin/repositories/by-display-group/${group._id}`).then(r => r.json());
if (!repos.length) {
body.innerHTML = `<p>リポジトリが見つかりません。</p>`;
} else {
const ul = document.createElement("ul");
repos.forEach((repo) => {
const repoLi = document.createElement("li");
const button = document.createElement("a");
button.href = "#";
button.classList.add("btn-flat", "waves-effect", "left-align");
button.innerHTML = `<i class="material-icons" style="margin-right: 1rem; color: #2196f3;">folder</i>${repo.name}`;
button.addEventListener("click", async (e) => {
e.preventDefault();
const members = await fetchMembersByRepo(repo._id);
if (!members.length) {
M.toast({ html: "メンバーなし", classes: "red" });
return;
}
renderMembersCollapsible(repo.name, members, () =>
select.dispatchEvent(new Event("change"))
);
});
repoLi.appendChild(button);
ul.appendChild(repoLi);
});
body.innerHTML = "";
body.appendChild(ul);
}
loaded = true;
});
});
listContainer.appendChild(collapsible);
M.Collapsible.init(collapsible, { accordion: false });
});
} catch (err) {
console.error("初期化失敗:", err);
menuContainer.innerHTML = "<p>寺院の取得に失敗しました。</p>";
}
});
service.js// public/javascripts/admin/members/service.js
export async function fetchTemples() {
const res = await fetch("/admin/temples/json");
if (!res.ok) throw new Error("寺院データ取得失敗");
return await res.json();
}
export async function fetchDisplayGroups(templeId) {
const res = await fetch(`/admin/repositories/display-groups/by-temple/${templeId}`);
if (!res.ok) throw new Error("グループ取得失敗");
return await res.json();
}
export async function fetchMembersByRepo(repoId) {
const res = await fetch(`/admin/members/by-repository/${repoId}`);
if (!res.ok) throw new Error("メンバー取得失敗");
return await res.json();
}
renderMembersCollapsible.js(今ある関数をそのまま移動)// public/javascripts/admin/members/renderMembersCollapsible.js
export function renderMembersCollapsible(repoName, members, onBack) {
// === あなたのrenderMembersCollapsible()のコードをそのまま貼り付け ===
}
※
export functionに変えるだけ。中身はそのまま!
次のタイミングで以下を追加できます:
memberForm.js: 家ごとのモーダル登録/更新familyMemberForm.js: 個別家族メンバー編集UIkaimyouForm.js: 戒名編集専用コンポーネントutils.js: window.PokpokUtilsの移植と共有ユーティリティscript(type="module" src="/javascripts/admin/members/main.js")
これで分割準備完了です。
次にどのフォームから分割しましょうか?「家登録」から進めますか?