mega-framework 0.1.0

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.
Files changed (322) hide show
  1. package/.env +127 -0
  2. package/.env.example +186 -0
  3. package/.prettierrc.json +8 -0
  4. package/CHANGELOG.md +259 -0
  5. package/LICENSE +21 -0
  6. package/README.md +153 -0
  7. package/bin/mega-ws-hub.js +15 -0
  8. package/bin/mega.js +38 -0
  9. package/docker-compose.yml +201 -0
  10. package/eslint.config.js +57 -0
  11. package/infra/otel-collector-config.yaml +43 -0
  12. package/jsconfig.json +18 -0
  13. package/package.json +121 -0
  14. package/sample/crud/.env +18 -0
  15. package/sample/crud/.env.example +50 -0
  16. package/sample/crud/README.md +85 -0
  17. package/sample/crud/apps/main/app.config.js +114 -0
  18. package/sample/crud/apps/main/channels/chat-bus.js +115 -0
  19. package/sample/crud/apps/main/channels/chat-channel.js +145 -0
  20. package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
  21. package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
  22. package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
  23. package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
  24. package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
  25. package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
  26. package/sample/crud/apps/main/controllers/note-controller.js +116 -0
  27. package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
  28. package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
  29. package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
  30. package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
  31. package/sample/crud/apps/main/controllers/user-controller.js +34 -0
  32. package/sample/crud/apps/main/controllers/web-controller.js +137 -0
  33. package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
  34. package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
  35. package/sample/crud/apps/main/jobs/email-job.js +72 -0
  36. package/sample/crud/apps/main/locales/client/en.json +3 -0
  37. package/sample/crud/apps/main/locales/client/ko.json +3 -0
  38. package/sample/crud/apps/main/locales/server/en.json +316 -0
  39. package/sample/crud/apps/main/locales/server/ko.json +316 -0
  40. package/sample/crud/apps/main/middleware/web-auth.js +40 -0
  41. package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
  42. package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
  43. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
  44. package/sample/crud/apps/main/models/note.js +71 -0
  45. package/sample/crud/apps/main/models/user.js +86 -0
  46. package/sample/crud/apps/main/public/css/app.css +101 -0
  47. package/sample/crud/apps/main/public/css/guide.css +137 -0
  48. package/sample/crud/apps/main/public/js/app.js +54 -0
  49. package/sample/crud/apps/main/public/js/perf.js +129 -0
  50. package/sample/crud/apps/main/public/js/theme-init.js +12 -0
  51. package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
  52. package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
  53. package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
  54. package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  55. package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
  56. package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
  57. package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
  58. package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
  59. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
  60. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
  61. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
  62. package/sample/crud/apps/main/routes/auth.js +15 -0
  63. package/sample/crud/apps/main/routes/cron.js +14 -0
  64. package/sample/crud/apps/main/routes/guide.js +25 -0
  65. package/sample/crud/apps/main/routes/jobs.js +14 -0
  66. package/sample/crud/apps/main/routes/logs.js +28 -0
  67. package/sample/crud/apps/main/routes/metrics.js +13 -0
  68. package/sample/crud/apps/main/routes/notes.js +19 -0
  69. package/sample/crud/apps/main/routes/perf.js +47 -0
  70. package/sample/crud/apps/main/routes/redis.js +14 -0
  71. package/sample/crud/apps/main/routes/tracing.js +14 -0
  72. package/sample/crud/apps/main/routes/upload.js +16 -0
  73. package/sample/crud/apps/main/routes/users.js +54 -0
  74. package/sample/crud/apps/main/routes/web.js +23 -0
  75. package/sample/crud/apps/main/routes/worker.js +15 -0
  76. package/sample/crud/apps/main/routes/ws.js +30 -0
  77. package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
  78. package/sample/crud/apps/main/services/auth-service.js +74 -0
  79. package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
  80. package/sample/crud/apps/main/services/guide-service.js +145 -0
  81. package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
  82. package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
  83. package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
  84. package/sample/crud/apps/main/services/note-service.js +75 -0
  85. package/sample/crud/apps/main/services/perf-service.js +302 -0
  86. package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
  87. package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
  88. package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
  89. package/sample/crud/apps/main/services/user-service.js +65 -0
  90. package/sample/crud/apps/main/views/auth/login.ejs +57 -0
  91. package/sample/crud/apps/main/views/auth/register.ejs +71 -0
  92. package/sample/crud/apps/main/views/cron/index.ejs +92 -0
  93. package/sample/crud/apps/main/views/guide/index.ejs +24 -0
  94. package/sample/crud/apps/main/views/guide/page.ejs +64 -0
  95. package/sample/crud/apps/main/views/home.ejs +82 -0
  96. package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
  97. package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
  98. package/sample/crud/apps/main/views/logs/index.ejs +80 -0
  99. package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
  100. package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
  101. package/sample/crud/apps/main/views/notes/list.ejs +74 -0
  102. package/sample/crud/apps/main/views/notes/new.ejs +45 -0
  103. package/sample/crud/apps/main/views/perf/index.ejs +90 -0
  104. package/sample/crud/apps/main/views/redis/index.ejs +65 -0
  105. package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
  106. package/sample/crud/apps/main/views/upload/index.ejs +79 -0
  107. package/sample/crud/apps/main/views/users/edit.ejs +48 -0
  108. package/sample/crud/apps/main/views/users/list.ejs +81 -0
  109. package/sample/crud/apps/main/views/users/new.ejs +48 -0
  110. package/sample/crud/apps/main/views/worker/index.ejs +70 -0
  111. package/sample/crud/apps/main/views/ws/index.ejs +62 -0
  112. package/sample/crud/apps/main/workers/hash-worker.js +17 -0
  113. package/sample/crud/apps/main/workers/hash.task.js +22 -0
  114. package/sample/crud/ecosystem.config.cjs +9 -0
  115. package/sample/crud/mega.config.js +105 -0
  116. package/sample/crud/package-lock.json +5665 -0
  117. package/sample/crud/package.json +28 -0
  118. package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
  119. package/sample/crud/test/apps/main/auth-service.test.js +93 -0
  120. package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
  121. package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
  122. package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
  123. package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
  124. package/sample/crud/test/apps/main/email-job.test.js +76 -0
  125. package/sample/crud/test/apps/main/guide-service.test.js +68 -0
  126. package/sample/crud/test/apps/main/hash-task.test.js +30 -0
  127. package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
  128. package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
  129. package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
  130. package/sample/crud/test/apps/main/note-service.test.js +68 -0
  131. package/sample/crud/test/apps/main/perf-service.test.js +121 -0
  132. package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
  133. package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
  134. package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
  135. package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
  136. package/sample/crud/test/apps/main/user-service.test.js +65 -0
  137. package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
  138. package/sample/crud/vitest.config.js +8 -0
  139. package/sample/crud/yarn.lock +2142 -0
  140. package/sample/simple/.env.example +15 -0
  141. package/sample/simple/README.md +52 -0
  142. package/sample/simple/apps/main/app.config.js +35 -0
  143. package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
  144. package/sample/simple/apps/main/locales/client/en.json +3 -0
  145. package/sample/simple/apps/main/locales/client/ko.json +3 -0
  146. package/sample/simple/apps/main/locales/server/en.json +23 -0
  147. package/sample/simple/apps/main/locales/server/ko.json +23 -0
  148. package/sample/simple/apps/main/public/css/app.css +101 -0
  149. package/sample/simple/apps/main/public/hello.txt +1 -0
  150. package/sample/simple/apps/main/public/js/app.js +54 -0
  151. package/sample/simple/apps/main/public/js/theme-init.js +12 -0
  152. package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  153. package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
  154. package/sample/simple/apps/main/routes/index.js +9 -0
  155. package/sample/simple/apps/main/routes/pages.js +12 -0
  156. package/sample/simple/apps/main/views/index.ejs +56 -0
  157. package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
  158. package/sample/simple/ecosystem.config.cjs +10 -0
  159. package/sample/simple/mega.config.js +27 -0
  160. package/sample/simple/package-lock.json +1851 -0
  161. package/sample/simple/package.json +25 -0
  162. package/sample/simple/test/apps/main/index.test.js +13 -0
  163. package/sample/simple/vitest.config.js +8 -0
  164. package/src/adapters/adapter-manager.js +305 -0
  165. package/src/adapters/adapter-options.js +208 -0
  166. package/src/adapters/file-adapter.js +350 -0
  167. package/src/adapters/file-session-adapter.js +363 -0
  168. package/src/adapters/index.js +38 -0
  169. package/src/adapters/maria-adapter.js +425 -0
  170. package/src/adapters/mega-adapter.js +511 -0
  171. package/src/adapters/mega-bus-adapter.js +81 -0
  172. package/src/adapters/mega-cache-adapter.js +94 -0
  173. package/src/adapters/mega-db-adapter.js +72 -0
  174. package/src/adapters/mega-lock-adapter.js +118 -0
  175. package/src/adapters/mega-log-sink-adapter.js +46 -0
  176. package/src/adapters/mega-session-adapter.js +72 -0
  177. package/src/adapters/mongo-adapter.js +396 -0
  178. package/src/adapters/nats-adapter.js +370 -0
  179. package/src/adapters/postgres-adapter.js +341 -0
  180. package/src/adapters/redis-adapter.js +331 -0
  181. package/src/adapters/redis-session-adapter.js +261 -0
  182. package/src/adapters/redlock-adapter.js +385 -0
  183. package/src/adapters/registry.js +157 -0
  184. package/src/adapters/sqlite-adapter.js +309 -0
  185. package/src/auth/index.js +103 -0
  186. package/src/cli/commands/console-cmd.js +56 -0
  187. package/src/cli/commands/new.js +101 -0
  188. package/src/cli/commands/routes.js +107 -0
  189. package/src/cli/commands/scaffold.js +120 -0
  190. package/src/cli/commands/test-cmd.js +45 -0
  191. package/src/cli/generators/index.js +368 -0
  192. package/src/cli/index.js +472 -0
  193. package/src/cli/template-engine.js +72 -0
  194. package/src/cli/ws-hub.js +582 -0
  195. package/src/core/ajv-mapper.js +80 -0
  196. package/src/core/boot.js +323 -0
  197. package/src/core/cluster-metrics.js +278 -0
  198. package/src/core/config-loader.js +115 -0
  199. package/src/core/config-validator.js +322 -0
  200. package/src/core/ctx-builder.js +253 -0
  201. package/src/core/envelope.js +88 -0
  202. package/src/core/error-mapper.js +116 -0
  203. package/src/core/formbody.js +69 -0
  204. package/src/core/hub-link.js +552 -0
  205. package/src/core/i18n.js +525 -0
  206. package/src/core/index.js +63 -0
  207. package/src/core/mega-app.js +1138 -0
  208. package/src/core/mega-cluster.js +232 -0
  209. package/src/core/mega-server.js +176 -0
  210. package/src/core/mega-service.js +41 -0
  211. package/src/core/migration-runner.js +196 -0
  212. package/src/core/multipart.js +282 -0
  213. package/src/core/openapi.js +114 -0
  214. package/src/core/router.js +388 -0
  215. package/src/core/routes-loader.js +57 -0
  216. package/src/core/scope-registry.js +53 -0
  217. package/src/core/security.js +275 -0
  218. package/src/core/services-loader.js +98 -0
  219. package/src/core/session-cleanup-schedule.js +57 -0
  220. package/src/core/session-store.js +55 -0
  221. package/src/core/session.js +414 -0
  222. package/src/core/static-assets.js +126 -0
  223. package/src/core/template.js +294 -0
  224. package/src/core/workers-manager.js +193 -0
  225. package/src/core/ws-compression.js +112 -0
  226. package/src/core/ws-controller.js +109 -0
  227. package/src/core/ws-message.js +176 -0
  228. package/src/core/ws-upgrade.js +445 -0
  229. package/src/errors/config-error.js +16 -0
  230. package/src/errors/http-errors.js +130 -0
  231. package/src/errors/index.js +19 -0
  232. package/src/errors/mega-error.js +34 -0
  233. package/src/eslint-plugin/index.js +15 -0
  234. package/src/eslint-plugin/no-direct-model-import.js +113 -0
  235. package/src/index.js +131 -0
  236. package/src/lib/asp/config.js +83 -0
  237. package/src/lib/asp/crypto.js +145 -0
  238. package/src/lib/asp/errors.js +49 -0
  239. package/src/lib/asp/nonce-cache.js +94 -0
  240. package/src/lib/asp/plugin.js +263 -0
  241. package/src/lib/asp/ws-terminator.js +101 -0
  242. package/src/lib/env-mapper.js +222 -0
  243. package/src/lib/hub-protocol.js +322 -0
  244. package/src/lib/index.js +42 -0
  245. package/src/lib/logger/telegram-core.js +150 -0
  246. package/src/lib/logger/telegram-transport.js +126 -0
  247. package/src/lib/mega-brute-force.js +225 -0
  248. package/src/lib/mega-circuit-breaker.js +412 -0
  249. package/src/lib/mega-cron.js +169 -0
  250. package/src/lib/mega-hash.js +179 -0
  251. package/src/lib/mega-health.js +91 -0
  252. package/src/lib/mega-job-queue.js +600 -0
  253. package/src/lib/mega-job-worker.js +295 -0
  254. package/src/lib/mega-job.js +140 -0
  255. package/src/lib/mega-logger.js +128 -0
  256. package/src/lib/mega-metrics.js +661 -0
  257. package/src/lib/mega-plugin.js +650 -0
  258. package/src/lib/mega-retry.js +95 -0
  259. package/src/lib/mega-schedule.js +507 -0
  260. package/src/lib/mega-shutdown.js +176 -0
  261. package/src/lib/mega-tracing.js +715 -0
  262. package/src/lib/mega-worker.js +653 -0
  263. package/src/lib/worker-runner/process-entry.js +30 -0
  264. package/src/lib/worker-runner/task-dispatch.js +72 -0
  265. package/src/lib/worker-runner/thread-entry.js +26 -0
  266. package/src/models/index.js +7 -0
  267. package/src/models/mega-model.js +151 -0
  268. package/src/test/index.js +288 -0
  269. package/templates/adapter/code.tpl +40 -0
  270. package/templates/adapter/test.tpl +13 -0
  271. package/templates/app/app.config.tpl +10 -0
  272. package/templates/app/route.tpl +10 -0
  273. package/templates/app/test.tpl +13 -0
  274. package/templates/channel/code.tpl +38 -0
  275. package/templates/channel/test.tpl +19 -0
  276. package/templates/controller/code.tpl +16 -0
  277. package/templates/controller/route.tpl +9 -0
  278. package/templates/controller/test.tpl +14 -0
  279. package/templates/job/code.tpl +23 -0
  280. package/templates/job/test.tpl +17 -0
  281. package/templates/locale/code.tpl +3 -0
  282. package/templates/locale/test.tpl +13 -0
  283. package/templates/middleware/code.tpl +13 -0
  284. package/templates/middleware/test.tpl +11 -0
  285. package/templates/migration/code.tpl +20 -0
  286. package/templates/migration/test.tpl +14 -0
  287. package/templates/model/code.tpl +21 -0
  288. package/templates/model/test.tpl +29 -0
  289. package/templates/project/app.config.tpl +8 -0
  290. package/templates/project/app.config.views.tpl +37 -0
  291. package/templates/project/ecosystem.config.tpl +10 -0
  292. package/templates/project/env.tpl +12 -0
  293. package/templates/project/gitignore.tpl +8 -0
  294. package/templates/project/locales/client/en.json.tpl +3 -0
  295. package/templates/project/locales/client/ko.json.tpl +3 -0
  296. package/templates/project/locales/server/en.json.tpl +17 -0
  297. package/templates/project/locales/server/ko.json.tpl +17 -0
  298. package/templates/project/mega.config.tpl +11 -0
  299. package/templates/project/package.tpl +25 -0
  300. package/templates/project/public/css/app.css +101 -0
  301. package/templates/project/public/js/app.js +54 -0
  302. package/templates/project/public/js/theme-init.js +12 -0
  303. package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  304. package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
  305. package/templates/project/readme.tpl +48 -0
  306. package/templates/project/route.test.tpl +13 -0
  307. package/templates/project/route.test.views.tpl +15 -0
  308. package/templates/project/route.tpl +10 -0
  309. package/templates/project/route.views.tpl +10 -0
  310. package/templates/project/views/index.ejs.tpl +58 -0
  311. package/templates/project/views/layout.ejs.tpl +73 -0
  312. package/templates/project/vitest.config.tpl +8 -0
  313. package/templates/route/code.tpl +11 -0
  314. package/templates/route/test.tpl +26 -0
  315. package/templates/schedule/code.tpl +19 -0
  316. package/templates/schedule/test.tpl +17 -0
  317. package/templates/service/code.tpl +18 -0
  318. package/templates/service/test.tpl +17 -0
  319. package/templates/worker/code.tpl +14 -0
  320. package/templates/worker/task.tpl +13 -0
  321. package/templates/worker/test.tpl +18 -0
  322. package/vitest.config.js +33 -0
@@ -0,0 +1,15 @@
1
+ // @ts-check
2
+ /**
3
+ * apps/main/routes/auth.js — 인증 라우트(자동 로딩, loadRoutes, ADR-155). 로그인/로그아웃/회원가입은
4
+ * `/admin/**` 보호 영역 **밖**에 둔다(보호 영역 안이면 비로그인 → 로그인 리다이렉트 루프). HTML 폼은
5
+ * GET/POST 만 쓰므로 로그아웃도 POST(+CSRF)다.
6
+ */
7
+ import { AuthController } from '../controllers/auth-controller.js'
8
+
9
+ export default (/** @type {any} */ router) => {
10
+ router.http.get('/auth/login', AuthController.loginForm)
11
+ router.http.post('/auth/login', AuthController.login)
12
+ router.http.post('/auth/logout', AuthController.logout)
13
+ router.http.get('/register', AuthController.registerForm)
14
+ router.http.post('/register', AuthController.register)
15
+ }
@@ -0,0 +1,14 @@
1
+ // @ts-check
2
+ /**
3
+ * apps/main/routes/cron.js — /demo/cron 스케줄러 데모 UI 라우트(자동 로딩, loadRoutes, ADR-157).
4
+ * `webRequireAuth`(ADR-155)로 보호한다(비로그인은 로그인 페이지로). 수동 실행은 HTML 폼이라 POST 다.
5
+ */
6
+ import { CronController } from '../controllers/cron-controller.js'
7
+ import { webRequireAuth } from '../middleware/web-auth.js'
8
+
9
+ export default (/** @type {any} */ router) => {
10
+ /** @type {{ before: Function[] }} 데모 UI 보호 가드(로그인 필요). */
11
+ const guarded = { before: [webRequireAuth] }
12
+ router.http.get('/demo/cron', CronController.index, guarded)
13
+ router.http.post('/demo/cron/run', CronController.runNow, guarded)
14
+ }
@@ -0,0 +1,25 @@
1
+ // @ts-check
2
+ /**
3
+ * apps/main/routes/guide.js — /guide 가이드 뷰어(MPA) 라우트(자동 로딩, loadRoutes, ADR-157).
4
+ * `webRequireAuth`(ADR-155)로 보호한다(비로그인은 로그인 페이지로). 둘 다 읽기 전용 GET 이다.
5
+ */
6
+ import { GuideController } from '../controllers/guide-controller.js'
7
+ import { webRequireAuth } from '../middleware/web-auth.js'
8
+
9
+ /**
10
+ * slug path 파라미터 스키마(ADR-019) — 라우터 AJV 가 형식을 1차 검증해 잘못된 slug 를 핸들러 진입 전에
11
+ * 400 으로 막는다(소문자·숫자로 시작, 이후 소문자·숫자·하이픈만). guide-service 의 화이트리스트·SLUG_RE
12
+ * 가드는 그대로 둔다 — 경로 조작 차단의 심층 방어이자, 형식은 맞지만 존재하지 않는 slug 의 404 처리를 맡는다.
13
+ */
14
+ const slugParams = {
15
+ type: 'object',
16
+ required: ['slug'],
17
+ properties: { slug: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*$', maxLength: 100 } },
18
+ }
19
+
20
+ export default (/** @type {any} */ router) => {
21
+ /** @type {{ before: Function[] }} 가이드 뷰어 보호 가드(로그인 필요). */
22
+ const guarded = { before: [webRequireAuth] }
23
+ router.http.get('/guide', GuideController.index, guarded)
24
+ router.http.get('/guide/:slug', GuideController.page, { ...guarded, schema: { params: slugParams } })
25
+ }
@@ -0,0 +1,14 @@
1
+ // @ts-check
2
+ /**
3
+ * apps/main/routes/jobs.js — /demo/jobs 잡 큐 데모 UI 라우트(자동 로딩, loadRoutes, ADR-157).
4
+ * `webRequireAuth`(ADR-155)로 보호한다. enqueue 는 HTML 폼이라 POST(CSRF 토큰 검증).
5
+ */
6
+ import { JobsController } from '../controllers/jobs-controller.js'
7
+ import { webRequireAuth } from '../middleware/web-auth.js'
8
+
9
+ export default (/** @type {any} */ router) => {
10
+ /** @type {{ before: Function[] }} 데모 UI 보호 가드(로그인 필요). */
11
+ const guarded = { before: [webRequireAuth] }
12
+ router.http.get('/demo/jobs', JobsController.index, guarded)
13
+ router.http.post('/demo/jobs/enqueue', JobsController.enqueue, guarded)
14
+ }
@@ -0,0 +1,28 @@
1
+ // @ts-check
2
+ /**
3
+ * apps/main/routes/logs.js — /demo/logs 구조적 로깅 데모 UI 라우트(자동 로딩, loadRoutes, ADR-157/163).
4
+ * `webRequireAuth`(ADR-155)로 보호한다. 로그 emit 은 HTML 폼이라 POST(PRG).
5
+ */
6
+ import { LogsController } from '../controllers/logs-controller.js'
7
+ import { webRequireAuth } from '../middleware/web-auth.js'
8
+
9
+ /**
10
+ * emit 폼 body 스키마(ADR-019) — 라우터 AJV 가 타입·길이를 검증한다. level 의 허용값(allowlist) 판정과
11
+ * 빈값 폴백은 logs-demo-service 가 도메인 규칙으로 계속 맡으므로(중복 금지) 여기선 타입만 본다. message 는
12
+ * 과대 페이로드(비치명적 본문) 차단용 상한만 둔다. CSRF 토큰(`_csrf`)이 같은 폼 body 로 오므로
13
+ * additionalProperties 는 닫지 않는다 — 글로벌 CSRF 가드(preHandler)가 검증 후 소비한다.
14
+ */
15
+ const emitBody = {
16
+ type: 'object',
17
+ properties: {
18
+ level: { type: 'string' },
19
+ message: { type: 'string', maxLength: 500 },
20
+ },
21
+ }
22
+
23
+ export default (/** @type {any} */ router) => {
24
+ /** @type {{ before: Function[] }} 데모 UI 보호 가드(로그인 필요). */
25
+ const guarded = { before: [webRequireAuth] }
26
+ router.http.get('/demo/logs', LogsController.index, guarded)
27
+ router.http.post('/demo/logs/emit', LogsController.emit, { ...guarded, schema: { body: emitBody } })
28
+ }
@@ -0,0 +1,13 @@
1
+ // @ts-check
2
+ /**
3
+ * apps/main/routes/metrics.js — /demo/metrics 관측 데모 UI 라우트(자동 로딩, loadRoutes, ADR-157/163).
4
+ * `webRequireAuth`(ADR-155)로 보호한다(비로그인은 로그인 페이지로).
5
+ */
6
+ import { MetricsController } from '../controllers/metrics-controller.js'
7
+ import { webRequireAuth } from '../middleware/web-auth.js'
8
+
9
+ export default (/** @type {any} */ router) => {
10
+ /** @type {{ before: Function[] }} 데모 UI 보호 가드(로그인 필요). */
11
+ const guarded = { before: [webRequireAuth] }
12
+ router.http.get('/demo/metrics', MetricsController.index, guarded)
13
+ }
@@ -0,0 +1,19 @@
1
+ // @ts-check
2
+ /**
3
+ * apps/main/routes/notes.js — mongo notes 데모 UI(MPA) 라우트(자동 로딩, loadRoutes, ADR-157).
4
+ * users 의 web.js 와 같은 규약 — HTML 폼은 GET/POST 만 쓰므로 수정/삭제도 POST 다. `/demo/notes`
5
+ * 네임스페이스 전체를 `webRequireAuth`(ADR-155)로 보호한다(비로그인은 로그인 페이지로 리다이렉트).
6
+ */
7
+ import { NoteController } from '../controllers/note-controller.js'
8
+ import { webRequireAuth } from '../middleware/web-auth.js'
9
+
10
+ export default (/** @type {any} */ router) => {
11
+ /** @type {{ before: Function[] }} 데모 UI 보호 가드(로그인 필요). */
12
+ const guarded = { before: [webRequireAuth] }
13
+ router.http.get('/demo/notes', NoteController.list, guarded)
14
+ router.http.get('/demo/notes/new', NoteController.newForm, guarded)
15
+ router.http.post('/demo/notes', NoteController.create, guarded)
16
+ router.http.get('/demo/notes/:id/edit', NoteController.editForm, guarded)
17
+ router.http.post('/demo/notes/:id', NoteController.update, guarded)
18
+ router.http.post('/demo/notes/:id/delete', NoteController.destroy, guarded)
19
+ }
@@ -0,0 +1,47 @@
1
+ // @ts-check
2
+ /**
3
+ * apps/main/routes/perf.js — /perf 성능 벤치마크 데모 UI + 실행 라우트(자동 로딩, loadRoutes, ADR-157).
4
+ * `webRequireAuth`(ADR-155)로 보호한다(비로그인은 로그인 페이지로). 실행(run)은 AJAX(JSON) 엔드포인트라
5
+ * CSRF 는 토큰 면제 + Origin 검증으로 통과한다(ADR-051). body 는 AJV 스키마로 검증한다(ADR-019).
6
+ */
7
+ import { PerfController } from '../controllers/perf-controller.js'
8
+ import { webRequireAuth } from '../middleware/web-auth.js'
9
+
10
+ /**
11
+ * POST /perf/run body 스키마(AJV). scenario enum 은 서비스 SCENARIO_LIMITS 키와 동일 집합. iterations 는
12
+ * 필수, concurrency/payloadSize 는 선택(미지정 시 서비스가 시나리오별 디폴트·clamp 적용). additionalProperties:
13
+ * false — JSON 요청이라 폼 `_csrf` 필드가 섞이지 않으므로 엄격하게 닫는다(per ADR-051 JSON 토큰 면제).
14
+ */
15
+ const runSchema = {
16
+ body: {
17
+ type: 'object',
18
+ required: ['scenario', 'iterations'],
19
+ additionalProperties: false,
20
+ properties: {
21
+ scenario: {
22
+ type: 'string',
23
+ enum: [
24
+ 'http.echo',
25
+ 'http.jsonSmall',
26
+ 'http.jsonLarge',
27
+ 'crypto.hash',
28
+ 'crypto.aspRoundtrip',
29
+ 'db.pg.insertSelect',
30
+ 'db.mongo.insertFind',
31
+ 'cache.redis.setGet',
32
+ 'session.createRead',
33
+ ],
34
+ },
35
+ iterations: { type: 'integer', minimum: 1, maximum: 100000 },
36
+ concurrency: { type: 'integer', minimum: 1, maximum: 100 },
37
+ payloadSize: { type: 'integer', minimum: 0, maximum: 1048576 },
38
+ },
39
+ },
40
+ }
41
+
42
+ export default (/** @type {any} */ router) => {
43
+ /** @type {{ before: Function[] }} 데모 UI 보호 가드(로그인 필요). */
44
+ const guarded = { before: [webRequireAuth] }
45
+ router.http.get('/perf', PerfController.index, guarded)
46
+ router.http.post('/perf/run', PerfController.run, { before: [webRequireAuth], schema: runSchema })
47
+ }
@@ -0,0 +1,14 @@
1
+ // @ts-check
2
+ /**
3
+ * apps/main/routes/redis.js — /demo/redis 데모 UI(MPA) 라우트(자동 로딩, loadRoutes, ADR-157).
4
+ * `webRequireAuth`(ADR-155)로 보호한다(비로그인은 로그인 페이지로). 캐시 비우기는 HTML 폼이라 POST 다.
5
+ */
6
+ import { RedisController } from '../controllers/redis-controller.js'
7
+ import { webRequireAuth } from '../middleware/web-auth.js'
8
+
9
+ export default (/** @type {any} */ router) => {
10
+ /** @type {{ before: Function[] }} 데모 UI 보호 가드(로그인 필요). */
11
+ const guarded = { before: [webRequireAuth] }
12
+ router.http.get('/demo/redis', RedisController.index, guarded)
13
+ router.http.post('/demo/redis/clear', RedisController.clearCache, guarded)
14
+ }
@@ -0,0 +1,14 @@
1
+ // @ts-check
2
+ /**
3
+ * apps/main/routes/tracing.js — /demo/tracing 분산 추적 데모 UI 라우트(자동 로딩, loadRoutes, ADR-157/163).
4
+ * `webRequireAuth`(ADR-155)로 보호한다. trace 생성은 HTML 폼이라 POST(PRG).
5
+ */
6
+ import { TracingController } from '../controllers/tracing-controller.js'
7
+ import { webRequireAuth } from '../middleware/web-auth.js'
8
+
9
+ export default (/** @type {any} */ router) => {
10
+ /** @type {{ before: Function[] }} 데모 UI 보호 가드(로그인 필요). */
11
+ const guarded = { before: [webRequireAuth] }
12
+ router.http.get('/demo/tracing', TracingController.index, guarded)
13
+ router.http.post('/demo/tracing/generate', TracingController.generate, guarded)
14
+ }
@@ -0,0 +1,16 @@
1
+ // @ts-check
2
+ /**
3
+ * apps/main/routes/upload.js — /demo/upload 파일 업로드 데모 UI 라우트(자동 로딩, loadRoutes, ADR-157/163).
4
+ * `webRequireAuth`(ADR-155)로 보호한다. 업로드는 multipart POST(fetch + csrf-token 헤더).
5
+ */
6
+ import { UploadController } from '../controllers/upload-controller.js'
7
+ import { webRequireAuth } from '../middleware/web-auth.js'
8
+
9
+ export default (/** @type {any} */ router) => {
10
+ /** @type {{ before: Function[] }} 데모 UI 보호 가드(로그인 필요). */
11
+ const guarded = { before: [webRequireAuth] }
12
+ router.http.get('/demo/upload', UploadController.index, guarded)
13
+ router.http.post('/demo/upload', UploadController.upload, guarded)
14
+ // 다운로드 — 소스에서 파일 읽어 전송(인증 필요). 가드를 떼면 공개 다운로드로 전환된다.
15
+ router.http.get('/demo/upload/file/:name', UploadController.download, guarded)
16
+ }
@@ -0,0 +1,54 @@
1
+ // @ts-check
2
+ /**
3
+ * apps/main/routes/users.js — users REST 라우트(자동 로딩, loadRoutes). 단일 시그니처
4
+ * `router.http.<method>(path, handler)`(ADR-074). 핸들러는 컨트롤러 정적 메서드 참조 — 라우트·컨트롤러는
5
+ * 모델을 직접 import 하지 않는다(ADR-022, `mega/no-direct-model-import`).
6
+ *
7
+ * JSON API 는 프레임워크 `requireAuth`(mega-framework/auth, ADR-143/155)로 보호한다 — 비로그인 호출은
8
+ * 401 `auth.required` envelope. 세션 쿠키(`mega.sid`)를 가진 요청만 통과한다(웹 UI 와 같은 세션).
9
+ */
10
+ import { requireAuth } from 'mega-framework/auth'
11
+ import { UserController } from '../controllers/user-controller.js'
12
+
13
+ /**
14
+ * users REST 라우트의 OpenAPI 메타·schema(ADR-070/140/163). openapi 메타(tags/summary/description)는 명세에
15
+ * 그대로 반영되고, body schema 는 명세 표시용으로 **비강제**(required 없음)라 기존 검증 동작을 바꾸지 않는다
16
+ * (필드 검증은 user-service 가 담당, ADR). path 파라미터(id)는 문자열로 문서화한다.
17
+ */
18
+ const idParams = { type: 'object', properties: { id: { type: 'string', description: '사용자 id' } } }
19
+ const userBody = {
20
+ type: 'object',
21
+ properties: {
22
+ name: { type: 'string', description: '사용자 이름' },
23
+ email: { type: 'string', format: 'email', description: '이메일' },
24
+ },
25
+ }
26
+
27
+ export default (/** @type {any} */ router) => {
28
+ /** @type {{ before: Function[] }} REST API 인증 가드(401 throw). */
29
+ const guarded = { before: [requireAuth] }
30
+ router.http.get('/users', UserController.index, {
31
+ ...guarded,
32
+ openapi: { tags: ['users'], summary: '사용자 목록', description: '모든 사용자를 반환한다.' },
33
+ })
34
+ router.http.get('/users/:id', UserController.show, {
35
+ ...guarded,
36
+ schema: { params: idParams },
37
+ openapi: { tags: ['users'], summary: '사용자 조회', description: 'id 로 단일 사용자를 반환한다.' },
38
+ })
39
+ router.http.post('/users', UserController.create, {
40
+ ...guarded,
41
+ schema: { body: userBody },
42
+ openapi: { tags: ['users'], summary: '사용자 생성', description: 'name·email 로 사용자를 생성한다.' },
43
+ })
44
+ router.http.put('/users/:id', UserController.update, {
45
+ ...guarded,
46
+ schema: { params: idParams, body: userBody },
47
+ openapi: { tags: ['users'], summary: '사용자 수정', description: 'id 사용자를 수정한다.' },
48
+ })
49
+ router.http.delete('/users/:id', UserController.destroy, {
50
+ ...guarded,
51
+ schema: { params: idParams },
52
+ openapi: { tags: ['users'], summary: '사용자 삭제', description: 'id 사용자를 삭제한다.' },
53
+ })
54
+ }
@@ -0,0 +1,23 @@
1
+ // @ts-check
2
+ /**
3
+ * apps/main/routes/web.js — 관리 UI(MPA) 라우트(자동 로딩, loadRoutes). JSON REST API(`/users`,
4
+ * routes/users.js)와 충돌하지 않도록 UI 는 `/admin/users` 네임스페이스를 쓴다. HTML 폼은 GET/POST 만
5
+ * 쓰므로 수정/삭제도 POST 다(브라우저 폼 제약 — PUT/DELETE 는 JSON API 쪽이 RESTful 하게 제공).
6
+ *
7
+ * `/admin/**` 는 `webRequireAuth`(ADR-155)로 보호한다 — 비로그인 접근은 로그인 페이지로 리다이렉트.
8
+ * 랜딩(`/`)은 공개라 가드를 걸지 않는다.
9
+ */
10
+ import { WebController } from '../controllers/web-controller.js'
11
+ import { webRequireAuth } from '../middleware/web-auth.js'
12
+
13
+ export default (/** @type {any} */ router) => {
14
+ /** @type {{ before: Function[] }} 관리 UI 보호 가드(로그인 필요). */
15
+ const guarded = { before: [webRequireAuth] }
16
+ router.http.get('/', WebController.home)
17
+ router.http.get('/admin/users', WebController.list, guarded)
18
+ router.http.get('/admin/users/new', WebController.newForm, guarded)
19
+ router.http.post('/admin/users', WebController.create, guarded)
20
+ router.http.get('/admin/users/:id/edit', WebController.editForm, guarded)
21
+ router.http.post('/admin/users/:id', WebController.update, guarded)
22
+ router.http.post('/admin/users/:id/delete', WebController.destroy, guarded)
23
+ }
@@ -0,0 +1,15 @@
1
+ // @ts-check
2
+ /**
3
+ * apps/main/routes/worker.js — /demo/worker CPU 워커 풀 데모 UI 라우트(자동 로딩, loadRoutes, ADR-157).
4
+ * `webRequireAuth`(ADR-155)로 보호한다. 실행(run)·하트비트(ping)는 AJAX(JSON) 엔드포인트다.
5
+ */
6
+ import { WorkerController } from '../controllers/worker-controller.js'
7
+ import { webRequireAuth } from '../middleware/web-auth.js'
8
+
9
+ export default (/** @type {any} */ router) => {
10
+ /** @type {{ before: Function[] }} 데모 UI 보호 가드(로그인 필요). */
11
+ const guarded = { before: [webRequireAuth] }
12
+ router.http.get('/demo/worker', WorkerController.index, guarded)
13
+ router.http.post('/demo/worker/run', WorkerController.run, guarded)
14
+ router.http.get('/demo/worker/ping', WorkerController.ping, guarded)
15
+ }
@@ -0,0 +1,30 @@
1
+ // @ts-check
2
+ /**
3
+ * apps/main/routes/ws.js — /demo/ws 채팅 데모 페이지 + /ws/chat WebSocket 엔드포인트(자동 로딩, ADR-158).
4
+ *
5
+ * 페이지(`/demo/ws`)는 webRequireAuth 로, WS upgrade(`/ws/chat`)는 makeWsRequireAuth(세션 직접 확인)로
6
+ * 보호한다 — 둘 다 로그인 필요. WS 채널 메시지 `chat.send` 의 payload(text)는 schemas 로 사전 검증된다(M2).
7
+ */
8
+ import { WsController } from '../controllers/ws-controller.js'
9
+ import { ChatChannel } from '../channels/chat-channel.js'
10
+ import { webRequireAuth } from '../middleware/web-auth.js'
11
+ import { makeWsRequireAuth } from '../middleware/ws-auth.js'
12
+
13
+ /** chat.send payload 스키마 — text 1~500자만 허용(빈 메시지·과대 페이로드 차단). */
14
+ const CHAT_SEND_SCHEMA = {
15
+ type: 'object',
16
+ required: ['text'],
17
+ properties: { text: { type: 'string', minLength: 1, maxLength: 500 } },
18
+ additionalProperties: false,
19
+ }
20
+
21
+ export default (/** @type {any} */ router) => {
22
+ // 데모 셸 페이지(MPA) — 로그인 가드(비로그인은 로그인 페이지로).
23
+ router.http.get('/demo/ws', WsController.index, { before: [webRequireAuth] })
24
+
25
+ // 실 WS 엔드포인트 — upgrade 시 세션 인증(makeWsRequireAuth). ns=/ws/chat 는 ASP E: 로 종단(app.config asp).
26
+ router.ws('/ws/chat', ChatChannel, {
27
+ before: [makeWsRequireAuth(router.app)],
28
+ schemas: { 'chat.send': CHAT_SEND_SCHEMA },
29
+ })
30
+ }
@@ -0,0 +1,30 @@
1
+ // @ts-check
2
+ import { MegaSchedule } from 'mega-framework'
3
+ import { CronDemoService } from '../services/cron-demo-service.js'
4
+
5
+ /**
6
+ * CronCounterSchedule — /demo/cron 정기 작업(ADR-028/118). `mega scheduler` 프로세스가 실행한다(config.schedules).
7
+ *
8
+ * 30초마다 'demo' 캐시(redis)의 누적 카운터를 1 올리고 실행 이력을 남긴다(CronDemoService.tick). 웹 페이지는
9
+ * 같은 redis 를 읽어 실행 이력과 다음 실행 시각을 보여준다.
10
+ *
11
+ * # 클러스터 중복방지 (leader election)
12
+ * 같은 스케줄을 scheduler 프로세스 여러 개에 띄우면(ecosystem 의 instances>1) 한 tick 에 양쪽이 동시에
13
+ * 카운터를 올려 **중복 실행**된다. `static lock` 을 선언하면 MegaScheduler 가 실행 직전 분산 락(redlock)을
14
+ * 딱 1개만 잡게 해(retryCount:0 — 못 잡으면 즉시 skip), tick 당 한 인스턴스만 실행한다(ADR-118/113).
15
+ * ttl(ms)은 tick(원자적 INCR, 수 ms) 소요보다 넉넉하면서 다음 주기(30초)보다는 짧게 둬, 락 해제가 실패해도
16
+ * 다음 주기엔 자동 만료돼 재경쟁한다.
17
+ */
18
+ export class CronCounterSchedule extends MegaSchedule {
19
+ static cron = CronDemoService.CRON_EXPR
20
+ static timezone = CronDemoService.TIMEZONE
21
+ static lock = { lock: 'main', ttl: 20_000 }
22
+
23
+ /**
24
+ * @param {Record<string, any>} ctx - scheduler 프로세스 컨텍스트(ctx.cache('demo') 글로벌 키).
25
+ * @returns {Promise<void>}
26
+ */
27
+ async run(ctx) {
28
+ await new CronDemoService(ctx).tick('schedule')
29
+ }
30
+ }
@@ -0,0 +1,74 @@
1
+ // @ts-check
2
+ import { MegaService } from 'mega-framework'
3
+ import { MegaHash } from 'mega-framework'
4
+ import { MegaValidationError, MegaConflictError } from 'mega-framework/errors'
5
+ import { User } from '../models/user.js'
6
+
7
+ /**
8
+ * AuthService — 회원가입·로그인 검증 로직(ADR-155). 파일명 `auth-service.js` → 자동 DI 이름 `auth`
9
+ * (ctx.services.auth, ADR-148). 비밀번호는 scrypt(MegaHash, ADR-130)로 해시해 저장하고, 검증은 상수
10
+ * 시간 비교로 한다 — 평문은 어디에도 저장·로그하지 않는다.
11
+ *
12
+ * brute-force(반복 시도 잠금)는 이 서비스가 아니라 컨트롤러가 `ctx.bruteForce`(redis, ADR-049/130)로
13
+ * 다룬다 — 잠금은 HTTP 요청(IP)·세션 흐름에 묶여 있어 컨트롤러 책임이기 때문이다.
14
+ */
15
+ export class AuthService extends MegaService {
16
+ /** 비밀번호 최소 길이(OWASP Password Storage Cheat Sheet — 최소 8자 권장). */
17
+ static MIN_PASSWORD = 8
18
+
19
+ /**
20
+ * 새 계정을 만든다. name·email·password 를 검증하고 비밀번호를 해시해 저장한다.
21
+ * @param {{ name?: unknown, email?: unknown, password?: unknown }} input
22
+ * @returns {Promise<{ id: number, name: string, email: string, created_at: string }>}
23
+ * @throws {MegaValidationError} `auth.invalid` - 필수값 누락 또는 비밀번호가 너무 짧음.
24
+ * @throws {MegaConflictError} `user.email_taken` - 이메일 중복.
25
+ */
26
+ async register(input) {
27
+ const name = typeof input?.name === 'string' ? input.name.trim() : ''
28
+ const email = typeof input?.email === 'string' ? input.email.trim().toLowerCase() : ''
29
+ const password = typeof input?.password === 'string' ? input.password : ''
30
+ if (!name || !email) {
31
+ throw new MegaValidationError('auth.invalid', 'name and email are required', { details: { name: !!name, email: !!email, password: password.length >= AuthService.MIN_PASSWORD } })
32
+ }
33
+ if (password.length < AuthService.MIN_PASSWORD) {
34
+ throw new MegaValidationError('auth.invalid', `password must be at least ${AuthService.MIN_PASSWORD} characters`, { details: { name: !!name, email: !!email, password: false } })
35
+ }
36
+ const passwordHash = await MegaHash.password.hash(password)
37
+ this.log.debug?.({ email }, 'auth.register')
38
+ try {
39
+ return await User.register({ name, email, passwordHash })
40
+ } catch (err) {
41
+ // 이메일 중복(postgres unique_violation 23505)은 도메인 충돌(409)로 매핑(P4 — 명시 처리 후 throw).
42
+ if (/** @type {any} */ (err)?.code === '23505') {
43
+ throw new MegaConflictError('user.email_taken', `email '${email}' already exists`, { details: { email }, cause: err })
44
+ }
45
+ throw err
46
+ }
47
+ }
48
+
49
+ /**
50
+ * 이메일+비밀번호를 검증한다. 일치하면 마지막 로그인 시각을 갱신하고 사용자 식별 정보를 돌려준다.
51
+ * 실패(없는 이메일·비밀번호 미설정 계정·불일치)는 **이유를 구분하지 않고** null 을 돌려준다
52
+ * (user enumeration 방지 — 어느 쪽이 틀렸는지 노출하지 않는다, OWASP Authentication Cheat Sheet).
53
+ * @param {{ email?: unknown, password?: unknown }} input
54
+ * @returns {Promise<{ id: number, name: string } | null>}
55
+ */
56
+ async authenticate(input) {
57
+ const email = typeof input?.email === 'string' ? input.email.trim().toLowerCase() : ''
58
+ const password = typeof input?.password === 'string' ? input.password : ''
59
+ if (!email || !password) return null
60
+ const row = await User.findByEmailWithHash(email)
61
+ this.log.debug?.({ email, found: row !== null }, 'auth.authenticate lookup')
62
+ if (row === null || typeof row.password_hash !== 'string') {
63
+ // 없는 이메일이거나 비밀번호 미설정(admin CRUD 로 만든 계정) — 둘 다 로그인 불가.
64
+ return null
65
+ }
66
+ const ok = await MegaHash.password.verify(password, row.password_hash)
67
+ if (!ok) {
68
+ this.log.debug?.({ email }, 'auth.authenticate mismatch')
69
+ return null
70
+ }
71
+ await User.touchLastLogin(row.id)
72
+ return { id: row.id, name: row.name }
73
+ }
74
+ }
@@ -0,0 +1,66 @@
1
+ // @ts-check
2
+ import { MegaService, MegaCron } from 'mega-framework'
3
+
4
+ /**
5
+ * CronDemoService — /demo/cron 스케줄러 데모 로직(ADR-028/118). 파일명 `cron-demo-service.js` → 자동 DI
6
+ * 이름 `cronDemo`(ctx.services.cronDemo, ADR-148).
7
+ *
8
+ * `CronCounterSchedule`(mega scheduler 프로세스)과 웹 수동 실행 버튼(mega start 프로세스)이 **같은 tick()
9
+ * 로직**을 공유한다 — 둘 다 'demo' 캐시(redis db1)에 원자적 카운터(INCR)를 올리고 최근 실행 이력을 redis
10
+ * LIST(LPUSH + LTRIM)에 남긴다. 두 프로세스가 같은 redis 를 보므로 스케줄 자동 실행과 수동 실행이 한 화면에
11
+ * 함께 쌓인다. 다음 실행 시각은 croner 래퍼(MegaCron)로 계산한다(타이머 없는 순수 계산).
12
+ *
13
+ * 키는 'demo' 캐시 안에서 `demo:cron:*` 네임스페이스로 두어 /demo/redis 의 `demo:redis:*` 와 분리한다.
14
+ */
15
+ export class CronDemoService extends MegaService {
16
+ /** cron 표현식(6필드 = 초 분 시 일 월 요일) — 30초마다. CronCounterSchedule 과 공유하는 정본. */
17
+ static CRON_EXPR = '*/30 * * * * *'
18
+ /** IANA 타임존 — 다음 실행 시각 계산·표시에 쓴다. */
19
+ static TIMEZONE = 'Asia/Seoul'
20
+ /** 누적 실행 카운터 키(원자적 INCR). */
21
+ static COUNT_KEY = 'demo:cron:count'
22
+ /** 최근 실행 이력 LIST 키(LPUSH 머리쪽 삽입 → 최신이 앞). */
23
+ static HISTORY_KEY = 'demo:cron:history'
24
+ /** 이력 보관 최대 건수(LTRIM 으로 유지). */
25
+ static HISTORY_MAX = 10
26
+
27
+ /**
28
+ * 실행 1회 기록 — 누적 카운터를 원자적으로 1 올리고, 실행 이력 1건을 LIST 머리에 넣은 뒤 최근 N건만 남긴다.
29
+ * 스케줄 자동 실행(source='schedule')과 웹 수동 실행(source='manual')이 같은 경로를 쓴다.
30
+ * @param {'schedule'|'manual'} source - 실행 출처(이력에 함께 저장 — 화면에서 구분 표시).
31
+ * @returns {Promise<{ count: number, at: string, source: 'schedule'|'manual' }>}
32
+ */
33
+ async tick(source) {
34
+ const redis = this.ctx.cache('demo').native
35
+ const count = await redis.incr(CronDemoService.COUNT_KEY)
36
+ const at = new Date().toISOString()
37
+ await redis.lpush(CronDemoService.HISTORY_KEY, JSON.stringify({ at, count, source }))
38
+ // 머리쪽(0)부터 N-1 까지만 남기고 나머지(오래된 꼬리)는 버린다 — 무한 적재 방지.
39
+ await redis.ltrim(CronDemoService.HISTORY_KEY, 0, CronDemoService.HISTORY_MAX - 1)
40
+ this.ctx.log?.debug?.({ count, source }, 'cron-demo.tick')
41
+ return { count, at, source }
42
+ }
43
+
44
+ /**
45
+ * 화면 렌더용 스냅샷 — 누적 카운터 + 최근 실행 이력(최신순) + 다음 실행 시각 N개.
46
+ * @returns {Promise<{ count: number, history: Array<{ at: string, count: number, source: string }>, nextRuns: Date[], cron: string, timezone: string }>}
47
+ */
48
+ async snapshot() {
49
+ const redis = this.ctx.cache('demo').native
50
+ const rawCount = await redis.get(CronDemoService.COUNT_KEY)
51
+ const count = rawCount === null || rawCount === undefined ? 0 : Number(rawCount)
52
+ const rawList = await redis.lrange(CronDemoService.HISTORY_KEY, 0, CronDemoService.HISTORY_MAX - 1)
53
+ const history = rawList.map((/** @type {string} */ s) => JSON.parse(s))
54
+ // 다음 5개 발생 시각(타이머 미가동 순수 계산). 표현식이 무효면 register 단계에서 이미 throw 됐을 것.
55
+ const nextRuns = MegaCron.nextRuns(CronDemoService.CRON_EXPR, 5, undefined, {
56
+ timezone: CronDemoService.TIMEZONE,
57
+ })
58
+ return {
59
+ count,
60
+ history,
61
+ nextRuns,
62
+ cron: CronDemoService.CRON_EXPR,
63
+ timezone: CronDemoService.TIMEZONE,
64
+ }
65
+ }
66
+ }