';
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;
})();