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,79 @@
|
|
|
1
|
+
<% layout('layouts/main') %>
|
|
2
|
+
<%
|
|
3
|
+
function fmt(d) { return new Date(d).toLocaleString('ko-KR', { hour12: false, timeZone: 'Asia/Seoul' }) }
|
|
4
|
+
function kb(b) { return (Number(b) / 1024).toFixed(1) + ' KB' }
|
|
5
|
+
%>
|
|
6
|
+
|
|
7
|
+
<div class="mb-4">
|
|
8
|
+
<h1 class="h3 mb-1"><%= t('upload_title', { defaultValue: '파일 업로드' }) %></h1>
|
|
9
|
+
<p class="text-body-secondary small mb-0"><%= t('upload_subtitle', { defaultValue: 'multipart 업로드를 프로젝트 폴더에 저장하고 메타·다운로드 링크를 보여줍니다.' }) %></p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="row g-3">
|
|
13
|
+
<div class="col-lg-5">
|
|
14
|
+
<div class="card h-100">
|
|
15
|
+
<div class="card-body">
|
|
16
|
+
<h2 class="h5 card-title"><%= t('upload_form_title', { defaultValue: '업로드' }) %></h2>
|
|
17
|
+
<p class="card-text text-body-secondary small"><%= t('upload_form_desc', { defaultValue: '이미지·PDF·텍스트 파일을 올립니다 (MIME·크기·개수 게이트).' }) %></p>
|
|
18
|
+
<form id="up-form" data-csrf="<%= csrfToken %>">
|
|
19
|
+
<div class="mb-3">
|
|
20
|
+
<label class="form-label small mb-1" for="up-file"><%= t('upload_file_label', { defaultValue: '파일 선택' }) %></label>
|
|
21
|
+
<input type="file" class="form-control form-control-sm" id="up-file" name="file" multiple accept="image/*,application/pdf,text/plain" />
|
|
22
|
+
<div class="form-text small"><%= t('upload_constraints', { defaultValue: '허용: 이미지/PDF/텍스트 · 최대 5MB · 한 번에 3개까지' }) %></div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="alert alert-light border small py-2 mb-3">
|
|
25
|
+
<%= t('upload_dir_hint', { defaultValue: '저장 위치(프로젝트 루트 기준):' }) %> <code><%= uploadDir %>/</code>
|
|
26
|
+
</div>
|
|
27
|
+
<button type="submit" class="btn btn-primary btn-sm" id="up-submit">
|
|
28
|
+
<span class="spinner-border spinner-border-sm d-none" id="up-spinner" role="status" aria-hidden="true"></span>
|
|
29
|
+
<%= t('upload_submit', { defaultValue: '업로드' }) %>
|
|
30
|
+
</button>
|
|
31
|
+
</form>
|
|
32
|
+
<div class="alert alert-danger small mt-3 mb-0 d-none" id="up-error">
|
|
33
|
+
<strong><%= t('upload_error', { defaultValue: '업로드 실패:' }) %></strong> <span id="up-error-detail"></span>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="col-lg-7">
|
|
40
|
+
<div class="card h-100">
|
|
41
|
+
<div class="card-body">
|
|
42
|
+
<h2 class="h5 card-title"><%= t('upload_recent_title', { defaultValue: '최근 업로드' }) %></h2>
|
|
43
|
+
<p class="card-text text-body-secondary small"><%= t('upload_recent_desc', { defaultValue: '저장된 파일의 메타데이터입니다 (파일명 클릭 시 다운로드).' }) %></p>
|
|
44
|
+
<% if (snap.recent.length === 0) { %>
|
|
45
|
+
<p class="text-body-secondary small mb-0"><%= t('upload_recent_empty', { defaultValue: '아직 업로드한 파일이 없습니다.' }) %></p>
|
|
46
|
+
<% } else { %>
|
|
47
|
+
<div class="table-responsive">
|
|
48
|
+
<table class="table table-sm align-middle mb-0">
|
|
49
|
+
<thead>
|
|
50
|
+
<tr>
|
|
51
|
+
<th scope="col"><%= t('upload_recent_at', { defaultValue: '시각' }) %></th>
|
|
52
|
+
<th scope="col"><%= t('upload_recent_name', { defaultValue: '파일명' }) %></th>
|
|
53
|
+
<th scope="col"><%= t('upload_recent_path', { defaultValue: '저장 경로' }) %></th>
|
|
54
|
+
<th scope="col"><%= t('upload_recent_mime', { defaultValue: 'MIME' }) %></th>
|
|
55
|
+
<th scope="col" class="text-end"><%= t('upload_recent_size', { defaultValue: '크기' }) %></th>
|
|
56
|
+
</tr>
|
|
57
|
+
</thead>
|
|
58
|
+
<tbody>
|
|
59
|
+
<% snap.recent.forEach(function (e) { %>
|
|
60
|
+
<tr>
|
|
61
|
+
<td class="font-monospace small"><%= fmt(e.at) %></td>
|
|
62
|
+
<td class="small text-break">
|
|
63
|
+
<a href="/demo/upload/file/<%= encodeURIComponent(e.filename) %>"><%= e.filename %></a>
|
|
64
|
+
</td>
|
|
65
|
+
<td class="font-monospace small text-break"><%= e.path || '—' %></td>
|
|
66
|
+
<td><span class="badge text-bg-light"><code><%= e.mimetype %></code></span></td>
|
|
67
|
+
<td class="text-end font-monospace small"><%= kb(e.bytes) %></td>
|
|
68
|
+
</tr>
|
|
69
|
+
<% }) %>
|
|
70
|
+
</tbody>
|
|
71
|
+
</table>
|
|
72
|
+
</div>
|
|
73
|
+
<% } %>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<script src="/static/js/upload-demo.js"></script>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<% layout('layouts/main') %>
|
|
2
|
+
|
|
3
|
+
<div class="row justify-content-center">
|
|
4
|
+
<div class="col-lg-6">
|
|
5
|
+
<h1 class="h3 mb-4"><%= t('edit_title', '사용자 수정') %> <span class="text-body-secondary">#<%= values.id %></span></h1>
|
|
6
|
+
|
|
7
|
+
<% if (typeof error !== 'undefined' && error) { %>
|
|
8
|
+
<div class="alert alert-danger" role="alert"><%= error %></div>
|
|
9
|
+
<% } %>
|
|
10
|
+
|
|
11
|
+
<form method="post" action="/admin/users/<%= values.id %>" novalidate>
|
|
12
|
+
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
|
13
|
+
|
|
14
|
+
<div class="mb-3">
|
|
15
|
+
<label for="name" class="form-label"><%= t('field_name', '이름') %></label>
|
|
16
|
+
<input
|
|
17
|
+
type="text"
|
|
18
|
+
class="form-control <%= invalid && invalid.name ? 'is-invalid' : '' %>"
|
|
19
|
+
id="name"
|
|
20
|
+
name="name"
|
|
21
|
+
value="<%= values && values.name ? values.name : '' %>"
|
|
22
|
+
placeholder="<%= t('field_name_ph', '예: 홍길동') %>"
|
|
23
|
+
required
|
|
24
|
+
/>
|
|
25
|
+
<div class="invalid-feedback"><%= t('field_name_required', '이름을 입력하세요.') %></div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="mb-4">
|
|
29
|
+
<label for="email" class="form-label"><%= t('field_email', '이메일') %></label>
|
|
30
|
+
<input
|
|
31
|
+
type="email"
|
|
32
|
+
class="form-control <%= invalid && invalid.email ? 'is-invalid' : '' %>"
|
|
33
|
+
id="email"
|
|
34
|
+
name="email"
|
|
35
|
+
value="<%= values && values.email ? values.email : '' %>"
|
|
36
|
+
placeholder="<%= t('field_email_ph', '예: hong@example.com') %>"
|
|
37
|
+
required
|
|
38
|
+
/>
|
|
39
|
+
<div class="invalid-feedback"><%= t('field_email_required', '올바른 이메일을 입력하세요.') %></div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="d-flex gap-2">
|
|
43
|
+
<button type="submit" class="btn btn-primary"><%= t('btn_save', '저장') %></button>
|
|
44
|
+
<a href="/admin/users" class="btn btn-outline-secondary"><%= t('btn_cancel', '취소') %></a>
|
|
45
|
+
</div>
|
|
46
|
+
</form>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<% layout('layouts/main') %>
|
|
2
|
+
|
|
3
|
+
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-4">
|
|
4
|
+
<h1 class="h3 mb-0"><%= t('users_title', '사용자 목록') %></h1>
|
|
5
|
+
<a href="/admin/users/new" class="btn btn-primary">+ <%= t('users_new', '사용자 추가') %></a>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<% if (typeof notice !== 'undefined' && notice) { %>
|
|
9
|
+
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
10
|
+
<%= notice %>
|
|
11
|
+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="close"></button>
|
|
12
|
+
</div>
|
|
13
|
+
<% } %>
|
|
14
|
+
|
|
15
|
+
<% if (!users || users.length === 0) { %>
|
|
16
|
+
<div class="text-center text-body-secondary py-5">
|
|
17
|
+
<div class="display-4 mb-3">📭</div>
|
|
18
|
+
<p class="mb-3"><%= t('users_empty', '아직 사용자가 없습니다.') %></p>
|
|
19
|
+
<a href="/admin/users/new" class="btn btn-outline-primary"><%= t('users_new', '사용자 추가') %></a>
|
|
20
|
+
</div>
|
|
21
|
+
<% } else { %>
|
|
22
|
+
<div class="table-responsive">
|
|
23
|
+
<table class="table table-striped table-hover align-middle">
|
|
24
|
+
<thead>
|
|
25
|
+
<tr>
|
|
26
|
+
<th scope="col" class="text-end" style="width: 5rem"><%= t('col_id', 'ID') %></th>
|
|
27
|
+
<th scope="col"><%= t('col_name', '이름') %></th>
|
|
28
|
+
<th scope="col"><%= t('col_email', '이메일') %></th>
|
|
29
|
+
<th scope="col"><%= t('col_created', '생성일') %></th>
|
|
30
|
+
<th scope="col" class="text-end"><%= t('col_actions', '관리') %></th>
|
|
31
|
+
</tr>
|
|
32
|
+
</thead>
|
|
33
|
+
<tbody>
|
|
34
|
+
<% users.forEach(function (u) { %>
|
|
35
|
+
<tr>
|
|
36
|
+
<td class="text-end text-body-secondary"><%= u.id %></td>
|
|
37
|
+
<td class="fw-medium"><%= u.name %></td>
|
|
38
|
+
<td><%= u.email %></td>
|
|
39
|
+
<td class="text-body-secondary small"><%= u.created_at %></td>
|
|
40
|
+
<td class="text-end text-nowrap">
|
|
41
|
+
<a href="/admin/users/<%= u.id %>/edit" class="btn btn-sm btn-outline-secondary"><%= t('action_edit', '수정') %></a>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
class="btn btn-sm btn-outline-danger"
|
|
45
|
+
data-bs-toggle="modal"
|
|
46
|
+
data-bs-target="#deleteModal"
|
|
47
|
+
data-action="/admin/users/<%= u.id %>/delete"
|
|
48
|
+
data-name="<%= u.name %>"
|
|
49
|
+
>
|
|
50
|
+
<%= t('action_delete', '삭제') %>
|
|
51
|
+
</button>
|
|
52
|
+
</td>
|
|
53
|
+
</tr>
|
|
54
|
+
<% }) %>
|
|
55
|
+
</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
</div>
|
|
58
|
+
<% } %>
|
|
59
|
+
|
|
60
|
+
<%# 삭제 확인 모달 — 한 개를 재사용. 클릭된 행의 data-action/data-name 으로 app.js 가 form·문구를 채운다
|
|
61
|
+
(show.bs.modal). 인라인 스크립트는 CSP(script-src 'self')에 막히므로 wiring 은 app.js(외부)에 둔다. %>
|
|
62
|
+
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
|
63
|
+
<div class="modal-dialog modal-dialog-centered">
|
|
64
|
+
<div class="modal-content">
|
|
65
|
+
<div class="modal-header">
|
|
66
|
+
<h5 class="modal-title" id="deleteModalLabel"><%= t('delete_confirm_title', '삭제 확인') %></h5>
|
|
67
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="modal-body">
|
|
70
|
+
<%= t('delete_confirm_body', '정말 삭제할까요?') %> <strong id="deleteModalName"></strong>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="modal-footer">
|
|
73
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><%= t('delete_confirm_cancel', '취소') %></button>
|
|
74
|
+
<form id="deleteForm" method="post" class="d-inline">
|
|
75
|
+
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
|
76
|
+
<button type="submit" class="btn btn-danger"><%= t('delete_confirm_ok', '삭제') %></button>
|
|
77
|
+
</form>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<% layout('layouts/main') %>
|
|
2
|
+
|
|
3
|
+
<div class="row justify-content-center">
|
|
4
|
+
<div class="col-lg-6">
|
|
5
|
+
<h1 class="h3 mb-4"><%= t('new_title', '사용자 추가') %></h1>
|
|
6
|
+
|
|
7
|
+
<% if (typeof error !== 'undefined' && error) { %>
|
|
8
|
+
<div class="alert alert-danger" role="alert"><%= error %></div>
|
|
9
|
+
<% } %>
|
|
10
|
+
|
|
11
|
+
<form method="post" action="/admin/users" novalidate>
|
|
12
|
+
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
|
13
|
+
|
|
14
|
+
<div class="mb-3">
|
|
15
|
+
<label for="name" class="form-label"><%= t('field_name', '이름') %></label>
|
|
16
|
+
<input
|
|
17
|
+
type="text"
|
|
18
|
+
class="form-control <%= invalid && invalid.name ? 'is-invalid' : '' %>"
|
|
19
|
+
id="name"
|
|
20
|
+
name="name"
|
|
21
|
+
value="<%= values && values.name ? values.name : '' %>"
|
|
22
|
+
placeholder="<%= t('field_name_ph', '예: 홍길동') %>"
|
|
23
|
+
required
|
|
24
|
+
/>
|
|
25
|
+
<div class="invalid-feedback"><%= t('field_name_required', '이름을 입력하세요.') %></div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="mb-4">
|
|
29
|
+
<label for="email" class="form-label"><%= t('field_email', '이메일') %></label>
|
|
30
|
+
<input
|
|
31
|
+
type="email"
|
|
32
|
+
class="form-control <%= invalid && invalid.email ? 'is-invalid' : '' %>"
|
|
33
|
+
id="email"
|
|
34
|
+
name="email"
|
|
35
|
+
value="<%= values && values.email ? values.email : '' %>"
|
|
36
|
+
placeholder="<%= t('field_email_ph', '예: hong@example.com') %>"
|
|
37
|
+
required
|
|
38
|
+
/>
|
|
39
|
+
<div class="invalid-feedback"><%= t('field_email_required', '올바른 이메일을 입력하세요.') %></div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="d-flex gap-2">
|
|
43
|
+
<button type="submit" class="btn btn-primary"><%= t('btn_create', '생성') %></button>
|
|
44
|
+
<a href="/admin/users" class="btn btn-outline-secondary"><%= t('btn_cancel', '취소') %></a>
|
|
45
|
+
</div>
|
|
46
|
+
</form>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<% layout('layouts/main') %>
|
|
2
|
+
|
|
3
|
+
<div class="mb-4">
|
|
4
|
+
<h1 class="h3 mb-1"><%= t('worker_title', { defaultValue: 'CPU 워커 데모 (MegaWorker)' }) %></h1>
|
|
5
|
+
<p class="text-body-secondary small mb-0"><%= t('worker_subtitle', { defaultValue: 'SHA-256 N회 반복 같은 CPU-bound 작업을 worker_threads 풀에서 돌립니다. 계산 도중에도 서버가 다른 요청에 즉시 응답하는지(메인 스레드 non-block) 하트비트로 확인합니다.' }) %></p>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="row g-3">
|
|
9
|
+
<div class="col-lg-6">
|
|
10
|
+
<div class="card h-100">
|
|
11
|
+
<div class="card-body d-flex flex-column">
|
|
12
|
+
<h2 class="h5 card-title"><%= t('worker_run_title', { defaultValue: '해시 계산 실행' }) %></h2>
|
|
13
|
+
<p class="card-text text-body-secondary small"><%= t('worker_run_desc', { defaultValue: '반복 횟수만큼 SHA-256 을 체인 해시합니다. 메인 스레드가 아니라 워커 스레드에서 돕니다.' }) %></p>
|
|
14
|
+
<div class="mb-3">
|
|
15
|
+
<label for="wd-rounds" class="form-label small"><%= t('worker_rounds_label', { defaultValue: '반복 횟수' }) %></label>
|
|
16
|
+
<input type="number" class="form-control form-control-sm" id="wd-rounds" value="<%= defaultRounds %>" min="100000" max="20000000" step="100000" />
|
|
17
|
+
</div>
|
|
18
|
+
<div class="d-flex align-items-center gap-2">
|
|
19
|
+
<button type="button" class="btn btn-primary btn-sm" id="wd-run">
|
|
20
|
+
<span class="spinner-border spinner-border-sm d-none" id="wd-spinner" role="status" aria-hidden="true"></span>
|
|
21
|
+
<span id="wd-run-label"><%= t('worker_run_btn', { defaultValue: '워커에서 실행' }) %></span>
|
|
22
|
+
</button>
|
|
23
|
+
<span class="text-body-secondary small">
|
|
24
|
+
<%= t('worker_pool_mode', { defaultValue: '모드' }) %>: <code><%= pool.mode %></code> · <%= t('worker_pool_size', { defaultValue: '풀 크기' }) %>: <code><%= pool.poolSize %></code>
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="alert alert-danger mt-3 d-none small" id="wd-error" role="alert">
|
|
29
|
+
<%= t('worker_error', { defaultValue: '실행 실패:' }) %> <span id="wd-error-detail" class="font-monospace"></span>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="mt-auto pt-3">
|
|
33
|
+
<div class="border rounded p-3 bg-body-tertiary d-none" id="wd-result">
|
|
34
|
+
<div class="d-flex justify-content-between"><span class="text-body-secondary small"><%= t('worker_result_rounds', { defaultValue: '반복 횟수' }) %></span><span class="font-monospace" id="wd-rounds-out">—</span></div>
|
|
35
|
+
<div class="d-flex justify-content-between"><span class="text-body-secondary small"><%= t('worker_result_ms', { defaultValue: '소요 시간' }) %></span><span class="font-monospace" id="wd-ms-out">—</span></div>
|
|
36
|
+
<div class="mt-2 text-body-secondary small"><%= t('worker_result_digest', { defaultValue: '최종 해시' }) %></div>
|
|
37
|
+
<div class="font-monospace text-break small" id="wd-digest-out">—</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="col-lg-6">
|
|
45
|
+
<div class="card h-100">
|
|
46
|
+
<div class="card-body d-flex flex-column">
|
|
47
|
+
<h2 class="h5 card-title"><%= t('worker_heartbeat_title', { defaultValue: '메인 스레드 하트비트' }) %></h2>
|
|
48
|
+
<p class="card-text text-body-secondary small"><%= t('worker_heartbeat_desc', { defaultValue: '1초마다 서버에 ping 을 보냅니다. 워커가 계산하는 동안에도 이 지연이 작게 유지되면 메인 스레드가 안 막힌 것입니다.' }) %></p>
|
|
49
|
+
<div class="d-flex gap-4 mt-2">
|
|
50
|
+
<div>
|
|
51
|
+
<div class="display-6 fw-bold" id="wd-ping-count">0</div>
|
|
52
|
+
<div class="text-body-secondary small"><%= t('worker_heartbeat_count', { defaultValue: 'ping 횟수' }) %></div>
|
|
53
|
+
</div>
|
|
54
|
+
<div>
|
|
55
|
+
<div class="display-6 fw-bold"><span id="wd-ping-latency">—</span><span class="fs-6 text-body-secondary">ms</span></div>
|
|
56
|
+
<div class="text-body-secondary small"><%= t('worker_heartbeat_latency', { defaultValue: '최근 왕복 지연' }) %></div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="mt-2 text-body-secondary small">
|
|
60
|
+
<%= t('worker_heartbeat_last', { defaultValue: '마지막 ping' }) %>: <span class="font-monospace" id="wd-ping-time">—</span>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="mt-auto pt-3">
|
|
63
|
+
<div class="alert alert-info small mb-0"><%= t('worker_heartbeat_proof', { defaultValue: '계산을 메인 스레드에서 했다면 이 ping 들이 계산이 끝날 때까지 통째로 멈춥니다. 워커 스레드라 멈추지 않습니다.' }) %></div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<script src="/static/js/worker-demo.js"></script>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<% layout('layouts/main') %>
|
|
2
|
+
|
|
3
|
+
<div class="mb-4 d-flex align-items-start justify-content-between flex-wrap gap-2">
|
|
4
|
+
<div>
|
|
5
|
+
<h1 class="h3 mb-1"><%= t('ws_title', { defaultValue: '실시간 채팅 (WebSocket + ASP)' }) %></h1>
|
|
6
|
+
<p class="text-body-secondary small mb-0"><%= t('ws_subtitle', { defaultValue: 'WASM MegaSocket 으로 /ws/chat 에 접속합니다. 메시지는 ASP 로 암호화(E: 프레임)되어 브라우저에서 직접 암복호화되고, 서버는 채널 전체에 broadcast 합니다.' }) %></p>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="text-end">
|
|
9
|
+
<span class="badge text-bg-success" title="<%= t('ws_asp_badge_title', { defaultValue: '모든 메시지가 AES-256-GCM(E: 프레임)으로 암호화되어 송수신됩니다.' }) %>">🔒 <%= t('ws_asp_badge', { defaultValue: 'ASP 암호화' }) %></span>
|
|
10
|
+
<div class="text-body-secondary small mt-1"><code>E:</code> AES-256-GCM · WASM</div>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<%# 루트 컨테이너 — data 속성으로 ws 경로·ASP 시크릿·번역 템플릿을 외부 모듈(ws-chat.js)에 전달.
|
|
15
|
+
CSP(script-src 'self')라 인라인 주입 불가 → data 속성 + 외부 ESM 스크립트. %>
|
|
16
|
+
<div
|
|
17
|
+
id="chat-root"
|
|
18
|
+
class="card"
|
|
19
|
+
data-ws-path="<%= wsPath %>"
|
|
20
|
+
data-asp-secret="<%= aspSecret %>"
|
|
21
|
+
data-me="<%= currentUser ? currentUser.name : '' %>"
|
|
22
|
+
data-t-status-connecting="<%= t('ws_status_connecting', { defaultValue: '연결 중…' }) %>"
|
|
23
|
+
data-t-status-open="<%= t('ws_status_open', { defaultValue: '연결됨' }) %>"
|
|
24
|
+
data-t-status-closed="<%= t('ws_status_closed', { defaultValue: '연결 끊김' }) %>"
|
|
25
|
+
data-t-status-error="<%= t('ws_status_error', { defaultValue: '오류' }) %>"
|
|
26
|
+
data-t-online="<%= t('ws_online', { defaultValue: '접속자 {n}명' }) %>"
|
|
27
|
+
data-t-join="<%= t('ws_presence_join', { defaultValue: '{user} 님이 입장했습니다.' }) %>"
|
|
28
|
+
data-t-leave="<%= t('ws_presence_leave', { defaultValue: '{user} 님이 퇴장했습니다.' }) %>"
|
|
29
|
+
>
|
|
30
|
+
<div class="card-header d-flex align-items-center justify-content-between">
|
|
31
|
+
<span class="d-flex align-items-center gap-2">
|
|
32
|
+
<span id="chat-status-dot" class="d-inline-block rounded-circle bg-secondary" style="width: 0.6rem; height: 0.6rem"></span>
|
|
33
|
+
<span id="chat-status" class="small text-body-secondary"><%= t('ws_status_connecting', { defaultValue: '연결 중…' }) %></span>
|
|
34
|
+
<%# 클러스터 워커 PID — 같은 명단·메시지가 서로 다른 워커에 닿는지 눈으로 확인(cluster broadcast 실증). %>
|
|
35
|
+
<span id="chat-worker" class="badge text-bg-secondary font-monospace" style="display: none"></span>
|
|
36
|
+
</span>
|
|
37
|
+
<span id="chat-online" class="badge text-bg-light" data-bs-toggle="tooltip" title=""></span>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<%# 메시지 목록 — JS 가 textContent 로만 채운다(XSS 방지: 사용자 입력을 innerHTML 로 넣지 않음). %>
|
|
41
|
+
<div id="chat-messages" class="list-group list-group-flush overflow-auto" style="height: 22rem" aria-live="polite">
|
|
42
|
+
<div data-empty class="list-group-item text-body-secondary small text-center py-5"><%= t('ws_empty', { defaultValue: '아직 메시지가 없습니다. 첫 메시지를 보내보세요.' }) %></div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="card-footer">
|
|
46
|
+
<form id="chat-form" class="d-flex gap-2" autocomplete="off">
|
|
47
|
+
<input
|
|
48
|
+
id="chat-input"
|
|
49
|
+
type="text"
|
|
50
|
+
class="form-control"
|
|
51
|
+
maxlength="500"
|
|
52
|
+
placeholder="<%= t('ws_input_placeholder', { defaultValue: '메시지를 입력하세요…' }) %>"
|
|
53
|
+
aria-label="<%= t('ws_input_placeholder', { defaultValue: '메시지를 입력하세요…' }) %>"
|
|
54
|
+
disabled
|
|
55
|
+
/>
|
|
56
|
+
<button id="chat-send" type="submit" class="btn btn-primary" disabled><%= t('ws_send', { defaultValue: '보내기' }) %></button>
|
|
57
|
+
</form>
|
|
58
|
+
<div class="text-body-secondary small mt-2"><%= t('ws_encrypted_note', { defaultValue: '메시지는 ASP(AES-256-GCM) E: 프레임으로 암호화되어 전송됩니다.' }) %></div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<script type="module" src="/static/js/ws-chat.js"></script>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { MegaWorker } from 'mega-framework'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HashWorker — /demo/worker CPU 격리 워커 풀(ADR-124). `config.workers` 로 등록하면 `mega start`(boot)가
|
|
6
|
+
* `ctx.workers['hash']` 에 배선한다. 호출: `ctx.workers['hash'].run('sha256Loop', { input, rounds })`.
|
|
7
|
+
*
|
|
8
|
+
* SHA-256 N회 반복 같은 CPU-bound 작업을 메인 스레드 대신 worker_threads 풀에서 돌려, 계산 중에도 HTTP
|
|
9
|
+
* 이벤트 루프가 다른 요청에 즉시 응답하게 한다(메인 스레드 non-block 시연). 작업 로직은 `static taskFile`
|
|
10
|
+
* 모듈의 named export 함수다(worker_threads 경계 제약, ADR-124).
|
|
11
|
+
*/
|
|
12
|
+
export class HashWorker extends MegaWorker {
|
|
13
|
+
static name = 'hash'
|
|
14
|
+
static taskFile = 'apps/main/workers/hash.task.js'
|
|
15
|
+
static mode = 'thread'
|
|
16
|
+
static poolSize = 2
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* HashWorker 의 작업 함수 모듈(= `static taskFile`, ADR-124). worker_threads 경계는 함수·클로저를 못 넘기므로
|
|
4
|
+
* 작업 로직은 이 별도 파일에 named export 한 async 함수로 두고, 워커가 로드해 `run(taskName, args)` 의
|
|
5
|
+
* taskName 으로 디스패치한다. node:crypto 만 쓰는 순수 CPU 작업이라 추가 의존성이 없다.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'node:crypto'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* SHA-256 을 N회 반복 체인 해시한다(이전 출력이 다음 입력) — 의도적 CPU-bound 작업. 이 루프는 워커 스레드에서
|
|
11
|
+
* 돌아 메인 스레드(HTTP 이벤트 루프)를 막지 않는다.
|
|
12
|
+
* @param {{ input?: string, rounds?: number }} task - 워커 경계를 넘는 직렬화 가능한 데이터만.
|
|
13
|
+
* @returns {Promise<{ digest: string, rounds: number }>} 최종 해시 + 반복 횟수.
|
|
14
|
+
*/
|
|
15
|
+
export async function sha256Loop(task) {
|
|
16
|
+
const rounds = typeof task.rounds === 'number' && task.rounds > 0 ? Math.floor(task.rounds) : 1_000_000
|
|
17
|
+
let digest = String(task.input ?? 'mega')
|
|
18
|
+
for (let i = 0; i < rounds; i++) {
|
|
19
|
+
digest = createHash('sha256').update(digest).digest('hex')
|
|
20
|
+
}
|
|
21
|
+
return { digest, rounds }
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// PM2 ecosystem — server / scheduler / worker 프로세스 정의.
|
|
2
|
+
// 사용: pm2 start ecosystem.config.cjs (PM2 는 별도 설치: npm i -g pm2)
|
|
3
|
+
module.exports = {
|
|
4
|
+
apps: [
|
|
5
|
+
{ name: 'sample-crud-server', script: 'node_modules/.bin/mega', args: 'start', instances: 1, autorestart: true },
|
|
6
|
+
{ name: 'sample-crud-scheduler', script: 'node_modules/.bin/mega', args: 'scheduler', instances: 1, autorestart: true },
|
|
7
|
+
{ name: 'sample-crud-worker', script: 'node_modules/.bin/mega', args: 'worker', instances: 2, autorestart: true },
|
|
8
|
+
],
|
|
9
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* mega.config.js — Global-only 스코프(ADR-061). 전역 자원(databases)·서버·관측성만 둔다.
|
|
4
|
+
* App-only 키(databases 별명 등)는 apps/<name>/app.config.js 로.
|
|
5
|
+
*/
|
|
6
|
+
import { CronCounterSchedule } from './apps/main/schedules/cron-counter-schedule.js'
|
|
7
|
+
import { EmailJob } from './apps/main/jobs/email-job.js'
|
|
8
|
+
import { HashWorker } from './apps/main/workers/hash-worker.js'
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
apps: ['main'],
|
|
12
|
+
|
|
13
|
+
server: {
|
|
14
|
+
// listen 포트 — PORT 환경변수 또는 `mega start --port N`(CLI 우선). per ADR-146.
|
|
15
|
+
port: Number(process.env.PORT ?? 3000),
|
|
16
|
+
// 세션 쿠키 HMAC 서명 시크릿(global 스코프, ADR-129). boot 이 앱에 주입한다. .env 의 SESSION_SECRET.
|
|
17
|
+
sessionSecret: process.env.SESSION_SECRET,
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
// ASP(Application-layer Secure Protocol) masterSecret — global 스코프 시크릿(ADR-127, scope-registry).
|
|
21
|
+
// boot 의 composeAspConfig 가 이 masterSecret 과 앱 스코프의 옵트인 범위(app.config.js 의
|
|
22
|
+
// asp.websocket.namespaces)를 합성해 앱에 주입한다. /ws/chat 의 E:/P: 프레임 키 유도에 쓰인다.
|
|
23
|
+
// ASP 는 클라이언트(WASM MegaSocket)도 같은 secret 으로 키를 유도하는 공유-키 구조라, 데모 페이지가
|
|
24
|
+
// 이 값을 브라우저에 주입한다 — 그래서 운영 secret 과 분리된 데모 전용 값을 .env 의 ASP_MASTER_SECRET 에 둔다.
|
|
25
|
+
asp: {
|
|
26
|
+
masterSecret: process.env.ASP_MASTER_SECRET,
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// 전역 어댑터(ADR-102/106/109) — globalKey 로 선언, 앱은 app.config.js 의 별명으로 참조.
|
|
30
|
+
services: {
|
|
31
|
+
databases: {
|
|
32
|
+
// DB 'primary' — 모델(static adapter='primary')·마이그레이션 러너(mega migrate)·ctx.db('db') 공유 인스턴스.
|
|
33
|
+
primary: {
|
|
34
|
+
driver: 'postgres',
|
|
35
|
+
url: process.env.DATABASE_URL,
|
|
36
|
+
},
|
|
37
|
+
// DB 'mongo' — Document DB 어댑터(ADR-108). notes 데모 컬렉션(Note 모델 static adapter='mongo')의 공유
|
|
38
|
+
// 인스턴스. url path 의 dbName(mega_test)을 어댑터가 추출한다. .env 의 MONGO_URL(authSource=admin).
|
|
39
|
+
mongo: {
|
|
40
|
+
driver: 'mongodb',
|
|
41
|
+
url: process.env.MONGO_URL,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
caches: {
|
|
45
|
+
// redis 캐시 'rate' — brute-force(ctx.bruteForce, ADR-049/130)의 원자적 INCR 백엔드. .env 의 REDIS_RATE_URL(db 2).
|
|
46
|
+
rate: {
|
|
47
|
+
driver: 'redis',
|
|
48
|
+
url: process.env.REDIS_RATE_URL,
|
|
49
|
+
},
|
|
50
|
+
// redis 캐시 'demo' — /demo/redis 데모(방문 카운터 INCR/EXPIRE + 쿼리 결과 캐시 GET/SET/TTL/DEL)와
|
|
51
|
+
// /demo/cron 카운터·이력, /demo/jobs 처리 이벤트의 백엔드. 세션(db0)·rate(db2)와 키 충돌을 피하려 별도
|
|
52
|
+
// 논리 DB(db1)를 쓴다. 용도별 키 네임스페이스(demo:redis:* / demo:cron:* / demo:jobs:*)로 구분한다.
|
|
53
|
+
demo: {
|
|
54
|
+
driver: 'redis',
|
|
55
|
+
url: process.env.REDIS_DEMO_URL,
|
|
56
|
+
},
|
|
57
|
+
// redis 캐시 'lock' — 분산 락(redlock, locks.main)이 참조하는 Redis 연결(ADR-113). redlock 은 자체 연결을
|
|
58
|
+
// 열지 않고 등록된 Redis 캐시 어댑터를 빌려 쓴다. 락 키(mega:schedule:*)를 데모 데이터와 분리하려 별도 db(3).
|
|
59
|
+
lock: {
|
|
60
|
+
driver: 'redis',
|
|
61
|
+
url: process.env.REDIS_LOCK_URL,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
// NATS 버스 'jobs' — 잡 큐(EmailJob, ADR-119)의 JetStream 백엔드. producer(웹)는 ctx.bus('jobs').native(nc)로
|
|
65
|
+
// enqueue, consumer(`mega worker`)는 같은 버스로 소비한다. .env 의 NATS_JOBS_URL.
|
|
66
|
+
buses: {
|
|
67
|
+
jobs: {
|
|
68
|
+
driver: 'nats',
|
|
69
|
+
url: process.env.NATS_JOBS_URL,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
// 분산 락 'main' — redlock(ADR-113). caches.lock(Redis) 을 빌려 단일 노드 redlock 을 구성한다. /demo/cron
|
|
73
|
+
// 스케줄(CronCounterSchedule.static lock)의 클러스터 중복방지(leader election)에 쓰인다.
|
|
74
|
+
locks: {
|
|
75
|
+
main: {
|
|
76
|
+
driver: 'redlock',
|
|
77
|
+
redis: 'lock',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// 정기 작업(ADR-028/118) — `mega scheduler` 프로세스가 등록·실행한다(config.schedules, ADR-123).
|
|
83
|
+
schedules: [CronCounterSchedule],
|
|
84
|
+
|
|
85
|
+
// 영속 잡(ADR-028/119) — `mega worker` 프로세스(ecosystem instances:2)가 소비한다(config.jobs, ADR-123).
|
|
86
|
+
jobs: [EmailJob],
|
|
87
|
+
|
|
88
|
+
// CPU 워커 풀(ADR-124) — `mega start`(boot)가 ctx.workers['hash'] 로 배선한다(config.workers).
|
|
89
|
+
workers: [HashWorker],
|
|
90
|
+
|
|
91
|
+
// pino 로깅(ADR-023/141) — console sink, dev pretty. redact 로 민감필드를 sink 출력 전 메인스레드에서
|
|
92
|
+
// 마스킹한다(token/password/secret/authorization). /demo/logs 데모가 이 마스킹을 시연한다(ADR-163).
|
|
93
|
+
logger: {
|
|
94
|
+
level: 'debug',
|
|
95
|
+
sinks: [{ type: 'console', pretty: true }],
|
|
96
|
+
redact: ['*.password', '*.token', '*.secret', '*.authorization', 'password', 'token', 'secret'],
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// 운영 관측성(ADR-072/131) — Prometheus /metrics 옵트인, loopback allowList.
|
|
100
|
+
health: {
|
|
101
|
+
exposeMetrics: true,
|
|
102
|
+
metricsPath: '/metrics',
|
|
103
|
+
metricsAllowList: ['127.0.0.1', '::1'],
|
|
104
|
+
},
|
|
105
|
+
}
|