(function (root, factory) { "use strict"; const api = Object.freeze(factory()); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnExpertWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function () { "use strict"; const DEFAULT_FADE_DURATION = 6; const DEFAULT_MAXIMUM_RESULTS = 100; const MAXIMUM_RESULTS = 500; const MAXIMUM_QUERY_LENGTH = 64; const DEFAULT_MAXIMUM_PREVIEW_ITEMS = 100; const MAXIMUM_PREVIEW_ITEMS = 100; const MAXIMUM_PAGE_COUNT = 20; const PAGE_SIZE = 5; const SCHEMA_VERSION = 1; const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/; const expertCodePattern = /^[0-9]{4}$/; const stockCodePattern = /^[A-Za-z0-9]{1,32}$/; const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u; const bridgeContract = Object.freeze({ searchRequestType: "search-experts", searchResultType: "expert-search-results", searchErrorType: "expert-search-error", previewRequestType: "request-expert-preview", previewResultType: "expert-preview-results", previewErrorType: "expert-preview-error", defaultMaximumResults: DEFAULT_MAXIMUM_RESULTS, maximumResults: MAXIMUM_RESULTS, maximumQueryLength: MAXIMUM_QUERY_LENGTH, defaultMaximumPreviewItems: DEFAULT_MAXIMUM_PREVIEW_ITEMS, maximumPreviewItems: MAXIMUM_PREVIEW_ITEMS }); const sourceContract = Object.freeze({ originalControl: "Control/UC6.cs", provider: "oracle", expertTable: "EXPERT_LIST", recommendationTable: "RECOMMEND_LIST", originalGroupLabel: "전문가 추천", originalCutLabel: "5단 표그래프", originalValueLabel: "현재가", builderKey: "s5074", cutCode: "5074", resolverGroup: "PAGED_EXPERT", pageSize: PAGE_SIZE, maximumPageCount: MAXIMUM_PAGE_COUNT }); const actions = Object.freeze([ Object.freeze({ id: "five-row-current", label: "전문가 추천 · 5단 표그래프 · 현재가", builderKey: "s5074", code: "5074", aliases: Object.freeze(["5074"]), pageSize: PAGE_SIZE }) ]); const action = actions[0]; const trustedPreviewResponses = new WeakSet(); function hasExactKeys(value, keys) { if (!value || typeof value !== "object" || Array.isArray(value)) return false; const actual = Object.keys(value).sort(); const expected = [...keys].sort(); return actual.length === expected.length && actual.every((key, index) => key === expected[index]); } function hasOnlyKeys(value, keys) { return value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).every(key => keys.includes(key)); } function isSafeCanonicalText(value, maximumLength) { return typeof value === "string" && value.length > 0 && value.length <= maximumLength && value === value.trim() && !unsafeTextPattern.test(value); } function isSafeRequestId(value) { return isSafeCanonicalText(value, 128) && identifierPattern.test(value); } function normalizeQuery(value) { if (typeof value !== "string" || unsafeTextPattern.test(value)) return null; const query = value.trim(); return query.length <= MAXIMUM_QUERY_LENGTH && !unsafeTextPattern.test(query) ? query : null; } function normalizeExpertIdentity(value) { if (!hasExactKeys(value, ["expertCode", "expertName"]) || !expertCodePattern.test(value.expertCode) || !isSafeCanonicalText(value.expertName, 128)) return null; return Object.freeze({ expertCode: value.expertCode, expertName: value.expertName }); } function normalizeExpert(value) { if (!hasExactKeys(value, ["expertCode", "expertName", "source"]) || value.source !== "oracle") return null; const identity = normalizeExpertIdentity({ expertCode: value.expertCode, expertName: value.expertName }); return identity && Object.freeze({ ...identity, source: "oracle" }); } function createExpertSearchRequest(requestId, query = "", maximumResults) { if (!isSafeRequestId(requestId)) { throw new TypeError("A safe expert-search request id is required."); } const normalizedQuery = normalizeQuery(query); if (normalizedQuery === null) { throw new TypeError(`An expert query must contain at most ${MAXIMUM_QUERY_LENGTH} safe characters.`); } if (maximumResults !== undefined && (!Number.isInteger(maximumResults) || maximumResults < 1 || maximumResults > MAXIMUM_RESULTS)) { throw new RangeError(`Expert results must be limited from 1 through ${MAXIMUM_RESULTS}.`); } const request = { requestId, query: normalizedQuery }; if (maximumResults !== undefined) request.maximumResults = maximumResults; return Object.freeze(request); } function compareExperts(left, right) { if (left.expertName < right.expertName) return -1; if (left.expertName > right.expertName) return 1; if (left.expertCode < right.expertCode) return -1; if (left.expertCode > right.expertCode) return 1; return 0; } function normalizeExpertSearchResponse(value, expectedRequest) { if (!hasExactKeys(value, [ "requestId", "query", "retrievedAt", "totalRowCount", "truncated", "results" ]) || !isSafeRequestId(value.requestId) || normalizeQuery(value.query) !== value.query || !isSafeCanonicalText(value.retrievedAt, 64) || !Number.isFinite(Date.parse(value.retrievedAt)) || !Number.isInteger(value.totalRowCount) || value.totalRowCount < 0 || value.totalRowCount > MAXIMUM_RESULTS || typeof value.truncated !== "boolean" || !Array.isArray(value.results) || value.results.length !== value.totalRowCount) return null; let expected = null; if (expectedRequest !== undefined) { try { expected = createExpertSearchRequest( expectedRequest?.requestId, expectedRequest?.query, expectedRequest?.maximumResults); } catch { return null; } const bound = expected.maximumResults ?? DEFAULT_MAXIMUM_RESULTS; if (value.requestId !== expected.requestId || value.query !== expected.query || value.results.length > bound || (value.truncated && value.results.length !== bound)) return null; } const results = []; const codes = new Set(); const names = new Set(); for (const raw of value.results) { const expert = normalizeExpert(raw); if (!expert || codes.has(expert.expertCode) || names.has(expert.expertName) || (results.length && compareExperts(results[results.length - 1], expert) > 0)) return null; codes.add(expert.expertCode); names.add(expert.expertName); results.push(expert); } return Object.freeze({ requestId: value.requestId, query: value.query, retrievedAt: value.retrievedAt, totalRowCount: value.totalRowCount, truncated: value.truncated, results: Object.freeze(results) }); } function normalizeExpertSearchError(value, expectedRequest) { if (!hasExactKeys(value, ["requestId", "query", "message"]) || !isSafeRequestId(value.requestId) || normalizeQuery(value.query) !== value.query || !isSafeCanonicalText(value.message, 512)) return null; if (expectedRequest) { let expected; try { expected = createExpertSearchRequest( expectedRequest.requestId, expectedRequest.query, expectedRequest.maximumResults); } catch { return null; } if (value.requestId !== expected.requestId || value.query !== expected.query) return null; } return Object.freeze({ requestId: value.requestId, query: value.query, message: value.message }); } function createExpertPreviewRequest(requestId, expertValue, maximumItems) { if (!isSafeRequestId(requestId)) { throw new TypeError("A safe expert-preview request id is required."); } const expert = normalizeExpert(expertValue); if (!expert) throw new TypeError("A typed Oracle expert is required."); if (maximumItems !== undefined && (!Number.isInteger(maximumItems) || maximumItems < 1 || maximumItems > MAXIMUM_PREVIEW_ITEMS)) { throw new RangeError(`Expert preview items must be limited from 1 through ${MAXIMUM_PREVIEW_ITEMS}.`); } const request = { requestId, expertCode: expert.expertCode, expertName: expert.expertName }; if (maximumItems !== undefined) request.maximumItems = maximumItems; return Object.freeze(request); } function normalizeRecommendation(value) { if (!hasExactKeys(value, ["playIndex", "stockCode", "stockName", "buyAmount"]) || !Number.isInteger(value.playIndex) || value.playIndex < 0 || value.playIndex > 9999 || typeof value.stockCode !== "string" || !stockCodePattern.test(value.stockCode) || !isSafeCanonicalText(value.stockName, 200) || !Number.isSafeInteger(value.buyAmount) || value.buyAmount <= 0) return null; return Object.freeze({ playIndex: value.playIndex, stockCode: value.stockCode, stockName: value.stockName, buyAmount: value.buyAmount }); } function normalizeExpertPreviewResponse(value, expectedRequest) { if (!hasExactKeys(value, [ "requestId", "selection", "retrievedAt", "totalRowCount", "truncated", "items" ]) || !isSafeRequestId(value.requestId) || !isSafeCanonicalText(value.retrievedAt, 64) || !Number.isFinite(Date.parse(value.retrievedAt)) || !Number.isInteger(value.totalRowCount) || value.totalRowCount < 0 || value.totalRowCount > MAXIMUM_PREVIEW_ITEMS || typeof value.truncated !== "boolean" || !Array.isArray(value.items) || value.items.length !== value.totalRowCount) return null; const selection = normalizeExpertIdentity(value.selection); if (!selection) return null; if (expectedRequest !== undefined) { let expected; try { expected = createExpertPreviewRequest( expectedRequest?.requestId, { expertCode: expectedRequest?.expertCode, expertName: expectedRequest?.expertName, source: "oracle" }, expectedRequest?.maximumItems); } catch { return null; } const bound = expected.maximumItems ?? DEFAULT_MAXIMUM_PREVIEW_ITEMS; if (value.requestId !== expected.requestId || selection.expertCode !== expected.expertCode || selection.expertName !== expected.expertName || value.items.length > bound || (value.truncated && value.items.length !== bound)) return null; } const items = []; const indexes = new Set(); const codes = new Set(); const names = new Set(); for (const raw of value.items) { const item = normalizeRecommendation(raw); if (!item || indexes.has(item.playIndex) || codes.has(item.stockCode) || names.has(item.stockName) || (items.length && items[items.length - 1].playIndex >= item.playIndex)) return null; indexes.add(item.playIndex); codes.add(item.stockCode); names.add(item.stockName); items.push(item); } const result = Object.freeze({ requestId: value.requestId, selection, retrievedAt: value.retrievedAt, totalRowCount: value.totalRowCount, truncated: value.truncated, items: Object.freeze(items) }); trustedPreviewResponses.add(result); return result; } function normalizeExpertPreviewError(value, expectedRequest) { if (!hasExactKeys(value, ["requestId", "selection", "message"]) || !isSafeRequestId(value.requestId) || !isSafeCanonicalText(value.message, 512)) return null; const selection = normalizeExpertIdentity(value.selection); if (!selection) return null; if (expectedRequest) { let expected; try { expected = createExpertPreviewRequest( expectedRequest.requestId, { expertCode: expectedRequest.expertCode, expertName: expectedRequest.expertName, source: "oracle" }, expectedRequest.maximumItems); } catch { return null; } if (value.requestId !== expected.requestId || selection.expertCode !== expected.expertCode || selection.expertName !== expected.expertName) return null; } return Object.freeze({ requestId: value.requestId, selection, message: value.message }); } function createSelectableExpert(expertValue, previewValue) { const expert = normalizeExpert(expertValue); if (!expert || !previewValue || typeof previewValue !== "object" || !trustedPreviewResponses.has(previewValue) || !normalizeExpertIdentity(previewValue.selection) || previewValue.selection.expertCode !== expert.expertCode || previewValue.selection.expertName !== expert.expertName || !Number.isInteger(previewValue.totalRowCount) || previewValue.totalRowCount < 0 || previewValue.totalRowCount > MAXIMUM_PREVIEW_ITEMS || typeof previewValue.truncated !== "boolean") return null; return Object.freeze({ ...expert, itemCount: previewValue.totalRowCount, previewTruncated: previewValue.truncated }); } function normalizeSelectableExpert(value) { if (!hasExactKeys(value, [ "expertCode", "expertName", "source", "itemCount", "previewTruncated" ]) || !Number.isInteger(value.itemCount) || value.itemCount < 0 || value.itemCount > MAXIMUM_PREVIEW_ITEMS || typeof value.previewTruncated !== "boolean") return null; const expert = normalizeExpert({ expertCode: value.expertCode, expertName: value.expertName, source: value.source }); return expert && Object.freeze({ ...expert, itemCount: value.itemCount, previewTruncated: value.previewTruncated }); } function calculatePagePreview(expertValue) { const expert = normalizeSelectableExpert(expertValue); if (!expert) return null; return Object.freeze({ itemCount: expert.itemCount, accessibleItemCount: expert.itemCount, pageSize: PAGE_SIZE, pageCount: Math.ceil(expert.itemCount / PAGE_SIZE), maximumPageCount: MAXIMUM_PAGE_COUNT, isTruncated: expert.previewTruncated }); } function buildExpertSelection(expertValue) { const expert = normalizeSelectableExpert(expertValue); if (!expert) throw new TypeError("A previewed expert selection is required."); return Object.freeze({ builderKey: action.builderKey, code: action.code, aliases: action.aliases, selection: Object.freeze({ groupCode: "PAGED_EXPERT", subject: expert.expertName, graphicType: "INPUT_ORDER", subtype: "CURRENT", dataCode: expert.expertCode }) }); } function requireOptions(options) { if (!hasOnlyKeys(options, ["id", "fadeDuration"]) || !isSafeRequestId(options.id)) { throw new TypeError("Safe expert playlist options are required."); } const fadeDuration = options.fadeDuration === undefined ? DEFAULT_FADE_DURATION : options.fadeDuration; if (!Number.isInteger(fadeDuration) || fadeDuration < 0 || fadeDuration > 60) { throw new RangeError("Fade duration must be an integer from 0 through 60."); } return Object.freeze({ id: options.id, fadeDuration }); } function createExpertPlaylistEntry(expertValue, options) { const expert = normalizeSelectableExpert(expertValue); if (!expert) throw new TypeError("A previewed expert selection is required."); if (expert.itemCount === 0) throw new RangeError("An expert with no recommendation rows cannot create a cut."); const normalizedOptions = requireOptions(options); const mapping = buildExpertSelection(expert); const pagePreview = calculatePagePreview(expert); return { id: normalizedOptions.id, builderKey: mapping.builderKey, code: mapping.code, aliases: [...mapping.aliases], title: expert.expertName, detail: action.label, category: "expert", source: expert.source, expertCode: expert.expertCode, expertName: expert.expertName, itemCount: expert.itemCount, previewTruncated: expert.previewTruncated, pageSize: pagePreview.pageSize, pageCount: pagePreview.pageCount, selection: mapping.selection, enabled: true, fadeDuration: normalizedOptions.fadeDuration, graphicType: mapping.selection.graphicType, subtype: mapping.selection.subtype, operator: Object.freeze({ source: "legacy-expert-workflow", schemaVersion: SCHEMA_VERSION, actionId: action.id, expert, pagePreview }) }; } 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) { return left && right && ["groupCode", "subject", "graphicType", "subtype", "dataCode"] .every(key => typeof left[key] === "string" && left[key] === right[key]); } function samePagePreview(left, right) { return left && right && [ "itemCount", "accessibleItemCount", "pageSize", "pageCount", "maximumPageCount", "isTruncated" ].every(key => left[key] === right[key]); } function restoreExpertPlaylistEntry(value) { if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.enabled !== "boolean") return null; const operator = value.operator; if (!hasExactKeys(operator, ["source", "schemaVersion", "actionId", "expert", "pagePreview"]) || operator.source !== "legacy-expert-workflow" || operator.schemaVersion !== SCHEMA_VERSION || operator.actionId !== action.id || !normalizeSelectableExpert(operator.expert)) return null; let recreated; try { recreated = createExpertPlaylistEntry(operator.expert, { id: value.id, fadeDuration: value.fadeDuration }); } catch { return null; } const scalars = [ "id", "builderKey", "code", "title", "detail", "category", "source", "expertCode", "expertName", "itemCount", "previewTruncated", "pageSize", "pageCount", "fadeDuration", "graphicType", "subtype" ]; if (!scalars.every(key => value[key] === recreated[key]) || !sameStringArray(value.aliases, recreated.aliases) || !sameSelection(value.selection, recreated.selection) || !samePagePreview(operator.pagePreview, recreated.operator.pagePreview)) return null; return { ...recreated, enabled: value.enabled }; } return { bridgeContract, sourceContract, actions, normalizeExpertIdentity, normalizeExpert, createExpertSearchRequest, normalizeExpertSearchResponse, normalizeExpertSearchError, createExpertPreviewRequest, normalizeRecommendation, normalizeExpertPreviewResponse, normalizeExpertPreviewError, createSelectableExpert, calculatePagePreview, buildExpertSelection, createExpertPlaylistEntry, restoreExpertPlaylistEntry, refreshExpertPlaylistEntry: restoreExpertPlaylistEntry }; });