(function () { "use strict"; const region = document.getElementById("workspace-region"); const navigation = document.getElementById("workspace-navigation"); const navigationItems = document.getElementById("workspace-nav-items"); const toggle = document.getElementById("workspace-nav-toggle"); const toggleLabel = toggle && toggle.querySelector(".workspace-nav-toggle-label"); const stockTab = document.getElementById("workspace-stock-tab"); const settingsTab = document.getElementById("workspace-settings-tab"); const stockWorkspace = document.getElementById("stock-workspace"); const catalogWorkspace = document.getElementById("catalog-workspace"); const settingsWorkspace = document.getElementById("settings-workspace"); const marketTabs = document.getElementById("market-tabs"); const title = document.getElementById("workspace-title"); const titleIcon = document.querySelector(".workspace-heading-icon"); const shell = document.querySelector(".operator-shell"); const splitter = document.getElementById("workspace-splitter"); const playlistPanel = document.querySelector(".playlist-panel"); const brandWordmark = document.querySelector("img.brand-wordmark"); const brandWordmarkPlate = brandWordmark?.closest(".brand-wordmark-plate"); function showBrandWordmarkFallback() { brandWordmarkPlate?.classList.add("wordmark-fallback-active"); } if (brandWordmark) { brandWordmark.addEventListener("error", showBrandWordmarkFallback, { once: true }); if (brandWordmark.complete && brandWordmark.naturalWidth === 0) { showBrandWordmarkFallback(); } } if (!region || !navigation || !navigationItems || !toggle || !toggleLabel || !stockTab || !settingsTab || !stockWorkspace || !catalogWorkspace || !settingsWorkspace || !marketTabs || !title) { return; } const views = Object.freeze({ stock: Object.freeze({ view: stockWorkspace, title: "종목·컷" }), catalog: Object.freeze({ view: catalogWorkspace, title: "기타 그래픽" }), settings: Object.freeze({ view: settingsWorkspace, title: "설정" }) }); let activeWorkspace = "stock"; let activeMarketTabId = marketTabs.querySelector("button.active[data-tab-id]") ?.dataset.tabId || marketTabs.querySelector("button[data-tab-id]")?.dataset.tabId || null; let pendingKeyboardMarketTabId = null; let rememberWorkspaceChanges = false; let startPreferenceApplied = false; let splitterGesture = null; let scheduleWidthPercent = null; const lastWorkspaceStorageKey = "mbn-stock-webview.last-workspace.v1"; const scheduleWidthStorageKey = "mbn-stock-webview.schedule-width.v1"; const scheduleMinimumPercent = 34; const scheduleMaximumPercent = 48; const workspaceMinimumPixels = 780; const scheduleMinimumPixels = 600; const splitterPixels = 8; function readStoredValue(key) { try { return window.localStorage && window.localStorage.getItem(key); } catch (_) { return null; } } function writeStoredValue(key, value) { try { if (window.localStorage) window.localStorage.setItem(key, value); } catch (_) { // Presentation preferences must never interrupt operator input. } } function currentWorkspaceToken() { if (activeWorkspace !== "catalog") return activeWorkspace; return activeMarketTabId ? "catalog:" + activeMarketTabId : "stock"; } function rememberActiveWorkspace() { if (!rememberWorkspaceChanges) return; writeStoredValue(lastWorkspaceStorageKey, currentWorkspaceToken()); } function storedWorkspaceToken() { const token = readStoredValue(lastWorkspaceStorageKey); if (token === "stock" || token === "settings") return token; if (typeof token !== "string" || !token.startsWith("catalog:")) return null; const tabId = token.slice("catalog:".length); return marketButton(tabId) ? token : null; } function marketButtons() { return Array.from(marketTabs.querySelectorAll("button[data-tab-id]")); } function marketButton(tabId) { return marketButtons().find(function (button) { return button.dataset.tabId === tabId; }) || null; } function marketLabel(button) { const label = button && button.querySelector(".workspace-nav-label"); return (label && label.textContent.trim()) || button?.textContent.trim() || views.catalog.title; } function selectedMenuButton(name) { if (name === "stock") return stockTab; if (name === "settings") return settingsTab; if (name === "catalog") { return marketButton(activeMarketTabId) || marketButtons()[0] || null; } return null; } function syncHeadingIcon(button) { const source = button && button.querySelector("svg.workspace-nav-icon[data-icon]"); if (!titleIcon || !source) return; const next = source.cloneNode(true); next.removeAttribute("aria-hidden"); titleIcon.dataset.icon = source.dataset.icon || ""; titleIcon.replaceChildren(next); } function refreshCurrentMenu() { const selected = selectedMenuButton(activeWorkspace); Array.from(navigation.querySelectorAll(".workspace-nav-item")).forEach(function (button) { const isCurrent = button === selected; button.classList.toggle("workspace-current", isCurrent); button.setAttribute("aria-current", isCurrent ? "page" : "false"); }); region.dataset.activeWorkspace = activeWorkspace; if (activeMarketTabId) region.dataset.activeMarketTab = activeMarketTabId; } function setNavigationExpanded(expanded) { const isExpanded = expanded === true; region.classList.toggle("nav-collapsed", !isExpanded); toggle.setAttribute("aria-expanded", isExpanded ? "true" : "false"); toggle.setAttribute("aria-label", isExpanded ? "작업 메뉴 접기" : "작업 메뉴 펼치기"); toggle.title = isExpanded ? "메뉴 접기" : "메뉴 펼치기"; toggleLabel.textContent = isExpanded ? "메뉴 접기" : "메뉴 펼치기"; } function setWorkspace(name) { const selected = views[name]; if (!selected) return false; const selectedButton = selectedMenuButton(name); const currentFocus = document.activeElement; const focusWillBeHidden = Object.keys(views).some(function (key) { return key !== name && views[key].view.contains(currentFocus); }); if (focusWillBeHidden && selectedButton) selectedButton.focus(); Object.keys(views).forEach(function (key) { const entry = views[key]; const isCurrent = key === name; entry.view.hidden = !isCurrent; entry.view.toggleAttribute("inert", !isCurrent); entry.view.setAttribute("aria-hidden", isCurrent ? "false" : "true"); }); activeWorkspace = name; refreshCurrentMenu(); syncHeadingIcon(selectedButton); title.textContent = name === "catalog" ? marketLabel(selectedMenuButton(name)) : selected.title; rememberActiveWorkspace(); return true; } function applyStartPreference(mode) { if (startPreferenceApplied) return true; const token = mode === "lastWorkspace" ? storedWorkspaceToken() : "stock"; if (!token || token === "stock" || token === "settings") { startPreferenceApplied = true; return setWorkspace(token === "settings" ? "settings" : "stock"); } const tabId = token.slice("catalog:".length); const button = marketButton(tabId); if (!button || button.disabled) return false; startPreferenceApplied = true; if (activeMarketTabId === tabId) { syncMarket(tabId); return setWorkspace("catalog"); } button.click(); return true; } function numericScheduleBounds() { if (!shell) { return { minimum: scheduleMinimumPercent, maximum: scheduleMaximumPercent }; } const width = shell.getBoundingClientRect().width; if (!Number.isFinite(width) || width <= 0) { return { minimum: scheduleMinimumPercent, maximum: scheduleMaximumPercent }; } const minimum = Math.max( scheduleMinimumPercent, scheduleMinimumPixels / width * 100); const maximum = Math.min( scheduleMaximumPercent, (width - workspaceMinimumPixels - splitterPixels) / width * 100); return { minimum: Math.min(minimum, maximum), maximum: Math.max(minimum, maximum) }; } function clampScheduleWidth(value) { const bounds = numericScheduleBounds(); return Math.min(bounds.maximum, Math.max(bounds.minimum, value)); } function applyScheduleWidth(value, persist) { if (!shell || !splitter || !Number.isFinite(value)) return false; scheduleWidthPercent = clampScheduleWidth(value); const serialized = scheduleWidthPercent.toFixed(3) + "%"; shell.style.setProperty("--schedule-pane-width", serialized); const bounds = numericScheduleBounds(); const rounded = Math.round(scheduleWidthPercent); splitter.setAttribute("aria-valuemin", bounds.minimum.toFixed(1)); splitter.setAttribute("aria-valuemax", bounds.maximum.toFixed(1)); splitter.setAttribute("aria-valuenow", scheduleWidthPercent.toFixed(1)); splitter.setAttribute("aria-valuetext", "송출 스케줄 " + rounded + "%"); if (persist === true) { writeStoredValue(scheduleWidthStorageKey, scheduleWidthPercent.toFixed(3)); } return true; } function schedulePercentFromPointer(clientX) { const bounds = shell.getBoundingClientRect(); return (bounds.right - clientX) / bounds.width * 100; } function finishSplitterGesture(restore) { if (!splitterGesture || !splitter) return; const gesture = splitterGesture; splitterGesture = null; if (splitter.hasPointerCapture?.(gesture.pointerId)) { splitter.releasePointerCapture(gesture.pointerId); } document.body.classList.remove("resizing-workspace"); splitter.classList.remove("dragging"); applyScheduleWidth( restore === true ? gesture.startPercent : scheduleWidthPercent, restore !== true); } function initializeSplitter() { if (!shell || !splitter || !playlistPanel) return; const stored = Number.parseFloat(readStoredValue(scheduleWidthStorageKey)); let initial = Number.isFinite(stored) ? stored : null; if (initial === null) { const shellBounds = shell.getBoundingClientRect(); const scheduleBounds = playlistPanel.getBoundingClientRect(); initial = shellBounds.width > 0 ? scheduleBounds.width / shellBounds.width * 100 : 40; } applyScheduleWidth(initial, false); splitter.addEventListener("pointerdown", function (event) { if (event.button !== 0 || event.isPrimary === false || splitterGesture) return; splitterGesture = { pointerId: event.pointerId, startPercent: scheduleWidthPercent }; splitter.setPointerCapture?.(event.pointerId); splitter.classList.add("dragging"); document.body.classList.add("resizing-workspace"); splitter.focus({ preventScroll: true }); event.preventDefault(); }); splitter.addEventListener("pointermove", function (event) { if (!splitterGesture || splitterGesture.pointerId !== event.pointerId || (event.buttons & 1) === 0) return; applyScheduleWidth(schedulePercentFromPointer(event.clientX), false); event.preventDefault(); }); splitter.addEventListener("pointerup", function (event) { if (!splitterGesture || splitterGesture.pointerId !== event.pointerId) return; finishSplitterGesture(false); event.preventDefault(); }); splitter.addEventListener("pointercancel", function (event) { if (!splitterGesture || splitterGesture.pointerId !== event.pointerId) return; finishSplitterGesture(true); }); splitter.addEventListener("lostpointercapture", function (event) { if (!splitterGesture || splitterGesture.pointerId !== event.pointerId) return; finishSplitterGesture(false); }); splitter.addEventListener("keydown", function (event) { if (event.key === "Escape" && splitterGesture) { event.preventDefault(); event.stopImmediatePropagation(); finishSplitterGesture(true); return; } let next = scheduleWidthPercent; if (event.key === "ArrowLeft") next += 2; else if (event.key === "ArrowRight") next -= 2; else if (event.key === "Home") next = numericScheduleBounds().minimum; else if (event.key === "End") next = numericScheduleBounds().maximum; else return; event.preventDefault(); event.stopPropagation(); applyScheduleWidth(next, true); }); window.addEventListener("resize", function () { window.requestAnimationFrame(function () { applyScheduleWidth(scheduleWidthPercent, false); }); }); } function syncMarket(tabId, options) { const button = marketButton(tabId); if (!button) return false; activeMarketTabId = tabId; region.dataset.activeMarketTab = tabId; if (activeWorkspace === "catalog") { refreshCurrentMenu(); syncHeadingIcon(button); title.textContent = marketLabel(button); } const authoritativeNativeRender = options && options.authoritativeNativeRender === true; if (authoritativeNativeRender && pendingKeyboardMarketTabId && pendingKeyboardMarketTabId !== tabId) { // The native operation completed on a different tab. Do not carry a // failed or superseded keyboard request into a later render. pendingKeyboardMarketTabId = null; } if (authoritativeNativeRender && pendingKeyboardMarketTabId === tabId && !button.disabled) { const pendingTabId = pendingKeyboardMarketTabId; // Native loading temporarily disables the selected menu button, which // can move focus to . Only the final, non-busy native render may // consume this request; the optimistic capture-phase click must not. window.requestAnimationFrame(function () { if (pendingKeyboardMarketTabId !== pendingTabId) return; pendingKeyboardMarketTabId = null; const pendingButton = marketButton(pendingTabId); const active = document.activeElement; const focusWasLost = !active || active === document.body || active === document.documentElement; if (activeWorkspace === "catalog" && pendingButton && !pendingButton.disabled && focusWasLost) { pendingButton.focus(); } }); } return true; } function hasPendingKeyboardMarketFocus(tabId) { return pendingKeyboardMarketTabId !== null && (tabId === undefined || tabId === pendingKeyboardMarketTabId); } function select(name) { if (views[name]) return setWorkspace(name); if (!syncMarket(name)) return false; return setWorkspace("catalog"); } function focusAdjacentItem(direction) { const items = Array.from(navigation.querySelectorAll(".workspace-nav-item")) .filter(function (button) { return button.disabled !== true; }); if (items.length === 0) return; const currentIndex = items.indexOf(document.activeElement); const nextIndex = currentIndex < 0 ? 0 : (currentIndex + direction + items.length) % items.length; items[nextIndex].focus(); } toggle.addEventListener("click", function () { setNavigationExpanded(toggle.getAttribute("aria-expanded") !== "true"); }); stockTab.addEventListener("click", function () { startPreferenceApplied = true; setWorkspace("stock"); }); settingsTab.addEventListener("click", function () { startPreferenceApplied = true; setWorkspace("settings"); }); navigation.addEventListener("keydown", function (event) { const menuButton = event.target.closest && event.target.closest(".workspace-nav-item"); if (!menuButton) return; if (event.key === "Enter" || event.key === " ") { event.preventDefault(); event.stopPropagation(); const keyboardMarketTabId = menuButton.dataset.tabId || null; pendingKeyboardMarketTabId = keyboardMarketTabId; menuButton.click(); return; } if (event.key === "Tab") { pendingKeyboardMarketTabId = null; return; } if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return; event.preventDefault(); event.stopPropagation(); focusAdjacentItem(event.key === "ArrowUp" ? -1 : 1); }); document.addEventListener("pointerdown", function () { // A real pointer action transfers focus ownership to the user. A delayed // native response must never pull it back to the keyboard-selected menu. pendingKeyboardMarketTabId = null; }, true); // Reveal the selected catalog screen before app.js dispatches its native // select-tab intent. The native state response remains authoritative and // is reconciled through syncMarket after every render. marketTabs.addEventListener("click", function (event) { const button = event.target.closest("button[data-tab-id]"); if (!button || !marketTabs.contains(button)) return; startPreferenceApplied = true; syncMarket(button.dataset.tabId); setWorkspace("catalog"); }, true); setNavigationExpanded(true); setWorkspace("stock"); rememberWorkspaceChanges = true; initializeSplitter(); window.LegacyWorkspaceNavigation = Object.freeze({ select: select, syncMarket: syncMarket, hasPendingKeyboardMarketFocus: hasPendingKeyboardMarketFocus, setExpanded: setNavigationExpanded, applyStartPreference: applyStartPreference, active: function () { return activeWorkspace; }, activeMarket: function () { return activeMarketTabId; }, expanded: function () { return toggle.getAttribute("aria-expanded") === "true"; }, scheduleWidth: function () { return scheduleWidthPercent; } }); }());