mega-framework 0.1.6 → 0.1.8

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 (248) hide show
  1. package/README.md +9 -0
  2. package/bin/mega-ws-hub.js +2 -2
  3. package/package.json +33 -9
  4. package/sample/crud/.env +10 -1
  5. package/sample/crud/.env.example +10 -1
  6. package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
  7. package/sample/crud/.mega/journal/snapshot.json +261 -0
  8. package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
  9. package/sample/crud/apps/main/controllers/web-controller.js +7 -5
  10. package/sample/crud/apps/main/locales/server/en.json +12 -1
  11. package/sample/crud/apps/main/locales/server/ko.json +12 -1
  12. package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
  13. package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
  14. package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
  15. package/sample/crud/apps/main/models/log-partition-model.js +105 -0
  16. package/sample/crud/apps/main/models/note-model.js +79 -0
  17. package/sample/crud/apps/main/models/user-level-model.js +24 -0
  18. package/sample/crud/apps/main/models/user-model.js +146 -0
  19. package/sample/crud/apps/main/models/user-type-model.js +21 -0
  20. package/sample/crud/apps/main/models/wallet-model.js +24 -0
  21. package/sample/crud/apps/main/routes/users.js +55 -10
  22. package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
  23. package/sample/crud/apps/main/services/auth-service.js +39 -24
  24. package/sample/crud/apps/main/services/log-partition-service.js +101 -0
  25. package/sample/crud/apps/main/services/note-service.js +6 -6
  26. package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
  27. package/sample/crud/apps/main/services/user-service.js +62 -21
  28. package/sample/crud/apps/main/views/auth/login.ejs +6 -6
  29. package/sample/crud/apps/main/views/auth/register.ejs +46 -5
  30. package/sample/crud/apps/main/views/users/edit.ejs +42 -5
  31. package/sample/crud/apps/main/views/users/list.ejs +6 -2
  32. package/sample/crud/apps/main/views/users/new.ejs +56 -4
  33. package/sample/crud/docs/log_partition_design.mm.md +23 -0
  34. package/sample/crud/mega.config.js +10 -2
  35. package/sample/crud/package.json +3 -3
  36. package/sample/crud/scripts/start-ws-hub.sh +20 -6
  37. package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  38. package/sample/simple/package.json +2 -2
  39. package/src/adapters/adapter-manager.js +2 -1
  40. package/src/adapters/adapter-options.js +44 -3
  41. package/src/adapters/file-adapter.js +9 -5
  42. package/src/adapters/file-session-adapter.js +4 -3
  43. package/src/adapters/maria-adapter.js +33 -7
  44. package/src/adapters/mega-cache-adapter.js +83 -6
  45. package/src/adapters/mega-db-adapter.js +10 -1
  46. package/src/adapters/mongo-adapter.js +40 -8
  47. package/src/adapters/postgres-adapter.js +33 -6
  48. package/src/adapters/redis-adapter.js +7 -3
  49. package/src/adapters/sqlite-adapter.js +26 -3
  50. package/src/cli/commands/console-cmd.js +3 -1
  51. package/src/cli/commands/new.js +13 -3
  52. package/src/cli/commands/scaffold.js +173 -33
  53. package/src/cli/generators/index.js +140 -3
  54. package/src/cli/index.js +437 -155
  55. package/src/cli/watch.js +188 -0
  56. package/src/core/ajv-mapper.js +30 -3
  57. package/src/core/boot.js +464 -245
  58. package/src/core/cluster-metrics.js +13 -4
  59. package/src/core/ctx-builder.js +65 -3
  60. package/src/core/envelope.js +119 -12
  61. package/src/core/hub-link.js +89 -18
  62. package/src/core/i18n.js +11 -1
  63. package/src/core/index.js +7 -3
  64. package/src/core/mega-app.js +253 -505
  65. package/src/core/mega-cluster.js +4 -1
  66. package/src/core/mega-server.js +40 -9
  67. package/src/core/migration/dialect-registry.js +107 -0
  68. package/src/core/migration/dialects/README.md +62 -0
  69. package/src/core/migration/dialects/maria.js +496 -0
  70. package/src/core/migration/dialects/mongo.js +824 -0
  71. package/src/core/migration/dialects/postgres.js +563 -0
  72. package/src/core/migration/dialects/sqlite.js +476 -0
  73. package/src/core/migration/differ.js +456 -0
  74. package/src/core/migration/generate.js +508 -0
  75. package/src/core/migration/journal.js +167 -0
  76. package/src/core/migration/model-scan.js +84 -0
  77. package/src/core/migration/mongo-migration-db.js +97 -0
  78. package/src/core/migration/schema-builder.js +400 -0
  79. package/src/core/migration/schema-validator.js +315 -0
  80. package/src/core/migration-lock.js +205 -0
  81. package/src/core/migration-runner.js +166 -38
  82. package/src/core/multipart.js +28 -5
  83. package/src/core/pipeline.js +131 -0
  84. package/src/core/router.js +70 -65
  85. package/src/core/scope-registry.js +1 -0
  86. package/src/core/security.js +70 -12
  87. package/src/core/session-store.js +14 -1
  88. package/src/core/workers-manager.js +12 -1
  89. package/src/core/ws-cluster.js +10 -3
  90. package/src/core/ws-message.js +48 -4
  91. package/src/core/ws-presence.js +636 -0
  92. package/src/core/ws-roster.js +50 -8
  93. package/src/core/ws-upgrade.js +223 -12
  94. package/src/index.js +1 -1
  95. package/src/lib/hub-protocol.js +29 -0
  96. package/src/lib/mega-circuit-breaker.js +5 -3
  97. package/src/lib/mega-health.js +35 -4
  98. package/src/lib/mega-job-queue.js +151 -34
  99. package/src/lib/mega-job.js +37 -1
  100. package/src/lib/mega-metrics.js +31 -13
  101. package/src/lib/mega-plugin.js +34 -3
  102. package/src/lib/mega-schedule.js +40 -22
  103. package/src/lib/mega-shutdown.js +114 -39
  104. package/src/lib/mega-tracing.js +66 -19
  105. package/src/lib/mega-worker.js +33 -6
  106. package/src/lib/otel-resource.js +36 -0
  107. package/src/{cli → lib}/ws-hub.js +139 -15
  108. package/src/models/crud-sql-builder.js +133 -0
  109. package/src/models/mega-model.js +82 -2
  110. package/src/models/model-crud.js +483 -0
  111. package/src/models/mongo-crud.js +285 -0
  112. package/templates/adr/code.tpl +23 -0
  113. package/templates/model/code-mongo.tpl +35 -0
  114. package/templates/model/code.tpl +15 -1
  115. package/templates/model/test-mongo.tpl +38 -0
  116. package/templates/model/test.tpl +4 -0
  117. package/types/adapters/adapter-manager.d.ts +95 -0
  118. package/types/adapters/adapter-options.d.ts +93 -0
  119. package/types/adapters/file-adapter.d.ts +105 -0
  120. package/types/adapters/file-session-adapter.d.ts +103 -0
  121. package/types/adapters/index.d.ts +20 -0
  122. package/types/adapters/maria-adapter.d.ts +117 -0
  123. package/types/adapters/mega-adapter.d.ts +215 -0
  124. package/types/adapters/mega-bus-adapter.d.ts +45 -0
  125. package/types/adapters/mega-cache-adapter.d.ts +73 -0
  126. package/types/adapters/mega-db-adapter.d.ts +50 -0
  127. package/types/adapters/mega-lock-adapter.d.ts +62 -0
  128. package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
  129. package/types/adapters/mega-session-adapter.d.ts +32 -0
  130. package/types/adapters/mongo-adapter.d.ts +150 -0
  131. package/types/adapters/nats-adapter.d.ts +108 -0
  132. package/types/adapters/postgres-adapter.d.ts +141 -0
  133. package/types/adapters/redis-adapter.d.ts +78 -0
  134. package/types/adapters/redis-session-adapter.d.ts +82 -0
  135. package/types/adapters/redlock-adapter.d.ts +149 -0
  136. package/types/adapters/registry.d.ts +46 -0
  137. package/types/adapters/sqlite-adapter.d.ts +112 -0
  138. package/types/auth/index.d.ts +24 -0
  139. package/types/cli/commands/console-cmd.d.ts +37 -0
  140. package/types/cli/commands/new.d.ts +16 -0
  141. package/types/cli/commands/routes.d.ts +36 -0
  142. package/types/cli/commands/scaffold.d.ts +78 -0
  143. package/types/cli/commands/test-cmd.d.ts +14 -0
  144. package/types/cli/generators/index.d.ts +122 -0
  145. package/types/cli/index.d.ts +234 -0
  146. package/types/cli/template-engine.d.ts +40 -0
  147. package/types/cli/watch.d.ts +59 -0
  148. package/types/core/ajv-mapper.d.ts +27 -0
  149. package/types/core/boot.d.ts +233 -0
  150. package/types/core/cluster-metrics.d.ts +52 -0
  151. package/types/core/config-loader.d.ts +13 -0
  152. package/types/core/config-validator.d.ts +30 -0
  153. package/types/core/ctx-builder.d.ts +103 -0
  154. package/types/core/envelope.d.ts +79 -0
  155. package/types/core/error-mapper.d.ts +17 -0
  156. package/types/core/formbody.d.ts +41 -0
  157. package/types/core/hub-link.d.ts +266 -0
  158. package/types/core/i18n.d.ts +178 -0
  159. package/types/core/index.d.ts +28 -0
  160. package/types/core/mega-app.d.ts +529 -0
  161. package/types/core/mega-cluster.d.ts +104 -0
  162. package/types/core/mega-server.d.ts +91 -0
  163. package/types/core/mega-service.d.ts +31 -0
  164. package/types/core/migration/dialect-registry.d.ts +22 -0
  165. package/types/core/migration/dialects/maria.d.ts +99 -0
  166. package/types/core/migration/dialects/mongo.d.ts +89 -0
  167. package/types/core/migration/dialects/postgres.d.ts +117 -0
  168. package/types/core/migration/dialects/sqlite.d.ts +111 -0
  169. package/types/core/migration/differ.d.ts +47 -0
  170. package/types/core/migration/generate.d.ts +56 -0
  171. package/types/core/migration/journal.d.ts +52 -0
  172. package/types/core/migration/model-scan.d.ts +19 -0
  173. package/types/core/migration/mongo-migration-db.d.ts +7 -0
  174. package/types/core/migration/schema-builder.d.ts +197 -0
  175. package/types/core/migration/schema-validator.d.ts +20 -0
  176. package/types/core/migration-lock.d.ts +33 -0
  177. package/types/core/migration-runner.d.ts +101 -0
  178. package/types/core/multipart.d.ts +86 -0
  179. package/types/core/openapi.d.ts +62 -0
  180. package/types/core/pipeline.d.ts +93 -0
  181. package/types/core/router.d.ts +159 -0
  182. package/types/core/routes-loader.d.ts +21 -0
  183. package/types/core/scope-registry.d.ts +14 -0
  184. package/types/core/security.d.ts +77 -0
  185. package/types/core/services-loader.d.ts +27 -0
  186. package/types/core/session-cleanup-schedule.d.ts +19 -0
  187. package/types/core/session-store.d.ts +25 -0
  188. package/types/core/session.d.ts +77 -0
  189. package/types/core/static-assets.d.ts +73 -0
  190. package/types/core/template.d.ts +106 -0
  191. package/types/core/workers-manager.d.ts +79 -0
  192. package/types/core/ws-cluster.d.ts +208 -0
  193. package/types/core/ws-compression.d.ts +112 -0
  194. package/types/core/ws-controller.d.ts +65 -0
  195. package/types/core/ws-message.d.ts +106 -0
  196. package/types/core/ws-presence.d.ts +273 -0
  197. package/types/core/ws-roster.d.ts +108 -0
  198. package/types/core/ws-upgrade.d.ts +260 -0
  199. package/types/errors/config-error.d.ts +10 -0
  200. package/types/errors/http-errors.d.ts +120 -0
  201. package/types/errors/index.d.ts +3 -0
  202. package/types/errors/mega-error.d.ts +32 -0
  203. package/types/index.d.ts +39 -0
  204. package/types/lib/asp/config.d.ts +49 -0
  205. package/types/lib/asp/crypto.d.ts +43 -0
  206. package/types/lib/asp/errors.d.ts +30 -0
  207. package/types/lib/asp/nonce-cache.d.ts +52 -0
  208. package/types/lib/asp/plugin.d.ts +30 -0
  209. package/types/lib/asp/ws-terminator.d.ts +45 -0
  210. package/types/lib/env-mapper.d.ts +14 -0
  211. package/types/lib/hub-protocol.d.ts +106 -0
  212. package/types/lib/index.d.ts +22 -0
  213. package/types/lib/logger/telegram-core.d.ts +104 -0
  214. package/types/lib/logger/telegram-transport.d.ts +45 -0
  215. package/types/lib/mega-brute-force.d.ts +66 -0
  216. package/types/lib/mega-circuit-breaker.d.ts +243 -0
  217. package/types/lib/mega-cron.d.ts +66 -0
  218. package/types/lib/mega-hash.d.ts +32 -0
  219. package/types/lib/mega-health.d.ts +48 -0
  220. package/types/lib/mega-job-queue.d.ts +188 -0
  221. package/types/lib/mega-job-worker.d.ts +130 -0
  222. package/types/lib/mega-job.d.ts +145 -0
  223. package/types/lib/mega-logger.d.ts +45 -0
  224. package/types/lib/mega-metrics.d.ts +285 -0
  225. package/types/lib/mega-plugin.d.ts +245 -0
  226. package/types/lib/mega-retry.d.ts +85 -0
  227. package/types/lib/mega-schedule.d.ts +260 -0
  228. package/types/lib/mega-shutdown.d.ts +135 -0
  229. package/types/lib/mega-tracing.d.ts +224 -0
  230. package/types/lib/mega-worker.d.ts +129 -0
  231. package/types/lib/otel-resource.d.ts +16 -0
  232. package/types/lib/worker-runner/process-entry.d.ts +1 -0
  233. package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
  234. package/types/lib/worker-runner/thread-entry.d.ts +1 -0
  235. package/types/lib/ws-hub.d.ts +259 -0
  236. package/types/models/crud-sql-builder.d.ts +48 -0
  237. package/types/models/index.d.ts +1 -0
  238. package/types/models/mega-model.d.ts +138 -0
  239. package/types/models/model-crud.d.ts +82 -0
  240. package/types/models/mongo-crud.d.ts +59 -0
  241. package/types/test/index.d.ts +84 -0
  242. package/.env +0 -127
  243. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
  244. package/sample/crud/apps/main/models/note.js +0 -71
  245. package/sample/crud/apps/main/models/user.js +0 -86
  246. package/sample/crud/package-lock.json +0 -5665
  247. package/sample/crud/yarn.lock +0 -2142
  248. package/sample/simple/package-lock.json +0 -1851
@@ -13,6 +13,21 @@
13
13
  <form method="post" action="/register" novalidate>
14
14
  <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
15
15
 
16
+ <div class="mb-3">
17
+ <label for="username" class="form-label"><%= t('field_username', '아이디') %></label>
18
+ <input
19
+ type="text"
20
+ class="form-control <%= invalid && invalid.username ? 'is-invalid' : '' %>"
21
+ id="username"
22
+ name="username"
23
+ value="<%= values && values.username ? values.username : '' %>"
24
+ placeholder="<%= t('field_username_ph', '예: hong123') %>"
25
+ autocomplete="username"
26
+ required
27
+ />
28
+ <div class="invalid-feedback"><%= t('field_username_required', '아이디를 입력하세요.') %></div>
29
+ </div>
30
+
16
31
  <div class="mb-3">
17
32
  <label for="name" class="form-label"><%= t('field_name', '이름') %></label>
18
33
  <input
@@ -29,18 +44,44 @@
29
44
  </div>
30
45
 
31
46
  <div class="mb-3">
32
- <label for="email" class="form-label"><%= t('field_email', '이메일') %></label>
47
+ <label for="nickname" class="form-label"><%= t('field_nickname', '별명') %></label>
48
+ <input
49
+ type="text"
50
+ class="form-control <%= invalid && invalid.nickname ? 'is-invalid' : '' %>"
51
+ id="nickname"
52
+ name="nickname"
53
+ value="<%= values && values.nickname ? values.nickname : '' %>"
54
+ placeholder="<%= t('field_nickname_ph', '예: 길동이') %>"
55
+ autocomplete="nickname"
56
+ required
57
+ />
58
+ <div class="invalid-feedback"><%= t('field_nickname_required', '별명을 입력하세요.') %></div>
59
+ </div>
60
+
61
+ <div class="mb-3">
62
+ <label for="email" class="form-label"><%= t('field_email', '이메일') %> <span class="text-body-secondary small">(<%= t('optional', '선택') %>)</span></label>
33
63
  <input
34
64
  type="email"
35
- class="form-control <%= invalid && invalid.email ? 'is-invalid' : '' %>"
65
+ class="form-control"
36
66
  id="email"
37
67
  name="email"
38
68
  value="<%= values && values.email ? values.email : '' %>"
39
69
  placeholder="<%= t('field_email_ph', '예: hong@example.com') %>"
40
- autocomplete="username"
41
- required
70
+ autocomplete="email"
71
+ />
72
+ </div>
73
+
74
+ <div class="mb-3">
75
+ <label for="phone_number" class="form-label"><%= t('field_phone', '전화번호') %> <span class="text-body-secondary small">(<%= t('optional', '선택') %>)</span></label>
76
+ <input
77
+ type="tel"
78
+ class="form-control"
79
+ id="phone_number"
80
+ name="phone_number"
81
+ value="<%= values && values.phone_number ? values.phone_number : '' %>"
82
+ placeholder="<%= t('field_phone_ph', '예: 010-1234-5678') %>"
83
+ autocomplete="tel"
42
84
  />
43
- <div class="invalid-feedback"><%= t('field_email_required', '올바른 이메일을 입력하세요.') %></div>
44
85
  </div>
45
86
 
46
87
  <div class="mb-4">
@@ -11,6 +11,19 @@
11
11
  <form method="post" action="/admin/users/<%= values.id %>" novalidate>
12
12
  <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
13
13
 
14
+ <div class="mb-3">
15
+ <label for="username" class="form-label"><%= t('field_username', '아이디') %></label>
16
+ <input
17
+ type="text"
18
+ class="form-control"
19
+ id="username"
20
+ value="<%= values && values.username ? values.username : '' %>"
21
+ readonly
22
+ disabled
23
+ />
24
+ <div class="form-text"><%= t('field_username_readonly', '아이디는 변경할 수 없습니다.') %></div>
25
+ </div>
26
+
14
27
  <div class="mb-3">
15
28
  <label for="name" class="form-label"><%= t('field_name', '이름') %></label>
16
29
  <input
@@ -25,18 +38,42 @@
25
38
  <div class="invalid-feedback"><%= t('field_name_required', '이름을 입력하세요.') %></div>
26
39
  </div>
27
40
 
28
- <div class="mb-4">
29
- <label for="email" class="form-label"><%= t('field_email', '이메일') %></label>
41
+ <div class="mb-3">
42
+ <label for="nickname" class="form-label"><%= t('field_nickname', '별명') %></label>
43
+ <input
44
+ type="text"
45
+ class="form-control <%= invalid && invalid.nickname ? 'is-invalid' : '' %>"
46
+ id="nickname"
47
+ name="nickname"
48
+ value="<%= values && values.nickname ? values.nickname : '' %>"
49
+ placeholder="<%= t('field_nickname_ph', '예: 길동이') %>"
50
+ required
51
+ />
52
+ <div class="invalid-feedback"><%= t('field_nickname_required', '별명을 입력하세요.') %></div>
53
+ </div>
54
+
55
+ <div class="mb-3">
56
+ <label for="email" class="form-label"><%= t('field_email', '이메일') %> <span class="text-body-secondary small">(<%= t('optional', '선택') %>)</span></label>
30
57
  <input
31
58
  type="email"
32
- class="form-control <%= invalid && invalid.email ? 'is-invalid' : '' %>"
59
+ class="form-control"
33
60
  id="email"
34
61
  name="email"
35
62
  value="<%= values && values.email ? values.email : '' %>"
36
63
  placeholder="<%= t('field_email_ph', '예: hong@example.com') %>"
37
- required
38
64
  />
39
- <div class="invalid-feedback"><%= t('field_email_required', '올바른 이메일을 입력하세요.') %></div>
65
+ </div>
66
+
67
+ <div class="mb-4">
68
+ <label for="phone_number" class="form-label"><%= t('field_phone', '전화번호') %> <span class="text-body-secondary small">(<%= t('optional', '선택') %>)</span></label>
69
+ <input
70
+ type="tel"
71
+ class="form-control"
72
+ id="phone_number"
73
+ name="phone_number"
74
+ value="<%= values && values.phone_number ? values.phone_number : '' %>"
75
+ placeholder="<%= t('field_phone_ph', '예: 010-1234-5678') %>"
76
+ />
40
77
  </div>
41
78
 
42
79
  <div class="d-flex gap-2">
@@ -24,7 +24,9 @@
24
24
  <thead>
25
25
  <tr>
26
26
  <th scope="col" class="text-end" style="width: 5rem"><%= t('col_id', 'ID') %></th>
27
+ <th scope="col"><%= t('col_username', '아이디') %></th>
27
28
  <th scope="col"><%= t('col_name', '이름') %></th>
29
+ <th scope="col"><%= t('col_nickname', '별명') %></th>
28
30
  <th scope="col"><%= t('col_email', '이메일') %></th>
29
31
  <th scope="col"><%= t('col_created', '생성일') %></th>
30
32
  <th scope="col" class="text-end"><%= t('col_actions', '관리') %></th>
@@ -34,8 +36,10 @@
34
36
  <% users.forEach(function (u) { %>
35
37
  <tr>
36
38
  <td class="text-end text-body-secondary"><%= u.id %></td>
37
- <td class="fw-medium"><%= u.name %></td>
38
- <td><%= u.email %></td>
39
+ <td class="fw-medium"><%= u.username %></td>
40
+ <td><%= u.name %></td>
41
+ <td><%= u.nickname %></td>
42
+ <td class="text-body-secondary"><%= u.email || '-' %></td>
39
43
  <td class="text-body-secondary small"><%= u.created_at %></td>
40
44
  <td class="text-end text-nowrap">
41
45
  <a href="/admin/users/<%= u.id %>/edit" class="btn btn-sm btn-outline-secondary"><%= t('action_edit', '수정') %></a>
@@ -11,6 +11,21 @@
11
11
  <form method="post" action="/admin/users" novalidate>
12
12
  <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
13
13
 
14
+ <div class="mb-3">
15
+ <label for="username" class="form-label"><%= t('field_username', '아이디') %></label>
16
+ <input
17
+ type="text"
18
+ class="form-control <%= invalid && invalid.username ? 'is-invalid' : '' %>"
19
+ id="username"
20
+ name="username"
21
+ value="<%= values && values.username ? values.username : '' %>"
22
+ placeholder="<%= t('field_username_ph', '예: hong123') %>"
23
+ autocomplete="off"
24
+ required
25
+ />
26
+ <div class="invalid-feedback"><%= t('field_username_required', '아이디를 입력하세요.') %></div>
27
+ </div>
28
+
14
29
  <div class="mb-3">
15
30
  <label for="name" class="form-label"><%= t('field_name', '이름') %></label>
16
31
  <input
@@ -25,18 +40,55 @@
25
40
  <div class="invalid-feedback"><%= t('field_name_required', '이름을 입력하세요.') %></div>
26
41
  </div>
27
42
 
28
- <div class="mb-4">
29
- <label for="email" class="form-label"><%= t('field_email', '이메일') %></label>
43
+ <div class="mb-3">
44
+ <label for="nickname" class="form-label"><%= t('field_nickname', '별명') %></label>
45
+ <input
46
+ type="text"
47
+ class="form-control <%= invalid && invalid.nickname ? 'is-invalid' : '' %>"
48
+ id="nickname"
49
+ name="nickname"
50
+ value="<%= values && values.nickname ? values.nickname : '' %>"
51
+ placeholder="<%= t('field_nickname_ph', '예: 길동이') %>"
52
+ required
53
+ />
54
+ <div class="invalid-feedback"><%= t('field_nickname_required', '별명을 입력하세요.') %></div>
55
+ </div>
56
+
57
+ <div class="mb-3">
58
+ <label for="email" class="form-label"><%= t('field_email', '이메일') %> <span class="text-body-secondary small">(<%= t('optional', '선택') %>)</span></label>
30
59
  <input
31
60
  type="email"
32
- class="form-control <%= invalid && invalid.email ? 'is-invalid' : '' %>"
61
+ class="form-control"
33
62
  id="email"
34
63
  name="email"
35
64
  value="<%= values && values.email ? values.email : '' %>"
36
65
  placeholder="<%= t('field_email_ph', '예: hong@example.com') %>"
66
+ />
67
+ </div>
68
+
69
+ <div class="mb-3">
70
+ <label for="phone_number" class="form-label"><%= t('field_phone', '전화번호') %> <span class="text-body-secondary small">(<%= t('optional', '선택') %>)</span></label>
71
+ <input
72
+ type="tel"
73
+ class="form-control"
74
+ id="phone_number"
75
+ name="phone_number"
76
+ value="<%= values && values.phone_number ? values.phone_number : '' %>"
77
+ placeholder="<%= t('field_phone_ph', '예: 010-1234-5678') %>"
78
+ />
79
+ </div>
80
+
81
+ <div class="mb-4">
82
+ <label for="password" class="form-label"><%= t('field_password', '비밀번호') %></label>
83
+ <input
84
+ type="password"
85
+ class="form-control <%= invalid && invalid.password ? 'is-invalid' : '' %>"
86
+ id="password"
87
+ name="password"
88
+ autocomplete="new-password"
37
89
  required
38
90
  />
39
- <div class="invalid-feedback"><%= t('field_email_required', '올바른 이메일을 입력하세요.') %></div>
91
+ <div class="form-text <%= invalid && invalid.password ? 'text-danger' : '' %>"><%= t('field_password_hint', '최소 8자 이상 입력하세요.') %></div>
40
92
  </div>
41
93
 
42
94
  <div class="d-flex gap-2">
@@ -0,0 +1,23 @@
1
+ # 🏗️ Log Table Partitioning Architecture
2
+
3
+ ## 📦 LogPartitionSchedule
4
+ - **Role**: 로그 테이블 파티션 자동 관리 스케줄러
5
+ - **Responsibilities**
6
+ - 매일 특정 시각(예: 새벽 2시)에 실행
7
+ - 미래(다음 달, 다다음 달) 파티션 테이블 자동 생성
8
+ - 과거(1개월 이전) 오래된 파티션 테이블 자동 분리(Detach) 및 삭제(Drop)
9
+ - **Concurrency Control**
10
+ - `static lock = { lock: 'log-partition', ttl: 60000 }` (Redlock을 활용하여 스케줄러 인스턴스 간 중복 실행 방지)
11
+
12
+ ## 📦 Database Partitioned Tables
13
+ - **Role**: 시간 기반 범위 파티셔닝(Range Partitioning by `created_at`)을 통한 로그 데이터 관리
14
+ - **Properties**
15
+ - `action_logs` (PARTITION BY RANGE (created_at))
16
+ - `wallet_logs` (PARTITION BY RANGE (created_at))
17
+ - `detail_logs` (PARTITION BY RANGE (created_at))
18
+ - **Responsibilities**
19
+ - 대량 로그 유입 시 쓰기 성능 향상 및 Vacuum 오버헤드 완화
20
+ - 보관 주기(1개월) 만료 데이터의 물리적 삭제 성능 최적화 (DROP PARTITION)
21
+ - **Constraints**
22
+ - Primary Key에 파티션 키 `created_at` 포함 필수: `PRIMARY KEY (id, created_at)`
23
+ - Unique Index에 파티션 키 `created_at` 포함 필수: `UNIQUE (uuid, created_at)` 등
@@ -4,6 +4,7 @@
4
4
  * App-only 키(databases 별명 등)는 apps/<name>/app.config.js 로.
5
5
  */
6
6
  import { CronCounterSchedule } from './apps/main/schedules/cron-counter-schedule.js'
7
+ import { LogPartitionSchedule } from './apps/main/schedules/log-partition-schedule.js'
7
8
  import { EmailJob } from './apps/main/jobs/email-job.js'
8
9
  import { HashWorker } from './apps/main/workers/hash-worker.js'
9
10
 
@@ -135,7 +136,7 @@ export default {
135
136
  // },
136
137
 
137
138
  // 정기 작업(ADR-028/118) — `mega scheduler` 프로세스가 등록·실행한다(config.schedules, ADR-123).
138
- schedules: [CronCounterSchedule],
139
+ schedules: [CronCounterSchedule, LogPartitionSchedule],
139
140
 
140
141
  // 영속 잡(ADR-028/119) — `mega worker` 프로세스(ecosystem instances:2)가 소비한다(config.jobs, ADR-123).
141
142
  jobs: [EmailJob],
@@ -168,10 +169,17 @@ export default {
168
169
  // (예시·미사용) liveness/readiness 경로 — 기본 '/health' · '/health/ready'.
169
170
  // paths: { live: '/health', ready: '/health/ready' },
170
171
  // (예시·미사용) 메트릭 resource service.name — 미지정 시 server.serviceName → MEGA_OTEL_SERVICE_NAME 폴백.
171
- // serviceName: 'sample-crud',
172
+ serviceName: 'sample-crud',
172
173
  },
173
174
 
174
175
  // OpenTelemetry 분산 트레이싱 — **config 블록이 아니라 .env 의 MEGA_OTEL_\*** 로 설정한다
175
176
  // (MegaTracing.fromEnv, boot.js). MEGA_OTEL_ENABLED='true' + MEGA_OTEL_SERVICE_NAME 필수.
176
177
  // (`tracing` 키는 스키마엔 있으나 현재 부팅이 소비하지 않음 — 죽은 설정 회피 위해 블록을 두지 않음.)
178
+
179
+ // dev watch(`mega start --watch`) ignore — .env 의 WATCH_IGNORE(콤마 구분 glob)가 정본이다
180
+ // (ADR-220, 숨은 코드 디폴트 없음). 폴더명을 바꾼 프로젝트는 .env 의 그 줄만 고치면 된다.
181
+ // 미설정이면 ignore 0 — 모든 변경이 재시작 대상(기동 시 경고 1줄).
182
+ watch: {
183
+ ignore: (process.env.WATCH_IGNORE ?? '').split(',').map((p) => p.trim()).filter(Boolean),
184
+ },
177
185
  }
@@ -7,7 +7,7 @@
7
7
  "node": ">=20"
8
8
  },
9
9
  "scripts": {
10
- "dev": "mega start --watch",
10
+ "dev": "NODE_ENV=development mega start --watch",
11
11
  "start": "NODE_ENV=production mega start",
12
12
  "migrate": "mega migrate",
13
13
  "migrate:down": "mega migrate:down",
@@ -23,6 +23,6 @@
23
23
  },
24
24
  "devDependencies": {
25
25
  "concurrently": "^9.0.0",
26
- "vitest": "^4.0.0"
26
+ "vitest": "^4.1.8"
27
27
  }
28
- }
28
+ }
@@ -3,11 +3,11 @@
3
3
  # sample/crud — WS Hub 서버 기동 스크립트 (ADR-032/176)
4
4
  #
5
5
  # 별도 hub 프로세스(`mega-ws-hub` 바이너리, mega-framework bin)를 localhost:3100 에 띄운다.
6
- # 앱(`yarn dev` = `mega start`)이 app.config 의 `bridgeHub` 로 이 허브에 **자동 연결**한다(ADR-176).
6
+ # 앱(`npm run dev` = `mega start`)이 app.config 의 `bridgeHub` 로 이 허브에 **자동 연결**한다(ADR-176).
7
7
  #
8
8
  # ⚠️ `mega-ws-hub` 는 `mega start` 와 달리 `.env` 를 자동 로드하지 않는다(직접 process.env 만 읽음,
9
- # src/cli/ws-hub.js). 그래서 Node 20.6+ 의 `--env-file` 로 .env 를 주입한다.
10
- # 읽는 env(src/cli/ws-hub.js runWsHubCli): MEGA_WSHUB_TOKENS(필수, 콤마구분) /
9
+ # src/lib/ws-hub.js). 그래서 Node 20.6+ 의 `--env-file` 로 .env 를 주입한다.
10
+ # 읽는 env(src/lib/ws-hub.js runWsHubCli): MEGA_WSHUB_TOKENS(필수, 콤마구분) /
11
11
  # MEGA_WSHUB_PORT(기본 3100) / MEGA_WSHUB_HOST(기본 0.0.0.0) / MEGA_WSHUB_HEARTBEAT_MS.
12
12
  #
13
13
  # 사용:
@@ -22,15 +22,29 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
22
22
  cd "$ROOT"
23
23
 
24
24
  ENV_FILE=".env"
25
- BIN="node_modules/mega-framework/bin/mega-ws-hub.js"
25
+
26
+ # mega-ws-hub 바이너리 탐색 — 두 설치 형상을 모두 지원한다:
27
+ # ① 독립 프로젝트(mega new 스캐폴드): 자기 node_modules 에 설치됨.
28
+ # ② 모노레포 workspaces(ADR-197): 의존성이 레포 루트로 hoist 되어 sample/crud 에는 node_modules 가
29
+ # 없다(정상). 루트의 mega-framework self-link 를 따라간다.
30
+ BIN=""
31
+ for candidate in \
32
+ "node_modules/mega-framework/bin/mega-ws-hub.js" \
33
+ "../../node_modules/mega-framework/bin/mega-ws-hub.js"; do
34
+ if [ -f "$candidate" ]; then
35
+ BIN="$candidate"
36
+ break
37
+ fi
38
+ done
26
39
 
27
40
  # 사전 점검 — 누락 시 silent 진행 금지, 이유를 명확히 알린다(P4/P7).
28
41
  [ -f "$ENV_FILE" ] || {
29
42
  echo "✗ $ROOT/$ENV_FILE 가 없습니다 — MEGA_WSHUB_TOKENS 등 허브 설정이 필요합니다." >&2
30
43
  exit 1
31
44
  }
32
- [ -f "$BIN" ] || {
33
- echo "✗ $BIN 없음 먼저 'yarn install --ignore-engines'(또는 npm install) 로 mega-framework 를 설치/동기화하세요." >&2
45
+ [ -n "$BIN" ] || {
46
+ echo "✗ mega-ws-hub.js 찾지 못했습니다(로컬/루트 node_modules 모두) 'npm install' 로 mega-framework 를 설치하세요." >&2
47
+ echo " (모노레포에서는 레포 루트에서 실행해야 합니다. yarn 은 미지원 — npm 사용.)" >&2
34
48
  exit 1
35
49
  }
36
50
 
@@ -0,0 +1 @@
1
+ {"version":"4.1.8","results":[[":test/apps/main/index.test.js",{"duration":1.6905000000000001,"failed":false}]]}
@@ -16,10 +16,10 @@
16
16
  "test": "mega test"
17
17
  },
18
18
  "dependencies": {
19
- "mega-framework": "^0.1.1"
19
+ "mega-framework": "file:../.."
20
20
  },
21
21
  "devDependencies": {
22
22
  "concurrently": "^9.0.0",
23
- "vitest": "^2.0.0"
23
+ "vitest": "^4.1.8"
24
24
  }
25
25
  }
@@ -127,7 +127,8 @@ export function buildFromGlobalConfig(globalConfig, { registerShutdownHook = tru
127
127
  if (registerShutdownHook) {
128
128
  // 재빌드 안전 — 항상 1개만 유지(MegaApp 의 hublink hook 패턴과 동일).
129
129
  MegaShutdown.unregister(SHUTDOWN_HOOK)
130
- MegaShutdown.register(SHUTDOWN_HOOK, async () => disconnectAll())
130
+ // 'adapters' stage — 서버/잡/앱 정리(어댑터를 쓰는 정리) 모두 끝난 뒤 disconnect 된다.
131
+ MegaShutdown.register(SHUTDOWN_HOOK, async () => disconnectAll(), { stage: 'adapters' })
131
132
  }
132
133
  }
133
134
 
@@ -47,6 +47,36 @@ function invalidOption(message, details) {
47
47
  return new MegaValidationError('adapter.invalid_option', message, { details })
48
48
  }
49
49
 
50
+ /**
51
+ * `withTransaction(fn, opts)` 의 isolation 값 → 표준 SQL 조각 (ADR-190).
52
+ * 화이트리스트 매핑이라 SQL 인젝션이 구조적으로 불가능하다(임의 문자열은 throw).
53
+ * @type {Record<string, string>}
54
+ */
55
+ const TX_ISOLATION_SQL = {
56
+ 'read uncommitted': 'READ UNCOMMITTED',
57
+ 'read committed': 'READ COMMITTED',
58
+ 'repeatable read': 'REPEATABLE READ',
59
+ serializable: 'SERIALIZABLE',
60
+ }
61
+
62
+ /**
63
+ * 트랜잭션 격리수준 옵션 검증 + SQL 조각 변환 (ADR-190). `undefined` 는 driver 디폴트 위임(undefined 반환).
64
+ * @param {unknown} isolation - `withTransaction` 의 `opts.isolation`.
65
+ * @param {string} driver - 에러 메시지용 driver 이름.
66
+ * @returns {string | undefined} `SET TRANSACTION ISOLATION LEVEL <조각>` 에 쓸 SQL 조각.
67
+ * @throws {MegaValidationError} `adapter.invalid_option` - 화이트리스트 외 값.
68
+ */
69
+ export function resolveTxIsolation(isolation, driver) {
70
+ if (isolation === undefined) return undefined
71
+ if (typeof isolation !== 'string' || !Object.prototype.hasOwnProperty.call(TX_ISOLATION_SQL, isolation)) {
72
+ throw invalidOption(
73
+ `${driver}: withTransaction "isolation" must be one of 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable'. Got ${JSON.stringify(isolation)}.`,
74
+ { driver, option: 'isolation', value: isolation },
75
+ )
76
+ }
77
+ return TX_ISOLATION_SQL[isolation]
78
+ }
79
+
50
80
  /**
51
81
  * 양의 정수(> 0) 검증 (undefined 통과). 위반 시 `adapter.invalid_option` throw.
52
82
  * @param {string} name @param {unknown} value @param {Record<string, unknown>} [extra] @returns {void}
@@ -135,6 +165,9 @@ export function resolveConnection(config, { driver, dbKey = 'database', dbConfli
135
165
  return out
136
166
  }
137
167
 
168
+ /** pool acquire 대기 한도 프레임워크 디폴트(ms) — 명시 0 으로 드라이버 무한 대기 옵트인(ADR-216). */
169
+ export const DEFAULT_ACQUIRE_TIMEOUT_MS = 10_000
170
+
138
171
  /**
139
172
  * pg 풀 매핑 — 값이 null 이면 미지원(throw), `{ key, divideBy? }` 면 키 이름 변경(+단위 변환).
140
173
  * @type {Record<string, { key: string, divideBy?: number } | null>}
@@ -186,11 +219,11 @@ export const MONGO_POOL_SPEC = {
186
219
  * @returns {Record<string, number>} 드라이버 풀 옵션 객체(빈 객체 가능).
187
220
  */
188
221
  export function normalizePool(pool, spec, driver) {
189
- if (pool === undefined) return {}
190
- assertPlainObject('pool', pool, { driver })
191
222
  /** @type {Record<string, number>} */
192
223
  const out = {}
193
- for (const [key, value] of Object.entries(/** @type {Record<string, unknown>} */ (pool))) {
224
+ if (pool !== undefined) assertPlainObject('pool', pool, { driver })
225
+ const entries = pool === undefined ? [] : Object.entries(/** @type {Record<string, unknown>} */ (pool))
226
+ for (const [key, value] of entries) {
194
227
  if (value === undefined) continue
195
228
  const map = spec[key]
196
229
  if (map === undefined) {
@@ -204,5 +237,13 @@ export function normalizePool(pool, spec, driver) {
204
237
  const num = /** @type {number} */ (value)
205
238
  out[map.key] = map.divideBy ? Math.floor(num / map.divideBy) : num
206
239
  }
240
+ // 프레임워크 디폴트(ADR-216 G2 H-1): acquire 무한 대기(pg connectionTimeoutMillis=0 ·
241
+ // mongo waitQueueTimeoutMS=0 드라이버 디폴트)는 연결 leak 1건을 전체 서비스 행으로 키운다 —
242
+ // 미지정 시 10s 를 기본 적용한다(maria 드라이버 디폴트 10s 와 정렬). 명시 `acquireTimeoutMs: 0`
243
+ // 은 드라이버 무한 대기 의미 그대로 통과(무한 옵트인).
244
+ const acquireMap = spec.acquireTimeoutMs
245
+ if (acquireMap !== null && acquireMap !== undefined && out[acquireMap.key] === undefined) {
246
+ out[acquireMap.key] = DEFAULT_ACQUIRE_TIMEOUT_MS
247
+ }
207
248
  return out
208
249
  }
@@ -75,6 +75,8 @@ const KNOWN_OPTIONS = new Set(['serializer', 'extension'])
75
75
  * @property {string} [basePath] - 캐시 파일 저장 디렉토리 (필수).
76
76
  * @property {string} [dir] - `basePath` 의 별칭 (ADR-082 정합, 하위 호환).
77
77
  * @property {{ serializer?: 'json' | 'raw', extension?: string }} [options]
78
+ * @property {string} [namespace] - 캐시 키 자동 prefix `mega:cache:<namespace>:` (ADR-064/213, 베이스 처리).
79
+ * @property {number} [defaultTtlSec] - `set` ttl 미지정 시 디폴트(초). 0 = 무한 옵트인. (ADR-216, 베이스 처리)
78
80
  */
79
81
 
80
82
  /**
@@ -94,7 +96,8 @@ export class MegaFileAdapter extends MegaCacheAdapter {
94
96
  #extension
95
97
 
96
98
  /**
97
- * @param {FileConfig} [config] - services.caches.<key> 설정.
99
+ * @param {FileConfig} [config] - services.caches.<key> 설정. 베이스(MegaCacheAdapter)의
100
+ * `namespace`(ADR-064 자동 prefix)/`defaultTtlSec`(ADR-216) 도 여기서 받는다.
98
101
  * @throws {MegaValidationError} `adapter.basepath_required` - basePath/dir 누락.
99
102
  * @throws {MegaValidationError} `adapter.invalid_option` - 옵션 타입/미지원 키 오류.
100
103
  */
@@ -245,7 +248,7 @@ export class MegaFileAdapter extends MegaCacheAdapter {
245
248
  */
246
249
  async get(key) {
247
250
  return this._instrument('get', { key }, async () => {
248
- const path = this.#pathFor(key)
251
+ const path = this.#pathFor(this._cacheKey(key))
249
252
  let raw
250
253
  try {
251
254
  raw = await readFile(path, 'utf8')
@@ -278,6 +281,7 @@ export class MegaFileAdapter extends MegaCacheAdapter {
278
281
  // TTL·직렬화 검증은 의도적으로 `_instrument` **밖**(fail-fast). 잘못된 인자는 디스크 I/O·hook·stats
279
282
  // 이전에 거부 — 프로그래밍 오류를 instrumented 호출 통계에 섞지 않는다(L-1, redis 어댑터와 동일 결정).
280
283
  this._assertTtl(ttl)
284
+ ttl = this._resolveTtl(ttl, key) // 미지정 → defaultTtlSec(ADR-216). 디폴트 값은 생성자에서 검증됨.
281
285
  if (this.#serializer === 'raw' && typeof value !== 'string') {
282
286
  throw new MegaValidationError('cache.unserializable', `file set("${key}"): serializer='raw' requires a string value (got ${typeof value}).`, {
283
287
  details: { key, type: typeof value, serializer: 'raw' },
@@ -300,7 +304,7 @@ export class MegaFileAdapter extends MegaCacheAdapter {
300
304
  value,
301
305
  expiresAt: ttl !== undefined ? Date.now() + ttl * 1000 : null,
302
306
  }
303
- await this.#atomicWrite(this.#pathFor(key), envelope)
307
+ await this.#atomicWrite(this.#pathFor(this._cacheKey(key)), envelope)
304
308
  })
305
309
  }
306
310
 
@@ -312,7 +316,7 @@ export class MegaFileAdapter extends MegaCacheAdapter {
312
316
  async del(key) {
313
317
  return this._instrument('del', { key }, async () => {
314
318
  try {
315
- await unlink(this.#pathFor(key))
319
+ await unlink(this.#pathFor(this._cacheKey(key)))
316
320
  } catch (err) {
317
321
  // ENOENT = 이미 없음(del idempotent — 정상). 그 외 I/O 에러는 전파(무차별 삼킴 X).
318
322
  if (/** @type {any} */ (err)?.code !== 'ENOENT') throw err
@@ -331,7 +335,7 @@ export class MegaFileAdapter extends MegaCacheAdapter {
331
335
  */
332
336
  async has(key) {
333
337
  return this._instrument('has', { key }, async () => {
334
- const path = this.#pathFor(key)
338
+ const path = this.#pathFor(this._cacheKey(key))
335
339
  let raw
336
340
  try {
337
341
  raw = await readFile(path, 'utf8')
@@ -199,11 +199,12 @@ export class MegaFileSessionAdapter extends MegaSessionAdapter {
199
199
  }
200
200
 
201
201
  /**
202
- * 누적 통계 + file 세션 특화.
203
- * @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, basePath: string, ttlMs: number }}
202
+ * 누적 통계 + file 세션 특화. `cleanupIntervalMs` 노출(0=내부 타이머 off) — 만료 스캔 주기의
203
+ * 적용 여부를 운영/테스트가 확인하는 단일 창구(ADR-215).
204
+ * @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, basePath: string, ttlMs: number, cleanupIntervalMs: number }}
204
205
  */
205
206
  getStats() {
206
- return { ...super.getStats(), driver: 'file', basePath: this.#basePath, ttlMs: this.#ttlMs }
207
+ return { ...super.getStats(), driver: 'file', basePath: this.#basePath, ttlMs: this.#ttlMs, cleanupIntervalMs: this.#cleanupIntervalMs }
207
208
  }
208
209
 
209
210
  /**