# 공개 진입점 하나. 사장님 앱 · 발행 사이트 · API 가 **같은 오리진**을 쓴다. # # / → 사장님 앱 (이미지에 구워 넣은 정적 번들, /srv/app) # /s/ → 발행 사이트 (site-out 볼륨에서 정적) # /assets/ → 발행본 공용 번들 (정적) # /robots.txt · /sitemap.xml → 크롤러가 읽는 파일 (정적) # /v1/... /healthz → API(solution-backend:9800) # # ★ 오리진을 가르지 않는 이유: robots.txt·sitemap.xml 은 RFC 9309 상 **오리진 루트에서만** # 읽힌다. 앱과 사이트를 다른 호스트에 두면 인증서도 DNS 도 두 벌이 되고 CORS 가 붙는다. # 개발에서는 Vite 프록시가 같은 일을 한다(solution/frontend/vite.config.ts). # # ★ 산출물은 named volume(site-out)으로 들어온다. 프리렌더가 쓰고 여기서 읽기만 한다 — # 호스트 경로가 등장하지 않으므로 재배포로 코드를 갈아엎어도 사이트가 죽지 않는다. server { listen 80; listen [::]:80; server_name _; # ★ nginx 이미지의 기본 문서 루트(/usr/share/nginx/html)를 쓰지 않는다. # named volume 을 거기 마운트하면 Docker 가 **이미지에 들어 있던 index.html 을 # 빈 볼륨으로 복사한다** — 그러면 오리진 루트가 "Welcome to nginx!" 를 띄우고, # 그게 크롤러에 잡힌다. 빈 경로에 마운트하면 복사될 것이 없다. root /srv/sites; charset utf-8; server_tokens off; client_max_body_size 20m; # ★ Docker 내장 DNS. 업스트림을 변수로 두면 nginx 가 **기동할 때** 이름을 풀지 않는다 — # 안 그러면 solution-backend 가 아직 안 떴을 때 nginx 자체가 죽는다. resolver 127.0.0.11 valid=10s ipv6=off; set $api http://solution-backend:9800; # 텍스트 산출물은 압축이 크게 먹는다(HTML 55KB → 10KB 안팎). gzip on; gzip_comp_level 6; gzip_min_length 1024; gzip_vary on; gzip_types text/plain text/css text/xml application/javascript application/json application/xml image/svg+xml; # ── 발행 사이트 ──────────────────────────────────────────── # ^~ 로 잡아 아래 정규식 location 들이 끼어들지 못하게 한다. location ^~ /s/ { # ★ `/s/` 자체(발행본 목록 페이지)를 위해 필요하다. try_files 의 첫 인자 `$uri` 가 # 끝 슬래시면 nginx 는 **디렉토리 검사**로 읽고, 디렉토리가 있으면 거기서 멈춘다 — # index 지시자가 없으면 그 순간 403 이다(=404 로도 안 떨어진다). index index.html; # $uri/ 를 거치면 nginx 가 끝 슬래시로 301 을 내보낸다. 크롤러가 리다이렉트를 # 한 번 더 타야 하므로 index.html 을 바로 준다. try_files $uri $uri/index.html =404; add_header Cache-Control "public, max-age=300, must-revalidate"; } # 파일명에 해시가 박혀 있다. 내용이 바뀌면 이름이 바뀌므로 영구 캐시가 안전하다. location ^~ /assets/ { add_header Cache-Control "public, max-age=31536000, immutable"; access_log off; try_files $uri =404; } # ★ 빌더와 발행본이 `/fonts/` 를 **각자** 쓴다(vite.config.ts 주석). 발행본을 먼저 보고 # 없으면 빌더 것으로 떨어뜨린다 — 한쪽만 잡으면 다른 쪽 폰트가 조용히 404 다. location ^~ /fonts/ { root /srv/sites; add_header Cache-Control "public, max-age=604800"; access_log off; try_files $uri @app_fonts; } location @app_fonts { root /srv/app; access_log off; try_files $uri =404; } # ── 크롤러가 읽는 파일 ───────────────────────────────────── # 발행하면 곧바로 반영되어야 한다. 길게 캐시하면 새 사업장이 사이트맵에 들어가도 # 크롤러가 옛 파일을 계속 본다. location = /robots.txt { add_header Cache-Control "public, max-age=300, must-revalidate"; try_files $uri =404; } location = /sitemap.xml { add_header Cache-Control "public, max-age=300, must-revalidate"; try_files $uri =404; } # ── API ──────────────────────────────────────────────────── # 앱과 같은 오리진이라 프리플라이트가 아예 발생하지 않는다. location ~ ^/(v1/|healthz$|openapi\.json$|docs|redoc) { proxy_pass $api; proxy_http_version 1.1; 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 $http_x_forwarded_proto; # 발행·수집 잡은 분 단위다. 기본 60s 면 게이트웨이가 먼저 끊는다. proxy_read_timeout 300s; proxy_send_timeout 300s; } # IndexNow 키 파일. 프리렌더가 루트에 .txt 를 굽고 검색엔진이 대조한다. # ★ `{8,128}` 같은 수량자는 못 쓴다 — nginx 는 `{`·`}` 를 블록 구분자로 먼저 읽는다. location ~ ^/[A-Za-z0-9_-]+\.txt$ { try_files $uri =404; } # ── 사장님 앱 (그 외 전부) ───────────────────────────────── # 해시가 박힌 번들. 내용이 바뀌면 이름이 바뀌므로 영구 캐시가 안전하다. # ★ 발행본 `/assets/` 와 겹치지 않게 빌더만 `builder-assets` 로 뺐다 # (solution/frontend/vite.config.ts 의 build.assetsDir). location ^~ /builder-assets/ { root /srv/app; add_header Cache-Control "public, max-age=31536000, immutable"; access_log off; try_files $uri =404; } # ★ 폴백은 `/index.html` 이 아니라 `__spa-fallback.html` 이다. # 프리렌더를 켠 뒤로 `/index.html` 은 **랜딩이 구워진 파일**이다 — 여기로 넘기면 # `/builder` 를 열었는데 랜딩 HTML 이 내려가고, 클라이언트 라우터는 다른 주소로 # 하이드레이트한다. 화면은 뜨는데 한 번 깜빡이고 마크업이 어긋나는 종류다. # 구워진 경로(`/` `/pricing` `/showcase`)는 그 앞의 `$uri/index.html` 이 먼저 잡는다. # ★ HTML 은 캐시하지 않는다 — 번들 해시가 박혀 있어서, 캐시되면 새로 배포해도 # 브라우저가 옛 번들 주소를 계속 부른다(404 → 흰 화면). location / { root /srv/app; try_files $uri $uri/index.html /__spa-fallback.html; add_header Cache-Control "no-cache"; } # 발행되지 않은 주소. 사장님이 오타를 냈을 때 흰 화면 대신 이유를 보여준다. error_page 404 /404.html; location = /404.html { internal; return 404 '페이지를 찾을 수 없습니다

페이지를 찾을 수 없습니다

주소를 다시 확인해 주세요.

'; add_header Content-Type "text/html; charset=utf-8"; } }