(function (root, factory) { "use strict"; const stockWorkflow = typeof module === "object" && module.exports ? require("./operator-workflow.js") : root.MbnOperatorWorkflow; const fixedWorkflow = typeof module === "object" && module.exports ? require("./legacy-fixed-workflow.js") : root.MbnLegacyFixedWorkflow; const themeWorkflow = typeof module === "object" && module.exports ? require("./theme-workflow.js") : root.MbnThemeWorkflow; const expertWorkflow = typeof module === "object" && module.exports ? require("./expert-workflow.js") : root.MbnExpertWorkflow; const comparisonWorkflow = typeof module === "object" && module.exports ? require("./comparison-workflow.js") : root.MbnComparisonWorkflow; const industryWorkflow = typeof module === "object" && module.exports ? require("./industry-workflow.js") : root.MbnIndustryWorkflow; const foreignIndexCandleWorkflow = typeof module === "object" && module.exports ? require("./legacy-foreign-index-candle-workflow.js") : root.MbnLegacyForeignIndexCandleWorkflow; const foreignStockWorkflow = typeof module === "object" && module.exports ? require("./named-foreign-stock-restore-workflow.js") : root.MbnNamedForeignStockRestoreWorkflow; const api = Object.freeze(factory( stockWorkflow, fixedWorkflow, themeWorkflow, expertWorkflow, comparisonWorkflow, industryWorkflow, foreignIndexCandleWorkflow, foreignStockWorkflow)); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnLegacyNamedRowWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function ( stockWorkflow, fixedWorkflow, themeWorkflow, expertWorkflow, comparisonWorkflow, industryWorkflow, foreignIndexCandleWorkflow, foreignStockWorkflow) { "use strict"; if (!stockWorkflow || !fixedWorkflow || !themeWorkflow || !expertWorkflow || !comparisonWorkflow || !industryWorkflow || !foreignIndexCandleWorkflow || !foreignStockWorkflow) { throw new Error("Legacy named-row dependencies are unavailable."); } const selectionKeys = Object.freeze([ "groupCode", "subject", "graphicType", "subtype", "dataCode" ]); const unsafeTextPattern = /[\u0000-\u001f\u007f\uFFFD]/; const themeCodePattern = /^[0-9]{3,8}$/; const expertCodePattern = /^[0-9]{4}$/; function isPlainObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } function isSafeField(value) { return typeof value === "string" && value.length <= 256 && !unsafeTextPattern.test(value) && !value.includes("^"); } function isSafeRawRow(value) { return isPlainObject(value) && selectionKeys.every(key => isSafeField(value[key])); } function sameSelection(left, right) { return isPlainObject(left) && isSafeRawRow(right) && selectionKeys.every(key => isSafeField(left[key]) && left[key] === right[key]); } function splitLegacyLabel(value) { if (!isSafeField(value) || !value) return null; const separator = value.indexOf("_"); return Object.freeze(separator < 0 ? { first: value, remainder: "" } : { first: value.slice(0, separator), remainder: value.slice(separator + 1) }); } function stockLegacySignature(operator) { const stock = operator?.stock; const label = splitLegacyLabel(operator?.legacyLabel); if (operator?.source !== "legacy-stock-workflow" || !isPlainObject(stock) || !label || !["kospi", "kosdaq"].includes(stock.market) || typeof stock.isNxt !== "boolean" || !isSafeField(stock.displayName) || !stock.displayName || !isSafeField(stock.stockCode) || !stock.stockCode) return null; const baseGroup = stock.market === "kospi" ? "코스피" : "코스닥"; return Object.freeze({ groupCode: stock.isNxt ? `${baseGroup}_NXT` : baseGroup, subject: stock.displayName, graphicType: label.first, subtype: label.remainder, dataCode: stock.stockCode }); } function fixedIndexGroup(label) { if (label.includes("코스피_NXT")) return "코스피_NXT"; if (label.includes("코스닥_NXT")) return "코스닥_NXT"; if (label.includes("코스피")) return "코스피"; if (label.includes("코스닥")) return "코스닥"; return "전체"; } function fixedLegacySignature(value) { const tab = value?.legacyTab ?? value?.market; const section = value?.legacySection ?? value?.section; const labelText = value?.legacyLabel ?? value?.label; const label = splitLegacyLabel(labelText); if (!label || !isSafeField(section) || !section || !["overseas", "exchange", "index"].includes(tab)) return null; if (tab === "overseas" || tab === "exchange") { return Object.freeze({ groupCode: tab === "overseas" ? "해외지수" : "환율", subject: label.first, graphicType: section, subtype: label.remainder, dataCode: "" }); } if (["5단 표그래프", "6종목 현재가", "12종목 현재가"].includes(section)) { return Object.freeze({ groupCode: fixedIndexGroup(labelText), subject: "지수", graphicType: section, subtype: labelText, dataCode: "" }); } if (section === "매매동향") { return Object.freeze({ groupCode: fixedIndexGroup(labelText), subject: "지수", graphicType: label.first, subtype: label.remainder, dataCode: "" }); } return Object.freeze({ groupCode: section, subject: "지수", graphicType: label.first, subtype: label.remainder, dataCode: "" }); } function themeLegacyDescriptor(rawRow, options = {}) { const isNxt = rawRow?.groupCode === "테마_NXT"; if (!isSafeRawRow(rawRow) || (rawRow.groupCode !== "테마" && !isNxt) || !rawRow.subject || !themeCodePattern.test(rawRow.dataCode)) return null; const match = /^테마-(현재가|예상체결가)(?:\((입력순|상승률순|하락률순)\))?$/.exec(rawRow.subtype); if (!match) return null; const valueKind = match[1] === "현재가" ? "CURRENT" : "EXPECTED"; if (isNxt && valueKind !== "CURRENT") return null; // The original s5074/s5077/s5088 code falls through to its final // "하락률순" branch when the persisted subtype has no parenthesized sort. const sort = match[2] ? ({ 입력순: "INPUT_ORDER", 상승률순: "GAIN_DESC", 하락률순: "GAIN_ASC" })[match[2]] : "GAIN_ASC"; const actionId = ({ "5단 표그래프|CURRENT": "five-row-current", "5단 표그래프|EXPECTED": "five-row-expected", "6종목 현재가|CURRENT": "six-row-current", "12종목 현재가|CURRENT": "twelve-row-current" })[`${rawRow.graphicType}|${valueKind}`]; if (!actionId) return null; let title = rawRow.subject; let session = null; if (isNxt) { const suffix = "(NXT)"; if (!title.endsWith(suffix) || title.length === suffix.length) return null; title = title.slice(0, -suffix.length); if (!isSafeField(title) || !title || title.includes(suffix)) return null; const suppliedNow = options?.now; const now = suppliedNow instanceof Date && Number.isFinite(suppliedNow.getTime()) ? suppliedNow : new Date(); // Original s5074/s5077/s5088 selects the NXT mark at scene construction: // local hour 00..12 is pre-market and 13..23 is after-market. session = now.getHours() <= 12 ? "PRE_MARKET" : "AFTER_MARKET"; } return Object.freeze({ actionId, sort, theme: Object.freeze({ market: isNxt ? "nxt" : "krx", themeCode: rawRow.dataCode, title, session, itemCount: 1 }) }); } function themeLegacySignature(operator) { const theme = operator?.theme; if (operator?.source !== "legacy-theme-workflow" || !isPlainObject(theme) || theme.market !== "krx" || !themeCodePattern.test(theme.themeCode) || !isSafeField(theme.title) || !theme.title) return null; const action = ({ "five-row-current": ["5단 표그래프", "현재가"], "five-row-expected": ["5단 표그래프", "예상체결가"], "six-row-current": ["6종목 현재가", "현재가"], "twelve-row-current": ["12종목 현재가", "현재가"] })[operator.actionId]; const sortLabel = ({ INPUT_ORDER: "입력순", GAIN_DESC: "상승률순", GAIN_ASC: "하락률순" })[operator.sort]; if (!action || !sortLabel) return null; return Object.freeze({ groupCode: "테마", subject: theme.title, graphicType: action[0], subtype: `테마-${action[1]}(${sortLabel})`, dataCode: theme.themeCode }); } function expertLegacyDescriptor(rawRow) { if (!isSafeRawRow(rawRow) || rawRow.groupCode !== "전문가 추천" || !rawRow.subject || rawRow.graphicType !== "5단 표그래프" || rawRow.subtype !== "현재가" || !expertCodePattern.test(rawRow.dataCode)) return null; return Object.freeze({ expertCode: rawRow.dataCode, expertName: rawRow.subject, source: "oracle", itemCount: 1, previewTruncated: false }); } function expertLegacySignature(operator) { const expert = operator?.expert; if (operator?.source !== "legacy-expert-workflow" || !isPlainObject(expert) || !expertCodePattern.test(expert.expertCode) || !isSafeField(expert.expertName) || !expert.expertName) return null; return Object.freeze({ groupCode: "전문가 추천", subject: expert.expertName, graphicType: "5단 표그래프", subtype: "현재가", dataCode: expert.expertCode }); } function comparisonLegacySignature(operator) { if (operator?.source !== "legacy-comparison-workflow" || !Array.isArray(operator.pair) || operator.pair.length !== 2) return null; const pair = comparisonWorkflow.normalizePair(operator.pair); if (!pair) return null; const subjects = pair.map(target => { if (target.kind === "market-target") return target.displayName; if (target.kind === "krx-stock") return target.stockName; return null; }); if (subjects.some(value => !isSafeField(value) || !value || value.includes(","))) return null; if (["two-column-current", "two-column-expected"].includes(operator.actionId)) { // The original "종목" row discarded each target's market. Only a pair of // closed fixed selectors can be projected without creating a lossy row. if (pair.some(target => target.kind !== "market-target")) return null; const subject = subjects.join(","); // UC1 persisted the generic "종목" group when neither label contained // "지수" (for example an exchange-only pair). That row cannot prove it // was not a same-name world stock, so do not create that lossy signature. if (!subject.includes("지수")) return null; return Object.freeze({ groupCode: "지수", subject, graphicType: "2열판", subtype: operator.actionId === "two-column-expected" ? "예상체결" : "", dataCode: "" }); } if (pair.some(target => target.kind !== "krx-stock")) return null; const action = ({ "comparison-candle": ["종목별 비교분석_캔들 그래프", ""], "comparison-line": ["종목별 비교분석_라인 그래프", ""], "return-5d": ["종목별 수익률 비교", "5일"], "return-1m": ["종목별 수익률 비교", "1개월"], "return-3m": ["종목별 수익률 비교", "3개월"], "return-6m": ["종목별 수익률 비교", "6개월"], "return-12m": ["종목별 수익률 비교", "12개월"] })[operator.actionId]; if (!action) return null; return Object.freeze({ groupCode: pair.map(target => target.market === "kospi" ? "코스피" : "코스닥").join(","), subject: subjects.join(","), graphicType: action[0], subtype: action[1], dataCode: "" }); } function industryFixedDescriptor(rawRow) { if (!isSafeRawRow(rawRow) || rawRow.subtype !== "" || rawRow.dataCode !== "") return null; const market = ({ "업종_코스피": "kospi", "업종_코스닥": "kosdaq" })[rawRow.groupCode]; if (!market || rawRow.subject !== (market === "kospi" ? "코스피" : "코스닥")) return null; const cutId = ({ "5단 표그래프": "five-row", "네모그래프": "square", "섹터지수": "sector" })[rawRow.graphicType]; return cutId ? Object.freeze({ market, cutId }) : null; } function industryFixedLegacySignature(operator) { if (operator?.source !== "legacy-industry-workflow" || !["kospi", "kosdaq"].includes(operator.market)) return null; const graphicType = ({ "five-row": "5단 표그래프", square: "네모그래프", sector: "섹터지수" })[operator.cutId]; if (!graphicType) return null; return Object.freeze({ groupCode: operator.market === "kospi" ? "업종_코스피" : "업종_코스닥", subject: operator.market === "kospi" ? "코스피" : "코스닥", graphicType, subtype: "", dataCode: "" }); } function matchesFixedActionRaw(rawRow, action) { return Boolean(action?.available && sameSelection(fixedLegacySignature(action), rawRow)); } function matchesCanonicalEntry(entry, rawRow, options = {}) { if (!isPlainObject(entry) || !isSafeRawRow(rawRow)) return false; if (sameSelection(entry.selection, rawRow)) return true; let signature = null; switch (entry.operator?.source) { case "legacy-stock-workflow": signature = stockLegacySignature(entry.operator); break; case "legacy-fixed-workflow": signature = fixedLegacySignature(entry.operator); break; case "legacy-theme-workflow": if (entry.operator?.theme?.market === "nxt") { const descriptor = themeLegacyDescriptor(rawRow, options); return Boolean(descriptor && descriptor.theme.market === "nxt" && descriptor.actionId === entry.operator.actionId && descriptor.sort === entry.operator.sort && descriptor.theme.themeCode === entry.operator.theme.themeCode && descriptor.theme.title === entry.operator.theme.title && descriptor.theme.session === entry.operator.theme.session); } signature = themeLegacySignature(entry.operator); if (sameSelection(signature, rawRow)) return true; if (entry.operator.sort === "GAIN_ASC" && signature) { const originalFallback = { ...signature, subtype: signature.subtype.replace(/\(하락률순\)$/, "") }; return sameSelection(originalFallback, rawRow); } break; case "legacy-expert-workflow": signature = expertLegacySignature(entry.operator); break; case "legacy-comparison-workflow": signature = comparisonLegacySignature(entry.operator); break; case "legacy-industry-workflow": signature = industryFixedLegacySignature(entry.operator); break; case "legacy-foreign-index-candle-workflow": return foreignIndexCandleWorkflow.matchesRaw(entry, rawRow); case "legacy-named-foreign-stock-workflow": return foreignStockWorkflow.matchesRaw(entry, rawRow); default: return false; } return sameSelection(signature, rawRow); } function fixedRestoreNow(entry) { const dateMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(entry?.selection?.dataCode || ""); const now = dateMatch ? new Date(Number(dateMatch[1]), Number(dateMatch[2]) - 1, Number(dateMatch[3]), 9, 0, 0, 0) : new Date(); if (entry?.selection?.graphicType === "PRE_MARKET") now.setHours(9, 0, 0, 0); if (entry?.selection?.graphicType === "AFTER_MARKET") now.setHours(14, 0, 0, 0); return now; } function restoreTrustedEntry(entry) { try { switch (entry?.operator?.source) { case "legacy-stock-workflow": return stockWorkflow.restoreStockPlaylistEntry(entry); case "legacy-fixed-workflow": return fixedWorkflow.restoreFixedPlaylistEntry(entry, { now: fixedRestoreNow(entry) }); case "legacy-theme-workflow": return themeWorkflow.restoreThemePlaylistEntry(entry); case "legacy-expert-workflow": return expertWorkflow.restoreExpertPlaylistEntry(entry); case "legacy-comparison-workflow": return comparisonWorkflow.restoreComparisonPlaylistEntry(entry); case "legacy-industry-workflow": return industryWorkflow.restoreIndustryPlaylistEntry(entry); case "legacy-foreign-index-candle-workflow": return foreignIndexCandleWorkflow.restorePlaylistEntry(entry); case "legacy-named-foreign-stock-workflow": return foreignStockWorkflow.restorePlaylistEntry(entry); default: return null; } } catch { return null; } } function legacySelectionForEntry(entry) { if (!isPlainObject(entry) || !isPlainObject(entry.selection) || !selectionKeys.every(key => isSafeField(entry.selection[key]))) return null; const trusted = restoreTrustedEntry(entry); if (!trusted || trusted.enabled !== entry.enabled) return null; switch (trusted.operator?.source) { case "legacy-stock-workflow": return stockLegacySignature(trusted.operator); case "legacy-fixed-workflow": return fixedLegacySignature(trusted.operator); case "legacy-theme-workflow": // The original NXT row did not persist PRE/AFTER session. Do not create // a new lossy row that this closed restore contract must later block. return themeLegacySignature(trusted.operator); case "legacy-expert-workflow": return expertLegacySignature(trusted.operator); case "legacy-comparison-workflow": return comparisonLegacySignature(trusted.operator); case "legacy-industry-workflow": return industryFixedLegacySignature(trusted.operator); case "legacy-foreign-index-candle-workflow": return foreignIndexCandleWorkflow.legacySelectionForEntry(trusted); case "legacy-named-foreign-stock-workflow": return foreignStockWorkflow.legacySelectionForEntry(trusted); default: return null; } } function semanticKey(entry) { const operator = entry?.operator; return JSON.stringify({ builderKey: entry?.builderKey, code: entry?.code, selection: entry?.selection, source: operator?.source ?? "", actionId: operator?.actionId ?? "", cutId: operator?.cutId ?? "" }); } function selectUniqueCanonicalEntry(candidates, rawRow, options = {}) { if (!Array.isArray(candidates) || !isSafeRawRow(rawRow)) return null; const matches = new Map(); for (const candidate of candidates) { if (!matchesCanonicalEntry(candidate, rawRow, options)) continue; const key = semanticKey(candidate); if (!matches.has(key)) matches.set(key, candidate); } return matches.size === 1 ? [...matches.values()][0] : null; } return { selectionKeys, fixedLegacySignature, comparisonLegacySignature, industryFixedDescriptor, industryFixedLegacySignature, matchesFixedActionRaw, themeLegacyDescriptor, expertLegacyDescriptor, matchesCanonicalEntry, legacySelectionForEntry, selectUniqueCanonicalEntry }; });