This commit is contained in:
2025-09-12 18:01:23 +09:00
parent 19c5d5473f
commit cc43ea8e0a
34 changed files with 461 additions and 3520 deletions
@@ -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">