From cc43ea8e0afd06ca55b6e045a5f9fd0672361742 Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Fri, 12 Sep 2025 18:01:23 +0900 Subject: [PATCH] ... --- build.gradle.kts | 2 +- .../back/lun/controllers/PuzzleController.kt | 12 +- .../back/lun/controllers/RankController.kt | 39 - .../back/lun/controllers/SpiderController.kt | 64 - .../back/lun/controllers/SudokuController.kt | 26 - .../kr/lunaticbum/back/lun/model/Post.kt | 33 +- .../lunaticbum/back/lun/model/PuzzleData.kt | 4 +- .../kr/lunaticbum/back/lun/model/Rank.kt | 40 - .../kr/lunaticbum/back/lun/model/Spider.kt | 22 - .../lunaticbum/back/lun/model/SudokuPuzzle.kt | 45 - src/main/resources/static/css/2048.css | 171 --- .../static/css/common_game_theme.css | 7 +- src/main/resources/static/css/nonogram.css | 314 ----- src/main/resources/static/css/spider.css | 41 - src/main/resources/static/css/sudoku.css | 260 ---- src/main/resources/static/js/2048.js | 238 ---- src/main/resources/static/js/admin-sudoku.js | 106 +- src/main/resources/static/js/common.js | 2 +- src/main/resources/static/js/easy_qrcode.zip | Bin 16943 -> 0 bytes src/main/resources/static/js/nonogram.js | 694 ----------- src/main/resources/static/js/spider.js | 1083 ----------------- src/main/resources/static/js/sudoku.js | 363 ------ src/main/resources/static/js/user.js | 0 .../templates/content/blog/editor.html | 7 +- .../templates/content/blog/posts.html | 8 +- .../templates/content/puzzle/2048.html | 68 +- .../templates/content/puzzle/spider.html | 3 +- .../templates/content/puzzle/sudoku.html | 6 +- .../templates/content/puzzle/sudoku_gen.html | 142 ++- .../templates/content/puzzle/upload.html | 177 ++- .../templates/content/user/join.html | 1 - .../templates/content/user/login.html | 1 - .../templates/fragments/includes.html | 1 - .../templates/layout/default_layout.html | 1 - 34 files changed, 461 insertions(+), 3520 deletions(-) delete mode 100644 src/main/kotlin/kr/lunaticbum/back/lun/controllers/RankController.kt delete mode 100644 src/main/kotlin/kr/lunaticbum/back/lun/controllers/SpiderController.kt delete mode 100644 src/main/kotlin/kr/lunaticbum/back/lun/controllers/SudokuController.kt delete mode 100644 src/main/kotlin/kr/lunaticbum/back/lun/model/Rank.kt delete mode 100644 src/main/kotlin/kr/lunaticbum/back/lun/model/Spider.kt delete mode 100644 src/main/kotlin/kr/lunaticbum/back/lun/model/SudokuPuzzle.kt delete mode 100644 src/main/resources/static/css/2048.css delete mode 100644 src/main/resources/static/css/nonogram.css delete mode 100644 src/main/resources/static/css/spider.css delete mode 100644 src/main/resources/static/css/sudoku.css delete mode 100644 src/main/resources/static/js/2048.js delete mode 100644 src/main/resources/static/js/nonogram.js delete mode 100644 src/main/resources/static/js/spider.js delete mode 100644 src/main/resources/static/js/sudoku.js delete mode 100644 src/main/resources/static/js/user.js diff --git a/build.gradle.kts b/build.gradle.kts index eb03e6b..f8745db 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -60,7 +60,7 @@ dependencies { implementation ("org.commonmark:commonmark:0.18.0") implementation ("net.coobird:thumbnailator:0.4.14") - + implementation("org.sejda.imageio:webp-imageio:0.1.6") implementation ("com.drewnoakes:metadata-extractor:2.19.0") implementation("org.springframework.boot:spring-boot-starter-security") compileOnly("org.projectlombok:lombok") diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/PuzzleController.kt b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/PuzzleController.kt index d45aa94..52fadd4 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/PuzzleController.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/PuzzleController.kt @@ -75,7 +75,7 @@ class PuzzleController( /** * 스도쿠: (관리용) 새 퍼즐 문제 생성 및 DB 저장 */ - @GetMapping("/sudoku/generate") + @GetMapping("/sudoku/sudoku_gen") suspend fun sudokuGenerateSinglePuzzle(): SudokuPuzzle { return puzzleService.sudoku_generateAndSavePuzzle() } @@ -182,6 +182,16 @@ class PuzzleController( return vm } + /** + * 스도쿠: 게임 페이지 서빙 + */ + @GetMapping("/sudoku_gen.bs") + suspend fun sudoku_gen(): ResultMV { + val vm = ResultMV("content/puzzle/sudoku_gen") + return vm + } + + /** * 스파이더: 게임 페이지 서빙 */ diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/RankController.kt b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/RankController.kt deleted file mode 100644 index 6df11df..0000000 --- a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/RankController.kt +++ /dev/null @@ -1,39 +0,0 @@ -//package kr.lunaticbum.back.lun.controllers -// -//import kr.lunaticbum.back.lun.model.Rank -//import kr.lunaticbum.back.lun.model.RankRepository -//import org.springframework.web.bind.annotation.* -//import reactor.core.publisher.Flux -//import reactor.core.publisher.Mono -// -// -//@RestController -//@RequestMapping("/rank") -//class RankController(val rankRepository: RankRepository) { -//// private val rankRepository: RankRepository -//// -//// init { -//// this.rankRepository = rankRepository -//// } -// -// /** -// * 새로운 랭킹을 저장합니다. -// * 요청 Body에 gameId가 포함되어야 합니다. -// * @param rank 저장할 랭크 정보 (gameId, name, score) -// * @return Mono -// */ -// @PostMapping("/ranks") -// fun saveRank(@RequestBody rank: Rank): Mono { // 👈 요청 Body는 Rank 모델을 그대로 사용 -// return rankRepository.save(rank) -// } -// -// /** -// * 특정 게임의 상위 10개 랭킹 리스트를 조회합니다. -// * @param gameId 경로 변수(Path Variable)로 게임 ID를 받습니다. -// * @return Flux -// */ -// @GetMapping("/ranks/{gameId}") // 👈 엔드포인트에 Path Variable 추가 -// fun getRankingsByGameId(@PathVariable gameId: String): Flux { -// return rankRepository.findTop10ByGameIdOrderByScoreDesc(gameId) -// } -//} \ No newline at end of file diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/SpiderController.kt b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/SpiderController.kt deleted file mode 100644 index 97d3c2e..0000000 --- a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/SpiderController.kt +++ /dev/null @@ -1,64 +0,0 @@ -//package kr.lunaticbum.back.lun.controllers -// -//import kr.lunaticbum.back.lun.model.SpiderGame -//import kr.lunaticbum.back.lun.model.SpiderRank -//import kr.lunaticbum.back.lun.model.SpiderService -//import org.springframework.http.ResponseEntity -//import org.springframework.web.bind.annotation.GetMapping -//import org.springframework.web.bind.annotation.PathVariable -//import org.springframework.web.bind.annotation.PostMapping -//import org.springframework.web.bind.annotation.RequestBody -//import org.springframework.web.bind.annotation.RequestMapping -//import org.springframework.web.bind.annotation.RequestParam -//import org.springframework.web.bind.annotation.RestController -//import reactor.core.publisher.Flux -//import reactor.core.publisher.Mono -// -//@RestController -//@RequestMapping("/spider") -//class SpiderController(private val spiderService: SpiderService,) { -// -// @GetMapping("/new") -// fun newGame(@RequestParam numSuits: Int, @RequestParam numCards: String): Mono { -// return spiderService.newGame(numSuits, numCards) -// } -// -// @GetMapping("/{id}") -// fun getGame(@PathVariable id: String): Mono { -// return spiderService.getGame(id) -// } -// -// @PostMapping("/update") -// fun updateGame(@RequestBody game: SpiderGame): Mono { -// return spiderService.updateGame(game) -// } -// -// // 랭킹 등록 엔드포인트 -// @PostMapping("/register") -// fun registerRank(@RequestBody rank: SpiderRank): Mono> { -// return spiderService.registerRank(rank) -// .map { savedRank -> ResponseEntity.ok(savedRank) } -// .onErrorResume(IllegalArgumentException::class.java) { e -> -// Mono.just(ResponseEntity.badRequest().body(null)) -// } -// } -// -// // 게임 ID별 랭킹 조회 엔드포인트 -// @GetMapping("/list/{gameId}") -// fun getRanks(@PathVariable gameId: String): Flux { -// return spiderService.getRanksByGameId(gameId) -// } -// -// @PostMapping("/deal") -// fun dealCards(@RequestBody request: Map): Mono { -// val gameId = request["gameId"] ?: return Mono.error(IllegalArgumentException("Game ID is required.")) -// return spiderService.dealCardsFromStock(gameId) -// } -// -// // 실행 취소 엔드포인트 추가 -// @PostMapping("/undo") -// fun undo(@RequestBody request: Map): Mono { -// val gameId = request["gameId"] ?: return Mono.error(IllegalArgumentException("Game ID is required.")) -// return spiderService.undoGame(gameId) -// } -//} \ No newline at end of file diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/SudokuController.kt b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/SudokuController.kt deleted file mode 100644 index e7474c0..0000000 --- a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/SudokuController.kt +++ /dev/null @@ -1,26 +0,0 @@ -//package kr.lunaticbum.back.lun.controllers -//import kr.lunaticbum.back.lun.model.GameRecord -//import kr.lunaticbum.back.lun.model.SudokuPuzzle -//import kr.lunaticbum.back.lun.model.SudokuService -//import org.springframework.web.bind.annotation.* -// -//@RestController -//@RequestMapping("/sudoku") -//class SudokuController(private val sudokuService: SudokuService) { -// -// @GetMapping("/start") -// suspend fun startGame(@RequestParam(defaultValue = "easy") difficulty: String): SudokuService.GameDto { -// return sudokuService.startGame(difficulty) -// } -// -// @PostMapping("/generate") -// suspend fun generateSinglePuzzle(): SudokuPuzzle { -// return sudokuService.generateAndSavePuzzle() -// } -// -// @PostMapping("/validate") -// suspend fun validate(@RequestBody validateDto: SudokuService.ValidateDto): Map { -// val isCorrect = sudokuService.validateSolution(validateDto) -// return mapOf("correct" to isCorrect) -// } -//} \ No newline at end of file diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/model/Post.kt b/src/main/kotlin/kr/lunaticbum/back/lun/model/Post.kt index e4e9116..10d1299 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/model/Post.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/model/Post.kt @@ -165,6 +165,35 @@ interface PostRepository : ReactiveMongoRepository { "{ \$count: \"totalCount\" }" // 고유 그룹의 개수를 셈 ]) fun countLatestUniqueOrigin(): Mono // 헬퍼 클래스로 매핑 + + + /** + * 익명 사용자를 위한 '고유 최신 글' 목록을 페이지네이션으로 조회합니다. + * [수정] posting이 true인 문서만 필터링하는 $match 단계를 추가합니다. + */ + @Aggregation(pipeline = [ + "{ \$sort: { modifyTime: -1 } }", + "{ \$group: { _id: { \$ifNull: [\"\$originId\", \"\$_id\"] }, post: { \$first: \"\$\$ROOT\" } } }", + "{ \$replaceRoot: { newRoot: \"\$post\" } }", + // [[[[[ 신규 추가된 라인 ]]]]] + "{ \$match: { posting: true } }", + "{ \$sort: { \"modifyTime\": -1 } }" + ]) + fun findLatestUniquePublishedPaginated(pageable: Pageable): Flux // 메서드 이름 변경 + + /** + * '고유 최신 글' 중 공개된 글의 총 개수를 카운트합니다. + * [수정] posting이 true인 문서만 필터링하는 $match 단계를 추가합니다. + */ + @Aggregation(pipeline = [ + "{ \$sort: { modifyTime: -1 } }", + "{ \$group: { _id: { \$ifNull: [\"\$originId\", \"\$_id\"] }, post: { \$first: \"\$\$ROOT\" } } }", + "{ \$replaceRoot: { newRoot: \"\$post\" } }", + // [[[[[ 신규 추가된 라인 ]]]]] + "{ \$match: { posting: true } }", + "{ \$count: \"totalCount\" }" + ]) + fun countLatestUniquePublished(): Mono // 메서드 이름 변경 } @@ -210,7 +239,7 @@ class PostManager( * [FIX]: This function should already be correct from the previous step. */ fun findLatestUniquePaginated(pageable: Pageable) : Mono> { // <-- Should already return Mono - return postRepository.findLatestUniqueOriginPaginated(pageable) + return postRepository.findLatestUniquePublishedPaginated(pageable) .collectList() } @@ -226,7 +255,7 @@ class PostManager( */ fun countLatestUnique(): Mono { // AggregationCount(totalCount=N) 객체에서 Long 값만 추출합니다. 결과가 없으면 0L을 반환합니다. - return postRepository.countLatestUniqueOrigin() + return postRepository.countLatestUniquePublished() .map { it.totalCount } .switchIfEmpty(Mono.just(0L)) } diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/model/PuzzleData.kt b/src/main/kotlin/kr/lunaticbum/back/lun/model/PuzzleData.kt index d2c1152..04b14bf 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/model/PuzzleData.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/model/PuzzleData.kt @@ -33,7 +33,7 @@ import org.springframework.data.mongodb.core.index.Indexed * ====================================================== */ -@Document("puzzles") // "puzzles" 컬렉션 (노노그램용) +@Document("nonogram") // "puzzles" 컬렉션 (노노그램용) data class NonogramPuzzle( @Id val id: String? = null, @@ -446,7 +446,7 @@ interface SpiderGameRepository : ReactiveMongoRepository { /** * 스도쿠 퍼즐 원본 데이터를 저장하는 모델 */ -@Document(collection = "puzzles") // (참고: 노노그램과 같은 컬렉션을 쓰지만 구조가 다름) +@Document(collection = "Sudoku") // (참고: 노노그램과 같은 컬렉션을 쓰지만 구조가 다름) data class SudokuPuzzle( @Id val id: String? = null, diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/model/Rank.kt b/src/main/kotlin/kr/lunaticbum/back/lun/model/Rank.kt deleted file mode 100644 index 025bc46..0000000 --- a/src/main/kotlin/kr/lunaticbum/back/lun/model/Rank.kt +++ /dev/null @@ -1,40 +0,0 @@ -package kr.lunaticbum.back.lun.model - -import org.springframework.data.annotation.Id -import org.springframework.data.mongodb.core.mapping.Document -import org.springframework.data.mongodb.repository.ReactiveMongoRepository -import org.springframework.stereotype.Repository -import reactor.core.publisher.Flux - - -@Document(collection = "ranks") -class Rank { - // Getters and Setters - @Id - var id: String? = null - - // 👈 Getter/Setter 추가 - var gameId: String? = null // 👈 게임 ID 필드 추가 - var name: String? = null - var score: Int = 0 - - // Constructors - constructor() - - constructor(gameId: String?, name: String?, score: Int) { - this.gameId = gameId - this.name = name - this.score = score - } -} - -@Repository -interface RankRepository : ReactiveMongoRepository { - /** - * 특정 gameId에 대해 점수가 높은 순서대로 상위 10개의 랭킹을 조회합니다. - * @param gameId 조회할 게임의 ID - * @return Flux - */ - // 쿼리 메소드 이름 변경 및 파라미터 추가 - fun findTop10ByGameIdOrderByScoreDesc(gameId: String): Flux // 👈 수정 -} \ No newline at end of file diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/model/Spider.kt b/src/main/kotlin/kr/lunaticbum/back/lun/model/Spider.kt deleted file mode 100644 index dc3e86b..0000000 --- a/src/main/kotlin/kr/lunaticbum/back/lun/model/Spider.kt +++ /dev/null @@ -1,22 +0,0 @@ -//package kr.lunaticbum.back.lun.model -// -//import org.springframework.data.annotation.Id -//import org.springframework.data.mongodb.core.mapping.Document -//import org.springframework.data.mongodb.repository.ReactiveMongoRepository -//import reactor.core.publisher.Mono -//// (★ 삭제) Service, Flux, Random 관련 import 제거 -// -// -///* -// * (★ 삭제됨) @Service class SpiderService (...) -// * -> 모든 로직이 PuzzleService로 통합됨. -// */ -// -///* * (★ 삭제됨) data class SpiderRank (...) -// * -> 통합 GameRank 모델로 대체됨. -// */ -// -///* -// * (★ 삭제됨) interface SpiderRankRepository (...) -// * -> 통합 GameRankRepository로 대체됨. -// */ \ No newline at end of file diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/model/SudokuPuzzle.kt b/src/main/kotlin/kr/lunaticbum/back/lun/model/SudokuPuzzle.kt deleted file mode 100644 index 6813d89..0000000 --- a/src/main/kotlin/kr/lunaticbum/back/lun/model/SudokuPuzzle.kt +++ /dev/null @@ -1,45 +0,0 @@ -//package kr.lunaticbum.back.lun.model -// -//// (★ 삭제) Service, Record 관련 import 모두 제거 -//import org.springframework.data.annotation.Id -//import org.springframework.data.mongodb.core.index.Indexed -//import org.springframework.data.mongodb.core.mapping.Document -//import org.springframework.data.repository.kotlin.CoroutineCrudRepository -//import org.springframework.stereotype.Repository -// -///** -// * 스도쿠 퍼즐 원본 데이터를 저장하는 모델 -// */ -//@Document(collection = "puzzles") // (참고: 노노그램과 같은 컬렉션을 쓰지만 구조가 다름) -//data class SudokuPuzzle( -// @Id -// val id: String? = null, -// val puzzleKey: Long? = null, -// @Indexed(unique = true) -// val puzzle: String? // 81자리 완성된 퍼즐 데이터 -//) -// -///** -// * 스도쿠 퍼즐 리포지토리 (PuzzleService에서 사용됨) -// */ -//@Repository -//interface SudokuPuzzleRepository : CoroutineCrudRepository { -// override suspend fun count(): Long -// suspend fun findByPuzzleKey(puzzleKey: Long): SudokuPuzzle? -// suspend fun findTopByOrderByPuzzleKeyDesc(): SudokuPuzzle? -//} -// -// -///* * (★ 삭제됨) data class GameRecord (...) -// * -> 통합 GameRank 모델로 대체됨. -// */ -// -///* -// * (★ 삭제됨) interface GameRecordRepository (...) -// * -> 통합 GameRankRepository로 대체됨. -// */ -// -///* -// * (★ 삭제됨) @Service class SudokuService (...) -// * -> 모든 로직이 PuzzleService로 통합됨. -// */ \ No newline at end of file diff --git a/src/main/resources/static/css/2048.css b/src/main/resources/static/css/2048.css deleted file mode 100644 index 933d03f..0000000 --- a/src/main/resources/static/css/2048.css +++ /dev/null @@ -1,171 +0,0 @@ -/*!* =================================*/ -/* 기본 및 전체 레이아웃 (수정됨)*/ -/* ================================= *!*/ -/*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;*/ -/*}*/ \ No newline at end of file diff --git a/src/main/resources/static/css/common_game_theme.css b/src/main/resources/static/css/common_game_theme.css index 05b081f..52d60ea 100644 --- a/src/main/resources/static/css/common_game_theme.css +++ b/src/main/resources/static/css/common_game_theme.css @@ -45,15 +45,18 @@ body { font-family: var(--font-main); background-color: var(--color-bg-page); color: var(--color-text-secondary); - text-align: center; margin: 0; +} + +/* Create a new class for the game's specific layout */ +.game-body-wrapper { + text-align: center; padding: 20px; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; } - /* (★ 통일) H1 (게임 제목) 스타일 통일 (변수 사용) */ h1 { font-size: clamp(2.2em, 8vw, 3.2em); diff --git a/src/main/resources/static/css/nonogram.css b/src/main/resources/static/css/nonogram.css deleted file mode 100644 index 02eb601..0000000 --- a/src/main/resources/static/css/nonogram.css +++ /dev/null @@ -1,314 +0,0 @@ -/*!* === 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 *!*/ -/*}*/ \ No newline at end of file diff --git a/src/main/resources/static/css/spider.css b/src/main/resources/static/css/spider.css deleted file mode 100644 index e398f76..0000000 --- a/src/main/resources/static/css/spider.css +++ /dev/null @@ -1,41 +0,0 @@ -/*!* (★ 수정) #game-container가 전체 화면(100vw/vh)을 차지하는 대신,*/ -/* common_game_theme의 body(#f4f7f9 배경) 위에 떠 있는*/ -/* '게임 테이블(카드)' 역할을 하도록 변경합니다. *!*/ -/*#game-container {*/ -/* !* (★ 남김) 내부 캔버스를 정렬하는 로직은 유지 *!*/ -/* display: flex;*/ -/* justify-content: center;*/ -/* align-items: flex-start;*/ - -/* !* (★ 유지/수정) '펠트' 배경색은 유지하되, 공통 '카드' 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; !* 유지 *!*/ -/*}*/ \ No newline at end of file diff --git a/src/main/resources/static/css/sudoku.css b/src/main/resources/static/css/sudoku.css deleted file mode 100644 index 3150c88..0000000 --- a/src/main/resources/static/css/sudoku.css +++ /dev/null @@ -1,260 +0,0 @@ -/*!* 기본 스타일 초기화 및 폰트 설정 *!*/ -/*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)하면 됩니다.*/ -/* 현재는 공통 스타일이 적용됩니다. *!*/ -/*}*/ - -/*!* (★ 삭제) 아래의 '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;*/ -/*}*/ -/**!*/ - - -/*!* ======================================= *!*/ -/*!* (★ 남김) 아래부터는 스도쿠 고유의 스타일입니다. (수정 불필요) *!*/ -/*!* ======================================= *!*/ - -/*!* 게임 컨테이너 *!*/ -/*#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;*/ -/*}*/ \ No newline at end of file diff --git a/src/main/resources/static/js/2048.js b/src/main/resources/static/js/2048.js deleted file mode 100644 index 17af730..0000000 --- a/src/main/resources/static/js/2048.js +++ /dev/null @@ -1,238 +0,0 @@ -// 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 = '
  • 로딩 중...
  • '; -// try { -// // (★ 수정) user.js의 공통 fetchRanks 함수 호출 -// const rankings = await fetchRanks(currentGameType, currentContextId); -// -// rankingList.innerHTML = ''; // 리스트 비우기 -// if (!rankings || rankings.length === 0) { -// rankingList.innerHTML = '
  • 등록된 랭킹이 없습니다.
  • '; -// return; -// } -// -// rankings.forEach((rank, index) => { -// const li = document.createElement('li'); -// // (★ 수정) 공통 모델(GameRank)의 필드명(playerName, primaryScore)을 사용 -// li.innerHTML = `${index + 1}. ${rank.playerName}${rank.primaryScore}점`; -// rankingList.appendChild(li); -// }); -// } catch (error) { -// console.error('Error fetching ranks:', error); -// rankingList.innerHTML = '
  • 랭킹을 불러올 수 없습니다.
  • '; -// } -// } -// -// // ----- 게임 시작 ----- -// updateRankingList(); -// initializeBoard(); -// }); \ No newline at end of file diff --git a/src/main/resources/static/js/admin-sudoku.js b/src/main/resources/static/js/admin-sudoku.js index a2e70e8..92d2dce 100644 --- a/src/main/resources/static/js/admin-sudoku.js +++ b/src/main/resources/static/js/admin-sudoku.js @@ -1,53 +1,53 @@ -document.addEventListener('DOMContentLoaded', () => { - const generateBtn = document.getElementById('generate-btn'); - const boardElement = document.getElementById('sudoku-board'); - const statusMessage = document.getElementById('status-message'); - - // 스도쿠 보드를 화면에 그리는 함수 - function renderBoard(puzzleString) { - boardElement.innerHTML = ''; // 기존 보드 초기화 - if (!puzzleString || puzzleString.length !== 81) { - statusMessage.textContent = '잘못된 퍼즐 데이터입니다.'; - return; - } - - for (const char of puzzleString) { - const cell = document.createElement('div'); - cell.classList.add('cell'); - cell.textContent = char; - boardElement.appendChild(cell); - } - } - - // 생성 버튼 클릭 이벤트 리스너 - generateBtn.addEventListener('click', async () => { - // 버튼 비활성화 및 상태 메시지 업데이트 - generateBtn.disabled = true; - statusMessage.textContent = '새로운 스도쿠 퍼즐을 생성 중입니다... 🧠'; - boardElement.innerHTML = ''; // 보드 비우기 - - try { - // 백엔드 API 호출 (POST 요청) - const response = await fetch('/sudoku/generate', { - method: 'POST', - }); - - if (!response.ok) { - throw new Error(`서버 오류: ${response.statusText}`); - } - - const result = await response.json(); - - // 성공 시 보드 렌더링 - renderBoard(result.puzzle); - statusMessage.textContent = `✅ 퍼즐 생성 완료! (ID: ${result.puzzleKey})`; - - } catch (error) { - console.error('Error generating Sudoku:', error); - statusMessage.textContent = `❌ 생성 실패: ${error.message}`; - } finally { - // 성공/실패 여부와 관계없이 버튼 다시 활성화 - generateBtn.disabled = false; - } - }); -}); \ No newline at end of file +// document.addEventListener('DOMContentLoaded', () => { +// const generateBtn = document.getElementById('generate-btn'); +// const boardElement = document.getElementById('sudoku-board'); +// const statusMessage = document.getElementById('status-message'); +// +// // 스도쿠 보드를 화면에 그리는 함수 +// function renderBoard(puzzleString) { +// boardElement.innerHTML = ''; // 기존 보드 초기화 +// if (!puzzleString || puzzleString.length !== 81) { +// statusMessage.textContent = '잘못된 퍼즐 데이터입니다.'; +// return; +// } +// +// for (const char of puzzleString) { +// const cell = document.createElement('div'); +// cell.classList.add('cell'); +// cell.textContent = char; +// boardElement.appendChild(cell); +// } +// } +// +// // 생성 버튼 클릭 이벤트 리스너 +// generateBtn.addEventListener('click', async () => { +// // 버튼 비활성화 및 상태 메시지 업데이트 +// generateBtn.disabled = true; +// statusMessage.textContent = '새로운 스도쿠 퍼즐을 생성 중입니다... 🧠'; +// boardElement.innerHTML = ''; // 보드 비우기 +// +// try { +// // 백엔드 API 호출 (POST 요청) +// const response = await fetch('/sudoku/generate', { +// method: 'POST', +// }); +// +// if (!response.ok) { +// throw new Error(`서버 오류: ${response.statusText}`); +// } +// +// const result = await response.json(); +// +// // 성공 시 보드 렌더링 +// renderBoard(result.puzzle); +// statusMessage.textContent = `✅ 퍼즐 생성 완료! (ID: ${result.puzzleKey})`; +// +// } catch (error) { +// console.error('Error generating Sudoku:', error); +// statusMessage.textContent = `❌ 생성 실패: ${error.message}`; +// } finally { +// // 성공/실패 여부와 관계없이 버튼 다시 활성화 +// generateBtn.disabled = false; +// } +// }); +// }); \ No newline at end of file diff --git a/src/main/resources/static/js/common.js b/src/main/resources/static/js/common.js index 6eaac40..21a3e74 100644 --- a/src/main/resources/static/js/common.js +++ b/src/main/resources/static/js/common.js @@ -541,7 +541,7 @@ function save() { dataToSend.content = encodeURIComponent(JSON.stringify(quill.getContents())); // Quill 콘텐츠는 JSON 문자열로 변환 후 인코딩 dataToSend.category = encodeURIComponent(dataToSend.category || 'none'); dataToSend.tags = encodeURIComponent(dataToSend.tags || ''); - + dataToSend.posting = document.getElementById('post-published-switch').checked // 3. 현재 위치 좌표를 '수정 좌표'로 업데이트 dataToSend.modifyLat = currentLat; dataToSend.modifyLon = currentLon; diff --git a/src/main/resources/static/js/easy_qrcode.zip b/src/main/resources/static/js/easy_qrcode.zip index e338f5451db1434c85b6f800d6eb75340d7ab324..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 16943 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 DICdg$ diff --git a/src/main/resources/static/js/nonogram.js b/src/main/resources/static/js/nonogram.js deleted file mode 100644 index fe126f1..0000000 --- a/src/main/resources/static/js/nonogram.js +++ /dev/null @@ -1,694 +0,0 @@ -// /** -// * ============================================== -// * 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 = '

    오류: 퍼즐 데이터를 불러올 수 없습니다.

    '; -// } -// 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('
    '); -// 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('
    '); -// 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}`; -// } -// }); -// }); -// }); \ No newline at end of file diff --git a/src/main/resources/static/js/spider.js b/src/main/resources/static/js/spider.js deleted file mode 100644 index ae613f7..0000000 --- a/src/main/resources/static/js/spider.js +++ /dev/null @@ -1,1083 +0,0 @@ -// /** -// * ============================================== -// * spider.js (Canvas 렌더링 게임) -// * (★ 리팩토링: CSS 변수 연동, 타이머 및 랭킹 API 기능 추가, API 경로 통합) -// * ============================================== -// */ -// -// document.addEventListener('DOMContentLoaded', () => { -// -// // ======================================= -// // 1. 상수 및 변수 선언 -// // ======================================= -// console.log("DOM 콘텐츠가 로드되었습니다. 초기 설정 시작."); -// -// // HTML 요소 -// const canvas = document.getElementById('gameCanvas'); -// const ctx = canvas.getContext('2d'); -// -// // ** UI 버튼 및 영역의 논리적 위치 정의 (캔버스 내 좌표) ** -// const UI_ELEMENTS = {}; -// -// // ** 비율 상수 정의 ** -// const CARD_WIDTH_RATIO = 1 / 11.5; -// const CARD_HEIGHT_RATIO = 1.4; -// const CARD_GAP_X_RATIO = 0.15; -// const CARD_OVERLAP_Y_RATIO = 0.3; -// const CARD_RANK_LEFT_PADDING = 0.1; -// const CARD_RANK_TOP_PADDING = 0.1; -// const CARD_SYMBOL_TOP_PADDING = 0.25; -// const CARD_SYMBOL_BOTTOM_PADDING = 0.15; -// const FOUNDATION_CARD_SPACING = 0.2; // 카드 겹침 비율 (20%만 보이게) -// const FOUNDATION_WIDTH_RATIO = 0.45; // 파운데이션 영역 최대 너비 -// -// // 게임 상태 및 데이터 -// let gameId = null; -// let currentGame = null; -// let isGameCompleted = false; -// -// // (★ 삭제) API_BASE_URL 삭제. 모든 경로는 root-relative('/puzzle/...')로 변경 -// // const API_BASE_URL = '/spider'; (삭제) -// -// // (★ 신규) 게임 타이머 및 랭킹 관련 변수 -// let gameStartTime = 0; // 게임 시작 시간 (ms) -// let completionTimeSeconds = 0; // 게임 완료 시간 (초) -// const currentGameType = 'SPIDER'; // 통합 랭킹용 GameType -// let currentContextId = ''; // 예: "1_SUITS_4,3" (난이도 저장용) -// -// -// // 동적으로 계산될 레이아웃 변수 -// let cardWidth = 0; -// let cardHeight = 0; -// let cardGapX = 0; -// let cardOverlapY = 0; -// let totalTableauWidth = 0; -// let tableauStartX = 0; -// -// // 드래그 및 애니메이션 관련 변수 -// let isAnimating = false; -// let isDragging = false; -// let dragStartX = 0; -// let dragStartY = 0; -// const DRAG_THRESHOLD = 5; -// let draggedCards = []; -// let dragOffsetX = 0; -// let dragOffsetY = 0; -// let animatedCard = null; -// let animationProgress = 0; -// let completedStackCards = []; -// let isAnimatingCompletion = false; -// -// // 하단 정렬을 위한 Y 좌표 기준 -// const BOTTOM_ROW_Y_RATIO = 0.9; -// let dpr = 1; -// const MAX_UNDO_COUNT = 5; -// -// // 카드 뒷면 이미지 로드 -// const cardBackImage = new Image(); -// cardBackImage.src = '../css/images/card-back.png'; -// let assetsLoaded = false; -// cardBackImage.onload = () => { -// assetsLoaded = true; -// resizeCanvas(); -// draw(); -// console.log("카드 뒷면 이미지가 로드되었습니다."); -// }; -// -// // UI 옵션 데이터 -// const suitOptions = [{ value: 1, text: '1개' }, { value: 2, text: '2개' }, { value: 4, text: '4개' }]; -// const cardDistributionOptions = { -// '1': [{ value: '4,3', text: '쉬움' }, { value: '5,4', text: '보통' }, { value: '6,5', text: '어려움' }], -// '2': [{ value: '5,4', text: '쉬움' }, { value: '6,5', text: '보통' }, { value: '7,6', text: '어려움' }], -// '4': [{ value: '6,5', text: '쉬움' }, { value: '7,6', text: '보통' }, { value: '8,7', text: '어려움' }] -// }; -// let selectedSuit = 1; -// let selectedCardCount = '4,3'; -// -// // ======================================= -// // 2. 렌더링 (그리기) 관련 함수 (★ CSS 변수 적용) -// // ======================================= -// console.log("렌더링 관련 함수 정의 시작."); -// -// /** -// * (★ 신규) CSS :root에서 정의된 변수 값을 읽어오는 헬퍼 함수 -// * @param {string} varName - 읽어올 CSS 변수 이름 (예: '--color-primary') -// * @returns {string} 변수의 값 (trim된 문자열) -// */ -// function getCssVar(varName) { -// // getComputedStyle을 통해 현재 적용된 CSS 변수 값을 읽어옴 -// return getComputedStyle(document.documentElement).getPropertyValue(varName).trim(); -// } -// -// // 캔버스 크기 조정 및 레이아웃 변수 계산 -// function resizeCanvas() { -// // ... (모든 캔버스 및 UI 좌표 계산 로직 - 수정 없음) ... -// // 이 로직은 캔버스 내부의 "좌표"만 계산하며, "색상"은 draw 함수에서 결정합니다. -// console.log("resizeCanvas 함수 호출."); -// const size = Math.min(window.innerWidth, window.innerHeight) * 0.95; -// canvas.style.width = `${size}px`; -// canvas.style.height = `${size}px`; -// dpr = window.devicePixelRatio || 1; -// canvas.width = size * dpr; -// canvas.height = size * dpr; -// ctx.scale(dpr, dpr); -// const logicalWidth = size; -// const logicalHeight = size; -// cardWidth = logicalWidth * CARD_WIDTH_RATIO; -// cardHeight = cardWidth * CARD_HEIGHT_RATIO; -// cardGapX = cardWidth * CARD_GAP_X_RATIO; -// cardOverlapY = cardHeight * CARD_OVERLAP_Y_RATIO; -// totalTableauWidth = cardWidth * 10 + cardGapX * 9; -// tableauStartX = (logicalWidth - totalTableauWidth) / 2; -// const buttonWidth = logicalWidth * 0.2; -// const buttonHeight = logicalHeight * 0.05; -// const buttonGap = 10; -// const startX = (logicalWidth - (buttonWidth * 3 + buttonGap * 2)) / 2; -// const startY = logicalHeight * 0.05; -// UI_ELEMENTS.difficultyUI = { x: logicalWidth / 2 - (buttonWidth * 1.5 + buttonGap), y: logicalHeight * 0.45, width: (buttonWidth * 3 + buttonGap * 2), height: buttonHeight }; -// UI_ELEMENTS.suitSelect = { x: startX, y: startY, width: buttonWidth, height: buttonHeight }; -// UI_ELEMENTS.cardCountSelect = { x: startX + buttonWidth + buttonGap, y: startY, width: buttonWidth, height: buttonHeight }; -// UI_ELEMENTS.startButton = { x: startX + buttonWidth * 2 + buttonGap * 2, y: startY, width: buttonWidth, height: buttonHeight }; -// const bottomY = logicalHeight * BOTTOM_ROW_Y_RATIO; -// const itemSpacing = 20; -// const foundationX = logicalWidth * 0.05; -// const foundationAreaWidth = logicalWidth * FOUNDATION_WIDTH_RATIO; -// UI_ELEMENTS.foundationArea = { x: foundationX, y: bottomY, width: foundationAreaWidth, height: cardHeight }; -// const undoButtonWidth = cardWidth * 0.8; -// const undoButtonHeight = cardHeight * 0.5; -// const undoCountDisplayWidth = cardWidth * 0.5; -// const undoButtonX = logicalWidth * 0.5 - (undoButtonWidth + undoCountDisplayWidth + itemSpacing) / 2; -// UI_ELEMENTS.undoButton = { x: undoButtonX, y: bottomY + (cardHeight - undoButtonHeight) / 2, width: undoButtonWidth, height: undoButtonHeight }; -// UI_ELEMENTS.undoCountDisplay = { x: undoButtonX + undoButtonWidth + itemSpacing, y: bottomY + (cardHeight - undoButtonHeight) / 2, width: undoCountDisplayWidth, height: undoButtonHeight }; -// const stockX = logicalWidth * 0.95 - cardWidth; -// UI_ELEMENTS.stockArea = { x: stockX, y: bottomY, width: cardWidth, height: cardHeight }; -// UI_ELEMENTS.restartButton = { x: logicalWidth / 2 - buttonWidth / 2, y: logicalHeight / 2 + 50, width: buttonWidth, height: buttonHeight }; -// } -// -// window.addEventListener('resize', resizeCanvas); -// -// // 메인 그리기 루프 -// function draw() { -// if (!assetsLoaded) { -// requestAnimationFrame(draw); -// return; -// } -// -// ctx.clearRect(0, 0, canvas.width, canvas.height); -// -// if (currentGame) { -// drawGame(currentGame); -// } -// drawUI(); -// -// requestAnimationFrame(draw); -// } -// /** -// * (★ 수정) UI 요소를 캔버스에 직접 그리는 함수 -// * 하드코딩된 색상 대신 CSS 변수 값을 읽어와서 사용합니다. -// */ -// /** -// * (★ 수정) UI 요소를 캔버스에 직접 그리는 함수 -// * 하드코딩된 색상 대신 CSS 변수 값을 읽어와서 사용합니다. -// */ -// function drawUI() { -// if (!currentGame) { -// // --- 게임 시작 전 난이도 선택 UI --- -// // (CSS 변수와 무관한 기본 UI 로직) -// const suitSelect = UI_ELEMENTS.suitSelect; -// ctx.fillStyle = '#f0f0f0'; -// ctx.fillRect(suitSelect.x, suitSelect.y, suitSelect.width, suitSelect.height); -// ctx.strokeStyle = '#333'; -// ctx.strokeRect(suitSelect.x, suitSelect.y, suitSelect.width, suitSelect.height); -// ctx.fillStyle = '#000'; -// ctx.font = '16px Arial'; -// ctx.textAlign = 'center'; -// ctx.textBaseline = 'middle'; -// ctx.fillText(`무늬: ${selectedSuit}개`, suitSelect.x + suitSelect.width / 2, suitSelect.y + suitSelect.height / 2); -// -// const cardCountSelect = UI_ELEMENTS.cardCountSelect; -// ctx.fillStyle = '#f0f0f0'; -// ctx.fillRect(cardCountSelect.x, cardCountSelect.y, cardCountSelect.width, cardCountSelect.height); -// ctx.strokeRect(cardCountSelect.x, cardCountSelect.y, cardCountSelect.width, cardCountSelect.height); -// ctx.fillStyle = '#000'; -// ctx.fillText(`카드: ${cardDistributionOptions[selectedSuit.toString()].find(opt => opt.value === selectedCardCount).text}`, -// cardCountSelect.x + cardCountSelect.width / 2, cardCountSelect.y + cardCountSelect.height / 2); -// -// // (★ 수정) 시작 버튼 색상을 CSS 변수('--color-success')에서 읽어옴 -// const startButton = UI_ELEMENTS.startButton; -// ctx.fillStyle = getCssVar('--color-success') || '#4CAF50'; // CSS 변수 읽기 (실패 시 기본값) -// ctx.fillRect(startButton.x, startButton.y, startButton.width, startButton.height); -// ctx.strokeStyle = '#333'; -// ctx.strokeRect(startButton.x, startButton.y, startButton.width, startButton.height); -// ctx.fillStyle = '#fff'; -// ctx.fillText('새 게임 시작', startButton.x + startButton.width / 2, startButton.y + startButton.height / 2); -// -// } else { -// // --- 게임 중 하단 UI (Undo 버튼 등) --- -// const undoButton = UI_ELEMENTS.undoButton; -// const undoCountDisplay = UI_ELEMENTS.undoCountDisplay; -// const isUndoPossible = currentGame.undoHistory.length > 0; -// const isUndoEnabled = currentGame.undoCount < MAX_UNDO_COUNT && isUndoPossible; -// const isSurrender = currentGame.undoCount >= MAX_UNDO_COUNT; -// -// if (isUndoEnabled) { -// // (★ 수정) '실행 취소' 버튼 색상을 CSS 변수('--color-warning')에서 읽어옴 -// const buttonColor = getCssVar('--color-warning') || '#ff9800'; -// const buttonText = '실행 취소'; // ★★★ 버그 수정: 변수 선언 추가 ★★★ -// ctx.fillStyle = buttonColor; -// ctx.fillRect(undoButton.x, undoButton.y, undoButton.width, undoButton.height); -// ctx.strokeStyle = '#333'; -// ctx.strokeRect(undoButton.x, undoButton.y, undoButton.width, undoButton.height); -// ctx.fillStyle = '#fff'; -// ctx.font = '14px Arial'; -// ctx.fillText(buttonText, undoButton.x + undoButton.width / 2, undoButton.y + undoButton.height / 2); -// -// // 남은 취소 횟수 표시 -// const remainingUndos = MAX_UNDO_COUNT - currentGame.undoCount; -// ctx.fillText(`${remainingUndos}`, undoCountDisplay.x + undoCountDisplay.width / 2, undoCountDisplay.y + undoCountDisplay.height / 2); -// -// } else if (isSurrender) { -// // (★ 수정) '게임 포기' 버튼 색상을 CSS 변수('--color-danger')에서 읽어옴 -// const buttonColor = getCssVar('--color-danger') || '#f44336'; -// const buttonText = '게임 포기'; // ★★★ 버그 수정: 변수 선언 추가 ★★★ -// ctx.fillStyle = buttonColor; -// ctx.fillRect(undoButton.x, undoButton.y, undoButton.width, undoButton.height); -// ctx.strokeStyle = '#333'; -// ctx.strokeRect(undoButton.x, undoButton.y, undoButton.width, undoButton.height); -// ctx.fillStyle = '#fff'; -// ctx.font = '14px Arial'; -// ctx.fillText(buttonText, undoButton.x + undoButton.width / 2, undoButton.y + undoButton.height / 2); -// } -// } -// } -// -// // 전체 게임 화면을 그리는 메인 함수 -// function drawGame(game) { -// drawBackground(); -// drawTableau(game.tableau); -// drawStockAndFoundation(game.stock, game.foundation); -// drawDraggedCards(draggedCards); -// drawCompletionAnimation(); -// -// // 게임 완료 시 메시지 표시 -// if (isGameCompleted) { -// drawCompletionMessage(); -// } -// } -// -// /** -// * (★ 수정) 게임 완료 메시지 그리기 (제출 버튼 로직 추가) -// */ -// function drawCompletionMessage() { -// const logicalWidth = canvas.width / dpr; -// const logicalHeight = canvas.height / dpr; -// -// ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; -// ctx.fillRect(0, 0, logicalWidth, logicalHeight); -// -// ctx.fillStyle = '#ffffff'; -// ctx.font = 'bold 36px Arial'; -// ctx.textAlign = 'center'; -// ctx.textBaseline = 'middle'; -// ctx.fillText('게임 완료! 축하합니다!', logicalWidth / 2, logicalHeight / 2); -// -// // (★ 수정) '다시 시작' 버튼 -> '랭킹 등록' 버튼으로 변경 -// // UI_ELEMENTS.restartButton 좌표를 그대로 사용 -// const submitButton = UI_ELEMENTS.restartButton; -// -// // (★ 수정) 성공 버튼 색상 CSS 변수 사용 -// ctx.fillStyle = getCssVar('--color-success') || '#4CAF50'; -// ctx.fillRect(submitButton.x, submitButton.y, submitButton.width, submitButton.height); -// ctx.strokeStyle = '#fff'; -// ctx.strokeRect(submitButton.x, submitButton.y, submitButton.width, submitButton.height); -// ctx.fillStyle = '#fff'; -// ctx.font = '20px Arial'; -// ctx.fillText('랭킹 등록', submitButton.x + submitButton.width / 2, submitButton.y + submitButton.height / 2); -// } -// -// /** -// * (★ 수정) 게임 배경 그리기 (CSS 변수 사용) -// */ -// function drawBackground() { -// // (★ 수정) 하드코딩된 '#008000' 대신 CSS 변수('--color-felt-green') 사용 -// ctx.fillStyle = getCssVar('--color-felt-green') || '#008000'; -// ctx.fillRect(0, 0, canvas.width, canvas.height); -// } -// -// // --- (모든 카드 그리기 로직 (drawTableau, drawDraggedCards 등) - 수정 없음) --- -// // (이 로직들은 색상이 카드 데이터(suit)에 따라 동적으로 결정되므로 CSS 변수와 무관함) -// function drawTableau(tableau) { -// const startY = cardHeight * 0.5 -// const draggingCards = isDragging ? new Set(draggedCards) : null; -// tableau.forEach((stack, stackIndex) => { -// stack.forEach((card, cardIndex) => { -// if (draggingCards && draggingCards.has(card)) { -// return; -// } -// const x = tableauStartX + stackIndex * (cardWidth + cardGapX); -// const y = startY + cardIndex * cardOverlapY; -// card.touchHeight = (cardIndex === stack.length - 1) ? cardHeight : cardOverlapY; -// drawSingleCard(card, x, y); -// }); -// }); -// } -// function drawDraggedCards(cards) { -// if (!isDragging || !Array.isArray(cards) || cards.length === 0) { -// return; -// } -// cards.forEach((card, index) => { -// const x = cards[0].x; -// const y = cards[0].y + index * cardOverlapY; -// drawSingleCard(card, x, y); -// }); -// } -// function drawCompletionAnimation() { -// if (isAnimatingCompletion) { -// const now = Date.now(); -// completedStackCards = completedStackCards.filter(card => { -// if (now < card.animEndTime) { -// const progress = (now - (card.animEndTime - 500)) / 500; -// const currentX = card.animStartX + (card.animTargetX - card.animStartX) * progress; -// const currentY = card.animStartY + (card.animTargetY - card.animStartY) * progress; -// drawSingleCard(card, currentX, currentY); -// return true; -// } else { -// return false; -// } -// }); -// if (completedStackCards.length === 0) { -// isAnimatingCompletion = false; -// } -// } -// } -// function drawSingleCard(card, x, y) {card.x = x; -// card.y = y; -// card.width = cardWidth; -// card.height = cardHeight; -// if (card.isFaceUp) { -// ctx.fillStyle = '#ffffff'; -// ctx.fillRect(x, y, cardWidth, cardHeight); -// ctx.strokeStyle = '#333333'; -// ctx.strokeRect(x, y, cardWidth, cardHeight); -// const isRed = (card.suit === 'heart' || card.suit === 'diamond'); -// ctx.fillStyle = isRed ? '#ff0000' : '#000000'; -// ctx.font = `${cardWidth * 0.25}px Arial`; -// ctx.textAlign = 'left'; -// ctx.textBaseline = 'top'; -// ctx.fillText(getRankText(card.rank), x + cardWidth * CARD_RANK_LEFT_PADDING, y + cardHeight * CARD_RANK_TOP_PADDING); -// drawSuitSymbols(card, x, y); -// } else { -// ctx.drawImage(cardBackImage, x, y, cardWidth, cardHeight); -// }} -// function drawSuitSymbols(card, x, y) { -// const symbol = getSuitSymbol(card.suit); -// let symbolSize; -// if (card.rank >= 2 && card.rank <= 5) { -// symbolSize = cardWidth * 0.2; -// } else { -// symbolSize = cardWidth * 0.15; -// } -// ctx.font = `${symbolSize}px Arial`; -// ctx.textAlign = 'center'; -// ctx.textBaseline = 'middle'; -// ctx.fillStyle = (card.suit === 'heart' || card.suit === 'diamond') ? '#ff0000' : '#000000'; -// const symbolAreaY = y + cardHeight * CARD_SYMBOL_TOP_PADDING; -// const symbolAreaHeight = cardHeight * (1 - CARD_SYMBOL_TOP_PADDING - CARD_SYMBOL_BOTTOM_PADDING); -// const symbolAreaMiddleY = symbolAreaY + symbolAreaHeight / 2; -// const symbolAreaLeftX = x + cardWidth * 0.25; -// const symbolAreaRightX = x + cardWidth * 0.75; -// const symbolGapY = symbolAreaHeight / 3; -// const positions = { -// top: { x: x + cardWidth / 2, y: symbolAreaY + symbolGapY * 0.5 }, -// bottom: { x: x + cardWidth / 2, y: symbolAreaY + symbolAreaHeight - symbolGapY * 0.5 }, -// center: { x: x + cardWidth / 2, y: symbolAreaMiddleY }, -// leftTop: { x: symbolAreaLeftX, y: symbolAreaY + symbolGapY * 0.5 }, -// rightTop: { x: symbolAreaRightX, y: symbolAreaY + symbolGapY * 0.5 }, -// leftCenter: { x: symbolAreaLeftX, y: symbolAreaMiddleY }, -// rightCenter: { x: symbolAreaRightX, y: symbolAreaMiddleY }, -// leftBottom: { x: symbolAreaLeftX, y: symbolAreaY + symbolAreaHeight - symbolGapY * 0.5 }, -// rightBottom: { x: symbolAreaRightX, y: symbolAreaY + symbolAreaHeight - symbolGapY * 0.5 }, -// middleTop: { x: x + cardWidth / 2, y: symbolAreaY + symbolGapY * 0.5 }, -// middleBottom: { x: x + cardWidth / 2, y: symbolAreaY + symbolAreaHeight - symbolGapY * 0.5 } -// }; -// -// switch (card.rank) { -// case 1: case 11: case 12: case 13: -// ctx.font = `${cardWidth * 0.6}px Arial`; -// ctx.fillText(symbol, x + cardWidth / 2, y + cardHeight / 2); -// break; -// case 2: -// ctx.fillText(symbol, positions.middleTop.x, positions.middleTop.y); -// ctx.fillText(symbol, positions.middleBottom.x, positions.middleBottom.y); -// break; -// case 3: -// ctx.fillText(symbol, positions.middleTop.x, positions.middleTop.y); -// ctx.fillText(symbol, positions.center.x, positions.center.y); -// ctx.fillText(symbol, positions.middleBottom.x, positions.middleBottom.y); -// break; -// case 4: -// ctx.fillText(symbol, positions.leftTop.x, positions.leftTop.y); -// ctx.fillText(symbol, positions.rightTop.x, positions.rightTop.y); -// ctx.fillText(symbol, positions.leftBottom.x, positions.leftBottom.y); -// ctx.fillText(symbol, positions.rightBottom.x, positions.rightBottom.y); -// break; -// case 5: -// ctx.fillText(symbol, positions.leftTop.x, positions.leftTop.y); -// ctx.fillText(symbol, positions.rightTop.x, positions.rightTop.y); -// ctx.fillText(symbol, positions.center.x, positions.center.y); -// ctx.fillText(symbol, positions.leftBottom.x, positions.leftBottom.y); -// ctx.fillText(symbol, positions.rightBottom.x, positions.rightBottom.y); -// break; -// case 6: -// ctx.fillText(symbol, positions.leftTop.x, positions.leftTop.y); -// ctx.fillText(symbol, positions.rightTop.x, positions.rightTop.y); -// ctx.fillText(symbol, positions.leftCenter.x, positions.leftCenter.y); -// ctx.fillText(symbol, positions.rightCenter.x, positions.rightCenter.y); -// ctx.fillText(symbol, positions.leftBottom.x, positions.leftBottom.y); -// ctx.fillText(symbol, positions.rightBottom.x, positions.rightBottom.y); -// break; -// case 7: -// ctx.fillText(symbol, positions.leftTop.x, positions.leftTop.y); -// ctx.fillText(symbol, positions.rightTop.x, positions.rightTop.y); -// ctx.fillText(symbol, positions.leftCenter.x, positions.leftCenter.y); -// ctx.fillText(symbol, positions.rightCenter.x, positions.rightCenter.y); -// ctx.fillText(symbol, positions.leftBottom.x, positions.leftBottom.y); -// ctx.fillText(symbol, positions.rightBottom.x, positions.rightBottom.y); -// ctx.fillText(symbol, positions.center.x, positions.center.y); -// break; -// case 8: -// ctx.fillText(symbol, positions.leftTop.x, positions.leftTop.y); -// ctx.fillText(symbol, positions.rightTop.x, positions.rightTop.y); -// ctx.fillText(symbol, positions.leftCenter.x, positions.leftCenter.y); -// ctx.fillText(symbol, positions.rightCenter.x, positions.rightCenter.y); -// ctx.fillText(symbol, positions.leftBottom.x, positions.leftBottom.y); -// ctx.fillText(symbol, positions.rightBottom.x, positions.rightBottom.y); -// ctx.fillText(symbol, positions.middleTop.x, positions.middleTop.y); -// ctx.fillText(symbol, positions.middleBottom.x, positions.middleBottom.y); -// break; -// case 9: -// ctx.fillText(symbol, positions.leftTop.x, positions.leftTop.y); -// ctx.fillText(symbol, positions.rightTop.x, positions.rightTop.y); -// ctx.fillText(symbol, positions.leftCenter.x, positions.leftCenter.y); -// ctx.fillText(symbol, positions.rightCenter.x, positions.rightCenter.y); -// ctx.fillText(symbol, positions.leftBottom.x, positions.leftBottom.y); -// ctx.fillText(symbol, positions.rightBottom.x, positions.rightBottom.y); -// ctx.fillText(symbol, positions.center.x, positions.center.y); -// ctx.fillText(symbol, positions.middleTop.x, positions.middleTop.y); -// ctx.fillText(symbol, positions.middleBottom.x, positions.middleBottom.y); -// break; -// case 10: -// ctx.fillText(symbol, positions.leftTop.x, positions.leftTop.y); -// ctx.fillText(symbol, positions.rightTop.x, positions.rightTop.y); -// ctx.fillText(symbol, positions.leftCenter.x, positions.leftCenter.y); -// ctx.fillText(symbol, positions.rightCenter.x, positions.rightCenter.y); -// ctx.fillText(symbol, positions.leftBottom.x, positions.leftBottom.y); -// ctx.fillText(symbol, positions.rightBottom.x, positions.rightBottom.y); -// ctx.fillText(symbol, positions.top.x, positions.top.y); -// ctx.fillText(symbol, positions.bottom.x, positions.bottom.y); -// ctx.fillText(symbol, positions.middleTop.x, (positions.top.y + symbolSize + positions.center.y) / 2 ); -// ctx.fillText(symbol, positions.middleBottom.x, ((positions.bottom.y + positions.center.y) - symbolSize) / 2); -// break; -// } -// } -// -// -// /** -// * (★ 수정) 스톡 및 파운데이션 그리기 (CSS 변수 사용) -// */ -// function drawStockAndFoundation(stock, foundation) { -// const logicalCanvasWidth = canvas.width / (window.devicePixelRatio || 1); -// const logicalCanvasHeight = canvas.height / (window.devicePixelRatio || 1); -// const stockArea = UI_ELEMENTS.stockArea; -// const foundationArea = UI_ELEMENTS.foundationArea; -// -// // (★ 신규) 캔버스 테두리 색상 변수('--color-felt-border') 사용 -// ctx.strokeStyle = getCssVar('--color-felt-border') || '#004d00'; -// -// // 파운데이션 영역 (테두리 추가) -// ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; -// ctx.fillRect(foundationArea.x, foundationArea.y, foundationArea.width, foundationArea.height); -// ctx.strokeRect(foundationArea.x, foundationArea.y, foundationArea.width, foundationArea.height); -// -// foundation.forEach((stack, index) => { -// const foundationX = foundationArea.x + index * (cardWidth * FOUNDATION_CARD_SPACING); // 겹쳐지게 그리기 -// if (stack.length > 0) { -// // 완성된 카드 스택을 그립니다. -// const topCard = stack[stack.length - 1]; -// drawSingleCard(topCard, foundationX, bottomY); -// } -// }); -// -// // 스톡 -// if (stock.length > 0) { -// ctx.drawImage(cardBackImage, stockArea.x, stockArea.y, cardWidth, cardHeight); -// ctx.strokeRect(stockArea.x, stockArea.y, cardWidth, cardHeight); // 테두리 추가 -// // 스톡 위에 남은 카드 수를 표시합니다. -// const remainingDeals = Math.floor(stock.length / 10); -// ctx.fillStyle = '#fff'; -// ctx.font = '20px Arial'; -// ctx.textAlign = 'center'; -// ctx.textBaseline = 'middle'; -// ctx.fillText(`${remainingDeals}`, stockArea.x + stockArea.width / 2, stockArea.y + stockArea.height / 2); -// } else { -// ctx.strokeRect(stockArea.x, stockArea.y, cardWidth, cardHeight); // 빈 스톡 테두리 -// } -// } -// -// // ======================================= -// // 3. 이벤트 핸들러 및 유틸리티 함수 -// // ======================================= -// console.log("이벤트 핸들러 등록 시작."); -// -// canvas.addEventListener('mousedown', handlePointerDown); -// canvas.addEventListener('mousemove', handlePointerMove); -// canvas.addEventListener('mouseup', handlePointerUp); -// canvas.addEventListener('touchstart', handlePointerDown); -// canvas.addEventListener('touchmove', handlePointerMove); -// canvas.addEventListener('touchend', handlePointerUp); -// canvas.addEventListener('dblclick', handleDoubleClick); -// -// function getCanvasCoordinates(event) { -// const rect = canvas.getBoundingClientRect(); -// const scaleX = canvas.width / rect.width; -// const scaleY = canvas.height / rect.height; -// const clientX = event.touches ? event.touches[0].clientX : event.clientX; -// const clientY = event.touches ? event.touches[0].clientY : event.clientY; -// return { -// x: (clientX - rect.left) * scaleX / dpr, -// y: (clientY - rect.top) * scaleY / dpr -// }; -// } -// -// /** -// * (★ 수정) 클릭된 위치 찾기 (게임 완료 시 'submitButton' 처리) -// */ -// function findElementAt(x, y) { -// if (isGameCompleted) { -// // 완료 화면의 '랭킹 등록' 버튼 (UI_ELEMENTS.restartButton 좌표 사용) -// const restartButton = UI_ELEMENTS.restartButton; -// if (x >= restartButton.x && x <= restartButton.x + restartButton.width && y >= restartButton.y && y <= restartButton.y + restartButton.height) { -// // (★ 수정) 'restartButton' -> 'submitButton'으로 로직명 변경 -// return { type: 'ui', name: 'submitButton' }; -// } -// } -// -// // 게임 진행 중일 때만 스톡과 실행 취소 버튼을 감지 -// if (currentGame) { -// // 스톡 클릭 감지 -// const stockArea = UI_ELEMENTS.stockArea; -// if (x >= stockArea.x && x <= stockArea.x + stockArea.width && y >= stockArea.y && y <= stockArea.y + stockArea.height) { -// return { type: 'stock' }; -// } -// -// // 실행 취소 버튼 클릭 감지 -// const undoButton = UI_ELEMENTS.undoButton; -// if (x >= undoButton.x && x <= undoButton.x + undoButton.width && y >= undoButton.y && y <= undoButton.y + undoButton.height) { -// return { type: 'ui', name: 'undoButton' }; -// } -// } -// -// // 게임 시작 전 난이도 선택 UI 감지 -// if (!currentGame) { -// // 난이도 선택 UI 클릭 감지 -// const suitSelect = UI_ELEMENTS.suitSelect; -// if (x >= suitSelect.x && x <= suitSelect.x + suitSelect.width && y >= suitSelect.y && y <= suitSelect.y + suitSelect.height) { -// return { type: 'ui', name: 'suitSelect' }; -// } -// const cardCountSelect = UI_ELEMENTS.cardCountSelect; -// if (x >= cardCountSelect.x && x <= cardCountSelect.x + cardCountSelect.width && y >= cardCountSelect.y && y <= cardCountSelect.y + cardCountSelect.height) { -// return { type: 'ui', name: 'cardCountSelect' }; -// } -// const startButton = UI_ELEMENTS.startButton; -// if (x >= startButton.x && x <= startButton.x + startButton.width && y >= startButton.y && y <= startButton.y + startButton.height) { -// return { type: 'ui', name: 'startButton' }; -// } -// } -// -// // 카드 클릭 감지 -// if (currentGame) { -// for (let stackIndex = 9; stackIndex >= 0; stackIndex--) { -// const stackCards = currentGame.tableau[stackIndex]; -// for (let cardIndex = stackCards.length - 1; cardIndex >= 0; cardIndex--) { -// const card = stackCards[cardIndex]; -// if (!card.isFaceUp) continue; -// if (x >= card.x && x <= card.x + card.width && y >= card.y && y <= card.y + card.touchHeight) { -// return { type: 'card', card, stackIndex, cardIndex }; -// } -// } -// } -// } -// -// return null; -// } -// -// // 이동 가능한 카드 묶음 검사 (기존 로직) -// function getCardStackForMove(card, stackIndex, cardIndex) { -// const stack = currentGame.tableau[stackIndex]; -// if (cardIndex === -1 || !card.isFaceUp) { -// return null; -// } -// const movableStack = []; -// for (let i = cardIndex; i < stack.length; i++) { -// if (stack[i].isFaceUp) { -// movableStack.push(stack[i]); -// } else { -// break; -// } -// } -// if (movableStack.length === 0) { -// return null; -// } -// for (let i = 0; i < movableStack.length - 1; i++) { -// if (movableStack[i].rank !== movableStack[i + 1].rank + 1 || movableStack[i].suit !== movableStack[i + 1].suit) { -// return null; -// } -// } -// return movableStack; -// } -// -// // ======================================= -// // 4. 게임 로직 및 상호작용 -// // ======================================= -// let touchStart = {}; -// -// /** -// * (★ 수정) handlePointerDown (랭킹 등록 로직 추가) -// */ -// function handlePointerDown(event) { -// if (isAnimating || isAnimatingCompletion) return; -// const coords = getCanvasCoordinates(event); -// touchStart = { x: coords.x, y: coords.y, time: Date.now() }; -// -// const element = findElementAt(coords.x, coords.y); -// if (!element) return; -// -// if (element.type === 'ui') { -// switch (element.name) { -// case 'startButton': -// startNewGame(); -// break; -// case 'undoButton': -// if (currentGame.undoCount < MAX_UNDO_COUNT) { -// handleUndo(); // API 호출 -// } else { -// currentGame = null; // 포기하고 메뉴로 -// draw(); -// } -// break; -// case 'suitSelect': -// selectedSuit = (selectedSuit === 1) ? 2 : (selectedSuit === 2) ? 4 : 1; -// selectedCardCount = cardDistributionOptions[selectedSuit.toString()][0].value; -// draw(); -// break; -// case 'cardCountSelect': -// const currentOptions = cardDistributionOptions[selectedSuit.toString()]; -// const currentIndex = currentOptions.findIndex(opt => opt.value === selectedCardCount); -// const nextIndex = (currentIndex + 1) % currentOptions.length; -// selectedCardCount = currentOptions[nextIndex].value; -// draw(); -// break; -// case 'submitButton': // (★ 신규) 'restartButton' 대신 'submitButton' 클릭 처리 -// handleRankSubmit(); // 랭킹 제출 함수 호출 -// break; -// } -// } else if (element.type === 'card') { -// const { card, stackIndex, cardIndex } = element; -// const movableStack = getCardStackForMove(card, stackIndex, cardIndex); -// if (movableStack && movableStack.length > 0) { -// draggedCards = movableStack; -// draggedCards.sourceStackIndex = stackIndex; -// const cardPos = getCardPosition(card, stackIndex); -// dragOffsetX = coords.x - cardPos.x; -// dragOffsetY = coords.y - cardPos.y; -// } -// } else if (element.type === 'stock') { -// handleStockClick(); // API 호출 -// } -// } -// -// /** -// * (★ 신규) 랭킹 등록 처리 함수 -// * user.js의 공통 submitRank 함수를 호출합니다. -// */ -// async function handleRankSubmit() { -// const playerName = prompt("랭킹에 등록할 이름을 입력하세요:", "Player"); -// if (!playerName || playerName.trim() === "") return; -// -// try { -// // (★ 신규) user.js의 공통 submitRank 함수 호출 -// // 주 점수(primaryScore) = 이동 횟수 (낮을수록 좋음) -// // 보조 점수(secondaryScore) = 완료 시간 (낮을수록 좋음) -// await submitRank( -// currentGameType, // 'SPIDER' -// currentContextId, // 예: "1_SUITS_4,3" -// playerName.trim(), // Player Name -// currentGame.moves, // Primary Score (Moves) -// completionTimeSeconds // Secondary Score (Time) -// ); -// -// alert("랭킹이 등록되었습니다!"); -// // 랭킹 등록 후 새 게임 시작 (메뉴 화면으로 돌아감) -// currentGame = null; -// isGameCompleted = false; -// draw(); -// -// } catch (error) { -// console.error("Rank submission failed:", error); -// alert("랭킹 등록에 실패했습니다: " + error.message); -// } -// } -// -// -// // --- (PointerMove, PointerUp, Stock/Double Click 핸들러 - 수정 없음) --- -// function handlePointerMove(event) { -// if (!draggedCards || draggedCards.length === 0) return; -// event.preventDefault(); -// const coords = getCanvasCoordinates(event); -// -// if (!isDragging) { -// const dx = coords.x - touchStart.x; -// const dy = coords.y - touchStart.y; -// const distance = Math.sqrt(dx * dx + dy * dy); -// if (distance > DRAG_THRESHOLD) { -// isDragging = true; -// } -// } -// -// if (isDragging) { -// draggedCards[0].x = coords.x - dragOffsetX; -// draggedCards[0].y = coords.y - dragOffsetY; -// } -// -// draw(); -// } -// function handlePointerUp(event) { -// if (!isDragging || draggedCards.length === 0) { -// returnToOriginalPosition(); -// return; -// } -// const coords = getCanvasCoordinates(event); -// const dropTargetStackId = findStackAt(coords.x, coords.y); -// const sourceStackIndex = draggedCards.sourceStackIndex; -// if (dropTargetStackId) { -// const destinationStackIndex = (parseInt(dropTargetStackId.split('-')[1]) || 1) - 1; -// const isValid = isValidMove(draggedCards, destinationStackIndex); -// if (isValid) { -// moveCardLocally(draggedCards, sourceStackIndex, destinationStackIndex); -// checkCompletedStacks(); -// updateGameOnServer(currentGame); -// } else { -// returnToOriginalPosition(); -// } -// } else { -// returnToOriginalPosition(); -// } -// isDragging = false; -// draggedCards = []; -// draw(); -// } -// function returnToOriginalPosition() { -// isDragging = false; -// draggedCards = []; -// } -// function handleStockClick() { -// if (!currentGame || isAnimating || currentGame.stock.length === 0) return; -// dealFromStock(); // API 호출 -// } -// function handleDoubleClick(event) { -// // (★ 수정) 게임이 시작되지 않았거나, 이미 완료되었다면 아무것도 하지 않음 -// if (!currentGame || isGameCompleted) { -// return; -// } -// -// const coords = getCanvasCoordinates(event); -// const clickedCardData = findCardAt(coords.x, coords.y); // 이제 이 코드는 currentGame이 있을 때만 실행됨 -// if (clickedCardData) { -// const { card, stackIndex, cardIndex } = clickedCardData; -// const movableStack = getCardStackForMove(card, stackIndex, cardIndex); -// if (movableStack) { -// const destinationStackId = getBestMoveForStack(movableStack); -// if (destinationStackId) { -// const destinationStackIndex = (parseInt(destinationStackId.split('-')[1]) || 1) - 1; -// moveCardLocally(movableStack, stackIndex, destinationStackIndex); -// checkCompletedStacks(); -// updateGameOnServer(currentGame); -// } -// } -// } -// } -// -// -// /** -// * (★ 수정) checkCompletedStacks (게임 완료 시 타이머 중지) -// */ -// function checkCompletedStacks() { -// let completedCount = 0; -// for (let stackIndex = 0; stackIndex < currentGame.tableau.length; stackIndex++) { -// const stack = currentGame.tableau[stackIndex]; -// if (stack.length < 13) continue; -// const last13Cards = stack.slice(stack.length - 13); -// let isCompleted = true; -// for (let i = 0; i < 12; i++) { -// if (last13Cards[i].rank !== last13Cards[i+1].rank + 1 || last13Cards[i].suit !== last13Cards[i+1].suit) { -// isCompleted = false; -// break; -// } -// } -// if (isCompleted) { -// completedCount++; -// isAnimatingCompletion = true; -// const cardsToRemove = stack.splice(stack.length - 13, 13); -// cardsToRemove.forEach(card => { -// const cardPos = getCardPosition(card, stackIndex); -// card.animStartX = cardPos.x; -// card.animStartY = cardPos.y; -// card.animEndTime = Date.now() + 500; -// card.animTargetX = (UI_ELEMENTS.foundationArea.x + currentGame.foundation.length * (cardWidth * FOUNDATION_CARD_SPACING)); -// card.animTargetY = UI_ELEMENTS.foundationArea.y; -// completedStackCards.push(card); -// }); -// if (stack.length > 0) { -// stack[stack.length - 1].isFaceUp = true; -// } -// // 완성된 스택을 foundation에 추가 -// currentGame.foundation.push(cardsToRemove); -// } -// } -// -// // 모든 카드가 foundation으로 이동했는지 확인 -// const totalFoundationCards = currentGame.foundation.reduce((sum, stack) => sum + stack.length, 0); -// -// // (★ 수정) 게임이 아직 완료되지 않았을 때만 체크 -// if (totalFoundationCards === 104 && !isGameCompleted) { -// isGameCompleted = true; -// -// // (★ 신규) 게임 완료 시점의 시간(초) 기록 -// completionTimeSeconds = Math.floor((Date.now() - gameStartTime) / 1000); -// console.log(`Game Won! Moves: ${currentGame.moves}, Time: ${completionTimeSeconds}s`); -// } -// } -// -// // --- (isValidMove, moveCardLocally - 수정 없음) --- -// function isValidMove(cardsToMove, destinationStackIndex) { -// if (cardsToMove.length === 0) return false; -// const firstCardToMove = cardsToMove[0]; -// const destStackCards = currentGame.tableau[destinationStackIndex]; -// if (destStackCards.length === 0) { -// return true; -// } -// const destTopCard = destStackCards[destStackCards.length - 1]; -// if (firstCardToMove.rank === destTopCard.rank - 1) { -// return true; -// } -// return false; -// } -// function moveCardLocally(cardsToMove, sourceStackIndex, destinationStackIndex) { -// const sourceStack = currentGame.tableau[sourceStackIndex]; -// const newSourceStack = sourceStack.slice(0, sourceStack.length - cardsToMove.length); -// const destinationStack = currentGame.tableau[destinationStackIndex]; -// const newDestinationStack = [...destinationStack, ...cardsToMove]; -// const newTableau = [...currentGame.tableau]; -// newTableau[sourceStackIndex] = newSourceStack; -// newTableau[destinationStackIndex] = newDestinationStack; -// if (newSourceStack.length > 0 && !newSourceStack[newSourceStack.length - 1].isFaceUp) { -// newSourceStack[newSourceStack.length - 1].isFaceUp = true; -// } -// currentGame.tableau = newTableau; -// currentGame.moves++; -// } -// -// -// /** -// * (★ 수정) handleUndo (통합 API 경로로 변경) -// */ -// async function handleUndo() { -// if (!currentGame || isAnimating || currentGame.undoCount >= MAX_UNDO_COUNT || currentGame.undoHistory.length === 0) { -// console.log("실행 취소 불가"); -// return; -// } -// try { -// // (★ 수정) API 경로 변경 -> /puzzle/spider/undo -// const response = await fetch(`/puzzle/spider/undo`, { -// method: 'POST', -// headers: { 'Content-Type': 'application/json' }, -// body: JSON.stringify({ gameId: currentGame.id }) -// }); -// if (!response.ok) throw new Error('Undo failed on server'); -// const newGame = await response.json(); -// currentGame = newGame; -// draw(); -// } catch (error) { -// console.error("실행 취소 중 오류 발생:", error); -// } -// } -// -// // ======================================= -// // 5. 서버 통신 함수 (★ API 경로 전체 수정) -// // ======================================= -// -// /** -// * (★ 수정) API 경로 변경 -> /puzzle/spider/deal -// */ -// async function dealFromStock() { -// if (!currentGame || isAnimating || currentGame.stock.length === 0) return; -// isAnimating = true; -// try { -// const response = await fetch(`/puzzle/spider/deal`, { -// method: 'POST', -// headers: { 'Content-Type': 'application/json' }, -// body: JSON.stringify({ gameId: currentGame.id }) -// }); -// if (!response.ok) throw new Error('Deal failed on server'); -// const newGame = await response.json(); -// currentGame = newGame; -// draw(); -// } catch (error) { -// console.error("카드 분배 중 오류 발생:", error); -// } finally { -// isAnimating = false; -// } -// } -// -// /** -// * (★ 수정) API 경로 변경 -> /puzzle/spider/update -// */ -// async function updateGameOnServer(updatedGame) { -// try { -// const response = await fetch(`/puzzle/spider/update`, { -// method: 'POST', -// headers: { 'Content-Type': 'application/json' }, -// body: JSON.stringify(updatedGame) -// }); -// if (!response.ok) throw new Error('Update failed on server'); -// const newGame = await response.json(); -// currentGame = newGame; -// isDragging = false; -// draggedCards = []; -// draw(); -// } catch (error) { -// console.error("게임 상태 업데이트 중 오류 발생:", error); -// } -// } -// -// /** -// * (★ 수정) API 경로 변경 및 타이머/ContextId 설정 -// */ -// async function startNewGame() { -// if (!assetsLoaded) return; -// const numSuits = selectedSuit; -// const numCards = selectedCardCount; -// -// // (★ 신규) 랭킹 등록용 Context ID 설정 (예: "1_SUITS_4,3") -// currentContextId = `${numSuits}_SUITS_${numCards.replace(',', '-')}`; -// -// try { -// // (★ 수정) API 경로 변경 -> /puzzle/spider/new -// const response = await fetch(`/puzzle/spider/new?numSuits=${numSuits}&numCards=${numCards}`); -// if (!response.ok) throw new Error('Failed to start new game'); -// currentGame = await response.json(); -// gameId = currentGame.id; -// isDragging = false; -// draggedCards = []; -// isGameCompleted = false; -// -// // (★ 신규) 새 게임 시작 시 타이머 시작 -// gameStartTime = Date.now(); -// completionTimeSeconds = 0; -// -// draw(); -// } catch (error) { -// console.error("새 게임 시작 중 오류 발생:", error); -// } -// } -// -// // ======================================= -// // 6. 기타 유틸리티 함수 -// // ======================================= -// // (★ 수정 없음) findStackAt, findCardAt, getCardPosition, getRankText, getSuitSymbol, getBestMoveForStack -// // --- (모든 유틸리티 함수 동일하게 유지) --- -// function findStackAt(x, y) { -// const startY = cardHeight * 0.5; -// for (let i = 0; i < 10; i++) { -// const stackX = tableauStartX + i * (cardWidth + cardGapX); -// const stackCards = currentGame.tableau[i]; -// if (stackCards.length === 0) { -// if (x >= stackX && x <= stackX + cardWidth && y >= startY) { -// return `tableau-${i + 1}`; -// } -// } -// const lastCardIndex = stackCards.length - 1; -// const lastCardY = startY + lastCardIndex * cardOverlapY; -// if (x >= stackX && x <= stackX + cardWidth && y >= lastCardY) { -// return `tableau-${i + 1}`; -// } -// } -// return null; -// } -// -// function findCardAt(x, y) { -// // (★ 수정) 안전을 위해 가드 절 추가. currentGame이 없으면 null 반환 -// if (!currentGame) { -// return null; -// } -// -// for (let stackIndex = 9; stackIndex >= 0; stackIndex--) { -// const stackCards = currentGame.tableau[stackIndex]; // 이제 이 코드는 currentGame이 있을 때만 실행됨 -// for (let cardIndex = stackCards.length - 1; cardIndex >= 0; cardIndex--) { -// const card = stackCards[cardIndex]; -// if (!card.isFaceUp) continue; -// if (x >= card.x && x <= card.x + card.width && y >= card.y && y <= card.y + card.touchHeight) { -// return { card, stackIndex, cardIndex }; -// } -// } -// } -// return null; -// } -// -// function getCardPosition(card, stackIndex) { -// const startY = cardHeight * 0.5; -// const stackCards = currentGame.tableau[stackIndex]; -// const cardIndexInStack = stackCards.findIndex(c => c.suit === card.suit && c.rank === card.rank); -// const x = tableauStartX + stackIndex * (cardWidth + cardGapX); -// const y = startY + cardIndexInStack * cardOverlapY; -// return { x, y }; -// } -// -// function getRankText(rank) { -// if (rank === 1) return 'A'; -// if (rank === 11) return 'J'; -// if (rank === 12) return 'Q'; -// if (rank === 13) return 'K'; -// return String(rank); -// } -// -// function getSuitSymbol(suit) { -// if (suit === 'spade') return '♠️'; -// if (suit === 'heart') return '♥️'; -// if (suit === 'club') return '♣️'; -// if (suit === 'diamond') return '♦️'; -// } -// -// function getBestMoveForStack(cardsToMove) { -// if (cardsToMove.length === 0) return null; -// const firstCardToMove = cardsToMove[0]; -// for (let i = 0; i < 10; i++) { -// const destStackId = `tableau-${i + 1}`; -// const destStackCards = currentGame.tableau[i]; -// if (destStackCards.length === 0) { -// return destStackId; -// } else { -// const destTopCard = destStackCards[destStackCards.length - 1]; -// if (firstCardToMove.rank === destTopCard.rank - 1) { -// return destStackId; -// } -// } -// } -// return null; -// } -// -// -// // --- 초기화 --- -// resizeCanvas(); -// draw(); -// }); \ No newline at end of file diff --git a/src/main/resources/static/js/sudoku.js b/src/main/resources/static/js/sudoku.js deleted file mode 100644 index 699b172..0000000 --- a/src/main/resources/static/js/sudoku.js +++ /dev/null @@ -1,363 +0,0 @@ -// 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 = '
  • 로딩 중...
  • '; -// -// try { -// // (★ 수정) user.js의 공통 fetchRanks 함수 호출 (스도쿠 퍼즐 ID(ContextId) 전달) -// const rankings = await fetchRanks(currentGameType, currentContextId); -// -// rankingList.innerHTML = ''; -// if (rankings.length === 0) { -// rankingList.innerHTML = '
  • 아직 등록된 랭킹이 없습니다.
  • '; -// } 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 = `${index + 1}위: ${rank.playerName} ${minutes}:${seconds}`; -// rankingList.appendChild(li); -// }); -// } -// } catch (error) { -// console.error('랭킹 조회 중 오류 발생:', error); -// rankingList.innerHTML = '
  • 랭킹을 불러올 수 없습니다.
  • '; -// } -// } -// -// /** -// * (★ 수정) 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(); -// }); -// });₩ \ No newline at end of file diff --git a/src/main/resources/static/js/user.js b/src/main/resources/static/js/user.js deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/resources/templates/content/blog/editor.html b/src/main/resources/templates/content/blog/editor.html index b6e1cd9..e19d8ea 100644 --- a/src/main/resources/templates/content/blog/editor.html +++ b/src/main/resources/templates/content/blog/editor.html @@ -33,7 +33,12 @@

    - +
    + + +
    diff --git a/src/main/resources/templates/content/blog/posts.html b/src/main/resources/templates/content/blog/posts.html index 45c7d69..e703c64 100644 --- a/src/main/resources/templates/content/blog/posts.html +++ b/src/main/resources/templates/content/blog/posts.html @@ -23,7 +23,13 @@

    - + + + + 비공개 + + + (읽음: 0) diff --git a/src/main/resources/templates/content/puzzle/2048.html b/src/main/resources/templates/content/puzzle/2048.html index 2a16135..eac1afa 100644 --- a/src/main/resources/templates/content/puzzle/2048.html +++ b/src/main/resources/templates/content/puzzle/2048.html @@ -7,7 +7,6 @@ > - -
    -

    스도쿠 퍼즐 생성기

    -

    버튼을 누르면 새로운 스도쿠 퍼즐을 생성하여 DB에 저장하고 화면에 보여줍니다.

    - -
    +

    스도쿠 퍼즐 생성기

    +

    버튼을 누르면 새로운 스도쿠 퍼즐을 생성하여 DB에 저장하고 화면에 보여줍니다.

    + +
    -

    생성된 퍼즐

    -
    -
    +

    생성된 퍼즐

    +
    +
    diff --git a/src/main/resources/templates/content/puzzle/upload.html b/src/main/resources/templates/content/puzzle/upload.html index 13a1799..5450298 100644 --- a/src/main/resources/templates/content/puzzle/upload.html +++ b/src/main/resources/templates/content/puzzle/upload.html @@ -6,12 +6,183 @@ layout:decorate="~{layout/default_layout}" > - - -
    diff --git a/src/main/resources/templates/content/user/join.html b/src/main/resources/templates/content/user/join.html index 8a5fba5..e91406c 100644 --- a/src/main/resources/templates/content/user/join.html +++ b/src/main/resources/templates/content/user/join.html @@ -12,7 +12,6 @@ onclickJoin([[${enc}]],[[${keyword}]]) } - diff --git a/src/main/resources/templates/content/user/login.html b/src/main/resources/templates/content/user/login.html index a766b93..185bc9a 100644 --- a/src/main/resources/templates/content/user/login.html +++ b/src/main/resources/templates/content/user/login.html @@ -8,7 +8,6 @@ Spring Boot - > diff --git a/src/main/resources/templates/fragments/includes.html b/src/main/resources/templates/fragments/includes.html index a8bc3af..3994a2e 100644 --- a/src/main/resources/templates/fragments/includes.html +++ b/src/main/resources/templates/fragments/includes.html @@ -53,7 +53,6 @@ readCount: [[${srcPost?.readCount ?: 0}]], voteCount: [[${srcPost?.voteCount ?: 0}]], unlikeCount: [[${srcPost?.unlikeCount ?: 0}]], - // --- Page-specific (모델 데이터 아님) --- enc: /*[[${enc ?: ''}]]*/, keyword: /*[[${keyword ?: ''}]]*/ diff --git a/src/main/resources/templates/layout/default_layout.html b/src/main/resources/templates/layout/default_layout.html index 5eee032..fca2acf 100644 --- a/src/main/resources/templates/layout/default_layout.html +++ b/src/main/resources/templates/layout/default_layout.html @@ -59,7 +59,6 @@ -