From f729b6fa82c76e1e0e570c40af30352eef1b2a32 Mon Sep 17 00:00:00 2001 From: zenn Date: Thu, 26 Mar 2026 17:53:32 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=B4=EB=A6=AC=EC=8A=A4:=20v0.1.37=20?= =?UTF-8?q?=EC=9A=B4=EC=98=81=20=EB=B0=B0=ED=8F=AC=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=EA=B3=BC=20=EB=A1=9C=EC=BB=AC=20=EB=B3=80=EA=B2=BD=20=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/index.js | 1 + backend/src/routes/auth.js | 23 +++++++++++------- docker-compose.prod.yml | 9 +++---- docs/history.md | 48 ++++++++++++++++++++++++------------- docs/spec.md | 2 +- docs/ugreen-nas-deploy.md | 8 +++---- docs/update.md | 27 ++++++++++++--------- frontend/nginx.conf | 6 ++--- frontend/src/App.vue | 2 +- frontend/src/lib/runtime.js | 9 +++++-- frontend/src/style.css | 2 +- 11 files changed, 86 insertions(+), 51 deletions(-) diff --git a/backend/index.js b/backend/index.js index 65823c0..813bae7 100644 --- a/backend/index.js +++ b/backend/index.js @@ -53,6 +53,7 @@ app.use( retries: 0, }), secret: SESSION_SECRET, + proxy: TRUST_PROXY > 0, resave: false, saveUninitialized: false, cookie: { diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index 6f2660c..ca769e6 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -44,7 +44,11 @@ router.post('/signup', async (req, res) => { req.session.userId = user.id req.session.isAdmin = !!user.isAdmin - res.json(user) + // 세션을 응답 전에 명시적으로 저장해 Set-Cookie가 확실히 내려오도록 보강 + req.session.save((err) => { + if (err) return res.status(500).json({ error: 'session_save_failed' }) + res.json(user) + }) }) router.post('/login', async (req, res) => { @@ -60,13 +64,16 @@ router.post('/login', async (req, res) => { req.session.userId = user.id req.session.isAdmin = !!user.isAdmin - res.json({ - id: user.id, - email: user.email, - nickname: user.nickname || '', - isAdmin: !!user.isAdmin, - avatarSrc: user.avatarSrc || '', - createdAt: user.createdAt, + req.session.save((err) => { + if (err) return res.status(500).json({ error: 'session_save_failed' }) + res.json({ + id: user.id, + email: user.email, + nickname: user.nickname || '', + isAdmin: !!user.isAdmin, + avatarSrc: user.avatarSrc || '', + createdAt: user.createdAt, + }) }) }) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 32c8b0a..6a52834 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -14,10 +14,11 @@ services: volumes: - tmaker_mariadb_data:/var/lib/mysql healthcheck: - test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -u$$MARIADB_USER -p$$MARIADB_PASSWORD --silent"] + test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -uroot -p$$MARIADB_ROOT_PASSWORD --silent"] + start_period: 150s interval: 10s timeout: 5s - retries: 10 + retries: 20 backend: build: @@ -55,7 +56,7 @@ services: depends_on: - backend ports: - - "8080:80" + - "18080:80" phpmyadmin: image: phpmyadmin:5.2-apache @@ -71,7 +72,7 @@ services: PMA_USER: ${MARIADB_USER} PMA_PASSWORD: ${MARIADB_PASSWORD} ports: - - "8081:80" + - "18081:80" volumes: tmaker_mariadb_data: diff --git a/docs/history.md b/docs/history.md index b6c66f5..b01f235 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,21 +1,5 @@ # 의사결정 이력 -## 2026-03-26 v0.1.36 -- 브라우저 탭 제목은 개발용 기본 문자열보다 서비스 이름을 직접 보여주는 편이 맞다고 판단해 `Tier Maker`로 고정했다. -- 무제목 티어표 기본값은 날짜형 임시 제목보다 사용자가 어떤 게임으로 작성했는지 즉시 알 수 있는 게임명 기준이 더 자연스럽다고 판단했다. - -## 2026-03-26 v0.1.35 -- NAS 운영 경로는 수동 파일 복사보다 Git clone 기반이 로컬 개발 흐름과 더 잘 맞고, 실수로 누락되는 파일을 줄일 수 있으므로 기본 배포 방식으로 권장하기로 결정했다. -- 운영 환경 변수와 Docker 볼륨은 Git 저장소 바깥의 NAS 자산으로 유지하고, 코드는 `git pull`로만 반영하는 역할 분리를 명확히 하기로 했다. - -## 2026-03-26 v0.1.34 -- 일부 NAS 환경에서 `favicon.svg` 정적 응답이 `403`으로 차단될 수 있으므로, 운영 안정성을 위해 별도 파일 요청이 필요 없는 인라인 데이터 URL 파비콘으로 전환하기로 결정했다. -- 관리자 기본 아이템 등록은 단일 파일 업로드만으로는 운영 부담이 크므로, 다중 선택과 드래그 앤 드롭을 지원하고 라벨은 파일명으로 자동 생성하는 방향을 채택했다. - -## 2026-03-26 v0.1.29 -- NAS에서 HTTPS를 종료한 뒤 내부 컨테이너끼리는 HTTP로 통신하는 구조에서는, 프런트 프록시가 백엔드에 원래 프로토콜을 정확히 전달하지 않으면 `secure` 세션 쿠키가 발급되지 않는다고 판단했다. -- 따라서 운영 프런트 Nginx는 백엔드 프록시 요청에 `X-Forwarded-Proto: https`를 명시하고, Express 세션도 프록시 환경을 명시적으로 신뢰하도록 설정하기로 결정했다. - ## 2026-03-19 - 초기 저장소는 빠른 구현을 위해 `lowdb(JSON 파일)`를 채택했다. - 인증은 JWT 대신 서버 세션(`express-session` + `session-file-store`)을 사용했다. @@ -121,3 +105,35 @@ ## 2026-03-26 v0.1.28 - UGREEN NAS에서 MariaDB 초기화가 길게 걸릴 수 있으므로, healthcheck는 앱 계정보다 `root` 기준 ping과 더 긴 유예 시간으로 두는 편이 안전하다고 판단했다. + +## 2026-03-26 v0.1.29 +- NAS에서 HTTPS를 종료한 뒤 내부 컨테이너끼리는 HTTP로 통신하는 구조에서는, 프런트 프록시가 백엔드에 원래 프로토콜을 정확히 전달하지 않으면 `secure` 세션 쿠키가 발급되지 않는다고 판단했다. +- 따라서 운영 프런트 Nginx는 백엔드 프록시 요청에 `X-Forwarded-Proto: https`를 명시하고, Express 세션도 프록시 환경을 명시적으로 신뢰하도록 설정하기로 결정했다. + +## 2026-03-26 v0.1.30 +- 운영 프런트는 다른 origin 절대 URL보다 같은 origin 상대 `/api` 요청을 우선해야 세션 쿠키 저장이 안정적이라고 판단했다. + +## 2026-03-26 v0.1.31 +- 세션 기반 로그인 응답은 저장소 반영 타이밍 차이를 줄이기 위해 `req.session.save()`를 명시 호출하는 쪽이 운영 안정성에 유리하다고 판단했다. + +## 2026-03-26 v0.1.32 +- 인증 문제를 빠르게 확인하기 위해 일시적으로 세션 저장 성공/실패 로그를 남기고 원인을 좁혀가는 접근을 선택했다. + +## 2026-03-26 v0.1.33 +- 프록시 환경의 실제 판단 값을 보기 위해 `req.secure`, `req.protocol`, `x-forwarded-proto`를 직접 로그로 비교해 원인을 확인하기로 했다. + +## 2026-03-26 v0.1.34 +- 일부 NAS 환경에서 `favicon.svg` 정적 응답이 `403`으로 차단될 수 있으므로, 운영 안정성을 위해 별도 파일 요청이 필요 없는 인라인 데이터 URL 파비콘으로 전환하기로 결정했다. +- 관리자 기본 아이템 등록은 단일 파일 업로드만으로는 운영 부담이 크므로, 다중 선택과 드래그 앤 드롭을 지원하고 라벨은 파일명으로 자동 생성하는 방향을 채택했다. + +## 2026-03-26 v0.1.35 +- NAS 운영 경로는 수동 파일 복사보다 Git clone 기반이 로컬 개발 흐름과 더 잘 맞고, 실수로 누락되는 파일을 줄일 수 있으므로 기본 배포 방식으로 권장하기로 결정했다. +- 운영 환경 변수와 Docker 볼륨은 Git 저장소 바깥의 NAS 자산으로 유지하고, 코드는 `git pull`로만 반영하는 역할 분리를 명확히 하기로 했다. + +## 2026-03-26 v0.1.36 +- 브라우저 탭 제목은 개발용 기본 문자열보다 서비스 이름을 직접 보여주는 편이 맞다고 판단해 `Tier Maker`로 고정했다. +- 무제목 티어표 기본값은 날짜형 임시 제목보다 사용자가 어떤 게임으로 작성했는지 즉시 알 수 있는 게임명 기준이 더 자연스럽다고 판단했다. + +## 2026-03-26 v0.1.37 +- 운영 포트 충돌을 피하기 위해 프로덕션 외부 포트는 `frontend=18080`, `phpMyAdmin=18081`로 고정하고, 리버스 프록시 문서도 그 기준으로 맞추기로 했다. +- 인증 장애 원인을 찾기 위한 디버그 로그는 문제 해결 후 제거하고, 실제 운영에는 세션 저장 보강과 프록시 헤더 설정만 유지하는 편이 낫다고 판단했다. diff --git a/docs/spec.md b/docs/spec.md index 5425682..a08fa49 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -144,7 +144,7 @@ ## 운영 배포 메모 - 프로덕션 컴포즈 파일은 [docker-compose.prod.yml](/Users/bicute/Desktop/zenn.dev/tier-cursor/docker-compose.prod.yml)이다. -- 외부 도메인 `tmaker.sori.studio`는 `frontend` 컨테이너의 `8080` 포트로 리버스 프록시하고, `/api`, `/uploads`, `/health`는 프런트 Nginx가 내부 `backend:5179`로 전달한다. +- 외부 도메인 `tmaker.sori.studio`는 `frontend` 컨테이너의 `18080` 포트로 리버스 프록시하고, `/api`, `/uploads`, `/health`는 프런트 Nginx가 내부 `backend:5179`로 전달한다. - 운영 볼륨은 MariaDB 데이터, 업로드 파일, 세션 파일을 각각 분리해 유지한다. - MariaDB healthcheck는 NAS 첫 기동 지연을 고려해 `root` 기준 ping과 긴 `start_period/retries`를 사용한다. diff --git a/docs/ugreen-nas-deploy.md b/docs/ugreen-nas-deploy.md index 7646e8c..0971490 100644 --- a/docs/ugreen-nas-deploy.md +++ b/docs/ugreen-nas-deploy.md @@ -99,7 +99,7 @@ docker compose --env-file .env.production -f docker-compose.prod.yml up -d --bui ```bash cd /volume1/docker/projects/apps/tier-maker git pull origin main -docker compose --env-file .env.production -f docker-compose.prod.yml up -d --build +sudo docker compose --env-file .env.production -f docker-compose.prod.yml up -d --build ``` - 아직 수동 복사 폴더만 있다면: @@ -118,14 +118,14 @@ docker compose --env-file .env.production -f docker-compose.prod.yml up -d --bui - 외부 도메인: `tmaker.sori.studio` - 내부 대상 프로토콜: `http` - 내부 대상 호스트: NAS IP 또는 `localhost` -- 내부 대상 포트: `8080` +- 내부 대상 포트: `18080` -즉 NAS 리버스 프록시는 `frontend` 컨테이너의 `8080`만 바라보면 된다. +즉 NAS 리버스 프록시는 `frontend` 컨테이너의 `18080`만 바라보면 된다. ## 5. HTTPS / 쿠키 - 현재 프로덕션 컴포즈는 `SESSION_COOKIE_SECURE=true`를 사용한다. - 따라서 `tmaker.sori.studio`에는 HTTPS 인증서가 연결되어 있어야 한다. -- NAS 리버스 프록시가 HTTPS 종료를 하고 내부는 `http://frontend:80` 또는 `localhost:8080`으로 전달하면 된다. +- NAS 리버스 프록시가 HTTPS 종료를 하고 내부는 `http://frontend:80` 또는 `localhost:18080`으로 전달하면 된다. - 최신 프런트 Nginx 설정은 백엔드로 `X-Forwarded-Proto: https`를 넘기므로, 로그인 직후 세션이 바로 풀리는 경우에는 프런트 이미지를 다시 빌드해 최신 설정이 반영됐는지 먼저 확인한다. ## 6. 데이터 위치 diff --git a/docs/update.md b/docs/update.md index acd0623..a6d2fab 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,10 @@ # 업데이트 로그 +## 2026-03-26 v0.1.37 +- **운영 포트 설정 반영**: 프로덕션 컴포즈의 `frontend/phpMyAdmin` 외부 포트를 `18080/18081` 기준으로 유지하고, NAS 배포 문서와 기술 명세의 리버스 프록시 포트 안내도 동일하게 정리 +- **인증 라우트 정리**: NAS 로그인 문제를 확인하기 위해 넣었던 `auth` 디버그 로그를 제거하고, 실제 운영에 필요한 세션 저장 보강만 유지 +- **이력 문서 정렬**: `docs/history.md`를 날짜/버전 흐름에 맞게 다시 정리해 추적성을 높임 + ## 2026-03-26 v0.1.36 - **브라우저 탭 이름 변경**: 프런트 문서 제목을 `frontend`에서 `Tier Maker`로 변경 - **무제목 티어표 기본값 조정**: 사용자가 제목을 입력하지 않으면 `이름 없음 + 날짜` 대신 현재 게임명을 기본 제목으로 사용하도록 변경하고, 관리자 임의 삭제 안내 문구는 유지 @@ -12,21 +17,21 @@ - **파비콘 정적 요청 제거**: 운영 환경에서 `/favicon.svg`가 `403`으로 막히는 경우를 피하기 위해, 별도 파일 대신 `index.html` 인라인 데이터 URL 파비콘으로 전환 - **관리자 기본 아이템 다중 업로드 추가**: 게임 관리 화면에서 기본 아이템을 여러 장 드래그 앤 드롭 또는 다중 파일 선택으로 한 번에 추가할 수 있도록 변경하고, 기본 라벨은 파일명 기준으로 자동 생성 -## 2026-03-26 v0.1.29 -- **NAS 로그인 유지 수정**: 프런트 Nginx가 백엔드에 전달하는 `X-Forwarded-Proto`를 `https`로 고정하고 Express 세션의 프록시 인지를 명시해, NAS HTTPS 리버스 프록시 뒤에서도 `secure` 세션 쿠키가 정상 발급되도록 조정 -- **운영 템플릿 복구**: 실수로 빠질 수 있는 `.env.production.example`를 다시 포함하고, NAS 재배포 시 최신 프런트 이미지를 다시 빌드하도록 문서 보강 - -## 2026-03-26 v0.1.30 -- **[NAS] /api 상대경로 호출**: 운영(`import.meta.env.PROD`)에서는 `http://localhost:...` 같은 다른 origin으로 API를 호출하지 않도록, `frontend/src/lib/runtime.js`에서 `/api` 호출을 상대경로로 고정해 세션 쿠키가 정상 저장되도록 수정 - -## 2026-03-26 v0.1.31 -- **[NAS] 세션 쿠키 발급 강제**: 백엔드 인증 라우트에서 `req.session.save()`를 명시 호출해 응답 전에 세션을 저장하고 `Set-Cookie`가 확실히 내려오도록 보강 +## 2026-03-26 v0.1.33 +- **[NAS] 요청 프로토콜 디버그**: `auth/login`/`auth/me`에서 `req.secure`, `req.protocol`, `x-forwarded-proto` 값을 로그로 출력해 프록시/HTTPS 판단 문제를 확인 ## 2026-03-26 v0.1.32 - **[NAS] 인증 디버그 로그 추가**: `auth/login`에서 `req.session.save` 성공/실패와 `auth/me`에서 세션 존재 여부를 콘솔 로그로 남겨 세션 쿠키 발급 문제를 빠르게 진단 -## 2026-03-26 v0.1.33 -- **[NAS] 요청 프로토콜 디버그**: `auth/login`/`auth/me`에서 `req.secure`, `req.protocol`, `x-forwarded-proto` 값을 로그로 출력해 프록시/HTTPS 판단 문제를 확인 +## 2026-03-26 v0.1.31 +- **[NAS] 세션 쿠키 발급 강제**: 백엔드 인증 라우트에서 `req.session.save()`를 명시 호출해 응답 전에 세션을 저장하고 `Set-Cookie`가 확실히 내려오도록 보강 + +## 2026-03-26 v0.1.30 +- **[NAS] /api 상대경로 호출**: 운영(`import.meta.env.PROD`)에서는 `http://localhost:...` 같은 다른 origin으로 API를 호출하지 않도록, `frontend/src/lib/runtime.js`에서 `/api` 호출을 상대경로로 고정해 세션 쿠키가 정상 저장되도록 수정 + +## 2026-03-26 v0.1.29 +- **NAS 로그인 유지 수정**: 프런트 Nginx가 백엔드에 전달하는 `X-Forwarded-Proto`를 `https`로 고정하고 Express 세션의 프록시 인지를 명시해, NAS HTTPS 리버스 프록시 뒤에서도 `secure` 세션 쿠키가 정상 발급되도록 조정 +- **운영 템플릿 복구**: 실수로 빠질 수 있는 `.env.production.example`를 다시 포함하고, NAS 재배포 시 최신 프런트 이미지를 다시 빌드하도록 문서 보강 ## 2026-03-26 v0.1.28 - **MariaDB healthcheck 완화**: UGREEN NAS 첫 초기화 시간이 길어도 `unhealthy`로 오판하지 않도록 프로덕션 컴포즈의 DB healthcheck를 `root` 기준과 더 긴 `start_period/retries`로 조정 diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 1c10ce8..9ef451e 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -15,7 +15,7 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto https; } location /uploads/ { @@ -24,7 +24,7 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto https; } location /health { @@ -33,6 +33,6 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto https; } } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 40b42ac..62ab56f 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -58,7 +58,7 @@ async function logout() {
Tier Maker - Vue + by zenn