"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.STOCK_ALERT_TYPE_OPTIONS = exports.STOCK_ALERT_LAYOUT_NAME = exports.STOCK_ALERT_VOLUME_SNAPSHOT_TABLE = exports.STOCK_ALERT_TABLE = void 0;
exports.resolveVolumeRate5dFromHistory = resolveVolumeRate5dFromHistory;
exports.searchStockAlertCandidates = searchStockAlertCandidates;
exports.resolveLatestQuoteFromMeta = resolveLatestQuoteFromMeta;
exports.resolveLatestQuoteFromNaverRealtime = resolveLatestQuoteFromNaverRealtime;
exports.fetchQuotesByCodes = fetchQuotesByCodes;
exports.listStockAlerts = listStockAlerts;
exports.createStockAlert = createStockAlert;
exports.updateStockAlert = updateStockAlert;
exports.deleteStockAlert = deleteStockAlert;
exports.saveStockAlerts = saveStockAlerts;
exports.buildCurrentPriceStockAlertLines = buildCurrentPriceStockAlertLines;
exports.buildChangeRateThresholdStockAlertLines = buildChangeRateThresholdStockAlertLines;
exports.buildChangeRateAndVolumeSpikeStockAlertCandidates = buildChangeRateAndVolumeSpikeStockAlertCandidates;
exports.buildChangeRateAndVolumeSpikeStockAlertLines = buildChangeRateAndVolumeSpikeStockAlertLines;
exports.buildStockAlertNotificationIdentity = buildStockAlertNotificationIdentity;
exports.sendManagedStockAlertWebPush = sendManagedStockAlertWebPush;
exports.sendCurrentPriceStockAlertWebPush = sendCurrentPriceStockAlertWebPush;
exports.updateStockAlertLayoutFeatureDescription = updateStockAlertLayoutFeatureDescription;
var notification_service_js_1 = require("./notification-service.js");
var client_js_1 = require("../db/client.js");
exports.STOCK_ALERT_TABLE = 'stock_alerts';
exports.STOCK_ALERT_VOLUME_SNAPSHOT_TABLE = 'stock_alert_volume_snapshots';
exports.STOCK_ALERT_LAYOUT_NAME = 'stock알림';
exports.STOCK_ALERT_TYPE_OPTIONS = [
{ value: 'all', label: '전체' },
{ value: 'price', label: '현재가' },
{ value: 'top3', label: '등락폭이 큰 상위3종목' },
];
var STOCK_ALERT_LABEL_MAP = new Map(exports.STOCK_ALERT_TYPE_OPTIONS.map(function (option) { return [option.value, option.label]; }));
var STOCK_ALERT_VALUE_SET = new Set(['price', 'top3']);
var KRX_CORP_LIST_URL = 'https://kind.krx.co.kr/corpgeneral/corpList.do?method=download&searchType=13';
var KRX_CORP_LIST_CACHE_TTL_MS = 1000 * 60 * 60 * 12;
var STOCK_ALERT_NOTIFICATION_TITLE = '현재 주가';
var STOCK_ALERT_NOTIFICATION_TARGET_DOMAIN = 'test.sm-home.cloud';
var STOCK_ALERT_NOTIFICATION_SCOPE = 'schedule-2-stock-alert';
var STOCK_ALERT_NOTIFICATION_TARGET_URL = "https://".concat(STOCK_ALERT_NOTIFICATION_TARGET_DOMAIN, "/?topMenu=play&playMenu=layout");
var KOREA_TIMEZONE = 'Asia/Seoul';
var KOREA_PREOPEN_RESET_HOUR = 5;
var KOREA_REGULAR_OPEN_HOUR = 9;
var cachedKrxListedStocks = null;
function normalizeTimestamp(value) {
if (!value) {
return new Date().toISOString();
}
if (value instanceof Date) {
return value.toISOString();
}
var parsed = Date.parse(value);
return Number.isNaN(parsed) ? new Date().toISOString() : new Date(parsed).toISOString();
}
function normalizeAlertType(value) {
var normalized = value.trim().toLowerCase();
if (!STOCK_ALERT_VALUE_SET.has(normalized)) {
throw new Error('알림유형은 현재가 또는 등락폭이 큰 상위3종목만 저장할 수 있습니다.');
}
return normalized;
}
function normalizeStockCode(value) {
var digits = value.replace(/\D+/g, '');
return digits.length === 6 ? digits : '';
}
function isFiniteNumber(value) {
return typeof value === 'number' && Number.isFinite(value);
}
function parseLooseNumber(value) {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value !== 'string') {
return null;
}
var normalized = value.replace(/[^0-9.-]+/g, '');
if (!normalized) {
return null;
}
var parsed = Number(normalized);
return Number.isFinite(parsed) ? parsed : null;
}
function applySign(value, sign) {
if (value === null || sign === null || value === 0) {
return value;
}
return Math.abs(value) * sign;
}
function resolveNaverDirectionSign(compareToPreviousPrice) {
var _a, _b;
var direction = ((_a = compareToPreviousPrice === null || compareToPreviousPrice === void 0 ? void 0 : compareToPreviousPrice.name) === null || _a === void 0 ? void 0 : _a.trim().toUpperCase()) || ((_b = compareToPreviousPrice === null || compareToPreviousPrice === void 0 ? void 0 : compareToPreviousPrice.code) === null || _b === void 0 ? void 0 : _b.trim());
if (direction === 'FALLING' || direction === '4' || direction === '5') {
return -1;
}
if (direction === 'RISING' || direction === '2') {
return 1;
}
return null;
}
function resolveSignedNaverChangeRate(rate, compareToPreviousClosePrice, compareToPreviousPrice) {
var signedChangeAmount = parseLooseNumber(compareToPreviousClosePrice);
if (signedChangeAmount !== null && signedChangeAmount !== 0) {
return applySign(rate, signedChangeAmount < 0 ? -1 : 1);
}
return applySign(rate, resolveNaverDirectionSign(compareToPreviousPrice));
}
function resolveCapturedTimestampMs(value) {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string' && value.trim()) {
var parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
function extractKoreaHour(timestampMs) {
var formatted = new Intl.DateTimeFormat('en-GB', {
timeZone: KOREA_TIMEZONE,
hour: '2-digit',
hour12: false,
}).format(new Date(timestampMs));
var hour = Number(formatted);
return Number.isFinite(hour) ? hour : null;
}
function isKoreaMorningResetWindow(timestampMs) {
if (!Number.isFinite(timestampMs)) {
return false;
}
var koreaHour = extractKoreaHour(timestampMs);
return koreaHour !== null && koreaHour >= KOREA_PREOPEN_RESET_HOUR && koreaHour < KOREA_REGULAR_OPEN_HOUR;
}
function getAlertTypeLabel(value) {
var _a;
return (_a = STOCK_ALERT_LABEL_MAP.get(value)) !== null && _a !== void 0 ? _a : value;
}
function average(values) {
if (!values.length) {
return null;
}
var sum = values.reduce(function (acc, value) { return acc + value; }, 0);
return sum / values.length;
}
function resolveVolumeRate5dFromHistory(currentVolume, historicalVolumes) {
var _a;
var normalizedCurrentVolume = isFiniteNumber(currentVolume) && currentVolume >= 0 ? currentVolume : null;
var normalizedVolumes = historicalVolumes.filter(function (value) { return isFiniteNumber(value) && value >= 0; });
if (normalizedVolumes.length < 2) {
return null;
}
var latestVolume = (_a = normalizedCurrentVolume !== null && normalizedCurrentVolume !== void 0 ? normalizedCurrentVolume : normalizedVolumes[normalizedVolumes.length - 1]) !== null && _a !== void 0 ? _a : null;
var previousFiveAverage = average(normalizedVolumes.slice(-6, -1));
if (latestVolume === null || previousFiveAverage === null || previousFiveAverage <= 0) {
return null;
}
return (latestVolume / previousFiveAverage) * 100;
}
function normalizeNonNegativeVolume(value) {
var parsed = parseLooseNumber(value);
if (!isFiniteNumber(parsed) || parsed < 0) {
return null;
}
return Math.round(parsed);
}
function calculateVolumeIncreasePercent(currentVolume, previousVolume) {
if (!isFiniteNumber(currentVolume) || !isFiniteNumber(previousVolume) || previousVolume <= 0 || currentVolume < previousVolume) {
return null;
}
return ((currentVolume - previousVolume) / previousVolume) * 100;
}
function normalizeStockAlertVolumeSnapshotRow(row) {
var _a;
return {
stockCode: normalizeStockCode(row.stock_code),
stockName: String((_a = row.stock_name) !== null && _a !== void 0 ? _a : '').trim() || normalizeStockCode(row.stock_code),
previousVolume: normalizeNonNegativeVolume(row.previous_volume),
currentVolume: normalizeNonNegativeVolume(row.current_volume),
volumeIncreasePercent: parseLooseNumber(row.volume_increase_percent),
currentPrice: parseLooseNumber(row.current_price),
changeRate: parseLooseNumber(row.change_rate),
quotedAt: row.quoted_at ? normalizeTimestamp(row.quoted_at) : null,
createdAt: normalizeTimestamp(row.created_at),
updatedAt: normalizeTimestamp(row.updated_at),
};
}
function buildStockAlertVolumeSnapshotRecord(item, currentVolume, previousSnapshot) {
var _a, _b;
var now = new Date().toISOString();
var normalizedCurrentVolume = normalizeNonNegativeVolume(currentVolume);
var previousCurrentVolume = (_a = previousSnapshot === null || previousSnapshot === void 0 ? void 0 : previousSnapshot.currentVolume) !== null && _a !== void 0 ? _a : null;
var shouldResetBaseline = normalizedCurrentVolume !== null &&
previousCurrentVolume !== null &&
normalizedCurrentVolume < previousCurrentVolume;
var comparisonBaseline = shouldResetBaseline ? normalizedCurrentVolume : previousCurrentVolume;
var volumeIncreasePercent = calculateVolumeIncreasePercent(normalizedCurrentVolume, comparisonBaseline);
var nextPreviousVolume = shouldResetBaseline
? normalizedCurrentVolume
: previousCurrentVolume !== null
? previousCurrentVolume
: normalizedCurrentVolume;
return {
stock_code: item.stockCode,
stock_name: item.stockName,
previous_volume: nextPreviousVolume,
current_volume: normalizedCurrentVolume,
volume_increase_percent: volumeIncreasePercent,
current_price: item.currentPrice,
change_rate: item.changeRate,
quoted_at: item.quotedAt,
created_at: (_b = previousSnapshot === null || previousSnapshot === void 0 ? void 0 : previousSnapshot.createdAt) !== null && _b !== void 0 ? _b : now,
updated_at: now,
};
}
function buildStockSymbols(stockCode) {
var normalizedCode = normalizeStockCode(stockCode);
if (!normalizedCode) {
return [];
}
return ["".concat(normalizedCode, ".KS"), "".concat(normalizedCode, ".KQ")];
}
function extractStockCodeFromSymbol(symbol) {
var match = symbol.trim().match(/^(\d{6})\.(?:KS|KQ)$/i);
return match ? match[1] : '';
}
function resolveMarketLabel(quote) {
var _a, _b, _c, _d, _e;
var symbol = (_b = (_a = quote.symbol) === null || _a === void 0 ? void 0 : _a.trim().toUpperCase()) !== null && _b !== void 0 ? _b : '';
if (symbol.endsWith('.KS')) {
return 'KOSPI';
}
if (symbol.endsWith('.KQ')) {
return 'KOSDAQ';
}
var exchange = ((_c = quote.exchDisp) === null || _c === void 0 ? void 0 : _c.trim()) || ((_d = quote.exchange) === null || _d === void 0 ? void 0 : _d.trim()) || ((_e = quote.typeDisp) === null || _e === void 0 ? void 0 : _e.trim());
if (!exchange) {
return '기타';
}
if (/KOSDAQ/i.test(exchange)) {
return 'KOSDAQ';
}
if (/KOSPI|KOSE/i.test(exchange)) {
return 'KOSPI';
}
return exchange;
}
function ensureStockAlertTable() {
return __awaiter(this, void 0, void 0, function () {
var exists;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, client_js_1.db.schema.hasTable(exports.STOCK_ALERT_TABLE)];
case 1:
exists = _a.sent();
if (exists) {
return [2 /*return*/];
}
return [4 /*yield*/, client_js_1.db.schema.createTable(exports.STOCK_ALERT_TABLE, function (table) {
table.text('id').primary();
table.text('stock_code').notNullable();
table.text('stock_name').notNullable();
table.text('alert_type').notNullable();
table.timestamp('created_at', { useTz: true }).notNullable();
table.timestamp('updated_at', { useTz: true }).notNullable();
})];
case 2:
_a.sent();
return [2 /*return*/];
}
});
});
}
function ensureStockAlertVolumeSnapshotTable() {
return __awaiter(this, void 0, void 0, function () {
var exists;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, client_js_1.db.schema.hasTable(exports.STOCK_ALERT_VOLUME_SNAPSHOT_TABLE)];
case 1:
exists = _a.sent();
if (exists) {
return [2 /*return*/];
}
return [4 /*yield*/, client_js_1.db.schema.createTable(exports.STOCK_ALERT_VOLUME_SNAPSHOT_TABLE, function (table) {
table.text('stock_code').primary();
table.text('stock_name').notNullable();
table.bigInteger('previous_volume').nullable();
table.bigInteger('current_volume').nullable();
table.decimal('volume_increase_percent', 10, 2).nullable();
table.decimal('current_price', 14, 2).nullable();
table.decimal('change_rate', 10, 4).nullable();
table.timestamp('quoted_at', { useTz: true }).nullable();
table.timestamp('created_at', { useTz: true }).notNullable();
table.timestamp('updated_at', { useTz: true }).notNullable();
})];
case 2:
_a.sent();
return [2 /*return*/];
}
});
});
}
function fetchJson(url, init) {
return __awaiter(this, void 0, void 0, function () {
var response;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, fetch(url, __assign(__assign({}, init), { headers: __assign({ accept: 'application/json', 'user-agent': 'ai-code-app/stock-alert' }, ((_a = init === null || init === void 0 ? void 0 : init.headers) !== null && _a !== void 0 ? _a : {})) }))];
case 1:
response = _b.sent();
if (!response.ok) {
throw new Error("\uC678\uBD80 \uC2DC\uC138 \uC751\uB2F5\uC744 \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. (".concat(response.status, ")"));
}
return [2 /*return*/, response.json()];
}
});
});
}
function fetchText(url, init) {
return __awaiter(this, void 0, void 0, function () {
var response;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, fetch(url, __assign(__assign({}, init), { headers: __assign({ accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'user-agent': 'ai-code-app/stock-alert' }, ((_a = init === null || init === void 0 ? void 0 : init.headers) !== null && _a !== void 0 ? _a : {})) }))];
case 1:
response = _b.sent();
if (!response.ok) {
throw new Error("\uC678\uBD80 \uC885\uBAA9 \uAC80\uC0C9 \uC751\uB2F5\uC744 \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. (".concat(response.status, ")"));
}
return [2 /*return*/, response.arrayBuffer()];
}
});
});
}
function decodeEucKr(value) {
return new TextDecoder('euc-kr').decode(value);
}
function decodeHtmlEntities(value) {
return value
.replace(/ /gi, ' ')
.replace(/&/gi, '&')
.replace(/</gi, '<')
.replace(/>/gi, '>')
.replace(/'/g, "'")
.replace(/"/gi, '"');
}
function stripHtmlTags(value) {
return decodeHtmlEntities(value.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim());
}
function normalizeSearchKeyword(value) {
return value.trim().replace(/\s+/g, '').toLowerCase();
}
function normalizeMarketLabel(value) {
var trimmedValue = value.trim();
if (/코스닥/i.test(trimmedValue)) {
return 'KOSDAQ';
}
if (/유가증권|코스피|유가/i.test(trimmedValue)) {
return 'KOSPI';
}
return trimmedValue || '기타';
}
function parseKrxListedStocks(html) {
var _a;
var rowMatches = (_a = html.match(/
/gi)) !== null && _a !== void 0 ? _a : [];
var items = [];
rowMatches.forEach(function (rowHtml) {
var _a, _b, _c, _d;
var cellMatches = (_a = rowHtml.match(/| /gi)) !== null && _a !== void 0 ? _a : [];
if (cellMatches.length < 3) {
return;
}
var stockName = stripHtmlTags((_b = cellMatches[0]) !== null && _b !== void 0 ? _b : '');
var market = normalizeMarketLabel(stripHtmlTags((_c = cellMatches[1]) !== null && _c !== void 0 ? _c : ''));
var stockCode = normalizeStockCode(stripHtmlTags((_d = cellMatches[2]) !== null && _d !== void 0 ? _d : ''));
if (!stockCode || !stockName) {
return;
}
items.push({
stockCode: stockCode,
stockName: stockName,
market: market,
});
});
return items;
}
function findKrxListedStockByCode(stockCode) {
return __awaiter(this, void 0, void 0, function () {
var normalizedCode, items;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
normalizedCode = normalizeStockCode(stockCode);
if (!normalizedCode) {
return [2 /*return*/, null];
}
return [4 /*yield*/, fetchKrxListedStocks()];
case 1:
items = _b.sent();
return [2 /*return*/, (_a = items.find(function (item) { return item.stockCode === normalizedCode; })) !== null && _a !== void 0 ? _a : null];
}
});
});
}
function fetchKrxListedStocks() {
return __awaiter(this, void 0, void 0, function () {
var buffer, decodedHtml, items;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (cachedKrxListedStocks && cachedKrxListedStocks.expiresAt > Date.now()) {
return [2 /*return*/, cachedKrxListedStocks.items];
}
return [4 /*yield*/, fetchText(KRX_CORP_LIST_URL)];
case 1:
buffer = _a.sent();
decodedHtml = decodeEucKr(buffer);
items = parseKrxListedStocks(decodedHtml);
cachedKrxListedStocks = {
expiresAt: Date.now() + KRX_CORP_LIST_CACHE_TTL_MS,
items: items,
};
return [2 /*return*/, items];
}
});
});
}
function searchKrxListedStocks(query_1) {
return __awaiter(this, arguments, void 0, function (query, limit) {
var normalizedKeyword, items, matchedItems;
if (limit === void 0) { limit = 20; }
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
normalizedKeyword = normalizeSearchKeyword(query);
if (!normalizedKeyword) {
return [2 /*return*/, []];
}
return [4 /*yield*/, fetchKrxListedStocks()];
case 1:
items = _a.sent();
matchedItems = items.filter(function (item) {
var normalizedCode = item.stockCode.toLowerCase();
var normalizedName = normalizeSearchKeyword(item.stockName);
return normalizedCode.includes(normalizedKeyword) || normalizedName.includes(normalizedKeyword);
});
matchedItems.sort(function (left, right) {
var trimmedQuery = query.trim();
var leftExactCode = left.stockCode === trimmedQuery ? 1 : 0;
var rightExactCode = right.stockCode === trimmedQuery ? 1 : 0;
if (leftExactCode !== rightExactCode) {
return rightExactCode - leftExactCode;
}
var leftExactName = normalizeSearchKeyword(left.stockName) === normalizedKeyword ? 1 : 0;
var rightExactName = normalizeSearchKeyword(right.stockName) === normalizedKeyword ? 1 : 0;
if (leftExactName !== rightExactName) {
return rightExactName - leftExactName;
}
var leftStartsWith = normalizeSearchKeyword(left.stockName).startsWith(normalizedKeyword) ? 1 : 0;
var rightStartsWith = normalizeSearchKeyword(right.stockName).startsWith(normalizedKeyword) ? 1 : 0;
if (leftStartsWith !== rightStartsWith) {
return rightStartsWith - leftStartsWith;
}
var leftLengthGap = Math.abs(left.stockName.length - trimmedQuery.length);
var rightLengthGap = Math.abs(right.stockName.length - trimmedQuery.length);
if (leftLengthGap !== rightLengthGap) {
return leftLengthGap - rightLengthGap;
}
return left.stockName.localeCompare(right.stockName, 'ko-KR');
});
return [2 /*return*/, matchedItems.slice(0, Math.max(1, Math.min(50, limit)))];
}
});
});
}
function resolveStockIdentity(input) {
return __awaiter(this, void 0, void 0, function () {
var codeFromInput, trimmedName, krxMatch, quotes, quote, krxMatches, exactMatch, searchUrl, payload, matchedQuote, resolvedCode;
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
return __generator(this, function (_l) {
switch (_l.label) {
case 0:
codeFromInput = normalizeStockCode((_a = input.stockCode) !== null && _a !== void 0 ? _a : '');
trimmedName = (_c = (_b = input.stockName) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : '';
if (!codeFromInput) return [3 /*break*/, 3];
return [4 /*yield*/, findKrxListedStockByCode(codeFromInput)];
case 1:
krxMatch = _l.sent();
if (krxMatch) {
return [2 /*return*/, {
stockCode: codeFromInput,
stockName: krxMatch.stockName,
}];
}
if (trimmedName) {
return [2 /*return*/, {
stockCode: codeFromInput,
stockName: trimmedName,
}];
}
return [4 /*yield*/, fetchQuotesByCodes([codeFromInput])];
case 2:
quotes = _l.sent();
quote = quotes.get(codeFromInput);
if (quote) {
return [2 /*return*/, {
stockCode: codeFromInput,
stockName: (_d = quote.stockName) !== null && _d !== void 0 ? _d : codeFromInput,
}];
}
return [2 /*return*/, {
stockCode: codeFromInput,
stockName: codeFromInput,
}];
case 3:
if (!trimmedName) {
throw new Error('종목명을 입력해 주세요.');
}
return [4 /*yield*/, searchKrxListedStocks(trimmedName, 10)];
case 4:
krxMatches = _l.sent();
exactMatch = (_f = (_e = krxMatches.find(function (item) { return normalizeSearchKeyword(item.stockName) === normalizeSearchKeyword(trimmedName); })) !== null && _e !== void 0 ? _e : krxMatches[0]) !== null && _f !== void 0 ? _f : null;
if (exactMatch) {
return [2 /*return*/, {
stockCode: exactMatch.stockCode,
stockName: exactMatch.stockName,
}];
}
searchUrl = new URL('https://query2.finance.yahoo.com/v1/finance/search');
searchUrl.searchParams.set('q', trimmedName);
searchUrl.searchParams.set('quotesCount', '10');
searchUrl.searchParams.set('newsCount', '0');
searchUrl.searchParams.set('lang', 'ko-KR');
searchUrl.searchParams.set('region', 'KR');
return [4 /*yield*/, fetchJson(searchUrl)];
case 5:
payload = _l.sent();
matchedQuote = (_h = (_g = payload.quotes) === null || _g === void 0 ? void 0 : _g.find(function (quote) { return typeof quote.symbol === 'string' && extractStockCodeFromSymbol(quote.symbol).length === 6; })) !== null && _h !== void 0 ? _h : null;
if (!(matchedQuote === null || matchedQuote === void 0 ? void 0 : matchedQuote.symbol)) {
throw new Error("\uC885\uBAA9\uBA85 \"".concat(trimmedName, "\"\uC5D0 \uD574\uB2F9\uD558\uB294 \uC885\uBAA9\uCF54\uB4DC\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4."));
}
resolvedCode = extractStockCodeFromSymbol(matchedQuote.symbol);
if (!resolvedCode) {
throw new Error("\uC885\uBAA9\uBA85 \"".concat(trimmedName, "\"\uC5D0 \uD574\uB2F9\uD558\uB294 \uC885\uBAA9\uCF54\uB4DC\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4."));
}
return [2 /*return*/, {
stockCode: resolvedCode,
stockName: ((_j = matchedQuote.shortname) === null || _j === void 0 ? void 0 : _j.trim()) || ((_k = matchedQuote.longname) === null || _k === void 0 ? void 0 : _k.trim()) || trimmedName,
}];
}
});
});
}
function searchYahooStocks(query_1) {
return __awaiter(this, arguments, void 0, function (query, quotesCount) {
var trimmedQuery, searchUrl, payload, error_1;
var _a;
if (quotesCount === void 0) { quotesCount = 20; }
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
trimmedQuery = query.trim();
if (!trimmedQuery) {
return [2 /*return*/, []];
}
searchUrl = new URL('https://query2.finance.yahoo.com/v1/finance/search');
searchUrl.searchParams.set('q', trimmedQuery);
searchUrl.searchParams.set('quotesCount', String(Math.max(1, Math.min(50, quotesCount))));
searchUrl.searchParams.set('newsCount', '0');
searchUrl.searchParams.set('lang', 'ko-KR');
searchUrl.searchParams.set('region', 'KR');
_b.label = 1;
case 1:
_b.trys.push([1, 3, , 4]);
return [4 /*yield*/, fetchJson(searchUrl)];
case 2:
payload = _b.sent();
return [2 /*return*/, (_a = payload.quotes) !== null && _a !== void 0 ? _a : []];
case 3:
error_1 = _b.sent();
// Yahoo search rejects some non-code Korean queries with HTTP 400.
if (/[^\x00-\x7F]/.test(trimmedQuery)) {
return [2 /*return*/, []];
}
throw error_1;
case 4: return [2 /*return*/];
}
});
});
}
function searchStockAlertCandidates(query_1) {
return __awaiter(this, arguments, void 0, function (query, limit) {
var normalizedLimit, _a, krxItems, quotes, seenCodes, items, krxByCode;
if (limit === void 0) { limit = 20; }
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
normalizedLimit = Math.max(1, Math.min(50, limit));
return [4 /*yield*/, Promise.all([
searchKrxListedStocks(query, normalizedLimit),
searchYahooStocks(query, normalizedLimit * 2),
])];
case 1:
_a = _b.sent(), krxItems = _a[0], quotes = _a[1];
seenCodes = new Set();
items = __spreadArray([], krxItems, true);
krxByCode = new Map(krxItems.map(function (item) { return [item.stockCode, item]; }));
krxItems.forEach(function (item) {
seenCodes.add(item.stockCode);
});
quotes.forEach(function (quote) {
var _a, _b, _c, _d;
if (!quote.symbol) {
return;
}
var stockCode = extractStockCodeFromSymbol(quote.symbol);
if (!stockCode || seenCodes.has(stockCode)) {
return;
}
var stockName = ((_a = quote.shortname) === null || _a === void 0 ? void 0 : _a.trim()) || ((_b = quote.longname) === null || _b === void 0 ? void 0 : _b.trim()) || stockCode;
var krxMatch = krxByCode.get(stockCode);
seenCodes.add(stockCode);
items.push({
stockCode: stockCode,
stockName: (_c = krxMatch === null || krxMatch === void 0 ? void 0 : krxMatch.stockName) !== null && _c !== void 0 ? _c : stockName,
market: (_d = krxMatch === null || krxMatch === void 0 ? void 0 : krxMatch.market) !== null && _d !== void 0 ? _d : resolveMarketLabel(quote),
});
});
return [2 /*return*/, items.slice(0, normalizedLimit)];
}
});
});
}
function ensureNoDuplicateStockCode(stockCode, currentId) {
return __awaiter(this, void 0, void 0, function () {
var normalizedCode, existing;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
normalizedCode = normalizeStockCode(stockCode);
if (!normalizedCode) {
throw new Error('종목코드를 확인할 수 없습니다.');
}
return [4 /*yield*/, (0, client_js_1.db)(exports.STOCK_ALERT_TABLE)
.select('id')
.where({ stock_code: normalizedCode })
.modify(function (query) {
if (currentId === null || currentId === void 0 ? void 0 : currentId.trim()) {
query.whereNot('id', currentId.trim());
}
})
.first()];
case 1:
existing = (_a.sent());
if (existing === null || existing === void 0 ? void 0 : existing.id) {
throw new Error('이미 추가된 종목입니다.');
}
return [2 /*return*/];
}
});
});
}
function resolveLatestQuoteFromMeta(meta) {
var _a, _b, _c, _d;
var marketState = String((_a = meta.marketState) !== null && _a !== void 0 ? _a : '')
.trim()
.toUpperCase();
var shouldPreferPremarket = ['PRE', 'PREPRE'].includes(marketState);
var shouldPreferPostmarket = ['POST', 'POSTPOST', 'CLOSED'].includes(marketState);
var preferredCandidate = shouldPreferPremarket
? {
price: meta.preMarketPrice,
changeRate: meta.preMarketChangePercent,
time: meta.preMarketTime,
}
: shouldPreferPostmarket
? {
price: meta.postMarketPrice,
changeRate: meta.postMarketChangePercent,
time: meta.postMarketTime,
}
: {
price: meta.regularMarketPrice,
changeRate: meta.regularMarketChangePercent,
time: meta.regularMarketTime,
};
var quoteCandidates = [
{
price: meta.regularMarketPrice,
changeRate: meta.regularMarketChangePercent,
time: meta.regularMarketTime,
},
{
price: meta.preMarketPrice,
changeRate: meta.preMarketChangePercent,
time: meta.preMarketTime,
},
{
price: meta.postMarketPrice,
changeRate: meta.postMarketChangePercent,
time: meta.postMarketTime,
},
];
var latestCandidate = (_b = quoteCandidates
.flatMap(function (item) {
return isFiniteNumber(item.price) && isFiniteNumber(item.time)
? [
{
price: item.price,
changeRate: item.changeRate,
time: item.time,
},
]
: [];
})
.sort(function (left, right) { return right.time - left.time; })[0]) !== null && _b !== void 0 ? _b : null;
var resolvedCandidate = isFiniteNumber(preferredCandidate.price) && isFiniteNumber(preferredCandidate.time) ? preferredCandidate : latestCandidate;
var currentPrice = isFiniteNumber(resolvedCandidate === null || resolvedCandidate === void 0 ? void 0 : resolvedCandidate.price)
? resolvedCandidate.price
: isFiniteNumber(meta.regularMarketPrice)
? meta.regularMarketPrice
: null;
var quotedAt = isFiniteNumber(resolvedCandidate === null || resolvedCandidate === void 0 ? void 0 : resolvedCandidate.time)
? new Date(resolvedCandidate.time * 1000).toISOString()
: isFiniteNumber(meta.regularMarketTime)
? new Date(meta.regularMarketTime * 1000).toISOString()
: null;
var previousClose = typeof meta.chartPreviousClose === 'number'
? meta.chartPreviousClose
: typeof meta.previousClose === 'number'
? meta.previousClose
: null;
var changeRate = isFiniteNumber(resolvedCandidate === null || resolvedCandidate === void 0 ? void 0 : resolvedCandidate.changeRate)
? resolvedCandidate.changeRate
: currentPrice !== null && previousClose !== null && previousClose !== 0
? ((currentPrice - previousClose) / previousClose) * 100
: null;
return {
currentPrice: currentPrice,
changeRate: changeRate,
volumeRate5d: null,
currentVolume: null,
quotedAt: quotedAt,
stockName: ((_c = meta.shortName) === null || _c === void 0 ? void 0 : _c.trim()) || ((_d = meta.longName) === null || _d === void 0 ? void 0 : _d.trim()) || null,
};
}
function resolveLatestQuoteFromNaverRealtime(data, capturedAt) {
var _a, _b, _c, _d, _e;
var overMarketInfo = data.nxtOverMarketPriceInfo;
var overPrice = parseLooseNumber(overMarketInfo === null || overMarketInfo === void 0 ? void 0 : overMarketInfo.overPrice);
var overChangeRate = resolveSignedNaverChangeRate(parseLooseNumber(overMarketInfo === null || overMarketInfo === void 0 ? void 0 : overMarketInfo.fluctuationsRatio), overMarketInfo === null || overMarketInfo === void 0 ? void 0 : overMarketInfo.compareToPreviousClosePrice, overMarketInfo === null || overMarketInfo === void 0 ? void 0 : overMarketInfo.compareToPreviousPrice);
var overQuotedAt = ((_a = overMarketInfo === null || overMarketInfo === void 0 ? void 0 : overMarketInfo.localTradedAt) === null || _a === void 0 ? void 0 : _a.trim()) || null;
var hasExtendedSessionQuote = overPrice !== null && overQuotedAt;
var baseQuotedAt = typeof capturedAt === 'number' && Number.isFinite(capturedAt)
? new Date(capturedAt).toISOString()
: typeof capturedAt === 'string' && capturedAt.trim()
? new Date(capturedAt).toISOString()
: null;
var capturedTimestampMs = resolveCapturedTimestampMs(capturedAt);
var basePrice = isFiniteNumber(data.nv) ? data.nv : null;
var baseChangeRate = applySign(isFiniteNumber(data.cr) ? data.cr : null, resolveNaverDirectionSign(((_b = data.rf) === null || _b === void 0 ? void 0 : _b.trim()) ? { code: data.rf } : undefined));
var previousClosePrice = isFiniteNumber(data.pcv) ? data.pcv : null;
var currentVolume = (_d = (_c = normalizeNonNegativeVolume(overMarketInfo === null || overMarketInfo === void 0 ? void 0 : overMarketInfo.accumulatedTradingVolume)) !== null && _c !== void 0 ? _c : normalizeNonNegativeVolume(data.accumulatedTradingVolume)) !== null && _d !== void 0 ? _d : normalizeNonNegativeVolume(data.aq);
var shouldResetToPreviousClose = !hasExtendedSessionQuote &&
isKoreaMorningResetWindow(capturedTimestampMs) &&
previousClosePrice !== null;
return {
currentPrice: hasExtendedSessionQuote ? overPrice : shouldResetToPreviousClose ? previousClosePrice : basePrice,
changeRate: hasExtendedSessionQuote ? overChangeRate : shouldResetToPreviousClose ? 0 : baseChangeRate,
volumeRate5d: null,
currentVolume: currentVolume,
quotedAt: hasExtendedSessionQuote ? overQuotedAt : baseQuotedAt,
stockName: ((_e = data.nm) === null || _e === void 0 ? void 0 : _e.trim()) || null,
};
}
function choosePreferredQuote(primary, fallback) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
if ((primary === null || primary === void 0 ? void 0 : primary.currentPrice) === null && (fallback === null || fallback === void 0 ? void 0 : fallback.currentPrice) === null) {
return (_a = primary !== null && primary !== void 0 ? primary : fallback) !== null && _a !== void 0 ? _a : null;
}
if (!primary) {
return fallback;
}
if (!fallback) {
return primary;
}
var primaryQuotedAt = primary.quotedAt ? Date.parse(primary.quotedAt) : Number.NaN;
var fallbackQuotedAt = fallback.quotedAt ? Date.parse(fallback.quotedAt) : Number.NaN;
if (Number.isFinite(primaryQuotedAt) && Number.isFinite(fallbackQuotedAt) && fallbackQuotedAt > primaryQuotedAt) {
return __assign(__assign({}, fallback), { stockName: (_b = fallback.stockName) !== null && _b !== void 0 ? _b : primary.stockName, currentVolume: (_c = fallback.currentVolume) !== null && _c !== void 0 ? _c : primary.currentVolume });
}
return __assign(__assign({}, primary), { stockName: (_d = primary.stockName) !== null && _d !== void 0 ? _d : fallback.stockName, currentPrice: (_e = primary.currentPrice) !== null && _e !== void 0 ? _e : fallback.currentPrice, changeRate: (_f = primary.changeRate) !== null && _f !== void 0 ? _f : fallback.changeRate, volumeRate5d: (_g = primary.volumeRate5d) !== null && _g !== void 0 ? _g : fallback.volumeRate5d, currentVolume: (_h = primary.currentVolume) !== null && _h !== void 0 ? _h : fallback.currentVolume, quotedAt: (_j = primary.quotedAt) !== null && _j !== void 0 ? _j : fallback.quotedAt });
}
function fetchNaverRealtimeQuoteByCode(stockCode) {
return __awaiter(this, void 0, void 0, function () {
var quoteUrl, payload, data;
var _a, _b, _c, _d, _e;
return __generator(this, function (_f) {
switch (_f.label) {
case 0:
quoteUrl = new URL('https://polling.finance.naver.com/api/realtime');
quoteUrl.searchParams.set('query', "SERVICE_ITEM:".concat(stockCode));
return [4 /*yield*/, fetchJson(quoteUrl, {
headers: {
accept: '*/*',
},
})];
case 1:
payload = _f.sent();
data = (_d = (_c = (_b = (_a = payload.result) === null || _a === void 0 ? void 0 : _a.areas) === null || _b === void 0 ? void 0 : _b.find(function (area) { return area.name === 'SERVICE_ITEM'; })) === null || _c === void 0 ? void 0 : _c.datas) === null || _d === void 0 ? void 0 : _d[0];
if (!data) {
return [2 /*return*/, null];
}
return [2 /*return*/, resolveLatestQuoteFromNaverRealtime(data, (_e = payload.result) === null || _e === void 0 ? void 0 : _e.time)];
}
});
});
}
function fetchQuoteBySymbol(symbol) {
return __awaiter(this, void 0, void 0, function () {
var quoteUrl, payload, result, meta, quote, dailyVolumes;
var _a, _b, _c, _d, _e, _f, _g, _h;
return __generator(this, function (_j) {
switch (_j.label) {
case 0:
quoteUrl = new URL("https://query1.finance.yahoo.com/v8/finance/chart/".concat(symbol));
quoteUrl.searchParams.set('range', '3mo');
quoteUrl.searchParams.set('interval', '1d');
quoteUrl.searchParams.set('includePrePost', 'true');
quoteUrl.searchParams.set('lang', 'ko-KR');
quoteUrl.searchParams.set('region', 'KR');
return [4 /*yield*/, fetchJson(quoteUrl)];
case 1:
payload = _j.sent();
result = (_b = (_a = payload.chart) === null || _a === void 0 ? void 0 : _a.result) === null || _b === void 0 ? void 0 : _b[0];
meta = result === null || result === void 0 ? void 0 : result.meta;
if (!meta) {
return [2 /*return*/, null];
}
quote = resolveLatestQuoteFromMeta(meta);
dailyVolumes = (_f = (_e = (_d = (_c = result === null || result === void 0 ? void 0 : result.indicators) === null || _c === void 0 ? void 0 : _c.quote) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.volume) !== null && _f !== void 0 ? _f : [];
return [2 /*return*/, __assign(__assign({}, quote), { currentVolume: (_g = dailyVolumes[dailyVolumes.length - 1]) !== null && _g !== void 0 ? _g : null, volumeRate5d: resolveVolumeRate5dFromHistory((_h = dailyVolumes[dailyVolumes.length - 1]) !== null && _h !== void 0 ? _h : null, dailyVolumes) })];
}
});
});
}
function fetchQuotesByCodes(stockCodes) {
return __awaiter(this, void 0, void 0, function () {
var normalizedCodes, quoteMap;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
normalizedCodes = Array.from(new Set(stockCodes
.map(function (value) { return normalizeStockCode(value); })
.filter(Boolean)));
quoteMap = new Map();
if (!normalizedCodes.length) {
return [2 /*return*/, quoteMap];
}
return [4 /*yield*/, Promise.all(normalizedCodes.map(function (stockCode) { return __awaiter(_this, void 0, void 0, function () {
var preferredQuote, _a, symbols, _i, symbols_1, symbol, yahooQuote, _b;
return __generator(this, function (_c) {
switch (_c.label) {
case 0:
preferredQuote = null;
_c.label = 1;
case 1:
_c.trys.push([1, 3, , 4]);
return [4 /*yield*/, fetchNaverRealtimeQuoteByCode(stockCode)];
case 2:
preferredQuote = _c.sent();
return [3 /*break*/, 4];
case 3:
_a = _c.sent();
return [3 /*break*/, 4];
case 4:
symbols = buildStockSymbols(stockCode);
_i = 0, symbols_1 = symbols;
_c.label = 5;
case 5:
if (!(_i < symbols_1.length)) return [3 /*break*/, 10];
symbol = symbols_1[_i];
_c.label = 6;
case 6:
_c.trys.push([6, 8, , 9]);
return [4 /*yield*/, fetchQuoteBySymbol(symbol)];
case 7:
yahooQuote = _c.sent();
preferredQuote = choosePreferredQuote(preferredQuote, yahooQuote);
if (preferredQuote &&
(preferredQuote.currentPrice !== null || preferredQuote.stockName) &&
preferredQuote.volumeRate5d !== null) {
quoteMap.set(stockCode, preferredQuote);
return [2 /*return*/];
}
return [3 /*break*/, 9];
case 8:
_b = _c.sent();
return [3 /*break*/, 9];
case 9:
_i++;
return [3 /*break*/, 5];
case 10:
if (preferredQuote && (preferredQuote.currentPrice !== null || preferredQuote.stockName || preferredQuote.volumeRate5d !== null)) {
quoteMap.set(stockCode, preferredQuote);
}
return [2 /*return*/];
}
});
}); }))];
case 1:
_a.sent();
return [2 /*return*/, quoteMap];
}
});
});
}
function listStockAlerts() {
return __awaiter(this, arguments, void 0, function (filterType) {
var rows, matchedCodes, quotes, krxItems, krxByCode, groupedItems;
if (filterType === void 0) { filterType = 'all'; }
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ensureStockAlertTable()];
case 1:
_a.sent();
rows = [];
if (!(filterType === 'all')) return [3 /*break*/, 3];
return [4 /*yield*/, (0, client_js_1.db)(exports.STOCK_ALERT_TABLE).select('*').orderBy('updated_at', 'desc')];
case 2:
rows = (_a.sent());
return [3 /*break*/, 6];
case 3: return [4 /*yield*/, (0, client_js_1.db)(exports.STOCK_ALERT_TABLE)
.select('stock_code')
.where({ alert_type: normalizeAlertType(filterType) })
.groupBy('stock_code')];
case 4:
matchedCodes = (_a.sent()).map(function (row) { return row.stock_code; });
if (!matchedCodes.length) {
return [2 /*return*/, []];
}
return [4 /*yield*/, (0, client_js_1.db)(exports.STOCK_ALERT_TABLE)
.select('*')
.whereIn('stock_code', matchedCodes)
.orderBy('updated_at', 'desc')];
case 5:
rows = (_a.sent());
_a.label = 6;
case 6: return [4 /*yield*/, fetchQuotesByCodes(rows.map(function (row) { return row.stock_code; }))];
case 7:
quotes = _a.sent();
return [4 /*yield*/, fetchKrxListedStocks()];
case 8:
krxItems = _a.sent();
krxByCode = new Map(krxItems.map(function (item) { return [item.stockCode, item]; }));
groupedItems = new Map();
rows.forEach(function (row) {
var _a, _b, _c, _d, _e;
var alertType = normalizeAlertType(row.alert_type);
var quote = quotes.get(row.stock_code);
var krxMatch = krxByCode.get(row.stock_code);
var existing = groupedItems.get(row.stock_code);
if (existing) {
if (!existing.alertTypes.includes(alertType)) {
existing.alertTypes.push(alertType);
existing.alertTypeLabels.push(getAlertTypeLabel(alertType));
}
var updatedAtTime = Date.parse(normalizeTimestamp(row.updated_at));
var existingUpdatedAtTime = Date.parse(existing.updatedAt);
if (Number.isFinite(updatedAtTime) && (!Number.isFinite(existingUpdatedAtTime) || updatedAtTime > existingUpdatedAtTime)) {
existing.updatedAt = normalizeTimestamp(row.updated_at);
}
return;
}
groupedItems.set(row.stock_code, {
id: row.stock_code,
stockCode: row.stock_code,
stockName: row.stock_name || (krxMatch === null || krxMatch === void 0 ? void 0 : krxMatch.stockName) || (quote === null || quote === void 0 ? void 0 : quote.stockName) || row.stock_code,
alertTypes: [alertType],
alertTypeLabels: [getAlertTypeLabel(alertType)],
currentPrice: (_a = quote === null || quote === void 0 ? void 0 : quote.currentPrice) !== null && _a !== void 0 ? _a : null,
changeRate: (_b = quote === null || quote === void 0 ? void 0 : quote.changeRate) !== null && _b !== void 0 ? _b : null,
volumeRate5d: (_c = quote === null || quote === void 0 ? void 0 : quote.volumeRate5d) !== null && _c !== void 0 ? _c : null,
currentVolume: (_d = quote === null || quote === void 0 ? void 0 : quote.currentVolume) !== null && _d !== void 0 ? _d : null,
quotedAt: (_e = quote === null || quote === void 0 ? void 0 : quote.quotedAt) !== null && _e !== void 0 ? _e : null,
createdAt: normalizeTimestamp(row.created_at),
updatedAt: normalizeTimestamp(row.updated_at),
});
});
return [2 /*return*/, Array.from(groupedItems.values()).sort(function (left, right) { return Date.parse(right.updatedAt) - Date.parse(left.updatedAt); })];
}
});
});
}
function createStockAlert(input) {
return __awaiter(this, void 0, void 0, function () {
var identity, alertTypes, existingRows, mergedAlertTypes, now, created;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ensureStockAlertTable()];
case 1:
_a.sent();
return [4 /*yield*/, resolveStockIdentity(input)];
case 2:
identity = _a.sent();
alertTypes = Array.from(new Set(input.alertTypes.map(function (value) { return normalizeAlertType(value); })));
if (!alertTypes.length) {
throw new Error('알림유형을 하나 이상 선택해 주세요.');
}
return [4 /*yield*/, (0, client_js_1.db)(exports.STOCK_ALERT_TABLE)
.select('*')
.where({ stock_code: identity.stockCode })];
case 3:
existingRows = (_a.sent());
if (existingRows.length) {
mergedAlertTypes = Array.from(new Set(__spreadArray(__spreadArray([], existingRows.map(function (row) { return normalizeAlertType(row.alert_type); }), true), alertTypes, true)));
return [2 /*return*/, updateStockAlert(identity.stockCode, {
stockCode: identity.stockCode,
stockName: identity.stockName,
alertTypes: mergedAlertTypes,
})];
}
now = new Date().toISOString();
return [4 /*yield*/, (0, client_js_1.db)(exports.STOCK_ALERT_TABLE).insert(alertTypes.map(function (alertType) { return ({
id: "stock-alert-".concat(Date.now().toString(36), "-").concat(Math.random().toString(36).slice(2, 8)),
stock_code: identity.stockCode,
stock_name: identity.stockName,
alert_type: alertType,
created_at: now,
updated_at: now,
}); }))];
case 4:
_a.sent();
return [4 /*yield*/, listStockAlerts('all').then(function (rows) { return rows.filter(function (row) { return row.stockCode === identity.stockCode; }); })];
case 5:
created = (_a.sent())[0];
if (!created) {
throw new Error('저장된 종목 알림을 다시 불러오지 못했습니다.');
}
return [2 /*return*/, created];
}
});
});
}
function updateStockAlert(id, input) {
return __awaiter(this, void 0, void 0, function () {
var currentRows, existing, identity, updatedAt, alertTypes, updated;
var _this = this;
var _a, _b;
return __generator(this, function (_c) {
switch (_c.label) {
case 0: return [4 /*yield*/, ensureStockAlertTable()];
case 1:
_c.sent();
return [4 /*yield*/, (0, client_js_1.db)(exports.STOCK_ALERT_TABLE)
.select('*')
.where(function (query) {
query.where({ stock_code: normalizeStockCode(id) || '__never__' }).orWhere({ id: id });
})];
case 2:
currentRows = (_c.sent());
if (!currentRows.length) {
throw new Error('수정할 종목 알림을 찾을 수 없습니다.');
}
existing = currentRows[0];
return [4 /*yield*/, resolveStockIdentity({
stockCode: (_a = input.stockCode) !== null && _a !== void 0 ? _a : existing === null || existing === void 0 ? void 0 : existing.stock_code,
stockName: (_b = input.stockName) !== null && _b !== void 0 ? _b : existing === null || existing === void 0 ? void 0 : existing.stock_name,
})];
case 3:
identity = _c.sent();
updatedAt = new Date().toISOString();
alertTypes = Array.from(new Set(input.alertTypes.map(function (value) { return normalizeAlertType(value); })));
if (!alertTypes.length) {
throw new Error('알림유형을 하나 이상 선택해 주세요.');
}
if (!(identity.stockCode !== existing.stock_code)) return [3 /*break*/, 5];
return [4 /*yield*/, ensureNoDuplicateStockCode(identity.stockCode)];
case 4:
_c.sent();
_c.label = 5;
case 5: return [4 /*yield*/, client_js_1.db.transaction(function (trx) { return __awaiter(_this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, trx(exports.STOCK_ALERT_TABLE).where({ stock_code: existing.stock_code }).delete()];
case 1:
_a.sent();
return [4 /*yield*/, trx(exports.STOCK_ALERT_TABLE).insert(alertTypes.map(function (alertType) {
var _a, _b;
return ({
id: "stock-alert-".concat(Date.now().toString(36), "-").concat(Math.random().toString(36).slice(2, 8)),
stock_code: identity.stockCode,
stock_name: identity.stockName,
alert_type: alertType,
created_at: (_b = (_a = currentRows.find(function (row) { return row.alert_type === alertType; })) === null || _a === void 0 ? void 0 : _a.created_at) !== null && _b !== void 0 ? _b : updatedAt,
updated_at: updatedAt,
});
}))];
case 2:
_a.sent();
return [2 /*return*/];
}
});
}); })];
case 6:
_c.sent();
return [4 /*yield*/, listStockAlerts('all').then(function (rows) { return rows.filter(function (row) { return row.stockCode === identity.stockCode; }); })];
case 7:
updated = (_c.sent())[0];
if (!updated) {
throw new Error('수정된 종목 알림을 다시 불러오지 못했습니다.');
}
return [2 /*return*/, updated];
}
});
});
}
function deleteStockAlert(id) {
return __awaiter(this, void 0, void 0, function () {
var normalizedCode, count, _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, ensureStockAlertTable()];
case 1:
_b.sent();
normalizedCode = normalizeStockCode(id);
if (!normalizedCode) return [3 /*break*/, 3];
return [4 /*yield*/, (0, client_js_1.db)(exports.STOCK_ALERT_TABLE).where({ stock_code: normalizedCode }).delete()];
case 2:
_a = _b.sent();
return [3 /*break*/, 5];
case 3: return [4 /*yield*/, (0, client_js_1.db)(exports.STOCK_ALERT_TABLE).where({ id: id }).delete()];
case 4:
_a = _b.sent();
_b.label = 5;
case 5:
count = _a;
if (!count) {
throw new Error('삭제할 종목 알림을 찾을 수 없습니다.');
}
return [2 /*return*/];
}
});
});
}
function saveStockAlerts(items) {
return __awaiter(this, void 0, void 0, function () {
var seenCodes, _i, items_1, item, normalizedCode, savedItems, _a, items_2, item, trimmedId, savedItem, _b;
var _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0: return [4 /*yield*/, ensureStockAlertTable()];
case 1:
_e.sent();
seenCodes = new Set();
for (_i = 0, items_1 = items; _i < items_1.length; _i++) {
item = items_1[_i];
normalizedCode = normalizeStockCode((_c = item.stockCode) !== null && _c !== void 0 ? _c : '');
if (!normalizedCode) {
continue;
}
if (seenCodes.has(normalizedCode)) {
throw new Error('동일한 종목코드를 중복 저장할 수 없습니다.');
}
if (!item.alertTypes.length) {
throw new Error('알림유형을 하나 이상 선택해 주세요.');
}
seenCodes.add(normalizedCode);
}
savedItems = [];
_a = 0, items_2 = items;
_e.label = 2;
case 2:
if (!(_a < items_2.length)) return [3 /*break*/, 8];
item = items_2[_a];
trimmedId = (_d = item.id) === null || _d === void 0 ? void 0 : _d.trim();
if (!trimmedId) return [3 /*break*/, 4];
return [4 /*yield*/, updateStockAlert(trimmedId, item)];
case 3:
_b = _e.sent();
return [3 /*break*/, 6];
case 4: return [4 /*yield*/, createStockAlert(item)];
case 5:
_b = _e.sent();
_e.label = 6;
case 6:
savedItem = _b;
savedItems.push(savedItem);
_e.label = 7;
case 7:
_a++;
return [3 /*break*/, 2];
case 8: return [2 /*return*/, savedItems];
}
});
});
}
function formatStockAlertPrice(value) {
if (!isFiniteNumber(value)) {
return '-';
}
return "".concat(Math.round(value).toLocaleString('ko-KR'), "\u20A9");
}
function formatStockAlertChangeRate(value) {
if (!isFiniteNumber(value)) {
return '(변동률 확인불가)';
}
if (value > 0) {
return "(+".concat(value.toFixed(2), "% \u25B2)");
}
if (value < 0) {
return "(".concat(value.toFixed(2), "% \u25BC)");
}
return '(0.00% -)';
}
function canBuildCurrentPriceStockAlertLine(item) {
return item.alertTypes.includes('price') && isFiniteNumber(item.currentPrice) && isFiniteNumber(item.changeRate);
}
function canBuildChangeThresholdStockAlertLine(item) {
return isFiniteNumber(item.currentPrice) && isFiniteNumber(item.changeRate);
}
function buildCurrentPriceStockAlertLines(items) {
return items
.filter(canBuildCurrentPriceStockAlertLine)
.map(function (item) { return "".concat(item.stockName, " ").concat(formatStockAlertPrice(item.currentPrice), " ").concat(formatStockAlertChangeRate(item.changeRate)); });
}
function buildChangeRateThresholdStockAlertLines(items, thresholdPercent) {
return items
.filter(function (item) { var _a; return canBuildChangeThresholdStockAlertLine(item) && Math.abs((_a = item.changeRate) !== null && _a !== void 0 ? _a : 0) >= thresholdPercent; })
.sort(function (left, right) {
var _a, _b;
var changeRateGap = Math.abs(((_a = right.changeRate) !== null && _a !== void 0 ? _a : 0)) - Math.abs(((_b = left.changeRate) !== null && _b !== void 0 ? _b : 0));
if (changeRateGap !== 0) {
return changeRateGap;
}
return left.stockName.localeCompare(right.stockName, 'ko-KR');
})
.map(function (item) { return "".concat(item.stockName, " ").concat(formatStockAlertPrice(item.currentPrice), " ").concat(formatStockAlertChangeRate(item.changeRate)); });
}
function listStockAlertVolumeSnapshots() {
return __awaiter(this, void 0, void 0, function () {
var rows;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ensureStockAlertVolumeSnapshotTable()];
case 1:
_a.sent();
return [4 /*yield*/, (0, client_js_1.db)(exports.STOCK_ALERT_VOLUME_SNAPSHOT_TABLE)
.select('*')
.orderBy('updated_at', 'desc')];
case 2:
rows = (_a.sent());
return [2 /*return*/, new Map(rows
.map(function (row) { return normalizeStockAlertVolumeSnapshotRow(row); })
.filter(function (row) { return row.stockCode; })
.map(function (row) { return [row.stockCode, row]; }))];
}
});
});
}
function upsertStockAlertVolumeSnapshots(items, previousSnapshots) {
return __awaiter(this, void 0, void 0, function () {
var records;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ensureStockAlertVolumeSnapshotTable()];
case 1:
_a.sent();
if (!items.length) {
return [2 /*return*/];
}
records = items.map(function (item) { var _a; return buildStockAlertVolumeSnapshotRecord(item, item.currentVolume, (_a = previousSnapshots.get(item.stockCode)) !== null && _a !== void 0 ? _a : null); });
return [4 /*yield*/, (0, client_js_1.db)(exports.STOCK_ALERT_VOLUME_SNAPSHOT_TABLE)
.insert(records)
.onConflict('stock_code')
.merge({
stock_name: client_js_1.db.ref('excluded.stock_name'),
previous_volume: client_js_1.db.ref('excluded.previous_volume'),
current_volume: client_js_1.db.ref('excluded.current_volume'),
volume_increase_percent: client_js_1.db.ref('excluded.volume_increase_percent'),
current_price: client_js_1.db.ref('excluded.current_price'),
change_rate: client_js_1.db.ref('excluded.change_rate'),
quoted_at: client_js_1.db.ref('excluded.quoted_at'),
updated_at: client_js_1.db.ref('excluded.updated_at'),
})];
case 2:
_a.sent();
return [2 /*return*/];
}
});
});
}
function buildChangeRateAndVolumeSpikeStockAlertCandidates(items, previousSnapshots, options) {
return items
.flatMap(function (item) {
var _a, _b;
if (!canBuildChangeThresholdStockAlertLine(item)) {
return [];
}
var previousSnapshot = previousSnapshots.get(item.stockCode);
var previousVolume = (_a = previousSnapshot === null || previousSnapshot === void 0 ? void 0 : previousSnapshot.currentVolume) !== null && _a !== void 0 ? _a : null;
var currentVolume = normalizeNonNegativeVolume(item.currentVolume);
var volumeIncreasePercent = calculateVolumeIncreasePercent(currentVolume, previousVolume);
if (volumeIncreasePercent === null ||
Math.abs((_b = item.changeRate) !== null && _b !== void 0 ? _b : 0) < options.thresholdPercent ||
volumeIncreasePercent < options.minVolumeIncreasePercent) {
return [];
}
return [
{
stockCode: item.stockCode,
stockName: item.stockName,
currentPrice: item.currentPrice,
changeRate: item.changeRate,
currentVolume: currentVolume,
previousVolume: previousVolume,
volumeIncreasePercent: volumeIncreasePercent,
quotedAt: item.quotedAt,
},
];
})
.sort(function (left, right) {
var _a, _b, _c, _d;
var changeRateGap = Math.abs(((_a = right.changeRate) !== null && _a !== void 0 ? _a : 0)) - Math.abs(((_b = left.changeRate) !== null && _b !== void 0 ? _b : 0));
if (changeRateGap !== 0) {
return changeRateGap;
}
var volumeGap = ((_c = right.volumeIncreasePercent) !== null && _c !== void 0 ? _c : 0) - ((_d = left.volumeIncreasePercent) !== null && _d !== void 0 ? _d : 0);
if (volumeGap !== 0) {
return volumeGap;
}
return left.stockName.localeCompare(right.stockName, 'ko-KR');
});
}
function buildChangeRateAndVolumeSpikeStockAlertLines(items, previousSnapshots, options) {
return buildChangeRateAndVolumeSpikeStockAlertCandidates(items, previousSnapshots, options).map(function (item) { return "".concat(item.stockName, " ").concat(formatStockAlertPrice(item.currentPrice), " ").concat(formatStockAlertChangeRate(item.changeRate)); });
}
function createSkippedNotificationResult(reason) {
var skippedWebResult = {
ok: true,
skipped: true,
reason: reason,
sentCount: 0,
failedCount: 0,
invalidEndpoints: [],
};
var skippedIosResult = {
ok: true,
skipped: true,
reason: reason,
sentCount: 0,
failedCount: 0,
invalidTokens: [],
};
return {
ios: skippedIosResult,
web: skippedWebResult,
};
}
function buildStockAlertNotificationIdentity(options) {
var modeKey = options.mode === 'price'
? 'current-price'
: options.mode === 'change-threshold'
? 'change-threshold'
: 'change-threshold-volume-spike';
var legacyNotificationKey = "".concat(options.serviceKey, ":current-price");
var legacyModeNotificationKey = "".concat(options.serviceKey, ":").concat(modeKey);
return {
threadId: "schedule-stock-alert:".concat(options.scheduleId),
notificationKey: "schedule-stock-alert:".concat(options.scheduleId),
notificationScope: "schedule-stock-alert:".concat(options.scheduleId),
notificationAliases: __spreadArray([], new Set([
options.serviceKey,
legacyNotificationKey,
legacyModeNotificationKey,
options.scheduleId === 2 ? STOCK_ALERT_NOTIFICATION_SCOPE : '',
options.scheduleId === 2 ? "".concat(STOCK_ALERT_NOTIFICATION_SCOPE, ":current-price") : '',
].filter(Boolean)), true),
};
}
function sendManagedStockAlertWebPush(options) {
return __awaiter(this, void 0, void 0, function () {
var thresholdPercent, minVolumeIncreasePercent, items, previousSnapshots, _a, lines, hasRegisteredTargets, hasComparableVolumeBaseline, skippedReason, skippedResult, body, notificationIdentity, result;
var _b, _c;
return __generator(this, function (_d) {
switch (_d.label) {
case 0:
thresholdPercent = Math.max(0, Number((_b = options.thresholdPercent) !== null && _b !== void 0 ? _b : 5));
minVolumeIncreasePercent = Math.max(0, Number((_c = options.minVolumeIncreasePercent) !== null && _c !== void 0 ? _c : 300));
return [4 /*yield*/, listStockAlerts(options.mode === 'price' ? 'price' : 'all')];
case 1:
items = _d.sent();
if (!(options.mode === 'change-threshold-volume-spike')) return [3 /*break*/, 3];
return [4 /*yield*/, listStockAlertVolumeSnapshots()];
case 2:
_a = _d.sent();
return [3 /*break*/, 4];
case 3:
_a = new Map();
_d.label = 4;
case 4:
previousSnapshots = _a;
lines = options.mode === 'price'
? buildCurrentPriceStockAlertLines(items)
: options.mode === 'change-threshold'
? buildChangeRateThresholdStockAlertLines(items, thresholdPercent)
: buildChangeRateAndVolumeSpikeStockAlertLines(items, previousSnapshots, {
thresholdPercent: thresholdPercent,
minVolumeIncreasePercent: minVolumeIncreasePercent,
});
hasRegisteredTargets = options.mode === 'price' ? items.some(function (item) { return item.alertTypes.includes('price'); }) : items.length > 0;
hasComparableVolumeBaseline = options.mode !== 'change-threshold-volume-spike'
? false
: items.some(function (item) {
var previousSnapshot = previousSnapshots.get(item.stockCode);
return (previousSnapshot === null || previousSnapshot === void 0 ? void 0 : previousSnapshot.currentVolume) !== null && (previousSnapshot === null || previousSnapshot === void 0 ? void 0 : previousSnapshot.currentVolume) !== undefined;
});
skippedReason = options.mode === 'price'
? hasRegisteredTargets
? '현재가 시세를 확인할 수 있는 종목이 없습니다.'
: '현재가로 등록된 종목이 없습니다.'
: options.mode === 'change-threshold'
? hasRegisteredTargets
? "".concat(thresholdPercent, "% \uC774\uC0C1 \uBCC0\uB3D9 \uC885\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.")
: '등록된 종목이 없습니다.'
: !hasRegisteredTargets
? '등록된 종목이 없습니다.'
: !hasComparableVolumeBaseline
? '\uC774\uC804 \uAC70\uB798\uB7C9 \uB610\uB294 5\uC601\uC5C5\uC77C \uD3C9\uADE0 \uAC70\uB798\uB7C9 \uBE44\uAD50 \uAE30\uC900\uC774 \uC5C6\uC5B4 \uC2A4\uB0C5\uC0F7\uB9CC \uAC31\uC2E0\uD588\uC2B5\uB2C8\uB2E4.'
: "\uB4F1\uB77D\uB960 ".concat(thresholdPercent, "% \uC774\uC0C1\uC774\uBA74\uC11C \uC9C1\uC804 \uAC70\uB798\uB7C9 \uC99D\uD3ED\uC218 \uB300\uBE44 \uC774\uBC88 \uAC70\uB798\uB7C9 \uC99D\uD3ED\uC774 ").concat(minVolumeIncreasePercent, "% \uC774\uC0C1\uC778 \uC885\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
skippedResult = createSkippedNotificationResult(skippedReason);
if (!(options.mode === 'change-threshold-volume-spike')) return [3 /*break*/, 6];
return [4 /*yield*/, upsertStockAlertVolumeSnapshots(items, previousSnapshots)];
case 5:
_d.sent();
_d.label = 6;
case 6:
if (!lines.length) {
return [2 /*return*/, {
ok: true,
skipped: true,
reason: skippedReason,
title: options.title,
body: '',
itemCount: 0,
lines: [],
ios: skippedResult.ios,
web: skippedResult.web,
}];
}
body = lines.join('\n');
notificationIdentity = buildStockAlertNotificationIdentity(options);
return [4 /*yield*/, (0, notification_service_js_1.sendNotifications)({
title: options.title,
body: body,
threadId: notificationIdentity.threadId,
targetAppDomains: [STOCK_ALERT_NOTIFICATION_TARGET_DOMAIN],
data: {
category: 'stock-alert',
eventType: options.mode === 'price'
? 'stock-alert-current-price'
: options.mode === 'change-threshold'
? 'stock-alert-change-threshold'
: 'stock-alert-change-threshold-volume-spike',
notificationKey: notificationIdentity.notificationKey,
notificationScope: notificationIdentity.notificationScope,
notificationAliases: JSON.stringify(notificationIdentity.notificationAliases),
replaceExistingScope: 'true',
source: options.serviceKey,
targetUrl: STOCK_ALERT_NOTIFICATION_TARGET_URL,
},
}, {
disableIos: true,
})];
case 7:
result = _d.sent();
return [2 /*return*/, __assign(__assign({}, result), { title: options.title, body: body, itemCount: lines.length, lines: lines })];
}
});
});
}
function sendCurrentPriceStockAlertWebPush() {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
return [2 /*return*/, sendManagedStockAlertWebPush({
scheduleId: 2,
serviceKey: STOCK_ALERT_NOTIFICATION_SCOPE,
title: STOCK_ALERT_NOTIFICATION_TITLE,
mode: 'price',
})];
});
});
}
function updateStockAlertLayoutFeatureDescription() {
return __awaiter(this, void 0, void 0, function () {
var layoutRecord, tree, changed, nextInteractions;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ensureStockAlertTable()];
case 1:
_a.sent();
return [4 /*yield*/, (0, client_js_1.db)('play_layouts').select('id', 'tree').where({ name: exports.STOCK_ALERT_LAYOUT_NAME }).first()];
case 2:
layoutRecord = _a.sent();
if (!layoutRecord || typeof layoutRecord !== 'object') {
return [2 /*return*/, false];
}
tree = layoutRecord.tree;
if (!tree || !Array.isArray(tree.interactions)) {
return [2 /*return*/, false];
}
changed = false;
nextInteractions = tree.interactions.map(function (interaction) {
var _a;
var title = (_a = interaction.title) === null || _a === void 0 ? void 0 : _a.trim();
if (title === '그리드 기본정의') {
var nextDescription = [
'## 그리드 필드를 아래로 정의하세요.',
' - 종목명, 등락률, 현재가, 기준일시, 알림유형',
'## 숨긴필드',
' - 종목코드',
'## DB관리 데이터',
' - 종목코드, 알림유형',
'## 외부 API 및 가공 데이터',
' - DB에 저장된 종목코드에 대한 현재가 등락률 기준일시를 표현',
' - 현재가 데이터는 정규장이 아닌 가격도 모두 가져오도록 해주세요.',
'## 서비스 구현',
' - 해당 그리드 데이터를 저장할 DB 및 서비스 API를 구현허고 연결하세요. CRUD',
' - 알림유형은 한종목에 멀티로 저장되어야 합니다.',
'## 입력',
'알림유형의 경우 멀티선택 가능하게 해주세요.',
].join('\n');
var nextNotes = 'GET /api/stock-alerts?alertType=... 로 조회하고 PUT /api/stock-alerts/batch, DELETE /api/stock-alerts/:id 로 저장/삭제합니다. 알림유형은 그리드 행에서 멀티선택 드롭다운으로 편집되며 한 종목에 멀티선택으로 저장됩니다. 현재가·등락률·기준일시는 네이버 실시간 시세를 우선 사용하고, 필요 시 Yahoo Finance를 보조로 사용하며 비정규장 가격도 반영합니다. 거래량은 네이버 실시간 누적거래량을 현재값으로 받고, Yahoo Finance 일봉 최근 5영업일 평균 대비 비율(%)로 계산해 제공합니다.';
if (interaction.description !== nextDescription || interaction.implementationNotes !== nextNotes) {
changed = true;
return __assign(__assign({}, interaction), { description: nextDescription, implementationNotes: nextNotes });
}
}
if (title === '얼림유형 검색') {
var nextNotes = 'Primary Pane Select Input 값은 all, price, top3 코드로 유지하고 Secondary Pane 그리드 조회 파라미터 alertType 으로 바로 전달합니다.';
if (interaction.implementationNotes !== nextNotes) {
changed = true;
return __assign(__assign({}, interaction), { implementationNotes: nextNotes });
}
}
if (title === '행추가 기능') {
var nextNotes = 'GET /api/stock-alerts/search?query=... 로 종목명/종목코드를 조회하고, 선택한 종목코드·종목명·시장구분을 그리드 신규 행으로 매핑합니다. 동일 종목코드는 중복 추가를 막고, 선택 후 해당 행으로 포커스를 이동합니다.';
if (interaction.implementationNotes !== nextNotes) {
changed = true;
return __assign(__assign({}, interaction), { implementationNotes: nextNotes });
}
}
return interaction;
});
if (!changed) {
return [2 /*return*/, false];
}
return [4 /*yield*/, (0, client_js_1.db)('play_layouts')
.where({ id: layoutRecord.id })
.update({
tree: __assign(__assign({}, tree), { interactions: nextInteractions }),
})];
case 3:
_a.sent();
return [2 /*return*/, true];
}
});
});
}
|