(function (root, factory) { "use strict"; const api = Object.freeze(factory()); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnThemeWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function () { "use strict"; const DEFAULT_FADE_DURATION = 6; const SCHEMA_VERSION = 1; const MAXIMUM_PAGE_COUNT = 20; const MAXIMUM_PREVIEW_ITEMS = 240; const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/; // ThemeA always generated new codes as D8, but the production SB_LIST and // historical PLAY_LIST still contain original three- and four-digit codes. const themeCodePattern = /^[0-9]{3,8}$/; const controlCharacterPattern = /[\u0000-\u001f\u007f]/; const sourceContract = Object.freeze({ originalControl: "Control/UC4.cs", krxResolverGroup: "PAGED_THEME", nxtResolverGroup: "PAGED_NXT_THEME", maximumPageCount: MAXIMUM_PAGE_COUNT, maximumPreviewItems: MAXIMUM_PREVIEW_ITEMS }); const themeSorts = Object.freeze([ Object.freeze({ id: "INPUT_ORDER", label: "입력순" }), Object.freeze({ id: "GAIN_DESC", label: "상승률순" }), Object.freeze({ id: "GAIN_ASC", label: "하락률순" }) ]); const themeSortsById = new Map(themeSorts.map(value => [value.id, value])); const nxtSessions = Object.freeze([ Object.freeze({ id: "PRE_MARKET", label: "프리마켓" }), Object.freeze({ id: "AFTER_MARKET", label: "애프터마켓" }) ]); function action(id, label, builderKey, code, pageSize, valueKind) { return Object.freeze({ id, label, builderKey, code, aliases: Object.freeze([code]), pageSize, valueKind }); } const actions = Object.freeze([ action("five-row-current", "5단 표그래프 · 현재가", "s5074", "5074", 5, "CURRENT"), action("five-row-expected", "5단 표그래프 · 예상체결가", "s5074", "5074", 5, "EXPECTED"), action("six-row-current", "6종목 현재가", "s5077", "5077", 6, "CURRENT"), action("twelve-row-current", "12종목 현재가", "s5088", "5088", 12, "CURRENT") ]); const actionsById = new Map(actions.map(value => [value.id, value])); function cleanText(value, maximumLength = 128) { if (typeof value !== "string") return null; const text = value.trim(); return text && text.length <= maximumLength && !controlCharacterPattern.test(text) ? text : null; } function normalizeMarket(value) { const text = String(value || "").trim().toLocaleLowerCase("en-US"); return text === "krx" || text === "nxt" ? text : null; } function normalizeNxtSession(value) { const text = String(value || "") .trim() .replace(/([a-z])([A-Z])/g, "$1_$2") .replace(/-/g, "_") .toLocaleUpperCase("en-US"); return text === "PRE_MARKET" || text === "AFTER_MARKET" ? text : null; } function isKrxEmptySession(value) { if (value === undefined || value === null || value === "") return true; const text = String(value) .trim() .replace(/([a-z])([A-Z])/g, "$1_$2") .replace(/-/g, "_") .toLocaleUpperCase("en-US"); return text === "NOT_APPLICABLE"; } function themeValidationReason(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return "invalid-theme"; const market = normalizeMarket(value.market ?? value.Market); if (!market) return "invalid-theme-market"; const rawThemeCode = value.themeCode ?? value.ThemeCode ?? value.code; const themeCode = cleanText(rawThemeCode, 8); if (!themeCode || themeCode !== rawThemeCode || !themeCodePattern.test(themeCode)) { return "invalid-theme-code"; } const title = cleanText(value.title ?? value.themeTitle ?? value.ThemeTitle); if (!title || title !== String(value.title ?? value.themeTitle ?? value.ThemeTitle) || title.includes("(NXT)")) return "invalid-theme-title"; const sessionValue = value.session ?? value.Session; if (market === "nxt" && !normalizeNxtSession(sessionValue)) return "nxt-session-required"; if (market === "krx" && !isKrxEmptySession(sessionValue)) return "krx-session-must-be-empty"; const itemCountValue = value.itemCount ?? value.previewItemCount ?? (Array.isArray(value.items) ? value.items.length : null); if (itemCountValue !== null && itemCountValue !== undefined && (!Number.isInteger(itemCountValue) || itemCountValue < 0 || itemCountValue > MAXIMUM_PREVIEW_ITEMS)) { return "invalid-theme-item-count"; } const canonicalDisplay = market === "nxt" ? `${title}(NXT)` : title; if (value.displayTitle !== undefined && value.displayTitle !== canonicalDisplay) { return "invalid-theme-display-title"; } return null; } function normalizeTheme(value) { if (themeValidationReason(value)) return null; const market = normalizeMarket(value.market ?? value.Market); const themeCode = value.themeCode ?? value.ThemeCode ?? value.code; const title = value.title ?? value.themeTitle ?? value.ThemeTitle; const itemCountValue = value.itemCount ?? value.previewItemCount ?? (Array.isArray(value.items) ? value.items.length : null); const session = market === "nxt" ? normalizeNxtSession(value.session ?? value.Session) : null; return Object.freeze({ market, themeCode, title, displayTitle: market === "nxt" ? `${title}(NXT)` : title, session, itemCount: itemCountValue === undefined ? null : itemCountValue }); } function normalizeThemeSort(value) { if (value && typeof value === "object" && !Array.isArray(value)) value = value.id; const id = String(value || "").trim().toLocaleUpperCase("en-US"); return themeSortsById.get(id) ?? null; } function unsupported(actionId, reason) { return Object.freeze({ actionId, supported: false, reason }); } function getCompatibility(actionId, themeValue, sortValue) { const selectedAction = actionsById.get(actionId); if (!selectedAction) return unsupported(actionId, "unknown-action"); const reason = themeValidationReason(themeValue); if (reason) return unsupported(actionId, reason); const theme = normalizeTheme(themeValue); if (!normalizeThemeSort(sortValue)) return unsupported(actionId, "invalid-theme-sort"); if (theme.itemCount === 0) return unsupported(actionId, "theme-preview-is-empty"); if (selectedAction.valueKind === "EXPECTED" && theme.market !== "krx") { return unsupported(actionId, "expected-theme-is-krx-only"); } return Object.freeze({ actionId, supported: true, reason: null }); } function isActionAllowed(actionId, themeValue, sortValue) { return getCompatibility(actionId, themeValue, sortValue).supported; } function calculatePagePreview(actionId, themeValue) { const selectedAction = actionsById.get(actionId); const theme = normalizeTheme(themeValue); if (!selectedAction || !theme) return null; const maximumItems = selectedAction.pageSize * MAXIMUM_PAGE_COUNT; if (theme.itemCount === null) { return Object.freeze({ itemCount: null, accessibleItemCount: null, pageSize: selectedAction.pageSize, pageCount: null, maximumPageCount: MAXIMUM_PAGE_COUNT, isTruncated: false }); } const accessibleItemCount = Math.min(theme.itemCount, maximumItems); return Object.freeze({ itemCount: theme.itemCount, accessibleItemCount, pageSize: selectedAction.pageSize, pageCount: Math.ceil(accessibleItemCount / selectedAction.pageSize), maximumPageCount: MAXIMUM_PAGE_COUNT, isTruncated: theme.itemCount > maximumItems }); } function buildThemeSelection(actionId, themeValue, sortValue) { const selectedAction = actionsById.get(actionId); const theme = normalizeTheme(themeValue); const sort = normalizeThemeSort(sortValue); const compatibility = getCompatibility(actionId, themeValue, sortValue); if (!selectedAction || !theme || !sort || !compatibility.supported) { throw new RangeError(`The theme action is unavailable: ${compatibility.reason || "invalid-selection"}.`); } const selection = theme.market === "krx" ? Object.freeze({ groupCode: "PAGED_THEME", subject: theme.title, graphicType: sort.id, subtype: selectedAction.valueKind, dataCode: theme.themeCode }) : Object.freeze({ groupCode: "PAGED_NXT_THEME", subject: theme.displayTitle, graphicType: theme.session, subtype: sort.id, dataCode: theme.themeCode }); return Object.freeze({ builderKey: selectedAction.builderKey, code: selectedAction.code, aliases: selectedAction.aliases, selection, pageSize: selectedAction.pageSize, pagePreview: calculatePagePreview(actionId, theme) }); } function requireOptions(options) { if (!options || typeof options !== "object" || Array.isArray(options)) { throw new TypeError("Theme playlist options are required."); } const id = cleanText(options.id); if (!id || !identifierPattern.test(id)) throw new TypeError("A safe theme playlist id is required."); const fadeDuration = options.fadeDuration === undefined ? DEFAULT_FADE_DURATION : Number(options.fadeDuration); if (!Number.isInteger(fadeDuration) || fadeDuration < 0 || fadeDuration > 60) { throw new RangeError("Fade duration must be an integer from 0 through 60."); } return { id, fadeDuration }; } function createThemePlaylistEntry(actionId, themeValue, sortValue, options) { const selectedAction = actionsById.get(actionId); if (!selectedAction) throw new RangeError("The theme action is unknown."); const theme = normalizeTheme(themeValue); if (!theme) throw new RangeError(`The theme selection is unavailable: ${themeValidationReason(themeValue)}.`); const sort = normalizeThemeSort(sortValue); if (!sort) throw new RangeError("The theme selection is unavailable: invalid-theme-sort."); const normalized = requireOptions(options); const mapping = buildThemeSelection(actionId, theme, sort); return { id: normalized.id, builderKey: mapping.builderKey, code: mapping.code, aliases: mapping.aliases, title: theme.displayTitle, detail: `${selectedAction.label} · ${sort.label}`, category: "plate", market: "theme", selection: mapping.selection, enabled: true, fadeDuration: normalized.fadeDuration, graphicType: mapping.selection.graphicType, subtype: mapping.selection.subtype, pageSize: mapping.pageSize, pagePreview: mapping.pagePreview, operator: Object.freeze({ source: "legacy-theme-workflow", schemaVersion: SCHEMA_VERSION, actionId, theme, sort: sort.id, resolverGroup: mapping.selection.groupCode }) }; } function sameSelection(left, right) { if (!left || typeof left !== "object" || !right || typeof right !== "object") return false; return ["groupCode", "subject", "graphicType", "subtype", "dataCode"] .every(key => typeof left[key] === "string" && left[key] === right[key]); } function samePagePreview(left, right) { if (!left || typeof left !== "object" || !right || typeof right !== "object") return false; return ["itemCount", "accessibleItemCount", "pageSize", "pageCount", "maximumPageCount", "isTruncated"] .every(key => left[key] === right[key]); } function restoreThemePlaylistEntry(value) { if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.enabled !== "boolean") return null; const operator = value.operator; if (!operator || typeof operator !== "object" || operator.source !== "legacy-theme-workflow" || operator.schemaVersion !== SCHEMA_VERSION || !actionsById.has(operator.actionId)) return null; let recreated; try { recreated = createThemePlaylistEntry(operator.actionId, operator.theme, operator.sort, { id: value.id, fadeDuration: value.fadeDuration }); } catch { return null; } const scalars = [ "id", "builderKey", "code", "title", "detail", "category", "market", "fadeDuration", "graphicType", "subtype", "pageSize" ]; if (!scalars.every(key => value[key] === recreated[key]) || !Array.isArray(value.aliases) || value.aliases.length !== recreated.aliases.length || !value.aliases.every((alias, index) => alias === recreated.aliases[index]) || !sameSelection(value.selection, recreated.selection) || !samePagePreview(value.pagePreview, recreated.pagePreview) || operator.resolverGroup !== recreated.operator.resolverGroup) return null; return { ...recreated, enabled: value.enabled }; } return { sourceContract, themeSorts, nxtSessions, actions, themeActions: actions, normalizeTheme, normalizeThemeSort, getCompatibility, isActionAllowed, calculatePagePreview, buildThemeSelection, createThemePlaylistEntry, restoreThemePlaylistEntry, refreshThemePlaylistEntry: restoreThemePlaylistEntry }; });