This commit is contained in:
2025-09-12 16:55:21 +09:00
parent d62a4a3c15
commit 19c5d5473f
41 changed files with 8039 additions and 4149 deletions
+162 -147
View File
@@ -1,156 +1,171 @@
/* =================================
기본 및 전체 레이아웃
================================= */
body {
font-family: Arial, sans-serif;
text-align: center;
background-color: #faf8ef;
color: #776e65;
margin: 0;
padding: 10px;
box-sizing: border-box;
}
/*!* =================================*/
/* 기본 및 전체 레이아웃 (수정됨)*/
/* ================================= *!*/
/*body {*/
/* !* (★ 삭제) font-family, text-align, background-color, color, margin, padding*/
/* -> 이 속성들은 모두 common_game_theme.css에서 관리합니다.*/
/* *!*/
/* box-sizing: border-box;*/
/*}*/
h1 {
font-size: 15vw;
margin: 20px 0;
}
/*h1 {*/
/* font-size: 15vw; !* 2048 고유의 큰 폰트 크기는 유지 *!*/
/* margin: 20px 0;*/
/* !* (★ 삭제) color 속성 삭제 -> common_game_theme에서 상속 *!*/
/*}*/
.score-container {
font-size: 24px;
margin-bottom: 20px;
}
/*.score-container {*/
/* font-size: 24px;*/
/* margin-bottom: 20px;*/
/*}*/
/* =================================
게임 보드 (가장 중요한 부분)
================================= */
#game-board {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 2vw;
width: 95vw;
max-width: 400px;
margin: 0 auto;
background-color: #bbada0;
padding: 2vw;
border-radius: 6px;
box-sizing: border-box;
aspect-ratio: 1 / 1; /* 정사각형 비율 유지 */
touch-action: none; /* 모바일에서 터치 시 화면 확대/이동 방지 */
}
/*!* =================================*/
/* 게임 보드 (테마 적용)*/
/* ================================= *!*/
/*#game-board {*/
/* display: grid;*/
/* grid-template-columns: repeat(4, 1fr);*/
/* grid-gap: 2vw;*/
/* width: 95vw;*/
/* max-width: 500px; !* (★ 수정) 400px -> 500px (다른 게임과 통일) *!*/
/* margin: 0 auto;*/
/* =================================
타일 공통 스타일
================================= */
.tile {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
border-radius: 3px;
background-color: #cdc1b4;
font-size: 7vw;
}
/* !* (★ 수정) 2048의 갈색/베이지 테마를 차가운 회색/파란색 테마로 변경 *!*/
/* background-color: #b0bec5; !* #bbada0 (갈색) -> #b0bec5 (블루 그레이) *!*/
/* =================================
타일 색상
================================= */
.tile-2 { background-color: #eee4da; color: #776e65; }
.tile-4 { background-color: #ede0c8; color: #776e65; }
.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-4096, .tile-8192 { font-size: 6vw; }
.tile-16384, .tile-32768 { font-size: 5vw; }
/* padding: 2vw;*/
/* border-radius: 6px;*/
/* box-sizing: border-box;*/
/* aspect-ratio: 1 / 1;*/
/* touch-action: none;*/
/* !* (★ 추가) 공통 카드 UI와 유사한 그림자 효과 추가 *!*/
/* box-shadow: 0 4px 10px rgba(0,0,0,0.08);*/
/*}*/
/*@media (min-width: 481px) {*/
/* #game-board {*/
/* grid-gap: 10px;*/
/* padding: 10px;*/
/* }*/
/*}*/
/* =================================
게임 오버 팝업
================================= */
.popup-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
}
.popup {
background-color: #faf8ef;
padding: 20px;
border-radius: 10px;
text-align: center;
width: 80vw;
max-width: 300px;
}
.popup input {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ccc;
border-radius: 5px;
box-sizing: border-box;
}
.popup button {
padding: 10px 20px;
background-color: #8f7a66;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
/*!* =================================*/
/* 타일 공통 스타일 (테마 적용)*/
/* ================================= *!*/
/*.tile {*/
/* width: 100%;*/
/* height: 100%;*/
/* display: flex;*/
/* justify-content: center;*/
/* align-items: center;*/
/* font-weight: bold;*/
/* border-radius: 3px;*/
/* =================================
랭킹 리스트
================================= */
.ranking-container {
width: 100%;
max-width: 400px;
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: #eee4da;
margin-bottom: 5px;
padding: 10px;
border-radius: 5px;
display: flex;
justify-content: space-between;
}
/* !* (★ 수정) 빈 타일 색상 변경 *!*/
/* background-color: #eceff1; !* #cdc1b4 (갈색) -> #eceff1 (밝은 블루 그레이) *!*/
/* =================================
반응형: PC & 태블릿 (화면이 481px 이상일 때)
================================= */
@media (min-width: 481px) {
h1 { font-size: 80px; }
#game-board {
grid-gap: 10px;
padding: 10px;
}
.tile { font-size: 45px; }
.tile-4096, .tile-8192 { font-size: 35px; }
.tile-16384, .tile-32768 { font-size: 30px; }
}
/* font-size: 5vw;*/
/*}*/
/*@media (min-width: 481px) {*/
/* .tile {*/
/* font-size: 30px;*/
/* }*/
/*}*/
/*!* =================================*/
/* 타일 색상 (테마 적용)*/
/* ================================= *!*/
/*!* (★ 수정) 2, 4 타일은 베이지색 계열이라 테마와 충돌하므로 파란색 계열로 변경 *!*/
/*.tile-2 { background-color: #e3f2fd; color: #333; } !* #eee4da (베이지) -> #e3f2fd (밝은 파랑) *!*/
/*.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; }*/
/*!* =================================*/
/* 게임 오버 팝업 (테마 적용)*/
/* ================================= *!*/
/*.popup-container {*/
/* position: fixed;*/
/* top: 0;*/
/* left: 0;*/
/* width: 100%;*/
/* height: 100%;*/
/* background-color: rgba(0, 0, 0, 0.5);*/
/* display: flex;*/
/* justify-content: center;*/
/* align-items: center;*/
/* z-index: 100;*/
/*}*/
/*.popup {*/
/* !* (★ 수정) 배경색을 테마에 맞게 흰색으로 변경 *!*/
/* background-color: #ffffff; !* #faf8ef (베이지) -> #ffffff (흰색) *!*/
/* padding: 20px;*/
/* border-radius: 10px;*/
/* text-align: center;*/
/* width: 80vw;*/
/* max-width: 300px;*/
/* box-shadow: 0 4px 15px rgba(0,0,0,0.2); !* 흰색 배경이므로 그림자 추가 *!*/
/*}*/
/*.popup input {*/
/* width: 100%;*/
/* padding: 10px;*/
/* margin: 10px 0;*/
/* border: 1px solid #ccc;*/
/* border-radius: 5px;*/
/* box-sizing: border-box;*/
/*}*/
/*.popup button {*/
/* padding: 10px 20px;*/
/* !* (★ 삭제) background-color, color, border -> common_game_theme의 파란색 버튼 스타일을 상속받음 *!*/
/* 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;*/
/*}*/
+13
View File
@@ -535,4 +535,17 @@ a.btn_layerClose:hover {
cursor: pointer;
font-size: 0.85em;
font-weight: bold;
}
@media screen and (max-width: 480px) {
.vote-controls {
display: flex; /* Flexbox 컨테이너로 변경 */
gap: 1em; /* 버튼 사이 간격 (기존 margin-left 대체) */
}
.vote-controls .button {
width: auto; /* main.css의 width: 100% 덮어쓰기 */
display: inline-block; /* main.css의 display: block 덮어쓰기 */
flex: 1 1 0; /* 1:1 비율로 공간을 나눠 갖도록 설정 */
margin-left: 0 !important; /* 혹시 모를 인라인 스타일 제거 */
}
}
@@ -0,0 +1,100 @@
/* =================================
common_game_theme.css
(모든 게임이 공유하는 공통 테마)
================================= */
/* (★ 신규) 모든 테마의 기준이 되는 CSS 변수 정의 */
:root {
--font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
/* 기본 색상 */
--color-text-primary: #1a1a1a;
--color-text-secondary: #333;
--color-bg-page: #f4f7f9;
--color-bg-card: #ffffff;
/* 공통 UI 색상 */
--color-primary: #007bff;
--color-primary-hover: #0056b3;
--color-disabled-bg: #cccccc;
--color-disabled-opacity: 0.7;
/* 게임 상태별 색상 */
--color-incorrect-bg: #ffdddd;
--color-incorrect-text: #d8000c;
--color-focus-bg: #dbeeff; /* 스도쿠 포커스 */
--color-highlight-bg: #e6e6e6; /* 스도쿠 동일 숫자 */
--color-selected-num-bg: #b3d7ff; /* 스도쿠 선택 숫자 */
/* 게임 고유 테마 색상 */
--color-felt-green: #008000; /* 스파이더: 테이블 배경 */
--color-felt-border: #004d00; /* 스파이더: 캔버스 테두리 */
--color-grid-bg-2048: #b0bec5; /* 2048: 보드 배경 */
--color-tile-empty: #eceff1; /* 2048: 빈 타일 */
--color-tile-2: #e3f2fd;
--color-tile-4: #bbdefb;
/* 공통 UI 속성 */
--border-radius-main: 8px;
--box-shadow-main: 0 4px 10px rgba(0,0,0,0.08);
}
/* (★ 통일) 기본 폰트 및 배경색 정의 (변수 사용) */
body {
font-family: var(--font-main);
background-color: var(--color-bg-page);
color: var(--color-text-secondary);
text-align: center;
margin: 0;
padding: 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
}
/* (★ 통일) H1 (게임 제목) 스타일 통일 (변수 사용) */
h1 {
font-size: clamp(2.2em, 8vw, 3.2em);
color: var(--color-text-primary);
margin: 10px 0 20px 0;
}
/* (★ 통일) 모든 <button> 기본 스타일 통일 (변수 사용) */
button {
padding: 10px 20px;
font-size: 1em;
font-weight: bold;
cursor: pointer;
background-color: var(--color-primary);
color: var(--color-bg-card);
border: none;
border-radius: 5px;
transition: background-color 0.2s, opacity 0.2s;
}
button:hover:not(:disabled) {
background-color: var(--color-primary-hover);
}
button:disabled {
background-color: var(--color-disabled-bg);
cursor: not-allowed;
opacity: var(--color-disabled-opacity);
}
/* (★ 통일) 게임 컨트롤/랭킹 등을 감싸는 공통 '카드' UI (변수 사용) */
#sudoku-game-app .container,
.ranking-container,
#setup-container,
#game-controls {
background: var(--color-bg-card);
padding: clamp(15px, 4vw, 25px);
border-radius: var(--border-radius-main);
box-shadow: var(--box-shadow-main);
box-sizing: border-box;
width: 100%;
max-width: 500px; /* 최대 너비 통일 */
margin: 15px auto;
}
+314
View File
@@ -0,0 +1,314 @@
/*!* === nonogram.css (게임 플레이용) === *!*/
/*body {*/
/* !* (★ 삭제) font-family, align-items: center*/
/* -> common_game_theme.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%;*/
/* !* (★ 삭제) 아래 속성들은 common_game_theme.css의 #game-controls 셀렉터가 관리합니다.*/
/* max-width: 500px;*/
/* margin-bottom: 15px;*/
/* *!*/
/* font-size: 1.2em;*/
/* flex-wrap: wrap;*/
/* gap: 15px;*/
/* !* (★ 참고) common_game_theme에서 이미 background: white, padding, box-shadow 등이 적용된 상태입니다.*/
/* 이 CSS는 그 내부의 flex 정렬만 담당하게 됩니다.*/
/* *!*/
/*}*/
/*#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 *!*/
/*}*/
-240
View File
@@ -1,240 +0,0 @@
body {
font-family: sans-serif;
align-items: center;
}
#board-viewport {
position: relative;
width: 100%;
max-width: 95vw; /* 화면 너비에 좀 더 맞춤 */
margin: 20px auto;
/* (★ 핵심 수정) Flexbox로 자식 요소를 중앙 정렬합니다. */
display: flex;
justify-content: center; /* 자식 요소를 수평 중앙 정렬 */
align-items: flex-start; /* 위쪽에 정렬 */
}
/* (★ ADD) Styles for the reveal images */
.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은 그대로 유지합니다. */
transform-origin: top;
}
/* (★ 수정) 게임 컨트롤 영역 스타일 */
#game-controls {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 500px;
margin-bottom: 15px;
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;
}
/* (★ 핵심) 선택된 라디오 버튼의 span 스타일 */
#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;
}
.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; /* 줄 간격 */
}
.grid-cell {
background-color: #fff;
border: 1px solid #ddd;
box-sizing: border-box;
cursor: pointer;
}
.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 클래스는 이제 overlay의 visible/hidden 상태 관리에 사용됩니다. */
.hidden {
display: none;
}
/* play.css */
/* (★ 추가) 완성된 힌트 스타일 */
.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);
}
-64
View File
@@ -1,64 +0,0 @@
#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;
}
.grid-cell {
width: 25px;
height: 25px;
background-color: #f0f0f0;
text-align: center;
line-height: 25px;
font-size: 14px;
}
.clue-cell {
background-color: #cce7ff;
display: flex;
justify-content: center;
align-items: center;
padding: 5px;
min-height: 25px;
font-weight: bold;
}
.solution-cell {
width: 25px;
height: 25px;
}
.filled {
background-color: #333;
}
.empty {
background-color: #fff;
}
#puzzle-wrapper {
position: relative; /* Needed for absolute positioning of children */
}
#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 */
}
+40 -16
View File
@@ -1,17 +1,41 @@
#game-container {
display: flex;
justify-content: center; /* 가로 중앙 정렬 */
align-items: flex-start; /* 세로 상단 정렬 */
background-color: #008000;
width: 100vw;
height: 100vh;
box-sizing: border-box;
}
/*!* (★ 수정) #game-container가 전체 화면(100vw/vh)을 차지하는 대신,*/
/* common_game_theme의 body(#f4f7f9 배경) 위에 떠 있는*/
/* '게임 테이블(카드)' 역할을 하도록 변경합니다. *!*/
/*#game-container {*/
/* !* (★ 남김) 내부 캔버스를 정렬하는 로직은 유지 *!*/
/* display: flex;*/
/* justify-content: center;*/
/* align-items: flex-start;*/
#gameCanvas {
background-color: #008000;
border: 2px solid #fff;
width: 95%;
max-height: min(95vw, 95vh);
box-sizing: border-box;
}
/* !* (★ 유지/수정) '펠트' 배경색은 유지하되, 공통 '카드' UI 요소를 추가 *!*/
/* background-color: #008000;*/
/* border-radius: 8px; !* (★ 추가) 공통 테마 둥근 모서리 *!*/
/* box-shadow: 0 4px 10px rgba(0,0,0,0.08); !* (★ 추가) 공통 테마 그림자 *!*/
/* padding: 15px; !* (★ 추가) 캔버스 주변 여백 *!*/
/* box-sizing: border-box;*/
/* !* (★ 삭제) 100vw, 100vh 속성을 삭제하여 body의 중앙 정렬이 동작하도록 함 *!*/
/* !* width: 100vw; (삭제) *!*/
/* !* height: 100vh; (삭제) *!*/
/* !* (★ 수정) 너비 관리: 다른 게임(500px)보다 넓은 반응형 최대 너비를 가짐 *!*/
/* width: 95%; !* 뷰포트의 95%를 사용 *!*/
/* max-width: 1200px; !* 단, 스파이더 게임에 맞게 1200px까지 허용 *!*/
/*}*/
/*#gameCanvas {*/
/* !* (★ 삭제) 컨테이너가 이미 녹색이므로 캔버스 자체의 배경은 불필요 *!*/
/* !* background-color: #008000; (삭제) *!*/
/* !* (★ 수정) 흰색 테두리보다 펠트 색과 대비되는 어두운 테두리로 변경 *!*/
/* border: 2px solid #004d00; !* #fff (흰색) -> #004d00 (어두운 녹색) *!*/
/* !* (★ 수정) 너비: 부모(#game-container) 패딩 영역의 100%를 차지 *!*/
/* width: 100%;*/
/* height: auto; !* 너비에 맞춰 캔버스 비율(JS가 설정한)을 따름 *!*/
/* !* (★ 삭제) 뷰포트 기준 max-height 삭제. 컨테이너 너비와 캔버스 비율로 크기가 결정됨. *!*/
/* !* max-height: min(95vw, 95vh); (삭제) *!*/
/* box-sizing: border-box; !* 유지 *!*/
/*}*/
+242 -220
View File
@@ -1,238 +1,260 @@
/* 기본 스타일 초기화 및 폰트 설정 */
body {
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;
}
/*!* 기본 스타일 초기화 및 폰트 설정 *!*/
/*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;
}
/*#sudoku-game-app {*/
/* width: 100%;*/
/* margin: 20px 0;*/
/*}*/
.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 {*/
/* !**/
/* (★ 삭제) 아래 속성들은 common_game_theme.css에서*/
/* '#sudoku-game-app .container' 셀렉터로 이미 관리하고 있습니다.*/
h1 {
font-size: 1.8em;
color: #333;
margin-top: 0;
margin-bottom: 20px;
}
/* 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;*/
/* *!*/
button {
padding: 10px 20px;
font-size: 1em;
cursor: pointer;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
transition: background-color 0.2s;
}
button:hover:not(:disabled) {
background-color: #0056b3;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
/* !* (★ 남김) .container에만 필요한 고유 속성 (text-align)은 남겨두거나 common_game_theme로 이동 *!*/
/* text-align: center;*/
/*}*/
/* 게임 컨테이너 */
#game-container {
display: flex;
flex-direction: column;
align-items: center;
max-width: 500px;
margin: 0 auto;
}
/*h1 {*/
/* font-size: 1.8em;*/
/* color: #333;*/
/* margin-top: 0;*/
/* margin-bottom: 20px;*/
/* !* (★ 참고) h1은 common_game_theme의 스타일을 상속받습니다.*/
/* 만약 스도쿠만 다른 스타일을 원한다면 여기에서 재정의(override)하면 됩니다.*/
/* 현재는 공통 스타일이 적용됩니다. *!*/
/*}*/
/* 게임 정보 (점수, 타이머) */
.game-info {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding: 0 10px;
box-sizing: border-box;
font-size: 1.5em;
font-weight: bold;
}
#score { color: #007bff; }
#timer { color: #333; }
/*!* (★ 삭제) 아래의 'button' 공통 스타일은 common_game_theme.css가 처리합니다. *!*/
/*!**/
/*button {*/
/* padding: 10px 20px;*/
/* font-size: 1em;*/
/* cursor: pointer;*/
/* background-color: #007bff;*/
/* color: white;*/
/* border: none;*/
/* border-radius: 5px;*/
/* transition: background-color 0.2s;*/
/*}*/
/*button:hover:not(:disabled) {*/
/* background-color: #0056b3;*/
/*}*/
/*button:disabled {*/
/* background-color: #cccccc;*/
/* cursor: not-allowed;*/
/*}*/
/**!*/
/* 스도쿠 보드 */
#sudoku-board {
display: grid;
grid-template-columns: repeat(9, 1fr);
grid-template-rows: repeat(9, 1fr);
width: 100%;
border: 3px solid #333;
aspect-ratio: 1 / 1;
}
.cell {
display: flex;
justify-content: center;
align-items: center;
font-size: clamp(1em, 4vw, 1.8em);
font-weight: bold;
color: #333;
border: 1px solid #ddd;
box-sizing: border-box;
cursor: pointer;
}
/*!* ======================================= *!*/
/*!* (★ 남김) 아래부터는 스도쿠 고유의 스타일입니다. (수정 불필요) *!*/
/*!* ======================================= *!*/
.cell:nth-child(3n) { border-right: 2px solid #333; }
.cell:nth-child(9n) { border-right-width: 1px; }
.cell:nth-child(n+19):nth-child(-n+27),
.cell:nth-child(n+46):nth-child(-n+54) {
border-bottom: 2px solid #333;
}
/*!* 게임 컨테이너 *!*/
/*#game-container {*/
/* display: flex;*/
/* flex-direction: column;*/
/* align-items: center;*/
/* max-width: 500px;*/
/* margin: 0 auto;*/
/*}*/
.cell:not(.editable) {
background-color: #f0f0f0;
color: #222;
cursor: default;
}
/*!* 게임 정보 (점수, 타이머) *!*/
/*.game-info {*/
/* width: 100%;*/
/* display: flex;*/
/* justify-content: space-between;*/
/* align-items: center;*/
/* margin-bottom: 15px;*/
/* padding: 0 10px;*/
/* box-sizing: border-box;*/
/* font-size: 1.5em;*/
/* font-weight: bold;*/
/*}*/
/*#score { color: #007bff; }*/
/*#timer { color: #333; }*/
/* 하이라이트 & 오답 스타일 */
.cell.incorrect {
background-color: #ffdddd !important;
color: #d8000c !important;
}
.highlight-focused {
background-color: #dbeeff !important;
}
.highlight-same-number {
background-color: #e6e6e6 !important;
}
.highlight-selected-number {
background-color: #b3d7ff !important;
}
/*!* 스도쿠 보드 *!*/
/*#sudoku-board {*/
/* display: grid;*/
/* grid-template-columns: repeat(9, 1fr);*/
/* grid-template-rows: repeat(9, 1fr);*/
/* width: 100%;*/
/* border: 3px solid #333;*/
/* aspect-ratio: 1 / 1;*/
/*}*/
/* 숫자 입력 버튼 */
#number-input-buttons {
display: flex;
justify-content: space-between;
width: 100%;
margin-top: 15px;
gap: 1%;
}
/*.cell {*/
/* display: flex;*/
/* justify-content: center;*/
/* align-items: center;*/
/* font-size: clamp(1em, 4vw, 1.8em);*/
/* font-weight: bold;*/
/* color: #333;*/
/* border: 1px solid #ddd;*/
/* box-sizing: border-box;*/
/* cursor: pointer;*/
/*}*/
#number-input-buttons .num-btn,
#number-input-buttons #undo-btn {
line-height: unset;
min-width: unset;
width: 9%;
aspect-ratio: 1/1;
font-size: clamp(1em, 4vw, 1.8em);
font-weight: bold;
border-radius: 8px;
background-color: #f0f0f0;
color: #333;
border: 1px solid #ccc;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
padding: 0;
transition: background-color 0.2s, transform 0.1s, opacity 0.2s;
}
/*.cell:nth-child(3n) { border-right: 2px solid #333; }*/
/*.cell:nth-child(9n) { border-right-width: 1px; }*/
/*.cell:nth-child(n+19):nth-child(-n+27),*/
/*.cell:nth-child(n+46):nth-child(-n+54) {*/
/* border-bottom: 2px solid #333;*/
/*}*/
#number-input-buttons .num-btn.selected {
background-color: #007bff;
color: white;
border-color: #007bff;
}
/*.cell:not(.editable) {*/
/* background-color: #f0f0f0;*/
/* color: #222;*/
/* cursor: default;*/
/*}*/
#number-input-buttons .num-btn.completed {
opacity: 0.4;
background-color: #e9ecef;
pointer-events: none;
}
/*!* 하이라이트 & 오답 스타일 *!*/
/*.cell.incorrect {*/
/* background-color: #ffdddd !important;*/
/* color: #d8000c !important;*/
/*}*/
/*.highlight-focused {*/
/* background-color: #dbeeff !important;*/
/*}*/
/*.highlight-same-number {*/
/* background-color: #e6e6e6 !important;*/
/*}*/
/*.highlight-selected-number {*/
/* background-color: #b3d7ff !important;*/
/*}*/
#number-input-buttons #undo-btn {
background-color: #f8f9fa;
color: #dc3545;
}
/*!* 숫자 입력 버튼 *!*/
/*#number-input-buttons {*/
/* display: flex;*/
/* justify-content: space-between;*/
/* width: 100%;*/
/* margin-top: 15px;*/
/* gap: 1%;*/
/*}*/
/* 액션 버튼 (힌트, 정답확인) */
.action-buttons {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 15px;
width: 100%;
}
.action-buttons button {
flex-grow: 1;
max-width: 200px;
}
/*#number-input-buttons .num-btn,*/
/*#number-input-buttons #undo-btn {*/
/* line-height: unset;*/
/* min-width: unset;*/
/* width: 9%;*/
/* aspect-ratio: 1/1;*/
/* font-size: clamp(1em, 4vw, 1.8em);*/
/* font-weight: bold;*/
/* border-radius: 8px;*/
/* background-color: #f0f0f0;*/
/* color: #333;*/
/* border: 1px solid #ccc;*/
/* display: flex;*/
/* justify-content: center;*/
/* align-items: center;*/
/* cursor: pointer;*/
/* padding: 0;*/
/* transition: background-color 0.2s, transform 0.1s, opacity 0.2s;*/
/*}*/
/* 모달 및 숨김 처리 */
.hidden {
display: none !important;
}
#modal-overlay, #game-over-modal {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background-color: rgba(0,0,0,0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
#modal-content {
background: white;
padding: 30px;
border-radius: 10px;
text-align: center;
width: 90%;
max-width: 400px;
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
}
#modal-content h2, #modal-content h3 {
color: #333;
margin-bottom: 15px;
}
#username-input {
width: calc(100% - 24px);
padding: 10px;
margin-bottom: 15px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 1em;
}
#ranking-list {
list-style-type: decimal;
list-style-position: inside;
padding: 0;
text-align: left;
margin-top: 20px;
}
#ranking-list li {
padding: 8px 0;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
#ranking-list li:last-child {
border-bottom: none;
}
/*#number-input-buttons .num-btn.selected {*/
/* background-color: #007bff;*/
/* color: white;*/
/* border-color: #007bff;*/
/*}*/
/*#number-input-buttons .num-btn.completed {*/
/* opacity: 0.4;*/
/* background-color: #e9ecef;*/
/* pointer-events: none;*/
/*}*/
/*#number-input-buttons #undo-btn {*/
/* background-color: #f8f9fa;*/
/* color: #dc3545;*/
/*}*/
/*!* 액션 버튼 (힌트, 정답확인) *!*/
/*.action-buttons {*/
/* display: flex;*/
/* justify-content: center;*/
/* gap: 10px;*/
/* margin-top: 15px;*/
/* width: 100%;*/
/*}*/
/*.action-buttons button {*/
/* flex-grow: 1;*/
/* max-width: 200px;*/
/*}*/
/*!* 모달 및 숨김 처리 *!*/
/*.hidden {*/
/* display: none !important;*/
/*}*/
/*#modal-overlay, #game-over-modal {*/
/* position: fixed;*/
/* top: 0; left: 0;*/
/* width: 100%; height: 100%;*/
/* background-color: rgba(0,0,0,0.7);*/
/* display: flex;*/
/* justify-content: center;*/
/* align-items: center;*/
/* z-index: 1000;*/
/*}*/
/*#modal-content {*/
/* background: white;*/
/* padding: 30px;*/
/* border-radius: 10px;*/
/* text-align: center;*/
/* width: 90%;*/
/* max-width: 400px;*/
/* box-shadow: 0 8px 20px rgba(0,0,0,0.2);*/
/*}*/
/*#modal-content h2, #modal-content h3 {*/
/* color: #333;*/
/* margin-bottom: 15px;*/
/*}*/
/*#username-input {*/
/* width: calc(100% - 24px);*/
/* padding: 10px;*/
/* margin-bottom: 15px;*/
/* border: 1px solid #ccc;*/
/* border-radius: 5px;*/
/* font-size: 1em;*/
/*}*/
/*#ranking-list {*/
/* list-style-type: decimal;*/
/* list-style-position: inside;*/
/* padding: 0;*/
/* text-align: left;*/
/* margin-top: 20px;*/
/*}*/
/*#ranking-list li {*/
/* padding: 8px 0;*/
/* border-bottom: 1px solid #eee;*/
/* display: flex;*/
/* justify-content: space-between;*/
/* align-items: center;*/
/*}*/
/*#ranking-list li:last-child {*/
/* border-bottom: none;*/
/*}*/
+238 -233
View File
@@ -1,233 +1,238 @@
document.addEventListener('DOMContentLoaded', () => {
// DOM 요소 가져오기
const gameBoard = document.getElementById('game-board');
const scoreDisplay = document.getElementById('score');
const gameOverPopup = document.getElementById('game-over-popup');
const finalScoreDisplay = document.getElementById('final-score');
const playerNameInput = document.getElementById('player-name');
const saveScoreButton = document.getElementById('save-score');
const rankingList = document.getElementById('ranking-list');
// 게임 설정 및 변수
const currentGameId = '2048';
const gridSize = 4;
let board = [];
let score = 0;
let touchStartX = 0, touchStartY = 0, touchEndX = 0, touchEndY = 0;
// ----- 게임 핵심 로직 -----
function initializeBoard() {
gameBoard.innerHTML = ''; // 기존 타일 초기화
for (let i = 0; i < gridSize * gridSize; i++) {
const tile = document.createElement('div');
tile.className = 'tile';
gameBoard.appendChild(tile);
}
board = Array(gridSize * gridSize).fill(0);
addNumber();
addNumber();
updateBoard();
}
function updateBoard() {
const tiles = gameBoard.children;
for (let i = 0; i < board.length; i++) {
const value = board[i];
const tile = tiles[i];
tile.textContent = value === 0 ? '' : value;
tile.className = 'tile' + (value > 0 ? ' tile-' + value : '');
}
scoreDisplay.textContent = score;
}
function addNumber() {
const available = board.map((val, i) => val === 0 ? i : -1).filter(i => i !== -1);
if (available.length > 0) {
const spot = available[Math.floor(Math.random() * available.length)];
board[spot] = Math.random() < 0.9 ? 2 : 4;
}
}
// ----- 타일 이동 및 병합 로직 -----
function moveRow(row) {
let arr = row.filter(val => val);
for (let i = 0; i < arr.length - 1; i++) {
if (arr[i] === arr[i + 1]) {
arr[i] *= 2;
score += arr[i];
arr[i + 1] = 0;
}
}
arr = arr.filter(val => val);
const missing = gridSize - arr.length;
const zeros = Array(missing).fill(0);
return arr.concat(zeros);
}
function moveLeft() {
let changed = false;
for (let i = 0; i < gridSize; i++) {
const rowStart = i * gridSize;
const row = board.slice(rowStart, rowStart + gridSize);
const newRow = moveRow(row);
if (JSON.stringify(row) !== JSON.stringify(newRow)) changed = true;
board.splice(rowStart, gridSize, ...newRow);
}
return changed;
}
function moveRight() {
let changed = false;
for (let i = 0; i < gridSize; i++) {
const rowStart = i * gridSize;
const row = board.slice(rowStart, rowStart + gridSize).reverse();
const newRow = moveRow(row).reverse();
if (JSON.stringify(board.slice(rowStart, rowStart + gridSize)) !== JSON.stringify(newRow)) changed = true;
board.splice(rowStart, gridSize, ...newRow);
}
return changed;
}
function moveUp() {
let changed = false;
for (let i = 0; i < gridSize; i++) {
const col = [board[i], board[i + gridSize], board[i + gridSize * 2], board[i + gridSize * 3]];
const newCol = moveRow(col);
if (JSON.stringify(col) !== JSON.stringify(newCol)) changed = true;
for (let j = 0; j < gridSize; j++) {
board[i + j * gridSize] = newCol[j];
}
}
return changed;
}
function moveDown() {
let changed = false;
for (let i = 0; i < gridSize; i++) {
const col = [board[i], board[i + gridSize], board[i + gridSize * 2], board[i + gridSize * 3]].reverse();
const newCol = moveRow(col).reverse();
if (JSON.stringify([board[i], board[i + gridSize], board[i + gridSize * 2], board[i + gridSize * 3]]) !== JSON.stringify(newCol)) changed = true;
for (let j = 0; j < gridSize; j++) {
board[i + j * gridSize] = newCol[j];
}
}
return changed;
}
// ----- 게임 상태 관리 -----
function isGameOver() {
if (!board.includes(0)) {
for (let i = 0; i < gridSize; i++) {
for (let j = 0; j < gridSize; j++) {
const current = board[i * gridSize + j];
if ((j < gridSize - 1 && current === board[i * gridSize + j + 1]) ||
(i < gridSize - 1 && current === board[(i + 1) * gridSize + j])) {
return false;
}
}
}
return true;
}
return false;
}
function handleMove(moveFunction) {
if (moveFunction()) {
addNumber();
updateBoard();
if (isGameOver()) {
finalScoreDisplay.textContent = score;
gameOverPopup.style.display = 'flex';
}
}
}
// ----- 이벤트 리스너 -----
document.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowUp': handleMove(moveUp); break;
case 'ArrowDown': handleMove(moveDown); break;
case 'ArrowLeft': handleMove(moveLeft); break;
case 'ArrowRight': handleMove(moveRight); break;
}
});
gameBoard.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
touchStartY = e.changedTouches[0].screenY;
});
gameBoard.addEventListener('touchmove', (e) => e.preventDefault(), { passive: false });
gameBoard.addEventListener('touchend', (e) => {
touchEndX = e.changedTouches[0].screenX;
touchEndY = e.changedTouches[0].screenY;
handleSwipe();
});
function handleSwipe() {
const deltaX = touchEndX - touchStartX;
const deltaY = touchEndY - touchStartY;
const swipeThreshold = 30;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
if (Math.abs(deltaX) > swipeThreshold) {
handleMove(deltaX > 0 ? moveRight : moveLeft);
}
} else {
if (Math.abs(deltaY) > swipeThreshold) {
handleMove(deltaY > 0 ? moveDown : moveUp);
}
}
}
// ----- 랭킹 API 연동 -----
saveScoreButton.addEventListener('click', async () => {
const playerName = playerNameInput.value.trim();
if (playerName === "") return alert("이름을 입력해주세요.");
const newScore = { gameId: currentGameId, name: playerName, score: score };
try {
const response = await fetch(getMainPath() + '/rank/ranks', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(newScore),
});
if (!response.ok) throw new Error('점수 저장 실패');
gameOverPopup.style.display = 'none';
playerNameInput.value = '';
score = 0; // 점수 초기화
updateRankingList();
initializeBoard();
} catch (error) {
console.error('Error:', error);
alert('서버와 통신 중 오류가 발생했습니다.');
}
});
async function updateRankingList() {
rankingList.innerHTML = '';
try {
const response = await fetch(getMainPath() +`/rank/ranks/${currentGameId}`);
if (!response.ok) throw new Error('랭킹 로딩 실패');
const rankings = await response.json();
if (rankings.length === 0) {
rankingList.innerHTML = '<li>등록된 랭킹이 없습니다.</li>';
return;
}
rankings.forEach((rank, index) => {
const li = document.createElement('li');
li.innerHTML = `<span>${index + 1}. ${rank.name}</span><strong>${rank.score}점</strong>`;
rankingList.appendChild(li);
});
} catch (error) {
console.error('Error:', error);
rankingList.innerHTML = '<li>랭킹을 불러올 수 없습니다.</li>';
}
}
// ----- 게임 시작 -----
updateRankingList();
initializeBoard();
});
// document.addEventListener('DOMContentLoaded', () => {
// // ... (DOM 요소 가져오기 - 동일)
// const gameBoard = document.getElementById('game-board');
// const scoreDisplay = document.getElementById('score');
// const gameOverPopup = document.getElementById('game-over-popup');
// 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과 일치)
// const currentContextId = null; // 2048은 별도 컨텍스트 ID가 없음
//
// let gridSize = 4;
// let board = [];
// let score = 0;
// let touchStartX = 0, touchStartY = 0, touchEndX = 0, touchEndY = 0;
//
// // ----- 게임 핵심 로직 -----
// function initializeBoard() {
// gameBoard.innerHTML = ''; // 기존 타일 초기화
// for (let i = 0; i < gridSize * gridSize; i++) {
// const tile = document.createElement('div');
// tile.className = 'tile';
// gameBoard.appendChild(tile);
// }
// board = Array(gridSize * gridSize).fill(0);
// addNumber();
// addNumber();
// updateBoard();
// }
//
// function updateBoard() {
// const tiles = gameBoard.children;
// for (let i = 0; i < board.length; i++) {
// const value = board[i];
// const tile = tiles[i];
// tile.textContent = value === 0 ? '' : value;
// tile.className = 'tile' + (value > 0 ? ' tile-' + value : '');
// }
// scoreDisplay.textContent = score;
// }
//
// function addNumber() {
// const available = board.map((val, i) => val === 0 ? i : -1).filter(i => i !== -1);
// if (available.length > 0) {
// const spot = available[Math.floor(Math.random() * available.length)];
// board[spot] = Math.random() < 0.9 ? 2 : 4;
// }
// }
//
// // ----- 타일 이동 및 병합 로직 -----
// function moveRow(row) {
// let arr = row.filter(val => val);
// for (let i = 0; i < arr.length - 1; i++) {
// if (arr[i] === arr[i + 1]) {
// arr[i] *= 2;
// score += arr[i];
// arr[i + 1] = 0;
// }
// }
// arr = arr.filter(val => val);
// const missing = gridSize - arr.length;
// const zeros = Array(missing).fill(0);
// return arr.concat(zeros);
// }
//
// function moveLeft() {
// let changed = false;
// for (let i = 0; i < gridSize; i++) {
// const rowStart = i * gridSize;
// const row = board.slice(rowStart, rowStart + gridSize);
// const newRow = moveRow(row);
// if (JSON.stringify(row) !== JSON.stringify(newRow)) changed = true;
// board.splice(rowStart, gridSize, ...newRow);
// }
// return changed;
// }
//
// function moveRight() {
// let changed = false;
// for (let i = 0; i < gridSize; i++) {
// const rowStart = i * gridSize;
// const row = board.slice(rowStart, rowStart + gridSize).reverse();
// const newRow = moveRow(row).reverse();
// if (JSON.stringify(board.slice(rowStart, rowStart + gridSize)) !== JSON.stringify(newRow)) changed = true;
// board.splice(rowStart, gridSize, ...newRow);
// }
// return changed;
// }
//
// function moveUp() {
// let changed = false;
// for (let i = 0; i < gridSize; i++) {
// const col = [board[i], board[i + gridSize], board[i + gridSize * 2], board[i + gridSize * 3]];
// const newCol = moveRow(col);
// if (JSON.stringify(col) !== JSON.stringify(newCol)) changed = true;
// for (let j = 0; j < gridSize; j++) {
// board[i + j * gridSize] = newCol[j];
// }
// }
// return changed;
// }
//
// function moveDown() {
// let changed = false;
// for (let i = 0; i < gridSize; i++) {
// const col = [board[i], board[i + gridSize], board[i + gridSize * 2], board[i + gridSize * 3]].reverse();
// const newCol = moveRow(col).reverse();
// if (JSON.stringify([board[i], board[i + gridSize], board[i + gridSize * 2], board[i + gridSize * 3]]) !== JSON.stringify(newCol)) changed = true;
// for (let j = 0; j < gridSize; j++) {
// board[i + j * gridSize] = newCol[j];
// }
// }
// return changed;
// }
//
// // ----- 게임 상태 관리 -----
// function isGameOver() {
// if (!board.includes(0)) {
// for (let i = 0; i < gridSize; i++) {
// for (let j = 0; j < gridSize; j++) {
// const current = board[i * gridSize + j];
// if ((j < gridSize - 1 && current === board[i * gridSize + j + 1]) ||
// (i < gridSize - 1 && current === board[(i + 1) * gridSize + j])) {
// return false;
// }
// }
// }
// return true;
// }
// return false;
// }
//
// function handleMove(moveFunction) {
// if (moveFunction()) {
// addNumber();
// updateBoard();
// if (isGameOver()) {
// finalScoreDisplay.textContent = score;
// gameOverPopup.style.display = 'flex';
// }
// }
// }
//
// // ----- 이벤트 리스너 -----
// document.addEventListener('keydown', (e) => {
// switch (e.key) {
// case 'ArrowUp': handleMove(moveUp); break;
// case 'ArrowDown': handleMove(moveDown); break;
// case 'ArrowLeft': handleMove(moveLeft); break;
// case 'ArrowRight': handleMove(moveRight); break;
// }
// });
//
// gameBoard.addEventListener('touchstart', (e) => {
// touchStartX = e.changedTouches[0].screenX;
// touchStartY = e.changedTouches[0].screenY;
// });
//
// gameBoard.addEventListener('touchmove', (e) => e.preventDefault(), { passive: false });
//
// gameBoard.addEventListener('touchend', (e) => {
// touchEndX = e.changedTouches[0].screenX;
// touchEndY = e.changedTouches[0].screenY;
// handleSwipe();
// });
//
// function handleSwipe() {
// const deltaX = touchEndX - touchStartX;
// const deltaY = touchEndY - touchStartY;
// const swipeThreshold = 30;
//
// if (Math.abs(deltaX) > Math.abs(deltaY)) {
// if (Math.abs(deltaX) > swipeThreshold) {
// handleMove(deltaX > 0 ? moveRight : moveLeft);
// }
// } else {
// if (Math.abs(deltaY) > swipeThreshold) {
// handleMove(deltaY > 0 ? moveDown : moveUp);
// }
// }
// }
//
// // ----- 랭킹 API 연동 -----
// saveScoreButton.addEventListener('click', async () => {
// const playerName = playerNameInput.value.trim();
// if (playerName === "") return alert("이름을 입력해주세요.");
//
// try {
// // (★ 수정) user.js의 공통 submitRank 함수 호출
// // 2048의 주 점수(primaryScore)는 score, 보조 점수(secondaryScore)는 없음.
// await submitRank(currentGameType, currentContextId, playerName, score, null);
//
// gameOverPopup.style.display = 'none';
// playerNameInput.value = '';
// score = 0;
// updateRankingList(); // 랭킹 리스트 새로고침
// initializeBoard(); // 새 게임 시작
//
// } catch (error) {
// console.error('Error submitting rank:', error);
// alert('랭킹 등록 중 오류가 발생했습니다: ' + 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();
// });
File diff suppressed because it is too large Load Diff
+694
View File
@@ -0,0 +1,694 @@
// /**
// * ==============================================
// * 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.top; // (오타 수정) 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}`;
// }
// });
// });
// });
-527
View File
@@ -1,527 +0,0 @@
/**
* Nonogram Game Logic - 최종 통합 및 수정 완료 버전
* * ## 포함된 모든 기능:
* 1. 동적 셀 크기 계산 및 가독성을 위한 넓은 힌트 영역
* 2. 게임 보드, 힌트, 상호작용 그리드 렌더링
* 3. 화면 크기에 맞는 반응형 보드 크기 조절
* 4. 사각형 선택 드래그 기능 등 모든 사용자 상호작용 처리
* 5. 게임 규칙 (포인트, 힌트, 실수 페널티, 승리/패배 조건)
* 6. '칠한 칸' 기준으로만 라인 완성 및 잠금 처리
* 7. 사용자가 마지막으로 수정한 라인만 검사하는 최적화
*/
document.addEventListener('DOMContentLoaded', () => {
// 백엔드(Thymeleaf)로부터 puzzleData가 제대로 전달되었는지 확인
if (typeof puzzleData === 'undefined' || !puzzleData) {
document.getElementById('game-board').innerHTML = '<h2>오류: 퍼즐 데이터를 불러올 수 없습니다.</h2>';
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 isDragging = false;
let dragAction = null; // 'fill', 'mark', 'clear'
let startCell = null; // 드래그 시작 셀 좌표 {row, col}
let lastHoveredCell = null; // 마지막으로 마우스가 지나간 셀 좌표
let currentSelection = new Set(); // 현재 드래그로 선택된 셀들의 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);
/**
* Updates the currentMode based on the selected radio button
*/
function updateMode() {
currentMode = document.querySelector('input[name="play-mode"]:checked').value;
}
/**
* 폰트 높이와 두 자릿수 숫자 너비를 기준으로 최적의 셀 크기를 계산합니다.
* @returns {number} 계산된 각 그리드 셀의 크기 (px)
*/
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;
}
/**
* 계산된 셀 크기를 사용하여 전체 게임 보드를 그립니다.
* @param {number} cellSize - 계산된 셀의 통일된 크기
*/
function drawBoard(cellSize) {
const rowClueAreaWidth = cellSize * 2;
const colClueAreaHeight = cellSize * 2;
gameBoard.style.gridTemplateColumns = `${rowClueAreaWidth}px 1fr`;
gameBoard.style.gridTemplateRows = `${colClueAreaHeight}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);
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);
}
}
/**
* (★ REVISED) Event Listeners for Mouse and Touch
*/
function attachEventListeners(grid) {
// --- MOUSE EVENTS ---
grid.addEventListener('mousedown', (e) => handleDragStart(e));
grid.addEventListener('mouseover', (e) => handleDragMove(e));
grid.addEventListener('contextmenu', (e) => e.preventDefault());
// --- TOUCH EVENTS ---
grid.addEventListener('touchstart', (e) => handleDragStart(e), { passive: false });
grid.addEventListener('touchmove', (e) => handleDragMove(e), { passive: false });
}
// End drag for both mouse and touch
window.addEventListener('mouseup', () => handleDragEnd());
window.addEventListener('touchend', () => handleDragEnd());
// Listen for changes on the mode selector
modeSelector.addEventListener('change', updateMode);
/**
* (★ NEW) Handles the start of a drag (mousedown or touchstart)
*/
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';
// UI를 실제 행동에 맞춰 동기화
document.querySelector('input[name="play-mode"][value="fill"]').checked = true;
} else if (e.button === 2) { // 우클릭
dragAction = (currentState === -1) ? 'clear' : 'mark';
// UI를 실제 행동에 맞춰 동기화
document.querySelector('input[name="play-mode"][value="mark"]').checked = true;
}
} else { // 터치 이벤트일 경우 ('touchstart')
// 현재 선택된 라디오 버튼 모드를 읽어옴
const currentMode = document.querySelector('input[name="play-mode"]:checked').value;
if (currentMode === 'fill') {
dragAction = (currentState === 1) ? 'clear' : 'fill';
} else { // 'mark' 모드
dragAction = (currentState === -1) ? 'clear' : 'mark';
}
}
// 드래그 시작점 기록 및 시각적 피드백 시작
startCell = { row: parseInt(cell.dataset.row), col: parseInt(cell.dataset.col) };
lastHoveredCell = startCell;
updateSelectionVisuals();
}
/**
* (★ NEW) Handles movement during a drag (mouseover or touchmove)
*/
function handleDragMove(e) {
if (!isDragging) return;
e.preventDefault();
// For touch events, we need to find the element under the finger
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();
}
}
}
/**
* (★ NEW) Handles the end of a drag (mouseup or touchend)
*/
function handleDragEnd() {
if (!isDragging) return;
currentSelection.forEach(cell => updateCellState(cell, dragAction));
clearSelectionVisuals();
if (dragAction === 'fill' || dragAction === 'clear') {
checkAndLockCompletedLines(affectedRows, affectedCols);
}
checkWinCondition();
// Reset state
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);
}
/**
* 성공/실패 모달을 동적으로 생성하여 표시
*/
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;
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 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;
// --- (★ 핵심) 애니메이션 위치 및 크기 계산 ---
// 1. 렌더링된 퍼즐 격자와 뷰포트의 실제 위치/크기 정보를 가져옵니다.
const gridRect = puzzleGridContainer.getBoundingClientRect();
const viewportRect = viewport.getBoundingClientRect();
// 2. 뷰포트를 기준으로 퍼즐 격자의 상대적인 위치(top, left)를 계산합니다.
const top = gridRect.top - viewportRect.top;
const left = gridRect.left - viewportRect.left;
// 3. 애니메이션 이미지들에 계산된 위치와 크기를 적용합니다.
[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: '퍼즐을 완성했습니다!',
buttons: [
{ text: '다른 문제 풀기', class: 'primary', action: () => window.location.href = '/puzzle/play' },
{ text: '홈으로 (Home)', 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를 통해 상태를 변경합니다.
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(); // Set initial mode
requestAnimationFrame(() => {
fitBoardToScreen();
window.addEventListener('resize', fitBoardToScreen);
});
});
File diff suppressed because it is too large Load Diff
+363 -356
View File
@@ -1,356 +1,363 @@
document.addEventListener('DOMContentLoaded', () => {
// DOM 요소
const setupContainer = document.getElementById('setup-container');
const gameContainer = document.getElementById('game-container');
const startBtn = document.getElementById('start-btn');
const boardElement = document.getElementById('sudoku-board');
const timerElement = document.getElementById('timer');
const scoreElement = document.getElementById('score');
const hintBtn = document.getElementById('hint-btn');
const undoBtn = document.getElementById('undo-btn');
const completeBtn = document.getElementById('complete-btn');
const numberInputButtons = document.getElementById('number-input-buttons');
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');
// 게임 상태 변수
let currentPuzzleId = null;
let solvedPuzzle = null;
let timerInterval = null;
let secondsElapsed = 0;
let selectedNumber = null;
let focusedCell = null;
let score = 5;
let history = [];
// --- 게임 초기화 및 시작 ---
startBtn.addEventListener('click', async () => {
const difficulty = document.getElementById('difficulty-select').value;
try {
const response = await fetch(`/sudoku/start?difficulty=${difficulty}`);
if (!response.ok) throw new Error('서버에서 게임 데이터를 가져오지 못했습니다.');
const gameData = await response.json();
currentPuzzleId = gameData.puzzleId;
solvedPuzzle = gameData.solution;
history = [];
score = 5;
updateScoreDisplay();
renderBoard(gameData.question);
startTimer();
updateButtonStates();
setupContainer.classList.add('hidden');
gameContainer.classList.remove('hidden');
numberInputButtons.classList.remove('hidden');
gameOverModal.classList.add('hidden');
} catch (error) {
alert('게임 로딩에 실패했습니다: ' + error.message);
console.error(error);
}
});
function renderBoard(puzzleString) {
boardElement.innerHTML = '';
for (let i = 0; i < 81; i++) {
const cell = document.createElement('div');
cell.classList.add('cell');
cell.dataset.index = i;
if (puzzleString[i] !== '0') {
cell.textContent = puzzleString[i];
} else {
cell.classList.add('editable');
}
boardElement.appendChild(cell);
}
}
function startTimer() {
secondsElapsed = 0;
timerElement.textContent = '00:00';
clearInterval(timerInterval);
timerInterval = setInterval(() => {
secondsElapsed++;
const minutes = Math.floor(secondsElapsed / 60).toString().padStart(2, '0');
const seconds = (secondsElapsed % 60).toString().padStart(2, '0');
timerElement.textContent = `${minutes}:${seconds}`;
}, 1000);
}
function updateScoreDisplay() {
scoreElement.textContent = `SCORE: ${score}`;
if (score <= 0) {
clearInterval(timerInterval);
gameOverModal.classList.remove('hidden');
}
}
function updateButtonStates() {
const counts = {};
for (let i = 1; i <= 9; i++) counts[i] = 0;
boardElement.querySelectorAll('.cell').forEach(cell => {
const num = cell.textContent;
if (num && counts[num] !== undefined) counts[num]++;
});
for (let i = 1; i <= 9; i++) {
const btn = numberInputButtons.querySelector(`.num-btn[data-number="${i}"]`);
if (btn) {
if (counts[i] >= 9) {
btn.classList.add('completed');
if (selectedNumber == i) {
selectedNumber = null;
btn.classList.remove('selected');
}
} else {
btn.classList.remove('completed');
}
}
}
}
// --- 게임 플레이 이벤트 핸들링 ---
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;
if (selectedNumber) target.classList.add('selected');
}
highlightCells();
});
boardElement.addEventListener('click', (event) => {
const targetCell = event.target.closest('.cell.editable');
if (!targetCell) {
if (focusedCell) focusedCell = null;
highlightCells();
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();
});
hintBtn.addEventListener('click', () => {
if (score <= 0) return;
const emptyCells = Array.from(boardElement.querySelectorAll('.cell.editable')).filter(cell => !cell.textContent);
if (emptyCells.length === 0) {
alert('모든 칸이 채워져 있습니다.');
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();
});
function undoAction() {
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) {
cell.classList.add('editable');
}
validateCell(cell, false);
updateButtonStates();
highlightCells();
}
}
function recordAction(cell, previousValue, newValue, wasHint = false) {
history.push({ index: cell.dataset.index, previousValue, newValue, wasHint });
}
function validateCell(cell, deductPoint = true) {
if (!cell.textContent) {
cell.classList.remove('incorrect');
return;
}
const cellIndex = parseInt(cell.dataset.index);
const isCorrect = (cell.textContent === solvedPuzzle[cellIndex]);
if (!isCorrect) {
cell.classList.add('incorrect');
if (deductPoint && score > 0) {
score--;
updateScoreDisplay();
}
} else {
cell.classList.remove('incorrect');
}
}
// --- 하이라이트 기능 ---
function highlightCells() {
document.querySelectorAll('.cell').forEach(cell => {
cell.classList.remove('highlight-focused', 'highlight-same-number', 'highlight-selected-number');
});
if (focusedCell) {
focusedCell.classList.add('highlight-focused');
const focusedValue = focusedCell.textContent;
if (focusedValue) {
document.querySelectorAll('.cell').forEach(cell => {
if (cell.textContent === focusedValue) cell.classList.add('highlight-same-number');
});
}
}
if (selectedNumber) {
document.querySelectorAll('.cell').forEach(cell => {
if (cell.textContent === selectedNumber) cell.classList.add('highlight-selected-number');
});
}
}
// --- 게임 완료 및 모달 ---
async function checkSolution() {
let answerString = "";
boardElement.childNodes.forEach(cell => {
answerString += cell.textContent || '0';
});
if (answerString.includes('0')) {
alert('모든 칸을 채워주세요!');
return;
}
try {
const response = await fetch('/sudoku/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ puzzleId: currentPuzzleId, answer: answerString })
});
const result = await response.json();
if (result.correct) {
clearInterval(timerInterval);
alert('🎉 정답입니다!');
showRankingModal();
} else {
alert('🤔 틀린 부분이 있습니다. 다시 확인해주세요.');
}
} catch (error) {
console.error('정답 확인 중 오류 발생:', error);
alert('정답 확인 중 오류가 발생했습니다.');
}
}
function checkIfBoardIsFull() {
const emptyEditableCells = boardElement.querySelector('.cell.editable:empty');
if (!emptyEditableCells) {
checkSolution();
}
}
completeBtn.addEventListener('click', checkSolution);
async function showRankingModal() {
modalOverlay.classList.remove('hidden');
document.getElementById('username-input').value = '';
submitRankBtn.disabled = false;
try {
const response = await fetch(`/sudoku/ranking/${currentPuzzleId}`);
const rankings = await response.json();
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.completionTime / 60).toString().padStart(2, '0');
const seconds = (rank.completionTime % 60).toString().padStart(2, '0');
li.innerHTML = `<span>${index + 1}위: ${rank.userName}</span> <span>${minutes}:${seconds}</span>`;
rankingList.appendChild(li);
});
}
} catch (error) {
console.error('랭킹 조회 중 오류 발생:', error);
rankingList.innerHTML = '<li>랭킹을 불러올 수 없습니다.</li>';
}
}
submitRankBtn.addEventListener('click', async () => {
const userName = document.getElementById('username-input').value.trim();
if (!userName) return alert('이름을 입력해주세요.');
try {
await fetch('/sudoku/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
puzzleId: currentPuzzleId,
userName: userName,
completionTime: secondsElapsed
})
});
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');
clearInterval(timerInterval);
selectedNumber = null;
focusedCell = null;
document.querySelectorAll('.cell').forEach(cell => {
cell.classList.remove('highlight-focused', 'highlight-same-number', 'highlight-selected-number');
});
document.querySelectorAll('.num-btn').forEach(btn => btn.classList.remove('selected', 'completed'));
}
closeModalBtn.addEventListener('click', () => {
modalOverlay.classList.add('hidden');
resetGameView();
});
retryBtn.addEventListener('click', () => {
gameOverModal.classList.add('hidden');
resetGameView();
});
});
// document.addEventListener('DOMContentLoaded', () => {
// // DOM 요소
// const setupContainer = document.getElementById('setup-container');
// const gameContainer = document.getElementById('game-container');
// const startBtn = document.getElementById('start-btn');
// const boardElement = document.getElementById('sudoku-board');
// const timerElement = document.getElementById('timer');
// const scoreElement = document.getElementById('score');
// const hintBtn = document.getElementById('hint-btn');
// const undoBtn = document.getElementById('undo-btn');
// const completeBtn = document.getElementById('complete-btn');
// const numberInputButtons = document.getElementById('number-input-buttons');
// 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');
//
// // 게임 상태 변수
// let currentPuzzleId = null;
// let solvedPuzzle = null;
// let timerInterval = null;
// let secondsElapsed = 0;
// let selectedNumber = null;
// let focusedCell = null;
// 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();
//
// currentContextId = gameData.puzzleId;
// solvedPuzzle = gameData.solution;
//
// history = [];
// score = 5;
// updateScoreDisplay();
//
// renderBoard(gameData.question);
// startTimer();
// updateButtonStates();
//
// setupContainer.classList.add('hidden');
// gameContainer.classList.remove('hidden');
// numberInputButtons.classList.remove('hidden');
// gameOverModal.classList.add('hidden');
// } catch (error) {
// alert('게임 로딩에 실패했습니다: ' + error.message);
// console.error(error);
// }
// });
//
// function renderBoard(puzzleString) {
// boardElement.innerHTML = '';
// for (let i = 0; i < 81; i++) {
// const cell = document.createElement('div');
// cell.classList.add('cell');
// cell.dataset.index = i;
//
// if (puzzleString[i] !== '0') {
// cell.textContent = puzzleString[i];
// } else {
// cell.classList.add('editable');
// }
// boardElement.appendChild(cell);
// }
// }
//
// function startTimer() {
// secondsElapsed = 0;
// timerElement.textContent = '00:00';
// clearInterval(timerInterval);
// timerInterval = setInterval(() => {
// secondsElapsed++;
// const minutes = Math.floor(secondsElapsed / 60).toString().padStart(2, '0');
// const seconds = (secondsElapsed % 60).toString().padStart(2, '0');
// timerElement.textContent = `${minutes}:${seconds}`;
// }, 1000);
// }
//
// function updateScoreDisplay() {
// scoreElement.textContent = `SCORE: ${score}`;
// if (score <= 0) {
// clearInterval(timerInterval);
// gameOverModal.classList.remove('hidden');
// }
// }
//
// function updateButtonStates() {
// const counts = {};
// for (let i = 1; i <= 9; i++) counts[i] = 0;
// boardElement.querySelectorAll('.cell').forEach(cell => {
// const num = cell.textContent;
// if (num && counts[num] !== undefined) counts[num]++;
// });
// for (let i = 1; i <= 9; i++) {
// const btn = numberInputButtons.querySelector(`.num-btn[data-number="${i}"]`);
// if (btn) {
// if (counts[i] >= 9) {
// btn.classList.add('completed');
// if (selectedNumber == i) {
// selectedNumber = null;
// btn.classList.remove('selected');
// }
// } else {
// btn.classList.remove('completed');
// }
// }
// }
// }
//
// // --- 게임 플레이 이벤트 핸들링 ---
// 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;
// if (selectedNumber) target.classList.add('selected');
// }
// highlightCells();
// });
//
// boardElement.addEventListener('click', (event) => {
// const targetCell = event.target.closest('.cell.editable');
// if (!targetCell) {
// if (focusedCell) focusedCell = null;
// highlightCells();
// 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();
// });
//
// hintBtn.addEventListener('click', () => {
// if (score <= 0) return;
// const emptyCells = Array.from(boardElement.querySelectorAll('.cell.editable')).filter(cell => !cell.textContent);
// if (emptyCells.length === 0) {
// alert('모든 칸이 채워져 있습니다.');
// 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();
// });
//
// function undoAction() {
// 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) {
// cell.classList.add('editable');
// }
// validateCell(cell, false);
// updateButtonStates();
// highlightCells();
// }
// }
//
// function recordAction(cell, previousValue, newValue, wasHint = false) {
// history.push({ index: cell.dataset.index, previousValue, newValue, wasHint });
// }
//
// function validateCell(cell, deductPoint = true) {
// if (!cell.textContent) {
// cell.classList.remove('incorrect');
// return;
// }
// const cellIndex = parseInt(cell.dataset.index);
// const isCorrect = (cell.textContent === solvedPuzzle[cellIndex]);
// if (!isCorrect) {
// cell.classList.add('incorrect');
// if (deductPoint && score > 0) {
// score--;
// updateScoreDisplay();
// }
// } else {
// cell.classList.remove('incorrect');
// }
// }
//
// // --- 하이라이트 기능 ---
// function highlightCells() {
// document.querySelectorAll('.cell').forEach(cell => {
// cell.classList.remove('highlight-focused', 'highlight-same-number', 'highlight-selected-number');
// });
// if (focusedCell) {
// focusedCell.classList.add('highlight-focused');
// const focusedValue = focusedCell.textContent;
// if (focusedValue) {
// document.querySelectorAll('.cell').forEach(cell => {
// if (cell.textContent === focusedValue) cell.classList.add('highlight-same-number');
// });
// }
// }
// if (selectedNumber) {
// document.querySelectorAll('.cell').forEach(cell => {
// if (cell.textContent === selectedNumber) cell.classList.add('highlight-selected-number');
// });
// }
// }
//
// // --- 게임 완료 및 모달 ---
// async function checkSolution() {
// let answerString = "";
// boardElement.childNodes.forEach(cell => {
// answerString += cell.textContent || '0';
// });
//
// if (answerString.includes('0')) {
// alert('모든 칸을 채워주세요!');
// return;
// }
//
// try {
// // (★ 수정) API 경로 변경: /sudoku/validate -> /puzzle/sudoku/validate
// // (★ 수정) contextId(puzzleId) 변수 사용
// const response = await fetch('/puzzle/sudoku/validate', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ puzzleId: currentContextId, answer: answerString })
// });
// const result = await response.json();
// if (result.correct) {
// clearInterval(timerInterval);
// alert('🎉 정답입니다!');
// showRankingModal(); // 랭킹 모달 표시
// } else {
// alert('🤔 틀린 부분이 있습니다. 다시 확인해주세요.');
// }
// } catch (error) {
// console.error('정답 확인 중 오류 발생:', error);
// alert('정답 확인 중 오류가 발생했습니다.');
// }
// }
//
// function checkIfBoardIsFull() {
// const emptyEditableCells = boardElement.querySelector('.cell.editable:empty');
// if (!emptyEditableCells) {
// checkSolution();
// }
// }
// 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(ContextId) 전달)
// const rankings = await fetchRanks(currentGameType, currentContextId);
//
// rankingList.innerHTML = '';
// if (rankings.length === 0) {
// rankingList.innerHTML = '<li>아직 등록된 랭킹이 없습니다.</li>';
// } else {
// rankings.forEach((rank, index) => {
// const li = document.createElement('li');
// // (★ 수정) 공통 모델 필드(primaryScore)를 시간(초)으로 변환
// 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 함수 호출
// // 스도쿠의 주 점수(primaryScore)는 완료 시간(secondsElapsed), 보조 점수는 없음.
// await submitRank(currentGameType, currentContextId, 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');
// clearInterval(timerInterval);
// selectedNumber = null;
// focusedCell = null;
// document.querySelectorAll('.cell').forEach(cell => {
// cell.classList.remove('highlight-focused', 'highlight-same-number', 'highlight-selected-number');
// });
// document.querySelectorAll('.num-btn').forEach(btn => btn.classList.remove('selected', 'completed'));
// }
//
// closeModalBtn.addEventListener('click', () => {
// modalOverlay.classList.add('hidden');
// resetGameView();
// });
//
// retryBtn.addEventListener('click', () => {
// gameOverModal.classList.add('hidden');
// resetGameView();
// });
// });₩
-166
View File
@@ -1,166 +0,0 @@
let currentPuzzleData = null;
// The success animation function
function showSuccessAnimation() {
if (!currentPuzzleData) return;
const puzzleContainer = document.getElementById('puzzle-container');
const grayscaleImg = document.getElementById('grayscale-reveal');
const originalImg = document.getElementById('original-reveal');
// 1. Set the image sources from the saved Base64 data
grayscaleImg.src = currentPuzzleData.grayscaleImage;
originalImg.src = currentPuzzleData.originalImage;
// 2. Hide the puzzle grid and fade in the grayscale image
puzzleContainer.style.transition = 'opacity 0.5s';
puzzleContainer.style.opacity = '0';
grayscaleImg.style.opacity = '1';
// 3. After a delay, fade out the grayscale and fade in the color image
setTimeout(() => {
grayscaleImg.style.opacity = '0';
originalImg.style.opacity = '1';
}, 2000); // 2-second delay
}
function drawPuzzle(puzzleData) {
const container = document.getElementById('puzzle-container');
container.innerHTML = ''; // Clear previous puzzle
const { solutionGrid, rowClues, colClues } = puzzleData;
const numRows = solutionGrid.length;
const numCols = solutionGrid[0].length;
// Set up the CSS Grid layout
container.style.gridTemplateColumns = `auto repeat(${numCols}, 1fr)`;
container.style.gridTemplateRows = `auto repeat(${numRows}, 1fr)`;
// 1. Create top-left empty corner
const corner = document.createElement('div');
corner.className = 'grid-cell';
container.appendChild(corner);
// 2. Create column clues (top row)
for (const clues of colClues) {
const clueCell = document.createElement('div');
clueCell.className = 'clue-cell';
clueCell.innerHTML = clues.join('<br>'); // Display clues vertically
container.appendChild(clueCell);
}
// 3. Create row clues and the solution grid
for (let i = 0; i < numRows; i++) {
// Create row clue cell for this row
const rowClueCell = document.createElement('div');
rowClueCell.className = 'clue-cell';
rowClueCell.textContent = rowClues[i].join(' ');
container.appendChild(rowClueCell);
// Create the solution cells for this row
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'); // Black square
} else {
cell.classList.add('empty'); // White square
}
container.appendChild(cell);
}
}
}
// Modify your existing click listener
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('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'); // Get the new button
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 = ''; // Clear old puzzle while loading
try {
const response = await fetch(getMainPath()+'/puzzle/upload.bjx', { // Make sure this URL is correct
method: 'POST',
body: formData,
});
if (response.ok) {
const puzzleData = await response.json();
statusDiv.textContent = '문제 생성 성공!';
// Call the new function to draw the puzzle!
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 {
// 백엔드에 DELETE 요청 보내기
const response = await fetch(getMainPath() + `/puzzle/${currentPuzzleData.id}.bjx`, {
method: 'DELETE',
});
if (response.ok) { // 204 No Content 포함
statusDiv.textContent = '퍼즐이 성공적으로 삭제되었습니다.';
// 화면 정리
puzzleContainer.innerHTML = '';
document.getElementById('success-animation-container').innerHTML = ''; // 이미지 컨테이너도 비움
testSuccessBtn.style.display = 'none';
deleteBtn.style.display = 'none';
currentPuzzleData = null;
} else {
statusDiv.textContent = `삭제 실패: 서버 오류 (${response.status})`;
}
} catch (error) {
console.error('삭제 중 네트워크 오류:', error);
statusDiv.textContent = '삭제 중 오류가 발생했습니다.';
}
});
playBtn.addEventListener('click', () => {
if (currentPuzzleData && currentPuzzleData.id) {
// Navigate to the play page with the puzzle ID
window.location.href = `/puzzle/play/${currentPuzzleData.id}`;
}
});
});
});
-77
View File
@@ -1,77 +0,0 @@
function onclickJoin(type, keyword) {
let user_id = document.getElementById('user_id')
let user_pw = document.getElementById('user_pw')
let user_pw_check = document.getElementById('user_pw_check')
let user_name = document.getElementById('user_name')
let user_email = document.getElementById('user_email')
var fields = [user_id,user_pw, user_pw_check, user_name, user_email]
var hasValues = true
const spPattern = /[~!@#$%<>^&*]/; //특수문자
const korean = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/; //한글
const eng = /[a-zA-Z]/; //영어
const numbers = /[0-9]/; //숫자
const email = /[A-za-z0-9\-][A-Za-z0-9_.\-]+@[A-za-z0-9\-][A-Za-z0-9\-]+\.[A-za-z0-9\-][A-za-z0-9\-]+/;
fields.forEach(function (field , idx , all) {
if ((field.value.length > 7 ||
(field===user_name && user_name.value.length > 2) ||
(field===user_id && user_id.value.length > 6)) &&
hasValues) {
const text = field.value
switch (field) {
case user_id :
if (korean.test(text)) {
hasValues = false
alert("id를 확인 해보슈.");
}
break;
case user_pw :
if (
korean.test(text) ||
false === numbers.test(text) ||
false === eng.test(text) ||
false === spPattern.test(text)
) {
hasValues = false
alert("pw 한글 노노 영문 숫자 특문(~!@#$%<>^&*) 섞으셈.");
}
break
case user_email : if(false === email.test(field.value)) {
hasValues = false
alert("email를 확인 해보슈.");
}
break
}
} else if (hasValues) {
hasValues = false
switch (field) {
case user_id : alert("id를 확인 해보슈.");break
case user_pw : alert("pw를 확인 해보슈.");break
case user_pw_check : alert("pw를 확인 해보슈.");break
case user_name : alert("name를 확인 해보슈.");break
case user_email : alert("email를 확인 해보슈.");break
}
}
})
if (hasValues) {
let data = {
'user_id': user_id.value,
'user_pw': user_pw.value,
'user_email': user_email.value,
'user_name': user_name.value
}
if (user_pw.value === user_pw_check.value) {
if(confirm(JSON.stringify(data) + "\n해당 내용으로\n유저 등록 하실??")) {
post("joinUser.bjx",type,JSON.stringify(data),keyword, function (resultData) {
alert(resultData)
})
} else {
}
} else {
alert("비번이 다름요")
}
}
}
@@ -13,7 +13,7 @@
<section class="wrapper style1">
<div class="container">
<div class="row justify-content-center">
<div class="col-8 col-12-narrower">
<div class="col-12">
<div id="content_inner">
<article>
<section th:each="post : ${postsPage.content}">
@@ -76,7 +76,7 @@
<button class="button" onclick="handleVote(this, 'like')">
👍 Like (<span class="like-count" th:text="${srcPost.voteCount}">0</span>)
</button>
<button class="button" onclick="handleVote(this, 'unlike')" style="margin-left: 1em;">
<button class="button" onclick="handleVote(this, 'unlike')">
👎 Unlike (<span class="unlike-count" th:text="${srcPost.unlikeCount}">0</span>)
</button>
</div>
+30 -20
View File
@@ -5,17 +5,14 @@
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
layout:decorate="~{layout/default_layout}">
<th:block layout:fragment="content" id="content">
<section id="banner">
<section id="banner"
th:styleappend="${randomBannerImage != null} ? 'background-image: url(\'' + @{${randomBannerImage}} + '\');' : ''">
<header>
<h2>Bum's: <em>짧은 헛소리 혹은 기사?! 링크 있으면 링크까지</a></em></h2>
<a href="#" class="button">더 읽으쉴?!<br>[Read More Gibberish]</a>
</header>
</section>
<!-- Highlights -->
<!-- Gigantic Heading -->
<section class="wrapper style2">
<div class="container">
<header class="major">
@@ -31,24 +28,38 @@
</header>
</div>
</section>
<!-- Posts -->
<section class="wrapper style2">
<section class="wrapper style1">
<div class="container">
<div class="row" th:each="row : ${Posts}">
<section class="col-6 col-12-narrower" th:each="post : ${row}">
<!-- <span th:text="${cell}"></span>-->
<div class="box post" onclick="goToViewer(this)" th:id="${post.id}">
<a href="#" class="image left"><img th:src="${#strings.length(post.thumb) > 0} ? ${post.thumb} : 'images/pic01.jpg'" alt="" /></a>
<div class="inner">
<h3 th:text="${#strings.length(post.title) > 0} ? ${post.title} : ('untitled[' + ${#temporals.format(T(java.time.Instant).ofEpochMilli(post.writeTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm:ss')} + ']')"></h3>
<p th:text="${#strings.abbreviate(post.html, 80)}" class="ellipsis"></p>
</div>
<div class="row">
<div class="col-12">
<div id="content_inner">
<article>
<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" />
</span>
<div class="inner">
<h3 style="display: flex; justify-content: space-between; align-items: center;">
<span th:text="${post.title != null and not #strings.isEmpty(post.title)} ? ${post.title} : 'untitled[' + ${#temporals.format(T(java.time.Instant).ofEpochMilli(post.writeTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm')} + ']'"></span>
<span style="font-size: 0.75em; color: #888; font-weight: normal; white-space: nowrap; margin-left: 1em;">
(읽음: <span th:text="${post.readCount}">0</span>)
</span>
</h3>
<p style="font-size: 0.9em; color: #555; margin-bottom: 0.5em;" th:text="${'by ' + post.writer}"></p>
<p th:text="${#strings.abbreviate(post.html, 80)}" class="ellipsis"></p>
</div>
</div>
</section>
</article>
</div>
</section>
</div>
</div>
</div>
</section>
<section class="wrapper style1">
<div class="container">
<div class="row gtr-200">
@@ -82,7 +93,6 @@
</div>
</div>
</section>
<!-- CTA -->
<section id="cta2" class="wrapper style3">
<div class="container">
<header>
@@ -91,4 +101,4 @@
</div>
</section>
</th:block>
</html>
</html>
@@ -5,16 +5,186 @@
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
layout:decorate="~{layout/default_layout}"
>
<th:block layout:fragment="head" id="head">
<script type="text/javascript" th:src="@{/js/2048.js}"></script>
<!-- <link th:href="@{/css/puzzle.css}" rel="stylesheet" />-->
<link th:href="@{/css/2048.css}" rel="stylesheet" />
<script th:inline="javascript">
</script>
<head>
<link th:href="@{/css/common_game_theme.css}" rel="stylesheet" />
<script type="text/javascript" th:src="@{/js/user.js}"></script>
</th:block >
<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;
margin-bottom: 20px;
}
/* =================================
게임 보드 (테마 적용)
================================= */
#game-board {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 2vw;
width: 95vw;
max-width: 500px; /* (★ 수정) 400px -> 500px (다른 게임과 통일) */
margin: 0 auto;
/* (★ 수정) 2048의 갈색/베이지 테마를 차가운 회색/파란색 테마로 변경 */
background-color: #b0bec5; /* #bbada0 (갈색) -> #b0bec5 (블루 그레이) */
padding: 2vw;
border-radius: 6px;
box-sizing: border-box;
aspect-ratio: 1 / 1;
touch-action: none;
/* (★ 추가) 공통 카드 UI와 유사한 그림자 효과 추가 */
box-shadow: 0 4px 10px rgba(0,0,0,0.08);
}
@media (min-width: 481px) {
#game-board {
grid-gap: 10px;
padding: 10px;
}
}
/* =================================
타일 공통 스타일 (테마 적용)
================================= */
.tile {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
border-radius: 3px;
/* (★ 수정) 빈 타일 색상 변경 */
background-color: #eceff1; /* #cdc1b4 (갈색) -> #eceff1 (밝은 블루 그레이) */
font-size: 5vw;
}
@media (min-width: 481px) {
.tile {
font-size: 30px;
}
}
/* =================================
타일 색상 (테마 적용)
================================= */
/* (★ 수정) 2, 4 타일은 베이지색 계열이라 테마와 충돌하므로 파란색 계열로 변경 */
.tile-2 { background-color: #e3f2fd; color: #333; } /* #eee4da (베이지) -> #e3f2fd (밝은 파랑) */
.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; }
/* =================================
게임 오버 팝업 (테마 적용)
================================= */
.popup-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
}
.popup {
/* (★ 수정) 배경색을 테마에 맞게 흰색으로 변경 */
background-color: #ffffff; /* #faf8ef (베이지) -> #ffffff (흰색) */
padding: 20px;
border-radius: 10px;
text-align: center;
width: 80vw;
max-width: 300px;
box-shadow: 0 4px 15px rgba(0,0,0,0.2); /* 흰색 배경이므로 그림자 추가 */
}
.popup input {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ccc;
border-radius: 5px;
box-sizing: border-box;
}
.popup button {
padding: 10px 20px;
/* (★ 삭제) background-color, color, border -> common_game_theme의 파란색 버튼 스타일을 상속받음 */
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>
<body>
<th:block layout:fragment="content">
<h1>2048</h1>
<p>화살표를 터치하거나 키보드 방향키를 이용해 타일을 합쳐보세요!</p>
@@ -24,18 +194,261 @@
</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 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>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', () => {
// ... (DOM 요소 가져오기 - 동일)
const gameBoard = document.getElementById('game-board');
const scoreDisplay = document.getElementById('score');
const gameOverPopup = document.getElementById('game-over-popup');
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과 일치)
const currentContextId = null; // 2048은 별도 컨텍스트 ID가 없음
let gridSize = 4;
let board = [];
let score = 0;
let touchStartX = 0, touchStartY = 0, touchEndX = 0, touchEndY = 0;
// ----- 게임 핵심 로직 -----
function initializeBoard() {
gameBoard.innerHTML = ''; // 기존 타일 초기화
for (let i = 0; i < gridSize * gridSize; i++) {
const tile = document.createElement('div');
tile.className = 'tile';
gameBoard.appendChild(tile);
}
board = Array(gridSize * gridSize).fill(0);
addNumber();
addNumber();
updateBoard();
}
function updateBoard() {
const tiles = gameBoard.children;
for (let i = 0; i < board.length; i++) {
const value = board[i];
const tile = tiles[i];
tile.textContent = value === 0 ? '' : value;
tile.className = 'tile' + (value > 0 ? ' tile-' + value : '');
}
scoreDisplay.textContent = score;
}
function addNumber() {
const available = board.map((val, i) => val === 0 ? i : -1).filter(i => i !== -1);
if (available.length > 0) {
const spot = available[Math.floor(Math.random() * available.length)];
board[spot] = Math.random() < 0.9 ? 2 : 4;
}
}
// ----- 타일 이동 및 병합 로직 -----
function moveRow(row) {
let arr = row.filter(val => val);
for (let i = 0; i < arr.length - 1; i++) {
if (arr[i] === arr[i + 1]) {
arr[i] *= 2;
score += arr[i];
arr[i + 1] = 0;
}
}
arr = arr.filter(val => val);
const missing = gridSize - arr.length;
const zeros = Array(missing).fill(0);
return arr.concat(zeros);
}
function moveLeft() {
let changed = false;
for (let i = 0; i < gridSize; i++) {
const rowStart = i * gridSize;
const row = board.slice(rowStart, rowStart + gridSize);
const newRow = moveRow(row);
if (JSON.stringify(row) !== JSON.stringify(newRow)) changed = true;
board.splice(rowStart, gridSize, ...newRow);
}
return changed;
}
function moveRight() {
let changed = false;
for (let i = 0; i < gridSize; i++) {
const rowStart = i * gridSize;
const row = board.slice(rowStart, rowStart + gridSize).reverse();
const newRow = moveRow(row).reverse();
if (JSON.stringify(board.slice(rowStart, rowStart + gridSize)) !== JSON.stringify(newRow)) changed = true;
board.splice(rowStart, gridSize, ...newRow);
}
return changed;
}
function moveUp() {
let changed = false;
for (let i = 0; i < gridSize; i++) {
const col = [board[i], board[i + gridSize], board[i + gridSize * 2], board[i + gridSize * 3]];
const newCol = moveRow(col);
if (JSON.stringify(col) !== JSON.stringify(newCol)) changed = true;
for (let j = 0; j < gridSize; j++) {
board[i + j * gridSize] = newCol[j];
}
}
return changed;
}
function moveDown() {
let changed = false;
for (let i = 0; i < gridSize; i++) {
const col = [board[i], board[i + gridSize], board[i + gridSize * 2], board[i + gridSize * 3]].reverse();
const newCol = moveRow(col).reverse();
if (JSON.stringify([board[i], board[i + gridSize], board[i + gridSize * 2], board[i + gridSize * 3]]) !== JSON.stringify(newCol)) changed = true;
for (let j = 0; j < gridSize; j++) {
board[i + j * gridSize] = newCol[j];
}
}
return changed;
}
// ----- 게임 상태 관리 -----
function isGameOver() {
if (!board.includes(0)) {
for (let i = 0; i < gridSize; i++) {
for (let j = 0; j < gridSize; j++) {
const current = board[i * gridSize + j];
if ((j < gridSize - 1 && current === board[i * gridSize + j + 1]) ||
(i < gridSize - 1 && current === board[(i + 1) * gridSize + j])) {
return false;
}
}
}
return true;
}
return false;
}
function handleMove(moveFunction) {
if (moveFunction()) {
addNumber();
updateBoard();
if (isGameOver()) {
finalScoreDisplay.textContent = score;
gameOverPopup.style.display = 'flex';
}
}
}
// ----- 이벤트 리스너 -----
document.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowUp': handleMove(moveUp); break;
case 'ArrowDown': handleMove(moveDown); break;
case 'ArrowLeft': handleMove(moveLeft); break;
case 'ArrowRight': handleMove(moveRight); break;
}
});
gameBoard.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
touchStartY = e.changedTouches[0].screenY;
});
gameBoard.addEventListener('touchmove', (e) => e.preventDefault(), { passive: false });
gameBoard.addEventListener('touchend', (e) => {
touchEndX = e.changedTouches[0].screenX;
touchEndY = e.changedTouches[0].screenY;
handleSwipe();
});
function handleSwipe() {
const deltaX = touchEndX - touchStartX;
const deltaY = touchEndY - touchStartY;
const swipeThreshold = 30;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
if (Math.abs(deltaX) > swipeThreshold) {
handleMove(deltaX > 0 ? moveRight : moveLeft);
}
} else {
if (Math.abs(deltaY) > swipeThreshold) {
handleMove(deltaY > 0 ? moveDown : moveUp);
}
}
}
// ----- 랭킹 API 연동 -----
saveScoreButton.addEventListener('click', async () => {
const playerName = playerNameInput.value.trim();
if (playerName === "") return alert("이름을 입력해주세요.");
try {
// (★ 수정) user.js의 공통 submitRank 함수 호출
// 2048의 주 점수(primaryScore)는 score, 보조 점수(secondaryScore)는 없음.
await submitRank(currentGameType, currentContextId, playerName, score, null);
gameOverPopup.style.display = 'none';
playerNameInput.value = '';
score = 0;
updateRankingList(); // 랭킹 리스트 새로고침
initializeBoard(); // 새 게임 시작
} catch (error) {
console.error('Error submitting rank:', error);
alert('랭킹 등록 중 오류가 발생했습니다: ' + 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>
</th:block>
</html>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -1,58 +0,0 @@
<!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">
<script type="text/javascript" th:src="@{/js/play.js}"></script>
<!-- <link th:href="@{/css/puzzle.css}" rel="stylesheet" />-->
<link th:href="@{/css/play.css}" rel="stylesheet" />
<script th:inline="javascript">
</script>
<script type="text/javascript" th:src="@{/js/user.js}"></script>
</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>
</th:block>
</html>
File diff suppressed because it is too large Load Diff
@@ -5,16 +5,253 @@
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
layout:decorate="~{layout/default_layout}"
>
<th:block layout:fragment="head" id="head">
<script type="text/javascript" th:src="@{/js/sudoku.js}"></script>
<!-- <link th:href="@{/css/puzzle.css}" rel="stylesheet" />-->
<link th:href="@{/css/sudoku.css}" rel="stylesheet" />
<script th:inline="javascript">
</script>
<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>
</th:block >
<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;
}
h1 {
font-size: 1.8em;
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;
}
/* 게임 정보 (점수, 타이머) */
.game-info {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding: 0 10px;
box-sizing: border-box;
font-size: 1.5em;
font-weight: bold;
}
#score { color: #007bff; }
#timer { color: #333; }
/* 스도쿠 보드 */
#sudoku-board {
display: grid;
grid-template-columns: repeat(9, 1fr);
grid-template-rows: repeat(9, 1fr);
width: 100%;
border: 3px solid #333;
aspect-ratio: 1 / 1;
}
.cell {
display: flex;
justify-content: center;
align-items: center;
font-size: clamp(1em, 4vw, 1.8em);
font-weight: bold;
color: #333;
border: 1px solid #ddd;
box-sizing: border-box;
cursor: pointer;
}
.cell:nth-child(3n) { border-right: 2px solid #333; }
.cell:nth-child(9n) { border-right-width: 1px; }
.cell:nth-child(n+19):nth-child(-n+27),
.cell:nth-child(n+46):nth-child(-n+54) {
border-bottom: 2px solid #333;
}
.cell:not(.editable) {
background-color: #f0f0f0;
color: #222;
cursor: default;
}
/* 하이라이트 & 오답 스타일 */
.cell.incorrect {
background-color: #ffdddd !important;
color: #d8000c !important;
}
.highlight-focused {
background-color: #dbeeff !important;
}
.highlight-same-number {
background-color: #e6e6e6 !important;
}
.highlight-selected-number {
background-color: #b3d7ff !important;
}
/* 숫자 입력 버튼 */
#number-input-buttons {
display: flex;
justify-content: space-between;
width: 100%;
margin-top: 15px;
gap: 1%;
}
#number-input-buttons .num-btn,
#number-input-buttons #undo-btn {
line-height: unset;
min-width: unset;
width: 9%;
aspect-ratio: 1/1;
font-size: clamp(1em, 4vw, 1.8em);
font-weight: bold;
border-radius: 8px;
background-color: #f0f0f0;
color: #333;
border: 1px solid #ccc;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
padding: 0;
transition: background-color 0.2s, transform 0.1s, opacity 0.2s;
}
#number-input-buttons .num-btn.selected {
background-color: #007bff;
color: white;
border-color: #007bff;
}
#number-input-buttons .num-btn.completed {
opacity: 0.4;
background-color: #e9ecef;
pointer-events: none;
}
#number-input-buttons #undo-btn {
background-color: #f8f9fa;
color: #dc3545;
}
/* 액션 버튼 (힌트, 정답확인) */
.action-buttons {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 15px;
width: 100%;
}
.action-buttons button {
flex-grow: 1;
max-width: 200px;
}
/* 모달 및 숨김 처리 */
.hidden {
display: none !important;
}
#modal-overlay, #game-over-modal {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background-color: rgba(0,0,0,0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
#modal-content {
background: white;
padding: 30px;
border-radius: 10px;
text-align: center;
width: 90%;
max-width: 400px;
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
}
#modal-content h2, #modal-content h3 {
color: #333;
margin-bottom: 15px;
}
#username-input {
width: calc(100% - 24px);
padding: 10px;
margin-bottom: 15px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 1em;
}
#ranking-list {
list-style-type: decimal;
list-style-position: inside;
padding: 0;
text-align: left;
margin-top: 20px;
}
#ranking-list li {
padding: 8px 0;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
#ranking-list li:last-child {
border-bottom: none;
}
</style>
</head>
<body>
<th:block layout:fragment="content">
<div id="sudoku-game-app">
<div class="container">
@@ -76,5 +313,373 @@
</div>
</div>
<script>
// sudoku.js의 내용을 여기에 삽입
document.addEventListener('DOMContentLoaded', () => {
// DOM 요소
const setupContainer = document.getElementById('setup-container');
const gameContainer = document.getElementById('game-container');
const startBtn = document.getElementById('start-btn');
const boardElement = document.getElementById('sudoku-board');
const timerElement = document.getElementById('timer');
const scoreElement = document.getElementById('score');
const hintBtn = document.getElementById('hint-btn');
const undoBtn = document.getElementById('undo-btn');
const completeBtn = document.getElementById('complete-btn');
const numberInputButtons = document.getElementById('number-input-buttons');
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');
// 게임 상태 변수
let currentPuzzleId = null;
let solvedPuzzle = null;
let timerInterval = null;
let secondsElapsed = 0;
let selectedNumber = null;
let focusedCell = null;
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();
currentPuzzleId = gameData.puzzleId;
solvedPuzzle = gameData.solution;
history = [];
score = 5;
updateScoreDisplay();
renderBoard(gameData.question);
startTimer();
updateButtonStates();
setupContainer.classList.add('hidden');
gameContainer.classList.remove('hidden');
numberInputButtons.classList.remove('hidden');
gameOverModal.classList.add('hidden');
} catch (error) {
alert('게임 로딩에 실패했습니다: ' + error.message);
console.error(error);
}
});
function renderBoard(puzzleString) {
boardElement.innerHTML = '';
for (let i = 0; i < 81; i++) {
const cell = document.createElement('div');
cell.classList.add('cell');
cell.dataset.index = i;
if (puzzleString[i] !== '0') {
cell.textContent = puzzleString[i];
} else {
cell.classList.add('editable');
}
boardElement.appendChild(cell);
}
}
function startTimer() {
secondsElapsed = 0;
timerElement.textContent = '00:00';
clearInterval(timerInterval);
timerInterval = setInterval(() => {
secondsElapsed++;
const minutes = Math.floor(secondsElapsed / 60).toString().padStart(2, '0');
const seconds = (secondsElapsed % 60).toString().padStart(2, '0');
timerElement.textContent = `${minutes}:${seconds}`;
}, 1000);
}
function updateScoreDisplay() {
scoreElement.textContent = `SCORE: ${score}`;
if (score <= 0) {
clearInterval(timerInterval);
gameOverModal.classList.remove('hidden');
}
}
function updateButtonStates() {
const counts = {};
for (let i = 1; i <= 9; i++) counts[i] = 0;
boardElement.querySelectorAll('.cell').forEach(cell => {
const num = cell.textContent;
if (num && counts[num] !== undefined) counts[num]++;
});
for (let i = 1; i <= 9; i++) {
const btn = numberInputButtons.querySelector(`.num-btn[data-number="${i}"]`);
if (btn) {
if (counts[i] >= 9) {
btn.classList.add('completed');
if (selectedNumber == i) {
selectedNumber = null;
btn.classList.remove('selected');
}
} else {
btn.classList.remove('completed');
}
}
}
}
// --- 게임 플레이 이벤트 핸들링 ---
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;
if (selectedNumber) target.classList.add('selected');
}
highlightCells();
});
boardElement.addEventListener('click', (event) => {
const targetCell = event.target.closest('.cell.editable');
if (!targetCell) {
if (focusedCell) focusedCell = null;
highlightCells();
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();
});
hintBtn.addEventListener('click', () => {
if (score <= 0) return;
const emptyCells = Array.from(boardElement.querySelectorAll('.cell.editable')).filter(cell => !cell.textContent);
if (emptyCells.length === 0) {
alert('모든 칸이 채워져 있습니다.');
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();
});
function undoAction() {
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) {
cell.classList.add('editable');
}
validateCell(cell, false);
updateButtonStates();
highlightCells();
}
}
function recordAction(cell, previousValue, newValue, wasHint = false) {
history.push({ index: cell.dataset.index, previousValue, newValue, wasHint });
}
function validateCell(cell, deductPoint = true) {
if (!cell.textContent) {
cell.classList.remove('incorrect');
return;
}
const cellIndex = parseInt(cell.dataset.index);
const isCorrect = (cell.textContent === solvedPuzzle[cellIndex]);
if (!isCorrect) {
cell.classList.add('incorrect');
if (deductPoint && score > 0) {
score--;
updateScoreDisplay();
}
} else {
cell.classList.remove('incorrect');
}
}
// --- 하이라이트 기능 ---
function highlightCells() {
document.querySelectorAll('.cell').forEach(cell => {
cell.classList.remove('highlight-focused', 'highlight-same-number', 'highlight-selected-number');
});
if (focusedCell) {
focusedCell.classList.add('highlight-focused');
const focusedValue = focusedCell.textContent;
if (focusedValue) {
document.querySelectorAll('.cell').forEach(cell => {
if (cell.textContent === focusedValue) cell.classList.add('highlight-same-number');
});
}
}
if (selectedNumber) {
document.querySelectorAll('.cell').forEach(cell => {
if (cell.textContent === selectedNumber) cell.classList.add('highlight-selected-number');
});
}
}
// --- 게임 완료 및 모달 ---
async function checkSolution() {
let answerString = "";
boardElement.childNodes.forEach(cell => {
answerString += cell.textContent || '0';
});
if (answerString.includes('0')) {
alert('모든 칸을 채워주세요!');
return;
}
try {
// (★ 수정) API 경로 변경: /sudoku/validate -> /puzzle/sudoku/validate
// (★ 수정) currentPuzzleId 변수 사용
const response = await fetch('/puzzle/sudoku/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ puzzleId: currentPuzzleId, answer: answerString })
});
const result = await response.json();
if (result.correct) {
clearInterval(timerInterval);
alert('🎉 정답입니다!');
showRankingModal(); // 랭킹 모달 표시
} else {
alert('🤔 틀린 부분이 있습니다. 다시 확인해주세요.');
}
} catch (error) {
console.error('정답 확인 중 오류 발생:', error);
alert('정답 확인 중 오류가 발생했습니다.');
}
}
function checkIfBoardIsFull() {
const emptyEditableCells = boardElement.querySelector('.cell.editable:empty');
if (!emptyEditableCells) {
checkSolution();
}
}
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');
clearInterval(timerInterval);
selectedNumber = null;
focusedCell = null;
document.querySelectorAll('.cell').forEach(cell => {
cell.classList.remove('highlight-focused', 'highlight-same-number', 'highlight-selected-number');
});
document.querySelectorAll('.num-btn').forEach(btn => btn.classList.remove('selected', 'completed'));
}
closeModalBtn.addEventListener('click', () => {
modalOverlay.classList.add('hidden');
resetGameView();
});
retryBtn.addEventListener('click', () => {
gameOverModal.classList.add('hidden');
resetGameView();
});
});
</script>
</th:block>
</html>
</body>
</html>
@@ -6,8 +6,8 @@
layout:decorate="~{layout/default_layout}"
>
<th:block layout:fragment="head" id="head">
<script type="text/javascript" th:src="@{/js/upload.js}"></script>
<link th:href="@{/css/puzzle.css}" rel="stylesheet" />
<script type="text/javascript" th:src="@{/js/nonogram.js}"></script>
<link th:href="@{/css/nonogram.css}" rel="stylesheet" />
<script th:inline="javascript">
</script>
@@ -6,12 +6,12 @@
<meta name="Referrer" content="origin"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width,height=device-height,initial-scale=1"/>
<script type="text/javascript" th:src="@{https://code.jquery.com/jquery-3.5.1.min.js}" crossorigin="anonymous"></script>
<title>BUM'sPace</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<script async th:src="@{https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-9504446465764716}" crossorigin="anonymous"></script>
<script type="text/javascript" th:src="@{/js/common.js}"></script>
<link th:href="@{/css/common.css}" rel="stylesheet" />
<link th:href="@{/css/main.css}" rel="stylesheet" />
<meta name="_csrf" th:content="${_csrf.token}"/>
@@ -20,6 +20,7 @@
/*
* [수정됨] 이 객체는 Post.kt 모델의 모든 필드를 포함해야 합니다.
* 여기서 누락된 필드는 편집 후 저장 시 서버에서 null 또는 0으로 초기화됩니다.
* (이 블록은 <head>에 남아있어도 괜찮습니다. 전역 변수를 정의하는 역할입니다.)
*/
var serverData = {
// --- Key IDs ---
@@ -56,8 +56,10 @@
<th:block layout:fragment="head"></th:block>
<th:block th:replace="~{fragments/footer :: footer}"></th:block>
</div>
<!-- Scripts -->
<script th:src="@{/js/jquery.min.js}"></script>
<script th:src="@{/js/common.js}"></script>
<script th:src="@{/js/user}"></script>
<script th:src="@{/js/jquery.dropotron.min.js}"></script>
<script th:src="@{/js/browser.min.js}"></script>
<script th:src="@{/js/breakpoints.min.js}"></script>