...
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
layout:decorate="~{layout/default_layout}">
|
||||
<th:block layout:fragment="content" id="content">
|
||||
<section id="banner"
|
||||
th:styleappend="${randomBannerImage != null} ? 'background-image: url(\'' + @{${randomBannerImage}} + '\');' : ''">
|
||||
th:styleappend="${randomBannerImage != null} ? |background-image: url('${apiBaseUrl}${randomBannerImage}');| : ''">
|
||||
<header>
|
||||
<h2>Bum's: <em>짧은 헛소리 혹은 기사?! 링크 있으면 링크까지</a></em></h2>
|
||||
<a href="#" class="button">더 읽으쉴?!<br>[Read More Gibberish]</a>
|
||||
@@ -38,7 +38,7 @@
|
||||
<section th:each="post : ${Posts}">
|
||||
<div class="box post" th:id="${post.id}" onclick="goToViewer(this)" style="cursor: pointer;">
|
||||
<span class="image left">
|
||||
<img th:src="${post.thumb != null and not #strings.isEmpty(post.thumb)} ? @{${post.thumb}} : @{/images/pic01.jpg}" alt="Post Thumbnail" />
|
||||
<img th:src="${post.thumb != null and not #strings.isEmpty(post.thumb)} ? ${apiBaseUrl + post.thumb} : @{/images/pic01.jpg}" alt="Post Thumbnail" />
|
||||
</span>
|
||||
|
||||
<div class="inner">
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<section th:each="post : ${postsPage.content}">
|
||||
<div class="box post" th:id="${post.id}">
|
||||
<a href="javascript:void(0);" th:onclick="'goToViewer(this.parentNode)'" class="image left">
|
||||
<img th:src="${post.thumb != null and not #strings.isEmpty(post.thumb)} ? @{${post.thumb}} : @{/images/pic01.jpg}" alt="Post Thumbnail" />
|
||||
<img th:src="${post.thumb != null and not #strings.isEmpty(post.thumb)} ? ${apiBaseUrl + post.thumb} : @{/images/pic01.jpg}" alt="Post Thumbnail" />
|
||||
</a>
|
||||
<div class="inner">
|
||||
<h3 style="display: flex; justify-content: space-between; align-items: center;">
|
||||
|
||||
@@ -9,21 +9,7 @@
|
||||
<link th:href="@{/css/common_game_theme.css}" rel="stylesheet" />
|
||||
|
||||
<style>
|
||||
/* =================================
|
||||
기본 및 전체 레이아웃 (수정됨)
|
||||
================================= */
|
||||
body {
|
||||
/* (★ 삭제) font-family, text-align, background-color, color, margin, padding
|
||||
-> 이 속성들은 모두 common_game_theme.css에서 관리합니다.
|
||||
*/
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 15vw; /* 2048 고유의 큰 폰트 크기는 유지 */
|
||||
margin: 20px 0;
|
||||
/* (★ 삭제) color 속성 삭제 -> common_game_theme에서 상속 */
|
||||
}
|
||||
|
||||
.score-container {
|
||||
font-size: 24px;
|
||||
@@ -36,6 +22,7 @@
|
||||
#game-board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-rows: repeat(4, 1fr); /* <-- 이 줄을 추가하세요! */
|
||||
grid-gap: 2vw;
|
||||
width: 95vw;
|
||||
max-width: 500px; /* (★ 수정) 400px -> 500px (다른 게임과 통일) */
|
||||
@@ -78,6 +65,7 @@
|
||||
background-color: #eceff1; /* #cdc1b4 (갈색) -> #eceff1 (밝은 블루 그레이) */
|
||||
|
||||
font-size: 5vw;
|
||||
line-height: 1; /* <-- 이 줄을 추가하세요! */
|
||||
}
|
||||
|
||||
@media (min-width: 481px) {
|
||||
@@ -95,8 +83,6 @@
|
||||
.tile-4 { background-color: #bbdefb; color: #333; } /* #ede0c8 (노란 베이지) -> #bbdefb (파랑) */
|
||||
|
||||
/* 8부터는 고유 색상이므로 유지 (새 테마와 잘 어울림) */
|
||||
.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; } /* 짙은 파랑 */
|
||||
@@ -151,37 +137,6 @@
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* =================================
|
||||
랭킹 리스트 (테마 적용)
|
||||
================================= */
|
||||
.ranking-container {
|
||||
/*
|
||||
(★ 참고) 이 컨테이너는 common_game_theme.css에서
|
||||
.game-card 스타일(흰색 배경, 그림자, 패딩)을 이미 적용받습니다.
|
||||
여기서는 내부 정렬만 담당합니다.
|
||||
*/
|
||||
width: 100%;
|
||||
max-width: 500px; /* 공통 테마와 동일하게 설정 (중복 선언이지만 명확성을 위해 둠) */
|
||||
margin: 30px auto;
|
||||
text-align: left;
|
||||
}
|
||||
.ranking-container h3 {
|
||||
text-align: center;
|
||||
}
|
||||
#ranking-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
#ranking-list li {
|
||||
/* (★ 수정) 배경색 변경 */
|
||||
background-color: #f0f4f8; /* #eee4da (베이지) -> #f0f4f8 (밝은 회색) */
|
||||
margin-bottom: 5px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
@@ -204,15 +159,13 @@
|
||||
<button id="save-score">점수 저장</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ranking-container">
|
||||
<h3>🏆 랭킹</h3>
|
||||
<ol id="ranking-list"></ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
window.pageContext = { pageType: 'game', gameType: 'GAME_2048', contextId: null };
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// ... (DOM 요소 가져오기 - 동일)
|
||||
const gameBoard = document.getElementById('game-board');
|
||||
const scoreDisplay = document.getElementById('score');
|
||||
@@ -220,7 +173,6 @@
|
||||
const finalScoreDisplay = document.getElementById('final-score');
|
||||
const playerNameInput = document.getElementById('player-name');
|
||||
const saveScoreButton = document.getElementById('save-score');
|
||||
const rankingList = document.getElementById('ranking-list');
|
||||
|
||||
// (★ 수정) 게임 ID 대신, 공통 Enum 타입 문자열 사용
|
||||
const currentGameType = 'GAME_2048'; // (GameType.GAME_2048과 일치)
|
||||
@@ -352,8 +304,17 @@
|
||||
addNumber();
|
||||
updateBoard();
|
||||
if (isGameOver()) {
|
||||
finalScoreDisplay.textContent = score;
|
||||
gameOverPopup.style.display = 'flex';
|
||||
// ▼▼▼ 기존 팝업 대신 통합 모달 호출 ▼▼▼
|
||||
showGameSuccessModal({
|
||||
gameType: 'GAME_2048',
|
||||
contextId: null,
|
||||
successMessage: `최종 점수 ${score}점을 달성했습니다!`,
|
||||
primaryScore: score,
|
||||
secondaryScore: null
|
||||
});
|
||||
|
||||
// 게임 보드 리셋 로직은 모달이 닫힐 때 처리하거나 여기에 남겨둘 수 있습니다.
|
||||
// 예: initializeBoard(); // 즉시 리셋
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -400,7 +361,7 @@
|
||||
// ----- 랭킹 API 연동 -----
|
||||
saveScoreButton.addEventListener('click', async () => {
|
||||
const playerName = playerNameInput.value.trim();
|
||||
if (playerName === "") return alert("이름을 입력해주세요.");
|
||||
if (playerName === "") return showAlert("알림","이름을 입력해주세요.");
|
||||
|
||||
try {
|
||||
// (★ 수정) user.js의 공통 submitRank 함수 호출
|
||||
@@ -410,44 +371,14 @@
|
||||
gameOverPopup.style.display = 'none';
|
||||
playerNameInput.value = '';
|
||||
score = 0;
|
||||
updateRankingList(); // 랭킹 리스트 새로고침
|
||||
initializeBoard(); // 새 게임 시작
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error submitting rank:', error);
|
||||
alert('랭킹 등록 중 오류가 발생했습니다: ' + error.message);
|
||||
showAlert("알림",'랭킹 등록 중 오류가 발생했습니다: ' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* (★ 수정) user.js의 공통 fetchRanks 함수를 사용하도록 수정
|
||||
*/
|
||||
async function updateRankingList() {
|
||||
rankingList.innerHTML = '<li>로딩 중...</li>';
|
||||
try {
|
||||
// (★ 수정) user.js의 공통 fetchRanks 함수 호출
|
||||
const rankings = await fetchRanks(currentGameType, currentContextId);
|
||||
|
||||
rankingList.innerHTML = ''; // 리스트 비우기
|
||||
if (!rankings || rankings.length === 0) {
|
||||
rankingList.innerHTML = '<li>등록된 랭킹이 없습니다.</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
rankings.forEach((rank, index) => {
|
||||
const li = document.createElement('li');
|
||||
// (★ 수정) 공통 모델(GameRank)의 필드명(playerName, primaryScore)을 사용
|
||||
li.innerHTML = `<span>${index + 1}. ${rank.playerName}</span><strong>${rank.primaryScore}점</strong>`;
|
||||
rankingList.appendChild(li);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching ranks:', error);
|
||||
rankingList.innerHTML = '<li>랭킹을 불러올 수 없습니다.</li>';
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 게임 시작 -----
|
||||
updateRankingList();
|
||||
initializeBoard();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -168,25 +168,7 @@
|
||||
}
|
||||
|
||||
|
||||
#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;
|
||||
@@ -350,6 +332,14 @@
|
||||
/*<![CDATA[*/
|
||||
const puzzleData = /*[[${puzzle}]]*/ null;
|
||||
/*]]>*/
|
||||
|
||||
if (puzzleData) {
|
||||
window.pageContext = {
|
||||
pageType: 'game',
|
||||
gameType: 'NONOGRAM',
|
||||
contextId: puzzleData.id
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
@@ -700,55 +690,9 @@
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -759,11 +703,12 @@
|
||||
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 = '/' }
|
||||
]
|
||||
showGameSuccessModal({
|
||||
gameType: 'NONOGRAM',
|
||||
contextId: puzzleData.id,
|
||||
successMessage: `퍼즐 완성! (시간: ${completionTimeSeconds}초, 힌트 사용: ${hintsUsed}개)`,
|
||||
primaryScore: completionTimeSeconds,
|
||||
secondaryScore: hintsUsed
|
||||
});
|
||||
}
|
||||
|
||||
@@ -797,7 +742,10 @@
|
||||
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;
|
||||
// [수정] Base64 대신 URL 경로를 사용하도록 변경
|
||||
img.src = (img.id === 'grayscale-reveal')
|
||||
? `/puzzle/images/${puzzleData.grayscaleImageFile}`
|
||||
: `/puzzle/images/${puzzleData.originalImageFile}`;
|
||||
});
|
||||
|
||||
// --- 애니메이션 순차 실행 ---
|
||||
@@ -807,19 +755,12 @@
|
||||
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 = '/' }
|
||||
]
|
||||
showGameSuccessModal({
|
||||
gameType: 'NONOGRAM',
|
||||
contextId: puzzleData.id,
|
||||
successMessage: `퍼즐 완성! (시간: ${completionTimeSeconds}초, 힌트 사용: ${hintsUsed}개)`,
|
||||
primaryScore: completionTimeSeconds,
|
||||
secondaryScore: hintsUsed
|
||||
});
|
||||
}, 2000);
|
||||
}, 2000);
|
||||
@@ -850,7 +791,7 @@
|
||||
checkAndLockCompletedLines(hintAffectedRows, hintAffectedCols);
|
||||
checkWinCondition();
|
||||
} else {
|
||||
alert("더 이상 사용할 힌트가 없습니다!");
|
||||
showAlert("알림","더 이상 사용할 힌트가 없습니다!");
|
||||
points++;
|
||||
updatePointsDisplay();
|
||||
}
|
||||
@@ -880,26 +821,7 @@
|
||||
*/
|
||||
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) {
|
||||
@@ -947,106 +869,6 @@
|
||||
}
|
||||
|
||||
|
||||
// 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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,41 +9,12 @@
|
||||
<link th:href="@{/css/common_game_theme.css}" rel="stylesheet" />
|
||||
<style>
|
||||
/* sudoku.css의 내용을 여기에 삽입 */
|
||||
body {
|
||||
/*
|
||||
(★ 삭제) 아래 속성들은 common_game_theme.css에서 관리합니다.
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f4f7f9;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
*/
|
||||
}
|
||||
|
||||
#sudoku-game-app {
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
/*
|
||||
(★ 삭제) 아래 속성들은 common_game_theme.css에서
|
||||
'#sudoku-game-app .container' 셀렉터로 이미 관리하고 있습니다.
|
||||
|
||||
background: white;
|
||||
padding: clamp(15px, 4vw, 30px);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
*/
|
||||
|
||||
/* (★ 남김) .container에만 필요한 고유 속성 (text-align)은 남겨두거나 common_game_theme로 이동 */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -52,22 +23,6 @@
|
||||
color: #333;
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
/* (★ 참고) h1은 common_game_theme의 스타일을 상속받습니다.
|
||||
만약 스도쿠만 다른 스타일을 원한다면 여기에서 재정의(override)하면 됩니다.
|
||||
현재는 공통 스타일이 적용됩니다. */
|
||||
}
|
||||
|
||||
/* ======================================= */
|
||||
/* (★ 남김) 아래부터는 스도쿠 고유의 스타일입니다. (수정 불필요) */
|
||||
/* ======================================= */
|
||||
|
||||
/* 게임 컨테이너 */
|
||||
#game-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 게임 정보 (점수, 타이머) */
|
||||
@@ -85,14 +40,47 @@
|
||||
#score { color: #007bff; }
|
||||
#timer { color: #333; }
|
||||
|
||||
/* 보드 영역의 크기를 미리 고정시키는 스타일 */
|
||||
#board-area {
|
||||
position: relative; /* 자식 요소의 absolute 위치 기준점 */
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin: 0 auto 15px auto;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
/* 난이도 선택 UI를 보드 영역 중앙에 배치 */
|
||||
#setup-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
#setup-container select, #setup-container button {
|
||||
font-size: 1.2em;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
/* 스도쿠 보드 */
|
||||
#sudoku-board {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(9, 1fr);
|
||||
grid-template-rows: repeat(9, 1fr);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 3px solid #333;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
#game-controls-container {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cell {
|
||||
@@ -255,21 +243,23 @@
|
||||
<div class="container">
|
||||
<h1>스도쿠를 즐겨보세요!</h1>
|
||||
|
||||
<div id="setup-container">
|
||||
<select id="difficulty-select">
|
||||
<option value="easy">쉬움</option>
|
||||
<option value="medium">중간</option>
|
||||
<option value="hard">어려움</option>
|
||||
</select>
|
||||
<button id="start-btn">게임 시작</button>
|
||||
<div id="board-area">
|
||||
<div id="setup-container">
|
||||
<select id="difficulty-select">
|
||||
<option value="easy">쉬움</option>
|
||||
<option value="medium">중간</option>
|
||||
<option value="hard">어려움</option>
|
||||
</select>
|
||||
<button id="start-btn">게임 시작</button>
|
||||
</div>
|
||||
<div id="sudoku-board" class="hidden"></div>
|
||||
</div>
|
||||
|
||||
<div id="game-container" class="hidden">
|
||||
<div id="game-controls-container" class="hidden">
|
||||
<div class="game-info">
|
||||
<div id="score">SCORE: 5</div>
|
||||
<div id="timer">00:00</div>
|
||||
</div>
|
||||
<div id="sudoku-board"></div>
|
||||
<div id="number-input-buttons">
|
||||
<button class="num-btn" data-number="1">1</button>
|
||||
<button class="num-btn" data-number="2">2</button>
|
||||
@@ -280,9 +270,8 @@
|
||||
<button class="num-btn" data-number="7">7</button>
|
||||
<button class="num-btn" data-number="8">8</button>
|
||||
<button class="num-btn" data-number="9">9</button>
|
||||
<button id="undo-btn" class="clear-btn">실행취소</button>
|
||||
<button id="undo-btn" class="clear-btn">↩</button>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button id="hint-btn">힌트 사용 (-1점)</button>
|
||||
<button id="complete-btn">정답 확인</button>
|
||||
@@ -312,11 +301,17 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// sudoku.js의 내용을 여기에 삽입
|
||||
window.pageContext = { pageType: 'game', gameType: 'SUDOKU', contextId: undefined };
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 페이지 로드 시 스도쿠 전체 랭킹 표시
|
||||
if (typeof updateGameRanking === 'function') {
|
||||
updateGameRanking('SUDOKU', null);
|
||||
}
|
||||
|
||||
// DOM 요소
|
||||
const setupContainer = document.getElementById('setup-container');
|
||||
const gameContainer = document.getElementById('game-container');
|
||||
const gameControlsContainer = document.getElementById('game-controls-container');
|
||||
const startBtn = document.getElementById('start-btn');
|
||||
const boardElement = document.getElementById('sudoku-board');
|
||||
const timerElement = document.getElementById('timer');
|
||||
@@ -328,11 +323,10 @@
|
||||
const modalOverlay = document.getElementById('modal-overlay');
|
||||
const gameOverModal = document.getElementById('game-over-modal');
|
||||
const retryBtn = document.getElementById('retry-btn');
|
||||
const submitRankBtn = document.getElementById('submit-rank-btn');
|
||||
const rankingList = document.getElementById('ranking-list');
|
||||
const closeModalBtn = document.getElementById('close-modal-btn');
|
||||
|
||||
// 게임 상태 변수
|
||||
const currentGameType = 'SUDOKU';
|
||||
let currentPuzzleId = null;
|
||||
let solvedPuzzle = null;
|
||||
let timerInterval = null;
|
||||
@@ -342,11 +336,9 @@
|
||||
let score = 5;
|
||||
let history = [];
|
||||
|
||||
// (★ 수정) API 호출 경로를 통합 컨트롤러(/puzzle) 경로로 변경
|
||||
startBtn.addEventListener('click', async () => {
|
||||
const difficulty = document.getElementById('difficulty-select').value;
|
||||
try {
|
||||
// (★ 수정) API 경로 변경: /sudoku/start -> /puzzle/sudoku/start
|
||||
const response = await fetch(`/puzzle/sudoku/start?difficulty=${difficulty}`);
|
||||
if (!response.ok) throw new Error('서버에서 게임 데이터를 가져오지 못했습니다.');
|
||||
const gameData = await response.json();
|
||||
@@ -354,6 +346,11 @@
|
||||
currentPuzzleId = gameData.puzzleId;
|
||||
solvedPuzzle = gameData.solution;
|
||||
|
||||
// 푸터 랭킹을 현재 퍼즐 랭킹으로 업데이트
|
||||
if (typeof updateGameRanking === 'function') {
|
||||
updateGameRanking(currentGameType, currentPuzzleId);
|
||||
}
|
||||
|
||||
history = [];
|
||||
score = 5;
|
||||
updateScoreDisplay();
|
||||
@@ -362,12 +359,14 @@
|
||||
startTimer();
|
||||
updateButtonStates();
|
||||
|
||||
// 화면 전환
|
||||
setupContainer.classList.add('hidden');
|
||||
gameContainer.classList.remove('hidden');
|
||||
numberInputButtons.classList.remove('hidden');
|
||||
boardElement.classList.remove('hidden');
|
||||
gameControlsContainer.classList.remove('hidden');
|
||||
gameOverModal.classList.add('hidden');
|
||||
|
||||
} catch (error) {
|
||||
alert('게임 로딩에 실패했습니다: ' + error.message);
|
||||
showAlert("알림",'게임 로딩에 실패했습니다: ' + error.message);
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
@@ -431,19 +430,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
// --- 게임 플레이 이벤트 핸들링 ---
|
||||
numberInputButtons.addEventListener('click', (event) => {
|
||||
const target = event.target.closest('button');
|
||||
if (!target) return;
|
||||
|
||||
if (target === undoBtn) {
|
||||
undoAction();
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.classList.contains('completed')) return;
|
||||
document.querySelectorAll('.num-btn').forEach(btn => btn.classList.remove('selected'));
|
||||
|
||||
if (target.classList.contains('num-btn')) {
|
||||
const num = target.dataset.number;
|
||||
selectedNumber = (selectedNumber === num) ? null : num;
|
||||
@@ -460,18 +455,15 @@
|
||||
return;
|
||||
}
|
||||
focusedCell = targetCell;
|
||||
|
||||
if (selectedNumber) {
|
||||
const previousValue = targetCell.textContent;
|
||||
let newValue = (previousValue === selectedNumber) ? '' : selectedNumber;
|
||||
targetCell.textContent = newValue;
|
||||
|
||||
recordAction(targetCell, previousValue, newValue);
|
||||
validateCell(targetCell);
|
||||
updateButtonStates();
|
||||
checkIfBoardIsFull();
|
||||
}
|
||||
|
||||
highlightCells();
|
||||
});
|
||||
|
||||
@@ -479,22 +471,18 @@
|
||||
if (score <= 0) return;
|
||||
const emptyCells = Array.from(boardElement.querySelectorAll('.cell.editable')).filter(cell => !cell.textContent);
|
||||
if (emptyCells.length === 0) {
|
||||
alert('모든 칸이 채워져 있습니다.');
|
||||
showAlert("알림",'모든 칸이 채워져 있습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
|
||||
const cellIndex = parseInt(randomCell.dataset.index);
|
||||
const correctAnswer = solvedPuzzle[cellIndex];
|
||||
const previousValue = randomCell.textContent;
|
||||
|
||||
score--;
|
||||
updateScoreDisplay();
|
||||
recordAction(randomCell, previousValue, correctAnswer, true);
|
||||
|
||||
randomCell.textContent = correctAnswer;
|
||||
randomCell.classList.remove('editable', 'incorrect');
|
||||
|
||||
updateButtonStates();
|
||||
highlightCells();
|
||||
checkIfBoardIsFull();
|
||||
@@ -504,7 +492,6 @@
|
||||
if (history.length === 0) return;
|
||||
const lastAction = history.pop();
|
||||
const cell = boardElement.querySelector(`.cell[data-index="${lastAction.index}"]`);
|
||||
|
||||
if (cell) {
|
||||
cell.textContent = lastAction.previousValue;
|
||||
if (lastAction.wasHint) {
|
||||
@@ -538,7 +525,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// --- 하이라이트 기능 ---
|
||||
function highlightCells() {
|
||||
document.querySelectorAll('.cell').forEach(cell => {
|
||||
cell.classList.remove('highlight-focused', 'highlight-same-number', 'highlight-selected-number');
|
||||
@@ -559,21 +545,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
// --- 게임 완료 및 모달 ---
|
||||
async function checkSolution() {
|
||||
let answerString = "";
|
||||
boardElement.childNodes.forEach(cell => {
|
||||
answerString += cell.textContent || '0';
|
||||
});
|
||||
|
||||
if (answerString.includes('0')) {
|
||||
alert('모든 칸을 채워주세요!');
|
||||
showAlert("알림",'모든 칸을 채워주세요!');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// (★ 수정) API 경로 변경: /sudoku/validate -> /puzzle/sudoku/validate
|
||||
// (★ 수정) currentPuzzleId 변수 사용
|
||||
const response = await fetch('/puzzle/sudoku/validate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -582,14 +563,23 @@
|
||||
const result = await response.json();
|
||||
if (result.correct) {
|
||||
clearInterval(timerInterval);
|
||||
alert('🎉 정답입니다!');
|
||||
showRankingModal(); // 랭킹 모달 표시
|
||||
|
||||
// ▼▼▼ 기존 alert 및 showRankingModal 대신 통합 모달 호출 ▼▼▼
|
||||
const minutes = Math.floor(secondsElapsed / 60);
|
||||
const seconds = secondsElapsed % 60;
|
||||
showGameSuccessModal({
|
||||
gameType: 'SUDOKU',
|
||||
contextId: currentPuzzleId,
|
||||
successMessage: `정답입니다! 완료 시간: ${minutes}분 ${seconds}초`,
|
||||
primaryScore: secondsElapsed,
|
||||
secondaryScore: null
|
||||
});
|
||||
} else {
|
||||
alert('🤔 틀린 부분이 있습니다. 다시 확인해주세요.');
|
||||
showAlert("알림",'🤔 틀린 부분이 있습니다. 다시 확인해주세요.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('정답 확인 중 오류 발생:', error);
|
||||
alert('정답 확인 중 오류가 발생했습니다.');
|
||||
showAlert("알림",'정답 확인 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,64 +590,14 @@
|
||||
}
|
||||
}
|
||||
completeBtn.addEventListener('click', checkSolution);
|
||||
/**
|
||||
* (★ 수정) user.js의 공통 fetchRanks 함수를 사용하도록 수정
|
||||
*/
|
||||
async function showRankingModal() {
|
||||
modalOverlay.classList.remove('hidden');
|
||||
document.getElementById('username-input').value = '';
|
||||
submitRankBtn.disabled = false;
|
||||
rankingList.innerHTML = '<li>로딩 중...</li>';
|
||||
|
||||
try {
|
||||
// user.js의 공통 fetchRanks 함수 호출 (스도쿠 퍼즐 ID 전달)
|
||||
// currentGameType 변수가 정의되어 있어야 합니다. 예: const currentGameType = 'sudoku';
|
||||
const currentGameType = 'SUDOKU';
|
||||
const rankings = await fetchRanks(currentGameType, currentPuzzleId);
|
||||
|
||||
rankingList.innerHTML = '';
|
||||
if (rankings.length === 0) {
|
||||
rankingList.innerHTML = '<li>아직 등록된 랭킹이 없습니다.</li>';
|
||||
} else {
|
||||
rankings.forEach((rank, index) => {
|
||||
const li = document.createElement('li');
|
||||
const minutes = Math.floor(rank.primaryScore / 60).toString().padStart(2, '0');
|
||||
const seconds = (rank.primaryScore % 60).toString().padStart(2, '0');
|
||||
li.innerHTML = `<span>${index + 1}위: ${rank.playerName}</span> <span>${minutes}:${seconds}</span>`;
|
||||
rankingList.appendChild(li);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('랭킹 조회 중 오류 발생:', error);
|
||||
rankingList.innerHTML = '<li>랭킹을 불러올 수 없습니다.</li>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (★ 수정) user.js의 공통 submitRank 함수를 사용하도록 수정
|
||||
*/
|
||||
submitRankBtn.addEventListener('click', async () => {
|
||||
const userName = document.getElementById('username-input').value.trim();
|
||||
if (!userName) return alert('이름을 입력해주세요.');
|
||||
|
||||
try {
|
||||
// user.js의 공통 submitRank 함수 호출
|
||||
const currentGameType = 'SUDOKU';
|
||||
await submitRank(currentGameType, currentPuzzleId, userName, secondsElapsed, null);
|
||||
|
||||
alert('랭킹이 성공적으로 등록되었습니다!');
|
||||
showRankingModal(); // 랭킹 목록 새로고침
|
||||
submitRankBtn.disabled = true; // 중복 등록 방지
|
||||
} catch (error) {
|
||||
console.error('랭킹 등록 중 오류 발생:', error);
|
||||
alert('랭킹 등록에 실패했습니다. 다시 시도해주세요.');
|
||||
}
|
||||
});
|
||||
|
||||
function resetGameView() {
|
||||
setupContainer.classList.remove('hidden');
|
||||
gameContainer.classList.add('hidden');
|
||||
numberInputButtons.classList.add('hidden');
|
||||
boardElement.classList.add('hidden');
|
||||
gameControlsContainer.classList.add('hidden');
|
||||
clearInterval(timerInterval);
|
||||
selectedNumber = null;
|
||||
focusedCell = null;
|
||||
@@ -670,11 +610,17 @@
|
||||
closeModalBtn.addEventListener('click', () => {
|
||||
modalOverlay.classList.add('hidden');
|
||||
resetGameView();
|
||||
if (typeof updateGameRanking === 'function') {
|
||||
updateGameRanking(currentGameType, null);
|
||||
}
|
||||
});
|
||||
|
||||
retryBtn.addEventListener('click', () => {
|
||||
gameOverModal.classList.add('hidden');
|
||||
resetGameView();
|
||||
if (typeof updateGameRanking === 'function') {
|
||||
updateGameRanking(currentGameType, null);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -23,8 +23,10 @@
|
||||
const grayscaleImg = document.getElementById('grayscale-reveal');
|
||||
const originalImg = document.getElementById('original-reveal');
|
||||
|
||||
grayscaleImg.src = currentPuzzleData.grayscaleImage;
|
||||
originalImg.src = currentPuzzleData.originalImage;
|
||||
// [수정] Base64 대신 URL 경로를 사용하도록 변경
|
||||
grayscaleImg.src = `/puzzle/images/${currentPuzzleData.grayscaleImageFile}`;
|
||||
originalImg.src = `/puzzle/images/${currentPuzzleData.originalImageFile}`;
|
||||
|
||||
|
||||
puzzleContainer.style.transition = 'opacity 0.5s';
|
||||
puzzleContainer.style.opacity = '0';
|
||||
@@ -150,10 +152,10 @@
|
||||
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
if (!currentPuzzleData || !currentPuzzleData.id) {
|
||||
alert('삭제할 퍼즐이 선택되지 않았습니다.');
|
||||
showAlert("알림",'삭제할 퍼즐이 선택되지 않았습니다.');
|
||||
return;
|
||||
}
|
||||
if (!confirm('정말로 이 퍼즐을 삭제하시겠습니까?')) {
|
||||
if (!showConfirm("확인",'정말로 이 퍼즐을 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -30,9 +30,11 @@
|
||||
<div class="tab-link active" onclick="openTab(event, 'myInfo')">내 정보</div>
|
||||
<div class="tab-link" onclick="openTab(event, 'myPosts')">내가 쓴 글</div>
|
||||
<div class="tab-link" onclick="openTab(event, 'myComments')">내가 쓴 댓글</div>
|
||||
<div class="tab-link" onclick="openTab(event, 'myRanks')">내 게임 랭킹</div>
|
||||
<th:block sec:authorize="hasRole('ADMIN')">
|
||||
<div class="tab-link" onclick="openTab(event, 'userManagement')">회원 관리</div>
|
||||
<div class="tab-link" onclick="openTab(event, 'contentManagement')">콘텐츠 관리</div>
|
||||
<div class="tab-link" onclick="openTab(event, 'postManagement')">게시물 관리</div>
|
||||
<div class="tab-link" onclick="openTab(event, 'bannerManagement')">배너 관리</div>
|
||||
</th:block>
|
||||
</div>
|
||||
|
||||
@@ -77,6 +79,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="myRanks" class="tab-content">
|
||||
<div class="box">
|
||||
<ul class="post-list">
|
||||
<li th:if="${#lists.isEmpty(myRanks)}">플레이한 게임 기록이 없습니다.</li>
|
||||
<li th:each="rank : ${myRanks}">
|
||||
<div>
|
||||
<span th:switch="${rank.gameType.name()}">
|
||||
<strong th:case="'GAME_2048'">2048</strong>
|
||||
<strong th:case="'SUDOKU'">스도쿠</strong>
|
||||
<strong th:case="'SPIDER'">스파이더 카드</strong>
|
||||
<strong th:case="'NONOGRAM'">노노그램</strong>
|
||||
<strong th:case="*">기타 게임</strong>
|
||||
</span>
|
||||
<span style="margin-left: 1em;" th:switch="${rank.gameType.name()}">
|
||||
<span th:case="'SUDOKU'" th:text="|기록: ${rank.primaryScore / 60}분 ${rank.primaryScore % 60}초|"></span>
|
||||
<span th:case="'NONOGRAM'" th:text="|기록: ${rank.primaryScore / 60}분 ${rank.primaryScore % 60}초|"></span>
|
||||
<span th:case="'SPIDER'" th:text="|기록: ${rank.primaryScore} moves|"></span>
|
||||
<span th:case="*" th:text="|점수: ${#numbers.formatInteger(rank.primaryScore, 3, 'COMMA')}|"></span>
|
||||
</span>
|
||||
</div>
|
||||
<span th:text="${#temporals.format(rank.timestamp, 'yyyy-MM-dd HH:mm')}"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="userManagement" class="tab-content" sec:authorize="hasRole('ADMIN')">
|
||||
<div class="box">
|
||||
<h4>권한 요청</h4>
|
||||
@@ -105,7 +133,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="contentManagement" class="tab-content" sec:authorize="hasRole('ADMIN')">
|
||||
<div id="postManagement" class="tab-content" sec:authorize="hasRole('ADMIN')">
|
||||
<div class="box">
|
||||
<h4>최신 글 관리</h4>
|
||||
<ul class="post-list">
|
||||
@@ -120,6 +148,29 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="bannerManagement" class="tab-content" sec:authorize="hasRole('ADMIN')">
|
||||
<div class="box">
|
||||
<h4>배너 이미지 관리</h4>
|
||||
<ul class="post-list">
|
||||
<li th:each="image : ${allImages}" th:id="'image-row-' + ${image.id}">
|
||||
<div style="display: flex; align-items: center; gap: 1em;">
|
||||
<img th:src="@{'/api/images/' + ${image.fileName}}" alt="Image Thumbnail" style="width: 100px; height: 60px; object-fit: cover; border-radius: 4px;"/>
|
||||
<div>
|
||||
<strong th:text="${image.fileName}"></strong><br>
|
||||
<span th:if="${image.isBannerCandidate}" style="color: #2a9d8f; font-weight: bold;">(배너로 사용 중)</span>
|
||||
<span th:unless="${image.isBannerCandidate}" style="color: #888;">(배너로 사용 안 함)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button th:if="${!image.isBannerCandidate}" class="button small primary" th:onclick="handleBannerPermission('[[${image.id}]]', 'approve')">배너로 승인</button>
|
||||
<button th:if="${image.isBannerCandidate}" class="button small alt" th:onclick="handleBannerPermission('[[${image.id}]]', 'revoke')">승인 해제</button>
|
||||
</div>
|
||||
</li>
|
||||
<li th:if="${#lists.isEmpty(allImages)}">업로드된 이미지가 없습니다.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -211,7 +262,8 @@
|
||||
* @param {'block' | 'unblock'} action - 수행할 작업
|
||||
*/
|
||||
function handleContent(postId, action) {
|
||||
const url = `/blog/post/${postId}/${action}`;
|
||||
const cleanpostId = postId.replace(/^"|"$/g, '');
|
||||
const url = `/blog/post/${cleanpostId}/${action}`;
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { [csrfHeader]: csrfToken }
|
||||
@@ -227,6 +279,42 @@
|
||||
})
|
||||
.catch(error => console.error('Error:', error));
|
||||
}
|
||||
|
||||
/**
|
||||
* (관리자) 이미지의 배너 사용 권한을 승인하거나 해제하는 함수
|
||||
* @param {string} imageId - 대상 이미지의 ID
|
||||
* @param {'approve' | 'revoke'} action - 수행할 작업
|
||||
*/
|
||||
function handleBannerPermission(imageId, action) {
|
||||
const cleanImageId = imageId.replace(/^"|"$/g, '');
|
||||
|
||||
// [수정] 깨끗하게 정리된 cleanImageId를 URL에 사용합니다.
|
||||
const url = `/api/images/${cleanImageId}/${action === 'approve' ? 'approve-banner' : 'revoke-banner'}`;
|
||||
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { [csrfHeader]: csrfToken }
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Server returned an error.');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data && data.id) {
|
||||
alert(`이미지 상태를 성공적으로 변경했습니다.`);
|
||||
location.reload(); // 페이지를 새로고침하여 상태(버튼, 텍스트)를 즉시 반영
|
||||
} else {
|
||||
alert('작업에 실패했습니다.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('작업 중 오류가 발생했습니다.');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
||||
Reference in New Issue
Block a user