You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
818 lines
25 KiB
818 lines
25 KiB
|
4 weeks ago
|
(function () {
|
||
|
|
const body = document.body;
|
||
|
|
const sidebarToggle = document.querySelector("[data-sidebar-toggle]");
|
||
|
|
const sidebar = document.querySelector(".sidebar");
|
||
|
|
const toastRoot = document.getElementById("toastRoot");
|
||
|
|
const workspaceTabsEl = document.querySelector(".workspace-tabs");
|
||
|
|
const openTabsKey = "teraclone.openTabs";
|
||
|
|
const collapsedNavGroupsKey = "teraclone.collapsedNavGroups";
|
||
|
|
const legacyHiddenTabsKey = "teraclone.hiddenTabs";
|
||
|
|
const portTargetsKey = "teraclone.portTargets";
|
||
|
|
|
||
|
|
function showToast(message) {
|
||
|
|
if (!toastRoot) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const toast = document.createElement("div");
|
||
|
|
toast.className = "toast";
|
||
|
|
toast.textContent = message;
|
||
|
|
toastRoot.appendChild(toast);
|
||
|
|
setTimeout(function () {
|
||
|
|
toast.remove();
|
||
|
|
}, 2600);
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseJSONSafely(response) {
|
||
|
|
return response.json().catch(function () {
|
||
|
|
return {};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
sidebarToggle?.addEventListener("click", function () {
|
||
|
|
body.classList.toggle("sidebar-open");
|
||
|
|
});
|
||
|
|
|
||
|
|
function closeSidebar() {
|
||
|
|
body.classList.remove("sidebar-open");
|
||
|
|
}
|
||
|
|
|
||
|
|
function readStoredObject(storageKey) {
|
||
|
|
try {
|
||
|
|
const parsed = JSON.parse(window.localStorage.getItem(storageKey) || "{}");
|
||
|
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
||
|
|
} catch (error) {
|
||
|
|
console.error(error);
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function writeStoredObject(storageKey, value) {
|
||
|
|
window.localStorage.setItem(storageKey, JSON.stringify(value));
|
||
|
|
}
|
||
|
|
|
||
|
|
function readOpenTabs() {
|
||
|
|
try {
|
||
|
|
const parsed = JSON.parse(window.localStorage.getItem(openTabsKey) || "[]");
|
||
|
|
if (!Array.isArray(parsed)) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
return parsed.filter(function (tab) {
|
||
|
|
return tab &&
|
||
|
|
typeof tab.key === "string" && tab.key &&
|
||
|
|
typeof tab.path === "string" && tab.path &&
|
||
|
|
typeof tab.label === "string" && tab.label;
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error(error);
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function writeOpenTabs(tabs) {
|
||
|
|
window.localStorage.setItem(openTabsKey, JSON.stringify(tabs));
|
||
|
|
}
|
||
|
|
|
||
|
|
function getCurrentTab() {
|
||
|
|
const path = body.getAttribute("data-current-path") || window.location.pathname;
|
||
|
|
if (path.startsWith("/ports/")) {
|
||
|
|
return { key: "ports", path: path, label: "포트 설정" };
|
||
|
|
}
|
||
|
|
|
||
|
|
const key = body.getAttribute("data-current-tab-key") || path;
|
||
|
|
const label = body.getAttribute("data-current-tab-label") || document.title;
|
||
|
|
return { key: key, path: path, label: label };
|
||
|
|
}
|
||
|
|
|
||
|
|
function dedupeTabs(tabs) {
|
||
|
|
const seenKeys = new Set();
|
||
|
|
return tabs.filter(function (tab) {
|
||
|
|
if (seenKeys.has(tab.key)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
seenKeys.add(tab.key);
|
||
|
|
return true;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function syncOpenTabs() {
|
||
|
|
const currentTab = getCurrentTab();
|
||
|
|
const openTabs = dedupeTabs(readOpenTabs());
|
||
|
|
const existingTab = openTabs.find(function (tab) {
|
||
|
|
return tab.key === currentTab.key;
|
||
|
|
});
|
||
|
|
|
||
|
|
if (existingTab) {
|
||
|
|
existingTab.key = currentTab.key;
|
||
|
|
existingTab.path = currentTab.path;
|
||
|
|
existingTab.label = currentTab.label;
|
||
|
|
} else {
|
||
|
|
openTabs.push(currentTab);
|
||
|
|
}
|
||
|
|
|
||
|
|
writeOpenTabs(openTabs);
|
||
|
|
return openTabs;
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderWorkspaceTabs() {
|
||
|
|
if (!workspaceTabsEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const currentTab = getCurrentTab();
|
||
|
|
const openTabs = syncOpenTabs();
|
||
|
|
workspaceTabsEl.replaceChildren();
|
||
|
|
|
||
|
|
openTabs.forEach(function (tab) {
|
||
|
|
const tabEl = document.createElement("div");
|
||
|
|
tabEl.className = "workspace-tab";
|
||
|
|
tabEl.setAttribute("data-tab-key", tab.key);
|
||
|
|
tabEl.setAttribute("data-tab-path", tab.path);
|
||
|
|
if (tab.key === currentTab.key) {
|
||
|
|
tabEl.classList.add("active");
|
||
|
|
}
|
||
|
|
|
||
|
|
const linkEl = document.createElement("a");
|
||
|
|
linkEl.className = "workspace-tab-link";
|
||
|
|
linkEl.href = tab.path;
|
||
|
|
linkEl.textContent = tab.label;
|
||
|
|
|
||
|
|
const closeEl = document.createElement("button");
|
||
|
|
closeEl.className = "workspace-close";
|
||
|
|
closeEl.type = "button";
|
||
|
|
closeEl.setAttribute("aria-label", tab.label + " 닫기");
|
||
|
|
closeEl.textContent = "x";
|
||
|
|
|
||
|
|
closeEl.addEventListener("click", function (event) {
|
||
|
|
event.preventDefault();
|
||
|
|
event.stopPropagation();
|
||
|
|
closeWorkspaceTab(tab.key);
|
||
|
|
});
|
||
|
|
|
||
|
|
tabEl.append(linkEl, closeEl);
|
||
|
|
workspaceTabsEl.appendChild(tabEl);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeWorkspaceTab(tabKey) {
|
||
|
|
const openTabs = syncOpenTabs();
|
||
|
|
const tabIndex = openTabs.findIndex(function (tab) {
|
||
|
|
return tab.key === tabKey;
|
||
|
|
});
|
||
|
|
|
||
|
|
if (tabIndex === -1) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const nextTabs = openTabs.filter(function (tab) {
|
||
|
|
return tab.key !== tabKey;
|
||
|
|
});
|
||
|
|
writeOpenTabs(nextTabs);
|
||
|
|
|
||
|
|
if (getCurrentTab().key === tabKey) {
|
||
|
|
const fallbackTab = nextTabs[tabIndex] || nextTabs[tabIndex - 1];
|
||
|
|
window.location.assign(fallbackTab ? fallbackTab.path : "/");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
renderWorkspaceTabs();
|
||
|
|
}
|
||
|
|
|
||
|
|
function applyNavGroupState() {
|
||
|
|
const storedGroupState = readStoredObject(collapsedNavGroupsKey);
|
||
|
|
|
||
|
|
document.querySelectorAll("[data-nav-group]").forEach(function (group) {
|
||
|
|
const groupPath = group.getAttribute("data-path");
|
||
|
|
const toggle = group.querySelector("[data-nav-toggle]");
|
||
|
|
const defaultExpanded = group.getAttribute("data-default-expanded") === "true";
|
||
|
|
if (!groupPath || !toggle) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const expanded = Object.prototype.hasOwnProperty.call(storedGroupState, groupPath)
|
||
|
|
? Boolean(storedGroupState[groupPath])
|
||
|
|
: defaultExpanded;
|
||
|
|
group.classList.toggle("expanded", expanded);
|
||
|
|
toggle.setAttribute("aria-expanded", expanded ? "true" : "false");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function bindNavGroups() {
|
||
|
|
applyNavGroupState();
|
||
|
|
|
||
|
|
document.querySelectorAll("[data-nav-toggle]").forEach(function (toggle) {
|
||
|
|
toggle.addEventListener("click", function (event) {
|
||
|
|
event.preventDefault();
|
||
|
|
|
||
|
|
const group = toggle.closest("[data-nav-group]");
|
||
|
|
const groupPath = group?.getAttribute("data-path");
|
||
|
|
if (!groupPath) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const storedGroupState = readStoredObject(collapsedNavGroupsKey);
|
||
|
|
storedGroupState[groupPath] = !group.classList.contains("expanded");
|
||
|
|
writeStoredObject(collapsedNavGroupsKey, storedGroupState);
|
||
|
|
applyNavGroupState();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function bindSidebarDismiss() {
|
||
|
|
document.addEventListener("click", function (event) {
|
||
|
|
if (!body.classList.contains("sidebar-open")) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const target = event.target;
|
||
|
|
if (!(target instanceof Element)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (sidebar?.contains(target) || sidebarToggle?.contains(target)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
closeSidebar();
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("keydown", function (event) {
|
||
|
|
if (event.key === "Escape") {
|
||
|
|
closeSidebar();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
document.querySelectorAll(".nav a:not([data-nav-toggle])").forEach(function (link) {
|
||
|
|
link.addEventListener("click", function () {
|
||
|
|
closeSidebar();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function bindModal() {
|
||
|
|
const modals = Array.from(document.querySelectorAll("[data-modal-root]"));
|
||
|
|
if (!modals.length) {
|
||
|
|
return {
|
||
|
|
openNamedModal: function () {},
|
||
|
|
closeNamedModal: function () {},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function openModal(modal) {
|
||
|
|
modals.forEach(function (item) {
|
||
|
|
item.hidden = true;
|
||
|
|
});
|
||
|
|
modal.hidden = false;
|
||
|
|
body.classList.add("modal-open");
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeModal(modal) {
|
||
|
|
modal.hidden = true;
|
||
|
|
if (modals.every(function (item) { return item.hidden; })) {
|
||
|
|
body.classList.remove("modal-open");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function openNamedModal(name) {
|
||
|
|
const modal = document.querySelector('[data-modal-root="' + name + '"]');
|
||
|
|
if (modal) {
|
||
|
|
openModal(modal);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeNamedModal(name) {
|
||
|
|
const modal = document.querySelector('[data-modal-root="' + name + '"]');
|
||
|
|
if (modal) {
|
||
|
|
closeModal(modal);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
document.querySelectorAll("[data-open-modal]").forEach(function (button) {
|
||
|
|
button.addEventListener("click", function (event) {
|
||
|
|
event.preventDefault();
|
||
|
|
const modalName = button.getAttribute("data-open-modal");
|
||
|
|
if (modalName) {
|
||
|
|
openNamedModal(modalName);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
modals.forEach(function (modal) {
|
||
|
|
modal.querySelectorAll("[data-modal-close]").forEach(function (button) {
|
||
|
|
button.addEventListener("click", function () {
|
||
|
|
closeModal(modal);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
modal.addEventListener("click", function (event) {
|
||
|
|
const target = event.target;
|
||
|
|
if (target instanceof Element && target.hasAttribute("data-modal-close")) {
|
||
|
|
closeModal(modal);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
modal.querySelectorAll("[data-mock-action]").forEach(function (button) {
|
||
|
|
button.addEventListener("click", function () {
|
||
|
|
window.setTimeout(function () {
|
||
|
|
closeModal(modal);
|
||
|
|
}, 0);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("keydown", function (event) {
|
||
|
|
if (event.key !== "Escape") {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
modals.forEach(function (modal) {
|
||
|
|
if (!modal.hidden) {
|
||
|
|
closeModal(modal);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
return {
|
||
|
|
openNamedModal: openNamedModal,
|
||
|
|
closeNamedModal: closeNamedModal,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function bindMockActions() {
|
||
|
|
document.querySelectorAll("[data-mock-action]").forEach(function (button) {
|
||
|
|
button.addEventListener("click", async function () {
|
||
|
|
const action = button.getAttribute("data-mock-action") || "mock-action";
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch("/api/mock/action", {
|
||
|
|
method: "POST",
|
||
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||
|
|
body: new URLSearchParams({ action: action }).toString(),
|
||
|
|
});
|
||
|
|
const payload = await parseJSONSafely(response);
|
||
|
|
showToast(payload.message || "동작을 처리했습니다.");
|
||
|
|
} catch (error) {
|
||
|
|
console.error(error);
|
||
|
|
showToast("동작 처리 중 오류가 발생했습니다.");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.querySelectorAll("[data-mock-form]").forEach(function (form) {
|
||
|
|
form.addEventListener("submit", async function (event) {
|
||
|
|
event.preventDefault();
|
||
|
|
|
||
|
|
const formData = new FormData(form);
|
||
|
|
if (!formData.get("action")) {
|
||
|
|
formData.set("action", "mock-form-submit");
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch(form.getAttribute("action") || "/api/mock/action", {
|
||
|
|
method: "POST",
|
||
|
|
body: new URLSearchParams(Array.from(formData.entries())),
|
||
|
|
});
|
||
|
|
const payload = await parseJSONSafely(response);
|
||
|
|
showToast(payload.message || "적용이 완료되었습니다.");
|
||
|
|
} catch (error) {
|
||
|
|
console.error(error);
|
||
|
|
showToast("처리 중 오류가 발생했습니다.");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function bindPortSelection() {
|
||
|
|
const portCards = Array.from(document.querySelectorAll("[data-port-card]"));
|
||
|
|
if (!portCards.length) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const currentPath = body.getAttribute("data-current-path") || window.location.pathname;
|
||
|
|
const currentPortMatch = currentPath.match(/^\/ports\/(\d+)\//);
|
||
|
|
const currentPort = currentPortMatch ? currentPortMatch[1] : "";
|
||
|
|
|
||
|
|
function readStoredTargets() {
|
||
|
|
try {
|
||
|
|
const parsed = JSON.parse(window.localStorage.getItem(portTargetsKey) || "[]");
|
||
|
|
if (!Array.isArray(parsed)) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
return parsed.map(function (value) {
|
||
|
|
return String(value);
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error(error);
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function writeStoredTargets() {
|
||
|
|
const selected = portCards
|
||
|
|
.map(function (card) {
|
||
|
|
return card.classList.contains("selected") ? card.getAttribute("data-port-card") : null;
|
||
|
|
})
|
||
|
|
.filter(Boolean);
|
||
|
|
window.localStorage.setItem(portTargetsKey, JSON.stringify(selected));
|
||
|
|
}
|
||
|
|
|
||
|
|
function syncCard(card) {
|
||
|
|
const checked = card.classList.contains("selected");
|
||
|
|
const number = card.getAttribute("data-port-card");
|
||
|
|
if (!number) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const shortcutButton = card.querySelector('[data-port-shortcut="' + number + '"]');
|
||
|
|
if (shortcutButton) {
|
||
|
|
shortcutButton.classList.toggle("selected", checked);
|
||
|
|
shortcutButton.textContent = checked ? "적용 대상 제외" : "적용 대상 추가";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const storedTargets = readStoredTargets();
|
||
|
|
portCards.forEach(function (card) {
|
||
|
|
const number = card.getAttribute("data-port-card");
|
||
|
|
card.classList.toggle("selected", Boolean(number && storedTargets.includes(number)));
|
||
|
|
if (number && number === currentPort) {
|
||
|
|
card.classList.add("active");
|
||
|
|
}
|
||
|
|
syncCard(card);
|
||
|
|
});
|
||
|
|
|
||
|
|
function togglePort(number) {
|
||
|
|
const card = document.querySelector('[data-port-card="' + number + '"]');
|
||
|
|
if (!card) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
card.classList.toggle("selected");
|
||
|
|
syncCard(card);
|
||
|
|
writeStoredTargets();
|
||
|
|
showToast(card.classList.contains("selected") ? ("Port " + number + " 추가됨") : ("Port " + number + " 제외됨"));
|
||
|
|
}
|
||
|
|
|
||
|
|
document.querySelectorAll("[data-port-shortcut]").forEach(function (button) {
|
||
|
|
button.addEventListener("click", function (event) {
|
||
|
|
event.preventDefault();
|
||
|
|
event.stopPropagation();
|
||
|
|
const number = button.getAttribute("data-port-shortcut");
|
||
|
|
if (number) {
|
||
|
|
togglePort(number);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.querySelectorAll("[data-port-view]").forEach(function (button) {
|
||
|
|
const number = button.getAttribute("data-port-view");
|
||
|
|
button.addEventListener("click", function () {
|
||
|
|
if (!number) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
writeStoredTargets();
|
||
|
|
window.location.assign("/ports/" + number + "/parameters");
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
writeStoredTargets();
|
||
|
|
}
|
||
|
|
|
||
|
|
function bindGroupManagement(modalApi) {
|
||
|
|
const groupModal = document.querySelector('[data-modal-root="group-create"]');
|
||
|
|
const groupForm = groupModal?.querySelector("[data-group-form]");
|
||
|
|
if (!groupModal || !groupForm) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const titleEl = groupModal.querySelector(".modal-header h2");
|
||
|
|
const messageEl = groupModal.querySelector(".modal-message");
|
||
|
|
const idInput = groupForm.querySelector('input[name="id"]');
|
||
|
|
const nameInput = groupForm.querySelector('input[name="name"]');
|
||
|
|
const descriptionInput = groupForm.querySelector('input[name="description"]');
|
||
|
|
const permissionInputs = Array.from(groupForm.querySelectorAll('input[name="permissions"]'));
|
||
|
|
const defaultState = {
|
||
|
|
title: titleEl?.textContent || "그룹 추가",
|
||
|
|
message: messageEl?.textContent || "",
|
||
|
|
};
|
||
|
|
|
||
|
|
function syncPermissionVisuals() {
|
||
|
|
permissionInputs.forEach(function (input) {
|
||
|
|
const chip = input.closest(".permission-chip");
|
||
|
|
if (chip) {
|
||
|
|
chip.classList.toggle("selected", Boolean(input.checked));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function resetGroupForm() {
|
||
|
|
if (idInput) {
|
||
|
|
idInput.value = "";
|
||
|
|
}
|
||
|
|
if (nameInput) {
|
||
|
|
nameInput.value = "";
|
||
|
|
}
|
||
|
|
if (descriptionInput) {
|
||
|
|
descriptionInput.value = "";
|
||
|
|
}
|
||
|
|
permissionInputs.forEach(function (input) {
|
||
|
|
input.checked = false;
|
||
|
|
});
|
||
|
|
if (titleEl) {
|
||
|
|
titleEl.textContent = defaultState.title;
|
||
|
|
}
|
||
|
|
if (messageEl) {
|
||
|
|
messageEl.textContent = defaultState.message;
|
||
|
|
}
|
||
|
|
syncPermissionVisuals();
|
||
|
|
}
|
||
|
|
|
||
|
|
function applyPermissions(permissions) {
|
||
|
|
const selected = new Set(Array.isArray(permissions) ? permissions : []);
|
||
|
|
permissionInputs.forEach(function (input) {
|
||
|
|
input.checked = selected.has(input.value);
|
||
|
|
});
|
||
|
|
syncPermissionVisuals();
|
||
|
|
}
|
||
|
|
|
||
|
|
permissionInputs.forEach(function (input) {
|
||
|
|
input.addEventListener("change", syncPermissionVisuals);
|
||
|
|
});
|
||
|
|
|
||
|
|
document.querySelectorAll('[data-open-modal="group-create"]').forEach(function (button) {
|
||
|
|
button.addEventListener("click", function () {
|
||
|
|
resetGroupForm();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.querySelectorAll("[data-group-edit]").forEach(function (button) {
|
||
|
|
button.addEventListener("click", async function () {
|
||
|
|
const id = button.getAttribute("data-group-edit");
|
||
|
|
if (!id) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch("/api/groups/" + encodeURIComponent(id));
|
||
|
|
const payload = await parseJSONSafely(response);
|
||
|
|
if (!response.ok || !payload.data) {
|
||
|
|
throw new Error(payload.message || "그룹 정보를 불러오지 못했습니다.");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (idInput) {
|
||
|
|
idInput.value = String(payload.data.id || "");
|
||
|
|
}
|
||
|
|
if (nameInput) {
|
||
|
|
nameInput.value = payload.data.name || "";
|
||
|
|
}
|
||
|
|
if (descriptionInput) {
|
||
|
|
descriptionInput.value = payload.data.description || "";
|
||
|
|
}
|
||
|
|
applyPermissions(payload.data.permissions);
|
||
|
|
if (titleEl) {
|
||
|
|
titleEl.textContent = "그룹 수정";
|
||
|
|
}
|
||
|
|
if (messageEl) {
|
||
|
|
messageEl.textContent = "사용자에게 연결된 그룹 권한을 수정합니다.";
|
||
|
|
}
|
||
|
|
|
||
|
|
modalApi.openNamedModal("group-create");
|
||
|
|
} catch (error) {
|
||
|
|
console.error(error);
|
||
|
|
showToast(error instanceof Error ? error.message : "그룹 정보를 불러오지 못했습니다.");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.querySelectorAll("[data-group-delete]").forEach(function (button) {
|
||
|
|
button.addEventListener("click", async function () {
|
||
|
|
const id = button.getAttribute("data-group-delete");
|
||
|
|
if (!id || !window.confirm("이 그룹을 삭제하시겠습니까?")) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch("/api/groups/" + encodeURIComponent(id), {
|
||
|
|
method: "DELETE",
|
||
|
|
});
|
||
|
|
const payload = await parseJSONSafely(response);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(payload.message || "그룹을 삭제하지 못했습니다.");
|
||
|
|
}
|
||
|
|
|
||
|
|
showToast(payload.message || "그룹을 삭제했습니다.");
|
||
|
|
window.location.reload();
|
||
|
|
} catch (error) {
|
||
|
|
console.error(error);
|
||
|
|
showToast(error instanceof Error ? error.message : "그룹을 삭제하지 못했습니다.");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
groupForm.addEventListener("submit", async function (event) {
|
||
|
|
event.preventDefault();
|
||
|
|
|
||
|
|
const groupID = idInput?.value.trim() || "";
|
||
|
|
const payload = {
|
||
|
|
name: nameInput?.value.trim() || "",
|
||
|
|
description: descriptionInput?.value.trim() || "",
|
||
|
|
permissions: permissionInputs.filter(function (input) {
|
||
|
|
return input.checked;
|
||
|
|
}).map(function (input) {
|
||
|
|
return input.value;
|
||
|
|
}),
|
||
|
|
};
|
||
|
|
const isEdit = groupID !== "";
|
||
|
|
const endpoint = isEdit ? "/api/groups/" + encodeURIComponent(groupID) : "/api/groups";
|
||
|
|
const method = isEdit ? "PUT" : "POST";
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch(endpoint, {
|
||
|
|
method: method,
|
||
|
|
headers: { "Content-Type": "application/json" },
|
||
|
|
body: JSON.stringify(payload),
|
||
|
|
});
|
||
|
|
const result = await parseJSONSafely(response);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(result.message || "그룹 저장에 실패했습니다.");
|
||
|
|
}
|
||
|
|
|
||
|
|
modalApi.closeNamedModal("group-create");
|
||
|
|
showToast(result.message || "그룹을 저장했습니다.");
|
||
|
|
window.location.reload();
|
||
|
|
} catch (error) {
|
||
|
|
console.error(error);
|
||
|
|
showToast(error instanceof Error ? error.message : "그룹 저장에 실패했습니다.");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
syncPermissionVisuals();
|
||
|
|
}
|
||
|
|
|
||
|
|
function bindUserManagement(modalApi) {
|
||
|
|
const userModal = document.querySelector('[data-modal-root="user-create"]');
|
||
|
|
const userForm = userModal?.querySelector("[data-user-form]");
|
||
|
|
if (!userModal || !userForm) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const titleEl = userModal.querySelector(".modal-header h2");
|
||
|
|
const messageEl = userModal.querySelector(".modal-message");
|
||
|
|
const idInput = userForm.querySelector('input[name="id"]');
|
||
|
|
const usernameInput = userForm.querySelector('input[name="username"]');
|
||
|
|
const passwordInput = userForm.querySelector('input[name="password"]');
|
||
|
|
const groupInput = userForm.querySelector('select[name="group"]');
|
||
|
|
const enabledInput = userForm.querySelector('select[name="enabled"]');
|
||
|
|
const defaultState = {
|
||
|
|
title: titleEl?.textContent || "사용자 추가",
|
||
|
|
message: messageEl?.textContent || "",
|
||
|
|
group: groupInput?.value || "administrator",
|
||
|
|
enabled: enabledInput?.value || "true",
|
||
|
|
};
|
||
|
|
|
||
|
|
function resetUserForm() {
|
||
|
|
if (idInput) {
|
||
|
|
idInput.value = "";
|
||
|
|
}
|
||
|
|
if (usernameInput) {
|
||
|
|
usernameInput.value = "";
|
||
|
|
}
|
||
|
|
if (passwordInput) {
|
||
|
|
passwordInput.value = "";
|
||
|
|
}
|
||
|
|
if (groupInput) {
|
||
|
|
groupInput.value = defaultState.group;
|
||
|
|
}
|
||
|
|
if (enabledInput) {
|
||
|
|
enabledInput.value = defaultState.enabled;
|
||
|
|
}
|
||
|
|
if (titleEl) {
|
||
|
|
titleEl.textContent = defaultState.title;
|
||
|
|
}
|
||
|
|
if (messageEl) {
|
||
|
|
messageEl.textContent = defaultState.message;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
document.querySelectorAll('[data-open-modal="user-create"]').forEach(function (button) {
|
||
|
|
button.addEventListener("click", function () {
|
||
|
|
resetUserForm();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.querySelectorAll("[data-user-edit]").forEach(function (button) {
|
||
|
|
button.addEventListener("click", async function () {
|
||
|
|
const id = button.getAttribute("data-user-edit");
|
||
|
|
if (!id) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch("/api/users/" + encodeURIComponent(id));
|
||
|
|
const payload = await parseJSONSafely(response);
|
||
|
|
if (!response.ok || !payload.data) {
|
||
|
|
throw new Error(payload.message || "사용자 정보를 불러오지 못했습니다.");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (idInput) {
|
||
|
|
idInput.value = String(payload.data.id || "");
|
||
|
|
}
|
||
|
|
if (usernameInput) {
|
||
|
|
usernameInput.value = payload.data.username || "";
|
||
|
|
}
|
||
|
|
if (passwordInput) {
|
||
|
|
passwordInput.value = "";
|
||
|
|
}
|
||
|
|
if (groupInput) {
|
||
|
|
groupInput.value = payload.data.group || defaultState.group;
|
||
|
|
}
|
||
|
|
if (enabledInput) {
|
||
|
|
enabledInput.value = String(payload.data.enabled !== false);
|
||
|
|
}
|
||
|
|
if (titleEl) {
|
||
|
|
titleEl.textContent = "사용자 수정";
|
||
|
|
}
|
||
|
|
if (messageEl) {
|
||
|
|
messageEl.textContent = "비밀번호를 비워두면 기존 값이 유지됩니다.";
|
||
|
|
}
|
||
|
|
|
||
|
|
modalApi.openNamedModal("user-create");
|
||
|
|
} catch (error) {
|
||
|
|
console.error(error);
|
||
|
|
showToast(error instanceof Error ? error.message : "사용자 정보를 불러오지 못했습니다.");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.querySelectorAll("[data-user-delete]").forEach(function (button) {
|
||
|
|
button.addEventListener("click", async function () {
|
||
|
|
const id = button.getAttribute("data-user-delete");
|
||
|
|
if (!id || !window.confirm("이 사용자를 삭제하시겠습니까?")) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch("/api/users/" + encodeURIComponent(id), {
|
||
|
|
method: "DELETE",
|
||
|
|
});
|
||
|
|
const payload = await parseJSONSafely(response);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(payload.message || "사용자를 삭제하지 못했습니다.");
|
||
|
|
}
|
||
|
|
|
||
|
|
showToast(payload.message || "사용자를 삭제했습니다.");
|
||
|
|
window.location.reload();
|
||
|
|
} catch (error) {
|
||
|
|
console.error(error);
|
||
|
|
showToast(error instanceof Error ? error.message : "사용자를 삭제하지 못했습니다.");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
userForm.addEventListener("submit", async function (event) {
|
||
|
|
event.preventDefault();
|
||
|
|
|
||
|
|
const userID = idInput?.value.trim() || "";
|
||
|
|
const payload = {
|
||
|
|
username: usernameInput?.value.trim() || "",
|
||
|
|
password: passwordInput?.value || "",
|
||
|
|
group: groupInput?.value || defaultState.group,
|
||
|
|
enabled: (enabledInput?.value || "true") === "true",
|
||
|
|
};
|
||
|
|
const isEdit = userID !== "";
|
||
|
|
const endpoint = isEdit ? "/api/users/" + encodeURIComponent(userID) : "/api/users";
|
||
|
|
const method = isEdit ? "PUT" : "POST";
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch(endpoint, {
|
||
|
|
method: method,
|
||
|
|
headers: { "Content-Type": "application/json" },
|
||
|
|
body: JSON.stringify(payload),
|
||
|
|
});
|
||
|
|
const result = await parseJSONSafely(response);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(result.message || "사용자 저장에 실패했습니다.");
|
||
|
|
}
|
||
|
|
|
||
|
|
modalApi.closeNamedModal("user-create");
|
||
|
|
showToast(result.message || "사용자를 저장했습니다.");
|
||
|
|
window.location.reload();
|
||
|
|
} catch (error) {
|
||
|
|
console.error(error);
|
||
|
|
showToast(error instanceof Error ? error.message : "사용자 저장에 실패했습니다.");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
window.localStorage.removeItem(legacyHiddenTabsKey);
|
||
|
|
renderWorkspaceTabs();
|
||
|
|
bindNavGroups();
|
||
|
|
bindSidebarDismiss();
|
||
|
|
const modalApi = bindModal();
|
||
|
|
bindMockActions();
|
||
|
|
bindPortSelection();
|
||
|
|
bindGroupManagement(modalApi);
|
||
|
|
bindUserManagement(modalApi);
|
||
|
|
})();
|