(function (root, factory) { "use strict"; const api = Object.freeze(factory()); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnIndustryWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function () { "use strict"; const DEFAULT_FADE_DURATION = 6; const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/; const codePattern = /^[A-Za-z0-9]{1,32}$/; const controlCharacterPattern = /[\u0000-\u001f\u007f]/; const datePattern = /^\d{4}-\d{2}-\d{2}$/; const markets = Object.freeze(["kospi", "kosdaq"]); const runtimeAssets = Object.freeze({ kospi: Object.freeze({ relativePath: "bin/Debug/Res/업종_코스피.ini", sha256: "F21DE58003315EAB41F9FE7B5A573C71653056203E6743D05AA96E7FF1B8BF52" }), kosdaq: Object.freeze({ relativePath: "bin/Debug/Res/업종_코스닥.ini", sha256: "F25B8926E8F20FC5FBC94A1891A9E2EAD22852E8C829321EF1A001FE3F42ECE4" }) }); const periodCuts = [ ["5일", "8061", "FiveDays"], ["20일", "8040", "TwentyDays"], ["60일", "8046", "SixtyDays"], ["120일", "8051", "OneHundredTwentyDays"], ["240일", "8056", "TwoHundredFortyDays"] ]; const candleCuts = [["일봉", "8035"], ...periodCuts.map(([label, code]) => [label, code])]; function cut(id, label, section, kind, details = {}) { return Object.freeze({ id, label, section, kind, ...details }); } const cuts = Object.freeze([ cut("one-column", "1열판", "업종 판", "single", { builderKey: "s5001" }), cut("two-column", "2열판", "업종 판", "pair", { builderKey: "s8018" }), ...periodCuts.map(([label, , period]) => cut(`yield-${label}`, `수익률그래프_${label}`, "수익률 그래프", "yield", { builderKey: "s5086", period })), ...candleCuts.map(([label, code]) => cut(`candle-${label}`, `캔들그래프_${label}`, "캔들 그래프", "candle", { builderKey: "s8010", code, mode: "PRICE" })), ...candleCuts.map(([label, code]) => cut(`candle-volume-${label}`, `캔들그래프(거래량)_${label}`, "캔들 그래프 · 거래량", "candle", { builderKey: "s8010", code, mode: "VOLUME" })), cut("five-row", "5단 표그래프", "시장 전체", "fixed", { builderKey: "s5074" }), cut("square", "네모그래프", "시장 전체", "fixed", { builderKey: "s8001" }), cut("sector", "섹터지수", "시장 전체", "fixed", { builderKey: "s5078" }) ]); if (cuts.length !== 22 || new Set(cuts.map(value => value.id)).size !== 22) { throw new Error("Each legacy industry tab must contain exactly 22 cut actions."); } const cutsById = new Map(cuts.map(value => [value.id, value])); 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 normalizeMarket(value) { const text = String(value || "").trim().toLocaleLowerCase("en-US"); return markets.includes(text) ? text : null; } function normalizeIndustry(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return null; const market = normalizeMarket(value.market); const name = cleanText(value.name ?? value.industryName); const code = cleanText(value.code ?? value.industryCode, 32); if (!market || !name || !code || !codePattern.test(code)) return null; return Object.freeze({ market, name, code }); } function localDateText(now = new Date()) { const year = String(now.getFullYear()).padStart(4, "0"); const month = String(now.getMonth() + 1).padStart(2, "0"); const day = String(now.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; } function requireOptions(options) { if (!options || typeof options !== "object" || Array.isArray(options)) { throw new TypeError("Industry playlist options are required."); } const id = cleanText(options.id); if (!id || !identifierPattern.test(id)) throw new TypeError("A safe industry 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."); } const now = options.now instanceof Date && !Number.isNaN(options.now.valueOf()) ? options.now : new Date(); const ma5 = options.ma5 === undefined ? false : options.ma5; const ma20 = options.ma20 === undefined ? false : options.ma20; if (typeof ma5 !== "boolean" || typeof ma20 !== "boolean") { throw new TypeError("Moving-average flags must be boolean values."); } return { id, fadeDuration, now, ma5, ma20 }; } function mappingFor(market, selectedCut, selected, primary, secondary, now, movingAverages) { const industryGroup = market === "kospi" ? "INDUSTRY_KOSPI" : "INDUSTRY_KOSDAQ"; const candleGroup = market === "kospi" ? "KOSPI_INDUSTRY" : "KOSDAQ_INDUSTRY"; const pagedGroup = market === "kospi" ? "PAGED_DOMESTIC_KOSPI" : "PAGED_DOMESTIC_KOSDAQ"; const marketName = market === "kospi" ? "KOSPI" : "KOSDAQ"; switch (selectedCut.kind) { case "single": if (!selected) throw new RangeError("Select one industry first."); return { code: "5001", selection: { groupCode: industryGroup, subject: selected.name, graphicType: "1열판", subtype: "CURRENT", dataCode: "" } }; case "pair": if (!primary || !secondary || primary.name.includes("-") || secondary.name.includes("-")) { throw new RangeError("Select two delimiter-safe industries first."); } return { code: market === "kospi" ? "8018" : "8032", selection: { groupCode: industryGroup, subject: `${primary.name}-${secondary.name}`, graphicType: "2열판", subtype: "", dataCode: "" } }; case "yield": if (!selected) throw new RangeError("Select one industry first."); return { code: "5086", selection: { groupCode: industryGroup, subject: selected.name, graphicType: "YIELD_GRAPH", subtype: selectedCut.period, dataCode: "" } }; case "candle": if (!selected) throw new RangeError("Select one industry first."); const flags = []; if (movingAverages.ma5) flags.push("MA5"); if (movingAverages.ma20) flags.push("MA20"); return { code: selectedCut.code, selection: { groupCode: candleGroup, subject: selected.name, graphicType: flags.join(","), subtype: selectedCut.mode, dataCode: "" } }; case "fixed": if (selectedCut.id === "five-row") { return { code: "5074", selection: { groupCode: pagedGroup, subject: "INDEX", graphicType: "", subtype: "INDUSTRY", dataCode: localDateText(now) } }; } if (selectedCut.id === "square") { return { code: market === "kospi" ? "8001" : "8002", selection: { groupCode: industryGroup, subject: marketName, graphicType: "네모그래프", subtype: "", dataCode: "" } }; } return { code: "5078", selection: { groupCode: industryGroup, subject: marketName, graphicType: "SECTOR_INDEX", subtype: "", dataCode: "" } }; default: throw new Error("The selected industry cut is unsupported."); } } function isCutAllowed(marketValue, cutId, selectedValue, primaryValue, secondaryValue) { const market = normalizeMarket(marketValue); const selectedCut = cutsById.get(cutId); const selected = normalizeIndustry(selectedValue); const primary = normalizeIndustry(primaryValue); const secondary = normalizeIndustry(secondaryValue); if (!market || !selectedCut) return false; if (selected && selected.market !== market) return false; if (primary && primary.market !== market) return false; if (secondary && secondary.market !== market) return false; if (selectedCut.kind === "fixed") return true; if (selectedCut.kind === "pair") { return Boolean(primary && secondary && !primary.name.includes("-") && !secondary.name.includes("-")); } return Boolean(selected); } function createIndustryPlaylistEntry(marketValue, cutId, options) { const market = normalizeMarket(marketValue); const selectedCut = cutsById.get(cutId); if (!market || !selectedCut) throw new RangeError("The industry action is unknown."); const selected = normalizeIndustry(options?.selected); const pair = Array.isArray(options?.pair) ? options.pair : []; const primary = normalizeIndustry(pair[0]); const secondary = normalizeIndustry(pair[1]); if ((selected && selected.market !== market) || (primary && primary.market !== market) || (secondary && secondary.market !== market)) { throw new RangeError("The selected industry market is inconsistent."); } if (!isCutAllowed(market, cutId, selected, primary, secondary)) { throw new RangeError(selectedCut.kind === "pair" ? "Select two industries first." : "Select one industry first."); } const normalized = requireOptions(options); const movingAverages = Object.freeze({ ma5: selectedCut.kind === "candle" && normalized.ma5, ma20: selectedCut.kind === "candle" && normalized.ma20 }); const mapping = mappingFor( market, selectedCut, selected, primary, secondary, normalized.now, movingAverages); const selection = Object.freeze(mapping.selection); return { id: normalized.id, builderKey: selectedCut.builderKey, code: mapping.code, aliases: [mapping.code], title: selectedCut.kind === "fixed" ? `${market === "kospi" ? "코스피" : "코스닥"} ${selectedCut.label}` : (selectedCut.kind === "pair" ? `${primary.name}-${secondary.name}` : selected.name), detail: selectedCut.label, category: selectedCut.kind === "candle" || selectedCut.kind === "yield" ? "chart" : "plate", market, selection, enabled: true, fadeDuration: normalized.fadeDuration, graphicType: selection.graphicType, subtype: selection.subtype, operator: Object.freeze({ source: "legacy-industry-workflow", cutId: selectedCut.id, market, ...(selectedCut.kind === "pair" ? { pair: Object.freeze([primary, secondary]) } : {}), ...(selectedCut.kind !== "pair" && selectedCut.kind !== "fixed" ? { selected } : {}), assetPath: runtimeAssets[market].relativePath, assetSha256: runtimeAssets[market].sha256, movingAverages }) }; } function sameSelection(left, right, allowDynamicDate) { if (!left || typeof left !== "object" || !right || typeof right !== "object") return false; return ["groupCode", "subject", "graphicType", "subtype", "dataCode"].every(key => { if (allowDynamicDate && key === "dataCode") return typeof left[key] === "string" && datePattern.test(left[key]); return typeof left[key] === "string" && left[key] === right[key]; }); } function restoreIndustryPlaylistEntry(value, options = {}) { 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-industry-workflow") return null; const market = normalizeMarket(operator.market); const selectedCut = cutsById.get(operator.cutId); if (!market || !selectedCut || operator.assetPath !== runtimeAssets[market].relativePath || operator.assetSha256 !== runtimeAssets[market].sha256) return null; let movingAverages = operator.movingAverages; if (movingAverages === undefined) { if (selectedCut.kind === "candle") { const graphicType = value?.selection?.graphicType; if (!["", "MA5", "MA20", "MA5,MA20"].includes(graphicType)) return null; movingAverages = { ma5: graphicType === "MA5" || graphicType === "MA5,MA20", ma20: graphicType === "MA20" || graphicType === "MA5,MA20" }; } else { movingAverages = { ma5: false, ma20: false }; } } if (!movingAverages || typeof movingAverages !== "object" || Array.isArray(movingAverages) || typeof movingAverages.ma5 !== "boolean" || typeof movingAverages.ma20 !== "boolean" || Object.keys(movingAverages).length !== 2 || (selectedCut.kind !== "candle" && (movingAverages.ma5 || movingAverages.ma20))) return null; let recreated; try { recreated = createIndustryPlaylistEntry(market, selectedCut.id, { id: value.id, fadeDuration: value.fadeDuration, selected: operator.selected, pair: operator.pair, now: options.now, ma5: movingAverages.ma5, ma20: movingAverages.ma20 }); } catch { return null; } const scalars = ["id", "builderKey", "code", "title", "detail", "category", "market", "fadeDuration"]; if (!scalars.every(key => value[key] === recreated[key]) || !Array.isArray(value.aliases) || value.aliases.length !== 1 || value.aliases[0] !== recreated.code || !sameSelection(value.selection, recreated.selection, selectedCut.id === "five-row")) return null; return { ...recreated, enabled: value.enabled }; } return { markets, runtimeAssets, cuts, normalizeIndustry, isCutAllowed, localDateText, createIndustryPlaylistEntry, restoreIndustryPlaylistEntry, refreshIndustryPlaylistEntry: restoreIndustryPlaylistEntry }; });