feat: refine branded operator experience

Add the MBN-branded startup intro, semantic sidebar icons, and refined card-based operator visuals. Strengthen packaged UI and input smoke coverage for the updated experience.
This commit is contained in:
2026-07-24 01:26:13 +09:00
parent f43483533a
commit b050f2f06c
25 changed files with 2316 additions and 164 deletions

View File

@@ -4,6 +4,18 @@ import { createHash } from "node:crypto";
const exactUrl = "https://legacy-parity.mbn.local/index.html";
const allowedArguments = new Set(["port", "output", "screenshot", "timeout-ms"]);
const expectedWorkspaceIconByTabId = Object.freeze({
overseas: "overseas",
exchange: "exchange",
index: "index",
kospiIndustry: "kospi",
kosdaqIndustry: "kosdaq",
comparison: "comparison",
theme: "theme",
overseasStocks: "overseas-stocks",
expert: "expert",
tradingHalt: "trading-halt"
});
const forbiddenCdpMethods = new Set([
"Browser.close", "Page.close", "Target.closeTarget", "Runtime.terminateExecution",
"Input.dispatchMouseEvent", "Input.dispatchKeyEvent", "Input.insertText"
@@ -273,6 +285,39 @@ async function readShell() {
bounds.left < document.documentElement.clientWidth &&
bounds.top < document.documentElement.clientHeight &&
bounds.display !== "none" && bounds.visibility !== "hidden");
const icon = container => {
const svg = container?.matches?.("svg.workspace-nav-icon[data-icon]")
? container
: container?.querySelector?.("svg.workspace-nav-icon[data-icon]");
const bounds = rectangle(svg);
let contentBounds = null;
try {
const box = svg?.getBBox?.();
contentBounds = box ? {
x: box.x,
y: box.y,
width: box.width,
height: box.height
} : null;
} catch (_) {
contentBounds = null;
}
const style = svg ? getComputedStyle(svg) : null;
const visible = isVisible(svg, bounds);
const painted = Boolean(contentBounds &&
contentBounds.width > 0 && contentBounds.height > 0);
return {
key: svg?.dataset?.icon || null,
reference: svg?.querySelector("use")?.getAttribute("href") || null,
visible,
painted,
rendered: Boolean(visible && painted && style &&
Number.parseFloat(style.opacity) > 0 &&
(style.stroke !== "none" || style.fill !== "none")),
bounds,
contentBounds
};
};
const operatorShell = document.querySelector(".operator-shell");
const workspaceRegion = document.getElementById("workspace-region");
const navigationToggle = document.getElementById("workspace-nav-toggle");
@@ -282,8 +327,14 @@ async function readShell() {
const stockWorkspace = document.getElementById("stock-workspace");
const catalogWorkspace = document.getElementById("catalog-workspace");
const settingsWorkspace = document.getElementById("settings-workspace");
const headingIcon = document.querySelector(".workspace-heading-icon");
const splitter = document.getElementById("workspace-splitter");
const schedule = document.querySelector(".playlist-panel");
const brandWordmark = document.querySelector("img.brand-wordmark");
const brandProduct = document.querySelector(".brand-product");
const brandDivider = document.querySelector(".brand-divider");
const brandWordmarkBounds = rectangle(brandWordmark);
const brandDividerBounds = rectangle(brandDivider);
const shellBounds = rectangle(operatorShell);
const workspaceBounds = rectangle(workspaceRegion);
const splitterBounds = rectangle(splitter);
@@ -305,6 +356,17 @@ async function readShell() {
connectionText: text("playout-connection-state"),
playoutStatus: text("playout-status"),
title: document.title,
brand: {
source: brandWordmark?.getAttribute("src") || null,
alt: brandWordmark?.getAttribute("alt") || null,
complete: brandWordmark?.complete === true,
naturalWidth: brandWordmark?.naturalWidth || 0,
naturalHeight: brandWordmark?.naturalHeight || 0,
rendered: brandWordmarkBounds,
visible: isVisible(brandWordmark, brandWordmarkBounds),
dividerVisible: isVisible(brandDivider, brandDividerBounds),
product: brandProduct?.textContent?.trim() || null
},
controls: {
search: button("stock-search-button"),
prepare: button("playout-prepare"),
@@ -314,20 +376,24 @@ async function readShell() {
},
layout: {
activeWorkspace: workspaceRegion?.dataset?.activeWorkspace || null,
activeMarketTab: workspaceRegion?.dataset?.activeMarketTab || null,
navigationAriaExpanded: navigationToggle?.getAttribute("aria-expanded") || null,
navigationCollapsedClass: workspaceRegion?.classList.contains("nav-collapsed") === true,
navigationExpanded: navigationToggle?.getAttribute("aria-expanded") === "true" &&
workspaceRegion?.classList.contains("nav-collapsed") !== true,
stockTabCurrent: stockTab?.getAttribute("aria-current") || null,
stockTabIcon: icon(stockTab),
marketTabCount: marketTabs.length,
marketTabsCurrent: marketTabs.map(button => ({
tabId: button.dataset.tabId,
label: (button.querySelector(".workspace-nav-label")?.textContent || "").trim(),
current: button.getAttribute("aria-current") || null,
hasIcon: Boolean(button.querySelector("svg")),
icon: icon(button),
hasLabel: Boolean(button.querySelector(".workspace-nav-label"))
})),
settingsTabCurrent: settingsTab?.getAttribute("aria-current") || null,
settingsTabIcon: icon(settingsTab),
headingIcon: icon(headingIcon),
currentMenuItemCount: document.querySelectorAll(
"#workspace-navigation .workspace-nav-item[aria-current='page']").length,
stockWorkspaceVisible: isVisible(stockWorkspace, rectangle(stockWorkspace)) &&
@@ -389,6 +455,18 @@ function assertShell(shell) {
shell.postMessageGuardInstalled !== true || shell.observedMessagesAfterGuard.length !== 0 || shell.blockedMessages.length !== 0) {
fail("KNOWN_FAILURE", "The package shell did not remain rendered DryRun/IDLE with zero guarded native intents.");
}
if (shell.brand?.source !== "Brand/logo.png" ||
shell.brand.alt !== "매일경제TV" ||
shell.brand.complete !== true ||
shell.brand.naturalWidth !== 176 ||
shell.brand.naturalHeight !== 43 ||
shell.brand.visible !== true ||
shell.brand.dividerVisible !== true ||
shell.brand.rendered?.width < 140 ||
shell.brand.rendered?.height < 33 ||
shell.brand.product !== "V-STOCK") {
fail("KNOWN_FAILURE", "The package did not render the verified customer wordmark and V-STOCK lockup.");
}
const layout = shell.layout;
const currentMarkets = layout?.marketTabsCurrent?.filter(tab => tab.current === "page") || [];
const navigationExpansionConsistent = layout &&
@@ -396,7 +474,28 @@ function assertShell(shell) {
(layout.navigationAriaExpanded === "false" && layout.navigationCollapsedClass === true));
const commonNavigationOk = layout && navigationExpansionConsistent &&
layout.marketTabCount === 10 && layout.currentMenuItemCount === 1 &&
layout.marketTabsCurrent.every(tab => tab.hasIcon === true && tab.hasLabel === true);
layout.stockTabIcon?.key === "stock-cut" &&
layout.stockTabIcon.reference === "#workspace-icon-stock-cut" &&
layout.stockTabIcon.rendered === true &&
layout.settingsTabIcon?.key === "settings" &&
layout.settingsTabIcon.reference === "#workspace-icon-settings" &&
layout.settingsTabIcon.rendered === true &&
layout.marketTabsCurrent.every(tab => {
const expectedIcon = expectedWorkspaceIconByTabId[tab.tabId];
return Boolean(expectedIcon) && tab.hasLabel === true &&
tab.icon?.key === expectedIcon &&
tab.icon.reference === `#workspace-icon-${expectedIcon}` &&
tab.icon.rendered === true;
});
const expectedHeadingIcon = layout?.activeWorkspace === "stock"
? "stock-cut"
: layout?.activeWorkspace === "settings"
? "settings"
: expectedWorkspaceIconByTabId[layout?.activeMarketTab];
const headingIconOk = Boolean(expectedHeadingIcon) &&
layout?.headingIcon?.key === expectedHeadingIcon &&
layout.headingIcon.reference === `#workspace-icon-${expectedHeadingIcon}` &&
layout.headingIcon.rendered === true;
const stockWorkspaceOk = layout?.activeWorkspace === "stock" &&
layout.stockTabCurrent === "page" && layout.settingsTabCurrent === "false" &&
currentMarkets.length === 0 && layout.stockWorkspaceVisible === true &&
@@ -412,7 +511,7 @@ function assertShell(shell) {
currentMarkets.length === 1 && layout.stockWorkspaceHidden === true &&
layout.catalogWorkspaceVisible === true && layout.settingsWorkspaceHidden === true &&
layout.workspaceTitle === currentMarkets[0]?.label;
if (!commonNavigationOk ||
if (!commonNavigationOk || !headingIconOk ||
[stockWorkspaceOk, settingsWorkspaceOk, catalogWorkspaceOk].filter(Boolean).length !== 1) {
fail("KNOWN_FAILURE", "The package did not render one internally consistent initial workspace and menu selection.");
}