From 331d6c5fe93170111a24ee4e1e6b126057fbe628 Mon Sep 17 00:00:00 2001 From: zenn Date: Mon, 22 Jun 2026 19:52:45 +0900 Subject: [PATCH] =?UTF-8?q?=EB=82=98=EC=8A=A4=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=A4=80=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 4 ++-- docker-compose.prod.yml | 26 +++++++++++++++++++++----- docs/deploy.md | 25 ++++++++++++++----------- nginx/Dockerfile | 2 +- nginx/default.conf | 5 +++++ package.json | 2 +- script/api.js | 3 ++- 7 files changed, 46 insertions(+), 21 deletions(-) diff --git a/.env.example b/.env.example index 2c2687c..a500774 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,7 @@ JWT_SECRET=여기에-32자-이상-랜덤-시크릿-입력 JWT_EXPIRES=7d # --- 웹 (docker-compose.prod.yml) --- -WEB_PORT=8080 +WEB_PORT=44082 # --- 관리자 (최초 기동 시 자동 생성) --- ADMIN_EMAIL=admin@example.com @@ -14,7 +14,7 @@ ADMIN_PASSWORD=change-me-8chars ADMIN_USERNAME=admin # --- CORS (옆 프로젝트·로컬 개발 포트 포함, 쉼표 구분) --- -CORS_ORIGINS=https://uniare.sori.studio,http://localhost:8080,http://127.0.0.1:8080,http://localhost:5500,http://127.0.0.1:5500,http://localhost:5173 +CORS_ORIGINS=https://uniare.sori.studio,http://localhost:44082,http://127.0.0.1:44082,http://localhost:8080,http://127.0.0.1:8080,http://localhost:5500,http://127.0.0.1:5500,http://localhost:5173 # --- 로컬 api:dev 전용 (npm run api:dev) --- DATAS_PATH=./datas diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index bb352f0..b93e995 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,7 +1,7 @@ # UADECK v4 프로덕션 스택 (NAS Docker) # 사용: docker compose -f docker-compose.prod.yml up -d --build # -# 정적 파일(HTML, images, datas)은 호스트 볼륨 마운트 — 카드/이미지 추가 시 재빌드 불필요 +# 정적 파일(HTML, images, datas)은 필요한 경로만 read-only 마운트 — 카드/이미지 추가 시 재빌드 불필요 services: uadeck-api: @@ -35,14 +35,30 @@ services: start_period: 15s uadeck-web: - image: nginx:alpine + build: + context: . + dockerfile: nginx/Dockerfile + image: uadeck-web:latest container_name: uadeck-web restart: unless-stopped ports: - - "${WEB_PORT:-8080}:80" + - "${WEB_PORT:-44082}:80" volumes: - - ./:/usr/share/nginx/html:ro - - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + - ./index.html:/usr/share/nginx/html/index.html:ro + - ./cardList.html:/usr/share/nginx/html/cardList.html:ro + - ./result.html:/usr/share/nginx/html/result.html:ro + - ./login.html:/usr/share/nginx/html/login.html:ro + - ./signup.html:/usr/share/nginx/html/signup.html:ro + - ./deckHistory.html:/usr/share/nginx/html/deckHistory.html:ro + - ./preset.html:/usr/share/nginx/html/preset.html:ro + - ./profile.html:/usr/share/nginx/html/profile.html:ro + - ./admin.html:/usr/share/nginx/html/admin.html:ro + - ./favicon.ico:/usr/share/nginx/html/favicon.ico:ro + - ./script:/usr/share/nginx/html/script:ro + - ./style:/usr/share/nginx/html/style:ro + - ./i18n:/usr/share/nginx/html/i18n:ro + - ./datas:/usr/share/nginx/html/datas:ro + - ./images:/usr/share/nginx/html/images:ro depends_on: uadeck-api: condition: service_healthy diff --git a/docs/deploy.md b/docs/deploy.md index 6eb9596..8d0aec5 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -17,7 +17,7 @@ cp .env.example .env | `JWT_SECRET` | 32자 이상 랜덤 문자열 (필수) | | `ADMIN_EMAIL` | 최초 관리자 이메일 | | `ADMIN_PASSWORD` | 최초 관리자 비밀번호 (8자+) | -| `WEB_PORT` | 웹 노출 포트 (기본 `8080`) | +| `WEB_PORT` | 웹 노출 포트 (기본 `44082`, 기존 정적 사이트 포트 유지) | | `CORS_ORIGINS` | 옆 프로젝트 도메인 포함, 쉼표 구분 | 예시: @@ -26,13 +26,13 @@ cp .env.example .env JWT_SECRET=your-random-secret-at-least-32-chars ADMIN_EMAIL=admin@uniare.sori.studio ADMIN_PASSWORD=your-secure-password -WEB_PORT=8080 -CORS_ORIGINS=https://uniare.sori.studio,https://your-other-app.sori.studio,http://localhost:8080,http://localhost:5500,http://127.0.0.1:5500 +WEB_PORT=44082 +CORS_ORIGINS=https://uniare.sori.studio,https://your-other-app.sori.studio,http://localhost:44082,http://127.0.0.1:44082,http://localhost:5500,http://127.0.0.1:5500 ``` ## 3. 로컬 테스트 -> **한 줄 요약:** 프로젝트 루트에서 `npm start` → 브라우저 **http://localhost:8080** +> **한 줄 요약:** 프로젝트 루트에서 `npm start` → 브라우저 **http://localhost:44082** 회원가입·로그인·덱 저장은 **API 서버**가 떠 있어야 합니다. HTML만 Live Server 등으로 열면 저장·로그인이 **405** 로 실패합니다. @@ -41,7 +41,7 @@ HTML만 Live Server 등으로 열면 저장·로그인이 **405** 로 실패합 ```bash npm start # 백그라운드 실행 (터미널 점유 안 함) -# 브라우저: http://localhost:8080 +# 브라우저: http://localhost:44082 npm run logs # 로그 보기 (종료: Ctrl+C) npm stop # 서버 종료 @@ -57,10 +57,10 @@ npm stop # 서버 종료 ### 저장이 안 될 때 -1. `curl http://localhost:8080/api/v1/health` → `{"ok":true,...}` 가 나와야 함 +1. `curl http://localhost:44082/api/v1/health` → `{"ok":true,...}` 가 나와야 함 2. 안 나오면 `npm start` 다시 실행 3. `npm run dev` 후 `Healthy`에서 멈춘 것처럼 보여도 **정상**이지만, `Ctrl+C`하면 서버가 꺼짐 → **`npm start` 사용 권장** -4. Live Server(`:5500`)로 HTML만 연 경우 → 아래 방법 B 또는 **반드시 `:8080` 사용** +4. Live Server(`:5500`)로 HTML만 연 경우 → 아래 방법 B 또는 **반드시 `:44082` 사용** ### 방법 A — Docker (`npm start`와 동일) @@ -68,7 +68,7 @@ npm stop # 서버 종료 npm start ``` -브라우저: **http://localhost:8080** (Live Server 포트가 아님) +브라우저: **http://localhost:44082** (Live Server 포트가 아님) ### 방법 B — API만 로컬 실행 (Live Server / Cursor 미리보기 사용 시) @@ -115,7 +115,7 @@ docker compose up -d --build 확인: ```bash -curl http://localhost:8080/api/v1/health +curl http://localhost:44082/api/v1/health # → {"ok":true,"version":"4.0.0"} ``` @@ -126,11 +126,14 @@ curl http://localhost:8080/api/v1/health ### 옵션 A — `uadeck-web` 포트만 프록시 (권장) ``` -https://uniare.sori.studio → http://NAS_IP:8080 +https://uniare.sori.studio → http://NAS_IP:44082 ``` `uadeck-web` 컨테이너가 `/api/`와 정적 파일을 모두 처리합니다. +`docker-compose.prod.yml`은 프로젝트 루트 전체를 웹 루트로 열지 않고, 필요한 HTML·정적 폴더만 read-only로 마운트합니다. +따라서 `.env`, `.git`, `api/`, `docs/` 같은 내부 파일은 Nginx에서 직접 노출되지 않습니다. + ### 옵션 B — 기존 정적 호스팅 + API만 Docker 1. `docker compose up -d` (API만, 포트 3000) @@ -171,7 +174,7 @@ docker cp uadeck-api:/data/uadeck.db ./backup/uadeck-$(date +%Y%m%d).db ## 8. 카드/이미지 업데이트 -`datas/`, `images/`, HTML 파일은 **볼륨 마운트**이므로: +`datas/`, `images/`, HTML 파일은 **허용된 경로만 read-only 볼륨 마운트**이므로: 1. Git pull (또는 파일 동기화) 2. **재빌드 불필요** — 브라우저 새로고침만으로 반영 diff --git a/nginx/Dockerfile b/nginx/Dockerfile index 9710663..56890e5 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -3,7 +3,7 @@ FROM nginx:alpine COPY nginx/default.conf /etc/nginx/conf.d/default.conf # 정적 사이트 (docker-compose build context = 프로젝트 루트) -COPY index.html cardList.html result.html login.html signup.html deckHistory.html preset.html admin.html /usr/share/nginx/html/ +COPY index.html cardList.html result.html login.html signup.html deckHistory.html preset.html profile.html admin.html favicon.ico /usr/share/nginx/html/ COPY script /usr/share/nginx/html/script COPY style /usr/share/nginx/html/style COPY i18n /usr/share/nginx/html/i18n diff --git a/nginx/default.conf b/nginx/default.conf index 4daba2a..51f0c9a 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -7,6 +7,11 @@ server { client_max_body_size 25m; + # 공유 링크 → 덱 로드 페이지 + location ~ ^/d/([A-Za-z0-9]+)$ { + return 302 /cardList.html?d=$1; + } + # API → 백엔드 컨테이너 location /api/ { proxy_pass http://uadeck-api:3000; diff --git a/package.json b/package.json index df7d855..f2570da 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "tailwindcss": "^4.1.12" }, "scripts": { - "start": "docker compose -f docker-compose.prod.yml up -d --build && printf '\\n✅ UADECK 실행 중 → http://localhost:8080\\n 종료: npm stop\\n 로그: npm run logs\\n\\n'", + "start": "docker compose -f docker-compose.prod.yml up -d --build && node -e \"console.log('\\nUADECK running: http://localhost:' + (process.env.WEB_PORT || '44082') + '\\nStop: npm stop\\nLogs: npm run logs\\n')\"", "stop": "docker compose -f docker-compose.prod.yml down", "logs": "docker compose -f docker-compose.prod.yml logs -f", "dev": "docker compose -f docker-compose.prod.yml up --build", diff --git a/script/api.js b/script/api.js index 1d8c18f..1f58519 100644 --- a/script/api.js +++ b/script/api.js @@ -15,7 +15,8 @@ const UaDeckApi = (() => { const { hostname, port, protocol } = window.location; const isLocal = hostname === 'localhost' || hostname === '127.0.0.1'; // Live Server 등 정적 전용 포트 → API 컨테이너/프로세스(3000)로 직접 연결 - if (isLocal && port && port !== '8080' && port !== '80' && port !== '443' && port !== '') { + const proxiedWebPorts = new Set(['44082', '8080', '80', '443', '']); + if (isLocal && port && !proxiedWebPorts.has(port)) { return `${protocol}//${hostname}:3000/api/v1`; }