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,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,137 @@
|
|
|
1
|
+
/* 가이드 뷰어 전용 스타일 — 좌측 목차 사이드바 + 서버사이드 렌더된 마크다운 본문. */
|
|
2
|
+
|
|
3
|
+
/* --- 사이드바(목차) --- */
|
|
4
|
+
.guide-sidebar {
|
|
5
|
+
position: sticky;
|
|
6
|
+
top: 1.5rem;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.guide-toc .nav-link {
|
|
10
|
+
padding: 0.25rem 0.5rem;
|
|
11
|
+
border-radius: 0.375rem;
|
|
12
|
+
color: var(--bs-secondary-color);
|
|
13
|
+
font-size: 0.9rem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.guide-toc .nav-link:hover {
|
|
17
|
+
background: var(--bs-secondary-bg);
|
|
18
|
+
color: var(--bs-body-color);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.guide-toc .nav-link.active {
|
|
22
|
+
background: var(--bs-primary-bg-subtle);
|
|
23
|
+
color: var(--bs-primary-text-emphasis);
|
|
24
|
+
font-weight: 600;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* --- 마크다운 본문 --- */
|
|
28
|
+
.guide-content {
|
|
29
|
+
line-height: 1.75;
|
|
30
|
+
word-break: break-word;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* 앵커 점프 시 헤딩이 화면 맨 위에 딱 붙지 않게 여유를 둔다(목차 링크 가독성). */
|
|
34
|
+
.guide-content :is(h1, h2, h3, h4) {
|
|
35
|
+
scroll-margin-top: 1.5rem;
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
line-height: 1.3;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.guide-content h1 {
|
|
41
|
+
font-size: 1.9rem;
|
|
42
|
+
margin-bottom: 1rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.guide-content h2 {
|
|
46
|
+
font-size: 1.5rem;
|
|
47
|
+
margin-top: 2.25rem;
|
|
48
|
+
margin-bottom: 0.85rem;
|
|
49
|
+
padding-bottom: 0.3rem;
|
|
50
|
+
border-bottom: 1px solid var(--bs-border-color);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.guide-content h3 {
|
|
54
|
+
font-size: 1.2rem;
|
|
55
|
+
margin-top: 1.75rem;
|
|
56
|
+
margin-bottom: 0.6rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.guide-content h4 {
|
|
60
|
+
font-size: 1.05rem;
|
|
61
|
+
margin-top: 1.25rem;
|
|
62
|
+
margin-bottom: 0.5rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.guide-content > p:first-child > strong:only-child,
|
|
66
|
+
.guide-content blockquote {
|
|
67
|
+
color: var(--bs-secondary-color);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.guide-content blockquote {
|
|
71
|
+
margin: 1rem 0;
|
|
72
|
+
padding: 0.25rem 1rem;
|
|
73
|
+
border-left: 4px solid var(--bs-border-color);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.guide-content blockquote > :last-child {
|
|
77
|
+
margin-bottom: 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* 인라인 코드 — 배경 칩. 코드블록(pre code)은 highlight.js 테마가 색을 입히므로 칩 스타일을 빼준다. */
|
|
81
|
+
.guide-content :not(pre) > code {
|
|
82
|
+
padding: 0.15em 0.4em;
|
|
83
|
+
font-size: 0.875em;
|
|
84
|
+
background: var(--bs-secondary-bg);
|
|
85
|
+
border-radius: 0.3rem;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.guide-content pre {
|
|
89
|
+
margin: 1rem 0;
|
|
90
|
+
border: 1px solid var(--bs-border-color);
|
|
91
|
+
border-radius: 0.5rem;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.guide-content pre code.hljs {
|
|
96
|
+
border-radius: 0.5rem;
|
|
97
|
+
font-size: 0.85rem;
|
|
98
|
+
line-height: 1.55;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* 표 — Bootstrap 의 .table 클래스를 못 붙이므로 마크다운 표에 직접 스타일을 준다. */
|
|
102
|
+
.guide-content table {
|
|
103
|
+
width: 100%;
|
|
104
|
+
margin: 1rem 0;
|
|
105
|
+
border-collapse: collapse;
|
|
106
|
+
font-size: 0.9rem;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.guide-content th,
|
|
110
|
+
.guide-content td {
|
|
111
|
+
padding: 0.5rem 0.75rem;
|
|
112
|
+
border: 1px solid var(--bs-border-color);
|
|
113
|
+
text-align: left;
|
|
114
|
+
vertical-align: top;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.guide-content thead th {
|
|
118
|
+
background: var(--bs-secondary-bg);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.guide-content img {
|
|
122
|
+
max-width: 100%;
|
|
123
|
+
height: auto;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.guide-content ul,
|
|
127
|
+
.guide-content ol {
|
|
128
|
+
padding-left: 1.5rem;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.guide-content li + li {
|
|
132
|
+
margin-top: 0.25rem;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.guide-content hr {
|
|
136
|
+
margin: 2rem 0;
|
|
137
|
+
}
|
|
@@ -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,129 @@
|
|
|
1
|
+
// perf.js — /perf 벤치마크 UI 클라이언트. CSP(script-src 'self')라 외부 파일·인라인 핸들러 없음.
|
|
2
|
+
// POST /perf/run 은 JSON 요청 → CSRF 토큰 면제(Origin 검증, ADR-051)라 토큰 헤더 불필요. 응답 envelope
|
|
3
|
+
// { ok, data, meta } 에서 data 를 꺼내 결과 표에 한 행씩 누적한다(페이지 reload 시 초기화).
|
|
4
|
+
(function () {
|
|
5
|
+
'use strict'
|
|
6
|
+
|
|
7
|
+
/** @param {string} id @returns {HTMLElement} */
|
|
8
|
+
function $(id) {
|
|
9
|
+
var el = document.getElementById(id)
|
|
10
|
+
if (!el) throw new Error('missing element #' + id)
|
|
11
|
+
return el
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
var runBtn = $('pf-run')
|
|
15
|
+
var clearBtn = $('pf-clear')
|
|
16
|
+
var spinner = $('pf-spinner')
|
|
17
|
+
var errorBox = $('pf-error')
|
|
18
|
+
var errorDetail = $('pf-error-detail')
|
|
19
|
+
var resultsBody = $('pf-results')
|
|
20
|
+
|
|
21
|
+
/** 텍스트 셀 생성(textContent — XSS 방지). @param {string|number} text @param {boolean} mono @returns {HTMLTableCellElement} */
|
|
22
|
+
function cell(text, mono) {
|
|
23
|
+
var td = document.createElement('td')
|
|
24
|
+
td.textContent = String(text)
|
|
25
|
+
td.className = mono ? 'text-end font-monospace' : 'text-end'
|
|
26
|
+
return td
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 숫자 천단위 콤마. @param {number} v @returns {string} */
|
|
30
|
+
function n(v) {
|
|
31
|
+
return Number(v).toLocaleString('en-US')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 결과 1건을 표 맨 위에 추가. @param {object} d - 응답 data. */
|
|
35
|
+
function addRow(d) {
|
|
36
|
+
var empty = document.getElementById('pf-empty')
|
|
37
|
+
if (empty) empty.remove()
|
|
38
|
+
|
|
39
|
+
var tr = document.createElement('tr')
|
|
40
|
+
|
|
41
|
+
var nameTd = document.createElement('td')
|
|
42
|
+
nameTd.className = 'font-monospace'
|
|
43
|
+
nameTd.textContent = d.scenario
|
|
44
|
+
if (d.clamped) {
|
|
45
|
+
// 시나리오 상한으로 반복/동시성이 줄어든 경우 배지로 드러낸다(조용한 절삭 아님).
|
|
46
|
+
var badge = document.createElement('span')
|
|
47
|
+
badge.className = 'badge text-bg-warning ms-1'
|
|
48
|
+
badge.textContent = 'clamped'
|
|
49
|
+
var parts = []
|
|
50
|
+
if (d.clamped.iterations) parts.push('iter ' + d.clamped.iterations.requested + '→' + d.clamped.iterations.applied)
|
|
51
|
+
if (d.clamped.concurrency) parts.push('conc ' + d.clamped.concurrency.requested + '→' + d.clamped.concurrency.applied)
|
|
52
|
+
badge.title = parts.join(', ')
|
|
53
|
+
nameTd.appendChild(badge)
|
|
54
|
+
}
|
|
55
|
+
tr.appendChild(nameTd)
|
|
56
|
+
|
|
57
|
+
tr.appendChild(cell(n(d.iterations), false))
|
|
58
|
+
tr.appendChild(cell(n(d.concurrency), false))
|
|
59
|
+
tr.appendChild(cell(n(d.durationMs), true))
|
|
60
|
+
tr.appendChild(cell(n(d.rps), true))
|
|
61
|
+
tr.appendChild(cell(d.avg, true))
|
|
62
|
+
tr.appendChild(cell(d.p50, true))
|
|
63
|
+
tr.appendChild(cell(d.p90, true))
|
|
64
|
+
tr.appendChild(cell(d.p95, true))
|
|
65
|
+
tr.appendChild(cell(d.p99, true))
|
|
66
|
+
tr.appendChild(cell(d.min, true))
|
|
67
|
+
tr.appendChild(cell(d.max, true))
|
|
68
|
+
tr.appendChild(cell(d.ok + '/' + d.fail, true))
|
|
69
|
+
|
|
70
|
+
resultsBody.insertBefore(tr, resultsBody.firstChild)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 폼 입력을 요청 body 로. 빈 concurrency/payload 는 보내지 않음(서비스 디폴트 적용). @returns {object} */
|
|
74
|
+
function readForm() {
|
|
75
|
+
var body = {
|
|
76
|
+
scenario: $('pf-scenario').value,
|
|
77
|
+
iterations: Number($('pf-iterations').value),
|
|
78
|
+
}
|
|
79
|
+
var conc = $('pf-concurrency').value
|
|
80
|
+
var payload = $('pf-payload').value
|
|
81
|
+
if (conc !== '') body.concurrency = Number(conc)
|
|
82
|
+
if (payload !== '') body.payloadSize = Number(payload)
|
|
83
|
+
return body
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
runBtn.addEventListener('click', function () {
|
|
87
|
+
runBtn.disabled = true
|
|
88
|
+
spinner.classList.remove('d-none')
|
|
89
|
+
errorBox.classList.add('d-none')
|
|
90
|
+
|
|
91
|
+
fetch('/perf/run', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
94
|
+
body: JSON.stringify(readForm()),
|
|
95
|
+
})
|
|
96
|
+
.then(function (res) {
|
|
97
|
+
return res.json().then(function (body) {
|
|
98
|
+
if (!res.ok || !body.ok) {
|
|
99
|
+
var msg = body && body.error ? body.error.code + ': ' + body.error.message : 'run ' + res.status
|
|
100
|
+
throw new Error(msg)
|
|
101
|
+
}
|
|
102
|
+
return body.data
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
.then(function (data) {
|
|
106
|
+
addRow(data)
|
|
107
|
+
})
|
|
108
|
+
.catch(function (err) {
|
|
109
|
+
errorDetail.textContent = String(err && err.message ? err.message : err)
|
|
110
|
+
errorBox.classList.remove('d-none')
|
|
111
|
+
})
|
|
112
|
+
.finally(function () {
|
|
113
|
+
runBtn.disabled = false
|
|
114
|
+
spinner.classList.add('d-none')
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
clearBtn.addEventListener('click', function () {
|
|
119
|
+
resultsBody.innerHTML = ''
|
|
120
|
+
var tr = document.createElement('tr')
|
|
121
|
+
tr.id = 'pf-empty'
|
|
122
|
+
var td = document.createElement('td')
|
|
123
|
+
td.colSpan = 13
|
|
124
|
+
td.className = 'text-center text-body-secondary py-3'
|
|
125
|
+
td.textContent = resultsBody.getAttribute('data-empty') || ''
|
|
126
|
+
tr.appendChild(td)
|
|
127
|
+
resultsBody.appendChild(tr)
|
|
128
|
+
})
|
|
129
|
+
})()
|
|
@@ -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
|
+
})()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/*
|
|
3
|
+
* /demo/upload 클라이언트 — 파일 업로드 데모(ADR-133).
|
|
4
|
+
*
|
|
5
|
+
* multipart/form-data 는 ADR-051 에서 "폼"으로 분류돼 CSRF 토큰 검증을 받는다. 스트리밍 multipart 는
|
|
6
|
+
* preHandler 시점에 body 가 안 파싱돼 `_csrf` 바디 필드를 못 읽으므로, 토큰을 **헤더**(`csrf-token`)로 보낸다.
|
|
7
|
+
* HTML 폼은 커스텀 헤더를 못 보내므로 fetch + FormData 로 제출한다. 성공하면 페이지를 새로고침해
|
|
8
|
+
* 서버 렌더 "최근 업로드" 목록(redis)을 갱신한다.
|
|
9
|
+
*/
|
|
10
|
+
;(function () {
|
|
11
|
+
var $ = function (/** @type {string} */ id) {
|
|
12
|
+
return /** @type {HTMLElement} */ (document.getElementById(id))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
var form = /** @type {HTMLFormElement} */ ($('up-form'))
|
|
16
|
+
if (!form) return
|
|
17
|
+
var csrf = form.getAttribute('data-csrf') || ''
|
|
18
|
+
var fileInput = /** @type {HTMLInputElement} */ ($('up-file'))
|
|
19
|
+
var submitBtn = /** @type {HTMLButtonElement} */ ($('up-submit'))
|
|
20
|
+
var spinner = $('up-spinner')
|
|
21
|
+
var errorBox = $('up-error')
|
|
22
|
+
var errorDetail = $('up-error-detail')
|
|
23
|
+
|
|
24
|
+
form.addEventListener('submit', function (e) {
|
|
25
|
+
e.preventDefault()
|
|
26
|
+
if (!fileInput.files || fileInput.files.length === 0) return
|
|
27
|
+
|
|
28
|
+
submitBtn.disabled = true
|
|
29
|
+
spinner.classList.remove('d-none')
|
|
30
|
+
errorBox.classList.add('d-none')
|
|
31
|
+
|
|
32
|
+
var fd = new FormData()
|
|
33
|
+
for (var i = 0; i < fileInput.files.length; i++) fd.append('file', fileInput.files[i])
|
|
34
|
+
|
|
35
|
+
fetch('/demo/upload', {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: { 'csrf-token': csrf, accept: 'application/json' },
|
|
38
|
+
body: fd,
|
|
39
|
+
})
|
|
40
|
+
.then(function (res) {
|
|
41
|
+
return res.json().then(function (body) {
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
// 프레임워크 에러 엔벨로프 { ok:false, error:{ message } } — MIME 거부(415)/크기 초과(413) 등.
|
|
44
|
+
var msg = body && body.error && body.error.message ? body.error.message : 'upload ' + res.status
|
|
45
|
+
throw new Error(msg)
|
|
46
|
+
}
|
|
47
|
+
return body
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
.then(function () {
|
|
51
|
+
// 성공 — 서버 렌더 최근 목록을 갱신하러 새로고침.
|
|
52
|
+
window.location.assign('/demo/upload')
|
|
53
|
+
})
|
|
54
|
+
.catch(function (err) {
|
|
55
|
+
errorDetail.textContent = String(err && err.message ? err.message : err)
|
|
56
|
+
errorBox.classList.remove('d-none')
|
|
57
|
+
})
|
|
58
|
+
.finally(function () {
|
|
59
|
+
submitBtn.disabled = false
|
|
60
|
+
spinner.classList.add('d-none')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
})()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/*
|
|
3
|
+
* /demo/worker 클라이언트 — CPU 워커 풀 데모(ADR-124).
|
|
4
|
+
*
|
|
5
|
+
* 1) 하트비트: 250ms 마다 /demo/worker/ping(JSON)을 때려 왕복 지연을 잰다. 워커 계산이 도는 동안에도 이
|
|
6
|
+
* 지연이 작게 유지되면 메인 스레드(이벤트 루프)가 안 막힌 것이다 — 만약 계산을 메인 스레드에서 했다면
|
|
7
|
+
* 이 ping 들이 계산이 끝날 때까지 통째로 멈춘다.
|
|
8
|
+
* 2) 실행: Run 버튼이 /demo/worker/run 에 JSON POST(폼 아님 → CSRF 토큰 면제 + Origin 검증, ADR-051)로
|
|
9
|
+
* 반복 횟수를 보내 워커 풀에서 해시를 돌리고 결과(digest/rounds/ms)를 보여준다.
|
|
10
|
+
*/
|
|
11
|
+
;(function () {
|
|
12
|
+
var $ = function (/** @type {string} */ id) {
|
|
13
|
+
return /** @type {HTMLElement} */ (document.getElementById(id))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ── 하트비트 ──────────────────────────────────────────────────────────────
|
|
17
|
+
var pingCount = 0
|
|
18
|
+
var countEl = $('wd-ping-count')
|
|
19
|
+
var latencyEl = $('wd-ping-latency')
|
|
20
|
+
var timeEl = $('wd-ping-time')
|
|
21
|
+
|
|
22
|
+
// 1초 간격 — 앱 기본 rate limit(100/분, ADR-073)을 넘지 않게 한다(라우터가 per-route 면제를 노출하지
|
|
23
|
+
// 않으므로 빈도 자체를 한도 안에 둔다). 탭이 숨겨지면 멈춰 불필요한 요청을 줄인다.
|
|
24
|
+
var HEARTBEAT_MS = 1000
|
|
25
|
+
|
|
26
|
+
function heartbeat() {
|
|
27
|
+
if (document.hidden) {
|
|
28
|
+
setTimeout(heartbeat, HEARTBEAT_MS)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
var started = performance.now()
|
|
32
|
+
fetch('/demo/worker/ping', { headers: { accept: 'application/json' } })
|
|
33
|
+
.then(function (res) {
|
|
34
|
+
if (!res.ok) throw new Error('ping ' + res.status)
|
|
35
|
+
return res.json()
|
|
36
|
+
})
|
|
37
|
+
.then(function () {
|
|
38
|
+
pingCount += 1
|
|
39
|
+
countEl.textContent = String(pingCount)
|
|
40
|
+
latencyEl.textContent = String(Math.round(performance.now() - started))
|
|
41
|
+
timeEl.textContent = new Date().toLocaleTimeString('ko-KR', { hour12: false })
|
|
42
|
+
})
|
|
43
|
+
.catch(function () {
|
|
44
|
+
// 세션 만료(로그인 리다이렉트)·네트워크 끊김 등은 다음 주기에 자연 복구된다 — 표시만 비운다.
|
|
45
|
+
latencyEl.textContent = '—'
|
|
46
|
+
})
|
|
47
|
+
.finally(function () {
|
|
48
|
+
setTimeout(heartbeat, HEARTBEAT_MS)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
heartbeat()
|
|
52
|
+
|
|
53
|
+
// ── 실행 ────────────────────────────────────────────────────────────────
|
|
54
|
+
var runBtn = /** @type {HTMLButtonElement} */ ($('wd-run'))
|
|
55
|
+
var spinner = $('wd-spinner')
|
|
56
|
+
var roundsInput = /** @type {HTMLInputElement} */ ($('wd-rounds'))
|
|
57
|
+
var resultBox = $('wd-result')
|
|
58
|
+
var errorBox = $('wd-error')
|
|
59
|
+
var errorDetail = $('wd-error-detail')
|
|
60
|
+
|
|
61
|
+
runBtn.addEventListener('click', function () {
|
|
62
|
+
runBtn.disabled = true
|
|
63
|
+
spinner.classList.remove('d-none')
|
|
64
|
+
errorBox.classList.add('d-none')
|
|
65
|
+
|
|
66
|
+
fetch('/demo/worker/run', {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
69
|
+
body: JSON.stringify({ rounds: Number(roundsInput.value) }),
|
|
70
|
+
})
|
|
71
|
+
.then(function (res) {
|
|
72
|
+
if (!res.ok) throw new Error('run ' + res.status)
|
|
73
|
+
return res.json()
|
|
74
|
+
})
|
|
75
|
+
.then(function (body) {
|
|
76
|
+
// 프레임워크 응답 엔벨로프 { ok, data, meta } — 실제 결과는 body.data 에 있다.
|
|
77
|
+
var data = body && body.data ? body.data : body
|
|
78
|
+
$('wd-rounds-out').textContent = String(data.rounds)
|
|
79
|
+
$('wd-ms-out').textContent = data.ms + ' ms'
|
|
80
|
+
$('wd-digest-out').textContent = data.digest
|
|
81
|
+
resultBox.classList.remove('d-none')
|
|
82
|
+
})
|
|
83
|
+
.catch(function (err) {
|
|
84
|
+
errorDetail.textContent = String(err && err.message ? err.message : err)
|
|
85
|
+
errorBox.classList.remove('d-none')
|
|
86
|
+
})
|
|
87
|
+
.finally(function () {
|
|
88
|
+
runBtn.disabled = false
|
|
89
|
+
spinner.classList.add('d-none')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
})()
|