(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 legacyAudienceByGroup = Object.freeze({ "개인 순매도 상위(수동)": "INDIVIDUAL", "외국인 순매도 상위(수동)": "FOREIGN", "기관 순매도 상위(수동)": "INSTITUTION" }); const marketByGroup = Object.freeze({ KOSPI: "kospi", KOSDAQ: "kosdaq", NXT_KOSPI: "nxt-kospi", NXT_KOSDAQ: "nxt-kosdaq", "코스피": "kospi", "코스닥": "kosdaq", "코스피_NXT": "nxt-kospi", "코스닥_NXT": "nxt-kosdaq" }); const financialByGraphic = Object.freeze(Object.fromEntries( manualFinancial.sourceContract.screens.flatMap(screen => [ [screen.graphicType, 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)) return null; return Object.freeze({ groupCode: raw.groupCode, subject: raw.subject, graphicType: raw.graphicType, subtype: raw.subtype, dataCode: raw.dataCode }); } 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()) || new Set(names).size !== names.length) 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") || (legacyAudience && selected.subject === "" && 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") { if (selected.subject === manualLists.viSnapshotSubject && versionPattern.test(selected.dataCode)) { descriptor = { kind: "vi", expectedVersion: selected.dataCode, historicalNames: null }; } else if (selected.dataCode === "") { const names = exactHistoricalViNames(selected.subject); if (names) descriptor = { kind: "vi", expectedVersion: null, historicalNames: names }; } } else if (selected.groupCode === "" && selected.graphicType === "5단 표그래프" && selected.subtype === "VI 발동(수동)" && selected.dataCode === "") { const names = exactHistoricalViNames(selected.subject); if (names) descriptor = { kind: "vi", expectedVersion: null, historicalNames: names }; } else { const profile = financialByGraphic[selected.graphicType]; const market = marketByGroup[selected.groupCode]; if (profile && market && safeText(selected.subject, 200) && selected.subtype === "" && selected.dataCode === "") { 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: selected }); 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 }); } else if (pending.kind === "vi") { const response = manualLists.normalizeViDataResponse(payload, requestEnvelope.payload.requestId); if (!response || response.itemCount < 1 || (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 }); } return manualLists.restorePlaylistEntry({ ...entry, enabled: pending.enabled }); } 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; } return { classify, createManualListReadRequest, materializeManualList, isPending, matchesMaterializedEntry }; });