Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f729b6fa82 | |||
| c413371935 | |||
| e0c2995518 |
@@ -53,6 +53,7 @@ app.use(
|
||||
retries: 0,
|
||||
}),
|
||||
secret: SESSION_SECRET,
|
||||
proxy: TRUST_PROXY > 0,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
# 의사결정 이력
|
||||
|
||||
## 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`)을 사용했다.
|
||||
@@ -113,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`로 고정하고, 리버스 프록시 문서도 그 기준으로 맞추기로 했다.
|
||||
- 인증 장애 원인을 찾기 위한 디버그 로그는 문제 해결 후 제거하고, 실제 운영에는 세션 저장 보강과 프록시 헤더 설정만 유지하는 편이 낫다고 판단했다.
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
- 커스텀 이미지 추가는 다중 파일 선택과 드래그 앤 드롭을 모두 지원한다.
|
||||
- 티어 행은 기본 5단으로 시작하지만, 사용자가 직접 추가하거나 제거할 수 있다.
|
||||
- 신규 티어표의 공개 여부 기본값은 `ON`이며, 기존 티어표는 편집 화면과 `내 티어표` 목록에서 직접 삭제할 수 있다.
|
||||
- 제목이 비어 있는 상태로 저장하면 내부 제목은 `이름 없음 + 날짜` 형식으로 자동 생성한다.
|
||||
- 제목이 비어 있는 상태로 저장하면 내부 제목은 현재 게임명을 기본값으로 사용한다.
|
||||
- 제목 입력이 비어 있는 동안에는 무분별한 도배 방지를 위한 관리자 임의 삭제 가능성 안내 문구를 표시한다.
|
||||
- 티어표 편집기의 아이콘 기본 크기는 `80px`이며, 사용자가 `48 / 60 / 80 / 100 / 120` 단계로 즉시 조절할 수 있다.
|
||||
- 공개 티어표 목록과 `내 티어표` 목록은 제목 옆에 작성자 아바타와 표시명을 함께 보여준다.
|
||||
@@ -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`를 사용한다.
|
||||
|
||||
|
||||
@@ -17,6 +17,37 @@
|
||||
- NAS 작업 폴더에 현재 프로젝트를 그대로 업로드한다.
|
||||
- 기존 로컬 MariaDB 내용은 무시하고 새 운영 DB로 시작해도 된다.
|
||||
|
||||
## 1-1. 권장 방식: Git clone 기반 운영
|
||||
- 수동 복사는 처음 1회에는 가능하지만, 이후부터는 로컬과 NAS가 쉽게 어긋난다.
|
||||
- 권장 방식은 NAS 작업 폴더를 Git 저장소로 두고, 로컬에서 작업 후 `push`, NAS에서는 `pull`만 하는 구조다.
|
||||
- 추천 흐름:
|
||||
- 로컬: 개발 → 테스트 → 커밋 → `git push`
|
||||
- NAS: `git pull` → `docker compose up -d --build`
|
||||
|
||||
처음부터 Git으로 시작하려면 NAS에서:
|
||||
|
||||
```bash
|
||||
cd /volume1/docker/projects/apps
|
||||
git clone https://git.sori.studio/zenn/tier-maker.git
|
||||
cd tier-maker
|
||||
cp .env.production.example .env.production
|
||||
```
|
||||
|
||||
- 이미 같은 경로에 수동 복사본이 있다면, 가장 깔끔한 방법은 기존 폴더를 다른 이름으로 잠시 옮기고 새로 `clone`하는 것이다.
|
||||
- `.env.production`은 Git에 올리지 않고 NAS에만 유지한다.
|
||||
|
||||
예시:
|
||||
|
||||
```bash
|
||||
cd /volume1/docker/projects/apps
|
||||
mv tier-maker tier-maker-manual-backup
|
||||
git clone https://git.sori.studio/zenn/tier-maker.git
|
||||
cd tier-maker
|
||||
cp .env.production.example .env.production
|
||||
```
|
||||
|
||||
- 이후 기존 수동 복사본의 `.env.production` 값을 새 clone 폴더의 `.env.production`에 그대로 옮기면 된다.
|
||||
|
||||
## 2. 환경변수 파일 준비
|
||||
- 루트에서 `.env.production.example`를 복사해 `.env.production`으로 만든다.
|
||||
- 최소 변경값:
|
||||
@@ -46,18 +77,56 @@ docker compose --env-file .env.production -f docker-compose.prod.yml up -d --bui
|
||||
docker compose --env-file .env.production -f docker-compose.prod.yml --profile admin up -d --build
|
||||
```
|
||||
|
||||
- MariaDB 첫 초기화는 NAS 환경에서 2분 이상 걸릴 수 있다.
|
||||
- 현재 프로덕션 컴포즈는 이를 고려해 `start_period: 150s`, `retries: 20` healthcheck를 사용한다.
|
||||
- 로그상 DB가 정상 기동했는데도 `unhealthy`가 뜬 적이 있다면 최신 `docker-compose.prod.yml`로 다시 올리는 것이 좋다.
|
||||
|
||||
## 3-1. 이후 업데이트 방식
|
||||
- Git clone 기반으로 전환한 뒤에는 파일을 하나씩 NAS에 덮어쓰지 않는다.
|
||||
- 로컬에서 커밋과 푸시가 끝난 뒤, NAS에서는 아래 두 줄이 기본 배포 절차다.
|
||||
|
||||
```bash
|
||||
git pull origin main
|
||||
docker compose --env-file .env.production -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
- Dockerfile, Nginx 설정, 프런트 소스, 백엔드 소스가 바뀐 경우에는 `--build`를 유지한다.
|
||||
- 단순 재시작만 필요할 때도 있지만, 운영에서는 실수 방지를 위해 `up -d --build`를 기본값으로 두는 편이 안전하다.
|
||||
|
||||
## 3-2. 이번 v0.1.34까지 적용하는 예시
|
||||
- 이미 NAS 폴더가 Git clone 상태라면:
|
||||
|
||||
```bash
|
||||
cd /volume1/docker/projects/apps/tier-maker
|
||||
git pull origin main
|
||||
sudo docker compose --env-file .env.production -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
- 아직 수동 복사 폴더만 있다면:
|
||||
|
||||
```bash
|
||||
cd /volume1/docker/projects/apps
|
||||
mv tier-maker tier-maker-manual-backup
|
||||
git clone https://git.sori.studio/zenn/tier-maker.git
|
||||
cd tier-maker
|
||||
cp .env.production.example .env.production
|
||||
# 여기서 기존 .env.production 값을 새 파일에 옮긴다.
|
||||
docker compose --env-file .env.production -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
## 4. NAS 리버스 프록시
|
||||
- 외부 도메인: `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. 데이터 위치
|
||||
- MariaDB 데이터: Docker volume `tmaker_mariadb_data`
|
||||
@@ -67,12 +136,25 @@ docker compose --env-file .env.production -f docker-compose.prod.yml --profile a
|
||||
## 7. 점검 포인트
|
||||
- 메인 접속: `https://tmaker.sori.studio`
|
||||
- 헬스체크: `https://tmaker.sori.studio/health`
|
||||
- 로그인 후 새로고침 또는 `내 티어표` 진입 시 세션이 유지되는지 확인
|
||||
- 관리자 로그인 후:
|
||||
- 게임 생성
|
||||
- 썸네일 업로드
|
||||
- 티어표 저장
|
||||
- 이미지 다운로드
|
||||
|
||||
## 8. 참고
|
||||
## 8. MariaDB가 unhealthy일 때
|
||||
- 로그에 `ready for connections`가 보이면 DB 자체는 정상일 가능성이 높다.
|
||||
- 이 경우는 대부분 healthcheck 시간이 짧아서 생기는 문제다.
|
||||
- 최신 컴포즈 파일 반영 후 아래 순서로 다시 시작한다:
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env.production -f docker-compose.prod.yml down -v
|
||||
docker compose --env-file .env.production -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
## 9. 참고
|
||||
- 현재 업로드 이미지는 서버 저장 전에 리사이즈/압축하지 않는다.
|
||||
- 운영 중 원본 이미지가 많이 쌓이면 이후 `sharp` 기반 최적화 단계를 추가하는 것이 좋다.
|
||||
- 프런트/백엔드/Nginx 설정을 바꿨다면 NAS에서 `docker compose --env-file .env.production -f docker-compose.prod.yml up -d --build`로 이미지를 다시 빌드해 반영한다.
|
||||
- 운영 중 `.env.production`, 업로드 파일 볼륨, MariaDB 볼륨은 Git과 별개로 NAS 로컬 자산이므로 백업 정책을 따로 잡아야 한다.
|
||||
|
||||
@@ -1,24 +1,37 @@
|
||||
# 업데이트 로그
|
||||
|
||||
## 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`로 변경
|
||||
- **무제목 티어표 기본값 조정**: 사용자가 제목을 입력하지 않으면 `이름 없음 + 날짜` 대신 현재 게임명을 기본 제목으로 사용하도록 변경하고, 관리자 임의 삭제 안내 문구는 유지
|
||||
|
||||
## 2026-03-26 v0.1.35
|
||||
- **NAS Git 배포 절차 추가**: UGREEN NAS에서 수동 복사 대신 `git clone`과 `git pull` 기반으로 운영 배포를 관리하는 절차를 배포 가이드에 정리
|
||||
- **v0.1.34 반영 명령 정리**: 이미 수동 복사본이 있는 경우 새 clone으로 전환한 뒤 최신 이미지를 다시 빌드하는 순서를 문서화
|
||||
|
||||
## 2026-03-26 v0.1.34
|
||||
- **파비콘 정적 요청 제거**: 운영 환경에서 `/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`로 조정
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='16' fill='%230b1220'/%3E%3Cpath d='M18 18h28v8H36v20h-8V26H18z' fill='%23f8fafc'/%3E%3C/svg%3E"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
<title>Tier Maker</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ async function logout() {
|
||||
<header class="app-header">
|
||||
<div class="brand" @click="$router.push('/')">
|
||||
<span class="brand__title">Tier Maker</span>
|
||||
<span class="brand__sub">Vue</span>
|
||||
<span class="brand__sub">by zenn</span>
|
||||
</div>
|
||||
<nav class="nav">
|
||||
<RouterLink to="/" class="nav__link">게임</RouterLink>
|
||||
|
||||
@@ -3,7 +3,12 @@ const FALLBACK_API_ORIGIN = 'http://localhost:5179'
|
||||
export const API_ORIGIN = (import.meta.env.VITE_API_ORIGIN || FALLBACK_API_ORIGIN).replace(/\/+$/, '')
|
||||
|
||||
export function toApiUrl(path = '') {
|
||||
if (!path) return API_ORIGIN
|
||||
const p = path.startsWith('/') ? path : `/${path}`
|
||||
|
||||
// 운영(Nginx 프록시)에서는 같은 도메인/프로토콜로 호출해야
|
||||
// 세션 쿠키(`Set-Cookie`)가 브라우저에 정상 저장됩니다.
|
||||
if (import.meta.env.PROD) return p
|
||||
|
||||
if (/^https?:\/\//.test(path)) return path
|
||||
return `${API_ORIGIN}${path.startsWith('/') ? path : `/${path}`}`
|
||||
return `${API_ORIGIN}${p}`
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ code {
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 1126px;
|
||||
/* width: 1126px; */
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
|
||||
@@ -51,7 +51,7 @@ const dropSortables = ref([])
|
||||
|
||||
const isNewTierList = computed(() => tierListId.value === 'new')
|
||||
const canEdit = computed(() => !!auth.user && (!ownerId.value || ownerId.value === auth.user.id))
|
||||
const iconSizeOptions = [48, 60, 80, 100, 120]
|
||||
const iconSizeOptions = [48, 64, 80, 96, 112]
|
||||
const hasCustomTitle = computed(() => !!(title.value || '').trim())
|
||||
const fallbackTimestamp = computed(() => (updatedAt.value ? updatedAt.value : Date.now()))
|
||||
const effectiveAuthorName = computed(() => {
|
||||
@@ -65,7 +65,7 @@ const effectiveAuthorName = computed(() => {
|
||||
const effectiveTitle = computed(() => {
|
||||
const customTitle = (title.value || '').trim()
|
||||
if (customTitle) return customTitle
|
||||
return `이름 없음 ${formatTitleDate(fallbackTimestamp.value)}`
|
||||
return (gameName.value || gameId.value || 'Tier Maker').trim()
|
||||
})
|
||||
const untitledWarning = computed(
|
||||
() =>
|
||||
|
||||
Reference in New Issue
Block a user