874 lines
29 KiB
HTML
Raw Normal View History

2025-09-12 16:55:21 +09:00
<!DOCTYPE html>
<html
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
layout:decorate="~{layout/default_layout}"
>
<th:block layout:fragment="head" id="head">
<link th:href="@{/css/common_game_theme.css}" rel="stylesheet" />
<style>
/* === nonogram.css (게임 플레이용) === */
#board-viewport {
position: relative;
width: 100%;
max-width: 95vw; /* 화면 너비에 좀 더 맞춤 */
margin: 20px auto;
/* (★ 핵심 수정) Flexbox로 자식 요소를 중앙 정렬합니다. */
display: flex;
justify-content: center; /* 자식 요소를 수평 중앙 정렬 */
align-items: flex-start; /* 위쪽에 정렬 */
}
.reveal-img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0; /* Hidden by default */
pointer-events: none; /* Make them unclickable */
transition: opacity 1.5s ease-in-out; /* Fade animation */
transform-origin: top left; /* Align with the game board's scaling */
}
.guide-line-right {
border-right: 2px solid #999 !important;
}
.guide-line-bottom {
border-bottom: 2px solid #999 !important;
}
#game-board {
display: grid;
gap: 1px;
background-color: #999;
border: 2px solid #333;
transform-origin: top;
}
#game-controls {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
font-size: 1.2em;
flex-wrap: wrap;
gap: 15px;
}
#mode-selector {
display: flex;
gap: 5px;
border: 1px solid #ccc;
border-radius: 8px;
padding: 4px;
background-color: #f0f0f0;
}
#mode-selector label {
cursor: pointer;
user-select: none;
}
#mode-selector span {
padding: 8px 15px;
border-radius: 5px;
display: block;
transition: background-color 0.2s, color 0.2s;
}
#mode-selector input[type="radio"] {
display: none;
}
#mode-selector input[type="radio"]:checked + span {
background-color: #007bff;
color: white;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
}
#hint-btn {
padding: 8px 15px;
font-weight: bold;
cursor: pointer;
border-radius: 5px;
border: 1px solid #ccc;
}
#hint-btn:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.col-clues-container, .row-clues-container {
display: flex;
}
.row-clues-container {
flex-direction: column;
}
.puzzle-grid-container {
display: grid;
border: 2px solid #333;
}
/* nonogram.css의 .clue-cell (게임용) */
.clue-cell {
background-color: #f0f0f0;
font-weight: bold;
font-size: 14px;
box-sizing: border-box;
display: flex;
padding: 5px;
}
.row-clue {
justify-content: flex-end; /* 힌트 오른쪽 정렬 */
align-items: center;
}
.col-clue {
justify-content: center; /* 힌트 가운데 정렬 */
align-items: flex-end; /* 힌트 아래쪽 정렬 */
text-align: center;
line-height: 1.2; /* 줄 간격 */
}
/* nonogram.css의 .grid-cell (게임용) */
.grid-cell {
background-color: #fff;
border: 1px solid #ddd;
box-sizing: border-box;
cursor: pointer;
}
/* nonogram.css의 .filled (게임용) */
.grid-cell.filled {
background-color: #333;
}
.grid-cell.marked::after {
content: 'X';
color: #ff5c5c;
font-weight: bold;
font-size: 1.2em;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.grid-cell.incorrect {
background-color: #ffcccc;
animation: shake 0.5s;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
2025-09-16 18:42:55 +09:00
2025-09-12 16:55:21 +09:00
#result-modal {
background-color: white;
padding: 20px 40px;
border-radius: 10px;
text-align: center;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
#modal-title {
margin-top: 0;
font-size: 2.5em;
}
#modal-buttons button {
padding: 10px 20px;
margin: 0 10px;
font-size: 1em;
cursor: pointer;
border-radius: 5px;
border: 1px solid #ccc;
min-width: 120px;
}
#modal-buttons button.primary {
background-color: #4CAF50;
color: white;
border-color: #4CAF50;
}
.hidden {
display: none;
}
.clue-cell.completed {
color: #999; /* 색상을 회색으로 */
text-decoration: line-through; /* 취소선 */
}
.grid-cell.locked {
opacity: 0.8; /* 약간 투명하게 */
}
.grid-cell.selecting {
background-color: rgba(0, 123, 255, 0.3); /* 반투명 파란색 배경 */
border-color: rgba(0, 123, 255, 0.5);
}
/* === puzzle.css (업로드 미리보기용) - 충돌 해결됨 === */
#puzzle-container {
display: grid;
/* We will set grid-template-columns/rows with JS */
grid-gap: 2px;
margin-top: 20px;
background-color: #333;
border: 2px solid #333;
width: fit-content;
}
/* (★충돌 해결) #puzzle-container 내부의 .grid-cell (미리보기 코너 셀) */
#puzzle-container .grid-cell {
width: 25px;
height: 25px;
background-color: #f0f0f0;
text-align: center;
line-height: 25px;
font-size: 14px;
/* nonogram.css의 .grid-cell 스타일과 겹치지 않음 */
cursor: default;
border: none;
}
/* (★충돌 해결) #puzzle-container 내부의 .clue-cell (미리보기 힌트 셀) */
#puzzle-container .clue-cell {
background-color: #cce7ff;
display: flex;
justify-content: center;
align-items: center;
padding: 5px;
min-height: 25px;
font-weight: bold;
/* nonogram.css의 .clue-cell 스타일과 겹치지 않음 */
font-size: 14px;
}
.solution-cell {
width: 25px;
height: 25px;
}
/* (★충돌 해결) .filled 대신 .solution-cell.filled 사용 */
.solution-cell.filled {
background-color: #333;
}
/* .empty는 .solution-cell.empty로 사용 (upload.js 기준) */
.solution-cell.empty {
background-color: #fff;
}
#puzzle-wrapper {
position: relative; /* Needed for absolute positioning of children */
}
/* 이 ID는 nonogram.html에서 사용되지 않으므로 충돌 없음 */
#success-animation-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none; /* Allows clicking through the container */
}
#success-animation-container img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0; /* Hidden by default */
transition: opacity 1.0s ease-in-out; /* Fade animation */
}
</style>
</th:block >
<th:block layout:fragment="content">
<h1>Solve the Puzzle! 🧩</h1>
<div id="game-controls">
<div id="mode-selector">
<label>
<input type="radio" name="play-mode" value="fill" checked><span> Fill</span>
</label>
<label>
<input type="radio" name="play-mode" value="mark"><span> Mark (X)</span>
</label>
</div>
<div id="points-info">
❤️ Points: <span id="points-display">5</span>
</div>
<button id="hint-btn">Hint (-1 Point)</button>
</div>
<div id="board-viewport">
<div id="game-board">
</div>
<img id="grayscale-reveal" class="reveal-img" src="" alt="Grayscale version">
<img id="original-reveal" class="reveal-img" src="" alt="Original version">
<div id="result-overlay" class="hidden">
<div id="result-modal">
<h2 id="modal-title"></h2>
<p id="modal-message"></p>
<div id="modal-buttons">
</div>
</div>
</div>
</div>
<script th:inline="javascript">
/*<![CDATA[*/
const puzzleData = /*[[${puzzle}]]*/ null;
/*]]>*/
2025-09-16 18:42:55 +09:00
if (puzzleData) {
window.pageContext = {
pageType: 'game',
gameType: 'NONOGRAM',
contextId: puzzleData.id
};
}
2025-09-12 16:55:21 +09:00
</script>
<script type="text/javascript">
/**
* ==============================================
* nonogram.js (게임 플레이 로직)
* (★ 리팩토링: 타이머 및 랭킹 등록 기능 추가)
* ==============================================
*/
document.addEventListener('DOMContentLoaded', () => {
// 백엔드(Thymeleaf)로부터 puzzleData가 제대로 전달되었는지 확인
// 이 변수는 nonogram.html에만 존재하므로, upload.html에서는 이 로직이 실행되지 않음.
if (typeof puzzleData === 'undefined' || !puzzleData) {
// game-board가 없는 upload.html에서는 오류를 뱉지 않고 조용히 종료됨.
const gb = document.getElementById('game-board');
if (gb) {
gb.innerHTML = '<h2>오류: 퍼즐 데이터를 불러올 수 없습니다.</h2>';
}
return; // upload.html에서는 여기서 즉시 return됨.
}
// --- DOM 요소 참조 (게임 페이지 전용) ---
const modeSelector = document.getElementById('mode-selector');
const gameBoard = document.getElementById('game-board');
const pointsDisplay = document.getElementById('points-display');
const hintBtn = document.getElementById('hint-btn');
const resultOverlay = document.getElementById('result-overlay');
const modalTitle = document.getElementById('modal-title');
const modalMessage = document.getElementById('modal-message');
const modalButtons = document.getElementById('modal-buttons');
// --- (★ 수정) 게임 상태 변수 (타이머 추가) ---
let currentMode = 'fill';
let points = 5;
let isGameFinished = false;
let gameStartTime = 0; // (★ 신규) 게임 시작 시간 (ms)
let isDragging = false;
let dragAction = null;
let startCell = null;
let lastHoveredCell = null;
let currentSelection = new Set();
let affectedRows = new Set();
let affectedCols = new Set();
// --- 퍼즐 데이터 및 플레이어 진행 상황 ---
const solution = puzzleData.solutionGrid;
const numRows = solution.length;
const numCols = solution[0].length;
let playerGrid = Array(numRows).fill(0).map(() => Array(numCols).fill(0));
let lockedRows = Array(numRows).fill(false);
let lockedCols = Array(numCols).fill(false);
function updateMode() {
currentMode = document.querySelector('input[name="play-mode"]:checked').value;
}
function calculateCellSize() {
// ... (셀 크기 계산 로직 - 수정 없음) ...
const tempContainer = document.createElement('div');
tempContainer.style.position = 'absolute';
tempContainer.style.visibility = 'hidden';
const tempCell = document.createElement('div');
tempCell.className = 'clue-cell';
tempCell.textContent = '0';
tempContainer.appendChild(tempCell);
document.body.appendChild(tempContainer);
const fontHeight = tempCell.offsetHeight;
tempCell.textContent = '10';
const doubleDigitWidth = tempCell.offsetWidth;
document.body.removeChild(tempContainer);
const baseSize = Math.max(fontHeight, doubleDigitWidth, 30);
return baseSize + 10;
}
/**
* (★ 수정) drawBoard (타이머 시작점 추가)
*/
function drawBoard(cellSize) {
// ... (모든 보드 그리기 DOM 생성 로직 - 수정 없음) ...
gameBoard.style.gridTemplateColumns = `${cellSize * 2}px 1fr`;
gameBoard.style.gridTemplateRows = `${cellSize * 2}px 1fr`;
const corner = document.createElement('div');
const colCluesContainer = document.createElement('div');
colCluesContainer.className = 'col-clues-container';
const rowCluesContainer = document.createElement('div');
rowCluesContainer.className = 'row-clues-container';
const puzzleGridContainer = document.createElement('div');
puzzleGridContainer.className = 'puzzle-grid-container';
puzzleGridContainer.style.gridTemplateColumns = `repeat(${numCols}, ${cellSize}px)`;
puzzleGridContainer.style.gridTemplateRows = `repeat(${numRows}, ${cellSize}px)`;
puzzleData.colClues.forEach((clues, index) => {
const clueCell = document.createElement('div');
clueCell.className = 'clue-cell col-clue';
clueCell.id = `col-clue-${index}`;
clueCell.style.width = `${cellSize}px`;
clueCell.innerHTML = clues.join('<br>');
if ((index + 1) % 5 === 0 && index < numCols - 1) clueCell.classList.add('guide-line-right');
colCluesContainer.appendChild(clueCell);
});
puzzleData.rowClues.forEach((clues, index) => {
const clueCell = document.createElement('div');
clueCell.className = 'clue-cell row-clue';
clueCell.id = `row-clue-${index}`;
clueCell.style.height = `${cellSize}px`;
clueCell.textContent = clues.join(' ');
if ((index + 1) % 5 === 0 && index < numRows - 1) clueCell.classList.add('guide-line-bottom');
rowCluesContainer.appendChild(clueCell);
});
for (let r = 0; r < numRows; r++) {
for (let c = 0; c < numCols; c++) {
const cell = document.createElement('div');
cell.className = 'grid-cell';
cell.dataset.row = r;
cell.dataset.col = c;
if ((c + 1) % 5 === 0 && c < numCols - 1) cell.classList.add('guide-line-right');
if ((r + 1) % 5 === 0 && r < numRows - 1) cell.classList.add('guide-line-bottom');
puzzleGridContainer.appendChild(cell);
}
}
gameBoard.appendChild(corner);
gameBoard.appendChild(colCluesContainer);
gameBoard.appendChild(rowCluesContainer);
gameBoard.appendChild(puzzleGridContainer);
// (★ 신규) 보드가 그려지는 시점을 게임 시작 시간으로 기록
gameStartTime = Date.now();
attachEventListeners(puzzleGridContainer);
}
/**
* 화면 너비에 맞춰 게임 보드 전체를 비율 그대로 축소/확대합니다.
*/
function fitBoardToScreen() {
const viewport = document.getElementById('board-viewport');
const board = document.getElementById('game-board');
board.style.transform = 'scale(1)';
const boardRect = board.getBoundingClientRect();
const viewportRect = viewport.getBoundingClientRect();
if (boardRect.width > viewportRect.width) {
const scale = viewportRect.width / boardRect.width;
board.style.transform = `scale(${scale})`;
viewport.style.height = `${boardRect.height * scale}px`;
} else {
board.style.transform = 'scale(1)';
viewport.style.height = `${boardRect.height}px`;
}
}
/**
* 셀의 상태를 변경하는 유일한 함수. 모든 사용자 입력은 이 함수를 거칩니다.
*/
function updateCellState(cell, action) {
if (isGameFinished) return;
const row = parseInt(cell.dataset.row);
const col = parseInt(cell.dataset.col);
if (lockedRows[row] || lockedCols[col]) return;
affectedRows.add(row);
affectedCols.add(col);
const currentState = playerGrid[row][col];
let newState = currentState;
if (action === 'fill') {
if (solution[row][col] === 0) {
points--;
updatePointsDisplay();
cell.classList.add('incorrect');
setTimeout(() => cell.classList.remove('incorrect'), 500);
if (points <= 0) triggerGameOver();
return;
}
newState = 1;
} else if (action === 'mark') {
newState = -1;
} else if (action === 'clear') {
newState = 0;
}
if (currentState !== newState) {
playerGrid[row][col] = newState;
cell.classList.toggle('filled', newState === 1);
cell.classList.toggle('marked', newState === -1);
}
}
// --- (이벤트 리스너 및 드래그/터치 핸들러) ---
// (★ 수정 없음) attachEventListeners, handleDragStart, handleDragMove, handleDragEnd
// (★ 수정 없음) updateSelectionVisuals, clearSelectionVisuals
// --- (모두 동일하게 유지) ---
function attachEventListeners(grid) {
grid.addEventListener('mousedown', (e) => handleDragStart(e));
grid.addEventListener('mouseover', (e) => handleDragMove(e));
grid.addEventListener('contextmenu', (e) => e.preventDefault());
grid.addEventListener('touchstart', (e) => handleDragStart(e), { passive: false });
grid.addEventListener('touchmove', (e) => handleDragMove(e), { passive: false });
}
window.addEventListener('mouseup', () => handleDragEnd());
window.addEventListener('touchend', () => handleDragEnd());
modeSelector.addEventListener('change', updateMode);
function handleDragStart(e) {
if (isGameFinished || !e.target.classList.contains('grid-cell')) return;
isDragging = true;
e.preventDefault();
const cell = e.target;
const currentState = playerGrid[parseInt(cell.dataset.row)][parseInt(cell.dataset.col)];
if (e.type === 'mousedown') {
if (e.button === 0) {
dragAction = (currentState === 1) ? 'clear' : 'fill';
document.querySelector('input[name="play-mode"][value="fill"]').checked = true;
} else if (e.button === 2) {
dragAction = (currentState === -1) ? 'clear' : 'mark';
document.querySelector('input[name="play-mode"][value="mark"]').checked = true;
}
} else {
const currentMode = document.querySelector('input[name="play-mode"]:checked').value;
if (currentMode === 'fill') {
dragAction = (currentState === 1) ? 'clear' : 'fill';
} else {
dragAction = (currentState === -1) ? 'clear' : 'mark';
}
}
startCell = { row: parseInt(cell.dataset.row), col: parseInt(cell.dataset.col) };
lastHoveredCell = startCell;
updateSelectionVisuals();
}
function handleDragMove(e) {
if (!isDragging) return;
e.preventDefault();
const target = (e.touches)
? document.elementFromPoint(e.touches[0].clientX, e.touches[0].clientY)
: e.target;
if (target && target.classList.contains('grid-cell')) {
const row = parseInt(target.dataset.row);
const col = parseInt(target.dataset.col);
if (row !== lastHoveredCell.row || col !== lastHoveredCell.col) {
lastHoveredCell = { row, col };
updateSelectionVisuals();
}
}
}
function handleDragEnd() {
if (!isDragging) return;
currentSelection.forEach(cell => updateCellState(cell, dragAction));
clearSelectionVisuals();
if (dragAction === 'fill' || dragAction === 'clear') {
checkAndLockCompletedLines(affectedRows, affectedCols);
}
checkWinCondition();
isDragging = false;
dragAction = null;
startCell = null;
lastHoveredCell = null;
currentSelection.clear();
affectedRows.clear();
affectedCols.clear();
}
/**
* 드래그 중인 사각형 영역을 계산하고 시각적으로 업데이트하는 함수
*/
function updateSelectionVisuals() {
const newSelection = new Set();
if (!startCell || !lastHoveredCell) return;
const r1 = Math.min(startCell.row, lastHoveredCell.row);
const r2 = Math.max(startCell.row, lastHoveredCell.row);
const c1 = Math.min(startCell.col, lastHoveredCell.col);
const c2 = Math.max(startCell.col, lastHoveredCell.col);
for (let r = r1; r <= r2; r++) {
for (let c = c1; c <= c2; c++) {
const cell = document.querySelector(`.grid-cell[data-row='${r}'][data-col='${c}']`);
if (cell) newSelection.add(cell);
}
}
currentSelection.forEach(cell => {
if (!newSelection.has(cell)) cell.classList.remove('selecting');
});
newSelection.forEach(cell => {
if (!currentSelection.has(cell)) cell.classList.add('selecting');
});
currentSelection = newSelection;
}
/**
* 모든 시각적 피드백을 제거하는 함수
*/
function clearSelectionVisuals() {
currentSelection.forEach(cell => cell.classList.remove('selecting'));
}
/**
* 특정 행이 '칠해야 할 칸'을 모두 만족했는지 검사
*/
function isRowComplete(rowIndex) {
for (let c = 0; c < numCols; c++) {
if (solution[rowIndex][c] === 1 && playerGrid[rowIndex][c] !== 1) return false;
}
return true;
}
/**
* 특정 열이 '칠해야 할 칸'을 모두 만족했는지 검사
*/
function isColComplete(colIndex) {
for (let r = 0; r < numRows; r++) {
if (solution[r][colIndex] === 1 && playerGrid[r][colIndex] !== 1) return false;
}
return true;
}
// --- (게임 완료 체크 로직) ---
/**
* 완성된 라인을 확인하고 잠금 처리 및 스타일 변경 (X 자동 완성 없음)
*/
function checkAndLockCompletedLines(rowsToCheck, colsToCheck) {
rowsToCheck.forEach(r => {
if (!lockedRows[r] && isRowComplete(r)) {
lockedRows[r] = true;
document.getElementById(`row-clue-${r}`).classList.add('completed');
for (let c = 0; c < numCols; c++) {
document.querySelector(`.grid-cell[data-row='${r}'][data-col='${c}']`).classList.add('locked');
}
}
});
colsToCheck.forEach(c => {
if (!lockedCols[c] && isColComplete(c)) {
lockedCols[c] = true;
document.getElementById(`col-clue-${c}`).classList.add('completed');
for (let r = 0; r < numRows; r++) {
document.querySelector(`.grid-cell[data-row='${r}'][data-col='${c}']`).classList.add('locked');
}
}
});
}
function checkWinCondition() {
if (isGameFinished) return;
for (let r = 0; r < numRows; r++) {
for (let c = 0; c < numCols; c++) {
const playerState = (playerGrid[r][c] === 1) ? 1 : 0;
if (playerState !== solution[r][c]) return;
}
}
triggerGameSuccess();
}
function updatePointsDisplay() {
pointsDisplay.textContent = points;
hintBtn.disabled = (points <= 0 || isGameFinished);
}
2025-09-16 18:42:55 +09:00
2025-09-12 16:55:21 +09:00
/**
* 게임 실패 처리
*/
function triggerGameOver() {
if (isGameFinished) return;
isGameFinished = true;
document.querySelector('.puzzle-grid-container').style.pointerEvents = 'none';
hintBtn.disabled = true;
2025-09-16 18:42:55 +09:00
showGameSuccessModal({
gameType: 'NONOGRAM',
contextId: puzzleData.id,
successMessage: `퍼즐 완성! (시간: ${completionTimeSeconds}초, 힌트 사용: ${hintsUsed}개)`,
primaryScore: completionTimeSeconds,
secondaryScore: hintsUsed
2025-09-12 16:55:21 +09:00
});
}
/**
* (★ 수정) 게임 성공 처리 (타이머 계산 및 랭킹 버튼 추가)
*/
function triggerGameSuccess() {
if (isGameFinished) return;
isGameFinished = true;
// (★ 신규) 게임 완료 시간 및 힌트 사용량 계산
const completionTimeSeconds = Math.floor((Date.now() - gameStartTime) / 1000);
const hintsUsed = 5 - points; // 힌트 사용량 = 5 - 남은 포인트
// --- 요소 참조 및 상호작용 비활성화 ---
const viewport = document.getElementById('board-viewport');
const puzzleGridContainer = document.querySelector('.puzzle-grid-container');
const grayscaleImg = document.getElementById('grayscale-reveal');
const originalImg = document.getElementById('original-reveal');
puzzleGridContainer.style.pointerEvents = 'none';
hintBtn.disabled = true;
// --- 애니메이션 위치 및 크기 계산 ---
const gridRect = puzzleGridContainer.getBoundingClientRect();
const viewportRect = viewport.getBoundingClientRect();
const top = gridRect.top - viewportRect.top;
const left = gridRect.left - viewportRect.left; // (오타 수정) viewportRect.top -> viewportRect.left
[grayscaleImg, originalImg].forEach(img => {
img.style.top = `${top}px`;
img.style.left = `${left}px`;
img.style.width = `${gridRect.width}px`;
img.style.height = `${gridRect.height}px`;
2025-09-16 18:42:55 +09:00
// [수정] Base64 대신 URL 경로를 사용하도록 변경
img.src = (img.id === 'grayscale-reveal')
? `/puzzle/images/${puzzleData.grayscaleImageFile}`
: `/puzzle/images/${puzzleData.originalImageFile}`;
2025-09-12 16:55:21 +09:00
});
// --- 애니메이션 순차 실행 ---
setTimeout(() => {
grayscaleImg.style.opacity = '1';
setTimeout(() => {
originalImg.style.opacity = '1';
setTimeout(() => {
// (★ 수정) 모달 버튼 설정에 "랭킹 등록" 버튼 추가
2025-09-16 18:42:55 +09:00
showGameSuccessModal({
gameType: 'NONOGRAM',
contextId: puzzleData.id,
successMessage: `퍼즐 완성! (시간: ${completionTimeSeconds}초, 힌트 사용: ${hintsUsed}개)`,
primaryScore: completionTimeSeconds,
secondaryScore: hintsUsed
2025-09-12 16:55:21 +09:00
});
}, 2000);
}, 2000);
}, 500);
}
// 힌트 버튼 클릭 이벤트 처리
hintBtn.addEventListener('click', () => {
if (points <= 0 || isGameFinished) return;
points--;
updatePointsDisplay();
const hintCandidates = [];
for (let r = 0; r < numRows; r++) {
for (let c = 0; c < numCols; c++) {
if (solution[r][c] === 1 && playerGrid[r][c] !== 1) {
hintCandidates.push({ r, c });
}
}
}
if (hintCandidates.length > 0) {
const hint = hintCandidates[Math.floor(Math.random() * hintCandidates.length)];
const cellToReveal = document.querySelector(`.grid-cell[data-row='${hint.r}'][data-col='${hint.c}']`);
updateCellState(cellToReveal, 'fill');
const hintAffectedRows = new Set([hint.r]);
const hintAffectedCols = new Set([hint.c]);
checkAndLockCompletedLines(hintAffectedRows, hintAffectedCols);
checkWinCondition();
} else {
2025-09-16 18:42:55 +09:00
showAlert("알림","더 이상 사용할 힌트가 없습니다!");
2025-09-12 16:55:21 +09:00
points++;
updatePointsDisplay();
}
if (points <= 0 && !isGameFinished) {
triggerGameOver();
}
});
// --- 초기 실행 ---
const optimalCellSize = calculateCellSize();
drawBoard(optimalCellSize);
updatePointsDisplay();
updateMode();
requestAnimationFrame(() => {
fitBoardToScreen();
window.addEventListener('resize', fitBoardToScreen);
});
});
/**
* ==============================================
* upload.js (업로드 페이지 로직)
* (★ 리팩토링: 통합 API 경로 사용)
* ==============================================
*/
let currentPuzzleData = null; // 업로드 성공 시 퍼즐 데이터 저장
// (★ 수정 없음) 업로드 페이지용 퍼즐 미리보기 그리기 함수
function drawPuzzle(puzzleData) {
const container = document.getElementById('puzzle-container');
container.innerHTML = '';
const { solutionGrid, rowClues, colClues } = puzzleData;
const numRows = solutionGrid.length;
const numCols = solutionGrid[0].length;
container.style.gridTemplateColumns = `auto repeat(${numCols}, 1fr)`;
container.style.gridTemplateRows = `auto repeat(${numRows}, 1fr)`;
// 1. 코너
const corner = document.createElement('div');
corner.className = 'grid-cell';
container.appendChild(corner);
// 2. 열 힌트
for (const clues of colClues) {
const clueCell = document.createElement('div');
clueCell.className = 'clue-cell';
clueCell.innerHTML = clues.join('<br>');
container.appendChild(clueCell);
}
// 3. 행 힌트 및 정답 그리드
for (let i = 0; i < numRows; i++) {
const rowClueCell = document.createElement('div');
rowClueCell.className = 'clue-cell';
rowClueCell.textContent = rowClues[i].join(' ');
container.appendChild(rowClueCell);
for (let j = 0; j < numCols; j++) {
const cell = document.createElement('div');
cell.className = 'solution-cell';
if (solutionGrid[i][j] === 1) {
cell.classList.add('filled');
} else {
cell.classList.add('empty');
}
container.appendChild(cell);
}
}
}
</script>
</th:block>
</html>