(function (root, factory) { "use strict"; const manualLists = typeof module === "object" && module.exports ? require("./manual-lists-workflow.js") : root.MbnManualListsWorkflow; const manualFinancial = typeof module === "object" && module.exports ? require("./manual-financial-workflow.js") : root.MbnManualFinancialWorkflow; const api = Object.freeze(factory(manualLists, manualFinancial)); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnNamedManualRestoreWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function (manualLists, manualFinancial) { "use strict"; if (!manualLists || !manualFinancial) throw new Error("Manual restore dependencies are unavailable."); const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/; const versionPattern = /^[a-f0-9]{64}$/; const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u; const pendingValues = new WeakSet(); const storedPendingValues = new WeakSet(); const legacyAudienceByGroup = Object.freeze({ "개인 순매도 상위(수동)": "INDIVIDUAL", "외국인 순매도 상위(수동)": "FOREIGN", "기관 순매도 상위(수동)": "INSTITUTION" }); const canonicalMarketByGroup = Object.freeze({ KOSPI: "kospi", KOSDAQ: "kosdaq", NXT_KOSPI: "nxt-kospi", NXT_KOSDAQ: "nxt-kosdaq" }); const historicalMarketByGroup = Object.freeze({ "코스피": "kospi", "코스닥": "kosdaq", "코스피_NXT": "nxt-kospi", "코스닥_NXT": "nxt-kosdaq" }); const canonicalFinancialByGraphic = Object.freeze(Object.fromEntries( manualFinancial.sourceContract.screens.map(screen => [screen.graphicType, screen]))); const historicalFinancialByGraphic = Object.freeze(Object.fromEntries( manualFinancial.sourceContract.screens.map(screen => [screen.label, screen]))); function safeText(value, maximum, allowEmpty = false) { return typeof value === "string" && value.length <= maximum && (allowEmpty || value.length > 0) && value === value.trim() && !value.includes("^") && !unsafeTextPattern.test(value); } function selection(raw) { if (!raw || typeof raw !== "object" || Array.isArray(raw) || typeof raw.enabled !== "boolean" || !Number.isInteger(raw.itemIndex) || raw.itemIndex < 0 || !safeText(raw.groupCode, 256, true) || !safeText(raw.subject, 4096, true) || !safeText(raw.graphicType, 256, true) || !safeText(raw.subtype, 256, true) || !safeText(raw.dataCode, 1024, true) || !safeText(raw.pageText, 9)) return null; return Object.freeze({ groupCode: raw.groupCode, subject: raw.subject, graphicType: raw.graphicType, subtype: raw.subtype, dataCode: raw.dataCode, pageText: raw.pageText }); } function options(value) { if (!value || typeof value !== "object" || Array.isArray(value) || !identifierPattern.test(value.id) || !Number.isInteger(value.fadeDuration) || value.fadeDuration < 0 || value.fadeDuration > 60) return null; return Object.freeze({ id: value.id, fadeDuration: value.fadeDuration }); } function exactHistoricalViNames(subject) { if (!safeText(subject, 4096) || subject.startsWith("@MBN_VI_SNAPSHOT_")) return null; const names = subject.split(","); if (names.length < 1 || names.length > manualLists.maximumViItems || names.some(name => !safeText(name, 200) || name !== name.trim())) return null; return Object.freeze(names); } function classify(raw, rawOptions) { const selected = selection(raw); const normalizedOptions = options(rawOptions); if (!selected || !normalizedOptions) return null; let descriptor = null; const canonicalAudience = manualLists.normalizeAudience(selected.groupCode); const legacyAudience = legacyAudienceByGroup[selected.groupCode] || null; if ((canonicalAudience && selected.subject === "INDEX" && selected.graphicType === "MANUAL_NET_SELL" && selected.subtype === "MANUAL_NET_SELL" && selected.pageText === "1/1") || (legacyAudience && selected.subject === "지수" && selected.pageText === "1/1" && selected.graphicType === "순매도 상위" && selected.subtype === "순매도 상위")) { if (selected.dataCode !== "") return null; descriptor = { kind: "net-sell", audience: canonicalAudience || legacyAudience, historical: Boolean(legacyAudience) }; } else if (selected.groupCode === "PAGED_VI" && selected.graphicType === "INPUT_ORDER" && selected.subtype === "CURRENT" && /^1\/(?:[1-9]|1[0-9]|20)$/.test(selected.pageText)) { if (selected.subject === manualLists.viSnapshotSubject && versionPattern.test(selected.dataCode)) { descriptor = { kind: "vi", expectedVersion: selected.dataCode, historicalNames: null }; } } else if (selected.groupCode === "" && selected.graphicType === "5단 표그래프" && selected.subtype === "VI 발동(수동)" && selected.dataCode === "") { const names = exactHistoricalViNames(selected.subject); if (names && selected.pageText === `1/${Math.ceil(names.length / manualLists.viPageSize)}`) { descriptor = { kind: "vi", expectedVersion: null, historicalNames: names }; } } else { const canonicalProfile = canonicalFinancialByGraphic[selected.graphicType]; const historicalProfile = historicalFinancialByGraphic[selected.graphicType]; const canonicalMarket = canonicalMarketByGroup[selected.groupCode]; const historicalMarket = historicalMarketByGroup[selected.groupCode]; const profile = canonicalProfile && canonicalMarket ? canonicalProfile : (historicalProfile && historicalMarket ? historicalProfile : null); const market = canonicalProfile && canonicalMarket ? canonicalMarket : historicalMarket; if (profile && market && safeText(selected.subject, 200) && selected.subtype === "" && selected.dataCode === "" && selected.pageText === "1/1") { descriptor = { kind: "financial", screen: profile.screen, builderKey: profile.builderKey, code: profile.code, market, stockName: selected.subject, identity: Object.freeze({ screen: profile.screen, stockName: selected.subject }) }; } } if (!descriptor) return null; const pending = Object.freeze({ ...descriptor, itemIndex: raw.itemIndex, enabled: raw.enabled, entry: Object.freeze({ id: normalizedOptions.id }), fadeDuration: normalizedOptions.fadeDuration, rawSelection: Object.freeze({ groupCode: selected.groupCode, subject: selected.subject, graphicType: selected.graphicType, subtype: selected.subtype, dataCode: selected.dataCode }), pageText: selected.pageText }); pendingValues.add(pending); return pending; } function createManualListReadRequest(requestId, pending) { if (!pendingValues.has(pending) || !["net-sell", "vi"].includes(pending.kind)) { throw new TypeError("A validated named manual-list restore is required."); } return pending.kind === "net-sell" ? manualLists.createNetSellReadRequest(requestId, pending.audience) : manualLists.createViReadRequest(requestId); } function materializeManualList(pending, payload, requestEnvelope) { if (!pendingValues.has(pending) || !requestEnvelope || requestEnvelope.type !== (pending.kind === "net-sell" ? manualLists.bridgeContract.netSellReadType : manualLists.bridgeContract.viReadType)) return null; let entry; if (pending.kind === "net-sell") { const response = manualLists.normalizeNetSellDataResponse(payload, requestEnvelope.payload); if (!response) return null; entry = manualLists.createNetSellPlaylistEntry(pending.audience, { id: pending.entry.id, fadeDuration: pending.fadeDuration, enabled: pending.enabled }, response); } else if (pending.kind === "vi") { const response = manualLists.normalizeViDataResponse(payload, requestEnvelope.payload.requestId); if (!response || response.itemCount < 1 || pending.pageText !== `1/${response.pageCount}` || (pending.expectedVersion !== null && response.version !== pending.expectedVersion)) return null; if (pending.historicalNames !== null) { const names = response.items.map(item => item.name); if (names.length !== pending.historicalNames.length || names.some((name, index) => name !== pending.historicalNames[index])) return null; } entry = manualLists.createViPlaylistEntry(response.items, response.version, { id: pending.entry.id, fadeDuration: pending.fadeDuration, enabled: pending.enabled }, response); } return manualLists.restorePlaylistEntry(entry) ? entry : null; } function isPending(value, kind) { return pendingValues.has(value) && (kind === undefined || value.kind === kind); } function matchesMaterializedEntry(pending, entry) { if (!pendingValues.has(pending) || !entry || entry.id !== pending.entry.id || entry.enabled !== pending.enabled || entry.fadeDuration !== pending.fadeDuration) return false; if (pending.kind === "net-sell") { const restored = manualLists.restorePlaylistEntry(entry); return !!restored && restored.builderKey === "s5025" && restored.operator.kind === "net-sell" && restored.operator.audience === pending.audience; } if (pending.kind === "vi") { const restored = manualLists.restorePlaylistEntry(entry); return !!restored && restored.builderKey === "s5074" && restored.operator.kind === "vi"; } if (pending.kind === "financial") { return manualFinancial.isPlaylistEntryPlayable(entry) && entry.builderKey === pending.builderKey && entry.code === pending.code && entry.stockName === pending.stockName && entry.market === pending.market; } return false; } function isTrustedEntryForSave(entry, itemIndex = 0) { if (!entry || typeof entry !== "object" || Array.isArray(entry) || !Number.isInteger(itemIndex) || itemIndex < 0 || typeof entry.enabled !== "boolean") return false; const source = entry.operator?.source; const fresh = source === "manual-financial-workflow" ? manualFinancial.isPlaylistEntryPlayable(entry) : source === "legacy-manual-lists-workflow" && manualLists.isPlaylistEntryPlayable(entry); if (!fresh) return false; const pageText = entry.operator?.kind === "vi" ? (Number.isInteger(entry.pageCount) ? `1/${entry.pageCount}` : "") : "1/1"; const pending = classify({ itemIndex, enabled: entry.enabled, groupCode: entry.selection?.groupCode, subject: entry.selection?.subject, graphicType: entry.selection?.graphicType, subtype: entry.selection?.subtype, dataCode: entry.selection?.dataCode, pageText }, { id: entry.id, fadeDuration: entry.fadeDuration }); return !!pending && matchesMaterializedEntry(pending, entry); } function createStoredManualListPending(entry, itemIndex = 0) { const restored = manualLists.restorePlaylistEntry(entry); if (!restored || !Number.isInteger(itemIndex) || itemIndex < 0) return null; const pageText = restored.operator.kind === "vi" ? `1/${restored.pageCount}` : "1/1"; const pending = classify({ itemIndex, enabled: restored.enabled, groupCode: restored.selection.groupCode, subject: restored.selection.subject, graphicType: restored.selection.graphicType, subtype: restored.selection.subtype, dataCode: restored.selection.dataCode, pageText }, { id: restored.id, fadeDuration: restored.fadeDuration }); if (!pending || !["net-sell", "vi"].includes(pending.kind) || !matchesMaterializedEntry(pending, restored)) return null; storedPendingValues.add(pending); return pending; } function isStoredManualListPending(value) { return storedPendingValues.has(value); } function legacyFieldsForEntry(entry, itemIndex = 0) { if (!isTrustedEntryForSave(entry, itemIndex)) return null; const enabled = entry.enabled ? "1" : "0"; let fields = null; if (entry.operator.source === "legacy-manual-lists-workflow" && entry.operator.kind === "net-sell") { const groupCode = `${manualLists.audienceLabels[entry.operator.audience]} 순매도 상위(수동)`; fields = [enabled, groupCode, "지수", "순매도 상위", "순매도 상위", "1/1", ""]; } else if (entry.operator.source === "legacy-manual-lists-workflow" && entry.operator.kind === "vi") { fields = [ enabled, "", entry.operator.items.map(item => item.name).join(","), "5단 표그래프", "VI 발동(수동)", `1/${entry.pageCount}`, "" ]; } else if (entry.operator.source === "manual-financial-workflow") { const market = { kospi: "코스피", kosdaq: "코스닥", "nxt-kospi": "코스피_NXT", "nxt-kosdaq": "코스닥_NXT" }[entry.market]; const profile = manualFinancial.getScreenDefinition(entry.operator.screen); if (market && profile) { fields = [enabled, market, entry.stockName, profile.label, "", "1/1", ""]; } } if (!fields) return null; const pending = classify({ itemIndex, enabled: fields[0] === "1", groupCode: fields[1], subject: fields[2], graphicType: fields[3], subtype: fields[4], pageText: fields[5], dataCode: fields[6] }, { id: entry.id, fadeDuration: entry.fadeDuration }); return pending && matchesMaterializedEntry(pending, entry) ? Object.freeze(fields) : null; } return { classify, createManualListReadRequest, materializeManualList, isPending, matchesMaterializedEntry, isTrustedEntryForSave, createStoredManualListPending, isStoredManualListPending, legacyFieldsForEntry }; });