mega-framework 0.1.5 → 0.1.7
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.
- package/bin/mega-ws-hub.js +2 -2
- package/package.json +32 -8
- package/sample/crud/.env +156 -8
- package/sample/crud/.env.example +153 -28
- package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
- package/sample/crud/.mega/journal/snapshot.json +261 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
- package/sample/crud/apps/main/controllers/web-controller.js +7 -5
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
- package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
- package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
- package/sample/crud/apps/main/models/log-partition-model.js +105 -0
- package/sample/crud/apps/main/models/note-model.js +79 -0
- package/sample/crud/apps/main/models/user-level-model.js +24 -0
- package/sample/crud/apps/main/models/user-model.js +146 -0
- package/sample/crud/apps/main/models/user-type-model.js +21 -0
- package/sample/crud/apps/main/models/wallet-model.js +24 -0
- package/sample/crud/apps/main/routes/users.js +55 -10
- package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
- package/sample/crud/apps/main/services/auth-service.js +39 -24
- package/sample/crud/apps/main/services/log-partition-service.js +101 -0
- package/sample/crud/apps/main/services/note-service.js +6 -6
- package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
- package/sample/crud/apps/main/services/user-service.js +62 -21
- package/sample/crud/apps/main/views/auth/login.ejs +6 -6
- package/sample/crud/apps/main/views/auth/register.ejs +46 -5
- package/sample/crud/apps/main/views/users/edit.ejs +42 -5
- package/sample/crud/apps/main/views/users/list.ejs +6 -2
- package/sample/crud/apps/main/views/users/new.ejs +56 -4
- package/sample/crud/docs/log_partition_design.mm.md +23 -0
- package/sample/crud/mega.config.js +63 -3
- package/sample/crud/package.json +3 -3
- package/sample/crud/scripts/start-ws-hub.sh +2 -2
- package/sample/simple/package.json +2 -2
- package/src/adapters/adapter-manager.js +2 -1
- package/src/adapters/adapter-options.js +30 -0
- package/src/adapters/maria-adapter.js +26 -3
- package/src/adapters/mega-db-adapter.js +7 -1
- package/src/adapters/mongo-adapter.js +19 -1
- package/src/adapters/postgres-adapter.js +25 -2
- package/src/adapters/sqlite-adapter.js +20 -1
- package/src/cli/commands/new.js +13 -3
- package/src/cli/commands/scaffold.js +137 -33
- package/src/cli/generators/index.js +82 -2
- package/src/cli/index.js +478 -104
- package/src/core/ajv-mapper.js +27 -2
- package/src/core/boot.js +485 -237
- package/src/core/cluster-metrics.js +13 -4
- package/src/core/config-validator.js +25 -0
- package/src/core/ctx-builder.js +6 -2
- package/src/core/envelope.js +112 -12
- package/src/core/hub-link.js +65 -4
- package/src/core/i18n.js +11 -1
- package/src/core/index.js +6 -2
- package/src/core/mega-app.js +223 -481
- package/src/core/mega-cluster.js +54 -13
- package/src/core/mega-server.js +40 -9
- package/src/core/migration/dialect-registry.js +107 -0
- package/src/core/migration/dialects/README.md +62 -0
- package/src/core/migration/dialects/maria.js +496 -0
- package/src/core/migration/dialects/mongo.js +824 -0
- package/src/core/migration/dialects/postgres.js +563 -0
- package/src/core/migration/dialects/sqlite.js +476 -0
- package/src/core/migration/differ.js +456 -0
- package/src/core/migration/generate.js +508 -0
- package/src/core/migration/journal.js +167 -0
- package/src/core/migration/model-scan.js +84 -0
- package/src/core/migration/mongo-migration-db.js +97 -0
- package/src/core/migration/schema-builder.js +400 -0
- package/src/core/migration/schema-validator.js +315 -0
- package/src/core/migration-lock.js +205 -0
- package/src/core/migration-runner.js +166 -38
- package/src/core/multipart.js +28 -5
- package/src/core/pipeline.js +129 -0
- package/src/core/router.js +70 -65
- package/src/core/scope-registry.js +0 -1
- package/src/core/security.js +67 -9
- package/src/core/workers-manager.js +12 -1
- package/src/core/ws-cluster.js +10 -3
- package/src/core/ws-message.js +48 -4
- package/src/core/ws-presence.js +624 -0
- package/src/core/ws-roster.js +4 -1
- package/src/core/ws-upgrade.js +118 -12
- package/src/index.js +1 -1
- package/src/lib/hub-protocol.js +29 -0
- package/src/lib/mega-health.js +25 -4
- package/src/lib/mega-job-queue.js +98 -21
- package/src/lib/mega-job.js +29 -0
- package/src/lib/mega-logger.js +1 -1
- package/src/lib/mega-metrics.js +3 -12
- package/src/lib/mega-plugin.js +34 -3
- package/src/lib/mega-schedule.js +40 -22
- package/src/lib/mega-shutdown.js +162 -49
- package/src/lib/mega-tracing.js +66 -19
- package/src/lib/mega-worker.js +5 -1
- package/src/lib/otel-resource.js +36 -0
- package/src/{cli → lib}/ws-hub.js +51 -8
- package/src/models/crud-sql-builder.js +133 -0
- package/src/models/mega-model.js +82 -2
- package/src/models/model-crud.js +483 -0
- package/src/models/mongo-crud.js +285 -0
- package/templates/model/code-mongo.tpl +35 -0
- package/templates/model/code.tpl +15 -1
- package/templates/model/test-mongo.tpl +38 -0
- package/templates/model/test.tpl +4 -0
- package/types/adapters/adapter-manager.d.ts +95 -0
- package/types/adapters/adapter-options.d.ts +91 -0
- package/types/adapters/file-adapter.d.ts +94 -0
- package/types/adapters/file-session-adapter.d.ts +101 -0
- package/types/adapters/index.d.ts +20 -0
- package/types/adapters/maria-adapter.d.ts +115 -0
- package/types/adapters/mega-adapter.d.ts +215 -0
- package/types/adapters/mega-bus-adapter.d.ts +45 -0
- package/types/adapters/mega-cache-adapter.d.ts +47 -0
- package/types/adapters/mega-db-adapter.d.ts +47 -0
- package/types/adapters/mega-lock-adapter.d.ts +62 -0
- package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
- package/types/adapters/mega-session-adapter.d.ts +32 -0
- package/types/adapters/mongo-adapter.d.ts +139 -0
- package/types/adapters/nats-adapter.d.ts +108 -0
- package/types/adapters/postgres-adapter.d.ts +139 -0
- package/types/adapters/redis-adapter.d.ts +70 -0
- package/types/adapters/redis-session-adapter.d.ts +82 -0
- package/types/adapters/redlock-adapter.d.ts +149 -0
- package/types/adapters/registry.d.ts +46 -0
- package/types/adapters/sqlite-adapter.d.ts +106 -0
- package/types/auth/index.d.ts +24 -0
- package/types/cli/commands/console-cmd.d.ts +37 -0
- package/types/cli/commands/new.d.ts +16 -0
- package/types/cli/commands/routes.d.ts +36 -0
- package/types/cli/commands/scaffold.d.ts +78 -0
- package/types/cli/commands/test-cmd.d.ts +14 -0
- package/types/cli/generators/index.d.ts +112 -0
- package/types/cli/index.d.ts +249 -0
- package/types/cli/template-engine.d.ts +40 -0
- package/types/core/ajv-mapper.d.ts +27 -0
- package/types/core/boot.d.ts +233 -0
- package/types/core/cluster-metrics.d.ts +52 -0
- package/types/core/config-loader.d.ts +13 -0
- package/types/core/config-validator.d.ts +30 -0
- package/types/core/ctx-builder.d.ts +80 -0
- package/types/core/envelope.d.ts +79 -0
- package/types/core/error-mapper.d.ts +17 -0
- package/types/core/formbody.d.ts +41 -0
- package/types/core/hub-link.d.ts +264 -0
- package/types/core/i18n.d.ts +178 -0
- package/types/core/index.d.ts +28 -0
- package/types/core/mega-app.d.ts +529 -0
- package/types/core/mega-cluster.d.ts +104 -0
- package/types/core/mega-server.d.ts +91 -0
- package/types/core/mega-service.d.ts +31 -0
- package/types/core/migration/dialect-registry.d.ts +22 -0
- package/types/core/migration/dialects/maria.d.ts +99 -0
- package/types/core/migration/dialects/mongo.d.ts +89 -0
- package/types/core/migration/dialects/postgres.d.ts +117 -0
- package/types/core/migration/dialects/sqlite.d.ts +111 -0
- package/types/core/migration/differ.d.ts +47 -0
- package/types/core/migration/generate.d.ts +56 -0
- package/types/core/migration/journal.d.ts +52 -0
- package/types/core/migration/model-scan.d.ts +19 -0
- package/types/core/migration/mongo-migration-db.d.ts +7 -0
- package/types/core/migration/schema-builder.d.ts +197 -0
- package/types/core/migration/schema-validator.d.ts +20 -0
- package/types/core/migration-lock.d.ts +33 -0
- package/types/core/migration-runner.d.ts +101 -0
- package/types/core/multipart.d.ts +86 -0
- package/types/core/openapi.d.ts +62 -0
- package/types/core/pipeline.d.ts +92 -0
- package/types/core/router.d.ts +159 -0
- package/types/core/routes-loader.d.ts +21 -0
- package/types/core/scope-registry.d.ts +14 -0
- package/types/core/security.d.ts +77 -0
- package/types/core/services-loader.d.ts +27 -0
- package/types/core/session-cleanup-schedule.d.ts +19 -0
- package/types/core/session-store.d.ts +18 -0
- package/types/core/session.d.ts +77 -0
- package/types/core/static-assets.d.ts +73 -0
- package/types/core/template.d.ts +106 -0
- package/types/core/workers-manager.d.ts +79 -0
- package/types/core/ws-cluster.d.ts +208 -0
- package/types/core/ws-compression.d.ts +112 -0
- package/types/core/ws-controller.d.ts +65 -0
- package/types/core/ws-message.d.ts +106 -0
- package/types/core/ws-presence.d.ts +273 -0
- package/types/core/ws-roster.d.ts +96 -0
- package/types/core/ws-upgrade.d.ts +231 -0
- package/types/errors/config-error.d.ts +10 -0
- package/types/errors/http-errors.d.ts +120 -0
- package/types/errors/index.d.ts +3 -0
- package/types/errors/mega-error.d.ts +32 -0
- package/types/index.d.ts +39 -0
- package/types/lib/asp/config.d.ts +49 -0
- package/types/lib/asp/crypto.d.ts +43 -0
- package/types/lib/asp/errors.d.ts +30 -0
- package/types/lib/asp/nonce-cache.d.ts +52 -0
- package/types/lib/asp/plugin.d.ts +30 -0
- package/types/lib/asp/ws-terminator.d.ts +45 -0
- package/types/lib/env-mapper.d.ts +14 -0
- package/types/lib/hub-protocol.d.ts +106 -0
- package/types/lib/index.d.ts +22 -0
- package/types/lib/logger/telegram-core.d.ts +104 -0
- package/types/lib/logger/telegram-transport.d.ts +45 -0
- package/types/lib/mega-brute-force.d.ts +66 -0
- package/types/lib/mega-circuit-breaker.d.ts +241 -0
- package/types/lib/mega-cron.d.ts +66 -0
- package/types/lib/mega-hash.d.ts +32 -0
- package/types/lib/mega-health.d.ts +41 -0
- package/types/lib/mega-job-queue.d.ts +176 -0
- package/types/lib/mega-job-worker.d.ts +130 -0
- package/types/lib/mega-job.d.ts +138 -0
- package/types/lib/mega-logger.d.ts +45 -0
- package/types/lib/mega-metrics.d.ts +285 -0
- package/types/lib/mega-plugin.d.ts +245 -0
- package/types/lib/mega-retry.d.ts +85 -0
- package/types/lib/mega-schedule.d.ts +260 -0
- package/types/lib/mega-shutdown.d.ts +135 -0
- package/types/lib/mega-tracing.d.ts +224 -0
- package/types/lib/mega-worker.d.ts +127 -0
- package/types/lib/otel-resource.d.ts +16 -0
- package/types/lib/worker-runner/process-entry.d.ts +1 -0
- package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
- package/types/lib/worker-runner/thread-entry.d.ts +1 -0
- package/types/lib/ws-hub.d.ts +234 -0
- package/types/models/crud-sql-builder.d.ts +48 -0
- package/types/models/index.d.ts +1 -0
- package/types/models/mega-model.d.ts +138 -0
- package/types/models/model-crud.d.ts +82 -0
- package/types/models/mongo-crud.d.ts +59 -0
- package/types/test/index.d.ts +84 -0
- package/.env +0 -127
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
- package/sample/crud/apps/main/models/note.js +0 -71
- package/sample/crud/apps/main/models/user.js +0 -86
- package/sample/crud/package-lock.json +0 -5665
- package/sample/crud/yarn.lock +0 -2142
- package/sample/simple/package-lock.json +0 -1851
|
@@ -1,56 +1,81 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { MegaService } from 'mega-framework'
|
|
3
|
+
import { MegaHash } from 'mega-framework'
|
|
3
4
|
import { MegaNotFoundError, MegaValidationError, MegaConflictError } from 'mega-framework/errors'
|
|
4
|
-
import {
|
|
5
|
+
import { UserModel } from '../models/user-model.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* UserService — user 도메인 비즈니스 로직(ADR-022). 컨트롤러는 모델을 직접 만지지 않고 이 서비스를
|
|
8
9
|
* 거친다. 파일명 `user-service.js` → 자동 DI 이름 `user`(ctx.services.user, ADR-148). 모델 import 는
|
|
9
10
|
* 서비스에서만 허용(유일한 모델 접근 경로).
|
|
11
|
+
*
|
|
12
|
+
* `users` 테이블이 username·name·nickname·password_hash 를 NOT NULL 로 요구하므로(ADR-204 schema),
|
|
13
|
+
* 관리자 생성도 회원가입과 동일하게 이 4개를 필수로 받고 비밀번호를 해시해 저장한다. 로그인 식별자는
|
|
14
|
+
* username 이다(email 은 선택).
|
|
10
15
|
*/
|
|
11
16
|
export class UserService extends MegaService {
|
|
17
|
+
/** 비밀번호 최소 길이(auth-service 와 동일 정책). */
|
|
18
|
+
static MIN_PASSWORD = 8
|
|
19
|
+
|
|
12
20
|
/** @returns {Promise<object[]>} */
|
|
13
21
|
async list() {
|
|
14
22
|
this.log.debug?.('user.list')
|
|
15
|
-
return
|
|
23
|
+
return UserModel.list()
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
/** @param {string|number} id @returns {Promise<object>} @throws {MegaNotFoundError} */
|
|
19
27
|
async get(id) {
|
|
20
|
-
const user = await
|
|
28
|
+
const user = await UserModel.findById(Number(id))
|
|
21
29
|
if (!user) throw new MegaNotFoundError('user.not_found', `User ${id} not found`, { details: { id } })
|
|
22
30
|
return user
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* 관리자 사용자 생성. 필수: username·name·nickname·password. 선택: email·phone_number·type_code·level_code.
|
|
35
|
+
* 비밀번호는 해시해 저장(평문 미보관). 반환은 공개 컬럼(해시 미노출).
|
|
36
|
+
* @param {{ username?: unknown, name?: unknown, nickname?: unknown, email?: unknown, phone_number?: unknown, password?: unknown, type_code?: unknown, level_code?: unknown }} input
|
|
37
|
+
* @returns {Promise<object>}
|
|
38
|
+
* @throws {MegaValidationError} `user.invalid` @throws {MegaConflictError} `user.username_taken`|`user.nickname_taken`
|
|
39
|
+
*/
|
|
26
40
|
async create(input) {
|
|
41
|
+
const username = typeof input?.username === 'string' ? input.username.trim() : ''
|
|
27
42
|
const name = typeof input?.name === 'string' ? input.name.trim() : ''
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
const nickname = typeof input?.nickname === 'string' ? input.nickname.trim() : ''
|
|
44
|
+
const email = typeof input?.email === 'string' ? input.email.trim().toLowerCase() : ''
|
|
45
|
+
const phone = typeof input?.phone_number === 'string' ? input.phone_number.trim() : ''
|
|
46
|
+
const password = typeof input?.password === 'string' ? input.password : ''
|
|
47
|
+
const valid = { username: !!username, name: !!name, nickname: !!nickname, password: password.length >= UserService.MIN_PASSWORD }
|
|
48
|
+
if (!username || !name || !nickname || password.length < UserService.MIN_PASSWORD) {
|
|
49
|
+
throw new MegaValidationError('user.invalid', 'username, name, nickname and password(8+) are required', { details: valid })
|
|
31
50
|
}
|
|
51
|
+
const passwordHash = await MegaHash.password.hash(password)
|
|
32
52
|
try {
|
|
33
|
-
return await
|
|
53
|
+
return await UserModel.create({
|
|
54
|
+
username,
|
|
55
|
+
name,
|
|
56
|
+
nickname,
|
|
57
|
+
email: email || null,
|
|
58
|
+
phone_number: phone || null,
|
|
59
|
+
passwordHash,
|
|
60
|
+
type_code: typeof input?.type_code === 'string' && input.type_code ? input.type_code : undefined,
|
|
61
|
+
level_code: typeof input?.level_code === 'string' && input.level_code ? input.level_code : undefined,
|
|
62
|
+
})
|
|
34
63
|
} catch (err) {
|
|
35
|
-
|
|
36
|
-
if (/** @type {any} */ (err)?.code === '23505') {
|
|
37
|
-
throw new MegaConflictError('user.email_taken', `email '${email}' already exists`, { details: { email }, cause: err })
|
|
38
|
-
}
|
|
39
|
-
throw err
|
|
64
|
+
throw UserService.#mapConflict(err, { username, nickname })
|
|
40
65
|
}
|
|
41
66
|
}
|
|
42
67
|
|
|
43
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* 부분 수정 — name·nickname·email·phone_number·type_code·level_code. username·password 는 안 바꾼다.
|
|
70
|
+
* @param {string|number} id @param {Record<string, any>} patch @returns {Promise<object>}
|
|
71
|
+
* @throws {MegaNotFoundError} @throws {MegaConflictError}
|
|
72
|
+
*/
|
|
44
73
|
async update(id, patch) {
|
|
45
74
|
let updated
|
|
46
75
|
try {
|
|
47
|
-
updated = await
|
|
76
|
+
updated = await UserModel.update(Number(id), patch ?? {})
|
|
48
77
|
} catch (err) {
|
|
49
|
-
|
|
50
|
-
if (/** @type {any} */ (err)?.code === '23505') {
|
|
51
|
-
throw new MegaConflictError('user.email_taken', `email '${patch?.email}' already exists`, { details: { email: patch?.email }, cause: err })
|
|
52
|
-
}
|
|
53
|
-
throw err
|
|
78
|
+
throw UserService.#mapConflict(err, { nickname: patch?.nickname })
|
|
54
79
|
}
|
|
55
80
|
if (!updated) throw new MegaNotFoundError('user.not_found', `User ${id} not found`, { details: { id } })
|
|
56
81
|
return updated
|
|
@@ -58,8 +83,24 @@ export class UserService extends MegaService {
|
|
|
58
83
|
|
|
59
84
|
/** @param {string|number} id @returns {Promise<{ deleted: true }>} @throws {MegaNotFoundError} */
|
|
60
85
|
async remove(id) {
|
|
61
|
-
const ok = await
|
|
86
|
+
const ok = await UserModel.remove(Number(id))
|
|
62
87
|
if (!ok) throw new MegaNotFoundError('user.not_found', `User ${id} not found`, { details: { id } })
|
|
63
88
|
return { deleted: true }
|
|
64
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* postgres unique_violation(23505) → username/nickname 도메인 충돌(409) 매핑(P4 — 명시 처리 후 throw).
|
|
93
|
+
* 그 외 에러는 그대로 반환(상위에서 throw).
|
|
94
|
+
* @param {unknown} err @param {{ username?: string, nickname?: string }} ctx @returns {Error}
|
|
95
|
+
*/
|
|
96
|
+
static #mapConflict(err, ctx) {
|
|
97
|
+
if (/** @type {any} */ (err)?.code === '23505') {
|
|
98
|
+
const constraint = String(/** @type {any} */ (err)?.constraint ?? '')
|
|
99
|
+
if (constraint.includes('nickname')) {
|
|
100
|
+
return new MegaConflictError('user.nickname_taken', `nickname '${ctx.nickname}' already exists`, { details: { nickname: ctx.nickname }, cause: /** @type {any} */ (err) })
|
|
101
|
+
}
|
|
102
|
+
return new MegaConflictError('user.username_taken', `username '${ctx.username}' already exists`, { details: { username: ctx.username }, cause: /** @type {any} */ (err) })
|
|
103
|
+
}
|
|
104
|
+
return /** @type {Error} */ (err)
|
|
105
|
+
}
|
|
65
106
|
}
|
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
|
18
18
|
|
|
19
19
|
<div class="mb-3">
|
|
20
|
-
<label for="
|
|
20
|
+
<label for="username" class="form-label"><%= t('field_username', '아이디') %></label>
|
|
21
21
|
<input
|
|
22
|
-
type="
|
|
22
|
+
type="text"
|
|
23
23
|
class="form-control"
|
|
24
|
-
id="
|
|
25
|
-
name="
|
|
26
|
-
value="<%= values && values.
|
|
27
|
-
placeholder="<%= t('
|
|
24
|
+
id="username"
|
|
25
|
+
name="username"
|
|
26
|
+
value="<%= values && values.username ? values.username : '' %>"
|
|
27
|
+
placeholder="<%= t('field_username_ph', '예: hong123') %>"
|
|
28
28
|
autocomplete="username"
|
|
29
29
|
required
|
|
30
30
|
/>
|
|
@@ -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="
|
|
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
|
|
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="
|
|
41
|
-
|
|
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-
|
|
29
|
-
<label for="
|
|
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
|
|
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
|
-
|
|
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.
|
|
38
|
-
<td><%= u.
|
|
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-
|
|
29
|
-
<label for="
|
|
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
|
|
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-
|
|
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
|
|
|
@@ -15,6 +16,13 @@ export default {
|
|
|
15
16
|
port: Number(process.env.PORT ?? 3000),
|
|
16
17
|
// 세션 쿠키 HMAC 서명 시크릿(global 스코프, ADR-129). boot 이 앱에 주입한다. .env 의 SESSION_SECRET.
|
|
17
18
|
sessionSecret: process.env.SESSION_SECRET,
|
|
19
|
+
// (예시·미사용) listen 호스트 — 기본 '0.0.0.0'. `mega start --host H`(CLI)가 우선.
|
|
20
|
+
// host: '0.0.0.0',
|
|
21
|
+
// (예시·미사용) cluster 워커 수 — CLI `--cluster` > .env MEGA_CLUSTER_WORKERS > 이 값(ADR-154). 정수 N | 'max'.
|
|
22
|
+
// cluster: 'max',
|
|
23
|
+
// (예시·미사용) 메트릭/트레이싱 resource 의 service.name·version(health.serviceName 미지정 시 폴백).
|
|
24
|
+
// serviceName: 'sample-crud',
|
|
25
|
+
// version: '0.1.0',
|
|
18
26
|
},
|
|
19
27
|
|
|
20
28
|
// ASP(Application-layer Secure Protocol) masterSecret — global 스코프 시크릿(ADR-127, scope-registry).
|
|
@@ -24,6 +32,8 @@ export default {
|
|
|
24
32
|
// 이 값을 브라우저에 주입한다 — 그래서 운영 secret 과 분리된 데모 전용 값을 .env 의 ASP_MASTER_SECRET 에 둔다.
|
|
25
33
|
asp: {
|
|
26
34
|
masterSecret: process.env.ASP_MASTER_SECRET,
|
|
35
|
+
// (예시·미사용) HTTP body/query 암호화 옵션(enabledPaths·driftMs·nonceCache 등)은 **앱 스코프** —
|
|
36
|
+
// apps/<app>/app.config.js 의 asp.http 에 둔다. 여기 global 블록엔 masterSecret 만(scope-registry).
|
|
27
37
|
},
|
|
28
38
|
|
|
29
39
|
// 전역 어댑터(ADR-102/106/109) — globalKey 로 선언, 앱은 app.config.js 의 별명으로 참조.
|
|
@@ -33,6 +43,11 @@ export default {
|
|
|
33
43
|
primary: {
|
|
34
44
|
driver: 'postgres',
|
|
35
45
|
url: process.env.DATABASE_URL,
|
|
46
|
+
// (예시·미사용) url 대신 discrete 도 가능: { host, port, user, password, database } (url 과 상호배타).
|
|
47
|
+
// (예시·미사용) 공통 풀 인터페이스 — 드라이버별 키로 자동 매핑(ADR-109). 단위 *Ms=밀리초.
|
|
48
|
+
// pool: { min: 0, max: 10, idleTimeoutMs: 10_000 },
|
|
49
|
+
// (예시·미사용) 드라이버 네이티브 옵션 passthrough(pg=snake_case).
|
|
50
|
+
// options: { ssl: false, statement_timeout: 30_000 },
|
|
36
51
|
},
|
|
37
52
|
// DB 'mongo' — Document DB 어댑터(ADR-108). notes 데모 컬렉션(Note 모델 static adapter='mongo')의 공유
|
|
38
53
|
// 인스턴스. url path 의 dbName(mega_test)을 어댑터가 추출한다. .env 의 MONGO_URL(authSource=admin).
|
|
@@ -40,6 +55,12 @@ export default {
|
|
|
40
55
|
driver: 'mongodb',
|
|
41
56
|
url: process.env.MONGO_URL,
|
|
42
57
|
},
|
|
58
|
+
// (예시·미사용) MariaDB 어댑터(ADR-105). 쓰려면 주석 해제 + .env 의 MARIA_URL 설정.
|
|
59
|
+
// 모델은 static adapter='maria'(globalKey)로 닿고, app.config.js databases 에 별명 추가.
|
|
60
|
+
// maria: {
|
|
61
|
+
// driver: 'mariadb',
|
|
62
|
+
// url: process.env.MARIA_URL,
|
|
63
|
+
// },
|
|
43
64
|
},
|
|
44
65
|
caches: {
|
|
45
66
|
// redis 캐시 'rate' — brute-force(ctx.bruteForce, ADR-049/130)의 원자적 INCR 백엔드. .env 의 REDIS_RATE_URL(db 2).
|
|
@@ -60,6 +81,12 @@ export default {
|
|
|
60
81
|
driver: 'redis',
|
|
61
82
|
url: process.env.REDIS_LOCK_URL,
|
|
62
83
|
},
|
|
84
|
+
// (예시·미사용) 범용 캐시 — 일반 키/값 캐싱이 필요하면 주석 해제 + .env 의 REDIS_CACHE_URL 설정.
|
|
85
|
+
// 기존 캐시(rate/demo/lock)와 키 충돌을 피해 별도 논리 DB(/4 등)를 권장. app.config.js caches 에 별명 추가.
|
|
86
|
+
// cache: {
|
|
87
|
+
// driver: 'redis',
|
|
88
|
+
// url: process.env.REDIS_CACHE_URL,
|
|
89
|
+
// },
|
|
63
90
|
},
|
|
64
91
|
// NATS 버스 'jobs' — 잡 큐(EmailJob, ADR-119)의 JetStream 백엔드. producer(웹)는 ctx.bus('jobs').native(nc)로
|
|
65
92
|
// enqueue, consumer(`mega worker`)는 같은 버스로 소비한다. .env 의 NATS_JOBS_URL.
|
|
@@ -68,6 +95,11 @@ export default {
|
|
|
68
95
|
driver: 'nats',
|
|
69
96
|
url: process.env.NATS_JOBS_URL,
|
|
70
97
|
},
|
|
98
|
+
// (예시·미사용) 이벤트 버스 — pub/sub 등 두 번째 NATS 버스가 필요하면 주석 해제 + .env 의 NATS_EVENTS_URL.
|
|
99
|
+
// events: {
|
|
100
|
+
// driver: 'nats',
|
|
101
|
+
// url: process.env.NATS_EVENTS_URL,
|
|
102
|
+
// },
|
|
71
103
|
},
|
|
72
104
|
// 분산 락 'main' — redlock(ADR-113). caches.lock(Redis) 을 빌려 단일 노드 redlock 을 구성한다. /demo/cron
|
|
73
105
|
// 스케줄(CronCounterSchedule.static lock)의 클러스터 중복방지(leader election)에 쓰인다.
|
|
@@ -79,6 +111,17 @@ export default {
|
|
|
79
111
|
},
|
|
80
112
|
},
|
|
81
113
|
|
|
114
|
+
// (예시·미사용) Embedded WS Hub(ADR-137) — `mega ws-hub` 별도 프로세스 대신 같은 프로세스에 허브를 띄움
|
|
115
|
+
// (single-node). 켜면 app.config.js bridgeHub.url 을 이 host:port 로 맞춘다. acceptedTokens 필수(빈 값=throw).
|
|
116
|
+
// wsHub: {
|
|
117
|
+
// enabled: true,
|
|
118
|
+
// port: 3100,
|
|
119
|
+
// host: '127.0.0.1',
|
|
120
|
+
// acceptedTokens: (process.env.MEGA_WSHUB_TOKENS ?? '').split(',').filter(Boolean),
|
|
121
|
+
// heartbeatMs: 25_000,
|
|
122
|
+
// // compression: { enabled: false },
|
|
123
|
+
// },
|
|
124
|
+
|
|
82
125
|
// ── 클러스터 전송 선택(ADR-176, 앱당 하나·상호배타) ───────────────────────────────────────────
|
|
83
126
|
// 이 샘플은 현재 **WS Hub**(app.config 의 bridgeHub → `mega ws-hub` 서버, localhost:3100)로 채팅을
|
|
84
127
|
// 클러스터 전파한다. boot 가 bridgeHub 를 보고 app.connectHub 를 자동 호출한다(개발자 배선 불요).
|
|
@@ -93,7 +136,7 @@ export default {
|
|
|
93
136
|
// },
|
|
94
137
|
|
|
95
138
|
// 정기 작업(ADR-028/118) — `mega scheduler` 프로세스가 등록·실행한다(config.schedules, ADR-123).
|
|
96
|
-
schedules: [CronCounterSchedule],
|
|
139
|
+
schedules: [CronCounterSchedule, LogPartitionSchedule],
|
|
97
140
|
|
|
98
141
|
// 영속 잡(ADR-028/119) — `mega worker` 프로세스(ecosystem instances:2)가 소비한다(config.jobs, ADR-123).
|
|
99
142
|
jobs: [EmailJob],
|
|
@@ -101,11 +144,20 @@ export default {
|
|
|
101
144
|
// CPU 워커 풀(ADR-124) — `mega start`(boot)가 ctx.workers['hash'] 로 배선한다(config.workers).
|
|
102
145
|
workers: [HashWorker],
|
|
103
146
|
|
|
147
|
+
// (예시·미사용) 플러그인 — 명시 등록만(auto-discovery 없음, ADR-079). 문자열 또는 { name, options }.
|
|
148
|
+
// plugins: [{ name: 'my-plugin', options: {} }],
|
|
149
|
+
|
|
104
150
|
// pino 로깅(ADR-023/141) — console sink, dev pretty. redact 로 민감필드를 sink 출력 전 메인스레드에서
|
|
105
151
|
// 마스킹한다(token/password/secret/authorization). /demo/logs 데모가 이 마스킹을 시연한다(ADR-163).
|
|
106
152
|
logger: {
|
|
107
|
-
level: '
|
|
108
|
-
sinks
|
|
153
|
+
level: process.env.LOG_LEVEL ?? 'info',
|
|
154
|
+
// sinks — 출력처 배열. console(dev pretty) 외에 file(pino-roll)·telegram sink 지원.
|
|
155
|
+
sinks: [
|
|
156
|
+
{ type: 'console', pretty: true },
|
|
157
|
+
// (예시·미사용) 파일 sink — 날짜 로테이션 + keep N.
|
|
158
|
+
// { type: 'file', path: './logs/app.log', rotation: 'daily', keep: 14 },
|
|
159
|
+
// (예시·미사용) telegram sink — warn 이상 알림. botToken/chatId 는 .env 등 시크릿으로 주입(코드·git 직접 X).
|
|
160
|
+
],
|
|
109
161
|
redact: ['*.password', '*.token', '*.secret', '*.authorization', 'password', 'token', 'secret'],
|
|
110
162
|
},
|
|
111
163
|
|
|
@@ -114,5 +166,13 @@ export default {
|
|
|
114
166
|
exposeMetrics: true,
|
|
115
167
|
metricsPath: '/metrics',
|
|
116
168
|
metricsAllowList: ['127.0.0.1', '::1'],
|
|
169
|
+
// (예시·미사용) liveness/readiness 경로 — 기본 '/health' · '/health/ready'.
|
|
170
|
+
// paths: { live: '/health', ready: '/health/ready' },
|
|
171
|
+
// (예시·미사용) 메트릭 resource service.name — 미지정 시 server.serviceName → MEGA_OTEL_SERVICE_NAME 폴백.
|
|
172
|
+
serviceName: 'sample-crud',
|
|
117
173
|
},
|
|
174
|
+
|
|
175
|
+
// OpenTelemetry 분산 트레이싱 — **config 블록이 아니라 .env 의 MEGA_OTEL_\*** 로 설정한다
|
|
176
|
+
// (MegaTracing.fromEnv, boot.js). MEGA_OTEL_ENABLED='true' + MEGA_OTEL_SERVICE_NAME 필수.
|
|
177
|
+
// (`tracing` 키는 스키마엔 있으나 현재 부팅이 소비하지 않음 — 죽은 설정 회피 위해 블록을 두지 않음.)
|
|
118
178
|
}
|
package/sample/crud/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"node": ">=20"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"dev": "
|
|
10
|
+
"dev": "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.
|
|
26
|
+
"vitest": "^4.1.8"
|
|
27
27
|
}
|
|
28
|
-
}
|
|
28
|
+
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
# 앱(`yarn dev` = `mega start`)이 app.config 의 `bridgeHub` 로 이 허브에 **자동 연결**한다(ADR-176).
|
|
7
7
|
#
|
|
8
8
|
# ⚠️ `mega-ws-hub` 는 `mega start` 와 달리 `.env` 를 자동 로드하지 않는다(직접 process.env 만 읽음,
|
|
9
|
-
# src/
|
|
10
|
-
# 읽는 env(src/
|
|
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
|
# 사용:
|
|
@@ -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
|
-
|
|
130
|
+
// 'adapters' stage — 서버/잡/앱 정리(어댑터를 쓰는 정리)가 모두 끝난 뒤 disconnect 된다.
|
|
131
|
+
MegaShutdown.register(SHUTDOWN_HOOK, async () => disconnectAll(), { stage: 'adapters' })
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|