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
@@ -0,0 +1,261 @@
1
+ {
2
+ "version": 1,
3
+ "generatedAt": "2026-06-12T09:25:43.485Z",
4
+ "adapters": {
5
+ "primary": {
6
+ "driver": "postgres",
7
+ "models": [
8
+ {
9
+ "name": "UserLevelModel",
10
+ "table": "user_levels",
11
+ "record": {
12
+ "table": "user_levels",
13
+ "adapter": "primary",
14
+ "columns": {
15
+ "id": {
16
+ "type": "serial",
17
+ "primary": true
18
+ },
19
+ "code": {
20
+ "type": "text",
21
+ "notNull": true,
22
+ "unique": true,
23
+ "comment": "회원 등급 코드"
24
+ },
25
+ "type_code": {
26
+ "type": "text",
27
+ "notNull": true,
28
+ "references": {
29
+ "model": "UserTypeModel",
30
+ "column": "code",
31
+ "onDelete": "cascade",
32
+ "table": "user_types"
33
+ },
34
+ "comment": "회원 유형 코드"
35
+ },
36
+ "sort_order": {
37
+ "type": "integer",
38
+ "notNull": true,
39
+ "default": 0,
40
+ "comment": "정렬 순서"
41
+ },
42
+ "name": {
43
+ "type": "text",
44
+ "notNull": true,
45
+ "comment": "회원 등급명"
46
+ },
47
+ "created_at": {
48
+ "type": "timestamptz",
49
+ "notNull": true,
50
+ "default": {
51
+ "raw": "now()"
52
+ },
53
+ "comment": "생성 시간"
54
+ }
55
+ },
56
+ "indexes": [
57
+ {
58
+ "columns": [
59
+ "type_code"
60
+ ],
61
+ "name": "idx_user_levels_type_code"
62
+ }
63
+ ]
64
+ }
65
+ },
66
+ {
67
+ "name": "UserModel",
68
+ "table": "users",
69
+ "record": {
70
+ "table": "users",
71
+ "adapter": "primary",
72
+ "columns": {
73
+ "id": {
74
+ "type": "bigSerial",
75
+ "primary": true
76
+ },
77
+ "uuid": {
78
+ "type": "uuid",
79
+ "notNull": true,
80
+ "default": {
81
+ "raw": "gen_random_uuid()"
82
+ },
83
+ "comment": "고유값"
84
+ },
85
+ "username": {
86
+ "type": "text",
87
+ "notNull": true,
88
+ "unique": true,
89
+ "comment": "로그인 아이디"
90
+ },
91
+ "password_hash": {
92
+ "type": "text",
93
+ "notNull": true,
94
+ "comment": "비밀번호 해시"
95
+ },
96
+ "type_code": {
97
+ "type": "text",
98
+ "notNull": true,
99
+ "default": "USER",
100
+ "comment": "회원 유형"
101
+ },
102
+ "level_code": {
103
+ "type": "text",
104
+ "notNull": true,
105
+ "default": "lv1",
106
+ "comment": "회원 등급"
107
+ },
108
+ "name": {
109
+ "type": "text",
110
+ "notNull": true,
111
+ "comment": "실명"
112
+ },
113
+ "nickname": {
114
+ "type": "text",
115
+ "notNull": true,
116
+ "unique": true,
117
+ "comment": "별명"
118
+ },
119
+ "email": {
120
+ "type": "text",
121
+ "comment": "이메일"
122
+ },
123
+ "phone_number": {
124
+ "type": "text",
125
+ "comment": "전화번호"
126
+ },
127
+ "last_login_at": {
128
+ "type": "timestamptz",
129
+ "comment": "마지막 로그인 시간"
130
+ },
131
+ "created_at": {
132
+ "type": "timestamptz",
133
+ "notNull": true,
134
+ "default": {
135
+ "raw": "now()"
136
+ },
137
+ "comment": "생성 시간"
138
+ }
139
+ },
140
+ "indexes": [
141
+ {
142
+ "columns": [
143
+ "uuid"
144
+ ],
145
+ "name": "idx_users_uuid",
146
+ "unique": true
147
+ },
148
+ {
149
+ "columns": [
150
+ "email"
151
+ ],
152
+ "name": "idx_users_email"
153
+ },
154
+ {
155
+ "columns": [
156
+ "type_code"
157
+ ],
158
+ "name": "idx_users_type_code"
159
+ },
160
+ {
161
+ "columns": [
162
+ "level_code"
163
+ ],
164
+ "name": "idx_users_level_code"
165
+ }
166
+ ]
167
+ }
168
+ },
169
+ {
170
+ "name": "UserTypeModel",
171
+ "table": "user_types",
172
+ "record": {
173
+ "table": "user_types",
174
+ "adapter": "primary",
175
+ "columns": {
176
+ "id": {
177
+ "type": "serial",
178
+ "primary": true
179
+ },
180
+ "code": {
181
+ "type": "text",
182
+ "notNull": true,
183
+ "unique": true,
184
+ "comment": "회원 유형 코드"
185
+ },
186
+ "sort_order": {
187
+ "type": "integer",
188
+ "notNull": true,
189
+ "default": 0,
190
+ "comment": "정렬 순서"
191
+ },
192
+ "name": {
193
+ "type": "text",
194
+ "notNull": true,
195
+ "comment": "회원 유형명"
196
+ },
197
+ "created_at": {
198
+ "type": "timestamptz",
199
+ "notNull": true,
200
+ "default": {
201
+ "raw": "now()"
202
+ },
203
+ "comment": "생성 시간"
204
+ }
205
+ },
206
+ "indexes": []
207
+ }
208
+ },
209
+ {
210
+ "name": "WalletModel",
211
+ "table": "wallets",
212
+ "record": {
213
+ "table": "wallets",
214
+ "adapter": "primary",
215
+ "columns": {
216
+ "id": {
217
+ "type": "serial",
218
+ "primary": true
219
+ },
220
+ "user_id": {
221
+ "type": "bigInteger",
222
+ "notNull": true,
223
+ "references": {
224
+ "model": "UserModel",
225
+ "column": "id",
226
+ "onDelete": "cascade",
227
+ "table": "users"
228
+ },
229
+ "comment": "회원 ID"
230
+ },
231
+ "balance": {
232
+ "type": "decimal",
233
+ "precision": 20,
234
+ "scale": 4,
235
+ "notNull": true,
236
+ "default": 0,
237
+ "comment": "잔액"
238
+ },
239
+ "created_at": {
240
+ "type": "timestamptz",
241
+ "notNull": true,
242
+ "default": {
243
+ "raw": "now()"
244
+ },
245
+ "comment": "생성 시간"
246
+ }
247
+ },
248
+ "indexes": [
249
+ {
250
+ "columns": [
251
+ "user_id"
252
+ ],
253
+ "name": "idx_wallets_user_id"
254
+ }
255
+ ]
256
+ }
257
+ }
258
+ ]
259
+ }
260
+ }
261
+ }
@@ -45,22 +45,22 @@ export class AuthController {
45
45
 
46
46
  /** POST /auth/login — 인증 → 세션 생성 → 관리 UI. 잠김/실패 시 폼 재렌더. @param {any} req @param {any} reply @param {any} ctx */
47
47
  static async login(req, reply, ctx) {
48
- const email = typeof req.body?.email === 'string' ? req.body.email.trim().toLowerCase() : ''
48
+ const username = typeof req.body?.username === 'string' ? req.body.username.trim() : ''
49
49
  const password = typeof req.body?.password === 'string' ? req.body.password : ''
50
- // subject = IP:emailemail 단독 잠금은 피해자 계정을 공격자가 잠그는 DoS 가 가능(I-1).
51
- const subject = `${req.ip}:${email}`
50
+ // subject = IP:usernameusername 단독 잠금은 피해자 계정을 공격자가 잠그는 DoS 가 가능(I-1).
51
+ const subject = `${req.ip}:${username}`
52
52
  const bf = ctx.bruteForce
53
53
 
54
54
  const status = await bf.check(subject)
55
55
  if (status.isLocked) {
56
- return AuthController.#renderLogin(reply, ctx, { email }, ctx.t('login_locked', '로그인 시도가 너무 많습니다. 잠시 후 다시 시도하세요.'), 423)
56
+ return AuthController.#renderLogin(reply, ctx, { username }, ctx.t('login_locked', '로그인 시도가 너무 많습니다. 잠시 후 다시 시도하세요.'), 423)
57
57
  }
58
58
 
59
- const user = await ctx.services.auth.authenticate({ email, password })
59
+ const user = await ctx.services.auth.authenticate({ username, password })
60
60
  if (!user) {
61
61
  const after = await bf.fail(subject)
62
- const msg = after.isLocked ? ctx.t('login_locked', '로그인 시도가 너무 많습니다. 잠시 후 다시 시도하세요.') : ctx.t('login_failed', '이메일 또는 비밀번호가 올바르지 않습니다.')
63
- return AuthController.#renderLogin(reply, ctx, { email }, msg, after.isLocked ? 423 : 401)
62
+ const msg = after.isLocked ? ctx.t('login_locked', '로그인 시도가 너무 많습니다. 잠시 후 다시 시도하세요.') : ctx.t('login_failed', '아이디 또는 비밀번호가 올바르지 않습니다.')
63
+ return AuthController.#renderLogin(reply, ctx, { username }, msg, after.isLocked ? 423 : 401)
64
64
  }
65
65
 
66
66
  await bf.reset(subject)
@@ -99,7 +99,13 @@ export class AuthController {
99
99
  reply.code(err instanceof MegaConflictError ? 409 : 400)
100
100
  return ctx.render('auth/register', {
101
101
  title: ctx.t('register_title', '회원가입'),
102
- values: { name: req.body?.name ?? '', email: req.body?.email ?? '' },
102
+ values: {
103
+ username: req.body?.username ?? '',
104
+ name: req.body?.name ?? '',
105
+ nickname: req.body?.nickname ?? '',
106
+ email: req.body?.email ?? '',
107
+ phone_number: req.body?.phone_number ?? '',
108
+ },
103
109
  invalid: AuthController.#invalidFields(err),
104
110
  error: err.message,
105
111
  currentUser: null,
@@ -112,7 +118,7 @@ export class AuthController {
112
118
 
113
119
  /**
114
120
  * 로그인 폼 재렌더 헬퍼(상태코드 + 에러 메시지 + 입력 보존 + 새 CSRF 토큰).
115
- * @param {any} reply @param {any} ctx @param {{ email: string }} values @param {string} error @param {number} code
121
+ * @param {any} reply @param {any} ctx @param {{ username: string }} values @param {string} error @param {number} code
116
122
  */
117
123
  static #renderLogin(reply, ctx, values, error, code) {
118
124
  reply.code(code)
@@ -128,17 +134,19 @@ export class AuthController {
128
134
 
129
135
  /**
130
136
  * 회원가입 검증/충돌 에러 → 폼 is-invalid 표시용 필드 맵.
131
- * @param {unknown} err @returns {{ name: boolean, email: boolean, password: boolean }}
137
+ * @param {unknown} err @returns {{ username: boolean, name: boolean, nickname: boolean, password: boolean }}
132
138
  */
133
139
  static #invalidFields(err) {
134
140
  if (err instanceof MegaValidationError) {
135
141
  const d = /** @type {any} */ (err).details ?? {}
136
- // auth-service 는 details.{name,email,password} = "값이 유효한가"(true=정상) 로 준다 → 없으면 invalid.
137
- return { name: !d.name, email: !d.email, password: !d.password }
142
+ // auth-service 는 details.{username,name,nickname,password} = "값이 유효한가"(true=정상) → 없으면 invalid.
143
+ return { username: !d.username, name: !d.name, nickname: !d.nickname, password: !d.password }
138
144
  }
139
145
  if (err instanceof MegaConflictError) {
140
- return { name: false, email: true, password: false } // 이메일 중복.
146
+ // username/nickname 중복 code 구분(user.nickname_taken vs user.username_taken).
147
+ const isNick = /** @type {any} */ (err).code === 'user.nickname_taken'
148
+ return { username: !isNick, name: false, nickname: isNick, password: false }
141
149
  }
142
- return { name: false, email: false, password: false }
150
+ return { username: false, name: false, nickname: false, password: false }
143
151
  }
144
152
  }
@@ -19,18 +19,20 @@ const NOTICE_KEYS = new Set(['created', 'updated', 'deleted'])
19
19
  /**
20
20
  * 검증/충돌 에러에서 어떤 필드가 잘못됐는지 계산한다(폼 is-invalid 표시용).
21
21
  * @param {unknown} err
22
- * @returns {{ name: boolean, email: boolean }}
22
+ * @returns {{ username: boolean, name: boolean, nickname: boolean, password: boolean }}
23
23
  */
24
24
  function invalidFields(err) {
25
25
  if (err instanceof MegaValidationError) {
26
26
  const d = /** @type {any} */ (err).details ?? {}
27
- // user-service 는 details.{name,email} = "값이 있는가"(true=정상) 로 준다 → 없으면 invalid.
28
- return { name: !d.name, email: !d.email }
27
+ // user-service 는 details.{username,name,nickname,password} = "값이 유효한가"(true=정상) → 없으면 invalid.
28
+ return { username: !d.username, name: !d.name, nickname: !d.nickname, password: !d.password }
29
29
  }
30
30
  if (err instanceof MegaConflictError) {
31
- return { name: false, email: true } // 이메일 중복.
31
+ // username/nickname 중복 code 구분.
32
+ const isNick = /** @type {any} */ (err).code === 'user.nickname_taken'
33
+ return { username: !isNick, name: false, nickname: isNick, password: false }
32
34
  }
33
- return { name: false, email: false }
35
+ return { username: false, name: false, nickname: false, password: false }
34
36
  }
35
37
 
36
38
  export class WebController {
@@ -313,5 +313,16 @@
313
313
  "csrf": {
314
314
  "invalid_token": "Invalid csrf token",
315
315
  "missing_secret": "Missing csrf secret"
316
- }
316
+ },
317
+ "field_username": "아이디",
318
+ "field_username_ph": "예: hong123",
319
+ "field_username_required": "아이디를 입력하세요.",
320
+ "field_nickname": "별명",
321
+ "field_nickname_ph": "예: 길동이",
322
+ "field_nickname_required": "별명을 입력하세요.",
323
+ "optional": "선택",
324
+ "field_phone": "전화번호",
325
+ "field_phone_ph": "예: 010-1234-5678",
326
+ "col_username": "아이디",
327
+ "col_nickname": "별명"
317
328
  }
@@ -313,5 +313,16 @@
313
313
  },
314
314
  "guide": {
315
315
  "not_found": "가이드를 찾을 수 없습니다."
316
- }
316
+ },
317
+ "field_username": "아이디",
318
+ "field_username_ph": "예: hong123",
319
+ "field_username_required": "아이디를 입력하세요.",
320
+ "field_nickname": "별명",
321
+ "field_nickname_ph": "예: 길동이",
322
+ "field_nickname_required": "별명을 입력하세요.",
323
+ "optional": "선택",
324
+ "field_phone": "전화번호",
325
+ "field_phone_ph": "예: 010-1234-5678",
326
+ "col_username": "아이디",
327
+ "col_nickname": "별명"
317
328
  }
@@ -1,27 +1,105 @@
1
1
  // @ts-check
2
2
  /**
3
- * 마이그레이션 create-users (20260606000001). up = users 테이블 생성, down = 제거(ADR-149).
4
- * `mega migrate`(up)·`mega migrate:down`·`mega migrate:status` 로 실행. 적용 이력은 대상 DB 의
5
- * `mega_migrations` 테이블이 추적하며, 마이그레이션은 트랜잭션으로 감싸 적용된다.
3
+ * 마이그레이션 create-users (20260612092543) `mega migrate:generate` 자동 생성 (ADR-204).
4
+ * 기준 스냅샷: .mega/journal/history/20260612092543-create-users.json
5
+ * 적용 SQL 검토 필수 파괴적 변경(-- 경고)·캐스트(-- TODO) 주석이 있으면 직접 확정하세요.
6
+ * 적용은 `mega migrate`(락·checksum 은 러너 관리, ADR-149/190), 롤백은 `mega migrate:down`.
6
7
  *
7
8
  * @param {{ query: (sql: string, params?: any[]) => Promise<any> }} db - 대상 DB 어댑터(트랜잭션 내 query).
8
9
  * @returns {Promise<void>}
9
10
  */
10
11
  export async function up(db) {
11
- await db.query(`
12
- CREATE TABLE IF NOT EXISTS users (
13
- id SERIAL PRIMARY KEY,
14
- name TEXT NOT NULL,
15
- email TEXT NOT NULL UNIQUE,
16
- created_at TIMESTAMPTZ NOT NULL DEFAULT now()
17
- )
18
- `)
12
+ await db.query(`CREATE TABLE user_levels (
13
+ id SERIAL PRIMARY KEY,
14
+ code TEXT NOT NULL,
15
+ type_code TEXT NOT NULL,
16
+ sort_order INTEGER NOT NULL DEFAULT 0,
17
+ name TEXT NOT NULL,
18
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
19
+ )`)
20
+ await db.query(`COMMENT ON TABLE user_levels IS '회원 등급 테이블'`)
21
+ await db.query(`ALTER TABLE user_levels ADD CONSTRAINT uniq_user_levels_code UNIQUE (code)`)
22
+ await db.query(`COMMENT ON COLUMN user_levels.code IS '회원 등급 코드'`)
23
+ await db.query(`COMMENT ON COLUMN user_levels.type_code IS '회원 유형 코드'`)
24
+ await db.query(`COMMENT ON COLUMN user_levels.sort_order IS '정렬 순서'`)
25
+ await db.query(`COMMENT ON COLUMN user_levels.name IS '회원 등급명'`)
26
+ await db.query(`COMMENT ON COLUMN user_levels.created_at IS '생성 시간'`)
27
+ await db.query(`CREATE TABLE users (
28
+ id BIGSERIAL PRIMARY KEY,
29
+ uuid UUID NOT NULL DEFAULT gen_random_uuid(),
30
+ username TEXT NOT NULL,
31
+ password_hash TEXT NOT NULL,
32
+ type_code TEXT NOT NULL DEFAULT 'USER',
33
+ level_code TEXT NOT NULL DEFAULT 'lv1',
34
+ name TEXT NOT NULL,
35
+ nickname TEXT NOT NULL,
36
+ email TEXT,
37
+ phone_number TEXT,
38
+ last_login_at TIMESTAMPTZ,
39
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
40
+ )`)
41
+ await db.query(`COMMENT ON TABLE users IS '회원 테이블'`)
42
+ await db.query(`COMMENT ON COLUMN users.uuid IS '고유값'`)
43
+ await db.query(`ALTER TABLE users ADD CONSTRAINT uniq_users_username UNIQUE (username)`)
44
+ await db.query(`COMMENT ON COLUMN users.username IS '로그인 아이디'`)
45
+ await db.query(`COMMENT ON COLUMN users.password_hash IS '비밀번호 해시'`)
46
+ await db.query(`COMMENT ON COLUMN users.type_code IS '회원 유형'`)
47
+ await db.query(`COMMENT ON COLUMN users.level_code IS '회원 등급'`)
48
+ await db.query(`COMMENT ON COLUMN users.name IS '실명'`)
49
+ await db.query(`ALTER TABLE users ADD CONSTRAINT uniq_users_nickname UNIQUE (nickname)`)
50
+ await db.query(`COMMENT ON COLUMN users.nickname IS '별명'`)
51
+ await db.query(`COMMENT ON COLUMN users.email IS '이메일'`)
52
+ await db.query(`COMMENT ON COLUMN users.phone_number IS '전화번호'`)
53
+ await db.query(`COMMENT ON COLUMN users.last_login_at IS '마지막 로그인 시간'`)
54
+ await db.query(`COMMENT ON COLUMN users.created_at IS '생성 시간'`)
55
+ await db.query(`CREATE TABLE user_types (
56
+ id SERIAL PRIMARY KEY,
57
+ code TEXT NOT NULL,
58
+ sort_order INTEGER NOT NULL DEFAULT 0,
59
+ name TEXT NOT NULL,
60
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
61
+ )`)
62
+ await db.query(`COMMENT ON TABLE user_types IS '회원 유형 테이블'`)
63
+ await db.query(`ALTER TABLE user_types ADD CONSTRAINT uniq_user_types_code UNIQUE (code)`)
64
+ await db.query(`COMMENT ON COLUMN user_types.code IS '회원 유형 코드'`)
65
+ await db.query(`COMMENT ON COLUMN user_types.sort_order IS '정렬 순서'`)
66
+ await db.query(`COMMENT ON COLUMN user_types.name IS '회원 유형명'`)
67
+ await db.query(`COMMENT ON COLUMN user_types.created_at IS '생성 시간'`)
68
+ await db.query(`CREATE TABLE wallets (
69
+ id SERIAL PRIMARY KEY,
70
+ user_id BIGINT NOT NULL,
71
+ balance NUMERIC(20, 4) NOT NULL DEFAULT 0,
72
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
73
+ )`)
74
+ await db.query(`COMMENT ON TABLE wallets IS '회원 지갑 테이블'`)
75
+ await db.query(`COMMENT ON COLUMN wallets.user_id IS '회원 ID'`)
76
+ await db.query(`COMMENT ON COLUMN wallets.balance IS '잔액'`)
77
+ await db.query(`COMMENT ON COLUMN wallets.created_at IS '생성 시간'`)
78
+ await db.query(`ALTER TABLE user_levels ADD CONSTRAINT fk_user_levels_type_code_user_types FOREIGN KEY (type_code) REFERENCES user_types (code) ON DELETE CASCADE`)
79
+ await db.query(`ALTER TABLE wallets ADD CONSTRAINT fk_wallets_user_id_users FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE`)
80
+ await db.query(`CREATE INDEX idx_user_levels_type_code ON user_levels (type_code)`)
81
+ await db.query(`CREATE UNIQUE INDEX idx_users_uuid ON users (uuid)`)
82
+ await db.query(`CREATE INDEX idx_users_email ON users (email)`)
83
+ await db.query(`CREATE INDEX idx_users_type_code ON users (type_code)`)
84
+ await db.query(`CREATE INDEX idx_users_level_code ON users (level_code)`)
85
+ await db.query(`CREATE INDEX idx_wallets_user_id ON wallets (user_id)`)
19
86
  }
20
87
 
21
88
  /**
22
- * @param {{ query: (sql: string, params?: any[]) => Promise<any> }} db
89
+ * @param {{ query: (sql: string, params?: any[]) => Promise<any> }} db - 대상 DB 어댑터(트랜잭션 내 query).
23
90
  * @returns {Promise<void>}
24
91
  */
25
92
  export async function down(db) {
26
- await db.query('DROP TABLE IF EXISTS users')
93
+ await db.query(`DROP INDEX IF EXISTS idx_wallets_user_id`)
94
+ await db.query(`DROP INDEX IF EXISTS idx_users_level_code`)
95
+ await db.query(`DROP INDEX IF EXISTS idx_users_type_code`)
96
+ await db.query(`DROP INDEX IF EXISTS idx_users_email`)
97
+ await db.query(`DROP INDEX IF EXISTS idx_users_uuid`)
98
+ await db.query(`DROP INDEX IF EXISTS idx_user_levels_type_code`)
99
+ await db.query(`ALTER TABLE wallets DROP CONSTRAINT fk_wallets_user_id_users`)
100
+ await db.query(`ALTER TABLE user_levels DROP CONSTRAINT fk_user_levels_type_code_user_types`)
101
+ await db.query(`DROP TABLE IF EXISTS wallets`)
102
+ await db.query(`DROP TABLE IF EXISTS user_types`)
103
+ await db.query(`DROP TABLE IF EXISTS users`)
104
+ await db.query(`DROP TABLE IF EXISTS user_levels`)
27
105
  }