(function (root, factory) { "use strict"; const api = Object.freeze(factory()); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnOperatorWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function () { "use strict"; const DEFAULT_FADE_DURATION = 6; const MAX_TEXT_LENGTH = 128; const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/; const stockCodePattern = /^[A-Za-z0-9]{1,32}$/; const controlCharacterPattern = /[\u0000-\u001f\u007f]/; const runtimeStockAsset = Object.freeze({ relativePath: "bin/Debug/Res/종목.ini", sha256: "45DFB1804828F0E4A45F73D681AF0709CE5662872FAA49CFC9F7A0B940409E20" }); function freezeCut(cut) { return Object.freeze({ ...cut, section: cut.group, alias: cut.aliases[0], cutCode: cut.aliases[0], aliases: Object.freeze([...cut.aliases]) }); } // bin/Debug/Res/종목.ini (SHA-256 45DFB180...940409E20), in runtime order. // The two '-' separator rows are deliberately absent. The source RES copy differs // at the after-hours label, so the operator-visible runtime asset is authoritative. const stockCuts = Object.freeze([ freezeCut({ id: "basic-expected", label: "1열판기본_예상체결가", group: "1열판 기본", detail: "예상체결가", builderKey: "s5001", aliases: ["5001"], kind: "basic", subtype: "EXPECTED_OPENING" }), freezeCut({ id: "basic-current", label: "1열판기본_현재가", group: "1열판 기본", detail: "현재가", builderKey: "s5001", aliases: ["5001", "N5001"], kind: "basic", subtype: "CURRENT" }), freezeCut({ id: "basic-after-hours", label: "1열판기본_시간외단일가", group: "1열판 기본", detail: "시간외단일가", builderKey: "s5001", aliases: ["5001"], kind: "basic", subtype: "AFTER_HOURS_SINGLE_PRICE" }), freezeCut({ id: "detail-open", label: "1열판상세_시가", group: "1열판 상세", detail: "시가", builderKey: "s5006", aliases: ["5006"], kind: "open-detail", subtype: "시가" }), freezeCut({ id: "detail-face-value", label: "1열판상세_액면가", group: "1열판 상세", detail: "액면가", builderKey: "s5011", aliases: ["5011"], kind: "detail", subtype: "FaceValue" }), freezeCut({ id: "detail-pbr", label: "1열판상세_PBR", group: "1열판 상세", detail: "PBR", builderKey: "s5011", aliases: ["5011"], kind: "detail", subtype: "Valuation" }), freezeCut({ id: "detail-volume", label: "1열판상세_거래량", group: "1열판 상세", detail: "거래량", builderKey: "s5011", aliases: ["5011"], kind: "detail", subtype: "Volume" }), freezeCut({ id: "yield-5d", label: "수익률그래프_5일", group: "수익률 그래프", detail: "5일", builderKey: "s5086", aliases: ["5086"], kind: "yield", subtype: "5일" }), freezeCut({ id: "yield-20d", label: "수익률그래프_20일", group: "수익률 그래프", detail: "20일", builderKey: "s5086", aliases: ["5086"], kind: "yield", subtype: "20일" }), freezeCut({ id: "yield-60d", label: "수익률그래프_60일", group: "수익률 그래프", detail: "60일", builderKey: "s5086", aliases: ["5086"], kind: "yield", subtype: "60일" }), freezeCut({ id: "yield-120d", label: "수익률그래프_120일", group: "수익률 그래프", detail: "120일", builderKey: "s5086", aliases: ["5086"], kind: "yield", subtype: "120일" }), freezeCut({ id: "yield-240d", label: "수익률그래프_240일", group: "수익률 그래프", detail: "240일", builderKey: "s5086", aliases: ["5086"], kind: "yield", subtype: "240일" }), freezeCut({ id: "candle-daily", label: "캔들그래프_일봉", group: "캔들 그래프", detail: "일봉", builderKey: "s8010", aliases: ["8035"], kind: "candle", subtype: "PRICE" }), freezeCut({ id: "candle-5d", label: "캔들그래프_5일", group: "캔들 그래프", detail: "5일", builderKey: "s8010", aliases: ["8061"], kind: "candle", subtype: "PRICE" }), freezeCut({ id: "candle-20d", label: "캔들그래프_20일", group: "캔들 그래프", detail: "20일", builderKey: "s8010", aliases: ["8040"], kind: "candle", subtype: "PRICE" }), freezeCut({ id: "candle-60d", label: "캔들그래프_60일", group: "캔들 그래프", detail: "60일", builderKey: "s8010", aliases: ["8046"], kind: "candle", subtype: "PRICE" }), freezeCut({ id: "candle-120d", label: "캔들그래프_120일", group: "캔들 그래프", detail: "120일", builderKey: "s8010", aliases: ["8051"], kind: "candle", subtype: "PRICE" }), freezeCut({ id: "candle-240d", label: "캔들그래프_240일", group: "캔들 그래프", detail: "240일", builderKey: "s8010", aliases: ["8056"], kind: "candle", subtype: "PRICE" }), freezeCut({ id: "candle-volume-daily", label: "캔들그래프(거래량)_일봉", group: "캔들 그래프 · 거래량", detail: "일봉", builderKey: "s8010", aliases: ["8035"], kind: "candle", subtype: "VOLUME" }), freezeCut({ id: "candle-volume-5d", label: "캔들그래프(거래량)_5일", group: "캔들 그래프 · 거래량", detail: "5일", builderKey: "s8010", aliases: ["8061"], kind: "candle", subtype: "VOLUME" }), freezeCut({ id: "candle-volume-20d", label: "캔들그래프(거래량)_20일", group: "캔들 그래프 · 거래량", detail: "20일", builderKey: "s8010", aliases: ["8040"], kind: "candle", subtype: "VOLUME" }), freezeCut({ id: "candle-volume-60d", label: "캔들그래프(거래량)_60일", group: "캔들 그래프 · 거래량", detail: "60일", builderKey: "s8010", aliases: ["8046"], kind: "candle", subtype: "VOLUME" }), freezeCut({ id: "candle-volume-120d", label: "캔들그래프(거래량)_120일", group: "캔들 그래프 · 거래량", detail: "120일", builderKey: "s8010", aliases: ["8051"], kind: "candle", subtype: "VOLUME" }), freezeCut({ id: "candle-volume-240d", label: "캔들그래프(거래량)_240일", group: "캔들 그래프 · 거래량", detail: "240일", builderKey: "s8010", aliases: ["8056"], kind: "candle", subtype: "VOLUME" }), freezeCut({ id: "candle-expected-5d", label: "캔들그래프(예상체결가)_5일", group: "캔들 그래프 · 예상체결가", detail: "5일", builderKey: "s8010", aliases: ["8061"], kind: "candle", subtype: "EXPECTED" }), freezeCut({ id: "candle-expected-20d", label: "캔들그래프(예상체결가)_20일", group: "캔들 그래프 · 예상체결가", detail: "20일", builderKey: "s8010", aliases: ["8040"], kind: "candle", subtype: "EXPECTED" }), freezeCut({ id: "candle-expected-60d", label: "캔들그래프(예상체결가)_60일", group: "캔들 그래프 · 예상체결가", detail: "60일", builderKey: "s8010", aliases: ["8046"], kind: "candle", subtype: "EXPECTED" }), freezeCut({ id: "candle-expected-120d", label: "캔들그래프(예상체결가)_120일", group: "캔들 그래프 · 예상체결가", detail: "120일", builderKey: "s8010", aliases: ["8051"], kind: "candle", subtype: "EXPECTED" }), freezeCut({ id: "candle-expected-240d", label: "캔들그래프(예상체결가)_240일", group: "캔들 그래프 · 예상체결가", detail: "240일", builderKey: "s8010", aliases: ["8056"], kind: "candle", subtype: "EXPECTED" }), freezeCut({ id: "order-book", label: "호가창_표그래프", group: "표 그래프", detail: "호가창", builderKey: "s8003", aliases: ["8003"], kind: "order-book", subtype: "TABLE_GRAPH" }), freezeCut({ id: "traders", label: "거래원_표그래프", group: "표 그래프", detail: "거래원", builderKey: "s5037", aliases: ["5037"], kind: "traders", subtype: "TABLE_GRAPH" }) ]); if (stockCuts.length !== 31 || new Set(stockCuts.map(cut => cut.id)).size !== 31) { throw new Error("The legacy stock workflow must contain exactly 31 unique cut variants."); } function manualAction(id, label, builderKey, alias, table, prerequisite) { return Object.freeze({ id, label, builderKey, alias, cutCode: alias, aliases: Object.freeze([alias]), available: false, autoAdd: false, status: "unavailable", prerequisiteTable: table, prerequisites: Object.freeze([ `선택 종목에 대한 ${table} 수동 입력 데이터`, prerequisite, "수동 데이터 검증과 명시적 플레이리스트 추가 흐름" ]) }); } // These buttons opened GraphE/INPUT_* manual-data workflows in the source program. // A stock search result alone is not sufficient input, so they must never auto-add a cut. const manualStockActions = Object.freeze([ manualAction("manual-revenue-mix", "주요매출 구성", "s5076", "5076", "INPUT_PIE", "구성 항목과 비중 및 기준일 입력"), manualAction("manual-growth-metrics", "성장성 지표", "s5079", "5079", "INPUT_GROW", "성장성 지표 값과 분기 입력"), manualAction("manual-sales", "매출액", "s5080", "5080", "INPUT_SELL", "기간별 매출액 수동 값 입력"), manualAction("manual-operating-profit", "영업이익", "s5081", "5081", "INPUT_PROFIT", "기간별 영업이익 수동 값 입력") ]); const cutsById = new Map(stockCuts.map(cut => [cut.id, cut])); function valueFrom(raw, names) { for (const name of names) { if (Object.prototype.hasOwnProperty.call(raw, name) && raw[name] !== undefined && raw[name] !== null) { return raw[name]; } } const keys = Object.keys(raw); for (const name of names) { const key = keys.find(candidate => candidate.toLocaleUpperCase("en-US") === name.toLocaleUpperCase("en-US")); if (key !== undefined && raw[key] !== undefined && raw[key] !== null) return raw[key]; } return undefined; } function cleanText(value) { if (typeof value !== "string") return null; const text = value.trim(); return text.length > 0 && text.length <= MAX_TEXT_LENGTH && !controlCharacterPattern.test(text) ? text : null; } function normalizeMarket(value) { const text = String(value ?? "") .trim() .toLocaleUpperCase("en-US") .replace(/[\s-]+/g, "_"); const isNxt = text.includes("NXT"); if (["KOSPI", "KOSPI_STOCK", "코스피", "코스피종목", "NXT_KOSPI", "KOSPI_NXT", "코스피_NXT", "NXT코스피"].includes(text)) { return { market: "kospi", isNxt }; } if (["KOSDAQ", "KOSDAQ_STOCK", "코스닥", "코스닥종목", "NXT_KOSDAQ", "KOSDAQ_NXT", "코스닥_NXT", "NXT코스닥"].includes(text)) { return { market: "kosdaq", isNxt }; } return null; } function normalizeBoolean(value) { if (value === true || value === false) return value; if (value === 1 || value === "1") return true; if (value === 0 || value === "0") return false; return null; } function validateStockResult(raw) { if (!raw || typeof raw !== "object" || Array.isArray(raw)) { return { valid: false, error: "stock-object-required", value: null }; } const marketValue = valueFrom(raw, ["market", "MARKET", "marketName", "MARKET_NAME", "groupCode", "GROUP_CODE"]); const normalizedMarket = normalizeMarket(marketValue); if (!normalizedMarket) return { valid: false, error: "unsupported-market", value: null }; const rawStockName = cleanText(valueFrom(raw, [ "stockName", "STOCK_NAME", "F_STOCK_NAME", "F_STOCK_WANNAME", "F_KNAM", "F_INPUT_NAME", "NAME", "WANNAME" ])); const rawDisplayName = cleanText(valueFrom(raw, [ "displayName", "DISPLAY_NAME", "F_STOCK_WANNAME", "F_STOCK_NAME", "F_KNAM", "F_INPUT_NAME", "NAME", "WANNAME" ])); if (!rawStockName && !rawDisplayName) { return { valid: false, error: "stock-name-required", value: null }; } const rawCode = valueFrom(raw, [ "stockCode", "STOCK_CODE", "F_STOCK_CODE", "F_SYMB", "SYMB", "F_PART_CODE", "CODE" ]); const stockCode = typeof rawCode === "number" && Number.isSafeInteger(rawCode) && rawCode >= 0 ? String(rawCode) : cleanText(rawCode); if (!stockCode || !stockCodePattern.test(stockCode)) { return { valid: false, error: "invalid-stock-code", value: null }; } const explicitNxtValue = valueFrom(raw, ["isNxt", "IS_NXT", "nxt", "NXT"]); const explicitNxt = explicitNxtValue === undefined ? null : normalizeBoolean(explicitNxtValue); if (explicitNxtValue !== undefined && explicitNxt === null) { return { valid: false, error: "invalid-nxt-flag", value: null }; } const nameImpliesNxt = `${rawStockName || ""} ${rawDisplayName || ""}`.includes("(NXT)"); const inferredNxt = normalizedMarket.isNxt || nameImpliesNxt; if (explicitNxt === false && inferredNxt) { return { valid: false, error: "inconsistent-nxt-selection", value: null }; } const isNxt = explicitNxt === true || inferredNxt; const withoutNxtSuffix = value => value.replace(/\s*\(NXT\)\s*/g, "").trim(); const stockName = cleanText(withoutNxtSuffix(rawStockName || rawDisplayName)); if (!stockName) return { valid: false, error: "stock-name-required", value: null }; const baseDisplayName = cleanText(rawDisplayName || rawStockName); if (!baseDisplayName) return { valid: false, error: "display-name-required", value: null }; const displayName = isNxt ? `${withoutNxtSuffix(baseDisplayName)}(NXT)` : baseDisplayName; return { valid: true, error: null, value: Object.freeze({ market: normalizedMarket.market, stockName, displayName, stockCode, isNxt }) }; } function normalizeStockResult(raw) { const validation = validateStockResult(raw); return validation.valid ? validation.value : null; } function getStockCut(cutId) { return typeof cutId === "string" ? cutsById.get(cutId) || null : null; } function isStockCutAllowed(stock, cutId) { const normalized = normalizeStockResult(stock); const cut = getStockCut(cutId); return Boolean(normalized && cut && (!normalized.isNxt || cut.id === "basic-current")); } function requireOptions(options) { if (!options || typeof options !== "object" || Array.isArray(options)) { throw new TypeError("Stock playlist options are required."); } const id = cleanText(options.id); if (!id || !identifierPattern.test(id)) { throw new TypeError("A safe stock playlist entry id is required."); } const ma5 = options.ma5 === undefined ? false : normalizeBoolean(options.ma5); const ma20 = options.ma20 === undefined ? false : normalizeBoolean(options.ma20); if (ma5 === null || ma20 === null) { throw new TypeError("Moving-average flags must be boolean values."); } 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, ma5, ma20, fadeDuration }; } function marketGroup(stock, stockSuffix) { const prefix = stock.isNxt ? "NXT_" : ""; return `${prefix}${stock.market.toLocaleUpperCase("en-US")}${stockSuffix}`; } function selectionFor(stock, cut, movingAverages) { const common = { subject: stock.displayName, dataCode: stock.stockCode }; switch (cut.kind) { case "basic": return { groupCode: marketGroup(stock, ""), ...common, graphicType: "1열판기본", subtype: cut.subtype }; case "open-detail": return { groupCode: marketGroup(stock, "_STOCK"), ...common, graphicType: "1열판상세", subtype: cut.subtype }; case "detail": return { groupCode: marketGroup(stock, "_STOCK"), ...common, graphicType: "1열판상세", subtype: cut.subtype }; case "yield": return { groupCode: marketGroup(stock, ""), ...common, graphicType: "수익률그래프", subtype: cut.subtype }; case "candle": { const flags = []; if (movingAverages.ma5) flags.push("MA5"); if (movingAverages.ma20) flags.push("MA20"); return { groupCode: marketGroup(stock, "_STOCK"), ...common, graphicType: flags.join(","), subtype: cut.subtype }; } case "order-book": return { groupCode: marketGroup(stock, ""), ...common, graphicType: "ORDER_BOOK", subtype: "TABLE_GRAPH" }; case "traders": return { groupCode: marketGroup(stock, ""), ...common, graphicType: "TRADER", subtype: "TABLE_GRAPH" }; default: throw new Error("The stock cut mapping is unavailable."); } } function createStockPlaylistEntry(stock, cutId, options) { const normalized = normalizeStockResult(stock); if (!normalized) throw new TypeError("A valid selected stock is required."); const cut = getStockCut(cutId); if (!cut) throw new RangeError("The selected stock cut is unknown."); if (!isStockCutAllowed(normalized, cut.id)) { throw new RangeError("NXT stocks support only 1열판기본_현재가."); } const normalizedOptions = requireOptions(options); const movingAverages = Object.freeze({ ma5: cut.kind === "candle" && normalizedOptions.ma5, ma20: cut.kind === "candle" && normalizedOptions.ma20 }); const selection = Object.freeze(selectionFor(normalized, cut, movingAverages)); const code = normalized.isNxt ? "N5001" : cut.aliases[0]; const operator = Object.freeze({ source: "legacy-stock-workflow", cutId: cut.id, legacyLabel: cut.label, legacyGroup: cut.group, legacyDetail: cut.detail, stock: normalized, movingAverages }); return { id: normalizedOptions.id, builderKey: cut.builderKey, code, aliases: [...cut.aliases], title: normalized.displayName, detail: cut.label, category: "stock", market: normalized.market, stockName: normalized.stockName, displayName: normalized.displayName, stockCode: normalized.stockCode, isNxt: normalized.isNxt, selection, enabled: true, fadeDuration: normalizedOptions.fadeDuration, graphicType: selection.graphicType, subtype: selection.subtype, operator }; } function sameStringArray(left, right) { return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => value === right[index]); } 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 restoreStockPlaylistEntry(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-stock-workflow" || typeof operator.cutId !== "string" || !operator.movingAverages || typeof operator.movingAverages !== "object" || typeof operator.movingAverages.ma5 !== "boolean" || typeof operator.movingAverages.ma20 !== "boolean") { return null; } let recreated; try { recreated = createStockPlaylistEntry(operator.stock, operator.cutId, { id: value.id, ma5: operator.movingAverages.ma5, ma20: operator.movingAverages.ma20, fadeDuration: value.fadeDuration }); } catch { return null; } const trustedScalarKeys = [ "id", "builderKey", "code", "title", "detail", "category", "market", "stockName", "displayName", "stockCode", "isNxt", "fadeDuration", "graphicType", "subtype" ]; if (!trustedScalarKeys.every(key => value[key] === recreated[key]) || !sameStringArray(value.aliases, recreated.aliases) || !sameSelection(value.selection, recreated.selection) || operator.legacyLabel !== recreated.operator.legacyLabel || operator.legacyGroup !== recreated.operator.legacyGroup || operator.legacyDetail !== recreated.operator.legacyDetail) { return null; } return { ...recreated, enabled: value.enabled }; } return { runtimeStockAsset, stockCuts, manualStockActions, normalizeMarket, validateStockResult, normalizeStockResult, getStockCut, isStockCutAllowed, createStockPlaylistEntry, restoreStockPlaylistEntry }; });