(function (root, factory) { "use strict"; const api = Object.freeze(factory()); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnManualListsWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function () { "use strict"; const SCHEMA_VERSION = 1; const MAXIMUM_VI_ITEMS = 100; const VI_PAGE_SIZE = 5; const MAXIMUM_PAGE_COUNT = 20; const VI_SNAPSHOT_SUBJECT = "@MBN_VI_SNAPSHOT_V1"; const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/; const rowVersionPattern = /^[a-f0-9]{64}$/; const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u; const audiences = Object.freeze(["INDIVIDUAL", "FOREIGN", "INSTITUTION"]); const statusCodes = new Set([ "READY", "IMPORT_INTERRUPTED", "IMPORTED_STATE_INVALID", "INITIALIZATION_FAILED", "UNSAFE_CONFIGURATION" ]); const trustedNetSellReadResponses = new WeakSet(); const trustedViReadResponses = new WeakSet(); const trustedPlaylistEntries = new WeakMap(); const audienceLabels = Object.freeze({ INDIVIDUAL: "개인", FOREIGN: "외국인", INSTITUTION: "기관" }); const bridgeContract = Object.freeze({ statusRequestType: "request-manual-operator-data-status", statusResultType: "manual-operator-data-status", legacyImportType: "import-legacy-manual-operator-data", legacyImportResultType: "legacy-manual-operator-import-result", netSellReadType: "request-manual-net-sell-data", netSellDataType: "manual-net-sell-data", netSellSaveType: "save-manual-net-sell-data", netSellSaveResultType: "manual-net-sell-save-result", viReadType: "request-vi-manual-list", viDataType: "vi-manual-list-data", viSaveType: "save-vi-manual-list", viSaveResultType: "vi-manual-list-save-result", errorType: "manual-operator-data-error" }); function hasExactKeys(value, keys) { if (!value || typeof value !== "object" || Array.isArray(value)) return false; const actual = Object.keys(value).sort(); const expected = [...keys].sort(); return actual.length === expected.length && actual.every((key, index) => key === expected[index]); } function isRequestId(value) { return typeof value === "string" && identifierPattern.test(value); } function isRowVersion(value) { return typeof value === "string" && rowVersionPattern.test(value); } function isSafeText(value, maximumLength, allowEmpty = true) { return typeof value === "string" && (allowEmpty || value.length > 0) && value.length <= maximumLength && !value.includes("^") && !unsafeTextPattern.test(value); } function normalizeAudience(value) { return typeof value === "string" && audiences.includes(value) ? value : null; } function normalizeNetSellRow(value) { if (!hasExactKeys(value, ["leftName", "leftAmount", "rightName", "rightAmount"])) return null; const keys = ["leftName", "leftAmount", "rightName", "rightAmount"]; if (!keys.every(key => isSafeText(value[key], 256))) return null; return Object.freeze({ leftName: value.leftName, leftAmount: value.leftAmount, rightName: value.rightName, rightAmount: value.rightAmount }); } function normalizeNetSellRows(value) { if (!Array.isArray(value) || value.length !== 5) return null; const rows = value.map(normalizeNetSellRow); return rows.every(Boolean) ? Object.freeze(rows) : null; } function normalizeViItem(value) { if (!hasExactKeys(value, ["code", "name"]) || !isSafeText(value.code, 64, false) || !isSafeText(value.name, 200, false) || value.code !== value.code.trim() || value.name !== value.name.trim() || value.name.includes(",")) return null; return Object.freeze({ code: value.code, name: value.name }); } function normalizeViItems(value, allowEmpty = true) { if (!Array.isArray(value) || value.length > MAXIMUM_VI_ITEMS || (!allowEmpty && value.length === 0)) { return null; } const items = value.map(normalizeViItem); return items.every(Boolean) ? Object.freeze(items) : null; } function request(type, requestId, extra = {}) { if (!isRequestId(requestId)) throw new TypeError("A safe manual-data request id is required."); return Object.freeze({ type, payload: Object.freeze({ requestId, ...extra }) }); } function createStatusRequest(requestId) { return request(bridgeContract.statusRequestType, requestId); } function createLegacyImportRequest(requestId) { return request(bridgeContract.legacyImportType, requestId); } function createNetSellReadRequest(requestId, audience) { const normalized = normalizeAudience(audience); if (!normalized) throw new RangeError("The manual net-sell audience is invalid."); return request(bridgeContract.netSellReadType, requestId, { audience: normalized }); } function createNetSellSaveRequest(requestId, audience, rows) { const normalizedAudience = normalizeAudience(audience); const normalizedRows = normalizeNetSellRows(rows); if (!normalizedAudience || !normalizedRows) throw new TypeError("Five valid manual net-sell rows are required."); return request(bridgeContract.netSellSaveType, requestId, { audience: normalizedAudience, rows: normalizedRows }); } function createViReadRequest(requestId) { return request(bridgeContract.viReadType, requestId); } function createViSaveRequest(requestId, items, expectedVersion) { const normalized = normalizeViItems(items); if (!normalized || !isRowVersion(expectedVersion)) { throw new TypeError("A valid VI manual list and its last-read version are required."); } return request(bridgeContract.viSaveType, requestId, { expectedVersion, items: normalized }); } function normalizeStatusResponse(value, expectedRequestId) { if (!hasExactKeys(value, ["requestId", "available", "code", "writeQuarantined", "message"]) || !isRequestId(value.requestId) || typeof value.available !== "boolean" || !statusCodes.has(value.code) || value.available !== (value.code === "READY") || typeof value.writeQuarantined !== "boolean" || !(value.message === null || isSafeText(value.message, 512, false)) || (expectedRequestId !== undefined && value.requestId !== expectedRequestId)) return null; return Object.freeze({ ...value }); } function normalizeLegacyImportResponse(value, expectedRequestId) { if (!hasExactKeys(value, [ "requestId", "markerVersion", "sourceSha256", "importedAt", "netSellRowCount", "viItemCount" ]) || !isRequestId(value.requestId) || value.markerVersion !== 1 || !isRowVersion(value.sourceSha256) || typeof value.importedAt !== "string" || !Number.isFinite(Date.parse(value.importedAt)) || value.netSellRowCount !== 15 || !Number.isInteger(value.viItemCount) || value.viItemCount < 0 || value.viItemCount > MAXIMUM_VI_ITEMS || (expectedRequestId !== undefined && value.requestId !== expectedRequestId)) return null; return Object.freeze({ ...value }); } function normalizeNetSellDataResponse(value, expectedRequest) { if (!hasExactKeys(value, ["requestId", "audience", "rows"]) || !isRequestId(value.requestId) || !normalizeAudience(value.audience) || (expectedRequest && (value.requestId !== expectedRequest.requestId || value.audience !== expectedRequest.audience))) return null; const rows = normalizeNetSellRows(value.rows); if (!rows) return null; const response = Object.freeze({ requestId: value.requestId, audience: value.audience, rows }); trustedNetSellReadResponses.add(response); return response; } function normalizeNetSellSaveResponse(value, expectedRequest) { if (!hasExactKeys(value, ["requestId", "audience", "saved", "recovered"]) || !isRequestId(value.requestId) || !normalizeAudience(value.audience) || value.saved !== true || typeof value.recovered !== "boolean" || (expectedRequest && (value.requestId !== expectedRequest.requestId || value.audience !== expectedRequest.audience))) return null; return Object.freeze({ ...value }); } function normalizeViDataResponse(value, expectedRequestId) { if (!hasExactKeys(value, ["requestId", "itemCount", "pageCount", "items", "version"]) || !isRequestId(value.requestId) || !isRowVersion(value.version) || !Number.isInteger(value.itemCount) || value.itemCount < 0 || value.itemCount > MAXIMUM_VI_ITEMS || !Number.isInteger(value.pageCount) || value.pageCount < 0 || value.pageCount > MAXIMUM_PAGE_COUNT || value.pageCount !== (value.itemCount === 0 ? 0 : Math.ceil(value.itemCount / VI_PAGE_SIZE)) || (expectedRequestId !== undefined && value.requestId !== expectedRequestId)) return null; const items = normalizeViItems(value.items); if (!items || items.length !== value.itemCount) return null; const response = Object.freeze({ requestId: value.requestId, itemCount: value.itemCount, pageCount: value.pageCount, items, version: value.version }); trustedViReadResponses.add(response); return response; } function normalizeViSaveResponse(value, expectedRequestId) { if (!hasExactKeys(value, [ "requestId", "saved", "recovered", "persisted", "itemCount", "pageCount", "version" ]) || !isRequestId(value.requestId) || value.saved !== true || typeof value.recovered !== "boolean" || typeof value.persisted !== "boolean" || !isRowVersion(value.version) || !Number.isInteger(value.itemCount) || value.itemCount < 0 || value.itemCount > MAXIMUM_VI_ITEMS || !Number.isInteger(value.pageCount) || value.pageCount !== (value.itemCount === 0 ? 0 : Math.ceil(value.itemCount / VI_PAGE_SIZE)) || (expectedRequestId !== undefined && value.requestId !== expectedRequestId)) return null; return Object.freeze({ ...value }); } function normalizeError(value, expectedRequestId) { if (!hasExactKeys(value, ["requestId", "operation", "code", "message", "retryable", "outcomeUnknown"]) || !(value.requestId === "" || isRequestId(value.requestId)) || !isSafeText(value.operation, 64, false) || !isSafeText(value.code, 64, false) || !isSafeText(value.message, 512, false) || typeof value.retryable !== "boolean" || typeof value.outcomeUnknown !== "boolean" || (value.outcomeUnknown && value.retryable) || (expectedRequestId !== undefined && value.requestId !== expectedRequestId)) return null; return Object.freeze({ ...value }); } function requireOptions(options) { const keys = options && typeof options === "object" ? Object.keys(options).sort() : []; const exact = keys.length === 2 && keys[0] === "fadeDuration" && keys[1] === "id"; const exactWithEnabled = keys.length === 3 && keys[0] === "enabled" && keys[1] === "fadeDuration" && keys[2] === "id"; if ((!exact && !exactWithEnabled) || !isRequestId(options.id) || !Number.isInteger(options.fadeDuration) || options.fadeDuration < 0 || options.fadeDuration > 60 || (exactWithEnabled && typeof options.enabled !== "boolean")) { throw new TypeError("Safe manual-list playlist options are required."); } return Object.freeze({ id: options.id, fadeDuration: options.fadeDuration, enabled: exactWithEnabled ? options.enabled : true }); } function createNetSellEntryObject(audience, options) { const normalizedAudience = normalizeAudience(audience); const normalizedOptions = requireOptions(options); if (!normalizedAudience) throw new RangeError("The manual net-sell audience is invalid."); const label = audienceLabels[normalizedAudience]; const selection = Object.freeze({ groupCode: normalizedAudience, subject: "INDEX", graphicType: "MANUAL_NET_SELL", subtype: "MANUAL_NET_SELL", dataCode: "" }); return { id: normalizedOptions.id, builderKey: "s5025", code: "5025", aliases: ["5025"], title: `${label} 순매도 상위(수동)`, detail: "매매동향 · 검증된 수동 입력 5행", category: "market", market: "index", selection, enabled: normalizedOptions.enabled, fadeDuration: normalizedOptions.fadeDuration, graphicType: selection.graphicType, subtype: selection.subtype, operator: Object.freeze({ source: "legacy-manual-lists-workflow", schemaVersion: SCHEMA_VERSION, kind: "net-sell", audience: normalizedAudience }) }; } function createViEntryObject(items, rowVersion, options) { const normalizedItems = normalizeViItems(items, false); const normalizedOptions = requireOptions(options); if (!normalizedItems || !isRowVersion(rowVersion)) { throw new TypeError("A versioned non-empty VI snapshot is required."); } const pageCount = Math.ceil(normalizedItems.length / VI_PAGE_SIZE); const selection = Object.freeze({ groupCode: "PAGED_VI", subject: VI_SNAPSHOT_SUBJECT, graphicType: "INPUT_ORDER", subtype: "CURRENT", dataCode: rowVersion }); return { id: normalizedOptions.id, builderKey: "s5074", code: "5074", aliases: ["5074"], title: normalizedItems.map(item => item.name).join(", "), detail: "5단 표그래프 · VI 발동(수동)", category: "plate", market: "index", itemCount: normalizedItems.length, pageSize: VI_PAGE_SIZE, pageCount, selection, enabled: normalizedOptions.enabled, fadeDuration: normalizedOptions.fadeDuration, graphicType: selection.graphicType, subtype: selection.subtype, operator: Object.freeze({ source: "legacy-manual-lists-workflow", schemaVersion: SCHEMA_VERSION, kind: "vi", items: normalizedItems, rowVersion, pagePreview: Object.freeze({ itemCount: normalizedItems.length, pageSize: VI_PAGE_SIZE, pageCount, maximumPageCount: MAXIMUM_PAGE_COUNT }) }) }; } function createNetSellPlaylistEntry(audience, options, freshRead) { const entry = createNetSellEntryObject(audience, options); if (trustedNetSellReadResponses.has(freshRead) && freshRead.audience === entry.operator.audience && freshRead.rows.length === 5) { trustedPlaylistEntries.set(entry, freshRead); } return entry; } function createViPlaylistEntry(items, rowVersion, options, freshRead) { const entry = createViEntryObject(items, rowVersion, options); if (trustedViReadResponses.has(freshRead) && freshRead.version === entry.operator.rowVersion && freshRead.itemCount === entry.itemCount && freshRead.pageCount === entry.pageCount && freshRead.items.length === entry.operator.items.length && freshRead.items.every((item, index) => item.code === entry.operator.items[index].code && item.name === entry.operator.items[index].name)) { trustedPlaylistEntries.set(entry, freshRead); } return entry; } function isPlaylistEntryPlayable(value) { if (!value) return false; const freshRead = trustedPlaylistEntries.get(value); const restored = restorePlaylistEntry(value); if (!freshRead || !restored) return false; if (restored.operator.kind === "net-sell") { return trustedNetSellReadResponses.has(freshRead) && freshRead.rows.length === 5 && freshRead.audience === restored.operator.audience; } return trustedViReadResponses.has(freshRead) && freshRead.version === restored.operator.rowVersion && freshRead.itemCount === restored.itemCount && freshRead.pageCount === restored.pageCount && freshRead.items.length === restored.operator.items.length && freshRead.items.every((item, index) => item.code === restored.operator.items[index].code && item.name === restored.operator.items[index].name); } function sameSelection(left, right) { const keys = ["groupCode", "subject", "graphicType", "subtype", "dataCode"]; return hasExactKeys(left, keys) && hasExactKeys(right, keys) && keys .every(key => typeof left[key] === "string" && left[key] === right[key]); } function restorePlaylistEntry(value) { if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.enabled !== "boolean") return null; const operator = value.operator; if (!operator || operator.source !== "legacy-manual-lists-workflow" || operator.schemaVersion !== SCHEMA_VERSION || !["net-sell", "vi"].includes(operator.kind)) return null; const topKeys = operator.kind === "net-sell" ? [ "id", "builderKey", "code", "aliases", "title", "detail", "category", "market", "selection", "enabled", "fadeDuration", "graphicType", "subtype", "operator" ] : [ "id", "builderKey", "code", "aliases", "title", "detail", "category", "market", "itemCount", "pageSize", "pageCount", "selection", "enabled", "fadeDuration", "graphicType", "subtype", "operator" ]; const operatorKeys = operator.kind === "net-sell" ? ["source", "schemaVersion", "kind", "audience"] : ["source", "schemaVersion", "kind", "items", "rowVersion", "pagePreview"]; if (!hasExactKeys(value, topKeys) || !hasExactKeys(operator, operatorKeys)) return null; if (operator.kind === "vi" && (!isRowVersion(operator.rowVersion) || !hasExactKeys(operator.pagePreview, ["itemCount", "pageSize", "pageCount", "maximumPageCount"]))) { return null; } let recreated; try { const options = { id: value.id, fadeDuration: value.fadeDuration, enabled: value.enabled }; recreated = operator.kind === "net-sell" ? createNetSellEntryObject(operator.audience, options) : createViEntryObject(operator.items, operator.rowVersion, options); } catch { return null; } const scalars = [ "id", "builderKey", "code", "title", "detail", "category", "market", "fadeDuration", "graphicType", "subtype" ]; if (!scalars.every(key => value[key] === recreated[key]) || !Array.isArray(value.aliases) || value.aliases.length !== 1 || value.aliases[0] !== recreated.aliases[0] || !sameSelection(value.selection, recreated.selection)) return null; if (operator.kind === "vi" && (value.itemCount !== recreated.itemCount || value.pageSize !== recreated.pageSize || value.pageCount !== recreated.pageCount || operator.pagePreview.itemCount !== recreated.operator.pagePreview.itemCount || operator.pagePreview.pageSize !== recreated.operator.pagePreview.pageSize || operator.pagePreview.pageCount !== recreated.operator.pagePreview.pageCount || operator.pagePreview.maximumPageCount !== recreated.operator.pagePreview.maximumPageCount)) return null; return recreated; } return { bridgeContract, audiences, audienceLabels, maximumViItems: MAXIMUM_VI_ITEMS, viPageSize: VI_PAGE_SIZE, maximumPageCount: MAXIMUM_PAGE_COUNT, viSnapshotSubject: VI_SNAPSHOT_SUBJECT, normalizeAudience, normalizeNetSellRow, normalizeNetSellRows, normalizeViItem, normalizeViItems, createStatusRequest, createLegacyImportRequest, createNetSellReadRequest, createNetSellSaveRequest, createViReadRequest, createViSaveRequest, normalizeStatusResponse, normalizeLegacyImportResponse, normalizeNetSellDataResponse, normalizeNetSellSaveResponse, normalizeViDataResponse, normalizeViSaveResponse, normalizeError, createNetSellPlaylistEntry, createViPlaylistEntry, isPlaylistEntryPlayable, restorePlaylistEntry, refreshPlaylistEntry: restorePlaylistEntry }; });