Fix clue leak, stop the 404 fallback, bust the proxy asset cache
세 가지를 함께 고쳤다. 러시아 편에서 고리키와 어머니의 단서가 서로의 정답을 그대로 담고 있어 두 칸이 한꺼번에 풀렸다. 양쪽 단서에서 상대 이름을 뺐다. nginx 의 try_files 폴백을 =404 로 바꿨다. 페이지가 여럿이 된 뒤로는 없는 경로도 200 으로 셸을 내려주어, 배포가 안 됐는데도 정상으로 보이는 일이 있었다. 프록시(NPM)가 js/css 에 제 캐시 헤더를 덧붙여 원본의 no-cache 를 덮어쓴다. 이미지 빌드 때 자산 내용의 sha256 앞 열 자리를 뽑아 각 페이지의 링크를 assets/crossword.js?v=<해시> 로 바꾸는 단계를 Dockerfile 에 넣었다. 저장소의 HTML 은 해시 없는 상태로 두고, 자산이 바뀌면 URL 이 저절로 바뀐다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+17
-1
@@ -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
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
# 기본 서버 설정 제거 후 8080 포트용 설정 적용
|
# 기본 서버 설정 제거 후 8080 포트용 설정 적용
|
||||||
RUN rm -f /etc/nginx/conf.d/default.conf
|
RUN rm -f /etc/nginx/conf.d/default.conf
|
||||||
COPY nginx/default.conf /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
|
EXPOSE 8080
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
| `site/assets/crossword.css` | 각 편이 공유하는 화면 양식 |
|
| `site/assets/crossword.css` | 각 편이 공유하는 화면 양식 |
|
||||||
| `site/assets/crossword.js` | 각 편이 공유하는 게임 엔진 (한글 IME 입력, 검사/힌트/타이머) |
|
| `site/assets/crossword.js` | 각 편이 공유하는 게임 엔진 (한글 IME 입력, 검사/힌트/타이머) |
|
||||||
| `nginx/default.conf` | nginx 설정 (8080 리슨, `/healthz` 헬스체크) |
|
| `nginx/default.conf` | nginx 설정 (8080 리슨, `/healthz` 헬스체크) |
|
||||||
| `Dockerfile` | nginx:1.27-alpine 정적 서빙 이미지 |
|
| `Dockerfile` | 자산 URL 에 내용 해시를 박는 단계 + nginx:1.27-alpine 정적 서빙 |
|
||||||
| `k8s/deployment.yaml` | Deployment + NodePort Service (31380) |
|
| `k8s/deployment.yaml` | Deployment + NodePort Service (31380) |
|
||||||
| `Jenkinsfile` | Checkout → 이미지 빌드/푸시 → kubectl apply |
|
| `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`)을 돌리면 아래가 순서대로 진행된다.
|
Jenkins 잡(`crossword`)을 돌리면 아래가 순서대로 진행된다.
|
||||||
|
|||||||
+3
-2
@@ -15,9 +15,10 @@ server {
|
|||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||||
|
|
||||||
# 단일 정적 페이지 - 어떤 경로로 들어와도 index.html
|
# 여러 페이지짜리 정적 사이트. 없는 경로는 index.html 로 되돌리지 말고 404 를 준다.
|
||||||
|
# (되돌리면 /없는페이지.html 이 200 으로 셸을 내려주어 배포 사고를 못 알아챈다)
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ =404;
|
||||||
add_header Cache-Control "no-cache, must-revalidate";
|
add_header Cache-Control "no-cache, must-revalidate";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -67,7 +67,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const PUZZLE = {"size": 9, "grid": [["아", "버", "지", "와", "아", "들", null, null, "고"], [null, null, null, null, "흐", null, null, null, "리"], ["브", null, "롤", null, "마", "야", "콥", "스", "키"], ["로", null, "리", null, "토", null, null, "페", null], ["츠", "베", "타", "예", "바", null, null, "이", null], ["키", null, null, "세", null, null, null, "드", null], [null, null, "부", "닌", null, "어", "둠", "의", "힘"], [null, null, "활", null, null, "머", null, "여", null], [null, null, null, null, null, "니", null, "왕", null]], "nums": [[1, 0, 0, 0, 2, 0, 0, 0, 3], [0, 0, 0, 0, 0, 0, 0, 0, 0], [4, 0, 5, 0, 6, 0, 0, 7, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [8, 0, 0, 9, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 10, 0, 0, 11, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]], "entries": [{"num": 1, "dir": "A", "r": 0, "c": 0, "answer": "아버지와아들", "clue": "허무주의자 바자로프가 등장하는 투르게네프 장편", "kind": "작품"}, {"num": 2, "dir": "D", "r": 0, "c": 4, "answer": "아흐마토바", "clue": "《레퀴엠》을 남긴 20세기 러시아의 여성 시인", "kind": "작가"}, {"num": 3, "dir": "D", "r": 0, "c": 8, "answer": "고리키", "clue": "《어머니》 《밑바닥에서》를 쓴 사회주의 리얼리즘 작가", "kind": "작가"}, {"num": 4, "dir": "D", "r": 2, "c": 0, "answer": "브로츠키", "clue": "미국으로 망명해 노벨문학상을 받은 러시아 시인", "kind": "작가"}, {"num": 5, "dir": "D", "r": 2, "c": 2, "answer": "롤리타", "clue": "나보코프를 세계적 논쟁의 중심에 세운 장편", "kind": "작품"}, {"num": 6, "dir": "A", "r": 2, "c": 4, "answer": "마야콥스키", "clue": "러시아 미래파를 대표하는 혁명기의 시인", "kind": "작가"}, {"num": 7, "dir": "D", "r": 2, "c": 7, "answer": "스페이드의여왕", "clue": "카드의 비밀에 집착한 게르만의 파멸", "kind": "작품"}, {"num": 8, "dir": "A", "r": 4, "c": 0, "answer": "츠베타예바", "clue": "망명과 귀국의 비극을 겪은 20세기 여성 시인", "kind": "작가"}, {"num": 9, "dir": "D", "r": 4, "c": 3, "answer": "예세닌", "clue": "농촌의 서정을 노래한 시인, 무용가 이사도라 덩컨의 남편", "kind": "작가"}, {"num": 10, "dir": "A", "r": 6, "c": 2, "answer": "부닌", "clue": "《미탸의 사랑》을 쓴, 러시아 최초의 노벨문학상 수상 작가", "kind": "작가"}, {"num": 10, "dir": "D", "r": 6, "c": 2, "answer": "부활", "clue": "네흘류도프와 카츄샤가 등장하는 톨스토이 후기 장편", "kind": "작품"}, {"num": 11, "dir": "A", "r": 6, "c": 5, "answer": "어둠의힘", "clue": "농촌의 죄와 참회를 다룬 톨스토이의 희곡", "kind": "작품"}, {"num": 11, "dir": "D", "r": 6, "c": 5, "answer": "어머니", "clue": "혁명에 눈뜨는 펠라게야를 그린 고리키의 장편", "kind": "작품"}]};
|
const PUZZLE = {"size": 9, "grid": [["아", "버", "지", "와", "아", "들", null, null, "고"], [null, null, null, null, "흐", null, null, null, "리"], ["브", null, "롤", null, "마", "야", "콥", "스", "키"], ["로", null, "리", null, "토", null, null, "페", null], ["츠", "베", "타", "예", "바", null, null, "이", null], ["키", null, null, "세", null, null, null, "드", null], [null, null, "부", "닌", null, "어", "둠", "의", "힘"], [null, null, "활", null, null, "머", null, "여", null], [null, null, null, null, null, "니", null, "왕", null]], "nums": [[1, 0, 0, 0, 2, 0, 0, 0, 3], [0, 0, 0, 0, 0, 0, 0, 0, 0], [4, 0, 5, 0, 6, 0, 0, 7, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [8, 0, 0, 9, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 10, 0, 0, 11, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]], "entries": [{"num": 1, "dir": "A", "r": 0, "c": 0, "answer": "아버지와아들", "clue": "허무주의자 바자로프가 등장하는 투르게네프 장편", "kind": "작품"}, {"num": 2, "dir": "D", "r": 0, "c": 4, "answer": "아흐마토바", "clue": "《레퀴엠》을 남긴 20세기 러시아의 여성 시인", "kind": "작가"}, {"num": 3, "dir": "D", "r": 0, "c": 8, "answer": "고리키", "clue": "《밑바닥에서》와 자전 삼부작을 남긴 사회주의 리얼리즘 작가", "kind": "작가"}, {"num": 4, "dir": "D", "r": 2, "c": 0, "answer": "브로츠키", "clue": "미국으로 망명해 노벨문학상을 받은 러시아 시인", "kind": "작가"}, {"num": 5, "dir": "D", "r": 2, "c": 2, "answer": "롤리타", "clue": "나보코프를 세계적 논쟁의 중심에 세운 장편", "kind": "작품"}, {"num": 6, "dir": "A", "r": 2, "c": 4, "answer": "마야콥스키", "clue": "러시아 미래파를 대표하는 혁명기의 시인", "kind": "작가"}, {"num": 7, "dir": "D", "r": 2, "c": 7, "answer": "스페이드의여왕", "clue": "카드의 비밀에 집착한 게르만의 파멸", "kind": "작품"}, {"num": 8, "dir": "A", "r": 4, "c": 0, "answer": "츠베타예바", "clue": "망명과 귀국의 비극을 겪은 20세기 여성 시인", "kind": "작가"}, {"num": 9, "dir": "D", "r": 4, "c": 3, "answer": "예세닌", "clue": "농촌의 서정을 노래한 시인, 무용가 이사도라 덩컨의 남편", "kind": "작가"}, {"num": 10, "dir": "A", "r": 6, "c": 2, "answer": "부닌", "clue": "《미탸의 사랑》을 쓴, 러시아 최초의 노벨문학상 수상 작가", "kind": "작가"}, {"num": 10, "dir": "D", "r": 6, "c": 2, "answer": "부활", "clue": "네흘류도프와 카츄샤가 등장하는 톨스토이 후기 장편", "kind": "작품"}, {"num": 11, "dir": "A", "r": 6, "c": 5, "answer": "어둠의힘", "clue": "농촌의 죄와 참회를 다룬 톨스토이의 희곡", "kind": "작품"}, {"num": 11, "dir": "D", "r": 6, "c": 5, "answer": "어머니", "clue": "아들이 잡혀간 뒤 혁명에 눈뜨는 펠라게야 닐로브나의 장편", "kind": "작품"}]};
|
||||||
</script>
|
</script>
|
||||||
<script src="assets/crossword.js"></script>
|
<script src="assets/crossword.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user