Files
MBN_STOCK_WEBVIEW/Web/named-manual-restore-workflow.js

323 lines
14 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 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
};
});