From c44e0999fee3d46185736e56d37a046f5cbdac36 Mon Sep 17 00:00:00 2001 From: hided62 Date: Wed, 19 Aug 2026 11:15:22 +0000 Subject: [PATCH] =?UTF-8?q?feat(runtime):=20=EA=B3=B5=ED=86=B5=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20=EC=84=A4=EC=A0=95=EC=9D=84=20=EC=98=81=EC=86=8D=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A1=9C=20=EB=B3=B4=EC=A1=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 최초 기동에만 Core 메뉴 기본값을 복사하고 이후 운영 편집은 배포와 컨테이너 재생성에서 유지한다. Compose 환경과 안전 검증 문서를 함께 갱신한다. --- README.md | 6 ++++++ compose.yaml | 1 + runtime/entrypoint.sh | 9 +++++++++ test/runtime-navigation-config.test.mjs | 13 +++++++++++++ 4 files changed, 29 insertions(+) create mode 100644 test/runtime-navigation-config.test.mjs diff --git a/README.md b/README.md index 81c28b2..a5d1c54 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,12 @@ mode 0600 secret 파일에만 둡니다. - PostgreSQL, Redis, Core clone/worktree, PM2 상태와 Caddy 인증서는 named volume에 보존됩니다. 새 user icon은 외부 이미지 서비스가 보존하며 runtime의 user icon volume은 기존 설치 호환용입니다. +- Gateway와 게임 공통 메뉴는 runtime volume의 `/srv/data/navigation.json`을 + 읽습니다. 최초 기동 때만 Core의 `resources/navigation.json`을 복사하며, 이후 + 배포와 container 재생성은 운영자가 편집한 파일을 덮어쓰지 않습니다. JSON을 + 유효하게 저장한 뒤 브라우저를 새로 열거나 새로고침하면 재빌드 없이 반영됩니다. + 잘못된 스키마나 `javascript:` URL은 API가 거부하므로 변경 전에 Core 문서의 + 메뉴 설정 검증 명령을 실행합니다. - `/image/*`는 앱 artifact가 아니며 `data/image/`의 별도 운영 자산을 제공합니다. - 일반 `docker compose down`은 volume을 보존합니다. `down --volumes`는 DB와 release 상태를 삭제하는 파괴적 명령이므로 backup 없이 실행하지 않습니다. diff --git a/compose.yaml b/compose.yaml index 11215ee..7cc7331 100644 --- a/compose.yaml +++ b/compose.yaml @@ -90,6 +90,7 @@ services: GATEWAY_API_PORT: '15001' GATEWAY_FRONTEND_PORT: '15000' GATEWAY_TRPC_PATH: /gateway/api/trpc + CORE_NAVIGATION_CONFIG_FILE: /srv/data/navigation.json GATEWAY_INTERNAL_API_URL: http://127.0.0.1:15001 GATEWAY_REDIS_PREFIX: sammo:gateway GATEWAY_WORKSPACE_ROOT: /srv/core/repository diff --git a/runtime/entrypoint.sh b/runtime/entrypoint.sh index 588e52f..019912c 100755 --- a/runtime/entrypoint.sh +++ b/runtime/entrypoint.sh @@ -71,6 +71,15 @@ esac git config --global --add safe.directory "$core_root" export GATEWAY_WORKSPACE_ROOT="$core_root" export RELEASE_CONTROLLER_WORKSPACE_ROOT="$core_root" + +navigation_config_file=${CORE_NAVIGATION_CONFIG_FILE:-/srv/data/navigation.json} +if [ ! -e "$navigation_config_file" ]; then + navigation_config_dir=$(dirname "$navigation_config_file") + mkdir -p "$navigation_config_dir" + cp "$core_root/resources/navigation.json" "$navigation_config_file" + chmod 644 "$navigation_config_file" +fi + export DATABASE_URL="$(node -e 'const u=new URL("postgresql://localhost");u.username=process.env.POSTGRES_USER||"sammo";u.password=process.env.POSTGRES_PASSWORD;u.hostname=process.env.POSTGRES_HOST||"postgres";u.port=process.env.POSTGRES_PORT||"5432";u.pathname="/"+(process.env.POSTGRES_DB||"sammo");u.searchParams.set("schema","public");process.stdout.write(u.href)')" export GATEWAY_DATABASE_URL="$DATABASE_URL" export REDIS_URL="$(node -e 'const u=new URL("redis://localhost/0");u.password=process.env.REDIS_PASSWORD;u.hostname=process.env.REDIS_HOST||"redis";u.port=process.env.REDIS_PORT||"6379";process.stdout.write(u.href)')" diff --git a/test/runtime-navigation-config.test.mjs b/test/runtime-navigation-config.test.mjs new file mode 100644 index 0000000..374138f --- /dev/null +++ b/test/runtime-navigation-config.test.mjs @@ -0,0 +1,13 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; + +test('runtime menu config is seeded once in persistent data and never overwritten', async () => { + const entrypoint = await fs.readFile(new URL('../runtime/entrypoint.sh', import.meta.url), 'utf8'); + const existenceGuard = entrypoint.indexOf('if [ ! -e "$navigation_config_file" ]; then'); + const copy = entrypoint.indexOf('cp "$core_root/resources/navigation.json" "$navigation_config_file"'); + + assert.ok(existenceGuard >= 0, 'persistent navigation config must have an existence guard'); + assert.ok(copy > existenceGuard, 'the default config must only be copied inside that guard'); + assert.equal(entrypoint.indexOf('cp -f "$core_root/resources/navigation.json"'), -1); +});