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,27 +1,105 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 마이그레이션 create-users (
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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(
|
|
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
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 마이그레이션 create-boards (20260606000002). up = 게시판 관련 테이블 생성, down = 제거(ADR-149).
|
|
4
|
+
* `mega migrate`(up)·`mega migrate:down`·`mega migrate:status` 로 실행. 적용 이력은 대상 DB 의
|
|
5
|
+
* `mega_migrations` 테이블이 추적하며, 각 마이그레이션은 트랜잭션으로 감싸 적용된다.
|
|
6
|
+
*
|
|
7
|
+
* @param {{ query: (sql: string, params?: any[]) => Promise<any> }} db - 대상 DB 어댑터(트랜잭션 내 query).
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
*/
|
|
10
|
+
export async function up(db) {
|
|
11
|
+
|
|
12
|
+
// ltree 확장 활성화 (계층형 게시물 sort_path 용)
|
|
13
|
+
await db.query(`CREATE EXTENSION IF NOT EXISTS ltree`)
|
|
14
|
+
|
|
15
|
+
// 게시판 설정 테이블
|
|
16
|
+
await db.query(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS board_configs (
|
|
18
|
+
id SERIAL PRIMARY KEY,
|
|
19
|
+
uuid UUID NOT NULL DEFAULT gen_random_uuid(),
|
|
20
|
+
code TEXT NOT NULL UNIQUE,
|
|
21
|
+
type TEXT NOT NULL,
|
|
22
|
+
use_comment BOOLEAN NOT NULL DEFAULT true,
|
|
23
|
+
use_file BOOLEAN NOT NULL DEFAULT true,
|
|
24
|
+
name TEXT NOT NULL,
|
|
25
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
26
|
+
is_enabled BOOLEAN NOT NULL DEFAULT true,
|
|
27
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
28
|
+
);
|
|
29
|
+
COMMENT ON TABLE board_configs IS '게시판 설정 테이블';
|
|
30
|
+
COMMENT ON COLUMN board_configs.uuid IS '고유값';
|
|
31
|
+
COMMENT ON COLUMN board_configs.code IS '게시판 코드';
|
|
32
|
+
COMMENT ON COLUMN board_configs.name IS '게시판 명칭';
|
|
33
|
+
COMMENT ON COLUMN board_configs.sort_order IS '정렬 순서';
|
|
34
|
+
COMMENT ON COLUMN board_configs.is_enabled IS '사용 여부';
|
|
35
|
+
COMMENT ON COLUMN board_configs.created_at IS '생성 시간';
|
|
36
|
+
CREATE UNIQUE INDEX idx_board_configs_uuid ON board_configs (uuid);
|
|
37
|
+
`)
|
|
38
|
+
|
|
39
|
+
// 게시물 테이블
|
|
40
|
+
await db.query(`
|
|
41
|
+
CREATE TABLE IF NOT EXISTS boards (
|
|
42
|
+
id BIGSERIAL PRIMARY KEY,
|
|
43
|
+
uuid UUID NOT NULL DEFAULT gen_random_uuid(),
|
|
44
|
+
config_code TEXT NOT NULL REFERENCES board_configs(code) ON DELETE CASCADE,
|
|
45
|
+
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
46
|
+
parent_id BIGINT REFERENCES boards(id) ON DELETE CASCADE,
|
|
47
|
+
depth INTEGER NOT NULL DEFAULT 0,
|
|
48
|
+
sort_path ltree NOT NULL DEFAULT '',
|
|
49
|
+
title TEXT NOT NULL,
|
|
50
|
+
content TEXT,
|
|
51
|
+
view_count INTEGER NOT NULL DEFAULT 0,
|
|
52
|
+
reply_count INTEGER NOT NULL DEFAULT 0,
|
|
53
|
+
like_count INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
dislike_count INTEGER NOT NULL DEFAULT 0,
|
|
55
|
+
is_pinned BOOLEAN NOT NULL DEFAULT false,
|
|
56
|
+
is_recommended BOOLEAN NOT NULL DEFAULT false,
|
|
57
|
+
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
|
58
|
+
deleted_at TIMESTAMPTZ,
|
|
59
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
60
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
61
|
+
);
|
|
62
|
+
COMMENT ON TABLE boards IS '게시물 테이블';
|
|
63
|
+
COMMENT ON COLUMN boards.uuid IS '고유값';
|
|
64
|
+
COMMENT ON COLUMN boards.user_id IS '작성자 ID';
|
|
65
|
+
COMMENT ON COLUMN boards.config_code IS '게시판 코드';
|
|
66
|
+
COMMENT ON COLUMN boards.parent_id IS '부모 게시물 ID (답글 원글)';
|
|
67
|
+
COMMENT ON COLUMN boards.depth IS '계층 깊이 (0=원글, 1=답글, 2=답답글...)';
|
|
68
|
+
COMMENT ON COLUMN boards.sort_path IS '계층 정렬 경로 (ltree)';
|
|
69
|
+
COMMENT ON COLUMN boards.title IS '제목';
|
|
70
|
+
COMMENT ON COLUMN boards.content IS '내용';
|
|
71
|
+
COMMENT ON COLUMN boards.view_count IS '조회수';
|
|
72
|
+
COMMENT ON COLUMN boards.reply_count IS '답변 수';
|
|
73
|
+
COMMENT ON COLUMN boards.like_count IS '좋아요 수';
|
|
74
|
+
COMMENT ON COLUMN boards.dislike_count IS '싫어요 수';
|
|
75
|
+
COMMENT ON COLUMN boards.is_pinned IS '고정 여부';
|
|
76
|
+
COMMENT ON COLUMN boards.is_recommended IS '추천 여부';
|
|
77
|
+
COMMENT ON COLUMN boards.is_deleted IS '삭제 여부';
|
|
78
|
+
COMMENT ON COLUMN boards.deleted_at IS '삭제 시간';
|
|
79
|
+
COMMENT ON COLUMN boards.created_at IS '생성 시간';
|
|
80
|
+
COMMENT ON COLUMN boards.updated_at IS '수정 시간';
|
|
81
|
+
CREATE UNIQUE INDEX idx_boards_uuid ON boards (uuid);
|
|
82
|
+
CREATE INDEX idx_boards_config_code ON boards (config_code);
|
|
83
|
+
CREATE INDEX idx_boards_user_id ON boards (user_id);
|
|
84
|
+
CREATE INDEX idx_boards_parent_id ON boards (parent_id);
|
|
85
|
+
CREATE INDEX idx_boards_sort_path ON boards USING GIST (sort_path);
|
|
86
|
+
CREATE INDEX idx_boards_created_at ON boards (created_at DESC) WHERE is_deleted = false;
|
|
87
|
+
`)
|
|
88
|
+
|
|
89
|
+
// 게시물 댓글 테이블
|
|
90
|
+
await db.query(`
|
|
91
|
+
CREATE TABLE IF NOT EXISTS board_comments (
|
|
92
|
+
id BIGSERIAL PRIMARY KEY,
|
|
93
|
+
uuid UUID NOT NULL DEFAULT gen_random_uuid(),
|
|
94
|
+
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
95
|
+
board_id BIGINT NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
|
|
96
|
+
parent_id BIGINT REFERENCES board_comments(id) ON DELETE CASCADE,
|
|
97
|
+
content TEXT,
|
|
98
|
+
like_count INTEGER NOT NULL DEFAULT 0,
|
|
99
|
+
dislike_count INTEGER NOT NULL DEFAULT 0,
|
|
100
|
+
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
|
101
|
+
deleted_at TIMESTAMPTZ,
|
|
102
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
103
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
104
|
+
);
|
|
105
|
+
COMMENT ON TABLE board_comments IS '게시물 댓글 테이블';
|
|
106
|
+
COMMENT ON COLUMN board_comments.uuid IS '고유값';
|
|
107
|
+
COMMENT ON COLUMN board_comments.user_id IS '작성자 ID';
|
|
108
|
+
COMMENT ON COLUMN board_comments.board_id IS '게시판 ID';
|
|
109
|
+
COMMENT ON COLUMN board_comments.parent_id IS '부모 댓글 ID (대댓글용)';
|
|
110
|
+
COMMENT ON COLUMN board_comments.content IS '내용';
|
|
111
|
+
COMMENT ON COLUMN board_comments.like_count IS '좋아요 수';
|
|
112
|
+
COMMENT ON COLUMN board_comments.dislike_count IS '싫어요 수';
|
|
113
|
+
COMMENT ON COLUMN board_comments.is_deleted IS '삭제 여부';
|
|
114
|
+
COMMENT ON COLUMN board_comments.deleted_at IS '삭제 시간';
|
|
115
|
+
COMMENT ON COLUMN board_comments.created_at IS '생성 시간';
|
|
116
|
+
COMMENT ON COLUMN board_comments.updated_at IS '수정 시간';
|
|
117
|
+
CREATE UNIQUE INDEX idx_board_comments_uuid ON board_comments (uuid);
|
|
118
|
+
CREATE INDEX idx_board_comments_user_id ON board_comments (user_id);
|
|
119
|
+
CREATE INDEX idx_board_comments_board_id ON board_comments (board_id);
|
|
120
|
+
CREATE INDEX idx_board_comments_parent_id ON board_comments (parent_id);
|
|
121
|
+
`)
|
|
122
|
+
|
|
123
|
+
// 게시글 첨부 파일 테이블
|
|
124
|
+
await db.query(`
|
|
125
|
+
CREATE TABLE IF NOT EXISTS board_files (
|
|
126
|
+
id SERIAL PRIMARY KEY,
|
|
127
|
+
uuid UUID NOT NULL DEFAULT gen_random_uuid(),
|
|
128
|
+
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
129
|
+
board_id BIGINT NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
|
|
130
|
+
original_name TEXT NOT NULL,
|
|
131
|
+
stored_name TEXT NOT NULL,
|
|
132
|
+
file_type TEXT NOT NULL DEFAULT 'unknown',
|
|
133
|
+
file_size INTEGER NOT NULL,
|
|
134
|
+
download_count INTEGER NOT NULL DEFAULT 0,
|
|
135
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
136
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
137
|
+
);
|
|
138
|
+
COMMENT ON TABLE board_files IS '게시글 첨부 파일 테이블';
|
|
139
|
+
COMMENT ON COLUMN board_files.uuid IS '고유값';
|
|
140
|
+
COMMENT ON COLUMN board_files.user_id IS '작성자 ID';
|
|
141
|
+
COMMENT ON COLUMN board_files.board_id IS '게시판 ID';
|
|
142
|
+
COMMENT ON COLUMN board_files.original_name IS '원본파일명';
|
|
143
|
+
COMMENT ON COLUMN board_files.stored_name IS '저장명';
|
|
144
|
+
COMMENT ON COLUMN board_files.file_type IS '파일 유형';
|
|
145
|
+
COMMENT ON COLUMN board_files.file_size IS '파일 크기';
|
|
146
|
+
COMMENT ON COLUMN board_files.download_count IS '다운로드 수';
|
|
147
|
+
COMMENT ON COLUMN board_files.created_at IS '생성 시간';
|
|
148
|
+
COMMENT ON COLUMN board_files.updated_at IS '수정 시간';
|
|
149
|
+
CREATE UNIQUE INDEX idx_board_files_uuid ON board_files (uuid);
|
|
150
|
+
CREATE INDEX idx_board_files_user_id ON board_files (user_id);
|
|
151
|
+
CREATE INDEX idx_board_files_board_id ON board_files (board_id);
|
|
152
|
+
`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {{ query: (sql: string, params?: any[]) => Promise<any> }} db
|
|
157
|
+
* @returns {Promise<void>}
|
|
158
|
+
*/
|
|
159
|
+
export async function down(db) {
|
|
160
|
+
await db.query('DROP TABLE IF EXISTS board_files')
|
|
161
|
+
await db.query('DROP TABLE IF EXISTS board_comments')
|
|
162
|
+
await db.query('DROP TABLE IF EXISTS boards')
|
|
163
|
+
await db.query('DROP TABLE IF EXISTS board_configs')
|
|
164
|
+
await db.query('DROP EXTENSION IF EXISTS ltree')
|
|
165
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 마이그레이션 create-logs (20260606000003). up = 로그 관련 파티션 테이블 및 초기 파티션 생성, down = 제거(ADR-149).
|
|
4
|
+
* `mega migrate`(up)·`mega migrate:down`·`mega migrate:status` 로 실행. 적용 이력은 대상 DB 의
|
|
5
|
+
* `mega_migrations` 테이블이 추적하며, 각 마이그레이션은 트랜잭션으로 감싸 적용된다.
|
|
6
|
+
*
|
|
7
|
+
* @param {{ query: (sql: string, params?: any[]) => Promise<any> }} db - 대상 DB 어댑터(트랜잭션 내 query).
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
*/
|
|
10
|
+
export async function up(db) {
|
|
11
|
+
|
|
12
|
+
// 활동 로그 파티션 테이블 생성 (created_at 컬럼을 기준으로 범위 파티셔닝 적용)
|
|
13
|
+
await db.query(`
|
|
14
|
+
CREATE TABLE IF NOT EXISTS action_logs (
|
|
15
|
+
id BIGSERIAL,
|
|
16
|
+
uuid UUID NOT NULL DEFAULT gen_random_uuid(),
|
|
17
|
+
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
18
|
+
action_type TEXT NOT NULL,
|
|
19
|
+
action_target TEXT NOT NULL,
|
|
20
|
+
action_target_id TEXT,
|
|
21
|
+
message TEXT,
|
|
22
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
23
|
+
PRIMARY KEY (id, created_at) -- 파티션 제약 조건에 따라 파티션 키가 PK에 포함되어야 함
|
|
24
|
+
) PARTITION BY RANGE (created_at);
|
|
25
|
+
COMMENT ON TABLE action_logs IS '활동 로그 파티션 테이블';
|
|
26
|
+
COMMENT ON COLUMN action_logs.uuid IS '고유값';
|
|
27
|
+
COMMENT ON COLUMN action_logs.action_type IS '활동 유형';
|
|
28
|
+
COMMENT ON COLUMN action_logs.action_target IS '활동 대상';
|
|
29
|
+
COMMENT ON COLUMN action_logs.action_target_id IS '활동 대상 ID';
|
|
30
|
+
COMMENT ON COLUMN action_logs.message IS '메시지';
|
|
31
|
+
COMMENT ON COLUMN action_logs.created_at IS '생성 시간';
|
|
32
|
+
CREATE UNIQUE INDEX idx_action_logs_uuid ON action_logs (uuid, created_at); -- 고유 인덱스에도 파티션 키 필수 포함
|
|
33
|
+
CREATE INDEX idx_action_logs_user_id ON action_logs (user_id);
|
|
34
|
+
CREATE INDEX idx_action_logs_action_type ON action_logs (action_type);
|
|
35
|
+
`)
|
|
36
|
+
|
|
37
|
+
// 지갑 로그 파티션 테이블 생성 (created_at 컬럼을 기준으로 범위 파티셔닝 적용)
|
|
38
|
+
await db.query(`
|
|
39
|
+
CREATE TABLE IF NOT EXISTS wallet_logs (
|
|
40
|
+
id BIGSERIAL,
|
|
41
|
+
uuid UUID NOT NULL DEFAULT gen_random_uuid(),
|
|
42
|
+
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
43
|
+
wallet_id BIGINT NOT NULL REFERENCES wallets(id) ON DELETE CASCADE,
|
|
44
|
+
action_type TEXT NOT NULL,
|
|
45
|
+
action_target TEXT NOT NULL,
|
|
46
|
+
action_target_id TEXT,
|
|
47
|
+
message TEXT,
|
|
48
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
49
|
+
PRIMARY KEY (id, created_at) -- 파티션 제약 조건에 따라 파티션 키가 PK에 포함되어야 함
|
|
50
|
+
) PARTITION BY RANGE (created_at);
|
|
51
|
+
COMMENT ON TABLE wallet_logs IS '지갑 로그 파티션 테이블';
|
|
52
|
+
COMMENT ON COLUMN wallet_logs.uuid IS '고유값';
|
|
53
|
+
COMMENT ON COLUMN wallet_logs.action_type IS '활동 유형';
|
|
54
|
+
COMMENT ON COLUMN wallet_logs.action_target IS '활동 대상';
|
|
55
|
+
COMMENT ON COLUMN wallet_logs.action_target_id IS '활동 대상 ID';
|
|
56
|
+
COMMENT ON COLUMN wallet_logs.message IS '메시지';
|
|
57
|
+
COMMENT ON COLUMN wallet_logs.created_at IS '생성 시간';
|
|
58
|
+
CREATE UNIQUE INDEX idx_wallet_logs_uuid ON wallet_logs (uuid, created_at); -- 고유 인덱스에도 파티션 키 필수 포함
|
|
59
|
+
CREATE INDEX idx_wallet_logs_user_id ON wallet_logs (user_id);
|
|
60
|
+
CREATE INDEX idx_wallet_logs_action_type ON wallet_logs (action_type);
|
|
61
|
+
`)
|
|
62
|
+
|
|
63
|
+
// 상세 로그 파티션 테이블 생성 (created_at 컬럼을 기준으로 범위 파티셔닝 적용)
|
|
64
|
+
await db.query(`
|
|
65
|
+
CREATE TABLE IF NOT EXISTS detail_logs (
|
|
66
|
+
id BIGSERIAL,
|
|
67
|
+
uuid UUID NOT NULL DEFAULT gen_random_uuid(),
|
|
68
|
+
log_id BIGINT NOT NULL,
|
|
69
|
+
log_json JSONB NOT NULL,
|
|
70
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
71
|
+
PRIMARY KEY (id, created_at) -- 파티션 제약 조건에 따라 파티션 키가 PK에 포함되어야 함
|
|
72
|
+
) PARTITION BY RANGE (created_at);
|
|
73
|
+
COMMENT ON TABLE detail_logs IS '상세 로그 파티션 테이블';
|
|
74
|
+
COMMENT ON COLUMN detail_logs.uuid IS '고유값';
|
|
75
|
+
COMMENT ON COLUMN detail_logs.log_id IS '로그 ID';
|
|
76
|
+
COMMENT ON COLUMN detail_logs.log_json IS '상세 로그 JSON';
|
|
77
|
+
COMMENT ON COLUMN detail_logs.created_at IS '생성 시간';
|
|
78
|
+
CREATE UNIQUE INDEX idx_detail_logs_uuid ON detail_logs (uuid, created_at); -- 고유 인덱스에도 파티션 키 필수 포함
|
|
79
|
+
CREATE UNIQUE INDEX idx_detail_logs_log_id ON detail_logs (log_id, created_at); -- 고유 인덱스에도 파티션 키 필수 포함
|
|
80
|
+
`)
|
|
81
|
+
|
|
82
|
+
// 초기 파티션 생성 (2026년 6월 및 7월)
|
|
83
|
+
// 데이터 유실 및 쿼리 에러를 미연에 방지하기 위해 마이그레이션 시점에 현재 월과 다음 월의 파티션을 생성해 둡니다.
|
|
84
|
+
await db.query(`
|
|
85
|
+
CREATE TABLE IF NOT EXISTS action_logs_y2026m06 PARTITION OF action_logs FOR VALUES FROM ('2026-06-01 00:00:00+09') TO ('2026-07-01 00:00:00+09');
|
|
86
|
+
CREATE TABLE IF NOT EXISTS action_logs_y2026m07 PARTITION OF action_logs FOR VALUES FROM ('2026-07-01 00:00:00+09') TO ('2026-08-01 00:00:00+09');
|
|
87
|
+
|
|
88
|
+
CREATE TABLE IF NOT EXISTS wallet_logs_y2026m06 PARTITION OF wallet_logs FOR VALUES FROM ('2026-06-01 00:00:00+09') TO ('2026-07-01 00:00:00+09');
|
|
89
|
+
CREATE TABLE IF NOT EXISTS wallet_logs_y2026m07 PARTITION OF wallet_logs FOR VALUES FROM ('2026-07-01 00:00:00+09') TO ('2026-08-01 00:00:00+09');
|
|
90
|
+
|
|
91
|
+
CREATE TABLE IF NOT EXISTS detail_logs_y2026m06 PARTITION OF detail_logs FOR VALUES FROM ('2026-06-01 00:00:00+09') TO ('2026-07-01 00:00:00+09');
|
|
92
|
+
CREATE TABLE IF NOT EXISTS detail_logs_y2026m07 PARTITION OF detail_logs FOR VALUES FROM ('2026-07-01 00:00:00+09') TO ('2026-08-01 00:00:00+09');
|
|
93
|
+
`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {{ query: (sql: string, params?: any[]) => Promise<any> }} db
|
|
98
|
+
* @returns {Promise<void>}
|
|
99
|
+
*/
|
|
100
|
+
export async function down(db) {
|
|
101
|
+
// 부모 테이블 삭제 시 하위 파티션 테이블(y2026m06 등)은 자동으로 함께 드롭됩니다.
|
|
102
|
+
await db.query(`
|
|
103
|
+
DROP TABLE IF EXISTS detail_logs;
|
|
104
|
+
DROP TABLE IF EXISTS action_logs;
|
|
105
|
+
DROP TABLE IF EXISTS wallet_logs;
|
|
106
|
+
`)
|
|
107
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { MegaModel } from 'mega-framework'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 로그 파티션 테이블에 대한 직접적인 DDL 쿼리를 실행하는 모델 클래스 (ADR-009/138).
|
|
6
|
+
* @extends {MegaModel}
|
|
7
|
+
*/
|
|
8
|
+
export class LogPartitionModel extends MegaModel {
|
|
9
|
+
static adapter = 'primary'
|
|
10
|
+
// MegaModel 요구사항: 기본 관리 대상이 되는 대표 테이블 지정
|
|
11
|
+
static table = 'action_logs'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 대상 부모 테이블의 하위 파티션 목록을 데이터베이스에서 조회합니다.
|
|
15
|
+
* @param {string} parentTable - 부모 테이블 이름 ('action_logs', 'wallet_logs', 'detail_logs')
|
|
16
|
+
* @returns {Promise<string[]>} 하위 파티션 테이블 명칭 배열
|
|
17
|
+
*/
|
|
18
|
+
static async getPartitions(parentTable) {
|
|
19
|
+
// 부모 테이블 이름 화이트리스트 검사 (SQL 인젝션 방지)
|
|
20
|
+
const allowed = ['action_logs', 'wallet_logs', 'detail_logs']
|
|
21
|
+
if (!allowed.includes(parentTable)) {
|
|
22
|
+
throw new Error(`허용되지 않은 부모 테이블명입니다: ${parentTable}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// pg_inherits 시스템 테이블을 조회하여 파티션 목록 조회
|
|
26
|
+
const sql = `
|
|
27
|
+
SELECT c.relname AS partition_name
|
|
28
|
+
FROM pg_inherits i
|
|
29
|
+
JOIN pg_class c ON c.oid = i.inhrelid
|
|
30
|
+
JOIN pg_class p ON p.oid = i.inhparent
|
|
31
|
+
WHERE p.relname = $1
|
|
32
|
+
ORDER BY c.relname ASC
|
|
33
|
+
`
|
|
34
|
+
const { rows } = await super.query(sql, [parentTable])
|
|
35
|
+
|
|
36
|
+
// 파티션 테이블명만 추출하여 리턴
|
|
37
|
+
return rows.map((/** @type {any} */ r) => r.partition_name)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 지정한 연월 범위의 파티션 테이블을 생성합니다.
|
|
42
|
+
* @param {string} parentTable - 부모 테이블 이름
|
|
43
|
+
* @param {number} year - 연도 (예: 2026)
|
|
44
|
+
* @param {number} month - 월 (예: 6)
|
|
45
|
+
* @returns {Promise<void>}
|
|
46
|
+
*/
|
|
47
|
+
static async createPartition(parentTable, year, month) {
|
|
48
|
+
const allowed = ['action_logs', 'wallet_logs', 'detail_logs']
|
|
49
|
+
if (!allowed.includes(parentTable)) {
|
|
50
|
+
throw new Error(`허용되지 않은 부모 테이블명입니다: ${parentTable}`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 월 자릿수를 두 자리 문자열로 포맷팅 (예: 6 -> '06')
|
|
54
|
+
const monthStr = month.toString().padStart(2, '0')
|
|
55
|
+
const partitionName = `${parentTable}_y${year}m${monthStr}`
|
|
56
|
+
|
|
57
|
+
// 시작일(해당 월 1일) 및 종료일(다음 달 1일) 계산
|
|
58
|
+
const startDate = `${year}-${monthStr}-01 00:00:00+09`
|
|
59
|
+
|
|
60
|
+
// 다음 달 계산 로직 (12월인 경우 다음 해 1월로 이월)
|
|
61
|
+
const nextYear = month === 12 ? year + 1 : year
|
|
62
|
+
const nextMonth = month === 12 ? 1 : month + 1
|
|
63
|
+
const nextMonthStr = nextMonth.toString().padStart(2, '0')
|
|
64
|
+
const endDate = `${nextYear}-${nextMonthStr}-01 00:00:00+09`
|
|
65
|
+
|
|
66
|
+
// 파티션 테이블 생성 쿼리 (안전한 명칭 포맷을 사전에 정규식 검증)
|
|
67
|
+
if (!/^[a-z0-9_]+$/.test(partitionName)) {
|
|
68
|
+
throw new Error(`부적절한 파티션 테이블 이름입니다: ${partitionName}`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 파티션 생성 쿼리 실행
|
|
72
|
+
await super.query(`
|
|
73
|
+
CREATE TABLE IF NOT EXISTS ${partitionName}
|
|
74
|
+
PARTITION OF ${parentTable}
|
|
75
|
+
FOR VALUES FROM ('${startDate}') TO ('${endDate}')
|
|
76
|
+
`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 지정한 파티션 테이블을 부모 테이블에서 분리(Detach)하고 물리적으로 제거(Drop)합니다.
|
|
81
|
+
* @param {string} parentTable - 부모 테이블 이름
|
|
82
|
+
* @param {string} partitionName - 드롭할 파티션 테이블 이름
|
|
83
|
+
* @returns {Promise<void>}
|
|
84
|
+
*/
|
|
85
|
+
static async dropPartition(parentTable, partitionName) {
|
|
86
|
+
const allowed = ['action_logs', 'wallet_logs', 'detail_logs']
|
|
87
|
+
if (!allowed.includes(parentTable)) {
|
|
88
|
+
throw new Error(`허용되지 않은 부모 테이블명입니다: ${parentTable}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 파티션 명칭 검증 (형식: 테이블명_yYYYYmMM)
|
|
92
|
+
if (!partitionName.startsWith(`${parentTable}_y`)) {
|
|
93
|
+
throw new Error(`부모 테이블에 일치하지 않는 파티션입니다: ${partitionName}`)
|
|
94
|
+
}
|
|
95
|
+
if (!/^[a-z0-9_]+$/.test(partitionName)) {
|
|
96
|
+
throw new Error(`부적절한 파티션 테이블 이름입니다: ${partitionName}`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 1단계: 부모 테이블로부터 파티션 분리 (락 오버헤드를 최소화하기 위해 선(先)분리 후(後)삭제 수행)
|
|
100
|
+
await super.query(`ALTER TABLE ${parentTable} DETACH PARTITION ${partitionName}`)
|
|
101
|
+
|
|
102
|
+
// 2단계: 분리된 독립 테이블 제거
|
|
103
|
+
await super.query(`DROP TABLE IF EXISTS ${partitionName}`)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import { MegaModel } from 'mega-framework'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* NoteModel — `notes` 컬렉션(globalKey 'mongo' 의 MongoDB, ADR-108). `static schema`(ADR-209 $jsonSchema)
|
|
7
|
+
* 선언으로 공통 CRUD(ADR-212 P3 — mongo 경로)를 켜고, 도메인 메서드 본문을 그 CRUD 로 작성한다. mongo CRUD 는
|
|
8
|
+
* SQL 과 동일 표면이되 내부적으로 `this.db.collection().find/insertOne/...` 도큐먼트 연산으로 디스패치된다.
|
|
9
|
+
*
|
|
10
|
+
* 식별자는 라우트/폼에서 다루기 쉬운 자체 `id`(UUID v4)를 쓰고, 조회 응답에서는 mongo 내부 `_id` 를
|
|
11
|
+
* projection(select)으로 제외해 도메인 형태만 노출한다. (공통 CRUD 의 PK 는 `_id` 지만, 도메인 lookup 은
|
|
12
|
+
* `findOne({ id })` 로 UUID 를 쓴다 — NoteModel 의 기존 패턴 보존.)
|
|
13
|
+
*
|
|
14
|
+
* @typedef {{ id: string, title: string, body: string, created_at: string }} NoteDoc
|
|
15
|
+
*/
|
|
16
|
+
export class NoteModel extends MegaModel {
|
|
17
|
+
static adapter = 'mongo'
|
|
18
|
+
static table = 'notes'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 컬렉션 스키마(ADR-209) — 공통 CRUD 식별자 화이트리스트 + (마이그레이션 생성 시) $jsonSchema validator.
|
|
22
|
+
* `_id` 는 mongo 네이티브 PK(ObjectId), `id` 는 도메인 식별자(UUID, unique). created_at 은 ISO 문자열로 저장.
|
|
23
|
+
*/
|
|
24
|
+
static schema = /** @param {any} t */ (t) => ({
|
|
25
|
+
_id: t.objectId().primary(),
|
|
26
|
+
id: t.uuid().notNull().unique(),
|
|
27
|
+
title: t.varchar(120).notNull(),
|
|
28
|
+
body: t.text(),
|
|
29
|
+
created_at: t.timestamptz().notNull(),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
/** 조회 공개 필드 — 내부 `_id` 를 빼고 도메인 필드만(공통 CRUD select projection). @type {string[]} */
|
|
33
|
+
static PUBLIC_FIELDS = ['id', 'title', 'body', 'created_at']
|
|
34
|
+
|
|
35
|
+
/** 최신순 전체 목록(ADR-212 findMany). @returns {Promise<NoteDoc[]>} */
|
|
36
|
+
static async list() {
|
|
37
|
+
return this.findMany({}, { select: this.PUBLIC_FIELDS, orderBy: [{ column: 'created_at', dir: 'desc' }] })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 단건 조회(도메인 id=UUID, 없으면 null). @param {string} id @returns {Promise<NoteDoc | null>} */
|
|
41
|
+
static async findById(id) {
|
|
42
|
+
return this.findOne({ id }, { select: this.PUBLIC_FIELDS })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 생성 — UUID·생성시각을 부여해 삽입(ADR-212 insertOne). 반환은 삽입한 도메인 도큐먼트. @param {{ title: string, body: string }} input @returns {Promise<NoteDoc>} */
|
|
46
|
+
static async create({ title, body }) {
|
|
47
|
+
/** @type {NoteDoc} */
|
|
48
|
+
const doc = { id: randomUUID(), title, body, created_at: new Date().toISOString() }
|
|
49
|
+
await this.insertOne({ ...doc }) // 새 _id 는 mongo 가 부여(반환값은 도메인 doc 사용)
|
|
50
|
+
return doc
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 수정 — 주어진 필드만 $set(undefined 제외). 대상 없으면 null. id 는 unique 라 0|1 매칭(updateOne).
|
|
55
|
+
* @param {string} id @param {{ title?: string, body?: string }} patch @returns {Promise<NoteDoc | null>}
|
|
56
|
+
*/
|
|
57
|
+
static async update(id, { title, body }) {
|
|
58
|
+
/** @type {Record<string, string>} */
|
|
59
|
+
const set = {}
|
|
60
|
+
if (title !== undefined) set.title = title
|
|
61
|
+
if (body !== undefined) set.body = body
|
|
62
|
+
if (Object.keys(set).length === 0) return this.findById(id) // 변경 없음
|
|
63
|
+
const n = await this.updateOne({ id }, set)
|
|
64
|
+
return n > 0 ? this.findById(id) : null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 삭제 — id(unique) 기준(deleteOne). @param {string} id @returns {Promise<boolean>} */
|
|
68
|
+
static async remove(id) {
|
|
69
|
+
return (await this.deleteOne({ id })) > 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 전체 도큐먼트 수(캐시 데모용). 도메인 메서드명 `count` 가 CRUD `count` 와 충돌하므로 `super.count()`
|
|
74
|
+
* (부모=ADR-212 CRUD)로 호출한다(자기 재귀 회피). @returns {Promise<number>}
|
|
75
|
+
*/
|
|
76
|
+
static async count() {
|
|
77
|
+
return super.count()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { MegaModel } from 'mega-framework'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* UserLevelModel — `user_levels`(회원 등급 lookup). type_code 는 user_types(code) 를 FK 참조(비-PK unique
|
|
6
|
+
* 컬럼 참조 — ADR-204 validator 가 허용). idx_user_levels_type_code 인덱스 동반.
|
|
7
|
+
*
|
|
8
|
+
* @typedef {{ id: number, code: string, type_code: string, sort_order: number, name: string, created_at: string }} UserLevelRow
|
|
9
|
+
*/
|
|
10
|
+
export class UserLevelModel extends MegaModel {
|
|
11
|
+
static adapter = 'primary'
|
|
12
|
+
static table = 'user_levels'
|
|
13
|
+
|
|
14
|
+
static schema = /** @param {any} t */ (t) => ({
|
|
15
|
+
id: t.serial().primary(),
|
|
16
|
+
code: t.text().notNull().unique().comment('회원 등급 코드'),
|
|
17
|
+
type_code: t.text().notNull().references('UserTypeModel', 'code', { onDelete: 'cascade' }).comment('회원 유형 코드'),
|
|
18
|
+
sort_order: t.integer().notNull().default(0).comment('정렬 순서'),
|
|
19
|
+
name: t.text().notNull().comment('회원 등급명'),
|
|
20
|
+
created_at: t.timestamptz().notNull().default({ raw: 'now()' }).comment('생성 시간'),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
static indexes = /** @param {any} t */ (t) => [t.index(['type_code'], { name: 'idx_user_levels_type_code' })]
|
|
24
|
+
}
|