Add German literature puzzle

셸에 독일 문학 탭을 더하고 site/german.html 을 추가했다. 9x9 41칸 14항목.

독일어 표기는 음절이 서로 겹치지 않아 격자가 성글게 나온다. 그래서 낱말 풀을
작품·작가에서 등장인물과 작품 속 핵심어까지 넓혀 교차 지점을 확보했다.
(로테, 헨젤, 카스토르프, 나르치스 같은 인물과 벌레·계약·향기 같은 핵심어)

격자는 기존 두 편과 같은 규약을 만족한다. 길이 2 이상 모든 구간이 단서가 붙은
항목과 일대일로 대응하고, 어떤 단서도 다른 항목의 정답을 그대로 담지 않는다.

탭이 셋이 되면서 아주 좁은 화면(560px 이하)에서는 상표를 접고 탭에 폭을 넘긴다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 14:03:09 +09:00
co-authored by Claude Opus 5
parent 96262fc690
commit 5b2d6e10da
3 changed files with 94 additions and 4 deletions
+8 -3
View File
@@ -11,9 +11,10 @@
|---|---|---|---|
| 러시아 문학 | `/russian.html` | 9 × 9 · 40칸 | 13 |
| 영국 문학 | `/british.html` | 9 × 9 · 39칸 | 12 |
| 독일 문학 | `/german.html` | 9 × 9 · 41칸 | 14 |
각 페이지는 단독으로 열어도 동작한다. 셸(`/`)의 탭은 해시로 상태를 남기므로
`/#russian`, `/#british` 로 바로 들어갈 수 있고, 탭을 다시 눌러도 풀던 내용은 유지된다.
`/#russian`, `/#british`, `/#german` 으로 바로 들어갈 수 있고, 탭을 다시 눌러도 풀던 내용은 유지된다.
## 구성
@@ -22,8 +23,9 @@
| `site/index.html` | 셸 — 왼쪽 탭(좁은 화면에서는 위쪽 탭) + 게임을 띄우는 iframe |
| `site/russian.html` | 러시아 문학 편 (머리말 + 퍼즐 데이터) |
| `site/british.html` | 영국 문학 편 (머리말 + 퍼즐 데이터) |
| `site/assets/crossword.css` | 두 편이 공유하는 화면 양식 |
| `site/assets/crossword.js` | 두 편이 공유하는 게임 엔진 (한글 IME 입력, 검사/힌트/타이머) |
| `site/german.html` | 독일 문학 편 (머리말 + 퍼즐 데이터) |
| `site/assets/crossword.css` | 각 편이 공유하는 화면 양식 |
| `site/assets/crossword.js` | 각 편이 공유하는 게임 엔진 (한글 IME 입력, 검사/힌트/타이머) |
| `nginx/default.conf` | nginx 설정 (8080 리슨, `/healthz` 헬스체크) |
| `Dockerfile` | nginx:1.27-alpine 정적 서빙 이미지 |
| `k8s/deployment.yaml` | Deployment + NodePort Service (31380) |
@@ -50,6 +52,9 @@ nginx 로 파일만 내려주면 끝.
등록돼 있어야 한다.** 우연히 생긴 두 글자 구간이 남아 있으면 그 칸을 채워도
완료 판정이 나지 않는 것처럼 보이므로, 격자를 새로 짜면 이 조건을 먼저 확인한다.
단서를 쓸 때는 **다른 항목의 정답을 단서 안에 그대로 적지 않는다.** 서로가 서로를
알려 주면 두 칸이 한꺼번에 풀려 버린다.
## 배포
Jenkins 잡(`crossword`)을 돌리면 아래가 순서대로 진행된다.
+74
View File
@@ -0,0 +1,74 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>가로세로 낱말풀이 · 독일 문학</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Black+Han+Sans&family=Gothic+A1:wght@300;500;700;900&family=Nanum+Myeongjo:wght@400;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/crossword.css">
</head>
<body>
<div class="wrap">
<header class="head">
<div class="eyebrow">Deutsche Literatur · 9 × 9</div>
<h1>독일 문학<em>가로세로 낱말풀이</em></h1>
<div class="meta">
<div><b id="timer">00:00</b><span>경과 시간</span></div>
<div><b id="progress">0/41</b><span>채운 칸</span></div>
</div>
</header>
<main class="stage">
<section>
<div class="cluebar">
<span class="cb-num" id="cbNum">–</span>
<span class="cb-dir" id="cbDir">가로</span>
<span class="cb-text" id="cbText">칸을 눌러 시작하세요.</span>
</div>
<div class="board-frame">
<div class="board" id="board" role="grid" aria-label="가로세로 낱말풀이 격자"></div>
<input id="ime" type="text" lang="ko" inputmode="text"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
aria-label="글자 입력">
</div>
<div class="tools">
<button type="button" data-act="flip">가로 ↔ 세로</button>
<button type="button" data-act="check">검사</button>
<button type="button" data-act="hint">이 칸 힌트</button>
<button type="button" data-act="reveal">정답 보기</button>
<button type="button" data-act="clear">전부 지우기</button>
</div>
<p class="tip">
같은 칸을 한 번 더 누르면 가로 ↔ 세로가 바뀌고, 오른쪽 단서도 그에 맞춰 바뀝니다.<br>
<kbd>화살표</kbd> 이동 · <kbd>Space</kbd> 방향 전환 · <kbd>Tab</kbd> 다음 단어 · <kbd>Backspace</kbd> 지우고 뒤로
</p>
</section>
<aside>
<section class="cluegroup">
<h2>가로 열쇠</h2>
<ol id="listA"></ol>
</section>
<section class="cluegroup">
<h2>세로 열쇠</h2>
<ol id="listD"></ol>
</section>
</aside>
</main>
</div>
<div class="done" id="done" hidden>
<span id="doneMsg">전부 맞혔습니다.</span>
</div>
<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]], "nums": [[0, 0, 0, 1, 0, 0, 0, 2, 0], [0, 3, 0, 0, 0, 0, 4, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 5], [0, 0, 0, 0, 0, 0, 6, 0, 0], [7, 0, 0, 0, 0, 8, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 9, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [10, 0, 0, 0, 11, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]], "entries": [{"num": 1, "dir": "D", "r": 0, "c": 3, "answer": "책읽어주는남자", "clue": "소년에게 책을 읽어 달라던 한나의 비밀", "kind": "작품"}, {"num": 2, "dir": "D", "r": 0, "c": 7, "answer": "괴테", "clue": "《파우스트》와 《젊은 베르테르의 슬픔》을 남긴 바이마르의 문호", "kind": "작가"}, {"num": 3, "dir": "D", "r": 1, "c": 1, "answer": "서푼짜리오페라", "clue": "거지 왕국과 도둑 매키를 그린 브레히트의 음악극", "kind": "작품"}, {"num": 4, "dir": "A", "r": 1, "c": 6, "answer": "로테", "clue": "베르테르가 사랑했으나 이미 약혼자가 있던 여인", "kind": "인물"}, {"num": 4, "dir": "D", "r": 1, "c": 6, "answer": "로렐라이", "clue": "라인 강 바위에서 뱃사람을 홀리는 처녀", "kind": "작품"}, {"num": 5, "dir": "D", "r": 2, "c": 8, "answer": "헨젤", "clue": "빵 부스러기로 길을 표시한 오빠", "kind": "인물"}, {"num": 6, "dir": "A", "r": 3, "c": 6, "answer": "라푼젤", "clue": "머리카락을 늘어뜨려 탑을 오르게 한 처녀", "kind": "작품"}, {"num": 7, "dir": "A", "r": 4, "c": 0, "answer": "피리부는사나이", "clue": "하멜른의 아이들을 데리고 사라진 사내", "kind": "작품"}, {"num": 8, "dir": "D", "r": 4, "c": 5, "answer": "나르치스", "clue": "수도원에 남아 사색을 택한 벗", "kind": "인물"}, {"num": 9, "dir": "A", "r": 5, "c": 7, "answer": "레싱", "clue": "《현자 나탄》을 쓴 계몽기의 극작가이자 비평가", "kind": "작가"}, {"num": 9, "dir": "D", "r": 5, "c": 7, "answer": "레마르크", "clue": "《서부 전선 이상 없다》의 작가", "kind": "작가"}, {"num": 10, "dir": "A", "r": 7, "c": 0, "answer": "그라스", "clue": "《양철북》으로 노벨문학상을 받은 단치히 태생의 작가", "kind": "작가"}, {"num": 10, "dir": "D", "r": 7, "c": 0, "answer": "그림", "clue": "민담을 모아 《동화집》을 엮은 형제의 성", "kind": "작가"}, {"num": 11, "dir": "A", "r": 7, "c": 4, "answer": "카스토르프", "clue": "요양원에 칠 년을 머문 마의 산의 청년", "kind": "인물"}]};
</script>
<script src="assets/crossword.js"></script>
</body>
</html>
+12 -1
View File
@@ -101,6 +101,12 @@ body{
.nav a .og{display:none}
.rail-foot{display:none}
}
/* 아주 좁은 화면: 탭이 셋 이상이면 상표를 접고 탭에 폭을 몰아 준다 */
@media (max-width:560px){
.brand{display:none}
.nav a{padding:10px 6px}
.nav a .kr{font-size:13px}
}
@media (prefers-reduced-motion:reduce){
*{animation:none !important;transition:none !important}
}
@@ -124,6 +130,10 @@ body{
<span class="kr">영국 문학</span>
<span class="og">British literature</span>
</a>
<a href="german.html" target="stage" data-key="german">
<span class="kr">독일 문학</span>
<span class="og">Deutsche Literatur</span>
</a>
</div>
<div class="rail-foot">
@@ -140,7 +150,8 @@ body{
var PAGES = {
russian: {file:'russian.html', name:'러시아 문학'},
british: {file:'british.html', name:'영국 문학'}
british: {file:'british.html', name:'영국 문학'},
german: {file:'german.html', name:'독일 문학'}
};
var DEFAULT = 'russian';