mega-framework 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +127 -0
- package/.env.example +186 -0
- package/.prettierrc.json +8 -0
- package/CHANGELOG.md +259 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/mega-ws-hub.js +15 -0
- package/bin/mega.js +38 -0
- package/docker-compose.yml +201 -0
- package/eslint.config.js +57 -0
- package/infra/otel-collector-config.yaml +43 -0
- package/jsconfig.json +18 -0
- package/package.json +121 -0
- package/sample/crud/.env +18 -0
- package/sample/crud/.env.example +50 -0
- package/sample/crud/README.md +85 -0
- package/sample/crud/apps/main/app.config.js +114 -0
- package/sample/crud/apps/main/channels/chat-bus.js +115 -0
- package/sample/crud/apps/main/channels/chat-channel.js +145 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
- package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
- package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
- package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
- package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
- package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
- package/sample/crud/apps/main/controllers/note-controller.js +116 -0
- package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
- package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
- package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
- package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
- package/sample/crud/apps/main/controllers/user-controller.js +34 -0
- package/sample/crud/apps/main/controllers/web-controller.js +137 -0
- package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
- package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
- package/sample/crud/apps/main/jobs/email-job.js +72 -0
- package/sample/crud/apps/main/locales/client/en.json +3 -0
- package/sample/crud/apps/main/locales/client/ko.json +3 -0
- package/sample/crud/apps/main/locales/server/en.json +316 -0
- package/sample/crud/apps/main/locales/server/ko.json +316 -0
- package/sample/crud/apps/main/middleware/web-auth.js +40 -0
- package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
- package/sample/crud/apps/main/models/note.js +71 -0
- package/sample/crud/apps/main/models/user.js +86 -0
- package/sample/crud/apps/main/public/css/app.css +101 -0
- package/sample/crud/apps/main/public/css/guide.css +137 -0
- package/sample/crud/apps/main/public/js/app.js +54 -0
- package/sample/crud/apps/main/public/js/perf.js +129 -0
- package/sample/crud/apps/main/public/js/theme-init.js +12 -0
- package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
- package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
- package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
- package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
- package/sample/crud/apps/main/routes/auth.js +15 -0
- package/sample/crud/apps/main/routes/cron.js +14 -0
- package/sample/crud/apps/main/routes/guide.js +25 -0
- package/sample/crud/apps/main/routes/jobs.js +14 -0
- package/sample/crud/apps/main/routes/logs.js +28 -0
- package/sample/crud/apps/main/routes/metrics.js +13 -0
- package/sample/crud/apps/main/routes/notes.js +19 -0
- package/sample/crud/apps/main/routes/perf.js +47 -0
- package/sample/crud/apps/main/routes/redis.js +14 -0
- package/sample/crud/apps/main/routes/tracing.js +14 -0
- package/sample/crud/apps/main/routes/upload.js +16 -0
- package/sample/crud/apps/main/routes/users.js +54 -0
- package/sample/crud/apps/main/routes/web.js +23 -0
- package/sample/crud/apps/main/routes/worker.js +15 -0
- package/sample/crud/apps/main/routes/ws.js +30 -0
- package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
- package/sample/crud/apps/main/services/auth-service.js +74 -0
- package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
- package/sample/crud/apps/main/services/guide-service.js +145 -0
- package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
- package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
- package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
- package/sample/crud/apps/main/services/note-service.js +75 -0
- package/sample/crud/apps/main/services/perf-service.js +302 -0
- package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
- package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
- package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
- package/sample/crud/apps/main/services/user-service.js +65 -0
- package/sample/crud/apps/main/views/auth/login.ejs +57 -0
- package/sample/crud/apps/main/views/auth/register.ejs +71 -0
- package/sample/crud/apps/main/views/cron/index.ejs +92 -0
- package/sample/crud/apps/main/views/guide/index.ejs +24 -0
- package/sample/crud/apps/main/views/guide/page.ejs +64 -0
- package/sample/crud/apps/main/views/home.ejs +82 -0
- package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
- package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
- package/sample/crud/apps/main/views/logs/index.ejs +80 -0
- package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
- package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
- package/sample/crud/apps/main/views/notes/list.ejs +74 -0
- package/sample/crud/apps/main/views/notes/new.ejs +45 -0
- package/sample/crud/apps/main/views/perf/index.ejs +90 -0
- package/sample/crud/apps/main/views/redis/index.ejs +65 -0
- package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
- package/sample/crud/apps/main/views/upload/index.ejs +79 -0
- package/sample/crud/apps/main/views/users/edit.ejs +48 -0
- package/sample/crud/apps/main/views/users/list.ejs +81 -0
- package/sample/crud/apps/main/views/users/new.ejs +48 -0
- package/sample/crud/apps/main/views/worker/index.ejs +70 -0
- package/sample/crud/apps/main/views/ws/index.ejs +62 -0
- package/sample/crud/apps/main/workers/hash-worker.js +17 -0
- package/sample/crud/apps/main/workers/hash.task.js +22 -0
- package/sample/crud/ecosystem.config.cjs +9 -0
- package/sample/crud/mega.config.js +105 -0
- package/sample/crud/package-lock.json +5665 -0
- package/sample/crud/package.json +28 -0
- package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
- package/sample/crud/test/apps/main/auth-service.test.js +93 -0
- package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
- package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
- package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
- package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
- package/sample/crud/test/apps/main/email-job.test.js +76 -0
- package/sample/crud/test/apps/main/guide-service.test.js +68 -0
- package/sample/crud/test/apps/main/hash-task.test.js +30 -0
- package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
- package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
- package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/note-service.test.js +68 -0
- package/sample/crud/test/apps/main/perf-service.test.js +121 -0
- package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
- package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
- package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
- package/sample/crud/test/apps/main/user-service.test.js +65 -0
- package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
- package/sample/crud/vitest.config.js +8 -0
- package/sample/crud/yarn.lock +2142 -0
- package/sample/simple/.env.example +15 -0
- package/sample/simple/README.md +52 -0
- package/sample/simple/apps/main/app.config.js +35 -0
- package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
- package/sample/simple/apps/main/locales/client/en.json +3 -0
- package/sample/simple/apps/main/locales/client/ko.json +3 -0
- package/sample/simple/apps/main/locales/server/en.json +23 -0
- package/sample/simple/apps/main/locales/server/ko.json +23 -0
- package/sample/simple/apps/main/public/css/app.css +101 -0
- package/sample/simple/apps/main/public/hello.txt +1 -0
- package/sample/simple/apps/main/public/js/app.js +54 -0
- package/sample/simple/apps/main/public/js/theme-init.js +12 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/simple/apps/main/routes/index.js +9 -0
- package/sample/simple/apps/main/routes/pages.js +12 -0
- package/sample/simple/apps/main/views/index.ejs +56 -0
- package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
- package/sample/simple/ecosystem.config.cjs +10 -0
- package/sample/simple/mega.config.js +27 -0
- package/sample/simple/package-lock.json +1851 -0
- package/sample/simple/package.json +25 -0
- package/sample/simple/test/apps/main/index.test.js +13 -0
- package/sample/simple/vitest.config.js +8 -0
- package/src/adapters/adapter-manager.js +305 -0
- package/src/adapters/adapter-options.js +208 -0
- package/src/adapters/file-adapter.js +350 -0
- package/src/adapters/file-session-adapter.js +363 -0
- package/src/adapters/index.js +38 -0
- package/src/adapters/maria-adapter.js +425 -0
- package/src/adapters/mega-adapter.js +511 -0
- package/src/adapters/mega-bus-adapter.js +81 -0
- package/src/adapters/mega-cache-adapter.js +94 -0
- package/src/adapters/mega-db-adapter.js +72 -0
- package/src/adapters/mega-lock-adapter.js +118 -0
- package/src/adapters/mega-log-sink-adapter.js +46 -0
- package/src/adapters/mega-session-adapter.js +72 -0
- package/src/adapters/mongo-adapter.js +396 -0
- package/src/adapters/nats-adapter.js +370 -0
- package/src/adapters/postgres-adapter.js +341 -0
- package/src/adapters/redis-adapter.js +331 -0
- package/src/adapters/redis-session-adapter.js +261 -0
- package/src/adapters/redlock-adapter.js +385 -0
- package/src/adapters/registry.js +157 -0
- package/src/adapters/sqlite-adapter.js +309 -0
- package/src/auth/index.js +103 -0
- package/src/cli/commands/console-cmd.js +56 -0
- package/src/cli/commands/new.js +101 -0
- package/src/cli/commands/routes.js +107 -0
- package/src/cli/commands/scaffold.js +120 -0
- package/src/cli/commands/test-cmd.js +45 -0
- package/src/cli/generators/index.js +368 -0
- package/src/cli/index.js +472 -0
- package/src/cli/template-engine.js +72 -0
- package/src/cli/ws-hub.js +582 -0
- package/src/core/ajv-mapper.js +80 -0
- package/src/core/boot.js +323 -0
- package/src/core/cluster-metrics.js +278 -0
- package/src/core/config-loader.js +115 -0
- package/src/core/config-validator.js +322 -0
- package/src/core/ctx-builder.js +253 -0
- package/src/core/envelope.js +88 -0
- package/src/core/error-mapper.js +116 -0
- package/src/core/formbody.js +69 -0
- package/src/core/hub-link.js +552 -0
- package/src/core/i18n.js +525 -0
- package/src/core/index.js +63 -0
- package/src/core/mega-app.js +1138 -0
- package/src/core/mega-cluster.js +232 -0
- package/src/core/mega-server.js +176 -0
- package/src/core/mega-service.js +41 -0
- package/src/core/migration-runner.js +196 -0
- package/src/core/multipart.js +282 -0
- package/src/core/openapi.js +114 -0
- package/src/core/router.js +388 -0
- package/src/core/routes-loader.js +57 -0
- package/src/core/scope-registry.js +53 -0
- package/src/core/security.js +275 -0
- package/src/core/services-loader.js +98 -0
- package/src/core/session-cleanup-schedule.js +57 -0
- package/src/core/session-store.js +55 -0
- package/src/core/session.js +414 -0
- package/src/core/static-assets.js +126 -0
- package/src/core/template.js +294 -0
- package/src/core/workers-manager.js +193 -0
- package/src/core/ws-compression.js +112 -0
- package/src/core/ws-controller.js +109 -0
- package/src/core/ws-message.js +176 -0
- package/src/core/ws-upgrade.js +445 -0
- package/src/errors/config-error.js +16 -0
- package/src/errors/http-errors.js +130 -0
- package/src/errors/index.js +19 -0
- package/src/errors/mega-error.js +34 -0
- package/src/eslint-plugin/index.js +15 -0
- package/src/eslint-plugin/no-direct-model-import.js +113 -0
- package/src/index.js +131 -0
- package/src/lib/asp/config.js +83 -0
- package/src/lib/asp/crypto.js +145 -0
- package/src/lib/asp/errors.js +49 -0
- package/src/lib/asp/nonce-cache.js +94 -0
- package/src/lib/asp/plugin.js +263 -0
- package/src/lib/asp/ws-terminator.js +101 -0
- package/src/lib/env-mapper.js +222 -0
- package/src/lib/hub-protocol.js +322 -0
- package/src/lib/index.js +42 -0
- package/src/lib/logger/telegram-core.js +150 -0
- package/src/lib/logger/telegram-transport.js +126 -0
- package/src/lib/mega-brute-force.js +225 -0
- package/src/lib/mega-circuit-breaker.js +412 -0
- package/src/lib/mega-cron.js +169 -0
- package/src/lib/mega-hash.js +179 -0
- package/src/lib/mega-health.js +91 -0
- package/src/lib/mega-job-queue.js +600 -0
- package/src/lib/mega-job-worker.js +295 -0
- package/src/lib/mega-job.js +140 -0
- package/src/lib/mega-logger.js +128 -0
- package/src/lib/mega-metrics.js +661 -0
- package/src/lib/mega-plugin.js +650 -0
- package/src/lib/mega-retry.js +95 -0
- package/src/lib/mega-schedule.js +507 -0
- package/src/lib/mega-shutdown.js +176 -0
- package/src/lib/mega-tracing.js +715 -0
- package/src/lib/mega-worker.js +653 -0
- package/src/lib/worker-runner/process-entry.js +30 -0
- package/src/lib/worker-runner/task-dispatch.js +72 -0
- package/src/lib/worker-runner/thread-entry.js +26 -0
- package/src/models/index.js +7 -0
- package/src/models/mega-model.js +151 -0
- package/src/test/index.js +288 -0
- package/templates/adapter/code.tpl +40 -0
- package/templates/adapter/test.tpl +13 -0
- package/templates/app/app.config.tpl +10 -0
- package/templates/app/route.tpl +10 -0
- package/templates/app/test.tpl +13 -0
- package/templates/channel/code.tpl +38 -0
- package/templates/channel/test.tpl +19 -0
- package/templates/controller/code.tpl +16 -0
- package/templates/controller/route.tpl +9 -0
- package/templates/controller/test.tpl +14 -0
- package/templates/job/code.tpl +23 -0
- package/templates/job/test.tpl +17 -0
- package/templates/locale/code.tpl +3 -0
- package/templates/locale/test.tpl +13 -0
- package/templates/middleware/code.tpl +13 -0
- package/templates/middleware/test.tpl +11 -0
- package/templates/migration/code.tpl +20 -0
- package/templates/migration/test.tpl +14 -0
- package/templates/model/code.tpl +21 -0
- package/templates/model/test.tpl +29 -0
- package/templates/project/app.config.tpl +8 -0
- package/templates/project/app.config.views.tpl +37 -0
- package/templates/project/ecosystem.config.tpl +10 -0
- package/templates/project/env.tpl +12 -0
- package/templates/project/gitignore.tpl +8 -0
- package/templates/project/locales/client/en.json.tpl +3 -0
- package/templates/project/locales/client/ko.json.tpl +3 -0
- package/templates/project/locales/server/en.json.tpl +17 -0
- package/templates/project/locales/server/ko.json.tpl +17 -0
- package/templates/project/mega.config.tpl +11 -0
- package/templates/project/package.tpl +25 -0
- package/templates/project/public/css/app.css +101 -0
- package/templates/project/public/js/app.js +54 -0
- package/templates/project/public/js/theme-init.js +12 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/templates/project/readme.tpl +48 -0
- package/templates/project/route.test.tpl +13 -0
- package/templates/project/route.test.views.tpl +15 -0
- package/templates/project/route.tpl +10 -0
- package/templates/project/route.views.tpl +10 -0
- package/templates/project/views/index.ejs.tpl +58 -0
- package/templates/project/views/layout.ejs.tpl +73 -0
- package/templates/project/vitest.config.tpl +8 -0
- package/templates/route/code.tpl +11 -0
- package/templates/route/test.tpl +26 -0
- package/templates/schedule/code.tpl +19 -0
- package/templates/schedule/test.tpl +17 -0
- package/templates/service/code.tpl +18 -0
- package/templates/service/test.tpl +17 -0
- package/templates/worker/code.tpl +14 -0
- package/templates/worker/task.tpl +13 -0
- package/templates/worker/test.tpl +18 -0
- package/vitest.config.js +33 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { describe, test, expect } from 'vitest'
|
|
3
|
+
import { MegaTest } from 'mega-framework/test'
|
|
4
|
+
import { {{Name}}Channel } from '{{importPath}}'
|
|
5
|
+
|
|
6
|
+
describe('{{Name}}Channel', () => {
|
|
7
|
+
test('{{camelName}}.message 디스패치 → broadcast', async () => {
|
|
8
|
+
const ch = new {{Name}}Channel()
|
|
9
|
+
const { sock, ctx } = MegaTest.makeWs({ user: { id: 1 } })
|
|
10
|
+
await ch.dispatch(sock, { type: '{{camelName}}.message', payload: { text: 'hi' } }, ctx)
|
|
11
|
+
expect(ctx.channel.broadcasts).toContainEqual({ type: '{{camelName}}.message', payload: { text: 'hi' } })
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('알 수 없는 type → onMessage 폴백(throw 없음)', async () => {
|
|
15
|
+
const ch = new {{Name}}Channel()
|
|
16
|
+
const { sock, ctx } = MegaTest.makeWs({})
|
|
17
|
+
await expect(ch.dispatch(sock, { type: 'nope' }, ctx)).resolves.toBeUndefined()
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* {{Name}}Controller — HTTP 컨트롤러는 코어 베이스 클래스 없음(ADR-074). 모든 메서드 static,
|
|
4
|
+
* 단순 namespace 역할(this 미사용). 라우트에서 `router.http.get(path, {{Name}}Controller.index)` 로 참조.
|
|
5
|
+
*/
|
|
6
|
+
export class {{Name}}Controller {
|
|
7
|
+
/**
|
|
8
|
+
* 목록 — ctx.services.{{camelName}}.list 위임. 반환값은 자동 envelope 로 감싸진다(ADR-018).
|
|
9
|
+
* @param {import('fastify').FastifyRequest} req
|
|
10
|
+
* @param {import('fastify').FastifyReply} _res
|
|
11
|
+
* @param {Record<string, any>} ctx
|
|
12
|
+
*/
|
|
13
|
+
static async index(req, _res, ctx) {
|
|
14
|
+
return ctx.services.{{camelName}}.list(req.query)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { {{Name}}Controller } from '{{controllerImport}}'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* apps/{{app}} {{version}} 라우트 — {{Name}}Controller 정적 메서드 참조 등록(ADR-069/074).
|
|
6
|
+
*/
|
|
7
|
+
export default (/** @type {any} */ router) => {
|
|
8
|
+
router.http.get('/api/{{version}}/{{name}}', {{Name}}Controller.index)
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { describe, test, expect, vi } from 'vitest'
|
|
3
|
+
import { MegaTest } from 'mega-framework/test'
|
|
4
|
+
import { {{Name}}Controller } from '{{importPath}}'
|
|
5
|
+
|
|
6
|
+
describe('{{Name}}Controller', () => {
|
|
7
|
+
test('index — ctx.services.{{camelName}}.list 에 위임', async () => {
|
|
8
|
+
const list = vi.fn(async () => [{ id: 1 }])
|
|
9
|
+
const { req, res, ctx } = MegaTest.makeHttp({ query: { page: '1' }, services: { {{camelName}}: { list } } })
|
|
10
|
+
const result = await {{Name}}Controller.index(req, res, ctx)
|
|
11
|
+
expect(result).toEqual([{ id: 1 }])
|
|
12
|
+
expect(list).toHaveBeenCalledWith({ page: '1' })
|
|
13
|
+
})
|
|
14
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { MegaJob } from 'mega-framework'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* {{Name}}Job — NATS subject 구독 잡(ADR-028). `mega worker` 프로세스가 소비한다. `run` 이 throw 하면
|
|
6
|
+
* `static retries`·backoff 로 재시도하고 한도 초과 시 DLQ 로 이동한다.
|
|
7
|
+
*/
|
|
8
|
+
export class {{Name}}Job extends MegaJob {
|
|
9
|
+
static subject = '{{subject}}'
|
|
10
|
+
static bus = 'jobs'
|
|
11
|
+
static concurrency = 1
|
|
12
|
+
static retries = 3
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {Record<string, any>} payload
|
|
16
|
+
* @param {Record<string, any>} ctx
|
|
17
|
+
* @returns {Promise<object>}
|
|
18
|
+
*/
|
|
19
|
+
async run(payload, ctx) {
|
|
20
|
+
ctx.log?.info?.({ payload }, '{{camelName}}: run')
|
|
21
|
+
return { ok: true }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { describe, test, expect } from 'vitest'
|
|
3
|
+
import { MegaJob } from 'mega-framework'
|
|
4
|
+
import { {{Name}}Job } from '{{importPath}}'
|
|
5
|
+
|
|
6
|
+
describe('{{Name}}Job', () => {
|
|
7
|
+
test('MegaJob 상속 + static subject/bus', () => {
|
|
8
|
+
expect(Object.getPrototypeOf({{Name}}Job)).toBe(MegaJob)
|
|
9
|
+
expect({{Name}}Job.subject).toBe('{{subject}}')
|
|
10
|
+
expect(typeof {{Name}}Job.bus).toBe('string')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('run — payload 처리 후 결과 반환', async () => {
|
|
14
|
+
const job = new {{Name}}Job()
|
|
15
|
+
expect(await job.run({ a: 1 }, { log: { info() {} } })).toEqual({ ok: true })
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { describe, test, expect } from 'vitest'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
describe('{{ns}} locale ({{lng}})', () => {
|
|
7
|
+
test('유효한 JSON 객체', () => {
|
|
8
|
+
const path = fileURLToPath(new URL('{{importPath}}', import.meta.url))
|
|
9
|
+
const data = JSON.parse(readFileSync(path, 'utf8'))
|
|
10
|
+
expect(typeof data).toBe('object')
|
|
11
|
+
expect(data).not.toBeNull()
|
|
12
|
+
})
|
|
13
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* {{camelName}} 미들웨어 (ADR-091 — before/transform/after 중 before 단계). 라우트 `before` 로 쓰면
|
|
4
|
+
* `(req, reply)`, 글로벌 미들웨어로 쓰면 `(req, reply, ctx)` 를 받는다(ADR-134). 양쪽 호환.
|
|
5
|
+
* @param {import('fastify').FastifyRequest} req
|
|
6
|
+
* @param {import('fastify').FastifyReply} _res
|
|
7
|
+
* @param {Record<string, any>} [ctx]
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
*/
|
|
10
|
+
export async function {{camelName}}(req, _res, ctx) {
|
|
11
|
+
const log = ctx?.log ?? req.log
|
|
12
|
+
log?.debug?.({ url: req.url }, '{{camelName}}: enter')
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { describe, test, expect, vi } from 'vitest'
|
|
3
|
+
import { {{camelName}} } from '{{importPath}}'
|
|
4
|
+
|
|
5
|
+
describe('{{camelName}} 미들웨어', () => {
|
|
6
|
+
test('debug 로그 후 통과(throw 없음)', async () => {
|
|
7
|
+
const debug = vi.fn()
|
|
8
|
+
await {{camelName}}(/** @type {any} */ ({ url: '/x', log: { debug } }), /** @type {any} */ ({}), undefined)
|
|
9
|
+
expect(debug).toHaveBeenCalled()
|
|
10
|
+
})
|
|
11
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 마이그레이션 {{name}} ({{timestamp}}). up = 적용, down = 롤백. `mega migrate`(up 일괄)·
|
|
4
|
+
* `mega migrate:down`(마지막 1개 롤백)·`mega migrate:status`(목록)로 실행한다(ADR-149). 적용 이력은
|
|
5
|
+
* 대상 DB 의 `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
|
+
await db.query('-- TODO: {{name}} 적용(up) SQL')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {{ query: (sql: string, params?: any[]) => Promise<any> }} db
|
|
16
|
+
* @returns {Promise<void>}
|
|
17
|
+
*/
|
|
18
|
+
export async function down(db) {
|
|
19
|
+
await db.query('-- TODO: {{name}} 롤백(down) SQL')
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { describe, test, expect } from 'vitest'
|
|
3
|
+
import { up, down } from '{{importPath}}'
|
|
4
|
+
|
|
5
|
+
describe('migration {{name}}', () => {
|
|
6
|
+
test('up/down — db.query 호출', async () => {
|
|
7
|
+
/** @type {string[]} */
|
|
8
|
+
const sqls = []
|
|
9
|
+
const db = { query: async (/** @type {string} */ sql) => void sqls.push(sql) }
|
|
10
|
+
await up(db)
|
|
11
|
+
await down(db)
|
|
12
|
+
expect(sqls).toHaveLength(2)
|
|
13
|
+
})
|
|
14
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { MegaModel } from 'mega-framework'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* {{Name}} 모델 — 데이터 소스는 `static table` 로 통일(SQL table / Mongo collection, ADR-081).
|
|
6
|
+
* `static adapter` 는 mega.config.js 의 services.databases 키를 가리킨다(ADR-061).
|
|
7
|
+
*/
|
|
8
|
+
export class {{Name}} extends MegaModel {
|
|
9
|
+
static adapter = 'primary'
|
|
10
|
+
static table = '{{table}}'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* id 로 1건 조회. `this.query` 는 계측된 어댑터 query 위임(ADR-138).
|
|
14
|
+
* @param {string|number} id
|
|
15
|
+
* @returns {Promise<object|null>}
|
|
16
|
+
*/
|
|
17
|
+
static async findById(id) {
|
|
18
|
+
const { rows } = await this.query('select * from {{table}} where id = $1', [id])
|
|
19
|
+
return rows[0] ?? null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { describe, test, expect } from 'vitest'
|
|
3
|
+
import { MegaModel } from 'mega-framework'
|
|
4
|
+
import { {{Name}} } from '{{importPath}}'
|
|
5
|
+
|
|
6
|
+
describe('{{Name}} model', () => {
|
|
7
|
+
test('MegaModel 을 상속하고 static table/adapter 설정됨', () => {
|
|
8
|
+
expect(Object.getPrototypeOf({{Name}})).toBe(MegaModel)
|
|
9
|
+
expect({{Name}}.table).toBe('{{table}}')
|
|
10
|
+
expect(typeof {{Name}}.adapter).toBe('string')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('findById — 어댑터 query 에 위임', async () => {
|
|
14
|
+
const orig = {{Name}}.query
|
|
15
|
+
/** @type {any[]} */
|
|
16
|
+
const calls = []
|
|
17
|
+
{{Name}}.query = async (/** @type {string} */ sql, /** @type {any[]} */ params) => {
|
|
18
|
+
calls.push({ sql, params })
|
|
19
|
+
return { rows: [{ id: params[0] }] }
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const row = await {{Name}}.findById(7)
|
|
23
|
+
expect(row).toEqual({ id: 7 })
|
|
24
|
+
expect(calls[0].params).toEqual([7])
|
|
25
|
+
} finally {
|
|
26
|
+
{{Name}}.query = orig
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* apps/main/app.config.js — App-only + Shared-Reference 스코프(ADR-061). 기본 앱 `main`.
|
|
4
|
+
* EJS + ejs-mate 서버사이드 템플릿(ADR-011/136) + i18next 다국어(ADR-037/038/039) +
|
|
5
|
+
* @fastify/static 정적 자산(ADR-071) + Bootstrap 5 뷰(ADR-151)를 함께 켠 골격.
|
|
6
|
+
*/
|
|
7
|
+
export default {
|
|
8
|
+
name: 'main',
|
|
9
|
+
hosts: ['localhost', 'main.localhost'],
|
|
10
|
+
|
|
11
|
+
// EJS + ejs-mate — `views.dir` 기준 레이아웃/파셜 lookup. 렌더 시 req.t/req.lang 자동 병합.
|
|
12
|
+
// views 옵트인이라 HTML 폼 제출(urlencoded)도 자동 파싱된다(ADR-151, @fastify/formbody).
|
|
13
|
+
views: {
|
|
14
|
+
dir: 'apps/main/views',
|
|
15
|
+
layoutDir: 'layouts',
|
|
16
|
+
partialsDir: 'partials',
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
// i18next 다국어 — locale 은 쿠키(`mega.lang`)로만 결정(ADR-038). `<localesDir>/<scope>/<lng>.json`
|
|
20
|
+
// 레이아웃(scope=server/client, ADR-039). `ctx.t()`/뷰 `t()` 는 server scope. navbar 의 언어 메뉴가
|
|
21
|
+
// 쿠키를 굽고 새로고침한다.
|
|
22
|
+
i18n: {
|
|
23
|
+
default: 'ko',
|
|
24
|
+
available: ['ko', 'en'],
|
|
25
|
+
fallback: 'en',
|
|
26
|
+
localesDir: 'apps/main/locales',
|
|
27
|
+
exposeTranslations: true,
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// 정적 자산 — `${prefix}/<파일>` 로 디스크 서빙(prefix 디폴트 `/static`). Bootstrap 5 vendored +
|
|
31
|
+
// 브랜드 CSS/JS 가 `apps/main/public/` 아래에 있다. dotfiles 기본 차단.
|
|
32
|
+
staticAssets: {
|
|
33
|
+
enabled: true,
|
|
34
|
+
dir: 'apps/main/public',
|
|
35
|
+
prefix: '/static',
|
|
36
|
+
},
|
|
37
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// PM2 ecosystem — server / scheduler / worker / ws-hub 프로세스 정의.
|
|
2
|
+
// 사용: pm2 start ecosystem.config.cjs (PM2 는 별도 설치: npm i -g pm2)
|
|
3
|
+
module.exports = {
|
|
4
|
+
apps: [
|
|
5
|
+
{ name: '{{name}}-server', script: 'node_modules/.bin/mega', args: 'start', instances: 1, autorestart: true },
|
|
6
|
+
{ name: '{{name}}-scheduler', script: 'node_modules/.bin/mega', args: 'scheduler', instances: 1, autorestart: true },
|
|
7
|
+
{ name: '{{name}}-worker', script: 'node_modules/.bin/mega', args: 'worker', instances: 2, autorestart: true },
|
|
8
|
+
{ name: '{{name}}-ws-hub', script: 'node_modules/.bin/mega-ws-hub', instances: 1, autorestart: true },
|
|
9
|
+
],
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# {{name}} 환경변수 (.env.example) — 복사해서 .env 로 쓰고 실제 값 채우기. .env 는 git 에 안 올림.
|
|
2
|
+
|
|
3
|
+
# HTTP 포트
|
|
4
|
+
PORT=3000
|
|
5
|
+
|
|
6
|
+
# 세션 서명 시크릿(세션 사용 시 필수) — 32바이트 이상 랜덤
|
|
7
|
+
# SESSION_SECRET=change-me
|
|
8
|
+
|
|
9
|
+
# 데이터베이스/캐시/버스 연결 (어댑터 사용 시)
|
|
10
|
+
# DATABASE_URL=postgres://user:pass@localhost:5432/{{name}}
|
|
11
|
+
# REDIS_URL=redis://localhost:6379
|
|
12
|
+
# NATS_URL=nats://localhost:4222
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"welcome": "Welcome",
|
|
3
|
+
"nav_home": "Home",
|
|
4
|
+
"theme_toggle": "Toggle theme",
|
|
5
|
+
"footer_built": "Built with MEGA-FRAMEWORK · Bootstrap 5",
|
|
6
|
+
"hero_title": "Welcome to {{name}}",
|
|
7
|
+
"hero_subtitle": "A project built with MEGA-FRAMEWORK. EJS + ejs-mate server-side views, cookie-based i18n, and a Bootstrap 5 design come built in.",
|
|
8
|
+
"hero_cta_primary": "Framework docs",
|
|
9
|
+
"hero_cta_secondary": "Bootstrap docs",
|
|
10
|
+
"features_heading": "Built in",
|
|
11
|
+
"feature1_title": "Polished views",
|
|
12
|
+
"feature1_desc": "A Bootstrap 5 navbar, hero, and card grid make up the first screen.",
|
|
13
|
+
"feature2_title": "Language toggle",
|
|
14
|
+
"feature2_desc": "Switch Korean / English instantly from the navbar menu (cookie-based).",
|
|
15
|
+
"feature3_title": "Light & dark mode",
|
|
16
|
+
"feature3_desc": "Bootstrap 5.3 color modes toggle the theme and remember it in the browser."
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"welcome": "환영합니다",
|
|
3
|
+
"nav_home": "홈",
|
|
4
|
+
"theme_toggle": "테마 전환",
|
|
5
|
+
"footer_built": "MEGA-FRAMEWORK 로 제작 · Bootstrap 5",
|
|
6
|
+
"hero_title": "{{name}} 에 오신 걸 환영합니다",
|
|
7
|
+
"hero_subtitle": "MEGA-FRAMEWORK 로 만든 프로젝트입니다. EJS + ejs-mate 서버사이드 뷰, 쿠키 기반 다국어, Bootstrap 5 디자인이 기본 탑재되어 있습니다.",
|
|
8
|
+
"hero_cta_primary": "프레임워크 문서",
|
|
9
|
+
"hero_cta_secondary": "Bootstrap 문서",
|
|
10
|
+
"features_heading": "기본 탑재 기능",
|
|
11
|
+
"feature1_title": "세련된 뷰",
|
|
12
|
+
"feature1_desc": "Bootstrap 5 navbar · hero · 카드 그리드로 첫 화면이 구성됩니다.",
|
|
13
|
+
"feature2_title": "다국어 토글",
|
|
14
|
+
"feature2_desc": "navbar 의 언어 메뉴로 한국어/English 를 즉시 전환합니다(쿠키 기반).",
|
|
15
|
+
"feature3_title": "라이트·다크 모드",
|
|
16
|
+
"feature3_desc": "Bootstrap 5.3 color modes 로 테마를 전환하고 브라우저에 저장합니다."
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* mega.config.js — Global-only 스코프(ADR-061). 활성 앱 whitelist·전역 자원(databases/caches/buses)·
|
|
4
|
+
* 서버 설정만 둔다. App-only 키(cors/helmet 등)는 apps/<name>/app.config.js 로.
|
|
5
|
+
*/
|
|
6
|
+
export default {
|
|
7
|
+
apps: ['main'],
|
|
8
|
+
server: {
|
|
9
|
+
port: Number(process.env.PORT ?? 3000),
|
|
10
|
+
},
|
|
11
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "mega start",
|
|
11
|
+
"start": "mega start",
|
|
12
|
+
"scheduler": "mega scheduler",
|
|
13
|
+
"worker": "mega worker",
|
|
14
|
+
"ws-hub": "mega-ws-hub",
|
|
15
|
+
"dev:all": "concurrently -n server,scheduler,worker -c blue,green,magenta \"mega start\" \"mega scheduler\" \"mega worker\"",
|
|
16
|
+
"test": "mega test"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"mega-framework": "^0.1.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"concurrently": "^9.0.0",
|
|
23
|
+
"vitest": "^4.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* app.css — MEGA-FRAMEWORK 브랜드 소폭 커스텀(Bootstrap 5.3 위에 얹는다).
|
|
3
|
+
* Bootstrap 컴포넌트는 인스턴스 CSS 변수(--bs-btn-*, --bs-link-* 등)로 색을 잡으므로
|
|
4
|
+
* Sass 재컴파일 없이 변수만 덮어써 브랜드 색을 입힌다. per Bootstrap 5.3 CSS variables 문서.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
--brand: #5b4bff;
|
|
9
|
+
--brand-rgb: 91, 75, 255;
|
|
10
|
+
--brand-dark: #4a3fd6;
|
|
11
|
+
--brand-light: #efedff;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* 링크·primary 강조를 브랜드 색으로(라이트/다크 공통) */
|
|
15
|
+
a {
|
|
16
|
+
--bs-link-color-rgb: var(--brand-rgb);
|
|
17
|
+
--bs-link-hover-color-rgb: 74, 63, 214;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.btn-primary {
|
|
21
|
+
--bs-btn-bg: var(--brand);
|
|
22
|
+
--bs-btn-border-color: var(--brand);
|
|
23
|
+
--bs-btn-hover-bg: var(--brand-dark);
|
|
24
|
+
--bs-btn-hover-border-color: var(--brand-dark);
|
|
25
|
+
--bs-btn-active-bg: var(--brand-dark);
|
|
26
|
+
--bs-btn-active-border-color: var(--brand-dark);
|
|
27
|
+
--bs-btn-disabled-bg: var(--brand);
|
|
28
|
+
--bs-btn-disabled-border-color: var(--brand);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.btn-outline-primary {
|
|
32
|
+
--bs-btn-color: var(--brand);
|
|
33
|
+
--bs-btn-border-color: var(--brand);
|
|
34
|
+
--bs-btn-hover-bg: var(--brand);
|
|
35
|
+
--bs-btn-hover-border-color: var(--brand);
|
|
36
|
+
--bs-btn-active-bg: var(--brand);
|
|
37
|
+
--bs-btn-active-border-color: var(--brand);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.text-brand {
|
|
41
|
+
color: var(--brand) !important;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.navbar-brand {
|
|
45
|
+
font-weight: 700;
|
|
46
|
+
letter-spacing: -0.02em;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.navbar-brand .brand-dot {
|
|
50
|
+
color: var(--brand);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* hero — 브랜드 그라데이션 배경 + 넉넉한 여백 */
|
|
54
|
+
.hero {
|
|
55
|
+
background: radial-gradient(120% 120% at 0% 0%, rgba(var(--brand-rgb), 0.12), transparent 60%),
|
|
56
|
+
radial-gradient(120% 120% at 100% 0%, rgba(var(--brand-rgb), 0.08), transparent 55%);
|
|
57
|
+
border-radius: 1rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.hero h1 {
|
|
61
|
+
letter-spacing: -0.03em;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* feature 카드 — hover 살짝 떠오르는 인터랙션 */
|
|
65
|
+
.feature-card {
|
|
66
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.feature-card:hover {
|
|
70
|
+
transform: translateY(-3px);
|
|
71
|
+
box-shadow: 0 0.75rem 1.5rem rgba(var(--brand-rgb), 0.12);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.feature-icon {
|
|
75
|
+
display: inline-flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
width: 3rem;
|
|
79
|
+
height: 3rem;
|
|
80
|
+
border-radius: 0.75rem;
|
|
81
|
+
background: var(--brand-light);
|
|
82
|
+
color: var(--brand);
|
|
83
|
+
font-size: 1.5rem;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
[data-bs-theme='dark'] .feature-icon {
|
|
87
|
+
background: rgba(var(--brand-rgb), 0.18);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
footer.site-footer {
|
|
91
|
+
border-top: 1px solid var(--bs-border-color);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* 테마 토글 아이콘 — 현재 테마에서 "전환 대상"을 보여준다(라이트면 🌙, 다크면 ☀️). */
|
|
95
|
+
[data-bs-theme='light'] .theme-icon-light {
|
|
96
|
+
display: none;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
[data-bs-theme='dark'] .theme-icon-dark {
|
|
100
|
+
display: none;
|
|
101
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* app.js — 다크모드 토글 + 언어 토글 + 삭제 확인 모달 wiring(클라이언트 동작).
|
|
3
|
+
*
|
|
4
|
+
* helmet CSP(script-src 'self')가 인라인 스크립트를 막으므로 모든 클라 동작은 외부 파일로 둔다(ADR-153).
|
|
5
|
+
* 다크모드: 페인트 전 적용은 theme-init.js(<head>), 토글은 여기서. localStorage('mega.theme') 저장.
|
|
6
|
+
* 언어: 프레임워크가 locale 을 쿠키(mega.lang)로만 감지하므로(ADR-038), 토글은 쿠키를 굽고 새로고침.
|
|
7
|
+
* 삭제모달: 트리거 버튼의 data-action/data-name 으로 form action·문구를 채운다(#deleteModal 있을 때만).
|
|
8
|
+
*/
|
|
9
|
+
;(function () {
|
|
10
|
+
'use strict'
|
|
11
|
+
|
|
12
|
+
/** @param {string} theme */
|
|
13
|
+
function applyTheme(theme) {
|
|
14
|
+
document.documentElement.setAttribute('data-bs-theme', theme)
|
|
15
|
+
try {
|
|
16
|
+
localStorage.setItem('mega.theme', theme)
|
|
17
|
+
} catch (e) {
|
|
18
|
+
// localStorage 차단(사생활 모드 등)은 비치명적 — 이번 페이지에서만 테마가 안 남을 뿐.
|
|
19
|
+
console.debug('theme persist skipped:', e && e.message)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
document.querySelectorAll('[data-theme-toggle]').forEach(function (btn) {
|
|
24
|
+
btn.addEventListener('click', function () {
|
|
25
|
+
var current = document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light'
|
|
26
|
+
applyTheme(current === 'dark' ? 'light' : 'dark')
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
document.querySelectorAll('[data-lang]').forEach(function (el) {
|
|
31
|
+
el.addEventListener('click', function (e) {
|
|
32
|
+
e.preventDefault()
|
|
33
|
+
var lang = el.getAttribute('data-lang')
|
|
34
|
+
// 1년 유지, 같은 사이트 요청에만 전송(samesite=lax).
|
|
35
|
+
document.cookie = 'mega.lang=' + encodeURIComponent(lang) + '; path=/; max-age=31536000; samesite=lax'
|
|
36
|
+
location.reload()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// 삭제 확인 모달 — 클릭된 행 버튼의 data-action(삭제 URL)·data-name 으로 form·문구를 채운다.
|
|
41
|
+
// #deleteModal 이 있는 페이지(예: 목록)에서만 동작하고, 없으면 no-op.
|
|
42
|
+
var deleteModal = document.getElementById('deleteModal')
|
|
43
|
+
if (deleteModal) {
|
|
44
|
+
deleteModal.addEventListener('show.bs.modal', function (event) {
|
|
45
|
+
var btn = event.relatedTarget
|
|
46
|
+
if (!btn) return
|
|
47
|
+
var form = document.getElementById('deleteForm')
|
|
48
|
+
var action = btn.getAttribute('data-action')
|
|
49
|
+
if (form && action) form.setAttribute('action', action)
|
|
50
|
+
var nameEl = document.getElementById('deleteModalName')
|
|
51
|
+
if (nameEl) nameEl.textContent = btn.getAttribute('data-name') || ''
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
})()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* theme-init.js — 저장된 테마를 페인트 전에 적용(FOUC 방지). layout <head> 에서 동기 로드한다.
|
|
3
|
+
* helmet CSP(script-src 'self')가 인라인 스크립트를 막으므로 외부 파일로 둔다(ADR-153).
|
|
4
|
+
*/
|
|
5
|
+
;(function () {
|
|
6
|
+
try {
|
|
7
|
+
var t = localStorage.getItem('mega.theme')
|
|
8
|
+
if (t) document.documentElement.setAttribute('data-bs-theme', t)
|
|
9
|
+
} catch (e) {
|
|
10
|
+
// localStorage 차단(사생활 모드 등)은 비치명적 — 기본 테마로 렌더된다.
|
|
11
|
+
}
|
|
12
|
+
})()
|