(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=0 && c { const cells = []; for (let k=0; k 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 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'; 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 = '' + e.num + '

' + e.clue + '(' + e.answer.length + '글자 · ' + e.kind + ')

'; 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 { 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 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 { 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=0 && nc 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 { 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; })();