...
This commit is contained in:
@@ -7,7 +7,6 @@
|
||||
>
|
||||
<head>
|
||||
<link th:href="@{/css/common_game_theme.css}" rel="stylesheet" />
|
||||
<script type="text/javascript" th:src="@{/js/user.js}"></script>
|
||||
|
||||
<style>
|
||||
/* =================================
|
||||
@@ -96,19 +95,21 @@
|
||||
.tile-4 { background-color: #bbdefb; color: #333; } /* #ede0c8 (노란 베이지) -> #bbdefb (파랑) */
|
||||
|
||||
/* 8부터는 고유 색상이므로 유지 (새 테마와 잘 어울림) */
|
||||
.tile-8 { background-color: #f2b179; color: #f9f6f2; }
|
||||
.tile-16 { background-color: #f59563; color: #f9f6f2; }
|
||||
.tile-32 { background-color: #f67c5f; color: #f9f6f2; }
|
||||
.tile-64 { background-color: #f65e3b; color: #f9f6f2; }
|
||||
.tile-128 { background-color: #edcf72; color: #f9f6f2; }
|
||||
.tile-256 { background-color: #edcc61; color: #f9f6f2; }
|
||||
.tile-512 { background-color: #edc850; color: #f9f6f2; }
|
||||
.tile-1024 { background-color: #edc53f; color: #f9f6f2; }
|
||||
.tile-2048 { background-color: #edc22e; color: #f9f6f2; }
|
||||
.tile-4096 { background-color: #3c3a32; color: #f9f6f2; }
|
||||
.tile-8192 { background-color: #ff3333; color: #f9f6f2; }
|
||||
.tile-16384 { background-color: #0077cc; color: #f9f6f2; }
|
||||
.tile-32768 { background-color: #9900cc; color: #f9f6f2; }
|
||||
.tile-2 { background-color: #E3F2FD; color: #333; } /* 아주 밝은 파랑 */
|
||||
.tile-4 { background-color: #BBDEFB; color: #333; } /* 밝은 파랑 */
|
||||
.tile-8 { background-color: #90CAF9; color: #fff; } /* 파랑 */
|
||||
.tile-16 { background-color: #64B5F6; color: #fff; } /* 조금 더 짙은 파랑 */
|
||||
.tile-32 { background-color: #42A5F5; color: #fff; } /* 짙은 파랑 */
|
||||
.tile-64 { background-color: #2196F3; color: #fff; } /* 선명한 파랑 */
|
||||
.tile-128 { background-color: #1E88E5; color: #fff; } /* 더 선명한 파랑 */
|
||||
.tile-256 { background-color: #1976D2; color: #fff; } /* 깊은 파랑 */
|
||||
.tile-512 { background-color: #1565C0; color: #fff; } /* 아주 깊은 파랑 */
|
||||
.tile-1024 { background-color: #0D47A1; color: #fff; } /* 남색에 가까운 파랑 */
|
||||
.tile-2048 { background-color: #283593; color: #fff; } /* 남색 */
|
||||
.tile-4096 { background-color: #3F51B5; color: #fff; } /* 인디고 */
|
||||
.tile-8192 { background-color: #673AB7; color: #fff; } /* 딥 퍼플 */
|
||||
.tile-16384 { background-color: #4527A0; color: #fff; } /* 더 짙은 딥 퍼플 */
|
||||
.tile-32768 { background-color: #311B92; color: #fff; } /* 가장 짙은 딥 퍼플 */
|
||||
|
||||
|
||||
/* =================================
|
||||
@@ -186,29 +187,30 @@
|
||||
</head>
|
||||
<body>
|
||||
<th:block layout:fragment="content">
|
||||
<h1>2048</h1>
|
||||
<p>화살표를 터치하거나 키보드 방향키를 이용해 타일을 합쳐보세요!</p>
|
||||
<div class="game-container">
|
||||
<div class="score-container">
|
||||
<strong>점수:</strong> <span id="score">0</span>
|
||||
</div>
|
||||
<div id="game-board"></div>
|
||||
|
||||
<div id="game-over-popup" class="popup-container" style="display:none;">
|
||||
<div class="popup">
|
||||
<h2>게임 오버!</h2>
|
||||
<p>최종 점수: <span id="final-score">0</span></p>
|
||||
<input type="text" id="player-name" placeholder="이름을 입력하세요">
|
||||
<button id="save-score">점수 저장</button>
|
||||
<div class="game-body-wrapper"> <h1>2048</h1>
|
||||
<p>화살표를 터치하거나 키보드 방향키를 이용해 타일을 합쳐보세요!</p>
|
||||
<div class="game-container">
|
||||
<div class="score-container">
|
||||
<strong>점수:</strong> <span id="score">0</span>
|
||||
</div>
|
||||
<div id="game-board"></div>
|
||||
|
||||
<div id="game-over-popup" class="popup-container" style="display:none;">
|
||||
<div class="popup">
|
||||
<h2>게임 오버!</h2>
|
||||
<p>최종 점수: <span id="final-score">0</span></p>
|
||||
<input type="text" id="player-name" placeholder="이름을 입력하세요">
|
||||
<button id="save-score">점수 저장</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ranking-container">
|
||||
<h3>🏆 랭킹</h3>
|
||||
<ol id="ranking-list"></ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ranking-container">
|
||||
<h3>🏆 랭킹</h3>
|
||||
<ol id="ranking-list"></ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// ... (DOM 요소 가져오기 - 동일)
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
layout:decorate="~{layout/default_layout}"
|
||||
>
|
||||
<th:block layout:fragment="head" id="head">
|
||||
<script type="text/javascript" th:src="@{/js/user.js}"></script>
|
||||
<link th:href="@{/css/common_game_theme.css}" rel="stylesheet" />
|
||||
|
||||
<style>
|
||||
@@ -1065,8 +1064,10 @@
|
||||
|
||||
</th:block >
|
||||
<th:block layout:fragment="content">
|
||||
<div class="game-body-wrapper">
|
||||
<div id="game-container">
|
||||
<canvas id="gameCanvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
</html>
|
||||
@@ -7,8 +7,6 @@
|
||||
>
|
||||
<head layout:fragment="head" id="head">
|
||||
<link th:href="@{/css/common_game_theme.css}" rel="stylesheet" />
|
||||
<script type="text/javascript" th:src="@{/js/user.js}"></script>
|
||||
|
||||
<style>
|
||||
/* sudoku.css의 내용을 여기에 삽입 */
|
||||
body {
|
||||
@@ -614,7 +612,7 @@
|
||||
try {
|
||||
// user.js의 공통 fetchRanks 함수 호출 (스도쿠 퍼즐 ID 전달)
|
||||
// currentGameType 변수가 정의되어 있어야 합니다. 예: const currentGameType = 'sudoku';
|
||||
const currentGameType = 'sudoku';
|
||||
const currentGameType = 'SUDOKU';
|
||||
const rankings = await fetchRanks(currentGameType, currentPuzzleId);
|
||||
|
||||
rankingList.innerHTML = '';
|
||||
@@ -644,7 +642,7 @@
|
||||
|
||||
try {
|
||||
// user.js의 공통 submitRank 함수 호출
|
||||
const currentGameType = 'sudoku';
|
||||
const currentGameType = 'SUDOKU';
|
||||
await submitRank(currentGameType, currentPuzzleId, userName, secondsElapsed, null);
|
||||
|
||||
alert('랭킹이 성공적으로 등록되었습니다!');
|
||||
|
||||
@@ -6,25 +6,145 @@
|
||||
layout:decorate="~{layout/default_layout}"
|
||||
>
|
||||
<th:block layout:fragment="head" id="head">
|
||||
<script type="text/javascript" th:src="@{/js/admin-sudoku.js}"></script>
|
||||
<!-- <link th:href="@{/css/puzzle.css}" rel="stylesheet" />-->
|
||||
<link th:href="@{/css/admin-sudoku.css}" rel="stylesheet" />
|
||||
<style>
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f4f7f9;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#generate-btn {
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
#generate-btn:disabled {
|
||||
background-color: #cccccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#generate-btn:hover:not(:disabled) {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
#status-message {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
#sudoku-board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(9, 40px);
|
||||
grid-template-rows: repeat(9, 40px);
|
||||
border: 3px solid #333;
|
||||
margin: 20px auto 0;
|
||||
width: 366px; /* (40px * 9) + 6px border */
|
||||
}
|
||||
|
||||
.cell {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid #ccc;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 3x3 구역을 위한 굵은 선 */
|
||||
.cell:nth-child(3n) { border-right: 2px solid #333; }
|
||||
.cell:nth-child(9n) { border-right: none; }
|
||||
.cell:nth-of-type(n+19):nth-of-type(-n+27),
|
||||
.cell:nth-of-type(n+46):nth-of-type(-n+54) {
|
||||
border-bottom: 2px solid #333;
|
||||
}
|
||||
</style>
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const generateBtn = document.getElementById('generate-btn');
|
||||
const boardElement = document.getElementById('sudoku-board');
|
||||
const statusMessage = document.getElementById('status-message');
|
||||
|
||||
// 스도쿠 보드를 화면에 그리는 함수
|
||||
function renderBoard(puzzleString) {
|
||||
boardElement.innerHTML = ''; // 기존 보드 초기화
|
||||
if (!puzzleString || puzzleString.length !== 81) {
|
||||
statusMessage.textContent = '잘못된 퍼즐 데이터입니다.';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const char of puzzleString) {
|
||||
const cell = document.createElement('div');
|
||||
cell.classList.add('cell');
|
||||
cell.textContent = char;
|
||||
boardElement.appendChild(cell);
|
||||
}
|
||||
}
|
||||
|
||||
// 생성 버튼 클릭 이벤트 리스너
|
||||
generateBtn.addEventListener('click', async () => {
|
||||
// 버튼 비활성화 및 상태 메시지 업데이트
|
||||
generateBtn.disabled = true;
|
||||
statusMessage.textContent = '새로운 스도쿠 퍼즐을 생성 중입니다... 🧠';
|
||||
boardElement.innerHTML = ''; // 보드 비우기
|
||||
|
||||
try {
|
||||
// 백엔드 API 호출 (POST 요청)
|
||||
const response = await fetch('/puzzle/sudoku/sudoku_gen');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`서버 오류: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// 성공 시 보드 렌더링
|
||||
renderBoard(result.puzzle);
|
||||
statusMessage.textContent = `✅ 퍼즐 생성 완료! (ID: ${result.puzzleKey})`;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating Sudoku:', error);
|
||||
statusMessage.textContent = `❌ 생성 실패: ${error.message}`;
|
||||
} finally {
|
||||
// 성공/실패 여부와 관계없이 버튼 다시 활성화
|
||||
generateBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script type="text/javascript" th:src="@{/js/user.js}"></script>
|
||||
</th:block >
|
||||
<th:block layout:fragment="content">
|
||||
<div class="container">
|
||||
<h1>스도쿠 퍼즐 생성기</h1>
|
||||
<p>버튼을 누르면 새로운 스도쿠 퍼즐을 생성하여 DB에 저장하고 화면에 보여줍니다.</p>
|
||||
<button id="generate-btn">새 스도쿠 생성</button>
|
||||
<div id="status-message"></div>
|
||||
<h1>스도쿠 퍼즐 생성기</h1>
|
||||
<p>버튼을 누르면 새로운 스도쿠 퍼즐을 생성하여 DB에 저장하고 화면에 보여줍니다.</p>
|
||||
<button id="generate-btn">새 스도쿠 생성</button>
|
||||
<div id="status-message"></div>
|
||||
|
||||
<h2>생성된 퍼즐</h2>
|
||||
<div id="sudoku-board">
|
||||
</div>
|
||||
<h2>생성된 퍼즐</h2>
|
||||
<div id="sudoku-board">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</th:block>
|
||||
|
||||
@@ -6,12 +6,183 @@
|
||||
layout:decorate="~{layout/default_layout}"
|
||||
>
|
||||
<th:block layout:fragment="head" id="head">
|
||||
<script type="text/javascript" th:src="@{/js/nonogram.js}"></script>
|
||||
<link th:href="@{/css/nonogram.css}" rel="stylesheet" />
|
||||
<script th:inline="javascript">
|
||||
/**
|
||||
* ==============================================
|
||||
* 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>
|
||||
<script type="text/javascript" th:src="@{/js/user.js}"></script>
|
||||
</th:block >
|
||||
<th:block layout:fragment="content">
|
||||
<div id="puzzle-container">
|
||||
|
||||
Reference in New Issue
Block a user