1052 lines
37 KiB
HTML
1052 lines
37 KiB
HTML
|
|
<!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); }
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
#result-overlay {
|
||
|
|
position: fixed; /* 화면 전체에 고정 */
|
||
|
|
top: 0;
|
||
|
|
left: 0;
|
||
|
|
width: 100vw; /* 뷰포트 너비 100% */
|
||
|
|
height: 100vh; /* 뷰포트 높이 100% */
|
||
|
|
background-color: rgba(0, 0, 0, 0.75); /* 반투명 검은 배경 */
|
||
|
|
display: flex;
|
||
|
|
justify-content: center;
|
||
|
|
align-items: center;
|
||
|
|
z-index: 100;
|
||
|
|
opacity: 0;
|
||
|
|
pointer-events: none;
|
||
|
|
transition: opacity 0.3s ease-in-out;
|
||
|
|
}
|
||
|
|
#result-overlay.visible {
|
||
|
|
opacity: 1;
|
||
|
|
pointer-events: auto;
|
||
|
|
}
|
||
|
|
#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;
|
||
|
|
/*]]>*/
|
||
|
|
</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);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* (★ 신규) 노노그램 랭킹 등록을 처리하는 함수
|
||
|
|
* 이 함수는 user.js에 정의된 공통 submitRank 함수를 호출합니다.
|
||
|
|
*/
|
||
|
|
async function submitNonogramRank(completionTime, hintsUsed) {
|
||
|
|
const playerName = prompt("랭킹에 등록할 이름을 입력하세요:", "Player");
|
||
|
|
if (!playerName || playerName.trim() === "") return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
// (★ 신규) user.js의 공통 submitRank 함수 호출
|
||
|
|
// 주 점수(primaryScore) = 완료 시간(초) (낮을수록 좋음)
|
||
|
|
// 보조 점수(secondaryScore) = 사용한 힌트 수(5-남은포인트) (낮을수록 좋음)
|
||
|
|
await submitRank(
|
||
|
|
'NONOGRAM', // GameType
|
||
|
|
puzzleData.id, // ContextId (퍼즐 고유 ID)
|
||
|
|
playerName.trim(), // playerName
|
||
|
|
completionTime, // primaryScore (시간)
|
||
|
|
hintsUsed // secondaryScore (힌트 사용 횟수)
|
||
|
|
);
|
||
|
|
|
||
|
|
alert("랭킹이 등록되었습니다!");
|
||
|
|
// 랭킹 등록 버튼 비활성화 (중복 제출 방지)
|
||
|
|
const submitBtn = document.getElementById('modal-submit-rank-btn');
|
||
|
|
if (submitBtn) submitBtn.disabled = true;
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Rank submission failed:", error);
|
||
|
|
alert("랭킹 등록에 실패했습니다: " + error.message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* (★ 수정) 성공/실패 모달 (랭킹 등록 버튼 추가를 위해 ID 할당 기능 추가)
|
||
|
|
*/
|
||
|
|
function showResultModal(config) {
|
||
|
|
modalTitle.textContent = config.title;
|
||
|
|
modalMessage.textContent = config.message;
|
||
|
|
modalButtons.innerHTML = '';
|
||
|
|
config.buttons.forEach(btnInfo => {
|
||
|
|
const button = document.createElement('button');
|
||
|
|
button.textContent = btnInfo.text;
|
||
|
|
button.className = btnInfo.class || '';
|
||
|
|
button.onclick = btnInfo.action;
|
||
|
|
if (btnInfo.id) button.id = btnInfo.id; // (★ 신규) 버튼 ID 할당 기능
|
||
|
|
modalButtons.appendChild(button);
|
||
|
|
});
|
||
|
|
resultOverlay.classList.remove('hidden');
|
||
|
|
setTimeout(() => resultOverlay.classList.add('visible'), 10);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 게임 실패 처리
|
||
|
|
*/
|
||
|
|
function triggerGameOver() {
|
||
|
|
if (isGameFinished) return;
|
||
|
|
isGameFinished = true;
|
||
|
|
document.querySelector('.puzzle-grid-container').style.pointerEvents = 'none';
|
||
|
|
hintBtn.disabled = true;
|
||
|
|
showResultModal({
|
||
|
|
title: 'Failure', message: '포인트를 모두 사용했습니다.', buttons: [
|
||
|
|
{ text: '재시도 (Retry)', class: 'primary', action: () => window.location.reload() },
|
||
|
|
{ text: '홈으로 (Home)', action: () => window.location.href = '/' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* (★ 수정) 게임 성공 처리 (타이머 계산 및 랭킹 버튼 추가)
|
||
|
|
*/
|
||
|
|
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`;
|
||
|
|
img.src = (img.id === 'grayscale-reveal') ? puzzleData.grayscaleImage : puzzleData.originalImage;
|
||
|
|
});
|
||
|
|
|
||
|
|
// --- 애니메이션 순차 실행 ---
|
||
|
|
setTimeout(() => {
|
||
|
|
grayscaleImg.style.opacity = '1';
|
||
|
|
setTimeout(() => {
|
||
|
|
originalImg.style.opacity = '1';
|
||
|
|
setTimeout(() => {
|
||
|
|
// (★ 수정) 모달 버튼 설정에 "랭킹 등록" 버튼 추가
|
||
|
|
showResultModal({
|
||
|
|
title: 'Success! 🎉',
|
||
|
|
message: `퍼즐을 완성했습니다! (시간: ${completionTimeSeconds}초, 힌트 사용: ${hintsUsed}개)`,
|
||
|
|
buttons: [
|
||
|
|
{
|
||
|
|
text: '랭킹 등록',
|
||
|
|
class: 'primary',
|
||
|
|
id: 'modal-submit-rank-btn', // (★ 신규) 랭킹 제출 버튼
|
||
|
|
action: () => submitNonogramRank(completionTimeSeconds, hintsUsed)
|
||
|
|
},
|
||
|
|
{ text: '다른 문제 풀기', action: () => window.location.href = '/puzzle/play' },
|
||
|
|
{ text: '홈으로', action: () => window.location.href = '/' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
}, 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 {
|
||
|
|
alert("더 이상 사용할 힌트가 없습니다!");
|
||
|
|
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 showSuccessAnimation() {
|
||
|
|
if (!currentPuzzleData) return;
|
||
|
|
|
||
|
|
const puzzleContainer = document.getElementById('puzzle-container');
|
||
|
|
const grayscaleImg = document.getElementById('grayscale-reveal');
|
||
|
|
const originalImg = document.getElementById('original-reveal');
|
||
|
|
|
||
|
|
grayscaleImg.src = currentPuzzleData.grayscaleImage;
|
||
|
|
originalImg.src = currentPuzzleData.originalImage;
|
||
|
|
|
||
|
|
puzzleContainer.style.transition = 'opacity 0.5s';
|
||
|
|
puzzleContainer.style.opacity = '0';
|
||
|
|
grayscaleImg.style.opacity = '1';
|
||
|
|
|
||
|
|
setTimeout(() => {
|
||
|
|
grayscaleImg.style.opacity = '0';
|
||
|
|
originalImg.style.opacity = '1';
|
||
|
|
}, 2000);
|
||
|
|
}
|
||
|
|
|
||
|
|
// (★ 수정 없음) 업로드 페이지용 퍼즐 미리보기 그리기 함수
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
// upload.js의 DOMContentLoaded 리스너
|
||
|
|
document.addEventListener('DOMContentLoaded', () => {
|
||
|
|
|
||
|
|
const createBtn = document.getElementById('createBtn');
|
||
|
|
|
||
|
|
// createBtn이 없는 nonogram.html(게임 페이지)에서는 이 리스너가 아무것도 실행하지 않음.
|
||
|
|
if (!createBtn) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// (업로드 페이지 전용 로직)
|
||
|
|
createBtn.addEventListener('click', async () => {
|
||
|
|
const uploader = document.getElementById('imageUploader');
|
||
|
|
const statusDiv = document.getElementById('status');
|
||
|
|
const puzzleContainer = document.getElementById('puzzle-container');
|
||
|
|
const testSuccessBtn = document.getElementById('test-success-btn');
|
||
|
|
const deleteBtn = document.getElementById('delete-btn');
|
||
|
|
const playBtn = document.getElementById('play-btn');
|
||
|
|
|
||
|
|
if (uploader.files.length === 0) {
|
||
|
|
statusDiv.textContent = '이미지 파일을 선택해주세요.';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const imageFile = uploader.files[0];
|
||
|
|
const formData = new FormData();
|
||
|
|
formData.append('imageFile', imageFile);
|
||
|
|
|
||
|
|
statusDiv.textContent = '문제를 생성하는 중...';
|
||
|
|
puzzleContainer.innerHTML = '';
|
||
|
|
|
||
|
|
try {
|
||
|
|
// (★ 수정) API 경로 변경 -> 통합 컨트롤러의 /puzzle/upload.bjx 호출
|
||
|
|
const response = await fetch('/puzzle/upload.bjx', {
|
||
|
|
method: 'POST',
|
||
|
|
body: formData,
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
const puzzleData = await response.json();
|
||
|
|
statusDiv.textContent = '문제 생성 성공!';
|
||
|
|
drawPuzzle(puzzleData); // 미리보기 그리기
|
||
|
|
|
||
|
|
currentPuzzleData = puzzleData;
|
||
|
|
testSuccessBtn.addEventListener('click', showSuccessAnimation);
|
||
|
|
|
||
|
|
testSuccessBtn.style.display = 'inline-block';
|
||
|
|
deleteBtn.style.display = 'inline-block';
|
||
|
|
playBtn.style.display = 'inline-block';
|
||
|
|
} else {
|
||
|
|
const errorMessage = await response.text();
|
||
|
|
statusDiv.textContent = `생성 실패: ${errorMessage}`;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('네트워크 오류:', error);
|
||
|
|
statusDiv.textContent = '서버와 통신 중 오류가 발생했습니다.';
|
||
|
|
}
|
||
|
|
|
||
|
|
deleteBtn.addEventListener('click', async () => {
|
||
|
|
if (!currentPuzzleData || !currentPuzzleData.id) {
|
||
|
|
alert('삭제할 퍼즐이 선택되지 않았습니다.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!confirm('정말로 이 퍼즐을 삭제하시겠습니까?')) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
// (★ 수정) API 경로 변경 -> 통합 컨트롤러의 /puzzle/{id}.bjx 호출
|
||
|
|
const response = await fetch(`/puzzle/${currentPuzzleData.id}.bjx`, {
|
||
|
|
method: 'DELETE',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
statusDiv.textContent = '퍼즐이 성공적으로 삭제되었습니다.';
|
||
|
|
puzzleContainer.innerHTML = '';
|
||
|
|
// (버그 수정) success-animation-container 내부의 img src를 초기화해야 함
|
||
|
|
document.getElementById('grayscale-reveal').src = "";
|
||
|
|
document.getElementById('original-reveal').src = "";
|
||
|
|
|
||
|
|
testSuccessBtn.style.display = 'none';
|
||
|
|
deleteBtn.style.display = 'none';
|
||
|
|
playBtn.style.display = 'none';
|
||
|
|
currentPuzzleData = null;
|
||
|
|
} else {
|
||
|
|
statusDiv.textContent = `삭제 실패: 서버 오류 (${response.status})`;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('삭제 중 네트워크 오류:', error);
|
||
|
|
statusDiv.textContent = '삭제 중 오류가 발생했습니다.';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
playBtn.addEventListener('click', () => {
|
||
|
|
if (currentPuzzleData && currentPuzzleData.id) {
|
||
|
|
// (★ 수정 없음) 이 경로는 PuzzleController의 페이지 서빙 경로와 일치하므로 올바름.
|
||
|
|
window.location.href = `/puzzle/play/${currentPuzzleData.id}`;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
</th:block>
|
||
|
|
</html>
|