diff --git a/Dockerfile b/Dockerfile
index fb7be2a..0df9f43 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,8 +1,24 @@
+# ---- 자산 URL 에 내용 해시를 박는 단계 ----
+# 프록시(NPM)가 js/css 응답에 제 캐시 헤더(Expires, max-age)를 덧붙이므로
+# 원본에서 no-cache 를 줘도 브라우저가 옛 파일을 계속 쓴다. 내용이 바뀌면
+# URL 도 바뀌게 해 두어야 새 배포가 제대로 반영된다.
+FROM alpine:3.20 AS stamp
+WORKDIR /site
+COPY site/ ./
+RUN set -eu; \
+ v=$(cat assets/crossword.css assets/crossword.js | sha256sum | cut -c1-10); \
+ for f in *.html; do \
+ sed -i -e "s|assets/crossword\.css|assets/crossword.css?v=${v}|g" \
+ -e "s|assets/crossword\.js|assets/crossword.js?v=${v}|g" "$f"; \
+ done; \
+ echo "asset stamp: ${v}"; \
+ grep -o 'assets/crossword\.[a-z]*?v=[0-9a-f]*' korean.html
+
FROM nginx:1.27-alpine
# 기본 서버 설정 제거 후 8080 포트용 설정 적용
RUN rm -f /etc/nginx/conf.d/default.conf
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
-COPY site/ /usr/share/nginx/html/
+COPY --from=stamp /site/ /usr/share/nginx/html/
EXPOSE 8080
diff --git a/README.md b/README.md
index 0624df4..5bd669a 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@
| `site/assets/crossword.css` | 각 편이 공유하는 화면 양식 |
| `site/assets/crossword.js` | 각 편이 공유하는 게임 엔진 (한글 IME 입력, 검사/힌트/타이머) |
| `nginx/default.conf` | nginx 설정 (8080 리슨, `/healthz` 헬스체크) |
-| `Dockerfile` | nginx:1.27-alpine 정적 서빙 이미지 |
+| `Dockerfile` | 자산 URL 에 내용 해시를 박는 단계 + nginx:1.27-alpine 정적 서빙 |
| `k8s/deployment.yaml` | Deployment + NodePort Service (31380) |
| `Jenkinsfile` | Checkout → 이미지 빌드/푸시 → kubectl apply |
@@ -65,6 +65,18 @@ nginx 로 파일만 내려주면 끝.
낱말 풀은 작품과 작가만으로는 성글다. 특히 원어 표기의 음절이 서로 겹치지 않는
편(독일 편)에서는 **등장인물과 작품 속 핵심어까지 넣어야** 교차 지점이 생긴다.
+## 캐시
+
+프록시(NPM)가 `js` / `css` 응답에 제 캐시 헤더를 덧붙인다. 원본 nginx 가
+`no-cache` 를 줘도 덮어써지므로, 브라우저가 옛 자산을 계속 쓰는 일을 막기 위해
+**이미지 빌드 때 자산 URL 에 내용 해시를 박는다.**
+
+- `Dockerfile` 의 `stamp` 단계가 `crossword.css` + `crossword.js` 의 sha256 앞
+ 열 자리를 뽑아 각 페이지의 링크를 `assets/crossword.js?v=<해시>` 로 바꾼다
+- 자산 내용이 바뀌면 해시가 바뀌고 URL 도 바뀌므로 캐시가 저절로 무효화된다
+- 저장소의 HTML 은 해시 없는 상태로 두면 된다. 손으로 붙일 필요 없다
+- HTML 자체는 프록시가 캐시하지 않으므로 새 해시가 바로 전달된다
+
## 배포
Jenkins 잡(`crossword`)을 돌리면 아래가 순서대로 진행된다.
diff --git a/nginx/default.conf b/nginx/default.conf
index 890cb21..fe35ef3 100644
--- a/nginx/default.conf
+++ b/nginx/default.conf
@@ -15,9 +15,10 @@ server {
gzip_min_length 1024;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
- # 단일 정적 페이지 - 어떤 경로로 들어와도 index.html
+ # 여러 페이지짜리 정적 사이트. 없는 경로는 index.html 로 되돌리지 말고 404 를 준다.
+ # (되돌리면 /없는페이지.html 이 200 으로 셸을 내려주어 배포 사고를 못 알아챈다)
location / {
- try_files $uri $uri/ /index.html;
+ try_files $uri $uri/ =404;
add_header Cache-Control "no-cache, must-revalidate";
}
diff --git a/site/russian.html b/site/russian.html
index d9d761e..6f378fa 100644
--- a/site/russian.html
+++ b/site/russian.html
@@ -67,7 +67,7 @@