(function (root, factory) { "use strict"; const api = Object.freeze(factory()); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnComparisonWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function () { "use strict"; const DEFAULT_FADE_DURATION = 6; const SCHEMA_VERSION = 1; const PAIR_LIST_SCHEMA_VERSION = 1; const MAX_SAVED_PAIRS = 500; const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/; const stockCodePattern = /^[A-Za-z0-9._-]{1,32}$/; const worldSymbolPattern = /^[A-Za-z0-9@._:-]{1,64}$/; const controlCharacterPattern = /[\u0000-\u001f\u007f]/; const runtimeAsset = Object.freeze({ relativePath: "bin/Debug/Res/종목비교.ini", sha256: "980F74C000EF6A34B85FDAAF6D2756269483637186C91A93548F35C5CFC1B7E4" }); const marketTargetRows = [ ["kospi", "코스피 지수", "Kospi"], ["kosdaq", "코스닥 지수", "Kosdaq"], ["kospi200", "코스피200 지수", "Kospi200"], ["futures", "선물 지수", "Futures"], ["krx100", "KRX100지수", "Krx100"], ["won-dollar", "환율-원달러", "WonDollar"], ["won-yen", "환율-원엔", "WonYen"], ["won-yuan", "환율-원위엔", "WonYuan"], ["won-euro", "환율-원유로", "WonEuro"], ["dow", "해외지수-다우", "Dow"], ["nasdaq", "해외지수-나스닥", "Nasdaq"], ["sp500", "해외지수-S&P", "Sp500"], ["germany-dax", "해외지수-독일", "GermanyDax"], ["united-kingdom-ftse", "해외지수-영국", "UnitedKingdomFtse"], ["france-cac", "해외지수-프랑스", "FranceCac"], ["nikkei", "해외지수-일본", "Nikkei"], ["hang-seng", "해외지수-홍콩", "HangSeng"], ["taiwan-weighted", "해외지수-대만", "TaiwanWeighted"], ["singapore-straits-times", "해외지수-싱가포르", "SingaporeStraitsTimes"], ["thailand-set", "해외지수-태국", "ThailandSet"], ["philippines-composite", "해외지수-필리핀", "PhilippinesComposite"], ["malaysia-klse", "해외지수-말레이시아", "MalaysiaKlse"], ["indonesia-composite", "해외지수-인도네시아", "IndonesiaComposite"], ["shanghai-composite", "해외지수-중국", "ShanghaiComposite"] ]; const marketTargets = Object.freeze(marketTargetRows.map(([id, displayName, target]) => Object.freeze({ kind: "market-target", id, displayName, target }))); if (marketTargets.length !== 24 || new Set(marketTargets.map(value => value.target)).size !== 24) { throw new Error("The legacy comparison selector must expose exactly 24 market targets."); } const marketTargetsByTarget = new Map(marketTargets.map(value => [value.target, value])); const marketTargetsByLookup = new Map(); for (const target of marketTargets) { marketTargetsByLookup.set(target.id.toLocaleLowerCase("en-US"), target); marketTargetsByLookup.set(target.target.toLocaleLowerCase("en-US"), target); marketTargetsByLookup.set(target.displayName.toLocaleLowerCase("ko-KR"), target); } function action(id, label, kind, details = {}) { return Object.freeze({ id, label, kind, ...details }); } const actions = Object.freeze([ action("two-column-expected", "2열판 예상체결", "two-column", { mode: "EXPECTED" }), action("two-column-current", "2열판", "two-column", { mode: "CURRENT" }), action("comparison-candle", "종목별 비교분석_캔들 그래프", "comparison", { builderKey: "s5026", code: "5026", graphicType: "COMPARISON_CANDLE", period: "FiveDays" }), action("comparison-line", "종목별 비교분석_라인 그래프", "comparison", { builderKey: "s5087", code: "5087", graphicType: "COMPARISON_LINE", period: "FiveDays" }), action("return-5d", "5일", "return", { period: "FiveDays" }), action("return-1m", "1개월", "return", { period: "OneMonth" }), action("return-3m", "3개월", "return", { period: "ThreeMonths" }), action("return-6m", "6개월", "return", { period: "SixMonths" }), action("return-12m", "12개월", "return", { period: "TwelveMonths" }) ]); if (actions.length !== 9 || new Set(actions.map(value => value.id)).size !== 9) { throw new Error("The legacy comparison tree must expose exactly nine actions."); } const actionsById = new Map(actions.map(value => [value.id, value])); const yieldActionIds = Object.freeze(actions .filter(value => value.kind === "return") .map(value => value.id)); const expectedMarketTargets = new Set(["Kospi", "Kosdaq", "Kospi200", "Krx100"]); function cleanText(value, maximumLength = 128) { if (typeof value !== "string") return null; const text = value.trim(); return text && text.length <= maximumLength && !controlCharacterPattern.test(text) ? text : null; } function getMarketTarget(value) { if (value && typeof value === "object" && !Array.isArray(value)) value = value.target ?? value.id ?? value.displayName; const text = cleanText(value); return text ? marketTargetsByLookup.get(text.toLocaleLowerCase("ko-KR")) ?? null : null; } function normalizeMarket(value) { const text = String(value || "").trim().toLocaleLowerCase("en-US"); return text === "kospi" || text === "kosdaq" ? text : null; } function normalizeTarget(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return null; switch (value.kind) { case "market-target": { const target = marketTargetsByTarget.get(value.target); if (!target) return null; if ((value.id !== undefined && value.id !== target.id) || (value.displayName !== undefined && value.displayName !== target.displayName)) return null; return target; } case "krx-stock": case "nxt-stock": { const market = normalizeMarket(value.market); const stockName = cleanText(value.stockName ?? value.name); const stockCode = cleanText(value.stockCode ?? value.code, 32); if (!market || !stockName || stockName.includes(",") || stockName.includes("(NXT)") || !stockCode || !stockCodePattern.test(stockCode)) return null; const displayName = value.kind === "nxt-stock" ? `${stockName}(NXT)` : stockName; if (value.displayName !== undefined && value.displayName !== displayName) return null; return Object.freeze({ kind: value.kind, market, stockName, stockCode, displayName }); } case "world-stock": { const inputName = cleanText(value.inputName); const displayName = cleanText(value.displayName ?? value.name); const symbol = cleanText(value.symbol, 64); const nation = cleanText(value.nation, 2)?.toLocaleUpperCase("en-US"); if (!inputName || inputName.includes(",") || !displayName || !symbol || !worldSymbolPattern.test(symbol) || (nation !== "US" && nation !== "TW")) return null; return Object.freeze({ kind: "world-stock", inputName, displayName, symbol, nation }); } default: return null; } } function targetKey(value) { const target = normalizeTarget(value); if (!target) return null; switch (target.kind) { case "market-target": return `market:${target.target}`; case "krx-stock": return `krx:${target.market}:${target.stockCode}`; case "nxt-stock": return `nxt:${target.market}:${target.stockCode}`; case "world-stock": return `world:${target.nation}:${target.symbol}`; default: return null; } } function subjectTokenForTarget(value) { const target = normalizeTarget(value); if (!target) return null; switch (target.kind) { case "market-target": return target.target; case "krx-stock": return target.stockName; case "nxt-stock": return `${target.stockName}(NXT)`; case "world-stock": return target.inputName; default: return null; } } function pairParts(value) { if (Array.isArray(value)) return [value[0], value[1]]; if (value && typeof value === "object") return [value.first ?? value.pair?.[0], value.second ?? value.pair?.[1]]; return [null, null]; } function normalizePair(value) { const [firstValue, secondValue] = pairParts(value); const first = normalizeTarget(firstValue); const second = normalizeTarget(secondValue); return first && second ? Object.freeze([first, second]) : null; } function normalizePairState(value) { const [firstValue, secondValue] = pairParts(value); const first = firstValue == null ? null : normalizeTarget(firstValue); const second = secondValue == null ? null : normalizeTarget(secondValue); return (firstValue == null || first) && (secondValue == null || second) ? Object.freeze([first, second]) : null; } function rotatePair(value, selectedValue) { const pair = normalizePairState(value); const selected = normalizeTarget(selectedValue); return pair && selected ? Object.freeze([selected, pair[0]]) : null; } function swapPair(value) { const pair = normalizePairState(value); return pair ? Object.freeze([pair[1], pair[0]]) : null; } function clearPair() { return Object.freeze([null, null]); } function pairLabel(value) { const pair = normalizePair(value); return pair ? `${pair[0].displayName} ↔ ${pair[1].displayName}` : ""; } function unsupported(actionId, reason) { return Object.freeze({ actionId, supported: false, reason, effectiveMode: null, warning: null }); } function supported(actionId, effectiveMode = null, warning = null) { return Object.freeze({ actionId, supported: true, reason: null, effectiveMode, warning }); } function getCompatibility(actionId, pairValue) { const selectedAction = actionsById.get(actionId); if (!selectedAction) return unsupported(actionId, "unknown-action"); const pair = normalizePair(pairValue); if (!pair) return unsupported(actionId, "pair-incomplete-or-invalid"); const futuresCount = pair.filter(value => value.kind === "market-target" && value.target === "Futures").length; const hasWorld = pair.some(value => value.kind === "world-stock"); if (selectedAction.kind === "comparison" || selectedAction.kind === "return") { return pair.every(value => value.kind === "krx-stock") ? supported(actionId, "GRAPH") : unsupported(actionId, "comparison-graphs-require-two-krx-stocks"); } if (futuresCount > 1) return unsupported(actionId, "two-futures-targets-are-unsupported"); if (futuresCount === 1) { const counterpart = pair.find(value => !(value.kind === "market-target" && value.target === "Futures")); if (!counterpart || counterpart.kind !== "market-target") { return unsupported(actionId, "futures-requires-a-market-target-counterpart"); } return selectedAction.mode === "EXPECTED" ? supported(actionId, "CURRENT", "The current Core routes a futures expected action through the current s5032 branch.") : supported(actionId, "CURRENT"); } if (selectedAction.mode === "EXPECTED") { const allowed = pair.every(value => value.kind === "krx-stock" || (value.kind === "market-target" && expectedMarketTargets.has(value.target))); return allowed ? supported(actionId, "EXPECTED") : unsupported(actionId, "expected-mode-supports-only-krx-stocks-and-four-domestic-indices"); } const hasMarketTarget = pair.some(value => value.kind === "market-target"); if (hasMarketTarget && hasWorld) { return unsupported(actionId, "mixed-market-and-world-is-unsupported"); } return supported(actionId, "CURRENT"); } function isActionAllowed(actionId, pairValue) { return getCompatibility(actionId, pairValue).supported; } function buildComparisonSelection(actionId, pairValue) { const selectedAction = actionsById.get(actionId); const pair = normalizePair(pairValue); const compatibility = getCompatibility(actionId, pair); if (!selectedAction || !pair || !compatibility.supported) { throw new RangeError(`The comparison action is unavailable: ${compatibility.reason || "invalid-selection"}.`); } const subject = pair.map(subjectTokenForTarget).join(","); if (selectedAction.kind === "two-column") { const futures = pair.some(value => value.kind === "market-target" && value.target === "Futures"); const code = futures ? "5032" : "8032"; return Object.freeze({ builderKey: futures ? "s5032" : "s8018", code, aliases: Object.freeze([code]), selection: Object.freeze({ groupCode: pair.some(value => value.kind === "market-target") ? "INDEX" : "STOCK", subject, graphicType: "2열판", subtype: selectedAction.mode, dataCode: "" }), effectiveMode: compatibility.effectiveMode, warning: compatibility.warning }); } const builderKey = selectedAction.kind === "return" ? "s5029" : selectedAction.builderKey; const code = selectedAction.kind === "return" ? "5029" : selectedAction.code; const graphicType = selectedAction.kind === "return" ? "RETURN_COMPARISON" : selectedAction.graphicType; return Object.freeze({ builderKey, code, aliases: Object.freeze([code]), selection: Object.freeze({ groupCode: "STOCK", subject, graphicType, subtype: selectedAction.period, dataCode: "" }), effectiveMode: compatibility.effectiveMode, warning: compatibility.warning }); } function requireOptions(options) { if (!options || typeof options !== "object" || Array.isArray(options)) { throw new TypeError("Comparison playlist options are required."); } const id = cleanText(options.id); if (!id || !identifierPattern.test(id)) throw new TypeError("A safe comparison playlist id is required."); const fadeDuration = options.fadeDuration === undefined ? DEFAULT_FADE_DURATION : Number(options.fadeDuration); if (!Number.isInteger(fadeDuration) || fadeDuration < 0 || fadeDuration > 60) { throw new RangeError("Fade duration must be an integer from 0 through 60."); } return { id, fadeDuration }; } function createComparisonPlaylistEntry(actionId, pairValue, options) { const selectedAction = actionsById.get(actionId); if (!selectedAction) throw new RangeError("The comparison action is unknown."); const pair = normalizePair(pairValue); if (!pair) throw new RangeError("Select two comparison targets first."); const normalized = requireOptions(options); const mapping = buildComparisonSelection(actionId, pair); const entry = { id: normalized.id, builderKey: mapping.builderKey, code: mapping.code, aliases: mapping.aliases, title: pairLabel(pair), detail: selectedAction.label, category: selectedAction.kind === "two-column" ? "plate" : "chart", market: "compare", selection: mapping.selection, enabled: true, fadeDuration: normalized.fadeDuration, graphicType: mapping.selection.graphicType, subtype: mapping.selection.subtype, effectiveMode: mapping.effectiveMode, warning: mapping.warning, operator: Object.freeze({ source: "legacy-comparison-workflow", schemaVersion: SCHEMA_VERSION, actionId, pair, effectiveMode: mapping.effectiveMode, assetPath: runtimeAsset.relativePath, assetSha256: runtimeAsset.sha256 }) }; return entry; } function createYieldBatchEntries(pairValue, options = {}) { if (!options || typeof options !== "object" || Array.isArray(options)) { throw new TypeError("Yield batch options must be an object."); } const idPrefix = cleanText(options.idPrefix ?? "comparison-return", 96); if (!idPrefix || !identifierPattern.test(idPrefix)) throw new TypeError("A safe yield batch id prefix is required."); return Object.freeze(yieldActionIds.map(actionId => createComparisonPlaylistEntry(actionId, pairValue, { id: `${idPrefix}-${actionId.replace("return-", "")}`, fadeDuration: options.fadeDuration }))); } function sameSelection(left, right) { if (!left || typeof left !== "object" || !right || typeof right !== "object") return false; return ["groupCode", "subject", "graphicType", "subtype", "dataCode"] .every(key => typeof left[key] === "string" && left[key] === right[key]); } function restoreComparisonPlaylistEntry(value) { if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.enabled !== "boolean") return null; const operator = value.operator; if (!operator || typeof operator !== "object" || operator.source !== "legacy-comparison-workflow" || operator.schemaVersion !== SCHEMA_VERSION || operator.assetPath !== runtimeAsset.relativePath || operator.assetSha256 !== runtimeAsset.sha256 || !actionsById.has(operator.actionId)) return null; let recreated; try { recreated = createComparisonPlaylistEntry(operator.actionId, operator.pair, { id: value.id, fadeDuration: value.fadeDuration }); } catch { return null; } const scalars = [ "id", "builderKey", "code", "title", "detail", "category", "market", "fadeDuration", "graphicType", "subtype", "effectiveMode", "warning" ]; if (!scalars.every(key => value[key] === recreated[key]) || !Array.isArray(value.aliases) || value.aliases.length !== recreated.aliases.length || !value.aliases.every((alias, index) => alias === recreated.aliases[index]) || !sameSelection(value.selection, recreated.selection) || operator.effectiveMode !== recreated.operator.effectiveMode) return null; return { ...recreated, enabled: value.enabled }; } function createPairRecord(idValue, pairValue) { const id = cleanText(idValue); const pair = normalizePair(pairValue); if (!id || !identifierPattern.test(id) || !pair) return null; return Object.freeze({ id, pair }); } function emptyPairList() { return Object.freeze({ schemaVersion: PAIR_LIST_SCHEMA_VERSION, pairs: Object.freeze([]) }); } function freezePairList(records) { return Object.freeze({ schemaVersion: PAIR_LIST_SCHEMA_VERSION, pairs: Object.freeze(records) }); } function normalizePairList(value) { if (!value || typeof value !== "object" || Array.isArray(value) || value.schemaVersion !== PAIR_LIST_SCHEMA_VERSION || !Array.isArray(value.pairs)) return emptyPairList(); const records = []; const ids = new Set(); for (const raw of value.pairs) { if (records.length >= MAX_SAVED_PAIRS) break; const record = createPairRecord(raw?.id, raw?.pair ?? raw); if (!record || ids.has(record.id)) continue; ids.add(record.id); records.push(record); } return freezePairList(records); } function appendPair(value, recordOrId, pairValue) { const list = normalizePairList(value); const record = typeof recordOrId === "string" ? createPairRecord(recordOrId, pairValue) : createPairRecord(recordOrId?.id, recordOrId?.pair ?? recordOrId); if (!record) throw new TypeError("A valid saved comparison pair is required."); if (list.pairs.length >= MAX_SAVED_PAIRS) throw new RangeError("The saved comparison pair limit was reached."); if (list.pairs.some(value => value.id === record.id)) throw new RangeError("The saved comparison pair id already exists."); return freezePairList([...list.pairs, record]); } function reorderPairList(value, idValue, directionValue) { const list = normalizePairList(value); const id = cleanText(idValue); const direction = directionValue === "up" ? -1 : directionValue === "down" ? 1 : Number(directionValue); if (!id || (direction !== -1 && direction !== 1)) return list; const from = list.pairs.findIndex(value => value.id === id); const to = from + direction; if (from < 0 || to < 0 || to >= list.pairs.length) return list; const records = [...list.pairs]; [records[from], records[to]] = [records[to], records[from]]; return freezePairList(records); } function deletePair(value, idValue) { const list = normalizePairList(value); const id = cleanText(idValue); return id ? freezePairList(list.pairs.filter(value => value.id !== id)) : list; } function deleteAllPairs() { return emptyPairList(); } return { runtimeAsset, marketTargets, marketQuoteTargets: marketTargets, actions, comparisonActions: actions, yieldActionIds, pairListSchemaVersion: PAIR_LIST_SCHEMA_VERSION, normalizeTarget, getMarketTarget, targetKey, subjectTokenForTarget, normalizePair, rotatePair, swapPair, clearPair, pairLabel, getCompatibility, isActionAllowed, buildComparisonSelection, createComparisonPlaylistEntry, createYieldBatchEntries, restoreComparisonPlaylistEntry, refreshComparisonPlaylistEntry: restoreComparisonPlaylistEntry, createPairRecord, normalizePairList, appendPair, reorderPairList, deletePair, deleteAllPairs }; });