diff --git a/README.md b/README.md index e2cd101..e465dfc 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,55 @@ # crossword -러시아 문학 가로세로 낱말풀이 — 정적 단일 페이지 사이트. +문학 가로세로 낱말풀이 — 정적 사이트. 왼쪽 탭에서 편을 골라 본 프레임에서 바로 푼다. - 공개 주소: https://crossword.plpark.ectech.co.kr - 내부 주소: http://192.168.0.216:31380 (NodePort) +## 수록 편 + +| 탭 | 경로 | 격자 | 항목 | +|---|---|---|---| +| 러시아 문학 | `/russian.html` | 9 × 9 · 40칸 | 13 | +| 영국 문학 | `/british.html` | 9 × 9 · 39칸 | 12 | + +각 페이지는 단독으로 열어도 동작한다. 셸(`/`)의 탭은 해시로 상태를 남기므로 +`/#russian`, `/#british` 로 바로 들어갈 수 있고, 탭을 다시 눌러도 풀던 내용은 유지된다. + ## 구성 | 경로 | 설명 | |---|---| -| `site/index.html` | 게임 본체 (원본: `프로젝트/02_crossword/russian-lit-crossword.html`) | +| `site/index.html` | 셸 — 왼쪽 탭(좁은 화면에서는 위쪽 탭) + 게임을 띄우는 iframe | +| `site/russian.html` | 러시아 문학 편 (머리말 + 퍼즐 데이터) | +| `site/british.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) | | `Jenkinsfile` | Checkout → 이미지 빌드/푸시 → kubectl apply | -Spring Boot 등 런타임은 없음. HTML/CSS/JS 만으로 동작하는 완전 정적 페이지라 +Spring Boot 등 런타임은 없음. HTML/CSS/JS 만으로 동작하는 완전 정적 사이트라 nginx 로 파일만 내려주면 끝. +## 편을 추가할 때 + +1. `site/<편>.html` 을 기존 편 하나를 복사해 만든다. 고칠 곳은 ``, + 머리말(`.eyebrow` / `h1`), 그리고 `const PUZZLE = {...}` 뿐이다. +2. `site/index.html` 의 `PAGES` 객체와 `.nav` 링크에 한 줄씩 추가한다. + +`PUZZLE` 규약: + +- `size` — 격자 한 변 (9) +- `grid[r][c]` — 정답 음절, 빈 칸은 `null` +- `nums[r][c]` — 칸에 찍히는 번호, 없으면 `0`. 길이 2 이상 구간의 시작 칸에 + 좌→우 · 위→아래 순서로 붙인다 +- `entries[]` — `{num, dir:"A"|"D", r, c, answer, clue, kind}` + +**가로·세로로 길이 2 이상 이어지는 모든 구간은 반드시 `entries` 에 단서와 함께 +등록돼 있어야 한다.** 우연히 생긴 두 글자 구간이 남아 있으면 그 칸을 채워도 +완료 판정이 나지 않는 것처럼 보이므로, 격자를 새로 짜면 이 조건을 먼저 확인한다. + ## 배포 Jenkins 잡(`crossword`)을 돌리면 아래가 순서대로 진행된다. @@ -42,4 +74,4 @@ Jenkins credentials 는 기존 loggen/pilot1 과 동일하게 ## 콘텐츠 수정 -`site/index.html` 만 고치고 커밋 → Jenkins 빌드. 그 외 변경 필요 없음. +`site/` 아래만 고치고 커밋 → Jenkins 빌드. 그 외 변경 필요 없음. diff --git a/site/assets/crossword.css b/site/assets/crossword.css new file mode 100644 index 0000000..5e5a2ee --- /dev/null +++ b/site/assets/crossword.css @@ -0,0 +1,166 @@ +:root{ + --ink:#12100E; + --panel:#1B1815; + --paper:#E7E1D3; + --paper-lit:#F3E3C8; + --amber:#E8B23C; + --vermilion:#C6362A; + --vermilion-dim:#8E2A21; + --slate:#8A8272; + --bone:#D8D2C4; + --cellfont:"Nanum Myeongjo","Apple SD Gothic Neo","Malgun Gothic",serif; + --ui:"Gothic A1","Apple SD Gothic Neo","Malgun Gothic","Noto Sans KR",sans-serif; + --display:"Black Han Sans","Gothic A1","Apple SD Gothic Neo",sans-serif; +} +*{box-sizing:border-box} +html,body{margin:0;padding:0} +body{ + background:var(--ink); + color:var(--bone); + font-family:var(--ui); + font-weight:300; + -webkit-text-size-adjust:100%; + padding:0 0 64px; +} +.wrap{max-width:1120px;margin:0 auto;padding:28px 20px 0} + +/* ---------- 머리말 ---------- */ +.head{border-bottom:2px solid var(--vermilion);padding-bottom:14px;margin-bottom:26px;position:relative} +.eyebrow{ + font-size:11px;letter-spacing:.32em;color:var(--slate); + font-weight:700;text-transform:uppercase;margin-bottom:10px; +} +.head h1{ + font-family:var(--display); + font-weight:400; + font-size:clamp(30px,7vw,54px); + line-height:.98; + margin:0; + color:var(--paper); + letter-spacing:-.01em; +} +.head h1 em{font-style:normal;color:var(--vermilion);display:block} +.meta{ + display:flex;gap:18px;align-items:baseline; + position:absolute;right:0;bottom:14px; +} +.meta b{font-family:var(--ui);font-weight:700;font-size:22px;color:var(--amber);font-variant-numeric:tabular-nums} +.meta span{font-size:11px;letter-spacing:.18em;color:var(--slate);display:block;font-weight:700} +@media (max-width:640px){ + .meta{position:static;margin-top:14px} +} + +/* ---------- 본문 배치 ---------- */ +.stage{display:grid;grid-template-columns:minmax(0,auto) minmax(280px,1fr);gap:40px;align-items:start} +@media (max-width:900px){.stage{grid-template-columns:1fr;gap:26px}} + +/* ---------- 현재 단서 띠 ---------- */ +.cluebar{ + display:flex;align-items:center;gap:12px; + background:var(--panel);border-left:5px solid var(--vermilion); + padding:11px 14px;margin-bottom:14px;min-height:56px; +} +.cb-num{ + background:var(--vermilion);color:#fff;font-weight:900;font-size:14px; + min-width:26px;height:26px;display:grid;place-items:center;flex:none; +} +.cb-dir{font-size:11px;font-weight:700;letter-spacing:.2em;color:var(--amber);flex:none} +.cb-text{font-size:15px;line-height:1.45;color:var(--paper);font-weight:300} + +/* ---------- 격자 ---------- */ +.board-frame{position:relative;width:min(94vw,468px)} +.board{ + display:grid;grid-template-columns:repeat(9,1fr);gap:2px; + background:#000;border:3px solid #000; + box-shadow:9px 9px 0 var(--vermilion-dim); + width:100%; +} +.cell{ + position:relative;aspect-ratio:1/1;background:var(--paper); + display:grid;place-items:center;cursor:pointer;user-select:none; + transition:background .09s linear; +} +.cell.block{background:#0B0A08;cursor:default} +.cell .n{ + position:absolute;top:2px;left:4px;font-family:var(--ui); + font-size:clamp(8px,1.9vw,10px);font-weight:700;color:var(--vermilion);line-height:1;pointer-events:none; +} +.cell .ch{ + font-family:var(--cellfont);font-weight:700;color:#14120F; + font-size:clamp(17px,4.4vw,27px);line-height:1;pointer-events:none; + transform:translateY(1px); +} +.cell.lit{background:var(--paper-lit)} +.cell.cur{background:var(--amber)} +.cell.given .ch{color:var(--vermilion)} +.cell.bad::after{ + content:"";position:absolute;inset:0;pointer-events:none; + background: + linear-gradient(to bottom left, transparent 46%, var(--vermilion) 46%, var(--vermilion) 54%, transparent 54%); + opacity:.85; +} +.board.solved .cell:not(.block){background:#EFE4C6} +#ime{ + position:absolute;pointer-events:none;opacity:0; + border:0;padding:0;margin:0;background:transparent; + color:transparent;caret-color:transparent;font-size:16px; + z-index:2; +} + +/* ---------- 도구 ---------- */ +.tools{display:flex;flex-wrap:wrap;gap:7px;margin-top:22px} +.tools button{ + font-family:var(--ui);font-size:12px;font-weight:700;letter-spacing:.06em; + color:var(--bone);background:transparent;border:1px solid #3A352D; + padding:9px 13px;cursor:pointer;transition:.14s; +} +.tools button:hover{border-color:var(--amber);color:var(--amber)} +.tools button.on{background:var(--vermilion);border-color:var(--vermilion);color:#fff} +.tools button:focus-visible{outline:2px solid var(--amber);outline-offset:2px} +.tip{font-size:12px;line-height:1.7;color:var(--slate);margin:16px 0 0;max-width:44ch} +.tip kbd{ + font-family:var(--ui);font-size:11px;font-weight:700;color:var(--bone); + border:1px solid #3A352D;padding:1px 5px; +} + +/* ---------- 단서 목록 ---------- */ +.cluegroup+.cluegroup{margin-top:30px} +.cluegroup h2{ + font-family:var(--ui);font-size:11px;font-weight:900;letter-spacing:.3em; + color:var(--ink);background:var(--amber);display:inline-block; + padding:4px 11px;margin:0 0 12px; +} +.cluegroup ol{list-style:none;margin:0;padding:0} +.cluegroup li{ + display:grid;grid-template-columns:30px 1fr;gap:10px;align-items:baseline; + padding:8px 10px 8px 6px;cursor:pointer;border-left:3px solid transparent; + border-bottom:1px solid #26221D; +} +.cluegroup li b{font-weight:900;font-size:13px;color:var(--slate);text-align:right} +.cluegroup li p{margin:0;font-size:13.5px;line-height:1.5;color:#BFB8A8;font-weight:300} +.cluegroup li i{font-style:normal;color:var(--slate);font-size:11px;margin-left:5px} +.cluegroup li:hover{background:#1B1815} +.cluegroup li.cross{border-left-color:var(--vermilion-dim)} +.cluegroup li.cross b{color:var(--bone)} +.cluegroup li.on{background:var(--vermilion);border-left-color:var(--amber)} +.cluegroup li.on b,.cluegroup li.on p,.cluegroup li.on i{color:#fff} +.cluegroup li.filled b{color:var(--amber)} + +/* ---------- 완료 ---------- */ +.done{ + position:fixed;inset:auto 0 0;background:var(--vermilion);color:#fff; + padding:16px 20px;display:flex;gap:14px;align-items:center;justify-content:center; + font-weight:700;font-size:15px;letter-spacing:.02em;z-index:9; + animation:rise .35s ease-out; +} +@keyframes rise{from{transform:translateY(100%)}to{transform:translateY(0)}} +.done[hidden]{display:none} +@media (prefers-reduced-motion:reduce){ + *{animation:none !important;transition:none !important} +} + +/* ---------- 셸(index.html) 프레임 안에서 ---------- */ +.framed body{padding-bottom:72px} +.framed .wrap{padding-top:18px} +.framed .head h1{font-size:clamp(26px,5.4vw,40px)} +.framed .head{padding-bottom:12px;margin-bottom:20px} diff --git a/site/assets/crossword.js b/site/assets/crossword.js new file mode 100644 index 0000000..f3cf3ac --- /dev/null +++ b/site/assets/crossword.js @@ -0,0 +1,344 @@ +(function(){ +"use strict"; + +/* 셸(index.html) 안에서 열렸는지 표시 - 여백만 살짝 줄인다 */ +if (window.self !== window.top) document.documentElement.classList.add('framed'); + +const N = PUZZLE.size; +const SOL = PUZZLE.grid; +const NUMS = PUZZLE.nums; + +const board = document.getElementById('board'); +const ime = document.getElementById('ime'); +const listA = document.getElementById('listA'); +const listD = document.getElementById('listD'); +const cbNum = document.getElementById('cbNum'); +const cbDir = document.getElementById('cbDir'); +const cbText = document.getElementById('cbText'); +const elTimer = document.getElementById('timer'); +const elProg = document.getElementById('progress'); +const doneBar = document.getElementById('done'); +const doneMsg = document.getElementById('doneMsg'); + +const idx = (r,c) => r*N + c; +const isOpen = (r,c) => r>=0 && r<N && c>=0 && c<N && SOL[r][c] !== null; + +/* ---------- 데이터 준비 ---------- */ +const ENTRIES = PUZZLE.entries.map((e, i) => { + const cells = []; + for (let k=0; k<e.answer.length; k++){ + cells.push(e.dir === 'A' ? [e.r, e.c+k] : [e.r+k, e.c]); + } + return {i, num:e.num, dir:e.dir, answer:e.answer, clue:e.clue, kind:e.kind, cells}; +}); +ENTRIES.sort((a,b) => a.num - b.num || (a.dir === 'A' ? -1 : 1)); +ENTRIES.forEach((e,i) => e.i = i); + +const owner = []; // owner[idx] = {A:entry|null, D:entry|null} +for (let i=0;i<N*N;i++) owner[i] = {A:null, D:null}; +ENTRIES.forEach(e => e.cells.forEach(([r,c]) => { owner[idx(r,c)][e.dir] = e; })); + +const TOTAL = SOL.flat().filter(v => v !== null).length; + +/* ---------- 상태 ---------- */ +const user = new Array(N*N).fill(''); +const given = new Set(); // 힌트/정답으로 채워진 칸 +let dir = 'A'; +let cur = ENTRIES[0].cells[0].slice(); +let session = null; // {cells, written} +let composing = false; +let checking = false; +let solved = false; +let t0 = null, tick = null; +let booted = false; // 첫 렌더 뒤에만 단서 목록을 따라 스크롤 + +/* ---------- 격자 그리기 ---------- */ +const cellEls = new Array(N*N).fill(null); +for (let r=0;r<N;r++){ + for (let c=0;c<N;c++){ + const d = document.createElement('div'); + d.className = 'cell' + (SOL[r][c] === null ? ' block' : ''); + if (SOL[r][c] !== null){ + d.setAttribute('role','gridcell'); + d.dataset.r = r; d.dataset.c = c; + if (NUMS[r][c]) d.innerHTML = '<span class="n">' + NUMS[r][c] + '</span>'; + const ch = document.createElement('span'); + ch.className = 'ch'; + d.appendChild(ch); + cellEls[idx(r,c)] = d; + } + board.appendChild(d); + } +} + +/* ---------- 단서 목록 ---------- */ +function buildList(ol, d){ + ENTRIES.filter(e => e.dir === d).forEach(e => { + const li = document.createElement('li'); + li.dataset.i = e.i; + li.innerHTML = '<b>' + e.num + '</b><p>' + e.clue + + '<i>(' + e.answer.length + '글자 · ' + e.kind + ')</i></p>'; + li.addEventListener('click', () => { + startTimer(); + const empty = e.cells.find(([r,c]) => !user[idx(r,c)]) || e.cells[0]; + focusCell(empty[0], empty[1], e.dir); + }); + ol.appendChild(li); + e.li = li; + }); +} +buildList(listA, 'A'); +buildList(listD, 'D'); + +/* ---------- 화면 갱신 ---------- */ +function currentEntry(){ return owner[idx(cur[0],cur[1])][dir]; } + +function render(){ + const e = currentEntry(); + const lit = new Set(e ? e.cells.map(([r,c]) => idx(r,c)) : []); + for (let i=0;i<N*N;i++){ + const el = cellEls[i]; + if (!el) continue; + const r = Math.floor(i/N), c = i%N; + el.querySelector('.ch').textContent = user[i] || ''; + el.classList.toggle('lit', lit.has(i) && i !== idx(cur[0],cur[1])); + el.classList.toggle('cur', i === idx(cur[0],cur[1])); + el.classList.toggle('given', given.has(i)); + el.classList.toggle('bad', checking && !!user[i] && user[i] !== SOL[r][c]); + } + const filled = user.filter(Boolean).length; + elProg.textContent = filled + '/' + TOTAL; + + ENTRIES.forEach(x => { + x.li.classList.remove('on','cross'); + x.li.classList.toggle('filled', x.cells.every(([r,c]) => !!user[idx(r,c)])); + }); + const other = owner[idx(cur[0],cur[1])][dir === 'A' ? 'D' : 'A']; + if (other) other.li.classList.add('cross'); + if (e){ + e.li.classList.add('on'); + if (booted) e.li.scrollIntoView({block:'nearest'}); + cbNum.textContent = e.num; + cbDir.textContent = e.dir === 'A' ? '가로' : '세로'; + cbText.textContent = e.clue; + } +} + +function placeIme(){ + const el = cellEls[idx(cur[0],cur[1])]; + if (!el) return; + ime.style.left = el.offsetLeft + 'px'; + ime.style.top = el.offsetTop + 'px'; + ime.style.width = el.offsetWidth + 'px'; + ime.style.height = el.offsetHeight + 'px'; +} +window.addEventListener('resize', placeIme); + +/* ---------- 입력 세션 ---------- */ +function commit(){ + if (!session) return; + for (let i=0;i<session.written;i++){ + const [r,c] = session.cells[i]; + const ch = user[idx(r,c)]; + if (ch && !/^[가-힣]$/.test(ch)) user[idx(r,c)] = ''; // 조합이 끝나지 않은 자모는 버린다 + } + ime.value = ''; + session = null; +} + +function focusCell(r, c, want){ + if (!isOpen(r,c)) return; + commit(); + const o = owner[idx(r,c)]; + dir = o[want] ? want : (o.A ? 'A' : 'D'); + cur = [r,c]; + const e = currentEntry(); + const k = e ? e.cells.findIndex(([a,b]) => a === r && b === c) : 0; + session = {cells: e ? e.cells.slice(k) : [[r,c]], written: 0}; + ime.value = ''; + placeIme(); + render(); + try { ime.focus({preventScroll:true}); } catch(_) { ime.focus(); } +} + +function setCell(r, c, ch){ + const i = idx(r,c); + user[i] = ch; + if (ch) given.delete(i); +} + +function onInput(){ + if (!session) return; + const cells = session.cells; + if (!composing){ + const cleaned = ime.value.replace(/\s+/g,''); + if (cleaned !== ime.value) ime.value = cleaned; + } + let buf = Array.from(ime.value).filter(ch => ch !== ' '); + if (!composing && buf.length > cells.length){ + buf = buf.slice(0, cells.length); + ime.value = buf.join(''); + } + for (let i=buf.length; i<session.written; i++){ + setCell(cells[i][0], cells[i][1], ''); + } + const n = Math.min(buf.length, cells.length); + for (let i=0;i<n;i++) setCell(cells[i][0], cells[i][1], buf[i]); + session.written = n; + + const k = Math.min(buf.length, cells.length - 1); + cur = cells[k].slice(); + placeIme(); + render(); + checkSolved(); +} + +ime.addEventListener('compositionstart', () => { composing = true; }); +ime.addEventListener('compositionend', () => { composing = false; onInput(); }); +ime.addEventListener('input', onInput); +ime.addEventListener('blur', () => { commit(); render(); }); + +ime.addEventListener('keydown', ev => { + if (ev.isComposing || ev.keyCode === 229) return; + const k = ev.key; + if (k === 'ArrowLeft' || k === 'ArrowRight' || k === 'ArrowUp' || k === 'ArrowDown'){ + ev.preventDefault(); arrow(k); return; + } + if (k === ' '){ ev.preventDefault(); flip(); return; } + if (k === 'Tab'){ ev.preventDefault(); hopEntry(ev.shiftKey ? -1 : 1); return; } + if (k === 'Enter'){ ev.preventDefault(); hopEntry(1); return; } + if (k === 'Backspace' && ime.value.length === 0){ ev.preventDefault(); stepBack(); return; } +}); + +/* ---------- 이동 ---------- */ +function arrow(key){ + const want = (key === 'ArrowLeft' || key === 'ArrowRight') ? 'A' : 'D'; + const [r,c] = cur; + if (dir !== want && owner[idx(r,c)][want]){ focusCell(r, c, want); return; } + const dr = key === 'ArrowUp' ? -1 : key === 'ArrowDown' ? 1 : 0; + const dc = key === 'ArrowLeft' ? -1 : key === 'ArrowRight' ? 1 : 0; + let nr = r + dr, nc = c + dc; + while (nr>=0 && nr<N && nc>=0 && nc<N){ + if (isOpen(nr,nc)){ focusCell(nr, nc, dir); return; } + nr += dr; nc += dc; + } +} + +function stepBack(){ + const e = currentEntry(); + if (!e) return; + const k = e.cells.findIndex(([a,b]) => a === cur[0] && b === cur[1]); + if (k > 0){ + const [r,c] = e.cells[k-1]; + setCell(r, c, ''); + focusCell(r, c, dir); + } +} + +function flip(){ + const o = owner[idx(cur[0],cur[1])]; + const nd = dir === 'A' ? 'D' : 'A'; + if (o[nd]) focusCell(cur[0], cur[1], nd); +} + +function hopEntry(step){ + const e = currentEntry(); + const at = e ? e.i : 0; + const next = ENTRIES[(at + step + ENTRIES.length) % ENTRIES.length]; + const empty = next.cells.find(([r,c]) => !user[idx(r,c)]) || next.cells[0]; + focusCell(empty[0], empty[1], next.dir); +} + +/* ---------- 클릭 ---------- */ +board.addEventListener('click', ev => { + const el = ev.target.closest('.cell'); + if (!el || el.classList.contains('block')) return; + startTimer(); + const r = +el.dataset.r, c = +el.dataset.c; + const same = (r === cur[0] && c === cur[1]); + const o = owner[idx(r,c)]; + let want = dir; + if (same && o[dir === 'A' ? 'D' : 'A']) want = dir === 'A' ? 'D' : 'A'; + focusCell(r, c, want); +}); + +/* ---------- 도구 ---------- */ +document.querySelector('.tools').addEventListener('click', ev => { + const b = ev.target.closest('button'); + if (!b) return; + const act = b.dataset.act; + if (act === 'flip'){ flip(); } + if (act === 'check'){ + checking = !checking; + b.classList.toggle('on', checking); + render(); + } + if (act === 'hint'){ + commit(); + const [r,c] = cur; + user[idx(r,c)] = SOL[r][c]; + given.add(idx(r,c)); + focusCell(r, c, dir); + checkSolved(); + } + if (act === 'reveal'){ + if (!confirm('정답을 모두 채웁니다. 계속할까요?')) return; + commit(); + for (let r=0;r<N;r++) for (let c=0;c<N;c++){ + if (SOL[r][c] === null) continue; + if (user[idx(r,c)] !== SOL[r][c]) given.add(idx(r,c)); + user[idx(r,c)] = SOL[r][c]; + } + stopTimer(); + solved = true; + board.classList.add('solved'); + doneMsg.textContent = '정답을 모두 펼쳤습니다.'; + doneBar.hidden = false; + render(); + } + if (act === 'clear'){ + if (!confirm('입력한 글자를 전부 지웁니다. 계속할까요?')) return; + commit(); + user.fill(''); + given.clear(); + solved = false; + board.classList.remove('solved'); + doneBar.hidden = true; + focusCell(cur[0], cur[1], dir); + } +}); + +/* ---------- 완료 판정 ---------- */ +function checkSolved(){ + if (solved) return; + for (let r=0;r<N;r++) for (let c=0;c<N;c++){ + if (SOL[r][c] === null) continue; + if (user[idx(r,c)] !== SOL[r][c]) return; + } + solved = true; + stopTimer(); + board.classList.add('solved'); + const helped = given.size; + doneMsg.textContent = helped + ? '전부 맞혔습니다. (' + elTimer.textContent + ' · 힌트 ' + helped + '칸)' + : '힌트 없이 전부 맞혔습니다. ' + elTimer.textContent; + doneBar.hidden = false; +} + +/* ---------- 시간 ---------- */ +function startTimer(){ + if (t0 || solved) return; + t0 = Date.now(); + tick = setInterval(() => { + const s = Math.floor((Date.now() - t0)/1000); + elTimer.textContent = String(Math.floor(s/60)).padStart(2,'0') + ':' + String(s%60).padStart(2,'0'); + }, 1000); +} +function stopTimer(){ if (tick) clearInterval(tick); tick = null; } + +/* ---------- 시작 ---------- */ +focusCell(cur[0], cur[1], 'A'); +ime.blur(); +render(); +booted = true; +})(); diff --git a/site/british.html b/site/british.html new file mode 100644 index 0000000..727fad1 --- /dev/null +++ b/site/british.html @@ -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>가로세로 낱말풀이 · 영국 문학 + + + + + + +
+
+
British literature · 9 × 9
+

영국 문학가로세로 낱말풀이

+
+
00:00경과 시간
+
0/39채운 칸
+
+
+ +
+
+
+ – + 가로 + 칸을 눌러 시작하세요. +
+ +
+
+ +
+ +
+ + + + + +
+ +

+ 같은 칸을 한 번 더 누르면 가로 ↔ 세로가 바뀌고, 오른쪽 단서도 그에 맞춰 바뀝니다.
+ 화살표 이동 · Space 방향 전환 · Tab 다음 단어 · Backspace 지우고 뒤로 +

+
+ + +
+
+ + + + + + + diff --git a/site/index.html b/site/index.html index dab2055..78b93a4 100644 --- a/site/index.html +++ b/site/index.html @@ -3,572 +3,189 @@ -가로세로 낱말풀이 · 러시아 문학 +가로세로 낱말풀이 - + -
-
-
Русская литература · 9 × 9
-

러시아 문학가로세로 낱말풀이

-
-
00:00경과 시간
-
0/40채운 칸
-
-
-
-
-
- – - 가로 - 칸을 눌러 시작하세요. -
+ -

- 같은 칸을 한 번 더 누르면 가로 ↔ 세로가 바뀌고, 오른쪽 단서도 그에 맞춰 바뀝니다.
- 화살표 이동 · Space 방향 전환 · Tab 다음 단어 · Backspace 지우고 뒤로 -

-
+ + - -
-
- - - - diff --git a/site/russian.html b/site/russian.html new file mode 100644 index 0000000..d9d761e --- /dev/null +++ b/site/russian.html @@ -0,0 +1,74 @@ + + + + + +가로세로 낱말풀이 · 러시아 문학 + + + + + + +
+
+
Русская литература · 9 × 9
+

러시아 문학가로세로 낱말풀이

+
+
00:00경과 시간
+
0/40채운 칸
+
+
+ +
+
+
+ – + 가로 + 칸을 눌러 시작하세요. +
+ +
+
+ +
+ +
+ + + + + +
+ +

+ 같은 칸을 한 번 더 누르면 가로 ↔ 세로가 바뀌고, 오른쪽 단서도 그에 맞춰 바뀝니다.
+ 화살표 이동 · Space 방향 전환 · Tab 다음 단어 · Backspace 지우고 뒤로 +

+
+ + +
+
+ + + + + + +