From 69ed4ad74463a4f1e4e241b66e4b6a26c3e5269a Mon Sep 17 00:00:00 2001 From: claude-bot Date: Wed, 13 May 2026 02:55:58 +0900 Subject: [PATCH] =?UTF-8?q?config:=20=EC=82=AC=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=C2=B7=EC=84=9C=EB=B2=84=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=9D=84=20.env=20=EB=A1=9C=20=EC=A4=91=EC=95=99?= =?UTF-8?q?=ED=99=94=20+=20=EC=84=A4=EC=B9=98=EA=B8=B0=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EC=A2=85=EB=A3=8C=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dotenv 도입, src/shared/env.ts 추가 - loadEnv() 가 프로젝트 루트 .env 를 로드 (override=false: 쉘 env 우선) - getSiteBaseUrl() / getManifestUrl() 헬퍼 - 서버/설치기/리소스팩설치기 진입점에서 loadEnv() 호출 - 설치기 두 종의 기본 MANIFEST_URL 을 SITE_BASE_URL 기반으로 변경 (운영 도메인을 한 곳에서만 바꾸면 됨) - .env.example 템플릿 + .gitignore 에 .env 추가 - README / docs/admin-site.md 에 환경변수 표·사용법 추가 - installer/renderer.js: 4단계 완료 후 자동 종료 다시 활성화 --- .env.example | 33 +++++++++++++++++++++++++++++++++ .gitignore | 3 +++ README.md | 3 +++ docs/admin-site.md | 16 +++++++++++++++- installer/renderer.js | 3 +-- package-lock.json | 22 +++++++++++++++++----- package.json | 1 + src/installer-rp/main.ts | 5 ++++- src/installer/main.ts | 5 ++++- src/server/app.ts | 3 +++ src/shared/env.ts | 33 +++++++++++++++++++++++++++++++++ 11 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 .env.example create mode 100644 src/shared/env.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3d92e1f --- /dev/null +++ b/.env.example @@ -0,0 +1,33 @@ +# ============================================================================= +# 음악퀴즈 통합 패키지 — 환경변수 템플릿 +# 이 파일을 복사해 `.env` 로 만든 뒤 값만 수정해 사용하세요. +# `.env` 는 .gitignore 로 제외되어 있습니다. +# ============================================================================= + +# ----- 관리 사이트(서버) ----- + +# 서버가 listen 할 포트 +PORT=3000 + +# 서버 바인드 주소. 127.0.0.1 이면 로컬 전용, 0.0.0.0 이면 외부 노출. +HOST=127.0.0.1 + +# Express 세션 시크릿. 운영 환경에서는 반드시 추측 어려운 무작위 값으로. +SESSION_SECRET=music-quiz-installer-dev-secret + +# ----- 사이트 도메인(설치기가 manifest 를 받아갈 주소) ----- + +# 설치기 두 종(installer / installer-rp) 이 첫 화면에서 자동으로 채워 넣는 +# manifest 의 호스트. 프로토콜 + 호스트(+포트) 까지만 적고 슬래시는 끝에 붙이지 않음. +# 예) 운영 도메인 : https://mq.example.com +# 로컬 개발 : http://127.0.0.1:3000 +SITE_BASE_URL=http://127.0.0.1:3000 + +# 위 SITE_BASE_URL 로부터 자동으로 `${SITE_BASE_URL}/manifest.json` 이 생성됩니다. +# 특별히 다른 경로를 쓰고 싶을 때만 아래를 풀어서 우선 적용시키세요. +# MANIFEST_URL=http://127.0.0.1:3000/manifest.json + +# ----- 리소스팩 설치기 ----- + +# yt-dlp 동시 다운로드 수(1~8). 비워두면 CPU 코어 수로 자동 결정. +# MUSIC_CONCURRENCY= diff --git a/.gitignore b/.gitignore index 7810a7e..c5cb917 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ release/ logs/ *.log conversations/ +.env +.env.local +.env.*.local diff --git a/README.md b/README.md index 8a60ad4..528998a 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,9 @@ # 의존성 설치 npm install +# 환경변수 템플릿 복사 (처음 한 번만) +cp .env.example .env + # 1) 관리 사이트 개발 실행 (http://localhost:3000) npm start diff --git a/docs/admin-site.md b/docs/admin-site.md index ac96c7a..6e4f10c 100644 --- a/docs/admin-site.md +++ b/docs/admin-site.md @@ -6,11 +6,25 @@ ```bash npm install -npm start # 기본 포트 3000. 환경변수로 PORT 조정 가능. +cp .env.example .env # 처음 한 번만. 운영 도메인이면 SITE_BASE_URL 만 바꾸면 됩니다. +npm start # 기본 포트 3000. ``` 배포 시에는 시스템 서비스(systemd 등) 로 등록해 두면 됩니다. +### 환경변수 (`.env`) + +| 키 | 기본값 | 설명 | +| --- | --- | --- | +| `PORT` | `3000` | Express 서버 listen 포트. | +| `HOST` | `127.0.0.1` | 바인드 주소. 외부 노출하려면 `0.0.0.0`. | +| `SESSION_SECRET` | dev secret | `/op` 세션 쿠키 서명 키. 운영에서는 반드시 임의값으로 교체. | +| `SITE_BASE_URL` | `http://127.0.0.1:3000` | 설치기 두 종이 첫 화면에서 자동으로 채우는 manifest 호스트. 운영 도메인으로 바꿔두면 manifest URL 도 자동으로 따라갑니다. | +| `MANIFEST_URL` | — | 특별히 다른 경로를 쓰고 싶을 때만 지정. 비우면 `${SITE_BASE_URL}/manifest.json`. | +| `MUSIC_CONCURRENCY` | (자동) | 리소스팩 설치기 yt-dlp 동시 다운로드 수(1~8). | + +`.env` 는 `.gitignore` 로 제외되어 있습니다. 새 환경을 셋업할 때 `.env.example` 을 복사해서 시작하세요. 쉘에서 직접 환경변수를 지정한 경우에는 `.env` 값을 덮어쓰지 않습니다. + ## 도메인 / 경로 구성 | 경로 | 내용 | diff --git a/installer/renderer.js b/installer/renderer.js index dc2f786..9daee53 100644 --- a/installer/renderer.js +++ b/installer/renderer.js @@ -695,8 +695,7 @@ function renderStep5() { // 마무리 액션 실패는 무시하고 종료 진행 } finishBtn.textContent = '완료됨' - // 자동 종료는 임시 비활성화 (런처 실행 오류 메시지 확인용). 필요 시 X 버튼으로 직접 닫는다. - // if (installerApi.quitApp) installerApi.quitApp() + if (installerApi.quitApp) installerApi.quitApp() }) } diff --git a/package-lock.json b/package-lock.json index cae8158..29dd7a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@types/archiver": "^7.0.0", "archiver": "^7.0.1", + "dotenv": "^17.4.2", "ejs": "^3.1.10", "express": "^4.19.2", "express-session": "^1.18.0", @@ -2630,12 +2631,14 @@ } }, "node_modules/dotenv": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", - "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", - "dev": true, + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/dotenv-expand": { @@ -4721,6 +4724,15 @@ "node": ">=12.0.0" } }, + "node_modules/read-config-file/node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", diff --git a/package.json b/package.json index 9523e03..d6ad284 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "@types/archiver": "^7.0.0", "archiver": "^7.0.1", + "dotenv": "^17.4.2", "ejs": "^3.1.10", "express": "^4.19.2", "express-session": "^1.18.0", diff --git a/src/installer-rp/main.ts b/src/installer-rp/main.ts index a3f51d7..1083d4f 100644 --- a/src/installer-rp/main.ts +++ b/src/installer-rp/main.ts @@ -10,6 +10,7 @@ import type { ChildProcess } from 'node:child_process' import type { Manifest, PackDefinition, PackList } from '../shared/types.js' import { normalizePackDefinition } from '../shared/store.js' import { getAppDataDir, getMcCustomDir } from '../shared/paths.js' +import { loadEnv, getManifestUrl } from '../shared/env.js' import type { RpFetchedPack } from './types.js' import { ensureYtDlpExe } from './ytdlp.js' import { ensureFfmpegExe } from './ffmpeg.js' @@ -17,6 +18,8 @@ import { downloadMusicTrack } from './music.js' import { downloadImage, normalizeToCover, coverFileName } from './images.js' import { buildResourcepackZip } from './pack.js' +loadEnv() + interface RpInstallerState { manifestUrl: string baseUrl: string @@ -68,7 +71,7 @@ function acquireMusicStartSlot(): Promise { return slot } -const DEFAULT_MANIFEST_URL = process.env.MANIFEST_URL ?? 'http://127.0.0.1:3000/manifest.json' +const DEFAULT_MANIFEST_URL = getManifestUrl() const state: RpInstallerState = { manifestUrl: DEFAULT_MANIFEST_URL, diff --git a/src/installer/main.ts b/src/installer/main.ts index 854ab3e..93011fd 100644 --- a/src/installer/main.ts +++ b/src/installer/main.ts @@ -20,6 +20,9 @@ import type { } from './types.js' import type { Manifest, PackDefinition } from '../shared/types.js' import { normalizePackDefinition } from '../shared/store.js' +import { loadEnv, getManifestUrl } from '../shared/env.js' + +loadEnv() interface InstallerState { manifestUrl: string @@ -31,7 +34,7 @@ interface InstallerState { configEditorPort: number | null } -const DEFAULT_MANIFEST_URL = process.env.MANIFEST_URL ?? 'http://127.0.0.1:3000/manifest.json' +const DEFAULT_MANIFEST_URL = getManifestUrl() const state: InstallerState = { manifestUrl: DEFAULT_MANIFEST_URL, diff --git a/src/server/app.ts b/src/server/app.ts index 032a188..9389211 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -3,9 +3,12 @@ import session from 'express-session' import path from 'node:path' import fsp from 'node:fs/promises' import { manifestRootPath, manifestDirPath, fileDirPath, viewsDirPath, publicDirPath } from '../shared/paths.js' +import { loadEnv } from '../shared/env.js' import { indexRouter } from './routes/index.js' import { opRouter } from './routes/op.js' +loadEnv() + const PORT = Number(process.env.PORT ?? 3000) // 터미널에서 Ctrl+클릭으로 바로 열 수 있도록 기본값은 127.0.0.1. // 외부 노출이 필요할 때만 HOST=0.0.0.0 환경변수로 덮어씀. diff --git a/src/shared/env.ts b/src/shared/env.ts new file mode 100644 index 0000000..82f39c0 --- /dev/null +++ b/src/shared/env.ts @@ -0,0 +1,33 @@ +import path from 'node:path' +import fs from 'node:fs' +import dotenv from 'dotenv' +import { projectRoot } from './paths.js' + +/** + * 프로젝트 루트의 `.env` 를 읽어 `process.env` 에 주입. + * + * - 이미 설정된 환경변수는 덮어쓰지 않음(쉘에서 넘긴 값이 우선). + * - 파일이 없으면 조용히 통과 — 운영 환경에서는 시스템 env 만으로도 동작해야 함. + * - 서버/설치기/리소스팩설치기 진입점에서 한 번씩 호출. + */ +export function loadEnv(): void { + const envPath = path.join(projectRoot, '.env') + if (!fs.existsSync(envPath)) return + dotenv.config({ path: envPath, override: false, quiet: true }) +} + +/** + * 사이트 베이스 URL. 관리 사이트가 호스팅되는 외부 주소(설치기가 manifest 를 + * 받아가는 도메인). 기본값은 로컬 개발용 `http://127.0.0.1:3000`. + */ +export function getSiteBaseUrl(): string { + const raw = (process.env.SITE_BASE_URL ?? 'http://127.0.0.1:3000').trim() + return raw.replace(/\/+$/, '') +} + +/** 사이트 베이스 URL + `/manifest.json`. `MANIFEST_URL` 가 따로 지정되면 그 값을 우선. */ +export function getManifestUrl(): string { + const explicit = process.env.MANIFEST_URL?.trim() + if (explicit) return explicit + return `${getSiteBaseUrl()}/manifest.json` +}