205 lines
8.9 KiB
JavaScript
205 lines
8.9 KiB
JavaScript
(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
|
|
};
|
|
});
|