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,101 @@
1
+ /*
2
+ * app.css — MEGA-FRAMEWORK 브랜드 소폭 커스텀(Bootstrap 5.3 위에 얹는다).
3
+ * Bootstrap 컴포넌트는 인스턴스 CSS 변수(--bs-btn-*, --bs-link-* 등)로 색을 잡으므로
4
+ * Sass 재컴파일 없이 변수만 덮어써 브랜드 색을 입힌다. per Bootstrap 5.3 CSS variables 문서.
5
+ */
6
+
7
+ :root {
8
+ --brand: #5b4bff;
9
+ --brand-rgb: 91, 75, 255;
10
+ --brand-dark: #4a3fd6;
11
+ --brand-light: #efedff;
12
+ }
13
+
14
+ /* 링크·primary 강조를 브랜드 색으로(라이트/다크 공통) */
15
+ a {
16
+ --bs-link-color-rgb: var(--brand-rgb);
17
+ --bs-link-hover-color-rgb: 74, 63, 214;
18
+ }
19
+
20
+ .btn-primary {
21
+ --bs-btn-bg: var(--brand);
22
+ --bs-btn-border-color: var(--brand);
23
+ --bs-btn-hover-bg: var(--brand-dark);
24
+ --bs-btn-hover-border-color: var(--brand-dark);
25
+ --bs-btn-active-bg: var(--brand-dark);
26
+ --bs-btn-active-border-color: var(--brand-dark);
27
+ --bs-btn-disabled-bg: var(--brand);
28
+ --bs-btn-disabled-border-color: var(--brand);
29
+ }
30
+
31
+ .btn-outline-primary {
32
+ --bs-btn-color: var(--brand);
33
+ --bs-btn-border-color: var(--brand);
34
+ --bs-btn-hover-bg: var(--brand);
35
+ --bs-btn-hover-border-color: var(--brand);
36
+ --bs-btn-active-bg: var(--brand);
37
+ --bs-btn-active-border-color: var(--brand);
38
+ }
39
+
40
+ .text-brand {
41
+ color: var(--brand) !important;
42
+ }
43
+
44
+ .navbar-brand {
45
+ font-weight: 700;
46
+ letter-spacing: -0.02em;
47
+ }
48
+
49
+ .navbar-brand .brand-dot {
50
+ color: var(--brand);
51
+ }
52
+
53
+ /* hero — 브랜드 그라데이션 배경 + 넉넉한 여백 */
54
+ .hero {
55
+ background: radial-gradient(120% 120% at 0% 0%, rgba(var(--brand-rgb), 0.12), transparent 60%),
56
+ radial-gradient(120% 120% at 100% 0%, rgba(var(--brand-rgb), 0.08), transparent 55%);
57
+ border-radius: 1rem;
58
+ }
59
+
60
+ .hero h1 {
61
+ letter-spacing: -0.03em;
62
+ }
63
+
64
+ /* feature 카드 — hover 살짝 떠오르는 인터랙션 */
65
+ .feature-card {
66
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
67
+ }
68
+
69
+ .feature-card:hover {
70
+ transform: translateY(-3px);
71
+ box-shadow: 0 0.75rem 1.5rem rgba(var(--brand-rgb), 0.12);
72
+ }
73
+
74
+ .feature-icon {
75
+ display: inline-flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ width: 3rem;
79
+ height: 3rem;
80
+ border-radius: 0.75rem;
81
+ background: var(--brand-light);
82
+ color: var(--brand);
83
+ font-size: 1.5rem;
84
+ }
85
+
86
+ [data-bs-theme='dark'] .feature-icon {
87
+ background: rgba(var(--brand-rgb), 0.18);
88
+ }
89
+
90
+ footer.site-footer {
91
+ border-top: 1px solid var(--bs-border-color);
92
+ }
93
+
94
+ /* 테마 토글 아이콘 — 현재 테마에서 "전환 대상"을 보여준다(라이트면 🌙, 다크면 ☀️). */
95
+ [data-bs-theme='light'] .theme-icon-light {
96
+ display: none;
97
+ }
98
+
99
+ [data-bs-theme='dark'] .theme-icon-dark {
100
+ display: none;
101
+ }
@@ -0,0 +1,137 @@
1
+ /* 가이드 뷰어 전용 스타일 — 좌측 목차 사이드바 + 서버사이드 렌더된 마크다운 본문. */
2
+
3
+ /* --- 사이드바(목차) --- */
4
+ .guide-sidebar {
5
+ position: sticky;
6
+ top: 1.5rem;
7
+ }
8
+
9
+ .guide-toc .nav-link {
10
+ padding: 0.25rem 0.5rem;
11
+ border-radius: 0.375rem;
12
+ color: var(--bs-secondary-color);
13
+ font-size: 0.9rem;
14
+ }
15
+
16
+ .guide-toc .nav-link:hover {
17
+ background: var(--bs-secondary-bg);
18
+ color: var(--bs-body-color);
19
+ }
20
+
21
+ .guide-toc .nav-link.active {
22
+ background: var(--bs-primary-bg-subtle);
23
+ color: var(--bs-primary-text-emphasis);
24
+ font-weight: 600;
25
+ }
26
+
27
+ /* --- 마크다운 본문 --- */
28
+ .guide-content {
29
+ line-height: 1.75;
30
+ word-break: break-word;
31
+ }
32
+
33
+ /* 앵커 점프 시 헤딩이 화면 맨 위에 딱 붙지 않게 여유를 둔다(목차 링크 가독성). */
34
+ .guide-content :is(h1, h2, h3, h4) {
35
+ scroll-margin-top: 1.5rem;
36
+ font-weight: 600;
37
+ line-height: 1.3;
38
+ }
39
+
40
+ .guide-content h1 {
41
+ font-size: 1.9rem;
42
+ margin-bottom: 1rem;
43
+ }
44
+
45
+ .guide-content h2 {
46
+ font-size: 1.5rem;
47
+ margin-top: 2.25rem;
48
+ margin-bottom: 0.85rem;
49
+ padding-bottom: 0.3rem;
50
+ border-bottom: 1px solid var(--bs-border-color);
51
+ }
52
+
53
+ .guide-content h3 {
54
+ font-size: 1.2rem;
55
+ margin-top: 1.75rem;
56
+ margin-bottom: 0.6rem;
57
+ }
58
+
59
+ .guide-content h4 {
60
+ font-size: 1.05rem;
61
+ margin-top: 1.25rem;
62
+ margin-bottom: 0.5rem;
63
+ }
64
+
65
+ .guide-content > p:first-child > strong:only-child,
66
+ .guide-content blockquote {
67
+ color: var(--bs-secondary-color);
68
+ }
69
+
70
+ .guide-content blockquote {
71
+ margin: 1rem 0;
72
+ padding: 0.25rem 1rem;
73
+ border-left: 4px solid var(--bs-border-color);
74
+ }
75
+
76
+ .guide-content blockquote > :last-child {
77
+ margin-bottom: 0;
78
+ }
79
+
80
+ /* 인라인 코드 — 배경 칩. 코드블록(pre code)은 highlight.js 테마가 색을 입히므로 칩 스타일을 빼준다. */
81
+ .guide-content :not(pre) > code {
82
+ padding: 0.15em 0.4em;
83
+ font-size: 0.875em;
84
+ background: var(--bs-secondary-bg);
85
+ border-radius: 0.3rem;
86
+ }
87
+
88
+ .guide-content pre {
89
+ margin: 1rem 0;
90
+ border: 1px solid var(--bs-border-color);
91
+ border-radius: 0.5rem;
92
+ overflow: hidden;
93
+ }
94
+
95
+ .guide-content pre code.hljs {
96
+ border-radius: 0.5rem;
97
+ font-size: 0.85rem;
98
+ line-height: 1.55;
99
+ }
100
+
101
+ /* 표 — Bootstrap 의 .table 클래스를 못 붙이므로 마크다운 표에 직접 스타일을 준다. */
102
+ .guide-content table {
103
+ width: 100%;
104
+ margin: 1rem 0;
105
+ border-collapse: collapse;
106
+ font-size: 0.9rem;
107
+ }
108
+
109
+ .guide-content th,
110
+ .guide-content td {
111
+ padding: 0.5rem 0.75rem;
112
+ border: 1px solid var(--bs-border-color);
113
+ text-align: left;
114
+ vertical-align: top;
115
+ }
116
+
117
+ .guide-content thead th {
118
+ background: var(--bs-secondary-bg);
119
+ }
120
+
121
+ .guide-content img {
122
+ max-width: 100%;
123
+ height: auto;
124
+ }
125
+
126
+ .guide-content ul,
127
+ .guide-content ol {
128
+ padding-left: 1.5rem;
129
+ }
130
+
131
+ .guide-content li + li {
132
+ margin-top: 0.25rem;
133
+ }
134
+
135
+ .guide-content hr {
136
+ margin: 2rem 0;
137
+ }
@@ -0,0 +1,54 @@
1
+ /*
2
+ * app.js — 다크모드 토글 + 언어 토글 + 삭제 확인 모달 wiring(클라이언트 동작).
3
+ *
4
+ * helmet CSP(script-src 'self')가 인라인 스크립트를 막으므로 모든 클라 동작은 외부 파일로 둔다(ADR-153).
5
+ * 다크모드: 페인트 전 적용은 theme-init.js(<head>), 토글은 여기서. localStorage('mega.theme') 저장.
6
+ * 언어: 프레임워크가 locale 을 쿠키(mega.lang)로만 감지하므로(ADR-038), 토글은 쿠키를 굽고 새로고침.
7
+ * 삭제모달: 트리거 버튼의 data-action/data-name 으로 form action·문구를 채운다(#deleteModal 있을 때만).
8
+ */
9
+ ;(function () {
10
+ 'use strict'
11
+
12
+ /** @param {string} theme */
13
+ function applyTheme(theme) {
14
+ document.documentElement.setAttribute('data-bs-theme', theme)
15
+ try {
16
+ localStorage.setItem('mega.theme', theme)
17
+ } catch (e) {
18
+ // localStorage 차단(사생활 모드 등)은 비치명적 — 이번 페이지에서만 테마가 안 남을 뿐.
19
+ console.debug('theme persist skipped:', e && e.message)
20
+ }
21
+ }
22
+
23
+ document.querySelectorAll('[data-theme-toggle]').forEach(function (btn) {
24
+ btn.addEventListener('click', function () {
25
+ var current = document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light'
26
+ applyTheme(current === 'dark' ? 'light' : 'dark')
27
+ })
28
+ })
29
+
30
+ document.querySelectorAll('[data-lang]').forEach(function (el) {
31
+ el.addEventListener('click', function (e) {
32
+ e.preventDefault()
33
+ var lang = el.getAttribute('data-lang')
34
+ // 1년 유지, 같은 사이트 요청에만 전송(samesite=lax).
35
+ document.cookie = 'mega.lang=' + encodeURIComponent(lang) + '; path=/; max-age=31536000; samesite=lax'
36
+ location.reload()
37
+ })
38
+ })
39
+
40
+ // 삭제 확인 모달 — 클릭된 행 버튼의 data-action(삭제 URL)·data-name 으로 form·문구를 채운다.
41
+ // #deleteModal 이 있는 페이지(예: 목록)에서만 동작하고, 없으면 no-op.
42
+ var deleteModal = document.getElementById('deleteModal')
43
+ if (deleteModal) {
44
+ deleteModal.addEventListener('show.bs.modal', function (event) {
45
+ var btn = event.relatedTarget
46
+ if (!btn) return
47
+ var form = document.getElementById('deleteForm')
48
+ var action = btn.getAttribute('data-action')
49
+ if (form && action) form.setAttribute('action', action)
50
+ var nameEl = document.getElementById('deleteModalName')
51
+ if (nameEl) nameEl.textContent = btn.getAttribute('data-name') || ''
52
+ })
53
+ }
54
+ })()
@@ -0,0 +1,129 @@
1
+ // perf.js — /perf 벤치마크 UI 클라이언트. CSP(script-src 'self')라 외부 파일·인라인 핸들러 없음.
2
+ // POST /perf/run 은 JSON 요청 → CSRF 토큰 면제(Origin 검증, ADR-051)라 토큰 헤더 불필요. 응답 envelope
3
+ // { ok, data, meta } 에서 data 를 꺼내 결과 표에 한 행씩 누적한다(페이지 reload 시 초기화).
4
+ (function () {
5
+ 'use strict'
6
+
7
+ /** @param {string} id @returns {HTMLElement} */
8
+ function $(id) {
9
+ var el = document.getElementById(id)
10
+ if (!el) throw new Error('missing element #' + id)
11
+ return el
12
+ }
13
+
14
+ var runBtn = $('pf-run')
15
+ var clearBtn = $('pf-clear')
16
+ var spinner = $('pf-spinner')
17
+ var errorBox = $('pf-error')
18
+ var errorDetail = $('pf-error-detail')
19
+ var resultsBody = $('pf-results')
20
+
21
+ /** 텍스트 셀 생성(textContent — XSS 방지). @param {string|number} text @param {boolean} mono @returns {HTMLTableCellElement} */
22
+ function cell(text, mono) {
23
+ var td = document.createElement('td')
24
+ td.textContent = String(text)
25
+ td.className = mono ? 'text-end font-monospace' : 'text-end'
26
+ return td
27
+ }
28
+
29
+ /** 숫자 천단위 콤마. @param {number} v @returns {string} */
30
+ function n(v) {
31
+ return Number(v).toLocaleString('en-US')
32
+ }
33
+
34
+ /** 결과 1건을 표 맨 위에 추가. @param {object} d - 응답 data. */
35
+ function addRow(d) {
36
+ var empty = document.getElementById('pf-empty')
37
+ if (empty) empty.remove()
38
+
39
+ var tr = document.createElement('tr')
40
+
41
+ var nameTd = document.createElement('td')
42
+ nameTd.className = 'font-monospace'
43
+ nameTd.textContent = d.scenario
44
+ if (d.clamped) {
45
+ // 시나리오 상한으로 반복/동시성이 줄어든 경우 배지로 드러낸다(조용한 절삭 아님).
46
+ var badge = document.createElement('span')
47
+ badge.className = 'badge text-bg-warning ms-1'
48
+ badge.textContent = 'clamped'
49
+ var parts = []
50
+ if (d.clamped.iterations) parts.push('iter ' + d.clamped.iterations.requested + '→' + d.clamped.iterations.applied)
51
+ if (d.clamped.concurrency) parts.push('conc ' + d.clamped.concurrency.requested + '→' + d.clamped.concurrency.applied)
52
+ badge.title = parts.join(', ')
53
+ nameTd.appendChild(badge)
54
+ }
55
+ tr.appendChild(nameTd)
56
+
57
+ tr.appendChild(cell(n(d.iterations), false))
58
+ tr.appendChild(cell(n(d.concurrency), false))
59
+ tr.appendChild(cell(n(d.durationMs), true))
60
+ tr.appendChild(cell(n(d.rps), true))
61
+ tr.appendChild(cell(d.avg, true))
62
+ tr.appendChild(cell(d.p50, true))
63
+ tr.appendChild(cell(d.p90, true))
64
+ tr.appendChild(cell(d.p95, true))
65
+ tr.appendChild(cell(d.p99, true))
66
+ tr.appendChild(cell(d.min, true))
67
+ tr.appendChild(cell(d.max, true))
68
+ tr.appendChild(cell(d.ok + '/' + d.fail, true))
69
+
70
+ resultsBody.insertBefore(tr, resultsBody.firstChild)
71
+ }
72
+
73
+ /** 폼 입력을 요청 body 로. 빈 concurrency/payload 는 보내지 않음(서비스 디폴트 적용). @returns {object} */
74
+ function readForm() {
75
+ var body = {
76
+ scenario: $('pf-scenario').value,
77
+ iterations: Number($('pf-iterations').value),
78
+ }
79
+ var conc = $('pf-concurrency').value
80
+ var payload = $('pf-payload').value
81
+ if (conc !== '') body.concurrency = Number(conc)
82
+ if (payload !== '') body.payloadSize = Number(payload)
83
+ return body
84
+ }
85
+
86
+ runBtn.addEventListener('click', function () {
87
+ runBtn.disabled = true
88
+ spinner.classList.remove('d-none')
89
+ errorBox.classList.add('d-none')
90
+
91
+ fetch('/perf/run', {
92
+ method: 'POST',
93
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
94
+ body: JSON.stringify(readForm()),
95
+ })
96
+ .then(function (res) {
97
+ return res.json().then(function (body) {
98
+ if (!res.ok || !body.ok) {
99
+ var msg = body && body.error ? body.error.code + ': ' + body.error.message : 'run ' + res.status
100
+ throw new Error(msg)
101
+ }
102
+ return body.data
103
+ })
104
+ })
105
+ .then(function (data) {
106
+ addRow(data)
107
+ })
108
+ .catch(function (err) {
109
+ errorDetail.textContent = String(err && err.message ? err.message : err)
110
+ errorBox.classList.remove('d-none')
111
+ })
112
+ .finally(function () {
113
+ runBtn.disabled = false
114
+ spinner.classList.add('d-none')
115
+ })
116
+ })
117
+
118
+ clearBtn.addEventListener('click', function () {
119
+ resultsBody.innerHTML = ''
120
+ var tr = document.createElement('tr')
121
+ tr.id = 'pf-empty'
122
+ var td = document.createElement('td')
123
+ td.colSpan = 13
124
+ td.className = 'text-center text-body-secondary py-3'
125
+ td.textContent = resultsBody.getAttribute('data-empty') || ''
126
+ tr.appendChild(td)
127
+ resultsBody.appendChild(tr)
128
+ })
129
+ })()
@@ -0,0 +1,12 @@
1
+ /*
2
+ * theme-init.js — 저장된 테마를 페인트 전에 적용(FOUC 방지). layout <head> 에서 동기 로드한다.
3
+ * helmet CSP(script-src 'self')가 인라인 스크립트를 막으므로 외부 파일로 둔다(ADR-153).
4
+ */
5
+ ;(function () {
6
+ try {
7
+ var t = localStorage.getItem('mega.theme')
8
+ if (t) document.documentElement.setAttribute('data-bs-theme', t)
9
+ } catch (e) {
10
+ // localStorage 차단(사생활 모드 등)은 비치명적 — 기본 테마로 렌더된다.
11
+ }
12
+ })()
@@ -0,0 +1,63 @@
1
+ // @ts-check
2
+ /*
3
+ * /demo/upload 클라이언트 — 파일 업로드 데모(ADR-133).
4
+ *
5
+ * multipart/form-data 는 ADR-051 에서 "폼"으로 분류돼 CSRF 토큰 검증을 받는다. 스트리밍 multipart 는
6
+ * preHandler 시점에 body 가 안 파싱돼 `_csrf` 바디 필드를 못 읽으므로, 토큰을 **헤더**(`csrf-token`)로 보낸다.
7
+ * HTML 폼은 커스텀 헤더를 못 보내므로 fetch + FormData 로 제출한다. 성공하면 페이지를 새로고침해
8
+ * 서버 렌더 "최근 업로드" 목록(redis)을 갱신한다.
9
+ */
10
+ ;(function () {
11
+ var $ = function (/** @type {string} */ id) {
12
+ return /** @type {HTMLElement} */ (document.getElementById(id))
13
+ }
14
+
15
+ var form = /** @type {HTMLFormElement} */ ($('up-form'))
16
+ if (!form) return
17
+ var csrf = form.getAttribute('data-csrf') || ''
18
+ var fileInput = /** @type {HTMLInputElement} */ ($('up-file'))
19
+ var submitBtn = /** @type {HTMLButtonElement} */ ($('up-submit'))
20
+ var spinner = $('up-spinner')
21
+ var errorBox = $('up-error')
22
+ var errorDetail = $('up-error-detail')
23
+
24
+ form.addEventListener('submit', function (e) {
25
+ e.preventDefault()
26
+ if (!fileInput.files || fileInput.files.length === 0) return
27
+
28
+ submitBtn.disabled = true
29
+ spinner.classList.remove('d-none')
30
+ errorBox.classList.add('d-none')
31
+
32
+ var fd = new FormData()
33
+ for (var i = 0; i < fileInput.files.length; i++) fd.append('file', fileInput.files[i])
34
+
35
+ fetch('/demo/upload', {
36
+ method: 'POST',
37
+ headers: { 'csrf-token': csrf, accept: 'application/json' },
38
+ body: fd,
39
+ })
40
+ .then(function (res) {
41
+ return res.json().then(function (body) {
42
+ if (!res.ok) {
43
+ // 프레임워크 에러 엔벨로프 { ok:false, error:{ message } } — MIME 거부(415)/크기 초과(413) 등.
44
+ var msg = body && body.error && body.error.message ? body.error.message : 'upload ' + res.status
45
+ throw new Error(msg)
46
+ }
47
+ return body
48
+ })
49
+ })
50
+ .then(function () {
51
+ // 성공 — 서버 렌더 최근 목록을 갱신하러 새로고침.
52
+ window.location.assign('/demo/upload')
53
+ })
54
+ .catch(function (err) {
55
+ errorDetail.textContent = String(err && err.message ? err.message : err)
56
+ errorBox.classList.remove('d-none')
57
+ })
58
+ .finally(function () {
59
+ submitBtn.disabled = false
60
+ spinner.classList.add('d-none')
61
+ })
62
+ })
63
+ })()
@@ -0,0 +1,92 @@
1
+ // @ts-check
2
+ /*
3
+ * /demo/worker 클라이언트 — CPU 워커 풀 데모(ADR-124).
4
+ *
5
+ * 1) 하트비트: 250ms 마다 /demo/worker/ping(JSON)을 때려 왕복 지연을 잰다. 워커 계산이 도는 동안에도 이
6
+ * 지연이 작게 유지되면 메인 스레드(이벤트 루프)가 안 막힌 것이다 — 만약 계산을 메인 스레드에서 했다면
7
+ * 이 ping 들이 계산이 끝날 때까지 통째로 멈춘다.
8
+ * 2) 실행: Run 버튼이 /demo/worker/run 에 JSON POST(폼 아님 → CSRF 토큰 면제 + Origin 검증, ADR-051)로
9
+ * 반복 횟수를 보내 워커 풀에서 해시를 돌리고 결과(digest/rounds/ms)를 보여준다.
10
+ */
11
+ ;(function () {
12
+ var $ = function (/** @type {string} */ id) {
13
+ return /** @type {HTMLElement} */ (document.getElementById(id))
14
+ }
15
+
16
+ // ── 하트비트 ──────────────────────────────────────────────────────────────
17
+ var pingCount = 0
18
+ var countEl = $('wd-ping-count')
19
+ var latencyEl = $('wd-ping-latency')
20
+ var timeEl = $('wd-ping-time')
21
+
22
+ // 1초 간격 — 앱 기본 rate limit(100/분, ADR-073)을 넘지 않게 한다(라우터가 per-route 면제를 노출하지
23
+ // 않으므로 빈도 자체를 한도 안에 둔다). 탭이 숨겨지면 멈춰 불필요한 요청을 줄인다.
24
+ var HEARTBEAT_MS = 1000
25
+
26
+ function heartbeat() {
27
+ if (document.hidden) {
28
+ setTimeout(heartbeat, HEARTBEAT_MS)
29
+ return
30
+ }
31
+ var started = performance.now()
32
+ fetch('/demo/worker/ping', { headers: { accept: 'application/json' } })
33
+ .then(function (res) {
34
+ if (!res.ok) throw new Error('ping ' + res.status)
35
+ return res.json()
36
+ })
37
+ .then(function () {
38
+ pingCount += 1
39
+ countEl.textContent = String(pingCount)
40
+ latencyEl.textContent = String(Math.round(performance.now() - started))
41
+ timeEl.textContent = new Date().toLocaleTimeString('ko-KR', { hour12: false })
42
+ })
43
+ .catch(function () {
44
+ // 세션 만료(로그인 리다이렉트)·네트워크 끊김 등은 다음 주기에 자연 복구된다 — 표시만 비운다.
45
+ latencyEl.textContent = '—'
46
+ })
47
+ .finally(function () {
48
+ setTimeout(heartbeat, HEARTBEAT_MS)
49
+ })
50
+ }
51
+ heartbeat()
52
+
53
+ // ── 실행 ────────────────────────────────────────────────────────────────
54
+ var runBtn = /** @type {HTMLButtonElement} */ ($('wd-run'))
55
+ var spinner = $('wd-spinner')
56
+ var roundsInput = /** @type {HTMLInputElement} */ ($('wd-rounds'))
57
+ var resultBox = $('wd-result')
58
+ var errorBox = $('wd-error')
59
+ var errorDetail = $('wd-error-detail')
60
+
61
+ runBtn.addEventListener('click', function () {
62
+ runBtn.disabled = true
63
+ spinner.classList.remove('d-none')
64
+ errorBox.classList.add('d-none')
65
+
66
+ fetch('/demo/worker/run', {
67
+ method: 'POST',
68
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
69
+ body: JSON.stringify({ rounds: Number(roundsInput.value) }),
70
+ })
71
+ .then(function (res) {
72
+ if (!res.ok) throw new Error('run ' + res.status)
73
+ return res.json()
74
+ })
75
+ .then(function (body) {
76
+ // 프레임워크 응답 엔벨로프 { ok, data, meta } — 실제 결과는 body.data 에 있다.
77
+ var data = body && body.data ? body.data : body
78
+ $('wd-rounds-out').textContent = String(data.rounds)
79
+ $('wd-ms-out').textContent = data.ms + ' ms'
80
+ $('wd-digest-out').textContent = data.digest
81
+ resultBox.classList.remove('d-none')
82
+ })
83
+ .catch(function (err) {
84
+ errorDetail.textContent = String(err && err.message ? err.message : err)
85
+ errorBox.classList.remove('d-none')
86
+ })
87
+ .finally(function () {
88
+ runBtn.disabled = false
89
+ spinner.classList.add('d-none')
90
+ })
91
+ })
92
+ })()