const csrfToken = document.body.dataset.csrfToken || ""; const hideInactiveUsersStorageKey = "consoleSuperadmin.hideInactiveUsers"; const consoleActionStorageKey = "consoleSuperadmin.selectedAction"; let latestUsersPayload = {users: []}; let selectedUserIds = new Set(); function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function fmtNumber(value, digits = 0) { const number = Number(value || 0); return Number.isFinite(number) ? number.toLocaleString("ko-KR", {maximumFractionDigits: digits}) : "-"; } function fmtKrw(value) { return `${fmtNumber(value, 0)} KRW`; } async function fetchJson(url, options = {}) { const response = await fetch(url, { cache: "no-store", credentials: "include", ...options, headers: { Accept: "application/json", ...(options.body ? {"Content-Type": "application/json"} : {}), ...(options.method && options.method !== "GET" ? {"X-CSRF-Token": csrfToken} : {}), ...(options.headers || {}), }, }); const data = await response.json().catch(() => ({})); if (!response.ok) { const detail = data.message || data.error || `HTTP ${response.status}`; const error = new Error(detail); error.status = response.status; error.payload = data; console.error("Console API request failed", {url, status: response.status, payload: data}); throw error; } return data; } function clearAuthStorage() { const authKeyPattern = /auth|oauth|token|session|user/i; for (const storage of [window.localStorage, window.sessionStorage]) { if (!storage) continue; const keys = []; for (let index = 0; index < storage.length; index += 1) { const key = storage.key(index); if (key && authKeyPattern.test(key)) keys.push(key); } keys.forEach((key) => storage.removeItem(key)); } } function describeError(error) { const payload = error?.payload || {}; const parts = [ payload.message || error?.message || "요청 처리에 실패했습니다.", payload.error ? `error=${payload.error}` : "", payload.operation ? `operation=${payload.operation}` : "", payload.max_assignable_krw !== undefined ? `max=${fmtNumber(payload.max_assignable_krw, 0)} KRW` : "", payload.requested_budget_krw !== undefined ? `requested=${fmtNumber(payload.requested_budget_krw, 0)} KRW` : "", ].filter(Boolean); return parts.join("\n"); } function formPayload(form) { const payload = Object.fromEntries(new FormData(form).entries()); for (const key of ["auto_trade_allowed"]) { if (key in payload) payload[key] = payload[key] === "true"; } for (const key of ["investment_krw", "allocation_pct", "amount", "budget_limit_krw"]) { if (key in payload) payload[key] = Number(payload[key] || 0); } return payload; } function selectedConsoleAction() { return document.getElementById("consoleActionSelect")?.value || "create_user"; } function syncConsoleActionForms() { const action = selectedConsoleAction(); localStorage.setItem(consoleActionStorageKey, action); document.querySelectorAll("[data-console-action-form]").forEach((element) => { const formAction = element.dataset.consoleActionForm; const shouldShow = formAction === action || (element.id === "cashflowQaForm" && ["deposit", "withdrawal"].includes(action)); element.hidden = !shouldShow; if (element.id === "cashflowQaForm" && shouldShow) { element.elements.type.value = action; } }); document.getElementById("accounts")?.classList.toggle("is-action-muted", action !== "account_manage"); document.getElementById("cashflow")?.classList.toggle("is-action-muted", !["deposit", "withdrawal"].includes(action)); document.getElementById("allocations")?.classList.toggle("is-action-muted", action !== "strategy_allocation"); updateBulkUserSelectionUi(); if (action === "strategy_allocation") refreshAllocationCapacity(); } function rowsOrEmpty(rows, colSpan) { return rows.length ? rows.join("") : `데이터가 없습니다.`; } function statusBadge(status) { const normalized = status === "inactive" ? "inactive" : "active"; return `${normalized === "inactive" ? "비활성" : "활성"}`; } function balanceCell(user) { const balance = Number(user.balance_amount || 0); const className = balance < 0 ? "balance-amount is-negative" : "balance-amount"; return `${fmtNumber(balance, 0)}`; } function shouldHideInactiveUsers() { return localStorage.getItem(hideInactiveUsersStorageKey) !== "false"; } function syncHideInactiveUsersCheckbox() { const checkbox = document.getElementById("hideInactiveUsersCheckbox"); if (checkbox) checkbox.checked = shouldHideInactiveUsers(); } function visibleSelectableUserIds() { return Array.from(document.querySelectorAll(".user-select-checkbox:not(:disabled)")).map((checkbox) => checkbox.value); } function updateBulkUserSelectionUi() { const visibleIds = visibleSelectableUserIds(); selectedUserIds = new Set(Array.from(selectedUserIds).filter((userId) => visibleIds.includes(userId))); document.querySelectorAll(".user-select-checkbox").forEach((checkbox) => { checkbox.checked = selectedUserIds.has(checkbox.value); }); const selectedCount = selectedUserIds.size; const countLabel = document.getElementById("selectedUsersCount"); if (countLabel) countLabel.textContent = `선택 ${selectedCount}명`; const bulkButton = document.getElementById("bulkDisableUsersButton"); if (bulkButton) bulkButton.disabled = selectedCount === 0; const selectAll = document.getElementById("selectAllUsersCheckbox"); if (selectAll) { selectAll.disabled = visibleIds.length === 0; selectAll.checked = visibleIds.length > 0 && selectedCount === visibleIds.length; selectAll.indeterminate = selectedCount > 0 && selectedCount < visibleIds.length; } } function showToast(message, tone = "success") { const region = document.getElementById("toastRegion"); if (!region) { window.alert(message); return; } const toast = document.createElement("div"); toast.className = `toast-message is-${tone}`; toast.textContent = message; region.appendChild(toast); window.setTimeout(() => toast.remove(), 5200); } function renderStatus(data) { document.getElementById("generatedAt").textContent = data.generated_at_kst || "-"; document.getElementById("realOrderFlag").textContent = data.runtime?.real_order_enabled ? "ENABLED" : "blocked/manual"; document.getElementById("topStrategiesStatus").textContent = (data.public_top_strategy_codes || []).join(", ") || "-"; document.getElementById("liveTransitionState").textContent = data.live_transition_status || "준비중/가능"; document.getElementById("apiStatusBody").textContent = JSON.stringify({ mode: data.mode, dry_run: data.dry_run, role: data.role, web_status: data.web_status, runtime: data.runtime, public_top_strategy_codes: data.public_top_strategy_codes, }, null, 2); } function renderUsers(data) { latestUsersPayload = data || {users: []}; const users = shouldHideInactiveUsers() ? (latestUsersPayload.users || []).filter((user) => user.status !== "inactive") : (latestUsersPayload.users || []); const rows = users.map((user) => ` ${escapeHtml(user.username || "-")}${escapeHtml(user.id || "")} ${escapeHtml(user.email || "")} ${escapeHtml(user.role || "-")} ${escapeHtml(user.approval_status || "-")} ${statusBadge(user.status)}${user.disabled_reason ? `${escapeHtml(user.disabled_reason)}` : ""} ${balanceCell(user)}입금 ${fmtNumber(user.deposit_total || 0)} / 출금 ${fmtNumber(user.withdrawal_total || 0)} ${escapeHtml(user.last_login_kst || "-")} ${escapeHtml(user.source || "-")} ${user.role === "super_admin" ? "-" : user.status === "inactive" ? `` : "-"} `); document.getElementById("usersBody").innerHTML = rowsOrEmpty(rows, 10); updateBulkUserSelectionUi(); } function renderAccounts(data) { const rows = (data.accounts || []).map((row) => ` ${escapeHtml(row.username || row.email || row.user_id || "-")} ${escapeHtml(row.account_status || "-")} ${row.auto_trade_allowed ? "허용" : "불가"} ${escapeHtml(row.note || "")} `); document.getElementById("accountsBody").innerHTML = rowsOrEmpty(rows, 4); } function renderCashflow(data) { const typeLabel = {deposit: "입금", withdrawal: "출금"}; const rows = (data.entries || []).map((row) => ` ${escapeHtml(typeLabel[row.type] || row.type || "-")} ${escapeHtml(row.username || "-")}예산 ${fmtKrw(row.budget_after_krw)} ${fmtKrw(row.amount_krw)} ${escapeHtml(row.date || "-")} ${escapeHtml(row.note || "")} `); document.getElementById("cashflowBody").innerHTML = rowsOrEmpty(rows, 5); } function strategyName(row) { return row.strategy_display_name_ko || row.display_name || row.strategy_name || row.strategy_code || "-"; } function renderStrategies(data) { const rows = data.strategies || []; const liveRows = rows.filter((row) => row.strategy_code === "live_strategy1_btc" || row.actual_strategy_code === "live_strategy1_btc"); const tempRows = rows.filter((row) => row.is_temp || row.is_admin_only || row.publish_status === "temp"); document.getElementById("strategyCount").textContent = rows.length; document.getElementById("liveStrategyStatus").textContent = liveRows.length ? "표시됨" : "미표시"; document.getElementById("liveStrategiesBody").innerHTML = rowsOrEmpty(liveRows.map((row) => ` ${escapeHtml(strategyName(row))} ${escapeHtml(row.strategy_code)} ${escapeHtml(row.market)} ${escapeHtml(row.publish_status)} ${escapeHtml(row.visibility)} ${escapeHtml(row.public_simulation_code || "-")} ${row.live_enabled ? "true" : "false"} ${row.real_order_flag ? "ENABLED" : "blocked/manual"} ${escapeHtml(row.runner_heartbeat || "-")} ${fmtNumber(row.today_buy_count)} / ${fmtNumber(row.today_sell_count)} ${fmtKrw(row.cumulative_realized_pnl_krw)} ${fmtNumber(row.profit_krw_per_minute || row.avg_profit_krw_per_minute, 4)} ${fmtNumber(row.cumulative_return_pct, 3)}% `), 13); document.getElementById("tempStrategiesBody").innerHTML = rowsOrEmpty(tempRows.map((row) => ` ${escapeHtml(strategyName(row))} ${escapeHtml(row.strategy_code)} 임시사용 ${escapeHtml(row.visibility || "admin_only")} ${escapeHtml(row.public_simulation_code || row.linked_live_strategy_code || "-")} live runner 자동 연결 없음 `), 6); } function renderAllocations(data) { const rows = (data.settings || []).map((row) => ` ${escapeHtml(row.username || "-")} ${escapeHtml(row.strategy_code || "-")} ${escapeHtml(row.investment_start_date || "-")} ${fmtKrw(row.budget_limit_krw)} ${escapeHtml(row.parameters_json || "{}")} ${escapeHtml(row.updated_at_kst || "-")} `); document.getElementById("allocationsBody").innerHTML = rowsOrEmpty(rows, 6); } function renderAllocationCapacity(data = {}) { const isOk = data.status !== "error" && !data.error; document.getElementById("upbitAvailableKrw").textContent = isOk ? fmtKrw(data.upbit_available_krw) : "조회 실패"; document.getElementById("alreadyAllocatedKrw").textContent = isOk ? fmtKrw(data.already_allocated_krw) : "-"; document.getElementById("remainingAssignableKrw").textContent = isOk ? fmtKrw(data.remaining_assignable_krw) : "-"; document.getElementById("selectedUserBalanceKrw").textContent = isOk ? fmtKrw(data.user_balance_krw) : "-"; document.getElementById("selectedUserMaxAssignableKrw").textContent = isOk ? fmtKrw(data.max_assignable_krw) : "-"; const form = document.getElementById("allocationQaForm"); const button = form?.querySelector("button[type='submit']"); const requested = Number(form?.elements.budget_limit_krw?.value || 0); const exceeds = isOk && requested > Number(data.max_assignable_krw || 0); if (button) { button.disabled = !isOk || exceeds; button.title = !isOk ? "업비트 주문가능 금액 조회 실패 시 저장할 수 없습니다." : exceeds ? "배정 가능 금액을 초과했습니다." : ""; } } async function refreshAllocationCapacity() { const form = document.getElementById("allocationQaForm"); const params = new URLSearchParams({ username: form?.elements.username?.value || "", strategy_code: form?.elements.strategy_code?.value || "live_strategy1_btc", budget_limit_krw: form?.elements.budget_limit_krw?.value || "0", }); try { renderAllocationCapacity(await fetchJson(`/api/superadmin/user-strategy-allocation-capacity?${params.toString()}`)); } catch (error) { renderAllocationCapacity({status: "error", error: error.message}); } } function renderInvestorStrategySummary(data) { const summary = data.summary || {}; document.getElementById("investorTotalDeposit").textContent = fmtKrw(summary.total_deposit_krw); document.getElementById("investorTotalAllocated").textContent = fmtKrw(summary.total_allocated_krw); document.getElementById("investorTotalValuation").textContent = fmtKrw(summary.total_valuation_krw); document.getElementById("investorTotalPnl").textContent = fmtKrw(summary.total_pnl_krw); document.getElementById("investorAverageReturn").textContent = `${fmtNumber(summary.average_return_pct, 3)}%`; const tableHead = document.querySelector(".investor-summary-table thead"); if (tableHead) { tableHead.innerHTML = ` 투자자 전략 전략 배정 금액 현재가치 실현손익 미실현손익 총손익 수익률 상태 `; } const rows = (data.rows || []).map((row) => ` ${escapeHtml(row.investor_name || row.investor_id || "-")}${escapeHtml(row.investor_id || "")} ${escapeHtml(row.strategy_name || row.strategy_code || "-")}${escapeHtml(row.strategy_code || "")} ${fmtKrw(row.strategy_investment_krw || row.allocated_amount_krw)} ${fmtKrw(row.current_valuation_krw)} ${fmtKrw(row.realized_pnl_krw)} ${fmtKrw(row.unrealized_pnl_krw)} ${fmtKrw(row.total_pnl_krw)} ${fmtNumber(row.return_pct, 3)}% ${row.real_trade_active ? "활성" : "비활성"}${escapeHtml(row.recent_trade_at_kst || "")} `); document.getElementById("investorStrategySummaryBody").innerHTML = rowsOrEmpty(rows, 9); } function renderDailyPnl(data) { const rows = (data.rows || []).map((row) => ` ${escapeHtml(row.date || "-")} ${escapeHtml(row.username || "-")} ${escapeHtml(row.strategy_code || "-")} ${fmtKrw(row.daily_pnl_krw)} ${fmtKrw(row.cumulative_pnl_krw)} ${fmtNumber(row.daily_return_pct, 3)}% ${fmtNumber(row.cumulative_return_pct, 3)}% `); document.getElementById("dailyPnlBody").innerHTML = rowsOrEmpty(rows, 7); } function renderControl(data) { document.getElementById("controlStatus").textContent = JSON.stringify(data.control || data, null, 2); } function renderTrades(data) { const rows = (data.trades || []).map((row) => { const funds = Number(row.funds || 0); const pnl = Number(row.pnl_krw || 0); const cumulative = Number(row.cumulative_realized_pnl_krw || 0); const returnPct = funds > 0 ? (cumulative / funds * 100) : Number(row.pnl_pct || 0); return ` ${escapeHtml(row.order_uuid || row.position_uuid || row.id || "-")} ${escapeHtml(row.event_time_kst || "-")} ${escapeHtml(row.market || "-")} ${escapeHtml(row.event_type || "-")} ${fmtKrw(row.funds)} ${fmtKrw(row.price && row.qty ? Number(row.price) * Number(row.qty) : row.funds)} ${fmtKrw(pnl)} ${fmtKrw(cumulative)} ${fmtNumber(row.profit_per_hour_krw, 4)} ${fmtNumber(returnPct, 3)}% `; }); document.getElementById("tradesBody").innerHTML = rowsOrEmpty(rows, 10); } function renderPnl(data) { const rows = (data.pnl || []).map((row) => ` ${escapeHtml(row.strategy_code || "-")} ${escapeHtml(row.market || "-")} ${fmtKrw(row.cumulative_realized_pnl_krw)} ${fmtKrw(row.cumulative_buy_funds_krw)} ${fmtNumber(row.profit_krw_per_minute || row.avg_profit_krw_per_minute, 4)} ${fmtNumber(row.cumulative_return_pct, 3)}% ${fmtNumber(row.realized_trade_count || row.buy_count)} ${escapeHtml(row.last_event_time_kst || "-")} `); document.getElementById("pnlBody").innerHTML = rowsOrEmpty(rows, 8); } function renderLogs(data) { const logs = data.logs || []; document.getElementById("logsBody").innerHTML = logs.length ? logs.map((log) => `
${escapeHtml(log.name)}${escapeHtml(log.last_write_time)} · ${fmtNumber(log.length)} bytes
${escapeHtml(log.preview || "")}
`).join("") : "

로그가 없습니다.

"; } async function refreshTrades(event) { event?.preventDefault(); const form = document.getElementById("tradeFilterForm"); const params = new URLSearchParams(new FormData(form)); const data = await fetchJson(`/api/console/superadmin/trades?${params.toString()}`); renderTrades(data); } async function refreshDailyPnl(event) { event?.preventDefault(); const form = document.getElementById("dailyPnlFilterForm"); const params = new URLSearchParams(new FormData(form)); const data = await fetchJson(`/api/superadmin/user-strategy-daily-pnl?${params.toString()}`); renderDailyPnl(data); } async function refreshAll() { const [status, users, accounts, cashflow, strategies, allocations, investorSummary, dailyPnl, control, trades, pnl, logs] = await Promise.all([ fetchJson("/api/console/superadmin/status"), fetchJson("/api/superadmin/users"), fetchJson("/api/console/superadmin/accounts"), fetchJson("/api/superadmin/user-deposits-withdrawals"), fetchJson("/api/console/superadmin/strategies"), fetchJson("/api/superadmin/user-strategy-settings"), fetchJson("/api/superadmin/investor-strategy-summary"), fetchJson("/api/superadmin/user-strategy-daily-pnl?strategy_code=live_strategy1_btc"), fetchJson("/api/console/superadmin/control-status"), fetchJson("/api/console/superadmin/trades?strategy=live_strategy1_btc&market=KRW-BTC&run_mode=real&limit=100"), fetchJson("/api/console/superadmin/pnl?strategy=live_strategy1_btc&market=KRW-BTC"), fetchJson("/api/console/superadmin/logs"), ]); renderStatus(status); renderUsers(users); renderAccounts(accounts); renderCashflow(cashflow); renderStrategies(strategies); renderAllocations(allocations); renderInvestorStrategySummary(investorSummary); renderDailyPnl(dailyPnl); renderControl(control); renderTrades(trades); renderPnl(pnl); renderLogs(logs); await refreshAllocationCapacity(); } function bindDryRunForm(formId, endpoint, refreshFn) { document.getElementById(formId)?.addEventListener("submit", async (event) => { event.preventDefault(); const form = event.currentTarget; const button = form.querySelector("button[type='submit']"); button.disabled = true; try { const payload = formPayload(form); payload.action = form.dataset.consoleActionForm || selectedConsoleAction(); await fetchJson(endpoint, {method: "POST", body: JSON.stringify(payload)}); form.reset(); await refreshFn(); } catch (error) { window.alert(`저장 실패:\n${describeError(error)}`); } finally { button.disabled = false; } }); } async function refreshUsersOnly() { renderUsers(await fetchJson("/api/superadmin/users")); } async function submitBulkDisableUsers(reason) { const userIds = Array.from(selectedUserIds); if (!userIds.length) throw new Error("selected_users_required"); const result = await fetchJson("/api/superadmin/console-action", { method: "POST", body: JSON.stringify({action: "disable_users", user_ids: userIds, reason}), }); const disabledCount = Number(result.disabled_count || (result.disabled_users || []).length || 0); const skipped = result.skipped || []; selectedUserIds = new Set(); await refreshUsersOnly(); if (skipped.length) { showToast(`비활성화 ${disabledCount}명 완료, ${skipped.length}명 스킵됨`, "warning"); } else { showToast(`비활성화 ${disabledCount}명 완료`); } return result; } document.getElementById("usersBody")?.addEventListener("click", async (event) => { const enableButton = event.target.closest("[data-enable-user]"); if (!enableButton) return; enableButton.disabled = true; try { await fetchJson("/api/superadmin/users/enable", { method: "POST", body: JSON.stringify({user_id: enableButton.dataset.enableUser}), }); selectedUserIds.delete(enableButton.dataset.enableUser); showToast("사용자를 활성화했습니다."); await refreshUsersOnly(); } catch (error) { showToast(`사용자 활성화 실패: ${error.message}`, "warning"); } finally { enableButton.disabled = false; } }); document.getElementById("usersBody")?.addEventListener("change", (event) => { const checkbox = event.target.closest(".user-select-checkbox"); if (!checkbox) return; if (checkbox.checked) selectedUserIds.add(checkbox.value); else selectedUserIds.delete(checkbox.value); updateBulkUserSelectionUi(); }); document.getElementById("selectAllUsersCheckbox")?.addEventListener("change", (event) => { const visibleIds = visibleSelectableUserIds(); selectedUserIds = event.currentTarget.checked ? new Set(visibleIds) : new Set(); updateBulkUserSelectionUi(); }); document.getElementById("bulkDisableUsersButton")?.addEventListener("click", () => { if (selectedUserIds.size === 0) return; const dialog = document.getElementById("disableUserDialog"); const form = document.getElementById("disableUserForm"); form?.reset(); const countLabel = document.getElementById("disableUserSelectedCount"); if (countLabel) countLabel.textContent = `선택 ${selectedUserIds.size}명`; if (dialog?.showModal) { dialog.showModal(); form?.elements.reason?.focus(); return; } const reason = window.prompt(`선택한 유저 ${selectedUserIds.size}명을 비활성화하시겠습니까?\n비활성화 사유를 입력하세요.`); if (!reason) return; submitBulkDisableUsers(reason); }); document.querySelector("[data-close-disable-dialog]")?.addEventListener("click", () => { document.getElementById("disableUserDialog")?.close(); }); document.getElementById("disableUserForm")?.addEventListener("submit", async (event) => { event.preventDefault(); const form = event.currentTarget; const button = form.querySelector("button[type='submit']"); button.disabled = true; try { await submitBulkDisableUsers(formPayload(form).reason || ""); document.getElementById("disableUserDialog")?.close(); form.reset(); } catch (error) { showToast(`사용자 비활성화 실패: ${error.message}`, "warning"); } finally { button.disabled = false; } }); syncHideInactiveUsersCheckbox(); document.getElementById("hideInactiveUsersCheckbox")?.addEventListener("change", (event) => { localStorage.setItem(hideInactiveUsersStorageKey, event.currentTarget.checked ? "true" : "false"); renderUsers(latestUsersPayload); }); document.getElementById("allocationQaForm")?.addEventListener("input", () => { refreshAllocationCapacity(); }); document.getElementById("consoleActionSelect")?.addEventListener("change", syncConsoleActionForms); document.getElementById("refreshConsoleButton")?.addEventListener("click", refreshAll); document.getElementById("consoleLogoutLink")?.addEventListener("click", async (event) => { event.preventDefault(); clearAuthStorage(); let loginUrl = "/auth/google?next=/console"; try { const result = await fetchJson("/api/auth/logout", {method: "POST"}); loginUrl = result.login_url || loginUrl; } catch (error) { console.error("Console logout failed; clearing local auth state anyway.", error); } finally { clearAuthStorage(); window.location.replace(loginUrl); } }); document.getElementById("tradeFilterForm")?.addEventListener("submit", refreshTrades); document.getElementById("dailyPnlFilterForm")?.addEventListener("submit", refreshDailyPnl); document.querySelectorAll("[data-control-action]").forEach((button) => { button.addEventListener("click", async () => { button.disabled = true; try { const data = await fetchJson("/api/console/superadmin/control-status", { method: "POST", body: JSON.stringify({action: button.dataset.controlAction}), }); renderControl(data); } finally { button.disabled = false; } }); }); bindDryRunForm("userQaForm", "/api/superadmin/console-action", async () => renderUsers(await fetchJson("/api/superadmin/users"))); bindDryRunForm("accountQaForm", "/api/superadmin/console-action", async () => renderAccounts(await fetchJson("/api/console/superadmin/accounts"))); document.getElementById("cashflowQaForm")?.addEventListener("submit", async (event) => { event.preventDefault(); const form = event.currentTarget; const payload = formPayload(form); payload.action = payload.type === "withdrawal" ? "withdrawal" : "deposit"; const button = form.querySelector("button[type='submit']"); button.disabled = true; try { await fetchJson("/api/superadmin/console-action", {method: "POST", body: JSON.stringify(payload)}); form.reset(); renderCashflow(await fetchJson("/api/superadmin/user-deposits-withdrawals")); renderInvestorStrategySummary(await fetchJson("/api/superadmin/investor-strategy-summary")); await refreshUsersOnly(); await refreshAllocationCapacity(); } catch (error) { window.alert(`입출금 저장 실패:\n${describeError(error)}`); } finally { button.disabled = false; } }); bindDryRunForm("allocationQaForm", "/api/superadmin/console-action", async () => { renderAllocations(await fetchJson("/api/superadmin/user-strategy-settings")); renderDailyPnl(await fetchJson("/api/superadmin/user-strategy-daily-pnl?strategy_code=live_strategy1_btc")); await refreshAllocationCapacity(); }); const storedAction = localStorage.getItem(consoleActionStorageKey); if (storedAction && document.getElementById("consoleActionSelect")) { document.getElementById("consoleActionSelect").value = storedAction; } syncConsoleActionForms(); refreshAll().catch((error) => { document.getElementById("apiStatusBody").textContent = `콘솔 데이터를 불러오지 못했습니다.\n${describeError(error)}`; });