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,71 @@
1
+ <% layout('layouts/main') %>
2
+
3
+ <div class="row justify-content-center">
4
+ <div class="col-md-6 col-lg-5">
5
+ <div class="card shadow-sm">
6
+ <div class="card-body p-4">
7
+ <h1 class="h4 mb-4 text-center"><%= t('register_title', '회원가입') %></h1>
8
+
9
+ <% if (typeof error !== 'undefined' && error) { %>
10
+ <div class="alert alert-danger" role="alert"><%= error %></div>
11
+ <% } %>
12
+
13
+ <form method="post" action="/register" novalidate>
14
+ <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
15
+
16
+ <div class="mb-3">
17
+ <label for="name" class="form-label"><%= t('field_name', '이름') %></label>
18
+ <input
19
+ type="text"
20
+ class="form-control <%= invalid && invalid.name ? 'is-invalid' : '' %>"
21
+ id="name"
22
+ name="name"
23
+ value="<%= values && values.name ? values.name : '' %>"
24
+ placeholder="<%= t('field_name_ph', '예: 홍길동') %>"
25
+ autocomplete="name"
26
+ required
27
+ />
28
+ <div class="invalid-feedback"><%= t('field_name_required', '이름을 입력하세요.') %></div>
29
+ </div>
30
+
31
+ <div class="mb-3">
32
+ <label for="email" class="form-label"><%= t('field_email', '이메일') %></label>
33
+ <input
34
+ type="email"
35
+ class="form-control <%= invalid && invalid.email ? 'is-invalid' : '' %>"
36
+ id="email"
37
+ name="email"
38
+ value="<%= values && values.email ? values.email : '' %>"
39
+ placeholder="<%= t('field_email_ph', '예: hong@example.com') %>"
40
+ autocomplete="username"
41
+ required
42
+ />
43
+ <div class="invalid-feedback"><%= t('field_email_required', '올바른 이메일을 입력하세요.') %></div>
44
+ </div>
45
+
46
+ <div class="mb-4">
47
+ <label for="password" class="form-label"><%= t('field_password', '비밀번호') %></label>
48
+ <input
49
+ type="password"
50
+ class="form-control <%= invalid && invalid.password ? 'is-invalid' : '' %>"
51
+ id="password"
52
+ name="password"
53
+ autocomplete="new-password"
54
+ required
55
+ />
56
+ <div class="form-text <%= invalid && invalid.password ? 'text-danger' : '' %>"><%= t('field_password_hint', '최소 8자 이상 입력하세요.') %></div>
57
+ </div>
58
+
59
+ <div class="d-grid">
60
+ <button type="submit" class="btn btn-primary"><%= t('btn_register', '가입') %></button>
61
+ </div>
62
+ </form>
63
+
64
+ <p class="text-center text-body-secondary small mt-4 mb-0">
65
+ <%= t('register_have_account', '이미 계정이 있으신가요?') %>
66
+ <a href="/auth/login"><%= t('login_title', '로그인') %></a>
67
+ </p>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
@@ -0,0 +1,92 @@
1
+ <% layout('layouts/main') %>
2
+ <%
3
+ // 서버사이드 시각 포맷 헬퍼 — 스케줄 타임존 기준 24시간제. nextRuns 는 Date, history.at 은 ISO 문자열.
4
+ function fmt(d) { return new Date(d).toLocaleString('ko-KR', { hour12: false, timeZone: snap.timezone }) }
5
+ function sourceBadge(s) { return s === 'manual' ? 'text-bg-secondary' : 'text-bg-primary' }
6
+ %>
7
+
8
+ <div class="mb-4">
9
+ <h1 class="h3 mb-1"><%= t('cron_title', { defaultValue: '스케줄러 데모 (MegaSchedule)' }) %></h1>
10
+ <p class="text-body-secondary small mb-0"><%= t('cron_subtitle', { defaultValue: 'mega scheduler 프로세스가 30초마다 Redis 카운터를 원자적으로 올립니다. 다중 인스턴스 중복 실행은 분산 락(redlock leader election)으로 막습니다.' }) %></p>
11
+ </div>
12
+
13
+ <% if (typeof notice !== 'undefined' && notice) { %>
14
+ <div class="alert alert-success alert-dismissible fade show" role="alert">
15
+ <%= notice %>
16
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="close"></button>
17
+ </div>
18
+ <% } %>
19
+
20
+ <div class="row g-3">
21
+ <div class="col-lg-6">
22
+ <div class="card h-100">
23
+ <div class="card-body d-flex flex-column">
24
+ <h2 class="h5 card-title"><%= t('cron_counter_title', { defaultValue: '실행 카운터' }) %></h2>
25
+ <p class="card-text text-body-secondary small"><%= t('cron_counter_desc', { defaultValue: '스케줄이 돌 때마다(또는 아래 수동 실행) Redis 카운터를 1씩 올립니다.' }) %></p>
26
+ <div class="display-5 fw-bold mt-2"><%= snap.count %></div>
27
+ <div class="text-body-secondary small"><%= t('cron_counter_total', { defaultValue: '누적 실행 횟수' }) %></div>
28
+ <div class="mt-3">
29
+ <code class="small"><%= snap.cron %></code>
30
+ <span class="badge text-bg-light ms-1"><%= snap.timezone %></span>
31
+ </div>
32
+ <div class="mt-auto pt-3">
33
+ <form method="post" action="/demo/cron/run" class="m-0">
34
+ <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
35
+ <button type="submit" class="btn btn-primary btn-sm"><%= t('cron_run_now', { defaultValue: '지금 실행' }) %></button>
36
+ </form>
37
+ <p class="text-body-secondary small mb-0 mt-2"><%= t('cron_run_hint', { defaultValue: 'cron 시각을 기다리지 않고 같은 작업을 즉시 1회 실행합니다(수동 트리거).' }) %></p>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <div class="col-lg-6">
44
+ <div class="card h-100">
45
+ <div class="card-body">
46
+ <h2 class="h5 card-title"><%= t('cron_next_title', { defaultValue: '다음 실행 시각' }) %></h2>
47
+ <p class="card-text text-body-secondary small"><%= t('cron_next_desc', { defaultValue: 'croner 가 계산한 다음 다섯 번의 실행 예정 시각입니다(타이머 미가동 순수 계산).' }) %></p>
48
+ <ul class="list-group list-group-flush mt-2">
49
+ <% snap.nextRuns.forEach(function (run, i) { %>
50
+ <li class="list-group-item d-flex justify-content-between align-items-center px-0">
51
+ <span class="font-monospace small"><%= fmt(run) %></span>
52
+ <% if (i === 0) { %><span class="badge text-bg-success"><%= t('cron_next_soonest', { defaultValue: '가장 가까움' }) %></span><% } %>
53
+ </li>
54
+ <% }) %>
55
+ </ul>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+
61
+ <div class="card mt-3">
62
+ <div class="card-body">
63
+ <h2 class="h5 card-title"><%= t('cron_history_title', { defaultValue: '최근 실행 이력' }) %></h2>
64
+ <p class="card-text text-body-secondary small"><%= t('cron_history_desc', { defaultValue: 'Redis LIST 에 최근 10건을 남깁니다(LPUSH + LTRIM). 자동 실행과 수동 실행이 함께 쌓입니다.' }) %></p>
65
+ <% if (snap.history.length === 0) { %>
66
+ <p class="text-body-secondary small mb-0"><%= t('cron_history_empty', { defaultValue: '아직 실행 이력이 없습니다. scheduler 프로세스를 켜거나 위에서 수동 실행해 보세요.' }) %></p>
67
+ <% } else { %>
68
+ <div class="table-responsive">
69
+ <table class="table table-sm align-middle mb-0">
70
+ <thead>
71
+ <tr>
72
+ <th scope="col">#</th>
73
+ <th scope="col"><%= t('cron_history_at', { defaultValue: '실행 시각' }) %></th>
74
+ <th scope="col"><%= t('cron_history_source', { defaultValue: '출처' }) %></th>
75
+ </tr>
76
+ </thead>
77
+ <tbody>
78
+ <% snap.history.forEach(function (e) { %>
79
+ <tr>
80
+ <td class="font-monospace"><%= e.count %></td>
81
+ <td class="font-monospace small"><%= fmt(e.at) %></td>
82
+ <td>
83
+ <span class="badge <%= sourceBadge(e.source) %>"><%= t('cron_source_' + e.source) %></span>
84
+ </td>
85
+ </tr>
86
+ <% }) %>
87
+ </tbody>
88
+ </table>
89
+ </div>
90
+ <% } %>
91
+ </div>
92
+ </div>
@@ -0,0 +1,24 @@
1
+ <% layout('layouts/main') %>
2
+
3
+ <div class="mb-4">
4
+ <h1 class="h3 mb-1"><%= t('guide_index_title', '가이드') %></h1>
5
+ <p class="text-body-secondary small mb-0"><%= t('guide_index_subtitle', 'MEGA-FRAMEWORK 사용 가이드 모음입니다.') %></p>
6
+ </div>
7
+
8
+ <div class="alert alert-secondary d-flex align-items-start gap-2" role="alert">
9
+ <span>📚</span>
10
+ <div><%= t('guide_index_welcome', '각 주제별 가이드를 골라 읽어보세요. 코드 예시는 서버에서 미리 하이라이트되어 표시됩니다.') %></div>
11
+ </div>
12
+
13
+ <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3 mt-1">
14
+ <% guides.forEach(function (g, i) { %>
15
+ <div class="col">
16
+ <a href="/guide/<%= g.slug %>" class="card h-100 text-decoration-none text-reset">
17
+ <div class="card-body">
18
+ <div class="text-body-secondary small mb-1">#<%= String(i + 1).padStart(2, '0') %></div>
19
+ <h2 class="h6 card-title mb-0"><%= g.title %></h2>
20
+ </div>
21
+ </a>
22
+ </div>
23
+ <% }) %>
24
+ </div>
@@ -0,0 +1,64 @@
1
+ <% layout('layouts/main') %>
2
+ <% block('head', '<link rel="stylesheet" href="/static/vendor/highlight/github.css" />') %>
3
+ <% block('head', '<link rel="stylesheet" href="/static/vendor/highlight/github-dark.css" />') %>
4
+ <% block('head', '<link rel="stylesheet" href="/static/css/guide.css" />') %>
5
+
6
+ <%
7
+ // 목차(가이드 목록) 마크업 — 데스크톱 사이드바와 모바일 offcanvas 가 공유한다. EJS 함수 안의 HTML 은
8
+ // 호출 시점에 출력 버퍼로 append 되므로, 정의 후 두 곳에서 호출하면 중복 없이 같은 마크업을 낸다.
9
+ function renderToc() {
10
+ %>
11
+ <ul class="nav flex-column guide-toc">
12
+ <% guides.forEach(function (g) { %>
13
+ <li class="nav-item">
14
+ <a
15
+ class="nav-link<%= g.slug === guide.slug ? ' active' : '' %>"
16
+ href="/guide/<%= g.slug %>"
17
+ ><%= g.title %></a
18
+ >
19
+ </li>
20
+ <% }) %>
21
+ </ul>
22
+ <% } %>
23
+
24
+ <div class="d-lg-none mb-3">
25
+ <button
26
+ class="btn btn-outline-secondary btn-sm"
27
+ type="button"
28
+ data-bs-toggle="offcanvas"
29
+ data-bs-target="#guideToc"
30
+ aria-controls="guideToc"
31
+ >
32
+ ☰ <%= t('guide_toc', '목차') %>
33
+ </button>
34
+ </div>
35
+
36
+ <div class="row g-4">
37
+ <div class="col-lg-3 d-none d-lg-block">
38
+ <nav class="guide-sidebar" aria-label="<%= t('guide_toc', '목차') %>">
39
+ <div class="text-body-secondary small text-uppercase fw-semibold mb-2"><%= t('guide_toc', '목차') %></div>
40
+ <% renderToc() %>
41
+ </nav>
42
+ </div>
43
+
44
+ <div class="col-lg-9">
45
+ <article class="guide-content">
46
+ <%- guide.html %>
47
+ </article>
48
+ <div class="mt-4 pt-3 border-top">
49
+ <a href="/guide" class="btn btn-outline-secondary btn-sm">← <%= t('guide_back', '목록으로') %></a>
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="offcanvas offcanvas-start" tabindex="-1" id="guideToc" aria-labelledby="guideTocLabel">
55
+ <div class="offcanvas-header">
56
+ <h2 class="offcanvas-title h6 mb-0" id="guideTocLabel"><%= t('guide_toc', '목차') %></h2>
57
+ <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="close"></button>
58
+ </div>
59
+ <div class="offcanvas-body">
60
+ <nav class="guide-sidebar">
61
+ <% renderToc() %>
62
+ </nav>
63
+ </div>
64
+ </div>
@@ -0,0 +1,82 @@
1
+ <% layout('layouts/main') %>
2
+
3
+ <section class="hero p-4 p-md-5 mb-5">
4
+ <div class="row align-items-center g-4">
5
+ <div class="col-lg-7">
6
+ <span class="badge text-bg-light border mb-3">sample / crud</span>
7
+ <h1 class="display-5 fw-bold mb-3"><%= t('home_title', '사용자 CRUD 관리') %></h1>
8
+ <p class="fs-5 text-body-secondary mb-4"><%= t('home_subtitle', 'postgres 기반 사용자 리소스를 Bootstrap 5 관리 UI 로 생성·조회·수정·삭제합니다. 같은 서비스 계층을 JSON REST API 도 함께 노출합니다.') %></p>
9
+ <div class="d-flex flex-wrap gap-2">
10
+ <a href="/admin/users" class="btn btn-primary btn-lg"><%= t('home_cta_manage', '사용자 관리') %></a>
11
+ <a href="/users" class="btn btn-outline-secondary btn-lg" target="_blank" rel="noopener"
12
+ ><%= t('home_cta_api', 'JSON API (/users)') %></a
13
+ >
14
+ </div>
15
+ </div>
16
+ <div class="col-lg-5 text-center d-none d-lg-block">
17
+ <div class="display-1">🗂️</div>
18
+ </div>
19
+ </div>
20
+ </section>
21
+
22
+ <div class="alert alert-secondary d-flex align-items-start gap-2" role="alert">
23
+ <span>ℹ️</span>
24
+ <div><%= t('home_api_note', '모든 요청은 정본 계층 흐름을 따릅니다:') %> <code>route → controller → service → model → postgres</code></div>
25
+ </div>
26
+
27
+ <div class="row row-cols-1 row-cols-md-2 g-3 mt-2">
28
+ <div class="col">
29
+ <a href="/demo/notes" class="card h-100 text-decoration-none text-reset">
30
+ <div class="card-body">
31
+ <div class="fs-2 mb-2">📝</div>
32
+ <h2 class="h5 card-title"><%= t('home_demo_notes', 'Mongo 노트 데모') %></h2>
33
+ <p class="card-text text-body-secondary small mb-0"><%= t('notes_subtitle', 'MongoDB 도큐먼트 어댑터로 notes 컬렉션을 생성·조회·수정·삭제합니다.') %></p>
34
+ </div>
35
+ </a>
36
+ </div>
37
+ <div class="col">
38
+ <a href="/demo/redis" class="card h-100 text-decoration-none text-reset">
39
+ <div class="card-body">
40
+ <div class="fs-2 mb-2">⚡</div>
41
+ <h2 class="h5 card-title"><%= t('home_demo_redis', 'Redis 캐시 데모') %></h2>
42
+ <p class="card-text text-body-secondary small mb-0"><%= t('redis_subtitle', 'Redis 어댑터로 방문 카운터(원자적 INCR/EXPIRE)와 쿼리 결과 캐시(GET/SET/TTL/DEL)를 시연합니다.') %></p>
43
+ </div>
44
+ </a>
45
+ </div>
46
+ <div class="col">
47
+ <a href="/demo/ws" class="card h-100 text-decoration-none text-reset">
48
+ <div class="card-body">
49
+ <div class="fs-2 mb-2">💬</div>
50
+ <h2 class="h5 card-title"><%= t('home_demo_ws', 'WebSocket 채팅 데모') %></h2>
51
+ <p class="card-text text-body-secondary small mb-0"><%= t('ws_subtitle', 'WASM MegaSocket 으로 /ws/chat 에 접속합니다. 메시지는 ASP 로 암호화(E: 프레임)되어 브라우저에서 직접 암복호화되고, 서버는 채널 전체에 broadcast 합니다.') %></p>
52
+ </div>
53
+ </a>
54
+ </div>
55
+ <div class="col">
56
+ <a href="/demo/cron" class="card h-100 text-decoration-none text-reset">
57
+ <div class="card-body">
58
+ <div class="fs-2 mb-2">⏰</div>
59
+ <h2 class="h5 card-title"><%= t('home_demo_cron', '스케줄러 데모') %></h2>
60
+ <p class="card-text text-body-secondary small mb-0"><%= t('cron_subtitle', 'mega scheduler 프로세스가 30초마다 Redis 카운터를 원자적으로 올립니다. 다중 인스턴스 중복 실행은 분산 락(redlock leader election)으로 막습니다.') %></p>
61
+ </div>
62
+ </a>
63
+ </div>
64
+ <div class="col">
65
+ <a href="/demo/jobs" class="card h-100 text-decoration-none text-reset">
66
+ <div class="card-body">
67
+ <div class="fs-2 mb-2">📮</div>
68
+ <h2 class="h5 card-title"><%= t('home_demo_jobs', '잡 큐 데모') %></h2>
69
+ <p class="card-text text-body-secondary small mb-0"><%= t('jobs_subtitle', 'EmailJob 을 NATS JetStream 에 enqueue 하면 mega worker 프로세스가 소비합니다. 일시 실패는 재시도되고, 영구 실패는 DLQ 로 격리됩니다.') %></p>
70
+ </div>
71
+ </a>
72
+ </div>
73
+ <div class="col">
74
+ <a href="/demo/worker" class="card h-100 text-decoration-none text-reset">
75
+ <div class="card-body">
76
+ <div class="fs-2 mb-2">🧵</div>
77
+ <h2 class="h5 card-title"><%= t('home_demo_worker', 'CPU 워커 데모') %></h2>
78
+ <p class="card-text text-body-secondary small mb-0"><%= t('worker_subtitle', 'SHA-256 N회 반복 같은 CPU-bound 작업을 worker_threads 풀에서 돌립니다. 계산 도중에도 서버가 다른 요청에 즉시 응답하는지(메인 스레드 non-block) 하트비트로 확인합니다.') %></p>
79
+ </div>
80
+ </a>
81
+ </div>
82
+ </div>
@@ -0,0 +1,113 @@
1
+ <% layout('layouts/main') %>
2
+ <%
3
+ function fmt(d) { return new Date(d).toLocaleString('ko-KR', { hour12: false }) }
4
+ function statusBadge(s) { return s === 'sent' ? 'text-bg-success' : (s === 'retry' ? 'text-bg-warning' : 'text-bg-danger') }
5
+ function modeBadge(m) { return m === 'ok' ? 'text-bg-success' : (m === 'flaky' ? 'text-bg-warning' : 'text-bg-danger') }
6
+ %>
7
+
8
+ <div class="mb-4">
9
+ <h1 class="h3 mb-1"><%= t('jobs_title', { defaultValue: '잡 큐 데모 (MegaJob)' }) %></h1>
10
+ <p class="text-body-secondary small mb-0"><%= t('jobs_subtitle', { defaultValue: 'EmailJob 을 NATS JetStream 에 enqueue 하면 mega worker 프로세스가 소비합니다. 일시 실패는 재시도되고, 영구 실패는 DLQ 로 격리됩니다.' }) %></p>
11
+ </div>
12
+
13
+ <% if (typeof notice !== 'undefined' && notice) { %>
14
+ <div class="alert alert-success alert-dismissible fade show" role="alert">
15
+ <%= notice %>
16
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="close"></button>
17
+ </div>
18
+ <% } %>
19
+
20
+ <div class="row g-3">
21
+ <div class="col-lg-5">
22
+ <div class="card h-100">
23
+ <div class="card-body">
24
+ <h2 class="h5 card-title"><%= t('jobs_enqueue_title', { defaultValue: '이메일 발송 잡 넣기' }) %></h2>
25
+ <p class="card-text text-body-secondary small"><%= t('jobs_enqueue_desc', { defaultValue: '실제 발송은 하지 않고 처리만 시뮬레이션합니다. 모드로 성공/재시도/영구실패 흐름을 시연합니다.' }) %></p>
26
+ <form method="post" action="/demo/jobs/enqueue" class="mt-3">
27
+ <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
28
+ <div class="mb-3">
29
+ <label for="to" class="form-label small"><%= t('jobs_field_to', { defaultValue: '받는 사람' }) %></label>
30
+ <input type="email" class="form-control form-control-sm" id="to" name="to" placeholder="demo@example.com" />
31
+ </div>
32
+ <div class="mb-3">
33
+ <label for="mode" class="form-label small"><%= t('jobs_field_mode', { defaultValue: '모드' }) %></label>
34
+ <select class="form-select form-select-sm" id="mode" name="mode">
35
+ <% modes.forEach(function (m) { %>
36
+ <option value="<%= m %>"><%= t('jobs_mode_' + m) %></option>
37
+ <% }) %>
38
+ </select>
39
+ <div class="form-text small">
40
+ <%= t('jobs_mode_ok', { defaultValue: '성공' }) %>: <%= t('jobs_mode_ok_hint', { defaultValue: '첫 시도에 바로 성공' }) %><br />
41
+ <%= t('jobs_mode_flaky', { defaultValue: '재시도' }) %>: <%= t('jobs_mode_flaky_hint', { defaultValue: '1번째 시도 실패 → 재시도 → 2번째 성공' }) %><br />
42
+ <%= t('jobs_mode_fail', { defaultValue: '영구 실패' }) %>: <%= t('jobs_mode_fail_hint', { defaultValue: '매 시도 실패 → 재시도 소진 후 DLQ 격리' }) %>
43
+ </div>
44
+ </div>
45
+ <button type="submit" class="btn btn-primary btn-sm"><%= t('jobs_enqueue_btn', { defaultValue: '큐에 넣기' }) %></button>
46
+ <a href="/demo/jobs" class="btn btn-outline-secondary btn-sm ms-1"><%= t('jobs_reload', { defaultValue: '새로고침' }) %></a>
47
+ </form>
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ <div class="col-lg-7">
53
+ <div class="card h-100">
54
+ <div class="card-body">
55
+ <h2 class="h5 card-title">
56
+ <%= t('jobs_dlq_title', { defaultValue: 'DLQ (격리된 잡)' }) %>
57
+ <span class="badge <%= dlq.count > 0 ? 'text-bg-danger' : 'text-bg-light' %> ms-1"><%= dlq.count %></span>
58
+ </h2>
59
+ <p class="card-text text-body-secondary small"><%= t('jobs_dlq_desc', { defaultValue: '재시도를 모두 소진한 영구 실패 잡이 모이는 곳입니다(NATS 스트림). 원인 분석·재처리용.' }) %></p>
60
+ <% if (dlq.count === 0) { %>
61
+ <p class="text-body-secondary small mb-0"><%= t('jobs_dlq_empty', { defaultValue: '아직 DLQ 로 간 잡이 없습니다.' }) %></p>
62
+ <% } else if (dlq.latest) { %>
63
+ <dl class="row small mb-0 mt-2">
64
+ <dt class="col-sm-4"><%= t('jobs_dlq_failed_at', { defaultValue: '실패 시각' }) %></dt>
65
+ <dd class="col-sm-8 font-monospace"><%= fmt(dlq.latest.failedAt) %></dd>
66
+ <dt class="col-sm-4"><%= t('jobs_dlq_deliveries', { defaultValue: '전달 횟수' }) %></dt>
67
+ <dd class="col-sm-8 font-monospace"><%= dlq.latest.deliveryCount %></dd>
68
+ <dt class="col-sm-4"><%= t('jobs_dlq_error', { defaultValue: '오류' }) %></dt>
69
+ <dd class="col-sm-8"><code class="small"><%= dlq.latest.error %></code></dd>
70
+ <dt class="col-sm-4"><%= t('jobs_dlq_payload', { defaultValue: '페이로드' }) %></dt>
71
+ <dd class="col-sm-8 font-monospace small"><%= JSON.stringify(dlq.latest.payload) %></dd>
72
+ </dl>
73
+ <% } %>
74
+ <div class="mt-3"><code class="small">NATS JetStream · workqueue → DLQ</code></div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ <div class="card mt-3">
81
+ <div class="card-body">
82
+ <h2 class="h5 card-title"><%= t('jobs_events_title', { defaultValue: '처리 이벤트' }) %></h2>
83
+ <p class="card-text text-body-secondary small"><%= t('jobs_events_desc', { defaultValue: '워커가 각 시도마다 남긴 타임라인입니다(최신순). 재시도가 일어나면 같은 잡이 여러 번 보입니다.' }) %></p>
84
+ <% if (events.length === 0) { %>
85
+ <p class="text-body-secondary small mb-0"><%= t('jobs_events_empty', { defaultValue: '아직 처리된 잡이 없습니다. 위에서 잡을 넣고 mega worker 프로세스를 켜 보세요.' }) %></p>
86
+ <% } else { %>
87
+ <div class="table-responsive">
88
+ <table class="table table-sm align-middle mb-0">
89
+ <thead>
90
+ <tr>
91
+ <th scope="col"><%= t('jobs_events_at', { defaultValue: '시각' }) %></th>
92
+ <th scope="col"><%= t('jobs_events_id', { defaultValue: '잡 ID' }) %></th>
93
+ <th scope="col"><%= t('jobs_field_mode', { defaultValue: '모드' }) %></th>
94
+ <th scope="col"><%= t('jobs_events_attempt', { defaultValue: '시도' }) %></th>
95
+ <th scope="col"><%= t('jobs_events_status', { defaultValue: '상태' }) %></th>
96
+ </tr>
97
+ </thead>
98
+ <tbody>
99
+ <% events.forEach(function (e) { %>
100
+ <tr>
101
+ <td class="font-monospace small"><%= fmt(e.at) %></td>
102
+ <td class="font-monospace small"><%= e.id %></td>
103
+ <td><span class="badge <%= modeBadge(e.mode) %>"><%= t('jobs_mode_' + e.mode) %></span></td>
104
+ <td class="font-monospace"><%= e.attempt %></td>
105
+ <td><span class="badge <%= statusBadge(e.status) %>"><%= t('jobs_status_' + e.status) %></span></td>
106
+ </tr>
107
+ <% }) %>
108
+ </tbody>
109
+ </table>
110
+ </div>
111
+ <% } %>
112
+ </div>
113
+ </div>
@@ -0,0 +1,112 @@
1
+ <!doctype html>
2
+ <html lang="<%= lang %>" data-bs-theme="light">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title><%= typeof title !== 'undefined' ? title : 'sample-crud' %></title>
7
+ <%# FOUC 방지 — 저장된 테마를 페인트 전에 적용. CSP(script-src 'self')라 인라인 불가 → 외부 파일. %>
8
+ <script src="/static/js/theme-init.js"></script>
9
+ <link rel="stylesheet" href="/static/vendor/bootstrap/bootstrap.min.css" />
10
+ <link rel="stylesheet" href="/static/css/app.css" />
11
+ <%# 페이지별 추가 head(가이드 뷰어가 highlight.js·가이드 CSS 를 여기에 주입한다, ejs-mate block). %>
12
+ <%- block('head') %>
13
+ </head>
14
+ <body class="d-flex flex-column min-vh-100">
15
+ <nav class="navbar navbar-expand-md border-bottom">
16
+ <div class="container">
17
+ <a class="navbar-brand" href="/">MEGA<span class="brand-dot">·</span>crud</a>
18
+ <button
19
+ class="navbar-toggler"
20
+ type="button"
21
+ data-bs-toggle="collapse"
22
+ data-bs-target="#navmenu"
23
+ aria-controls="navmenu"
24
+ aria-expanded="false"
25
+ aria-label="menu"
26
+ >
27
+ <span class="navbar-toggler-icon"></span>
28
+ </button>
29
+ <div class="collapse navbar-collapse" id="navmenu">
30
+ <ul class="navbar-nav me-auto">
31
+ <li class="nav-item"><a class="nav-link" href="/"><%= t('nav_home', '홈') %></a></li>
32
+ <li class="nav-item"><a class="nav-link" href="/admin/users"><%= t('nav_users', '사용자') %></a></li>
33
+ <li class="nav-item dropdown">
34
+ <a
35
+ class="nav-link dropdown-toggle"
36
+ href="#"
37
+ role="button"
38
+ data-bs-toggle="dropdown"
39
+ aria-expanded="false"
40
+ ><%= t('nav_demo', '데모') %></a
41
+ >
42
+ <ul class="dropdown-menu">
43
+ <li><a class="dropdown-item" href="/demo/notes"><%= t('nav_notes', '노트 (Mongo)') %></a></li>
44
+ <li><a class="dropdown-item" href="/demo/redis"><%= t('nav_redis', '캐시 (Redis)') %></a></li>
45
+ <li><a class="dropdown-item" href="/demo/ws"><%= t('nav_ws', '채팅 (WS+ASP)') %></a></li>
46
+ <li><a class="dropdown-item" href="/demo/cron"><%= t('nav_cron', '스케줄러 (Cron)') %></a></li>
47
+ <li><a class="dropdown-item" href="/demo/jobs"><%= t('nav_jobs', '잡 큐 (NATS)') %></a></li>
48
+ <li><a class="dropdown-item" href="/demo/worker"><%= t('nav_worker', '워커 (Threads)') %></a></li>
49
+ <li><hr class="dropdown-divider" /></li>
50
+ <li><a class="dropdown-item" href="/demo/metrics"><%= t('nav_metrics', '메트릭') %></a></li>
51
+ <li><a class="dropdown-item" href="/demo/tracing"><%= t('nav_tracing', '트레이싱') %></a></li>
52
+ <li><a class="dropdown-item" href="/demo/logs"><%= t('nav_logs', '로그') %></a></li>
53
+ <li><a class="dropdown-item" href="/demo/upload"><%= t('nav_upload', '업로드') %></a></li>
54
+ </ul>
55
+ </li>
56
+ <li class="nav-item"><a class="nav-link" href="/perf"><%= t('nav_perf', '성능') %></a></li>
57
+ <li class="nav-item"><a class="nav-link" href="/guide"><%= t('nav_guide', '가이드') %></a></li>
58
+ <li class="nav-item"><a class="nav-link" href="/docs"><%= t('nav_docs', 'API 문서') %></a></li>
59
+ </ul>
60
+ <div class="d-flex align-items-center gap-2">
61
+ <% var _user = typeof currentUser !== 'undefined' ? currentUser : null %>
62
+ <% if (_user) { %>
63
+ <span class="navbar-text small"><%= _user.name %></span>
64
+ <form method="post" action="/auth/logout" class="m-0">
65
+ <input type="hidden" name="_csrf" value="<%= typeof csrfToken !== 'undefined' ? csrfToken : '' %>" />
66
+ <button type="submit" class="btn btn-outline-secondary btn-sm"><%= t('btn_logout', '로그아웃') %></button>
67
+ </form>
68
+ <% } else { %>
69
+ <a class="btn btn-outline-primary btn-sm" href="/auth/login"><%= t('btn_login', '로그인') %></a>
70
+ <% } %>
71
+ <button
72
+ type="button"
73
+ class="btn btn-outline-secondary btn-sm"
74
+ data-theme-toggle
75
+ aria-label="<%= t('theme_toggle', '테마 전환') %>"
76
+ title="<%= t('theme_toggle', '테마 전환') %>"
77
+ >
78
+ <span class="theme-icon-light">☀️</span><span class="theme-icon-dark">🌙</span>
79
+ </button>
80
+ <div class="dropdown">
81
+ <button
82
+ class="btn btn-outline-secondary btn-sm dropdown-toggle"
83
+ type="button"
84
+ data-bs-toggle="dropdown"
85
+ aria-expanded="false"
86
+ >
87
+ <%= lang === 'ko' ? '한국어' : 'English' %>
88
+ </button>
89
+ <ul class="dropdown-menu dropdown-menu-end">
90
+ <li><a class="dropdown-item" href="#" data-lang="ko">한국어</a></li>
91
+ <li><a class="dropdown-item" href="#" data-lang="en">English</a></li>
92
+ </ul>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </nav>
98
+
99
+ <main class="container py-4 py-md-5 flex-grow-1">
100
+ <%- body %>
101
+ </main>
102
+
103
+ <footer class="site-footer py-4 mt-auto">
104
+ <div class="container text-center text-body-secondary small">
105
+ <%= t('footer_built', 'MEGA-FRAMEWORK 로 제작 · Bootstrap 5') %>
106
+ </div>
107
+ </footer>
108
+
109
+ <script src="/static/vendor/bootstrap/bootstrap.bundle.min.js"></script>
110
+ <script src="/static/js/app.js"></script>
111
+ </body>
112
+ </html>