@cartanova/qgrid-cli 2.6.8 → 2.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/dist/application/qgrid/expired-token-reminder.js +20 -5
- package/bundle/dist/application/setting/setting.constant.js +226 -0
- package/bundle/dist/application/setting/setting.model.js +20 -2
- package/bundle/dist/application/setting/setting.types.js +5 -2
- package/bundle/dist/sonamu.config.js +2 -2
- package/bundle/dist/utils/providers/openai/openai-worker-pool-config.js +2 -2
- package/bundle/dist/utils/quiet-hours.js +30 -26
- package/bundle/dist/utils/slack-notify.js +6 -2
- package/bundle/src/application/qgrid/expired-token-reminder.test.ts +43 -0
- package/bundle/src/application/qgrid/expired-token-reminder.ts +22 -2
- package/bundle/src/application/setting/{setting.schema.test.ts → setting.constant.test.ts} +30 -1
- package/bundle/src/application/setting/{setting.schema.ts → setting.constant.ts} +86 -10
- package/bundle/src/application/setting/setting.model.ts +15 -1
- package/bundle/src/application/setting/setting.types.ts +3 -1
- package/bundle/src/application/sonamu.generated.http +7 -0
- package/bundle/src/sonamu.config.ts +3 -1
- package/bundle/src/utils/providers/openai/openai-worker-pool-config.ts +1 -1
- package/bundle/src/utils/quiet-hours.ts +56 -17
- package/bundle/src/utils/slack-notify.test.ts +96 -13
- package/bundle/src/utils/slack-notify.ts +8 -1
- package/bundle/web-dist/client/assets/{index-DrsXE13r.js → index-BRRNJRgu.js} +3 -3
- package/bundle/web-dist/client/assets/index-rdbxJinT.css +1 -0
- package/bundle/web-dist/client/assets/{logs-e4-6aYnO.js → logs-BI5K8oe4.js} +1 -1
- package/bundle/web-dist/client/assets/{monit-eOAlYTK9.js → monit-4Y-Razet.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-Qie4ogtJ.js → routes-CkBXB-V4.js} +1 -1
- package/bundle/web-dist/client/assets/services.generated-BzwTuZRP.js +1 -0
- package/bundle/web-dist/client/assets/settings-B62_r1i4.js +1 -0
- package/bundle/web-dist/client/assets/{show-60iOOM-e.js → show-CGRchJj7.js} +1 -1
- package/bundle/web-dist/client/assets/tokens-Bx5mQnoJ.js +1 -0
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-0ZGq74bZ.js → use-oauth-login-flow-Bz87RV5v.js} +1 -1
- package/bundle/web-dist/client/index.html +2 -2
- package/bundle/web-dist/server/assets/{logs-DmkzGxGU.js → logs-C1rxLh8H.js} +1 -1
- package/bundle/web-dist/server/assets/{monit-DyKLJX4s.js → monit-C8gx9Sar.js} +1 -1
- package/bundle/web-dist/server/assets/{routes-g62E9xmC.js → routes-ZW_Ih5Em.js} +8 -5
- package/bundle/web-dist/server/assets/{services.generated-DADr9OGH.js → services.generated-CZIChgHV.js} +8 -0
- package/bundle/web-dist/server/assets/{settings-CMJKayff.js → settings--aoatERH.js} +72 -22
- package/bundle/web-dist/server/assets/{show-BGAiCPGq.js → show-BY27w9oi.js} +1 -1
- package/bundle/web-dist/server/assets/{tokens-DmhIfVct.js → tokens-DIObtTuZ.js} +13 -53
- package/bundle/web-dist/server/assets/{use-oauth-login-flow-B6MY65IN.js → use-oauth-login-flow-V3k8PSqQ.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +7 -7
- package/package.json +1 -1
- package/skills/qgrid/references/cli-env-and-server-boot.md +5 -2
- package/skills/qgrid/references/decision-rationale.md +7 -0
- package/skills/qgrid/references/token-auth-quota-lifecycle.md +2 -1
- package/skills/qgrid/references/tool-calling-and-multiturn.md +21 -0
- package/bundle/dist/application/setting/setting.schema.js +0 -157
- package/bundle/web-dist/client/assets/index-7SZyiqAx.css +0 -1
- package/bundle/web-dist/client/assets/services.generated-T4ctVcia.js +0 -1
- package/bundle/web-dist/client/assets/settings-CTVZpAAg.js +0 -1
- package/bundle/web-dist/client/assets/tokens-DdDccGlX.js +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SETTING_DEFS } from "../../../application/setting/setting.
|
|
1
|
+
import { SETTING_DEFS } from "../../../application/setting/setting.constant.js";
|
|
2
2
|
import { getSetting } from "../../../application/setting/setting.store.js";
|
|
3
3
|
//#region src/utils/providers/openai/openai-worker-pool-config.ts
|
|
4
4
|
const MAX_OPENAI_WORKERS_PER_TOKEN = 20;
|
|
@@ -48,4 +48,4 @@ function estimateOpenAIWorkerRssGiB(totalWorkers) {
|
|
|
48
48
|
//#endregion
|
|
49
49
|
export { MAX_OPENAI_WORKERS_PER_TOKEN, OPENAI_WORKER_BASE_RSS_GIB, OPENAI_WORKER_RSS_GIB, estimateOpenAIWorkerRssGiB, resolveOpenAIWorkerPoolConfig };
|
|
50
50
|
|
|
51
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,
|
|
51
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BlbmFpLXdvcmtlci1wb29sLWNvbmZpZy5qcyIsIm5hbWVzIjpbXSwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvdXRpbHMvcHJvdmlkZXJzL29wZW5haS9vcGVuYWktd29ya2VyLXBvb2wtY29uZmlnLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7XG4gIE1BWF9PUEVOQUlfRVNUSU1BVEVEX1JTU19HSUIsXG4gIE1BWF9PUEVOQUlfTUlOX0hPU1RfQVZBSUxBQkxFX0dJQixcbiAgU0VUVElOR19ERUZTLFxufSBmcm9tIFwiLi4vLi4vLi4vYXBwbGljYXRpb24vc2V0dGluZy9zZXR0aW5nLmNvbnN0YW50XCI7XG5pbXBvcnQgeyBnZXRTZXR0aW5nIH0gZnJvbSBcIi4uLy4uLy4uL2FwcGxpY2F0aW9uL3NldHRpbmcvc2V0dGluZy5zdG9yZVwiO1xuXG5leHBvcnQgY29uc3QgTUFYX09QRU5BSV9XT1JLRVJTX1BFUl9UT0tFTiA9IDIwO1xuY29uc3QgREVGQVVMVF9PUEVOQUlfTUlOX1dPUktFUlNfUEVSX1RPS0VOID0gMTtcbmNvbnN0IERFRkFVTFRfT1BFTkFJX01BWF9XT1JLRVJTX1BFUl9UT0tFTiA9IDM7XG5cbmV4cG9ydCBjb25zdCBPUEVOQUlfV09SS0VSX0JBU0VfUlNTX0dJQiA9IDAuNzE7XG5leHBvcnQgY29uc3QgT1BFTkFJX1dPUktFUl9SU1NfR0lCID0gMC4xNTc7XG5cbmV4cG9ydCB0eXBlIE9wZW5BSVdvcmtlclBvb2xDb25maWcgPSB7XG4gIGF1dG9zY2FsZTogYm9vbGVhbjtcbiAgbWluV29ya2Vyc1BlclRva2VuOiBudW1iZXI7XG4gIG1heFdvcmtlcnNQZXJUb2tlbjogbnVtYmVyO1xuICBzY2FsZUludGVydmFsTXM6IG51bWJlcjtcbiAgc2NhbGVEb3duSWRsZU1zOiBudW1iZXI7XG4gIG1heEVzdGltYXRlZFJzc0dpQjogbnVtYmVyO1xuICBtaW5Ib3N0QXZhaWxhYmxlR2lCOiBudW1iZXI7XG59O1xuXG5mdW5jdGlvbiBib3VuZGVkSW50ZWdlcihcbiAgdmFsdWU6IHN0cmluZyB8IHVuZGVmaW5lZCxcbiAgZmFsbGJhY2s6IG51bWJlcixcbiAgbWluOiBudW1iZXIsXG4gIG1heDogbnVtYmVyLFxuKTogbnVtYmVyIHtcbiAgY29uc3QgcGFyc2VkID0gTnVtYmVyKHZhbHVlKTtcbiAgaWYgKCFOdW1iZXIuaXNGaW5pdGUocGFyc2VkKSkgcmV0dXJuIGZhbGxiYWNrO1xuICByZXR1cm4gTWF0aC5tYXgobWluLCBNYXRoLm1pbihNYXRoLmZsb29yKHBhcnNlZCksIG1heCkpO1xufVxuXG5mdW5jdGlvbiBib3VuZGVkTnVtYmVyKFxuICB2YWx1ZTogc3RyaW5nIHwgdW5kZWZpbmVkLFxuICBmYWxsYmFjazogbnVtYmVyLFxuICBtaW46IG51bWJlcixcbiAgbWF4OiBudW1iZXIsXG4pOiBudW1iZXIge1xuICBjb25zdCBwYXJzZWQgPSBOdW1iZXIodmFsdWUpO1xuICBpZiAoIU51bWJlci5pc0Zpbml0ZShwYXJzZWQpKSByZXR1cm4gZmFsbGJhY2s7XG4gIHJldHVybiBNYXRoLm1heChtaW4sIE1hdGgubWluKHBhcnNlZCwgbWF4KSk7XG59XG5cbi8qKlxuICog7ISk7KCVIOyggOyepeyGjChEQikg66W8IOuovOyggCDrs7Tqs6Ag7JeG7Jy866m0IGVudiDroZwg65ao7Ja07KeA64qUIOyhsO2ajCDqsJ3ssrQuXG4gKlxuICog6riw7KG0IO2MjOyLsSDroZzsp4HsnbQgYGVudltLRVldYCDtmJXtg5zrpbwg7KCE7KCc7ZWY66+A66GcLCDshozsiqTrp4wg67CU6r+UIOuBvOyasOqzoCDqsoDspp3Ct+q4sOuzuOqwkiDsspjrpqzripRcbiAqIOq3uOuMgOuhnCDrkZTri6QuIGBnZXRgIOygkeq3vOunjCDsp4Dsm5DtlZjrr4DroZwgYGluYOydtOuCmCBgT2JqZWN0LmtleXMoKWDroZwg7KGw7ZqM7ZWY7KeAIOyViuuKlOuLpC4g7YWM7Iqk7Yq464qUXG4gKiBgZW52YCDsnbjsnpDsl5Ag7Iic7IiY7ZWcIOunteydhCDrhJjqsqgg7KCA7J6l7IaM66W8IOyasO2ajO2VnOuLpC5cbiAqL1xuZnVuY3Rpb24gc2V0dGluZ3NCYWNrZWRFbnYoKTogUmVjb3JkPHN0cmluZywgc3RyaW5nIHwgdW5kZWZpbmVkPiB7XG4gIHJldHVybiBuZXcgUHJveHkoe30gYXMgUmVjb3JkPHN0cmluZywgc3RyaW5nIHwgdW5kZWZpbmVkPiwge1xuICAgIGdldDogKF90YXJnZXQsIHByb3A6IHN0cmluZykgPT4ge1xuICAgICAgY29uc3QgZGVmID0gU0VUVElOR19ERUZTLmZpbmQoKGQpID0+IGQuZW52S2V5ID09PSBwcm9wKTtcbiAgICAgIHJldHVybiBkZWYgPyBnZXRTZXR0aW5nKGRlZi5rZXksIGRlZi5lbnZLZXkpIDogcHJvY2Vzcy5lbnZbcHJvcF07XG4gICAgfSxcbiAgfSk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiByZXNvbHZlT3BlbkFJV29ya2VyUG9vbENvbmZpZyhcbiAgZW52OiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmcgfCB1bmRlZmluZWQ+ID0gc2V0dGluZ3NCYWNrZWRFbnYoKSxcbik6IE9wZW5BSVdvcmtlclBvb2xDb25maWcge1xuICBjb25zdCBhdXRvc2NhbGUgPSBlbnYuUUdSSURfT1BFTkFJX0FVVE9TQ0FMRSAhPT0gXCJmYWxzZVwiICYmIGVudi5RR1JJRF9PUEVOQUlfQVVUT1NDQUxFICE9PSBcIjBcIjtcbiAgY29uc3QgbWluV29ya2Vyc1BlclRva2VuID0gYm91bmRlZEludGVnZXIoXG4gICAgZW52LlFHUklEX09QRU5BSV9NSU5fV09SS0VSU19QRVJfVE9LRU4sXG4gICAgREVGQVVMVF9PUEVOQUlfTUlOX1dPUktFUlNfUEVSX1RPS0VOLFxuICAgIDEsXG4gICAgTUFYX09QRU5BSV9XT1JLRVJTX1BFUl9UT0tFTixcbiAgKTtcbiAgcmV0dXJuIHtcbiAgICBhdXRvc2NhbGUsXG4gICAgbWluV29ya2Vyc1BlclRva2VuLFxuICAgIC8vIGF1dG9zY2FsZSDsnbQg6rq87KeA66m0IG1heCDripQgbWluIOycvOuhnCDqs6DsoJXrkJjrr4DroZwgZW52IO2MjOyLsSDsnpDssrTqsIAgYXV0b3NjYWxlIOu2hOq4sCDslYjsl5Drp4wg7J6I64ukLlxuICAgIG1heFdvcmtlcnNQZXJUb2tlbjogYXV0b3NjYWxlXG4gICAgICA/IE1hdGgubWF4KFxuICAgICAgICAgIG1pbldvcmtlcnNQZXJUb2tlbixcbiAgICAgICAgICBib3VuZGVkSW50ZWdlcihcbiAgICAgICAgICAgIGVudi5RR1JJRF9PUEVOQUlfTUFYX1dPUktFUlNfUEVSX1RPS0VOLFxuICAgICAgICAgICAgREVGQVVMVF9PUEVOQUlfTUFYX1dPUktFUlNfUEVSX1RPS0VOLFxuICAgICAgICAgICAgMSxcbiAgICAgICAgICAgIE1BWF9PUEVOQUlfV09SS0VSU19QRVJfVE9LRU4sXG4gICAgICAgICAgKSxcbiAgICAgICAgKVxuICAgICAgOiBtaW5Xb3JrZXJzUGVyVG9rZW4sXG4gICAgc2NhbGVJbnRlcnZhbE1zOiBib3VuZGVkSW50ZWdlcihlbnYuUUdSSURfT1BFTkFJX1NDQUxFX0lOVEVSVkFMX01TLCA1XzAwMCwgMjUwLCAzMDBfMDAwKSxcbiAgICBzY2FsZURvd25JZGxlTXM6IGJvdW5kZWRJbnRlZ2VyKFxuICAgICAgZW52LlFHUklEX09QRU5BSV9TQ0FMRV9ET1dOX0lETEVfTVMsXG4gICAgICAxMCAqIDYwXzAwMCxcbiAgICAgIDFfMDAwLFxuICAgICAgMjQgKiA2MCAqIDYwXzAwMCxcbiAgICApLFxuICAgIG1heEVzdGltYXRlZFJzc0dpQjogYm91bmRlZE51bWJlcihcbiAgICAgIGVudi5RR1JJRF9PUEVOQUlfTUFYX0VTVElNQVRFRF9SU1NfR0lCLFxuICAgICAgMTYsXG4gICAgICAxLFxuICAgICAgTUFYX09QRU5BSV9FU1RJTUFURURfUlNTX0dJQixcbiAgICApLFxuICAgIG1pbkhvc3RBdmFpbGFibGVHaUI6IGJvdW5kZWROdW1iZXIoXG4gICAgICBlbnYuUUdSSURfT1BFTkFJX01JTl9IT1NUX0FWQUlMQUJMRV9HSUIsXG4gICAgICAyMCxcbiAgICAgIDAsXG4gICAgICBNQVhfT1BFTkFJX01JTl9IT1NUX0FWQUlMQUJMRV9HSUIsXG4gICAgKSxcbiAgfTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGVzdGltYXRlT3BlbkFJV29ya2VyUnNzR2lCKHRvdGFsV29ya2VyczogbnVtYmVyKTogbnVtYmVyIHtcbiAgcmV0dXJuIE9QRU5BSV9XT1JLRVJfQkFTRV9SU1NfR0lCICsgT1BFTkFJX1dPUktFUl9SU1NfR0lCICogdG90YWxXb3JrZXJzO1xufVxuIl0sIm1hcHBpbmdzIjoiOzs7QUFPQSxNQUFhLCtCQUErQjtBQUM1QyxNQUFNLHVDQUF1QztBQUM3QyxNQUFNLHVDQUF1QztBQUU3QyxNQUFhLDZCQUE2QjtBQUMxQyxNQUFhLHdCQUF3QjtBQVlyQyxTQUFTLGVBQ1AsT0FDQSxVQUNBLEtBQ0EsS0FDUTtDQUNSLE1BQU0sU0FBUyxPQUFPLEtBQUs7Q0FDM0IsSUFBSSxDQUFDLE9BQU8sU0FBUyxNQUFNLEdBQUcsT0FBTztDQUNyQyxPQUFPLEtBQUssSUFBSSxLQUFLLEtBQUssSUFBSSxLQUFLLE1BQU0sTUFBTSxHQUFHLEdBQUcsQ0FBQztBQUN4RDtBQUVBLFNBQVMsY0FDUCxPQUNBLFVBQ0EsS0FDQSxLQUNRO0NBQ1IsTUFBTSxTQUFTLE9BQU8sS0FBSztDQUMzQixJQUFJLENBQUMsT0FBTyxTQUFTLE1BQU0sR0FBRyxPQUFPO0NBQ3JDLE9BQU8sS0FBSyxJQUFJLEtBQUssS0FBSyxJQUFJLFFBQVEsR0FBRyxDQUFDO0FBQzVDOzs7Ozs7OztBQVNBLFNBQVMsb0JBQXdEO0NBQy9ELE9BQU8sSUFBSSxNQUFNLENBQUMsR0FBeUMsRUFDekQsTUFBTSxTQUFTLFNBQWlCO0VBQzlCLE1BQU0sTUFBTSxhQUFhLE1BQU0sTUFBTSxFQUFFLFdBQVcsSUFBSTtFQUN0RCxPQUFPLE1BQU0sV0FBVyxJQUFJLEtBQUssSUFBSSxNQUFNLElBQUksUUFBUSxJQUFJO0NBQzdELEVBQ0YsQ0FBQztBQUNIO0FBRUEsU0FBZ0IsOEJBQ2QsTUFBMEMsa0JBQWtCLEdBQ3BDO0NBQ3hCLE1BQU0sWUFBWSxJQUFJLDJCQUEyQixXQUFXLElBQUksMkJBQTJCO0NBQzNGLE1BQU0scUJBQXFCLGVBQ3pCLElBQUksb0NBQ0osc0NBQ0EsR0FBQSxFQUVGO0NBQ0EsT0FBTztFQUNMO0VBQ0E7RUFFQSxvQkFBb0IsWUFDaEIsS0FBSyxJQUNILG9CQUNBLGVBQ0UsSUFBSSxvQ0FDSixzQ0FDQSxHQUFBLEVBRUYsQ0FDRixJQUNBO0VBQ0osaUJBQWlCLGVBQWUsSUFBSSxnQ0FBZ0MsS0FBTyxLQUFLLEdBQU87RUFDdkYsaUJBQWlCLGVBQ2YsSUFBSSxpQ0FDSixLQUFLLEtBQ0wsS0FDQSxPQUFVLEdBQ1o7RUFDQSxvQkFBb0IsY0FDbEIsSUFBSSxvQ0FDSixJQUNBLEdBQUEsRUFFRjtFQUNBLHFCQUFxQixjQUNuQixJQUFJLHFDQUNKLElBQ0EsR0FBQSxFQUVGO0NBQ0Y7QUFDRjtBQUVBLFNBQWdCLDJCQUEyQixjQUE4QjtDQUN2RSxPQUFPLDZCQUE2Qix3QkFBd0I7QUFDOUQifQ==
|
|
@@ -1,29 +1,19 @@
|
|
|
1
1
|
//#region src/utils/quiet-hours.ts
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* 토큰 만료는 사람이 재로그인해야 풀리는데, 그 사람이 자거나 쉬는 동안 알려도 할 수 있는
|
|
6
|
-
* 일이 없다. 평일 업무 시간에만 알리고 나머지는 다음 근무 시간으로 미룬다 — 만료 알림은
|
|
7
|
-
* 주기적으로 반복되므로 조용 시간에 버려도 아침 첫 주기에 다시 온다.
|
|
8
|
-
*
|
|
9
|
-
* 다만 provider 의 마지막 토큰이 죽는 것은 서비스 정지 신호라 이 규칙에서 제외한다
|
|
10
|
-
* (호출부가 `notifySlack` 을 그냥 호출하면 된다).
|
|
11
|
-
*/
|
|
12
|
-
/** 이 시각부터(포함) 조용해진다. */
|
|
13
|
-
const QUIET_FROM_HOUR = 20;
|
|
14
|
-
/** 이 시각부터(포함) 다시 알린다. */
|
|
15
|
-
const QUIET_UNTIL_HOUR = 8;
|
|
2
|
+
/** `setting.constant.ts` 의 두 fallback 과 같아야 한다. 일치 여부는 테스트가 지킨다. */
|
|
3
|
+
const DEFAULT_QUIET_FROM_HOUR = 20;
|
|
4
|
+
const DEFAULT_QUIET_UNTIL_HOUR = 8;
|
|
16
5
|
/**
|
|
17
6
|
* 서버가 UTC 로 돌아도 한국 시간으로 판정한다. dev0 는 UTC 이므로 `getHours()` 를 그대로
|
|
18
7
|
* 쓰면 "저녁 8시"가 한국 시간 새벽 5시가 된다.
|
|
19
8
|
*/
|
|
9
|
+
const SEOUL_FORMATTER = new Intl.DateTimeFormat("en-US", {
|
|
10
|
+
timeZone: "Asia/Seoul",
|
|
11
|
+
hour: "numeric",
|
|
12
|
+
hour12: false,
|
|
13
|
+
weekday: "short"
|
|
14
|
+
});
|
|
20
15
|
function seoulParts(at) {
|
|
21
|
-
const parts =
|
|
22
|
-
timeZone: "Asia/Seoul",
|
|
23
|
-
hour: "numeric",
|
|
24
|
-
hour12: false,
|
|
25
|
-
weekday: "short"
|
|
26
|
-
}).formatToParts(at);
|
|
16
|
+
const parts = SEOUL_FORMATTER.formatToParts(at);
|
|
27
17
|
const hour = Number(parts.find((p) => p.type === "hour")?.value ?? "0");
|
|
28
18
|
const weekday = parts.find((p) => p.type === "weekday")?.value ?? "";
|
|
29
19
|
return {
|
|
@@ -31,13 +21,27 @@ function seoulParts(at) {
|
|
|
31
21
|
weekday
|
|
32
22
|
};
|
|
33
23
|
}
|
|
34
|
-
/**
|
|
35
|
-
function
|
|
24
|
+
/** 0..23 범위를 벗어나거나 숫자가 아니면 기본값으로 떨어진다. */
|
|
25
|
+
function boundedHour(raw, fallback) {
|
|
26
|
+
const parsed = Number(raw);
|
|
27
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 23) return fallback;
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 조용 시간인지 판정한다.
|
|
32
|
+
*
|
|
33
|
+
* 시작 > 종료면 자정을 넘는 구간(20시~익일 8시), 시작 < 종료면 같은 날 구간(0시~8시처럼)
|
|
34
|
+
* 으로 읽는다. 야간 근무처럼 낮에 조용하고 싶은 팀도 있어 뒤집힌 값을 막지 않는다.
|
|
35
|
+
*/
|
|
36
|
+
function isQuietHours(at, readSetting) {
|
|
36
37
|
const { hour, weekday } = seoulParts(at);
|
|
37
|
-
if (weekday === "Sat" || weekday === "Sun") return true;
|
|
38
|
-
|
|
38
|
+
if ((weekday === "Sat" || weekday === "Sun") && readSetting("slack.notifyOnWeekends", "SLACK_NOTIFY_ON_WEEKENDS") !== "true") return true;
|
|
39
|
+
const from = boundedHour(readSetting("slack.quietFromHour", "SLACK_QUIET_FROM_HOUR"), 20);
|
|
40
|
+
const until = boundedHour(readSetting("slack.quietUntilHour", "SLACK_QUIET_UNTIL_HOUR"), 8);
|
|
41
|
+
if (from === until) return false;
|
|
42
|
+
return from > until ? hour >= from || hour < until : hour >= from && hour < until;
|
|
39
43
|
}
|
|
40
44
|
//#endregion
|
|
41
|
-
export { isQuietHours };
|
|
45
|
+
export { DEFAULT_QUIET_FROM_HOUR, DEFAULT_QUIET_UNTIL_HOUR, isQuietHours };
|
|
42
46
|
|
|
43
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,
|
|
47
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicXVpZXQtaG91cnMuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL3F1aWV0LWhvdXJzLnRzIl0sInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogU2xhY2sg7JWM66a8IOyhsOyaqSDsi5zqsIQuXG4gKlxuICog7Yag7YGwIOunjOujjOuKlCDsgqzrnozsnbQg7J6s66Gc6re47J247ZW07JW8IO2SgOumrOuKlOuNsCwg6re4IOyCrOuejOydtCDsnpDqsbDrgpgg7Ims64qUIOuPmeyViCDslYzroKTrj4Qg7ZWgIOyImCDsnojripRcbiAqIOydvOydtCDsl4bri6QuIOyXheustCDsi5zqsITsl5Drp4wg7JWM66as6rOgIOuCmOuouOyngOuKlCDri6TsnYwg6re866y0IOyLnOqwhOycvOuhnCDrr7jro6zri6Qg4oCUIOunjOujjCDslYzrprzsnYBcbiAqIOyjvOq4sOyggeycvOuhnCDrsJjrs7XrkJjrr4DroZwg7KGw7JqpIOyLnOqwhOyXkCDrsoTroKTrj4Qg7JWE7LmoIOyyqyDso7zquLDsl5Ag64uk7IucIOyYqOuLpC5cbiAqXG4gKiDri6Trp4wgcHJvdmlkZXIg7J2YIOuniOyngOuniSDthqDtgbDsnbQg7KO964qUIOqyg+ydgCDshJzruYTsiqQg7KCV7KeAIOyLoO2YuOudvCDsnbQg6rec7LmZ7JeQ7IScIOygnOyZuO2VnOuLpFxuICogKO2YuOy2nOu2gOqwgCBgdXJnZW50OiB0cnVlYCDroZwg67O064K066m0IOuQnOuLpCkuXG4gKi9cbmltcG9ydCB7IHR5cGUgZ2V0U2V0dGluZyB9IGZyb20gXCIuLi9hcHBsaWNhdGlvbi9zZXR0aW5nL3NldHRpbmcuc3RvcmVcIjtcblxuLyoqXG4gKiDshKTsoJUg7KGw7ZqMIO2VqOyImOydmCDtmJXtg5zrp4wg67mM66aw64ukIOKAlCBgdHlwZWAgaW1wb3J0IOudvCDrn7Dtg4DsnoQg7J2Y7KG07J20IOyDneq4sOyngCDslYrripTri6QuXG4gKlxuICog6rCSIOyekOyytOuKlCDtla3sg4Eg7Zi47Lac67aAKGBzbGFjay1ub3RpZnlgKeqwgCDrhJjquLTri6QuIOyXrOq4sOyEnCBgZ2V0U2V0dGluZ2Ag7J2EIOyLpOygnOuhnCBpbXBvcnQg7ZWY66m0XG4gKiBgc2V0dGluZy5zdG9yZSDihpIgc2V0dGluZy5tb2RlbCDihpIgdG9rZW4ubW9kZWxgIOydtCDrlLjroKTsmYAsIGB0b2tlbi5tb2RlbGAg7J2EIG1vY2sg7ZWY64qUXG4gKiDthYzsiqTtirjrk6TsnbQg7Iuk7KCcIOuqqOuTiOydhCDrqLzsoIAg66Gc65Oc7ZW0IOuyhOumsOuLpC4g7KGw7JqpIOyLnOqwhCDtjJDsoJXsnYAg7Iuc6rCBIOqzhOyCsOydtOyngCDshKTsoJUg7KGw7ZqM6rCAXG4gKiDslYTri4jrr4DroZwsIOydveuKlCDssYXsnoTsnYAg7J2066+4IOyEpOygleydhCDsnb3qs6Ag7J6I64qUIO2YuOy2nOu2gOyXkCDrkZTri6QuXG4gKi9cbnR5cGUgUmVhZFNldHRpbmcgPSB0eXBlb2YgZ2V0U2V0dGluZztcblxuLyoqIGBzZXR0aW5nLmNvbnN0YW50LnRzYCDsnZgg65GQIGZhbGxiYWNrIOqzvCDqsJnslYTslbwg7ZWc64ukLiDsnbzsuZgg7Jes67aA64qUIO2FjOyKpO2KuOqwgCDsp4Dtgqjri6QuICovXG5leHBvcnQgY29uc3QgREVGQVVMVF9RVUlFVF9GUk9NX0hPVVIgPSAyMDtcbmV4cG9ydCBjb25zdCBERUZBVUxUX1FVSUVUX1VOVElMX0hPVVIgPSA4O1xuXG4vKipcbiAqIOyEnOuyhOqwgCBVVEMg66GcIOuPjOyVhOuPhCDtlZzqta0g7Iuc6rCE7Jy866GcIO2MkOygle2VnOuLpC4gZGV2MCDripQgVVRDIOydtOuvgOuhnCBgZ2V0SG91cnMoKWAg66W8IOq3uOuMgOuhnFxuICog7JOw66m0IFwi7KCA64WBIDjsi5xcIuqwgCDtlZzqta0g7Iuc6rCEIOyDiOuyvSA17Iuc6rCAIOuQnOuLpC5cbiAqL1xuY29uc3QgU0VPVUxfRk9STUFUVEVSID0gbmV3IEludGwuRGF0ZVRpbWVGb3JtYXQoXCJlbi1VU1wiLCB7XG4gIHRpbWVab25lOiBcIkFzaWEvU2VvdWxcIixcbiAgaG91cjogXCJudW1lcmljXCIsXG4gIGhvdXIxMjogZmFsc2UsXG4gIHdlZWtkYXk6IFwic2hvcnRcIixcbn0pO1xuXG5mdW5jdGlvbiBzZW91bFBhcnRzKGF0OiBEYXRlKTogeyBob3VyOiBudW1iZXI7IHdlZWtkYXk6IHN0cmluZyB9IHtcbiAgY29uc3QgcGFydHMgPSBTRU9VTF9GT1JNQVRURVIuZm9ybWF0VG9QYXJ0cyhhdCk7XG4gIGNvbnN0IGhvdXIgPSBOdW1iZXIocGFydHMuZmluZCgocCkgPT4gcC50eXBlID09PSBcImhvdXJcIik/LnZhbHVlID8/IFwiMFwiKTtcbiAgY29uc3Qgd2Vla2RheSA9IHBhcnRzLmZpbmQoKHApID0+IHAudHlwZSA9PT0gXCJ3ZWVrZGF5XCIpPy52YWx1ZSA/PyBcIlwiO1xuICAvLyBJbnRsIOydgCDsnpDsoJXsnYQgMjQg66GcIOykhCDsiJgg7J6I64ukLiAwLi4yMyDsnLzroZwg66ee7LaY64ukLlxuICByZXR1cm4geyBob3VyOiBob3VyICUgMjQsIHdlZWtkYXkgfTtcbn1cblxuLyoqIDAuLjIzIOuylOychOulvCDrspfslrTrgpjqsbDrgpgg7Iir7J6Q6rCAIOyVhOuLiOuptCDquLDrs7jqsJLsnLzroZwg65ao7Ja07KeE64ukLiAqL1xuZnVuY3Rpb24gYm91bmRlZEhvdXIocmF3OiBzdHJpbmcgfCB1bmRlZmluZWQsIGZhbGxiYWNrOiBudW1iZXIpOiBudW1iZXIge1xuICBjb25zdCBwYXJzZWQgPSBOdW1iZXIocmF3KTtcbiAgaWYgKCFOdW1iZXIuaXNJbnRlZ2VyKHBhcnNlZCkgfHwgcGFyc2VkIDwgMCB8fCBwYXJzZWQgPiAyMykgcmV0dXJuIGZhbGxiYWNrO1xuICByZXR1cm4gcGFyc2VkO1xufVxuXG4vKipcbiAqIOyhsOyaqSDsi5zqsITsnbjsp4Ag7YyQ7KCV7ZWc64ukLlxuICpcbiAqIOyLnOyekSA+IOyiheujjOuptCDsnpDsoJXsnYQg64SY64qUIOq1rOqwhCgyMOyLnH7snbXsnbwgOOyLnCksIOyLnOyekSA8IOyiheujjOuptCDqsJnsnYAg64KgIOq1rOqwhCgw7Iucfjjsi5zsspjrn7wpXG4gKiDsnLzroZwg7J2964qU64ukLiDslbzqsIQg6re866y07LKY65+8IOuCruyXkCDsobDsmqntlZjqs6Ag7Iu27J2AIO2MgOuPhCDsnojslrQg65Kk7KeR7Z6MIOqwkuydhCDrp4nsp4Ag7JWK64qU64ukLlxuICovXG5leHBvcnQgZnVuY3Rpb24gaXNRdWlldEhvdXJzKGF0OiBEYXRlLCByZWFkU2V0dGluZzogUmVhZFNldHRpbmcpOiBib29sZWFuIHtcbiAgY29uc3QgeyBob3VyLCB3ZWVrZGF5IH0gPSBzZW91bFBhcnRzKGF0KTtcblxuICBjb25zdCBpc1dlZWtlbmQgPSB3ZWVrZGF5ID09PSBcIlNhdFwiIHx8IHdlZWtkYXkgPT09IFwiU3VuXCI7XG4gIGlmIChpc1dlZWtlbmQgJiYgcmVhZFNldHRpbmcoXCJzbGFjay5ub3RpZnlPbldlZWtlbmRzXCIsIFwiU0xBQ0tfTk9USUZZX09OX1dFRUtFTkRTXCIpICE9PSBcInRydWVcIikge1xuICAgIHJldHVybiB0cnVlO1xuICB9XG5cbiAgY29uc3QgZnJvbSA9IGJvdW5kZWRIb3VyKFxuICAgIHJlYWRTZXR0aW5nKFwic2xhY2sucXVpZXRGcm9tSG91clwiLCBcIlNMQUNLX1FVSUVUX0ZST01fSE9VUlwiKSxcbiAgICBERUZBVUxUX1FVSUVUX0ZST01fSE9VUixcbiAgKTtcbiAgY29uc3QgdW50aWwgPSBib3VuZGVkSG91cihcbiAgICByZWFkU2V0dGluZyhcInNsYWNrLnF1aWV0VW50aWxIb3VyXCIsIFwiU0xBQ0tfUVVJRVRfVU5USUxfSE9VUlwiKSxcbiAgICBERUZBVUxUX1FVSUVUX1VOVElMX0hPVVIsXG4gICk7XG5cbiAgLy8g7Iuc7J6R6rO8IOyiheujjOqwgCDqsJnsnLzrqbQg6rWs6rCE7J20IOu5hOyWtCDsnojri6TripQg65y77Jy866GcIOydveyWtCDsobDsmqkg7Iuc6rCE7J2EIOuRkOyngCDslYrripTri6QuXG4gIGlmIChmcm9tID09PSB1bnRpbCkgcmV0dXJuIGZhbHNlO1xuICByZXR1cm4gZnJvbSA+IHVudGlsID8gaG91ciA+PSBmcm9tIHx8IGhvdXIgPCB1bnRpbCA6IGhvdXIgPj0gZnJvbSAmJiBob3VyIDwgdW50aWw7XG59XG4iXSwibWFwcGluZ3MiOiI7O0FBdUJBLE1BQWEsMEJBQTBCO0FBQ3ZDLE1BQWEsMkJBQTJCOzs7OztBQU14QyxNQUFNLGtCQUFrQixJQUFJLEtBQUssZUFBZSxTQUFTO0NBQ3ZELFVBQVU7Q0FDVixNQUFNO0NBQ04sUUFBUTtDQUNSLFNBQVM7QUFDWCxDQUFDO0FBRUQsU0FBUyxXQUFXLElBQTZDO0NBQy9ELE1BQU0sUUFBUSxnQkFBZ0IsY0FBYyxFQUFFO0NBQzlDLE1BQU0sT0FBTyxPQUFPLE1BQU0sTUFBTSxNQUFNLEVBQUUsU0FBUyxNQUFNLENBQUMsRUFBRSxTQUFTLEdBQUc7Q0FDdEUsTUFBTSxVQUFVLE1BQU0sTUFBTSxNQUFNLEVBQUUsU0FBUyxTQUFTLENBQUMsRUFBRSxTQUFTO0NBRWxFLE9BQU87RUFBRSxNQUFNLE9BQU87RUFBSTtDQUFRO0FBQ3BDOztBQUdBLFNBQVMsWUFBWSxLQUF5QixVQUEwQjtDQUN0RSxNQUFNLFNBQVMsT0FBTyxHQUFHO0NBQ3pCLElBQUksQ0FBQyxPQUFPLFVBQVUsTUFBTSxLQUFLLFNBQVMsS0FBSyxTQUFTLElBQUksT0FBTztDQUNuRSxPQUFPO0FBQ1Q7Ozs7Ozs7QUFRQSxTQUFnQixhQUFhLElBQVUsYUFBbUM7Q0FDeEUsTUFBTSxFQUFFLE1BQU0sWUFBWSxXQUFXLEVBQUU7Q0FHdkMsS0FEa0IsWUFBWSxTQUFTLFlBQVksVUFDbEMsWUFBWSwwQkFBMEIsMEJBQTBCLE1BQU0sUUFDckYsT0FBTztDQUdULE1BQU0sT0FBTyxZQUNYLFlBQVksdUJBQXVCLHVCQUF1QixHQUFBLEVBRTVEO0NBQ0EsTUFBTSxRQUFRLFlBQ1osWUFBWSx3QkFBd0Isd0JBQXdCLEdBQUEsQ0FFOUQ7Q0FHQSxJQUFJLFNBQVMsT0FBTyxPQUFPO0NBQzNCLE9BQU8sT0FBTyxRQUFRLFFBQVEsUUFBUSxPQUFPLFFBQVEsUUFBUSxRQUFRLE9BQU87QUFDOUUifQ==
|
|
@@ -25,7 +25,11 @@ async function notifySlack(notification, readSetting = getSetting) {
|
|
|
25
25
|
logger.debug(`slack not configured, skipping notification: ${title} ${subject ?? ""}`);
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
|
-
if (!urgent &&
|
|
28
|
+
if (!urgent && readSetting("slack.enabled", "SLACK_ENABLED") === "false") {
|
|
29
|
+
logger.debug(`slack disabled, skipping notification: ${title} ${subject ?? ""}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!urgent && isQuietHours(now ?? /* @__PURE__ */ new Date(), readSetting)) {
|
|
29
33
|
logger.debug(`quiet hours, skipping notification: ${title} ${subject ?? ""}`);
|
|
30
34
|
return;
|
|
31
35
|
}
|
|
@@ -69,4 +73,4 @@ async function notifySlack(notification, readSetting = getSetting) {
|
|
|
69
73
|
//#endregion
|
|
70
74
|
export { SLACK_COLOR, notifySlack };
|
|
71
75
|
|
|
72
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhY2stbm90aWZ5LmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9zbGFjay1ub3RpZnkudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBTbGFjayDslYzrprwgKGNoYXQucG9zdE1lc3NhZ2UpLlxuICpcbiAqIGZhaWwtb3BlbjogZW52IOuvuOyEpOygleydtOuptCDsobDsmqntnoggbm8tb3Ag7J206rOgLCDsoITshqEg7Iuk7Yyo64qUIHdhcm4g66Gc6re466Gc66eMIOuCqOq4tOuLpC5cbiAqIOyhsOyaqSDsi5zqsIQo7KO866eQwrcyMH447IucKeyXkOuKlCBgdXJnZW50YCDqsIAg7JWE64uMIOyVjOumvOydhCDrs7TrgrTsp4Ag7JWK64qU64ukLlxuICog7JWM66a8IOqyveuhnOqwgCDthqDtgbAg65287Jqw7YyF7J2064KYIOyYpOulmCDsoITtjIzrpbwg7KeA7JewwrfssKjri6jtlbTshJzripQg7JWIIOuQnOuLpC5cbiAqL1xuaW1wb3J0IHsgZ2V0TG9nZ2VyIH0gZnJvbSBcIkBsb2d0YXBlL2xvZ3RhcGVcIjtcblxuaW1wb3J0IHsgZ2V0U2V0dGluZyB9IGZyb20gXCIuLi9hcHBsaWNhdGlvbi9zZXR0aW5nL3NldHRpbmcuc3RvcmVcIjtcbmltcG9ydCB7IGlzUXVpZXRIb3VycyB9IGZyb20gXCIuL3F1aWV0LWhvdXJzXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGdldExvZ2dlcihbXCJxZ3JpZFwiLCBcInNsYWNrXCJdKTtcblxuY29uc3QgU0xBQ0tfUE9TVF9NRVNTQUdFX1VSTCA9IFwiaHR0cHM6Ly9zbGFjay5jb20vYXBpL2NoYXQucG9zdE1lc3NhZ2VcIjtcbmNvbnN0IFNMQUNLX1RJTUVPVVRfTVMgPSA1XzAwMDtcblxuLyoqIOyDge2DnOulvCDsg4nsnLzroZwg66i87KCAIOydve2eiOqyjCDtlZjripQgYXR0YWNobWVudCDsg4nsg4Eg67CULiAqL1xuZXhwb3J0IGNvbnN0IFNMQUNLX0NPTE9SID0ge1xuICBnb29kOiBcIiMyZWI4ODZcIixcbiAgYmFkOiBcIiNlMDFlNWFcIixcbn0gYXMgY29uc3Q7XG5cbmV4cG9ydCB0eXBlIFNsYWNrTm90aWZpY2F0aW9uID0ge1xuICAvKiog7ZWcIOykhCDsoJzrqqkuIOustOyKqCDsnbzsnbQg7J287Ja064Ks64qU7KeA66eMIOuLtOuKlOuLpC4gKi9cbiAgdGl0bGU6IHN0cmluZztcbiAgLyoqIOygnOuqqSDsmIYg7L2U65OcIOyKpO2DgOydvOuhnCDrtpnripQg64yA7IOBIOyLneuzhOyekC4gKi9cbiAgc3ViamVjdD86IHN0cmluZztcbiAgLyoqIOygnOuqqSDslYTrnpgg7J6R7J2AIOq4gOyUqOuhnCDrgrTroKTqsIDripQg67aA6rCAIOygleuztCDigJQg7J297KeAIOyViuyVhOuPhCDrkJjripQg6rKD65OkLiAqL1xuICBjb250ZXh0Pzogc3RyaW5nO1xuICBjb2xvcj86ICh0eXBlb2YgU0xBQ0tfQ09MT1IpW2tleW9mIHR5cGVvZiBTTEFDS19DT0xPUl07XG4gIC8qKlxuICAgKiDsobDsmqkg7Iuc6rCEKOyjvOunkMK3MjB+OOyLnCnsl5Drj4Qg67O064K464ukLiDshJzruYTsiqTqsIAg66mI7LaYIOyDge2DnOyymOufvCDsp4DquIgg7JWM7KeAIOyViuycvOuptCDshpDtlbTqsIBcbiAgICog7Luk7KeA64qUIOyCrOqxtOyXkOunjCDsk7Tri6Qg4oCUIOuCqOyaqe2VmOuptCDsobDsmqkg7Iuc6rCEIOyekOyytOqwgCDrrLTsnZjrr7jtlbTsp4Tri6QuXG4gICAqL1xuICB1cmdlbnQ/
|
|
76
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhY2stbm90aWZ5LmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9zbGFjay1ub3RpZnkudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBTbGFjayDslYzrprwgKGNoYXQucG9zdE1lc3NhZ2UpLlxuICpcbiAqIGZhaWwtb3BlbjogZW52IOuvuOyEpOygleydtOuptCDsobDsmqntnoggbm8tb3Ag7J206rOgLCDsoITshqEg7Iuk7Yyo64qUIHdhcm4g66Gc6re466Gc66eMIOuCqOq4tOuLpC5cbiAqIOyhsOyaqSDsi5zqsIQo7KO866eQwrcyMH447IucKeyXkOuKlCBgdXJnZW50YCDqsIAg7JWE64uMIOyVjOumvOydhCDrs7TrgrTsp4Ag7JWK64qU64ukLlxuICog7JWM66a8IOqyveuhnOqwgCDthqDtgbAg65287Jqw7YyF7J2064KYIOyYpOulmCDsoITtjIzrpbwg7KeA7JewwrfssKjri6jtlbTshJzripQg7JWIIOuQnOuLpC5cbiAqL1xuaW1wb3J0IHsgZ2V0TG9nZ2VyIH0gZnJvbSBcIkBsb2d0YXBlL2xvZ3RhcGVcIjtcblxuaW1wb3J0IHsgZ2V0U2V0dGluZyB9IGZyb20gXCIuLi9hcHBsaWNhdGlvbi9zZXR0aW5nL3NldHRpbmcuc3RvcmVcIjtcbmltcG9ydCB7IGlzUXVpZXRIb3VycyB9IGZyb20gXCIuL3F1aWV0LWhvdXJzXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGdldExvZ2dlcihbXCJxZ3JpZFwiLCBcInNsYWNrXCJdKTtcblxuY29uc3QgU0xBQ0tfUE9TVF9NRVNTQUdFX1VSTCA9IFwiaHR0cHM6Ly9zbGFjay5jb20vYXBpL2NoYXQucG9zdE1lc3NhZ2VcIjtcbmNvbnN0IFNMQUNLX1RJTUVPVVRfTVMgPSA1XzAwMDtcblxuLyoqIOyDge2DnOulvCDsg4nsnLzroZwg66i87KCAIOydve2eiOqyjCDtlZjripQgYXR0YWNobWVudCDsg4nsg4Eg67CULiAqL1xuZXhwb3J0IGNvbnN0IFNMQUNLX0NPTE9SID0ge1xuICBnb29kOiBcIiMyZWI4ODZcIixcbiAgYmFkOiBcIiNlMDFlNWFcIixcbn0gYXMgY29uc3Q7XG5cbmV4cG9ydCB0eXBlIFNsYWNrTm90aWZpY2F0aW9uID0ge1xuICAvKiog7ZWcIOykhCDsoJzrqqkuIOustOyKqCDsnbzsnbQg7J287Ja064Ks64qU7KeA66eMIOuLtOuKlOuLpC4gKi9cbiAgdGl0bGU6IHN0cmluZztcbiAgLyoqIOygnOuqqSDsmIYg7L2U65OcIOyKpO2DgOydvOuhnCDrtpnripQg64yA7IOBIOyLneuzhOyekC4gKi9cbiAgc3ViamVjdD86IHN0cmluZztcbiAgLyoqIOygnOuqqSDslYTrnpgg7J6R7J2AIOq4gOyUqOuhnCDrgrTroKTqsIDripQg67aA6rCAIOygleuztCDigJQg7J297KeAIOyViuyVhOuPhCDrkJjripQg6rKD65OkLiAqL1xuICBjb250ZXh0Pzogc3RyaW5nO1xuICBjb2xvcj86ICh0eXBlb2YgU0xBQ0tfQ09MT1IpW2tleW9mIHR5cGVvZiBTTEFDS19DT0xPUl07XG4gIC8qKlxuICAgKiDsobDsmqkg7Iuc6rCEKOyjvOunkMK3MjB+OOyLnCnsl5Drj4Qg67O064K464ukLiDshJzruYTsiqTqsIAg66mI7LaYIOyDge2DnOyymOufvCDsp4DquIgg7JWM7KeAIOyViuycvOuptCDshpDtlbTqsIBcbiAgICog7Luk7KeA64qUIOyCrOqxtOyXkOunjCDsk7Tri6Qg4oCUIOuCqOyaqe2VmOuptCDsobDsmqkg7Iuc6rCEIOyekOyytOqwgCDrrLTsnZjrr7jtlbTsp4Tri6QuXG4gICAqL1xuICB1cmdlbnQ/OiBib29sZWFuO1xuICAvKiog7KGw7JqpIOyLnOqwhCDtjJDsoJUg6riw7KSAIOyLnOqwgS4g7YWM7Iqk7Yq47JeQ7IScIOyLnOqzhOulvCDqs6DsoJXtlaAg65WM66eMIOuEmOq4tOuLpC4gKi9cbiAgbm93PzogRGF0ZTtcbn07XG5cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBub3RpZnlTbGFjayhcbiAgbm90aWZpY2F0aW9uOiBTbGFja05vdGlmaWNhdGlvbixcbiAgcmVhZFNldHRpbmc6IHR5cGVvZiBnZXRTZXR0aW5nID0gZ2V0U2V0dGluZyxcbik6IFByb21pc2U8dm9pZD4ge1xuICBjb25zdCBib3RUb2tlbiA9IHJlYWRTZXR0aW5nKFwic2xhY2suYm90VG9rZW5cIiwgXCJTTEFDS19CT1RfVE9LRU5cIik7XG4gIGNvbnN0IGNoYW5uZWwgPSByZWFkU2V0dGluZyhcInNsYWNrLmNoYW5uZWxJZFwiLCBcIlNMQUNLX0NIQU5ORUxfSURcIik7XG4gIGNvbnN0IHsgdGl0bGUsIHN1YmplY3QsIGNvbnRleHQsIGNvbG9yLCB1cmdlbnQsIG5vdyB9ID0gbm90aWZpY2F0aW9uO1xuICBpZiAoIWJvdFRva2VuIHx8ICFjaGFubmVsKSB7XG4gICAgbG9nZ2VyLmRlYnVnKGBzbGFjayBub3QgY29uZmlndXJlZCwgc2tpcHBpbmcgbm90aWZpY2F0aW9uOiAke3RpdGxlfSAke3N1YmplY3QgPz8gXCJcIn1gKTtcbiAgICByZXR1cm47XG4gIH1cblxuICAvLyDsl7DtnLTsspjrn7wg6rec7LmZ7Jy866GcIOyeoeydhCDsiJgg7JeG64qUIOq4sOqwhOyXkCDqtIDrpqzsnpDqsIAg64K066as64qUIOyKpOychOy5mC4gdXJnZW50IOuKlCDthrXqs7zsi5ztgqjri6Qg4oCUXG4gIC8vIOuBiCDsg4Htg5zroZwgcHJvdmlkZXIg6rCAIOyghOu2gCDso73snLzrqbQg7Jew7Zy0IOuCtOuCtCDshJzruYTsiqTqsIAg66mI7LaYIOykhCDslYTrrLTrj4Qg66qo66W464ukLlxuICBpZiAoIXVyZ2VudCAmJiByZWFkU2V0dGluZyhcInNsYWNrLmVuYWJsZWRcIiwgXCJTTEFDS19FTkFCTEVEXCIpID09PSBcImZhbHNlXCIpIHtcbiAgICBsb2dnZXIuZGVidWcoYHNsYWNrIGRpc2FibGVkLCBza2lwcGluZyBub3RpZmljYXRpb246ICR7dGl0bGV9ICR7c3ViamVjdCA/PyBcIlwifWApO1xuICAgIHJldHVybjtcbiAgfVxuXG4gIC8vIOunjOujjCDslYzrprzsnYAg7KO86riw7KCB7Jy866GcIOuwmOuzteuQmOuvgOuhnCDsl6zquLDshJwg67KE66Ck64+EIOuLpOydjCDqt7zrrLQg7Iuc6rCEIOyyqyDso7zquLDsl5Ag64uk7IucIOyYqOuLpC5cbiAgaWYgKCF1cmdlbnQgJiYgaXNRdWlldEhvdXJzKG5vdyA/PyBuZXcgRGF0ZSgpLCByZWFkU2V0dGluZykpIHtcbiAgICBsb2dnZXIuZGVidWcoYHF1aWV0IGhvdXJzLCBza2lwcGluZyBub3RpZmljYXRpb246ICR7dGl0bGV9ICR7c3ViamVjdCA/PyBcIlwifWApO1xuICAgIHJldHVybjtcbiAgfVxuXG4gIGNvbnN0IGJsb2NrczogdW5rbm93bltdID0gW1xuICAgIHtcbiAgICAgIHR5cGU6IFwic2VjdGlvblwiLFxuICAgICAgdGV4dDogeyB0eXBlOiBcIm1ya2R3blwiLCB0ZXh0OiBzdWJqZWN0ID8gYCoke3RpdGxlfSogIFxcYCR7c3ViamVjdH1cXGBgIDogYCoke3RpdGxlfSpgIH0sXG4gICAgfSxcbiAgXTtcbiAgaWYgKGNvbnRleHQpIHtcbiAgICBibG9ja3MucHVzaCh7IHR5cGU6IFwiY29udGV4dFwiLCBlbGVtZW50czogW3sgdHlwZTogXCJtcmtkd25cIiwgdGV4dDogY29udGV4dCB9XSB9KTtcbiAgfVxuXG4gIHRyeSB7XG4gICAgY29uc3QgcmVzID0gYXdhaXQgZmV0Y2goU0xBQ0tfUE9TVF9NRVNTQUdFX1VSTCwge1xuICAgICAgbWV0aG9kOiBcIlBPU1RcIixcbiAgICAgIGhlYWRlcnM6IHtcbiAgICAgICAgXCJDb250ZW50LVR5cGVcIjogXCJhcHBsaWNhdGlvbi9qc29uOyBjaGFyc2V0PXV0Zi04XCIsXG4gICAgICAgIEF1dGhvcml6YXRpb246IGBCZWFyZXIgJHtib3RUb2tlbn1gLFxuICAgICAgfSxcbiAgICAgIGJvZHk6IEpTT04uc3RyaW5naWZ5KHtcbiAgICAgICAgY2hhbm5lbCxcbiAgICAgICAgLy8gZmFsbGJhY2sg7J2AIGF0dGFjaG1lbnQg7JWI7JeQIOuRlOuLpC4g7LWc7IOB7JyEIHRleHQg66GcIOuztOuCtOuptCBibG9ja3Mg6rCAIOygleyDgSDroIzrjZTrkJjripRcbiAgICAgICAgLy8g7ZmU66m07JeQ7ISc64+EIO2VqOq7mCDstpzroKXrj7wg7KCc66qp7J20IOuRkCDrsogg67O07J2464ukLiBhdHRhY2htZW50IGZhbGxiYWNrIOydgCBibG9ja3Mg66W8XG4gICAgICAgIC8vIOuquyDqt7jrpqzripQg7YG065287J207Ja47Yq47JmAIO2RuOyLnCDslYzrprzsl5DshJzrp4wg7JOw7J2464ukLlxuICAgICAgICBhdHRhY2htZW50czogW1xuICAgICAgICAgIHtcbiAgICAgICAgICAgIGZhbGxiYWNrOiBzdWJqZWN0ID8gYCR7dGl0bGV9IOKAlCAke3N1YmplY3R9YCA6IHRpdGxlLFxuICAgICAgICAgICAgLi4uKGNvbG9yID8geyBjb2xvciB9IDoge30pLFxuICAgICAgICAgICAgYmxvY2tzLFxuICAgICAgICAgIH0sXG4gICAgICAgIF0sXG4gICAgICB9KSxcbiAgICAgIHNpZ25hbDogQWJvcnRTaWduYWwudGltZW91dChTTEFDS19USU1FT1VUX01TKSxcbiAgICB9KTtcblxuICAgIC8vIGJvdCDrr7jstIjrjIAobm90X2luX2NoYW5uZWwpwrfsnpjrqrvrkJwg7LGE64SQIOuTsSDqsIDsnqUg7Z2U7ZWcIOyYpOyEpOygleydgCBIVFRQIDIwMCArIG9rOmZhbHNlIOuhnCDsmKjri6QuXG4gICAgLy8gc3RhdHVzIOunjCDrs7TrqbQg7KGw7Jqp7Z6IIOyEseqztSDsspjrpqzrj7wg7JWM66a87J20IO2GteynuOuhnCDsgqzrnbzsp4Tri6QuXG4gICAgY29uc3QgYm9keSA9IChhd2FpdCByZXMuanNvbigpLmNhdGNoKCgpID0+IG51bGwpKSBhcyB7IG9rPzogYm9vbGVhbjsgZXJyb3I/OiBzdHJpbmcgfSB8IG51bGw7XG4gICAgaWYgKCFib2R5Py5vaykge1xuICAgICAgbG9nZ2VyLndhcm4oYHNsYWNrIG5vdGlmaWNhdGlvbiByZWplY3RlZDogJHtib2R5Py5lcnJvciA/PyBgaHR0cCAke3Jlcy5zdGF0dXN9YH1gKTtcbiAgICB9XG4gIH0gY2F0Y2ggKGUpIHtcbiAgICBsb2dnZXIud2Fybihgc2xhY2sgbm90aWZpY2F0aW9uIGZhaWxlZDogJHsoZSBhcyBFcnJvcikubWVzc2FnZX1gKTtcbiAgfVxufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7OztBQVlBLE1BQU0sU0FBUyxVQUFVLENBQUMsU0FBUyxPQUFPLENBQUM7QUFFM0MsTUFBTSx5QkFBeUI7QUFDL0IsTUFBTSxtQkFBbUI7O0FBR3pCLE1BQWEsY0FBYztDQUN6QixNQUFNO0NBQ04sS0FBSztBQUNQO0FBbUJBLGVBQXNCLFlBQ3BCLGNBQ0EsY0FBaUMsWUFDbEI7Q0FDZixNQUFNLFdBQVcsWUFBWSxrQkFBa0IsaUJBQWlCO0NBQ2hFLE1BQU0sVUFBVSxZQUFZLG1CQUFtQixrQkFBa0I7Q0FDakUsTUFBTSxFQUFFLE9BQU8sU0FBUyxTQUFTLE9BQU8sUUFBUSxRQUFRO0NBQ3hELElBQUksQ0FBQyxZQUFZLENBQUMsU0FBUztFQUN6QixPQUFPLE1BQU0sZ0RBQWdELE1BQU0sR0FBRyxXQUFXLElBQUk7RUFDckY7Q0FDRjtDQUlBLElBQUksQ0FBQyxVQUFVLFlBQVksaUJBQWlCLGVBQWUsTUFBTSxTQUFTO0VBQ3hFLE9BQU8sTUFBTSwwQ0FBMEMsTUFBTSxHQUFHLFdBQVcsSUFBSTtFQUMvRTtDQUNGO0NBR0EsSUFBSSxDQUFDLFVBQVUsYUFBYSx1QkFBTyxJQUFJLEtBQUssR0FBRyxXQUFXLEdBQUc7RUFDM0QsT0FBTyxNQUFNLHVDQUF1QyxNQUFNLEdBQUcsV0FBVyxJQUFJO0VBQzVFO0NBQ0Y7Q0FFQSxNQUFNLFNBQW9CLENBQ3hCO0VBQ0UsTUFBTTtFQUNOLE1BQU07R0FBRSxNQUFNO0dBQVUsTUFBTSxVQUFVLElBQUksTUFBTSxPQUFPLFFBQVEsTUFBTSxJQUFJLE1BQU07RUFBRztDQUN0RixDQUNGO0NBQ0EsSUFBSSxTQUNGLE9BQU8sS0FBSztFQUFFLE1BQU07RUFBVyxVQUFVLENBQUM7R0FBRSxNQUFNO0dBQVUsTUFBTTtFQUFRLENBQUM7Q0FBRSxDQUFDO0NBR2hGLElBQUk7RUFDRixNQUFNLE1BQU0sTUFBTSxNQUFNLHdCQUF3QjtHQUM5QyxRQUFRO0dBQ1IsU0FBUztJQUNQLGdCQUFnQjtJQUNoQixlQUFlLFVBQVU7R0FDM0I7R0FDQSxNQUFNLEtBQUssVUFBVTtJQUNuQjtJQUlBLGFBQWEsQ0FDWDtLQUNFLFVBQVUsVUFBVSxHQUFHLE1BQU0sS0FBSyxZQUFZO0tBQzlDLEdBQUksUUFBUSxFQUFFLE1BQU0sSUFBSSxDQUFDO0tBQ3pCO0lBQ0YsQ0FDRjtHQUNGLENBQUM7R0FDRCxRQUFRLFlBQVksUUFBUSxnQkFBZ0I7RUFDOUMsQ0FBQztFQUlELE1BQU0sT0FBUSxNQUFNLElBQUksS0FBSyxDQUFDLENBQUMsWUFBWSxJQUFJO0VBQy9DLElBQUksQ0FBQyxNQUFNLElBQ1QsT0FBTyxLQUFLLGdDQUFnQyxNQUFNLFNBQVMsUUFBUSxJQUFJLFVBQVU7Q0FFckYsU0FBUyxHQUFHO0VBQ1YsT0FBTyxLQUFLLDhCQUErQixFQUFZLFNBQVM7Q0FDbEU7QUFDRiJ9
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
buildReminderContext,
|
|
11
11
|
type ExpiredTokenReminderDeps,
|
|
12
12
|
rescheduleExpiredTokenReminder,
|
|
13
|
+
sendExpiredTokenReminderNow,
|
|
13
14
|
startExpiredTokenReminder,
|
|
14
15
|
stopExpiredTokenReminder,
|
|
15
16
|
} from "./expired-token-reminder";
|
|
@@ -88,12 +89,14 @@ describe("buildReminderContext", () => {
|
|
|
88
89
|
describe("expired token reminder scheduling", () => {
|
|
89
90
|
let minutes = "10";
|
|
90
91
|
let rawUserMap = "yds:U-OLD";
|
|
92
|
+
let remindersEnabled = "true";
|
|
91
93
|
let deps: ExpiredTokenReminderDeps;
|
|
92
94
|
|
|
93
95
|
beforeEach(() => {
|
|
94
96
|
vi.useFakeTimers();
|
|
95
97
|
minutes = "10";
|
|
96
98
|
rawUserMap = "yds:U-OLD";
|
|
99
|
+
remindersEnabled = "true";
|
|
97
100
|
findInactiveMock.mockReset();
|
|
98
101
|
getSettingMock.mockReset();
|
|
99
102
|
notifySlackMock.mockReset();
|
|
@@ -101,6 +104,7 @@ describe("expired token reminder scheduling", () => {
|
|
|
101
104
|
getSettingMock.mockImplementation((key: string) => {
|
|
102
105
|
if (key === "slack.expiryReminderMinutes") return minutes;
|
|
103
106
|
if (key === "slack.userMap") return rawUserMap;
|
|
107
|
+
if (key === "slack.remindersEnabled") return remindersEnabled;
|
|
104
108
|
return undefined;
|
|
105
109
|
});
|
|
106
110
|
notifySlackMock.mockResolvedValue(undefined);
|
|
@@ -124,6 +128,45 @@ describe("expired token reminder scheduling", () => {
|
|
|
124
128
|
expect(notifySlackMock).toHaveBeenCalledTimes(1);
|
|
125
129
|
});
|
|
126
130
|
|
|
131
|
+
it("반복을 끄면 부팅해도 알리지 않는다", async () => {
|
|
132
|
+
remindersEnabled = "false";
|
|
133
|
+
|
|
134
|
+
startExpiredTokenReminder(deps);
|
|
135
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
136
|
+
await vi.advanceTimersByTimeAsync(10 * 60_000);
|
|
137
|
+
|
|
138
|
+
expect(notifySlackMock).not.toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("반복을 꺼도 주기 값은 남아 다시 켜면 그대로 쓴다", async () => {
|
|
142
|
+
remindersEnabled = "false";
|
|
143
|
+
rescheduleExpiredTokenReminder(deps);
|
|
144
|
+
|
|
145
|
+
remindersEnabled = "true";
|
|
146
|
+
rescheduleExpiredTokenReminder(deps);
|
|
147
|
+
await vi.advanceTimersByTimeAsync(10 * 60_000);
|
|
148
|
+
|
|
149
|
+
// 저장된 10분 주기가 그대로 살아 있다.
|
|
150
|
+
expect(notifySlackMock).toHaveBeenCalledTimes(1);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("수동 트리거는 조용 시간을 통과하도록 urgent 로 보낸다", async () => {
|
|
154
|
+
// 사람이 직접 누른 것이라 밤이라고 삼키면 버튼이 고장 난 것으로 읽힌다.
|
|
155
|
+
const sent = await sendExpiredTokenReminderNow(deps);
|
|
156
|
+
|
|
157
|
+
expect(sent).toBe(1);
|
|
158
|
+
expect(notifySlackMock).toHaveBeenCalledWith(expect.objectContaining({ urgent: true }));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("보낼 대상이 없으면 0을 돌려주고 아무것도 보내지 않는다", async () => {
|
|
162
|
+
findInactiveMock.mockResolvedValue([]);
|
|
163
|
+
|
|
164
|
+
const sent = await sendExpiredTokenReminderNow(deps);
|
|
165
|
+
|
|
166
|
+
expect(sent).toBe(0);
|
|
167
|
+
expect(notifySlackMock).not.toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
|
|
127
170
|
it("start를 다시 호출해도 interval은 하나만 남는다", async () => {
|
|
128
171
|
startExpiredTokenReminder(deps);
|
|
129
172
|
startExpiredTokenReminder(deps);
|
|
@@ -53,9 +53,9 @@ export function buildReminderContext(
|
|
|
53
53
|
return [...lines, "재로그인이 필요합니다"].filter(Boolean).join("\n");
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
async function sendReminder(deps: ExpiredTokenReminderDeps): Promise<
|
|
56
|
+
async function sendReminder(deps: ExpiredTokenReminderDeps, urgent = false): Promise<number> {
|
|
57
57
|
const inactive = await deps.findInactive();
|
|
58
|
-
if (inactive.length === 0) return;
|
|
58
|
+
if (inactive.length === 0) return 0;
|
|
59
59
|
|
|
60
60
|
const userMap = deps.getSlackUserMap();
|
|
61
61
|
|
|
@@ -65,7 +65,21 @@ async function sendReminder(deps: ExpiredTokenReminderDeps): Promise<void> {
|
|
|
65
65
|
subject: inactive.length > 1 ? `${inactive.length}건` : (inactive[0]!.name ?? "unnamed"),
|
|
66
66
|
context: buildReminderContext(inactive, userMap),
|
|
67
67
|
color: SLACK_COLOR.bad,
|
|
68
|
+
urgent,
|
|
68
69
|
});
|
|
70
|
+
return inactive.length;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 화면의 "지금 보내기". 사람이 직접 누른 것이므로 조용 시간·마스터 스위치를 통과시킨다 —
|
|
75
|
+
* 눌렀는데 아무 일도 안 일어나면 버튼이 고장 난 것으로 읽힌다.
|
|
76
|
+
*
|
|
77
|
+
* 보낸 건수를 돌려줘 호출부가 "보낼 대상 없음"과 "보냄"을 구분해 보여준다.
|
|
78
|
+
*/
|
|
79
|
+
export function sendExpiredTokenReminderNow(
|
|
80
|
+
deps: ExpiredTokenReminderDeps = defaultDeps,
|
|
81
|
+
): Promise<number> {
|
|
82
|
+
return sendReminder(deps, true);
|
|
69
83
|
}
|
|
70
84
|
|
|
71
85
|
let timer: ReturnType<typeof setInterval> | null = null;
|
|
@@ -73,6 +87,12 @@ let timer: ReturnType<typeof setInterval> | null = null;
|
|
|
73
87
|
function scheduleExpiredTokenReminder(runNow: boolean, deps: ExpiredTokenReminderDeps): void {
|
|
74
88
|
stopExpiredTokenReminder();
|
|
75
89
|
|
|
90
|
+
// 주기 값과 별개인 스위치다. 꺼도 주기는 남아 있어 다시 켤 때 고르던 값이 그대로다.
|
|
91
|
+
if (deps.readSetting("slack.remindersEnabled", "SLACK_REMINDERS_ENABLED") === "false") {
|
|
92
|
+
logger.info("expired token reminder disabled");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
76
96
|
const minutes = Number(
|
|
77
97
|
deps.readSetting("slack.expiryReminderMinutes", "SLACK_EXPIRY_REMINDER_INTERVAL_MINUTES") ?? 0,
|
|
78
98
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { findSettingDef, maskSecret, SETTING_DEFS, validateSettingValue } from "./setting.
|
|
3
|
+
import { findSettingDef, maskSecret, SETTING_DEFS, validateSettingValue } from "./setting.constant";
|
|
4
4
|
|
|
5
5
|
describe("validateSettingValue", () => {
|
|
6
6
|
const intDef = findSettingDef("openai.maxWorkersPerToken")!;
|
|
@@ -37,6 +37,35 @@ describe("validateSettingValue", () => {
|
|
|
37
37
|
});
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
describe("preset 검증", () => {
|
|
41
|
+
const presetDef = findSettingDef("slack.expiryReminderMinutes")!;
|
|
42
|
+
|
|
43
|
+
it("목록에 있는 값만 받는다", () => {
|
|
44
|
+
expect(validateSettingValue(presetDef, "30")).toMatchObject({ ok: true, value: "30" });
|
|
45
|
+
expect(validateSettingValue(presetDef, "180")).toMatchObject({ ok: true });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("목록 밖의 값은 거부한다", () => {
|
|
49
|
+
// 저장되면 화면에서 아무 버튼도 선택돼 보이지 않아 "왜 안 바뀌지"가 된다.
|
|
50
|
+
expect(validateSettingValue(presetDef, "45")).toMatchObject({ ok: false });
|
|
51
|
+
expect(validateSettingValue(presetDef, "0")).toMatchObject({ ok: false });
|
|
52
|
+
expect(validateSettingValue(presetDef, "많이")).toMatchObject({ ok: false });
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("조용 시간 설정", () => {
|
|
57
|
+
// fallback("20"/"8")은 quiet-hours.ts 의 DEFAULT_QUIET_* 와 같아야 하지만 테스트로 묶지
|
|
58
|
+
// 않는다 — 두 모듈을 한 파일에서 import 하면 setting.store → token.model 이 딸려와,
|
|
59
|
+
// token.model 을 mock 하는 테스트가 같은 워커에 배치될 때 실제 모듈을 먼저 로드해 버린다.
|
|
60
|
+
it("0..23 을 벗어나면 거부한다", () => {
|
|
61
|
+
const fromDef = findSettingDef("slack.quietFromHour")!;
|
|
62
|
+
expect(validateSettingValue(fromDef, "24")).toMatchObject({ ok: false });
|
|
63
|
+
expect(validateSettingValue(fromDef, "-1")).toMatchObject({ ok: false });
|
|
64
|
+
expect(validateSettingValue(fromDef, "0")).toMatchObject({ ok: true });
|
|
65
|
+
expect(validateSettingValue(fromDef, "23")).toMatchObject({ ok: true });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
40
69
|
describe("maskSecret", () => {
|
|
41
70
|
it("긴 값은 앞뒤만 남긴다", () => {
|
|
42
71
|
expect(maskSecret("xoxb-1234567890-abcdefghij")).toBe("xoxb-123...ghij");
|
|
@@ -3,12 +3,21 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 키·env 이름·타입·검증을 한 곳에 모아 API 와 UI 가 같은 정의를 본다. 여기 없는 env 는
|
|
5
5
|
* 화면에서 바꿀 수 없다 — DB 접속 정보나 포트처럼 서버가 뜨기 전에 필요한 값들이다.
|
|
6
|
+
*
|
|
7
|
+
* `settings` 테이블은 `(key, value)` 문자열 쌍만 담는 그릇이라(`setting.entity.json`),
|
|
8
|
+
* "이 키가 정수인지·범위가 얼마인지·화면에 뭐라고 쓸지"를 담을 곳이 따로 필요하다.
|
|
9
|
+
* 그 메타데이터가 여기 있다 — DB 스키마가 아니라 앱이 아는 상수 목록이다.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
|
-
export type SettingKind = "boolean" | "integer" | "number" | "string" | "secret";
|
|
12
|
+
export type SettingKind = "boolean" | "integer" | "number" | "string" | "secret" | "preset";
|
|
9
13
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
14
|
+
/**
|
|
15
|
+
* 화면에서 묶어 보여줄 단위. 평평하게 나열하면 어디를 봐야 할지 알 수 없다.
|
|
16
|
+
*
|
|
17
|
+
* Slack 은 "언제 보낼까"(slack)와 "어디로 보낼까"(slackConnection)를 나눈다 — 앞은 운영
|
|
18
|
+
* 중 자주 만지고 뒤는 한 번 맞추면 끝이라 수명이 다르다.
|
|
19
|
+
*/
|
|
20
|
+
export type SettingGroup = "openai" | "slack" | "slackConnection";
|
|
12
21
|
|
|
13
22
|
export type SettingDef = {
|
|
14
23
|
key: string;
|
|
@@ -20,6 +29,8 @@ export type SettingDef = {
|
|
|
20
29
|
applies: "immediate" | "restart";
|
|
21
30
|
min?: number;
|
|
22
31
|
max?: number;
|
|
32
|
+
/** `preset` 종류에서 고를 수 있는 값. 이 목록 밖의 값은 저장되지 않는다. */
|
|
33
|
+
presets?: number[];
|
|
23
34
|
/**
|
|
24
35
|
* 저장값도 env 도 없을 때 실제로 적용되는 값. 화면을 비워두면 "설정 안 됨"으로 읽히지만
|
|
25
36
|
* 실제로는 이 값이 돌고 있다 — 소비처의 기본값과 반드시 같아야 한다.
|
|
@@ -90,22 +101,77 @@ export const SETTING_DEFS: SettingDef[] = [
|
|
|
90
101
|
max: MAX_OPENAI_MIN_HOST_AVAILABLE_GIB,
|
|
91
102
|
help: "호스트 여유가 이 값 아래면 스케일업을 멈춥니다. 확장을 끄려면 오토스케일을 끄세요",
|
|
92
103
|
},
|
|
104
|
+
{
|
|
105
|
+
key: "slack.enabled",
|
|
106
|
+
fallback: "true",
|
|
107
|
+
group: "slack",
|
|
108
|
+
envKey: "SLACK_ENABLED",
|
|
109
|
+
label: "Slack 알림",
|
|
110
|
+
kind: "boolean",
|
|
111
|
+
applies: "immediate",
|
|
112
|
+
help: "연휴처럼 규칙으로 잡을 수 없는 기간에 끕니다. 서비스 정지급 장애 알림은 꺼도 전달됩니다",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
key: "slack.remindersEnabled",
|
|
116
|
+
fallback: "true",
|
|
117
|
+
group: "slack",
|
|
118
|
+
envKey: "SLACK_REMINDERS_ENABLED",
|
|
119
|
+
label: "만료 알림 반복",
|
|
120
|
+
kind: "boolean",
|
|
121
|
+
applies: "immediate",
|
|
122
|
+
help: "끄면 반복만 멈추고 만료 순간의 알림은 그대로 갑니다",
|
|
123
|
+
},
|
|
93
124
|
{
|
|
94
125
|
key: "slack.expiryReminderMinutes",
|
|
95
|
-
fallback: "
|
|
126
|
+
fallback: "60",
|
|
96
127
|
group: "slack",
|
|
97
128
|
envKey: "SLACK_EXPIRY_REMINDER_INTERVAL_MINUTES",
|
|
98
|
-
label: "
|
|
129
|
+
label: "반복 주기",
|
|
130
|
+
kind: "preset",
|
|
131
|
+
applies: "immediate",
|
|
132
|
+
// 자유 입력 대신 고정 선택지를 쓴다 — "얼마나 자주 재촉할까"라서 1분 단위 정밀도가
|
|
133
|
+
// 의미 없고, 잘못 넣으면 채널이 시끄러워진다. 끄는 것은 remindersEnabled 가 맡는다.
|
|
134
|
+
presets: [10, 30, 60, 180],
|
|
135
|
+
help: "재로그인이 필요한 토큰이 남아 있는 동안 이 주기로 다시 알립니다",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
key: "slack.quietFromHour",
|
|
139
|
+
fallback: "20",
|
|
140
|
+
group: "slack",
|
|
141
|
+
envKey: "SLACK_QUIET_FROM_HOUR",
|
|
142
|
+
label: "조용 시간 시작",
|
|
143
|
+
kind: "integer",
|
|
144
|
+
applies: "immediate",
|
|
145
|
+
min: 0,
|
|
146
|
+
max: 23,
|
|
147
|
+
help: "이 시각부터 알리지 않습니다 (한국 시간)",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
key: "slack.quietUntilHour",
|
|
151
|
+
fallback: "8",
|
|
152
|
+
group: "slack",
|
|
153
|
+
envKey: "SLACK_QUIET_UNTIL_HOUR",
|
|
154
|
+
label: "조용 시간 종료",
|
|
99
155
|
kind: "integer",
|
|
100
156
|
applies: "immediate",
|
|
101
157
|
min: 0,
|
|
102
|
-
max:
|
|
103
|
-
help: "
|
|
158
|
+
max: 23,
|
|
159
|
+
help: "이 시각부터 다시 알립니다. 시작보다 빠르면 자정을 넘는 구간이 됩니다",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
key: "slack.notifyOnWeekends",
|
|
163
|
+
fallback: "false",
|
|
164
|
+
group: "slack",
|
|
165
|
+
envKey: "SLACK_NOTIFY_ON_WEEKENDS",
|
|
166
|
+
label: "주말에도 알림",
|
|
167
|
+
kind: "boolean",
|
|
168
|
+
applies: "immediate",
|
|
169
|
+
help: "켜면 주말에도 위의 조용 시간 규칙만 적용됩니다",
|
|
104
170
|
},
|
|
105
171
|
{
|
|
106
172
|
key: "slack.channelId",
|
|
107
173
|
fallback: "",
|
|
108
|
-
group: "
|
|
174
|
+
group: "slackConnection",
|
|
109
175
|
envKey: "SLACK_CHANNEL_ID",
|
|
110
176
|
label: "Slack 채널 ID",
|
|
111
177
|
kind: "string",
|
|
@@ -115,7 +181,7 @@ export const SETTING_DEFS: SettingDef[] = [
|
|
|
115
181
|
{
|
|
116
182
|
key: "slack.userMap",
|
|
117
183
|
fallback: "",
|
|
118
|
-
group: "
|
|
184
|
+
group: "slackConnection",
|
|
119
185
|
envKey: "SLACK_USER_MAP",
|
|
120
186
|
label: "Slack 사용자 매핑",
|
|
121
187
|
kind: "string",
|
|
@@ -125,7 +191,7 @@ export const SETTING_DEFS: SettingDef[] = [
|
|
|
125
191
|
{
|
|
126
192
|
key: "slack.botToken",
|
|
127
193
|
fallback: "",
|
|
128
|
-
group: "
|
|
194
|
+
group: "slackConnection",
|
|
129
195
|
envKey: "SLACK_BOT_TOKEN",
|
|
130
196
|
label: "Slack 봇 토큰",
|
|
131
197
|
kind: "secret",
|
|
@@ -149,6 +215,16 @@ export function validateSettingValue(
|
|
|
149
215
|
return { ok: true, value: trimmed };
|
|
150
216
|
}
|
|
151
217
|
|
|
218
|
+
if (def.kind === "preset") {
|
|
219
|
+
// 화면은 버튼만 주지만 API 는 아무 값이나 받을 수 있다. 목록 밖의 값이 들어오면
|
|
220
|
+
// 화면에서 아무 버튼도 선택돼 보이지 않아 "왜 안 바뀌지"가 된다.
|
|
221
|
+
const parsed = Number(trimmed);
|
|
222
|
+
if (!def.presets?.includes(parsed)) {
|
|
223
|
+
return { ok: false, error: `${def.presets?.join(", ") ?? ""} 중 하나여야 합니다` };
|
|
224
|
+
}
|
|
225
|
+
return { ok: true, value: String(parsed) };
|
|
226
|
+
}
|
|
227
|
+
|
|
152
228
|
if (def.kind === "integer" || def.kind === "number") {
|
|
153
229
|
const parsed = Number(trimmed);
|
|
154
230
|
if (!Number.isFinite(parsed)) return { ok: false, error: "숫자를 입력하세요" };
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { SD } from "../../i18n/sd.generated";
|
|
12
12
|
import { type SettingSubsetKey, type SettingSubsetMapping } from "../sonamu.generated";
|
|
13
13
|
import { settingSubsetQueries, settingLoaderQueries } from "../sonamu.generated.sso";
|
|
14
|
-
import { findSettingDef, maskSecret, SETTING_DEFS, validateSettingValue } from "./setting.
|
|
14
|
+
import { findSettingDef, maskSecret, SETTING_DEFS, validateSettingValue } from "./setting.constant";
|
|
15
15
|
import {
|
|
16
16
|
getSetting,
|
|
17
17
|
isStored,
|
|
@@ -213,6 +213,7 @@ class SettingModelClass extends BaseModelClass<
|
|
|
213
213
|
source,
|
|
214
214
|
min: def.min ?? null,
|
|
215
215
|
max: def.max ?? null,
|
|
216
|
+
presets: def.presets ?? [],
|
|
216
217
|
help: def.help ?? null,
|
|
217
218
|
};
|
|
218
219
|
});
|
|
@@ -258,6 +259,19 @@ class SettingModelClass extends BaseModelClass<
|
|
|
258
259
|
await resetStoredSetting(key);
|
|
259
260
|
return { applies: def.applies };
|
|
260
261
|
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 만료 알림을 지금 한 번 보낸다. 주기를 기다리지 않고 설정이 제대로 붙었는지 확인하거나,
|
|
265
|
+
* 재로그인을 재촉할 때 쓴다. 보낸 건수를 돌려줘 "보낼 대상 없음"과 구분한다.
|
|
266
|
+
*/
|
|
267
|
+
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
268
|
+
async triggerExpiryReminder(): Promise<{ sent: number }> {
|
|
269
|
+
// 동적 import 로 알림 모듈을 호출 시점에만 끌어온다. 정적으로 걸면
|
|
270
|
+
// setting.model → expired-token-reminder → token.model 경로가 생겨,
|
|
271
|
+
// token.model 을 mock 하는 테스트가 실제 모듈을 먼저 로드하게 된다.
|
|
272
|
+
const { sendExpiredTokenReminderNow } = await import("../qgrid/expired-token-reminder");
|
|
273
|
+
return { sent: await sendExpiredTokenReminderNow() };
|
|
274
|
+
}
|
|
261
275
|
}
|
|
262
276
|
|
|
263
277
|
export const SettingModel = new SettingModelClass();
|
|
@@ -12,7 +12,7 @@ export type SettingSaveParams = z.infer<typeof SettingSaveParams>;
|
|
|
12
12
|
|
|
13
13
|
// ── 설정 API 응답 스키마 ──
|
|
14
14
|
|
|
15
|
-
export const SettingKind = z.enum(["boolean", "integer", "number", "string", "secret"]);
|
|
15
|
+
export const SettingKind = z.enum(["boolean", "integer", "number", "string", "secret", "preset"]);
|
|
16
16
|
export type SettingKind = z.infer<typeof SettingKind>;
|
|
17
17
|
|
|
18
18
|
export const SettingApplies = z.enum(["immediate", "restart"]);
|
|
@@ -33,6 +33,8 @@ export const SettingItem = z.object({
|
|
|
33
33
|
source: SettingSource,
|
|
34
34
|
min: z.number().nullable(),
|
|
35
35
|
max: z.number().nullable(),
|
|
36
|
+
/** `preset` 종류에서 고를 수 있는 값. 다른 종류는 빈 배열. */
|
|
37
|
+
presets: z.number().array(),
|
|
36
38
|
help: z.string().nullable(),
|
|
37
39
|
});
|
|
38
40
|
export type SettingItem = z.infer<typeof SettingItem>;
|
|
@@ -176,7 +176,9 @@ export default defineConfig({
|
|
|
176
176
|
await loadSettings();
|
|
177
177
|
// 런타임 설정 소비자를 상위 계층에서 연결해 setting 모듈이 qgrid 를 역참조하지 않게 한다.
|
|
178
178
|
setSettingChangeHandler((key) => {
|
|
179
|
-
if (key === "slack.expiryReminderMinutes")
|
|
179
|
+
if (key === "slack.expiryReminderMinutes" || key === "slack.remindersEnabled") {
|
|
180
|
+
rescheduleExpiredTokenReminder();
|
|
181
|
+
}
|
|
180
182
|
});
|
|
181
183
|
|
|
182
184
|
let triggerReady = true;
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
MAX_OPENAI_ESTIMATED_RSS_GIB,
|
|
3
3
|
MAX_OPENAI_MIN_HOST_AVAILABLE_GIB,
|
|
4
4
|
SETTING_DEFS,
|
|
5
|
-
} from "../../../application/setting/setting.
|
|
5
|
+
} from "../../../application/setting/setting.constant";
|
|
6
6
|
import { getSetting } from "../../../application/setting/setting.store";
|
|
7
7
|
|
|
8
8
|
export const MAX_OPENAI_WORKERS_PER_TOKEN = 20;
|