@cartanova/qgrid-cli 2.6.6 → 2.6.7

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.
@@ -17,12 +17,18 @@ import { getLogger } from "@logtape/logtape";
17
17
  */
18
18
  const logger = getLogger(["qgrid", "expired-reminder"]);
19
19
  const MINUTE_MS = 6e4;
20
+ /**
21
+ * 1건이면 제목(subject)에 토큰명이 이미 있으므로 본문에서는 뺀다 — 같은 이름을 두 번 쓰면
22
+ * 읽는 사람이 다른 정보인 줄 알고 한 번 더 본다. 여러 건이면 제목이 건수라 본문에 남긴다.
23
+ */
20
24
  function buildReminderContext(tokens, userMap) {
21
- return tokens.map((token) => {
25
+ const showLabel = tokens.length > 1;
26
+ return [...tokens.map((token) => {
22
27
  const mention = mentionFor(token.name, userMap);
23
28
  const label = token.name ?? "unnamed";
29
+ if (!showLabel) return mention;
24
30
  return mention ? `${mention} ${label}` : label;
25
- }).join("\n");
31
+ }), "재로그인이 필요합니다"].filter(Boolean).join("\n");
26
32
  }
27
33
  async function sendReminder(userMap) {
28
34
  const inactive = await TokenModel.findInactive("A");
@@ -30,7 +36,7 @@ async function sendReminder(userMap) {
30
36
  await notifySlack({
31
37
  title: "세션 만료",
32
38
  subject: inactive.length > 1 ? `${inactive.length}건` : inactive[0].name ?? "unnamed",
33
- context: `${buildReminderContext(inactive, userMap)}\n재로그인이 필요합니다`,
39
+ context: buildReminderContext(inactive, userMap),
34
40
  color: SLACK_COLOR.bad
35
41
  });
36
42
  }
@@ -46,9 +52,9 @@ function startExpiredTokenReminder() {
46
52
  if (!Number.isFinite(minutes) || minutes <= 0) return;
47
53
  const userMap = getSlackUserMap();
48
54
  logger.info(`expired token reminder every ${minutes}m (${userMap.size} user mappings)`);
49
- timer = setInterval(() => {
50
- sendReminder(userMap).catch((e) => logger.warn(`reminder failed: ${e.message}`));
51
- }, minutes * MINUTE_MS);
55
+ const run = () => sendReminder(userMap).catch((e) => logger.warn(`reminder failed: ${e.message}`));
56
+ run();
57
+ timer = setInterval(run, minutes * MINUTE_MS);
52
58
  }
53
59
  function stopExpiredTokenReminder() {
54
60
  if (!timer) return;
@@ -58,4 +64,4 @@ function stopExpiredTokenReminder() {
58
64
  //#endregion
59
65
  export { buildReminderContext, startExpiredTokenReminder, stopExpiredTokenReminder };
60
66
 
61
- //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXhwaXJlZC10b2tlbi1yZW1pbmRlci5qcyIsIm5hbWVzIjpbXSwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvYXBwbGljYXRpb24vcWdyaWQvZXhwaXJlZC10b2tlbi1yZW1pbmRlci50cyJdLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIOyEuOyFmCDrp4zro4wg7JWM66a8IOuwmOuztS5cbiAqXG4gKiDrp4zro4wg7Iic6rCE7J2YIOyVjOumvChgdG9rZW4tZGVhdGgudHNgKeydgCDqt7jrlYwg7LGE64SQ7J2EIOuztOyngCDslYrsnLzrqbQg64aT7Lmc64ukLiDsnqzroZzqt7jsnbjsnYAg7IKs656M7J20XG4gKiDtlbTslbwg7ZWY64qUIOydvOydtOudvCDrhpPsuZjrqbQg7Yag7YGw7J20IOqzhOyGjSDruaDsp4Qg7LGE66GcIOuCqOuKlOuLpC4g6re4656Y7IScIOyVhOyngSDsnqzroZzqt7jsnbjrkJjsp4Ag7JWK7J2AXG4gKiDthqDtgbDsnYQg6rCZ7J2AIOusuOq1rOuhnCDri6Tsi5wg7JWM66aw64ukIOKAlCDsgqzqsbTsnbQg7ZWY64KY7J2066+A66GcIOyVjOumvOuPhCDtlZwg7KKF66WY64ukLlxuICpcbiAqIOyXrOufrCDthqDtgbDsnbQg66eM66OM64+8IOyeiOycvOuptCDtlZwg66mU7Iuc7KeA7JeQIOuqqOydgOuLpC4g7Yag7YGw66eI64ukIOuplOyLnOyngOulvCDrs7TrgrTrqbQg7LGE64SQ7J20IOyLnOuBhOufrOybjFxuICog7KCV7J6RIOydve2eiOyngCDslYrripTri6QuXG4gKlxuICog67mE7Zmc7ISxIO2GoO2BsOydtCDsl4bsnLzrqbQg67O064K07KeAIOyViuuKlOuLpCDigJQgXCLsnbTsg4Eg7JeG7J2MXCLsnYQg7KO86riw7KCB7Jy866GcIOyVjOumrOuptCDssYTrhJDsnbQg66y0646M7KeE64ukLlxuICovXG5pbXBvcnQgeyBnZXRMb2dnZXIgfSBmcm9tIFwiQGxvZ3RhcGUvbG9ndGFwZVwiO1xuXG5pbXBvcnQgeyBub3RpZnlTbGFjaywgU0xBQ0tfQ09MT1IgfSBmcm9tIFwiLi4vLi4vdXRpbHMvc2xhY2stbm90aWZ5XCI7XG5pbXBvcnQgeyBUb2tlbk1vZGVsIH0gZnJvbSBcIi4uL3Rva2VuL3Rva2VuLm1vZGVsXCI7XG5pbXBvcnQgeyBnZXRTbGFja1VzZXJNYXAsIG1lbnRpb25Gb3IgfSBmcm9tIFwiLi9zbGFjay11c2VyLW1hcFwiO1xuXG5jb25zdCBsb2dnZXIgPSBnZXRMb2dnZXIoW1wicWdyaWRcIiwgXCJleHBpcmVkLXJlbWluZGVyXCJdKTtcblxuY29uc3QgTUlOVVRFX01TID0gNjBfMDAwO1xuXG5leHBvcnQgZnVuY3Rpb24gYnVpbGRSZW1pbmRlckNvbnRleHQoXG4gIHRva2VuczogeyBuYW1lOiBzdHJpbmcgfCBudWxsOyBwcm92aWRlcjogc3RyaW5nIH1bXSxcbiAgdXNlck1hcDogTWFwPHN0cmluZywgc3RyaW5nPixcbik6IHN0cmluZyB7XG4gIHJldHVybiB0b2tlbnNcbiAgICAubWFwKCh0b2tlbikgPT4ge1xuICAgICAgY29uc3QgbWVudGlvbiA9IG1lbnRpb25Gb3IodG9rZW4ubmFtZSwgdXNlck1hcCk7XG4gICAgICBjb25zdCBsYWJlbCA9IHRva2VuLm5hbWUgPz8gXCJ1bm5hbWVkXCI7XG4gICAgICAvLyDrqZjshZjsnbQg7J6I7Ja064+EIOyWtOuWpCDthqDtgbDsnbjsp4DripQg64Ko6ri064ukIOKAlCDtlZwg7IKs656M7J20IHByb3ZpZGVyIOuzhOuhnCDsl6zrn6wg6rCc66W8IOqwgOynhOuLpC5cbiAgICAgIHJldHVybiBtZW50aW9uID8gYCR7bWVudGlvbn0gJHtsYWJlbH1gIDogbGFiZWw7XG4gICAgfSlcbiAgICAuam9pbihcIlxcblwiKTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gc2VuZFJlbWluZGVyKHVzZXJNYXA6IE1hcDxzdHJpbmcsIHN0cmluZz4pOiBQcm9taXNlPHZvaWQ+IHtcbiAgY29uc3QgaW5hY3RpdmUgPSBhd2FpdCBUb2tlbk1vZGVsLmZpbmRJbmFjdGl2ZShcIkFcIik7XG4gIGlmIChpbmFjdGl2ZS5sZW5ndGggPT09IDApIHJldHVybjtcblxuICBhd2FpdCBub3RpZnlTbGFjayh7XG4gICAgLy8g66eM66OMIOyInOqwhCDslYzrprzqs7wg6rCZ7J2AIOygnOuqqeydhCDsk7Tri6QuIOqwmeydgCDsgqzqsbTsnbTrr4DroZwg64uk66W4IOydtOumhOydhCDrtpnsnbTrqbQg67OE6rCcIOusuOygnOuhnCDsnb3tnozri6QuXG4gICAgdGl0bGU6IFwi7IS47IWYIOunjOujjFwiLFxuICAgIHN1YmplY3Q6IGluYWN0aXZlLmxlbmd0aCA+IDEgPyBgJHtpbmFjdGl2ZS5sZW5ndGh96rG0YCA6IChpbmFjdGl2ZVswXSEubmFtZSA/PyBcInVubmFtZWRcIiksXG4gICAgY29udGV4dDogYCR7YnVpbGRSZW1pbmRlckNvbnRleHQoaW5hY3RpdmUsIHVzZXJNYXApfVxcbuyerOuhnOq3uOyduOydtCDtlYTsmpTtlanri4jri6RgLFxuICAgIGNvbG9yOiBTTEFDS19DT0xPUi5iYWQsXG4gIH0pO1xufVxuXG5sZXQgdGltZXI6IFJldHVyblR5cGU8dHlwZW9mIHNldEludGVydmFsPiB8IG51bGwgPSBudWxsO1xuXG4vKipcbiAqIGBTTEFDS19FWFBJUllfUkVNSU5ERVJfSU5URVJWQUxfTUlOVVRFU2Ag6rCAIOyWkeyImOydvCDrlYzrp4wg7Lyc7KeE64ukLiDrr7jshKTsoJXCtzAg7J2066m0IOu5hO2ZnOyEsS5cbiAqXG4gKiDsnbjsiqTthLTsiqTqsIAg7Jes65+sIOqwnOuptCDqsIHsnpAg67O064K464ukLiDrp4zro4wg7Iic6rCEIOyVjOumvOqzvCDri6zrpqwg7IOB7YOcIOuzgO2ZlOqwgCDsl4bslrQgREIg66GcIOyWteygnO2VoCDsiJhcbiAqIOyXhuqzoCwg7ZWY66OoIOuqhyDqsbQg7IiY7KSA7J206528IOykkeuzteydhCDqsJDsiJjtlZjripQg7Y647J20IOyeoOq4iCDsnqXsuZjrpbwg65GQ64qUIOqyg+uztOuLpCDrgqvri6QuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzdGFydEV4cGlyZWRUb2tlblJlbWluZGVyKCk6IHZvaWQge1xuICBjb25zdCBtaW51dGVzID0gTnVtYmVyKHByb2Nlc3MuZW52LlNMQUNLX0VYUElSWV9SRU1JTkRFUl9JTlRFUlZBTF9NSU5VVEVTID8/IDApO1xuICBpZiAoIU51bWJlci5pc0Zpbml0ZShtaW51dGVzKSB8fCBtaW51dGVzIDw9IDApIHJldHVybjtcblxuICBjb25zdCB1c2VyTWFwID0gZ2V0U2xhY2tVc2VyTWFwKCk7XG4gIGxvZ2dlci5pbmZvKGBleHBpcmVkIHRva2VuIHJlbWluZGVyIGV2ZXJ5ICR7bWludXRlc31tICgke3VzZXJNYXAuc2l6ZX0gdXNlciBtYXBwaW5ncylgKTtcblxuICB0aW1lciA9IHNldEludGVydmFsKCgpID0+IHtcbiAgICBzZW5kUmVtaW5kZXIodXNlck1hcCkuY2F0Y2goKGUpID0+IGxvZ2dlci53YXJuKGByZW1pbmRlciBmYWlsZWQ6ICR7KGUgYXMgRXJyb3IpLm1lc3NhZ2V9YCkpO1xuICB9LCBtaW51dGVzICogTUlOVVRFX01TKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHN0b3BFeHBpcmVkVG9rZW5SZW1pbmRlcigpOiB2b2lkIHtcbiAgaWYgKCF0aW1lcikgcmV0dXJuO1xuICBjbGVhckludGVydmFsKHRpbWVyKTtcbiAgdGltZXIgPSBudWxsO1xufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7OztBQWtCQSxNQUFNLFNBQVMsVUFBVSxDQUFDLFNBQVMsa0JBQWtCLENBQUM7QUFFdEQsTUFBTSxZQUFZO0FBRWxCLFNBQWdCLHFCQUNkLFFBQ0EsU0FDUTtDQUNSLE9BQU8sT0FDSixLQUFLLFVBQVU7RUFDZCxNQUFNLFVBQVUsV0FBVyxNQUFNLE1BQU0sT0FBTztFQUM5QyxNQUFNLFFBQVEsTUFBTSxRQUFRO0VBRTVCLE9BQU8sVUFBVSxHQUFHLFFBQVEsR0FBRyxVQUFVO0NBQzNDLENBQUMsQ0FBQyxDQUNELEtBQUssSUFBSTtBQUNkO0FBRUEsZUFBZSxhQUFhLFNBQTZDO0NBQ3ZFLE1BQU0sV0FBVyxNQUFNLFdBQVcsYUFBYSxHQUFHO0NBQ2xELElBQUksU0FBUyxXQUFXLEdBQUc7Q0FFM0IsTUFBTSxZQUFZO0VBRWhCLE9BQU87RUFDUCxTQUFTLFNBQVMsU0FBUyxJQUFJLEdBQUcsU0FBUyxPQUFPLEtBQU0sU0FBUyxFQUFFLENBQUUsUUFBUTtFQUM3RSxTQUFTLEdBQUcscUJBQXFCLFVBQVUsT0FBTyxFQUFFO0VBQ3BELE9BQU8sWUFBWTtDQUNyQixDQUFDO0FBQ0g7QUFFQSxJQUFJLFFBQStDOzs7Ozs7O0FBUW5ELFNBQWdCLDRCQUFrQztDQUNoRCxNQUFNLFVBQVUsT0FBTyxRQUFRLElBQUksMENBQTBDLENBQUM7Q0FDOUUsSUFBSSxDQUFDLE9BQU8sU0FBUyxPQUFPLEtBQUssV0FBVyxHQUFHO0NBRS9DLE1BQU0sVUFBVSxnQkFBZ0I7Q0FDaEMsT0FBTyxLQUFLLGdDQUFnQyxRQUFRLEtBQUssUUFBUSxLQUFLLGdCQUFnQjtDQUV0RixRQUFRLGtCQUFrQjtFQUN4QixhQUFhLE9BQU8sQ0FBQyxDQUFDLE9BQU8sTUFBTSxPQUFPLEtBQUssb0JBQXFCLEVBQVksU0FBUyxDQUFDO0NBQzVGLEdBQUcsVUFBVSxTQUFTO0FBQ3hCO0FBRUEsU0FBZ0IsMkJBQWlDO0NBQy9DLElBQUksQ0FBQyxPQUFPO0NBQ1osY0FBYyxLQUFLO0NBQ25CLFFBQVE7QUFDViJ9
67
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXhwaXJlZC10b2tlbi1yZW1pbmRlci5qcyIsIm5hbWVzIjpbXSwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvYXBwbGljYXRpb24vcWdyaWQvZXhwaXJlZC10b2tlbi1yZW1pbmRlci50cyJdLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIOyEuOyFmCDrp4zro4wg7JWM66a8IOuwmOuztS5cbiAqXG4gKiDrp4zro4wg7Iic6rCE7J2YIOyVjOumvChgdG9rZW4tZGVhdGgudHNgKeydgCDqt7jrlYwg7LGE64SQ7J2EIOuztOyngCDslYrsnLzrqbQg64aT7Lmc64ukLiDsnqzroZzqt7jsnbjsnYAg7IKs656M7J20XG4gKiDtlbTslbwg7ZWY64qUIOydvOydtOudvCDrhpPsuZjrqbQg7Yag7YGw7J20IOqzhOyGjSDruaDsp4Qg7LGE66GcIOuCqOuKlOuLpC4g6re4656Y7IScIOyVhOyngSDsnqzroZzqt7jsnbjrkJjsp4Ag7JWK7J2AXG4gKiDthqDtgbDsnYQg6rCZ7J2AIOusuOq1rOuhnCDri6Tsi5wg7JWM66aw64ukIOKAlCDsgqzqsbTsnbQg7ZWY64KY7J2066+A66GcIOyVjOumvOuPhCDtlZwg7KKF66WY64ukLlxuICpcbiAqIOyXrOufrCDthqDtgbDsnbQg66eM66OM64+8IOyeiOycvOuptCDtlZwg66mU7Iuc7KeA7JeQIOuqqOydgOuLpC4g7Yag7YGw66eI64ukIOuplOyLnOyngOulvCDrs7TrgrTrqbQg7LGE64SQ7J20IOyLnOuBhOufrOybjFxuICog7KCV7J6RIOydve2eiOyngCDslYrripTri6QuXG4gKlxuICog67mE7Zmc7ISxIO2GoO2BsOydtCDsl4bsnLzrqbQg67O064K07KeAIOyViuuKlOuLpCDigJQgXCLsnbTsg4Eg7JeG7J2MXCLsnYQg7KO86riw7KCB7Jy866GcIOyVjOumrOuptCDssYTrhJDsnbQg66y0646M7KeE64ukLlxuICovXG5pbXBvcnQgeyBnZXRMb2dnZXIgfSBmcm9tIFwiQGxvZ3RhcGUvbG9ndGFwZVwiO1xuXG5pbXBvcnQgeyBub3RpZnlTbGFjaywgU0xBQ0tfQ09MT1IgfSBmcm9tIFwiLi4vLi4vdXRpbHMvc2xhY2stbm90aWZ5XCI7XG5pbXBvcnQgeyBUb2tlbk1vZGVsIH0gZnJvbSBcIi4uL3Rva2VuL3Rva2VuLm1vZGVsXCI7XG5pbXBvcnQgeyBnZXRTbGFja1VzZXJNYXAsIG1lbnRpb25Gb3IgfSBmcm9tIFwiLi9zbGFjay11c2VyLW1hcFwiO1xuXG5jb25zdCBsb2dnZXIgPSBnZXRMb2dnZXIoW1wicWdyaWRcIiwgXCJleHBpcmVkLXJlbWluZGVyXCJdKTtcblxuY29uc3QgTUlOVVRFX01TID0gNjBfMDAwO1xuXG4vKipcbiAqIDHqsbTsnbTrqbQg7KCc66qpKHN1YmplY3Qp7JeQIO2GoO2BsOuqheydtCDsnbTrr7gg7J6I7Jy866+A66GcIOuzuOusuOyXkOyEnOuKlCDruoDri6Qg4oCUIOqwmeydgCDsnbTrpoTsnYQg65GQIOuyiCDsk7DrqbRcbiAqIOydveuKlCDsgqzrnozsnbQg64uk66W4IOygleuztOyduCDspIQg7JWM6rOgIO2VnCDrsogg642UIOuzuOuLpC4g7Jes65+sIOqxtOydtOuptCDsoJzrqqnsnbQg6rG07IiY6528IOuzuOusuOyXkCDrgqjquLTri6QuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBidWlsZFJlbWluZGVyQ29udGV4dChcbiAgdG9rZW5zOiB7IG5hbWU6IHN0cmluZyB8IG51bGw7IHByb3ZpZGVyOiBzdHJpbmcgfVtdLFxuICB1c2VyTWFwOiBNYXA8c3RyaW5nLCBzdHJpbmc+LFxuKTogc3RyaW5nIHtcbiAgY29uc3Qgc2hvd0xhYmVsID0gdG9rZW5zLmxlbmd0aCA+IDE7XG4gIGNvbnN0IGxpbmVzID0gdG9rZW5zLm1hcCgodG9rZW4pID0+IHtcbiAgICBjb25zdCBtZW50aW9uID0gbWVudGlvbkZvcih0b2tlbi5uYW1lLCB1c2VyTWFwKTtcbiAgICBjb25zdCBsYWJlbCA9IHRva2VuLm5hbWUgPz8gXCJ1bm5hbWVkXCI7XG4gICAgaWYgKCFzaG93TGFiZWwpIHJldHVybiBtZW50aW9uO1xuICAgIHJldHVybiBtZW50aW9uID8gYCR7bWVudGlvbn0gJHtsYWJlbH1gIDogbGFiZWw7XG4gIH0pO1xuICByZXR1cm4gWy4uLmxpbmVzLCBcIuyerOuhnOq3uOyduOydtCDtlYTsmpTtlanri4jri6RcIl0uZmlsdGVyKEJvb2xlYW4pLmpvaW4oXCJcXG5cIik7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIHNlbmRSZW1pbmRlcih1c2VyTWFwOiBNYXA8c3RyaW5nLCBzdHJpbmc+KTogUHJvbWlzZTx2b2lkPiB7XG4gIGNvbnN0IGluYWN0aXZlID0gYXdhaXQgVG9rZW5Nb2RlbC5maW5kSW5hY3RpdmUoXCJBXCIpO1xuICBpZiAoaW5hY3RpdmUubGVuZ3RoID09PSAwKSByZXR1cm47XG5cbiAgYXdhaXQgbm90aWZ5U2xhY2soe1xuICAgIC8vIOunjOujjCDsiJzqsIQg7JWM66a86rO8IOqwmeydgCDsoJzrqqnsnYQg7JO064ukLiDqsJnsnYAg7IKs6rG07J2066+A66GcIOuLpOuluCDsnbTrpoTsnYQg67aZ7J2066m0IOuzhOqwnCDrrLjsoJzroZwg7J297Z6M64ukLlxuICAgIHRpdGxlOiBcIuyEuOyFmCDrp4zro4xcIixcbiAgICBzdWJqZWN0OiBpbmFjdGl2ZS5sZW5ndGggPiAxID8gYCR7aW5hY3RpdmUubGVuZ3RofeqxtGAgOiAoaW5hY3RpdmVbMF0hLm5hbWUgPz8gXCJ1bm5hbWVkXCIpLFxuICAgIGNvbnRleHQ6IGJ1aWxkUmVtaW5kZXJDb250ZXh0KGluYWN0aXZlLCB1c2VyTWFwKSxcbiAgICBjb2xvcjogU0xBQ0tfQ09MT1IuYmFkLFxuICB9KTtcbn1cblxubGV0IHRpbWVyOiBSZXR1cm5UeXBlPHR5cGVvZiBzZXRJbnRlcnZhbD4gfCBudWxsID0gbnVsbDtcblxuLyoqXG4gKiBgU0xBQ0tfRVhQSVJZX1JFTUlOREVSX0lOVEVSVkFMX01JTlVURVNgIOqwgCDslpHsiJjsnbwg65WM66eMIOy8nOynhOuLpC4g66+47ISk7KCVwrcwIOydtOuptCDruYTtmZzshLEuXG4gKlxuICog7J247Iqk7YS07Iqk6rCAIOyXrOufrCDqsJzrqbQg6rCB7J6QIOuztOuCuOuLpC4g66eM66OMIOyInOqwhCDslYzrprzqs7wg64us66asIOyDge2DnCDrs4DtmZTqsIAg7JeG7Ja0IERCIOuhnCDslrXsoJztlaAg7IiYXG4gKiDsl4bqs6AsIO2VmOujqCDrqocg6rG0IOyImOykgOydtOudvCDspJHrs7XsnYQg6rCQ7IiY7ZWY64qUIO2OuOydtCDsnqDquIgg7J6l7LmY66W8IOuRkOuKlCDqsoPrs7Tri6Qg64Kr64ukLlxuICovXG5leHBvcnQgZnVuY3Rpb24gc3RhcnRFeHBpcmVkVG9rZW5SZW1pbmRlcigpOiB2b2lkIHtcbiAgY29uc3QgbWludXRlcyA9IE51bWJlcihwcm9jZXNzLmVudi5TTEFDS19FWFBJUllfUkVNSU5ERVJfSU5URVJWQUxfTUlOVVRFUyA/PyAwKTtcbiAgaWYgKCFOdW1iZXIuaXNGaW5pdGUobWludXRlcykgfHwgbWludXRlcyA8PSAwKSByZXR1cm47XG5cbiAgY29uc3QgdXNlck1hcCA9IGdldFNsYWNrVXNlck1hcCgpO1xuICBsb2dnZXIuaW5mbyhgZXhwaXJlZCB0b2tlbiByZW1pbmRlciBldmVyeSAke21pbnV0ZXN9bSAoJHt1c2VyTWFwLnNpemV9IHVzZXIgbWFwcGluZ3MpYCk7XG5cbiAgY29uc3QgcnVuID0gKCkgPT5cbiAgICBzZW5kUmVtaW5kZXIodXNlck1hcCkuY2F0Y2goKGUpID0+IGxvZ2dlci53YXJuKGByZW1pbmRlciBmYWlsZWQ6ICR7KGUgYXMgRXJyb3IpLm1lc3NhZ2V9YCkpO1xuXG4gIC8vIOq4sOuPmSDsp4Htm4Tsl5Drj4Qg7ZWcIOuyiCDrs7Trgrjri6QuIOyerOq4sOuPmeydgCDrsLDtj6zrgpgg7J6l7JWgIOuzteq1rCDrlYwg7J287Ja064KY64qU642wLCDqt7gg7Iuc7KCQ7JeQIOydtOuvuFxuICAvLyDrp4zro4zrkJwg7Yag7YGw7J20IOyeiOycvOuptCDssqsg7KO86riw6rCAIOuPjCDrlYzquYzsp4Ag7KGw7Jqp7ZWcIOqyg+ydtCDsnbTsg4HtlZjri6QuXG4gIHZvaWQgcnVuKCk7XG5cbiAgdGltZXIgPSBzZXRJbnRlcnZhbChydW4sIG1pbnV0ZXMgKiBNSU5VVEVfTVMpO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gc3RvcEV4cGlyZWRUb2tlblJlbWluZGVyKCk6IHZvaWQge1xuICBpZiAoIXRpbWVyKSByZXR1cm47XG4gIGNsZWFySW50ZXJ2YWwodGltZXIpO1xuICB0aW1lciA9IG51bGw7XG59XG4iXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBa0JBLE1BQU0sU0FBUyxVQUFVLENBQUMsU0FBUyxrQkFBa0IsQ0FBQztBQUV0RCxNQUFNLFlBQVk7Ozs7O0FBTWxCLFNBQWdCLHFCQUNkLFFBQ0EsU0FDUTtDQUNSLE1BQU0sWUFBWSxPQUFPLFNBQVM7Q0FPbEMsT0FBTyxDQUFDLEdBTk0sT0FBTyxLQUFLLFVBQVU7RUFDbEMsTUFBTSxVQUFVLFdBQVcsTUFBTSxNQUFNLE9BQU87RUFDOUMsTUFBTSxRQUFRLE1BQU0sUUFBUTtFQUM1QixJQUFJLENBQUMsV0FBVyxPQUFPO0VBQ3ZCLE9BQU8sVUFBVSxHQUFHLFFBQVEsR0FBRyxVQUFVO0NBQzNDLENBQ1csR0FBTyxhQUFhLENBQUMsQ0FBQyxPQUFPLE9BQU8sQ0FBQyxDQUFDLEtBQUssSUFBSTtBQUM1RDtBQUVBLGVBQWUsYUFBYSxTQUE2QztDQUN2RSxNQUFNLFdBQVcsTUFBTSxXQUFXLGFBQWEsR0FBRztDQUNsRCxJQUFJLFNBQVMsV0FBVyxHQUFHO0NBRTNCLE1BQU0sWUFBWTtFQUVoQixPQUFPO0VBQ1AsU0FBUyxTQUFTLFNBQVMsSUFBSSxHQUFHLFNBQVMsT0FBTyxLQUFNLFNBQVMsRUFBRSxDQUFFLFFBQVE7RUFDN0UsU0FBUyxxQkFBcUIsVUFBVSxPQUFPO0VBQy9DLE9BQU8sWUFBWTtDQUNyQixDQUFDO0FBQ0g7QUFFQSxJQUFJLFFBQStDOzs7Ozs7O0FBUW5ELFNBQWdCLDRCQUFrQztDQUNoRCxNQUFNLFVBQVUsT0FBTyxRQUFRLElBQUksMENBQTBDLENBQUM7Q0FDOUUsSUFBSSxDQUFDLE9BQU8sU0FBUyxPQUFPLEtBQUssV0FBVyxHQUFHO0NBRS9DLE1BQU0sVUFBVSxnQkFBZ0I7Q0FDaEMsT0FBTyxLQUFLLGdDQUFnQyxRQUFRLEtBQUssUUFBUSxLQUFLLGdCQUFnQjtDQUV0RixNQUFNLFlBQ0osYUFBYSxPQUFPLENBQUMsQ0FBQyxPQUFPLE1BQU0sT0FBTyxLQUFLLG9CQUFxQixFQUFZLFNBQVMsQ0FBQztDQUk1RixBQUFLLElBQUk7Q0FFVCxRQUFRLFlBQVksS0FBSyxVQUFVLFNBQVM7QUFDOUM7QUFFQSxTQUFnQiwyQkFBaUM7Q0FDL0MsSUFBSSxDQUFDLE9BQU87Q0FDWixjQUFjLEtBQUs7Q0FDbkIsUUFBUTtBQUNWIn0=
@@ -8,22 +8,22 @@ const userMap = new Map([
8
8
  ]);
9
9
 
10
10
  describe("buildReminderContext", () => {
11
- it("매핑된 토큰은 멘션으로 개인에게 꽂는다", () => {
11
+ it("1건이면 제목에 있는 토큰명을 본문에서 반복하지 않는다", () => {
12
12
  const text = buildReminderContext([{ name: "anthropic/yds", provider: "anthropic" }], userMap);
13
13
 
14
- expect(text).toBe("<@U09NAJUQSFQ> anthropic/yds");
14
+ expect(text).toBe("<@U09NAJUQSFQ>\n재로그인이 필요합니다");
15
15
  });
16
16
 
17
- it("공용 계정처럼 매핑이 없으면 멘션 없이 이름만 남긴다", () => {
17
+ it("1건이고 매핑이 없으면 안내만 남는다", () => {
18
18
  const text = buildReminderContext(
19
19
  [{ name: "anthropic/dev-common", provider: "anthropic" }],
20
20
  userMap,
21
21
  );
22
22
 
23
- expect(text).toBe("anthropic/dev-common");
23
+ expect(text).toBe("재로그인이 필요합니다");
24
24
  });
25
25
 
26
- it("같은 사람의 provider 별 토큰을 각각 구분해 보여준다", () => {
26
+ it("여러 건이면 제목이 건수라 본문에 토큰명을 남긴다", () => {
27
27
  const text = buildReminderContext(
28
28
  [
29
29
  { name: "anthropic/yds", provider: "anthropic" },
@@ -32,22 +32,42 @@ describe("buildReminderContext", () => {
32
32
  userMap,
33
33
  );
34
34
 
35
- // 멘션만 두 번 나오면 어느 토큰인지 알 수 없다 — 토큰명이 함께 있어야 한다.
36
35
  expect(text.split("\n")).toEqual([
37
36
  "<@U09NAJUQSFQ> anthropic/yds",
38
37
  "<@U09NAJUQSFQ> openai/yds",
38
+ "재로그인이 필요합니다",
39
39
  ]);
40
40
  });
41
41
 
42
- it("provider prefix 없이 저장된 이름도 매핑한다", () => {
43
- const text = buildReminderContext([{ name: "haze", provider: "anthropic" }], userMap);
42
+ it("여러 건 중 매핑 없는 공용 계정은 이름만 남긴다", () => {
43
+ const text = buildReminderContext(
44
+ [
45
+ { name: "anthropic/haze", provider: "anthropic" },
46
+ { name: "anthropic/dev-common", provider: "anthropic" },
47
+ ],
48
+ userMap,
49
+ );
44
50
 
45
- expect(text).toBe("<@U09N96NHZB7> haze");
51
+ expect(text.split("\n")).toEqual([
52
+ "<@U09N96NHZB7> anthropic/haze",
53
+ "anthropic/dev-common",
54
+ "재로그인이 필요합니다",
55
+ ]);
46
56
  });
47
57
 
48
58
  it("이름이 없는 토큰도 줄을 잃지 않는다", () => {
49
- const text = buildReminderContext([{ name: null, provider: "openai" }], userMap);
59
+ const text = buildReminderContext(
60
+ [
61
+ { name: null, provider: "openai" },
62
+ { name: "anthropic/haze", provider: "anthropic" },
63
+ ],
64
+ userMap,
65
+ );
50
66
 
51
- expect(text).toBe("unnamed");
67
+ expect(text.split("\n")).toEqual([
68
+ "unnamed",
69
+ "<@U09N96NHZB7> anthropic/haze",
70
+ "재로그인이 필요합니다",
71
+ ]);
52
72
  });
53
73
  });
@@ -20,18 +20,22 @@ const logger = getLogger(["qgrid", "expired-reminder"]);
20
20
 
21
21
  const MINUTE_MS = 60_000;
22
22
 
23
+ /**
24
+ * 1건이면 제목(subject)에 토큰명이 이미 있으므로 본문에서는 뺀다 — 같은 이름을 두 번 쓰면
25
+ * 읽는 사람이 다른 정보인 줄 알고 한 번 더 본다. 여러 건이면 제목이 건수라 본문에 남긴다.
26
+ */
23
27
  export function buildReminderContext(
24
28
  tokens: { name: string | null; provider: string }[],
25
29
  userMap: Map<string, string>,
26
30
  ): string {
27
- return tokens
28
- .map((token) => {
29
- const mention = mentionFor(token.name, userMap);
30
- const label = token.name ?? "unnamed";
31
- // 멘션이 있어도 어떤 토큰인지는 남긴다 — 한 사람이 provider 별로 여러 개를 가진다.
32
- return mention ? `${mention} ${label}` : label;
33
- })
34
- .join("\n");
31
+ const showLabel = tokens.length > 1;
32
+ const lines = tokens.map((token) => {
33
+ const mention = mentionFor(token.name, userMap);
34
+ const label = token.name ?? "unnamed";
35
+ if (!showLabel) return mention;
36
+ return mention ? `${mention} ${label}` : label;
37
+ });
38
+ return [...lines, "재로그인이 필요합니다"].filter(Boolean).join("\n");
35
39
  }
36
40
 
37
41
  async function sendReminder(userMap: Map<string, string>): Promise<void> {
@@ -42,7 +46,7 @@ async function sendReminder(userMap: Map<string, string>): Promise<void> {
42
46
  // 만료 순간 알림과 같은 제목을 쓴다. 같은 사건이므로 다른 이름을 붙이면 별개 문제로 읽힌다.
43
47
  title: "세션 만료",
44
48
  subject: inactive.length > 1 ? `${inactive.length}건` : (inactive[0]!.name ?? "unnamed"),
45
- context: `${buildReminderContext(inactive, userMap)}\n재로그인이 필요합니다`,
49
+ context: buildReminderContext(inactive, userMap),
46
50
  color: SLACK_COLOR.bad,
47
51
  });
48
52
  }
@@ -62,9 +66,14 @@ export function startExpiredTokenReminder(): void {
62
66
  const userMap = getSlackUserMap();
63
67
  logger.info(`expired token reminder every ${minutes}m (${userMap.size} user mappings)`);
64
68
 
65
- timer = setInterval(() => {
69
+ const run = () =>
66
70
  sendReminder(userMap).catch((e) => logger.warn(`reminder failed: ${(e as Error).message}`));
67
- }, minutes * MINUTE_MS);
71
+
72
+ // 기동 직후에도 한 번 보낸다. 재기동은 배포나 장애 복구 때 일어나는데, 그 시점에 이미
73
+ // 만료된 토큰이 있으면 첫 주기가 돌 때까지 조용한 것이 이상하다.
74
+ void run();
75
+
76
+ timer = setInterval(run, minutes * MINUTE_MS);
68
77
  }
69
78
 
70
79
  export function stopExpiredTokenReminder(): void {