This commit is contained in:
lunaticbum 2025-09-12 18:01:23 +09:00
parent 19c5d5473f
commit cc43ea8e0a
34 changed files with 461 additions and 3520 deletions

View File

@ -60,7 +60,7 @@ dependencies {
implementation ("org.commonmark:commonmark:0.18.0") implementation ("org.commonmark:commonmark:0.18.0")
implementation ("net.coobird:thumbnailator:0.4.14") 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 ("com.drewnoakes:metadata-extractor:2.19.0")
implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-security")
compileOnly("org.projectlombok:lombok") compileOnly("org.projectlombok:lombok")

View File

@ -75,7 +75,7 @@ class PuzzleController(
/** /**
* 스도쿠: (관리용) 퍼즐 문제 생성 DB 저장 * 스도쿠: (관리용) 퍼즐 문제 생성 DB 저장
*/ */
@GetMapping("/sudoku/generate") @GetMapping("/sudoku/sudoku_gen")
suspend fun sudokuGenerateSinglePuzzle(): SudokuPuzzle { suspend fun sudokuGenerateSinglePuzzle(): SudokuPuzzle {
return puzzleService.sudoku_generateAndSavePuzzle() return puzzleService.sudoku_generateAndSavePuzzle()
} }
@ -182,6 +182,16 @@ class PuzzleController(
return vm return vm
} }
/**
* 스도쿠: 게임 페이지 서빙
*/
@GetMapping("/sudoku_gen.bs")
suspend fun sudoku_gen(): ResultMV {
val vm = ResultMV("content/puzzle/sudoku_gen")
return vm
}
/** /**
* 스파이더: 게임 페이지 서빙 * 스파이더: 게임 페이지 서빙
*/ */

View File

@ -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<Rank>
// </Rank> */
// @PostMapping("/ranks")
// fun saveRank(@RequestBody rank: Rank): Mono<Rank?> { // 👈 요청 Body는 Rank 모델을 그대로 사용
// return rankRepository.save(rank)
// }
//
// /**
// * 특정 게임의 상위 10개 랭킹 리스트를 조회합니다.
// * @param gameId 경로 변수(Path Variable)로 게임 ID를 받습니다.
// * @return Flux<Rank>
// </Rank> */
// @GetMapping("/ranks/{gameId}") // 👈 엔드포인트에 Path Variable 추가
// fun getRankingsByGameId(@PathVariable gameId: String): Flux<Rank?> {
// return rankRepository.findTop10ByGameIdOrderByScoreDesc(gameId)
// }
//}

View File

@ -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<SpiderGame> {
// return spiderService.newGame(numSuits, numCards)
// }
//
// @GetMapping("/{id}")
// fun getGame(@PathVariable id: String): Mono<SpiderGame> {
// return spiderService.getGame(id)
// }
//
// @PostMapping("/update")
// fun updateGame(@RequestBody game: SpiderGame): Mono<SpiderGame> {
// return spiderService.updateGame(game)
// }
//
// // 랭킹 등록 엔드포인트
// @PostMapping("/register")
// fun registerRank(@RequestBody rank: SpiderRank): Mono<ResponseEntity<SpiderRank>> {
// 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<SpiderRank> {
// return spiderService.getRanksByGameId(gameId)
// }
//
// @PostMapping("/deal")
// fun dealCards(@RequestBody request: Map<String, String>): Mono<SpiderGame> {
// val gameId = request["gameId"] ?: return Mono.error(IllegalArgumentException("Game ID is required."))
// return spiderService.dealCardsFromStock(gameId)
// }
//
// // 실행 취소 엔드포인트 추가
// @PostMapping("/undo")
// fun undo(@RequestBody request: Map<String, String>): Mono<SpiderGame> {
// val gameId = request["gameId"] ?: return Mono.error(IllegalArgumentException("Game ID is required."))
// return spiderService.undoGame(gameId)
// }
//}

View File

@ -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<String, Boolean> {
// val isCorrect = sudokuService.validateSolution(validateDto)
// return mapOf("correct" to isCorrect)
// }
//}

View File

@ -165,6 +165,35 @@ interface PostRepository : ReactiveMongoRepository<Post, String> {
"{ \$count: \"totalCount\" }" // 고유 그룹의 개수를 셈 "{ \$count: \"totalCount\" }" // 고유 그룹의 개수를 셈
]) ])
fun countLatestUniqueOrigin(): Mono<AggregationCount> // 헬퍼 클래스로 매핑 fun countLatestUniqueOrigin(): Mono<AggregationCount> // 헬퍼 클래스로 매핑
/**
* 익명 사용자를 위한 '고유 최신 ' 목록을 페이지네이션으로 조회합니다.
* [수정] 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<Post> // 메서드 이름 변경
/**
* '고유 최신 ' 공개된 글의 개수를 카운트합니다.
* [수정] 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<AggregationCount> // 메서드 이름 변경
} }
@ -210,7 +239,7 @@ class PostManager(
* [FIX]: This function should already be correct from the previous step. * [FIX]: This function should already be correct from the previous step.
*/ */
fun findLatestUniquePaginated(pageable: Pageable) : Mono<List<Post>> { // <-- Should already return Mono fun findLatestUniquePaginated(pageable: Pageable) : Mono<List<Post>> { // <-- Should already return Mono
return postRepository.findLatestUniqueOriginPaginated(pageable) return postRepository.findLatestUniquePublishedPaginated(pageable)
.collectList() .collectList()
} }
@ -226,7 +255,7 @@ class PostManager(
*/ */
fun countLatestUnique(): Mono<Long> { fun countLatestUnique(): Mono<Long> {
// AggregationCount(totalCount=N) 객체에서 Long 값만 추출합니다. 결과가 없으면 0L을 반환합니다. // AggregationCount(totalCount=N) 객체에서 Long 값만 추출합니다. 결과가 없으면 0L을 반환합니다.
return postRepository.countLatestUniqueOrigin() return postRepository.countLatestUniquePublished()
.map { it.totalCount } .map { it.totalCount }
.switchIfEmpty(Mono.just(0L)) .switchIfEmpty(Mono.just(0L))
} }

View File

@ -33,7 +33,7 @@ import org.springframework.data.mongodb.core.index.Indexed
* ====================================================== * ======================================================
*/ */
@Document("puzzles") // "puzzles" 컬렉션 (노노그램용) @Document("nonogram") // "puzzles" 컬렉션 (노노그램용)
data class NonogramPuzzle( data class NonogramPuzzle(
@Id @Id
val id: String? = null, val id: String? = null,
@ -446,7 +446,7 @@ interface SpiderGameRepository : ReactiveMongoRepository<SpiderGame, String> {
/** /**
* 스도쿠 퍼즐 원본 데이터를 저장하는 모델 * 스도쿠 퍼즐 원본 데이터를 저장하는 모델
*/ */
@Document(collection = "puzzles") // (참고: 노노그램과 같은 컬렉션을 쓰지만 구조가 다름) @Document(collection = "Sudoku") // (참고: 노노그램과 같은 컬렉션을 쓰지만 구조가 다름)
data class SudokuPuzzle( data class SudokuPuzzle(
@Id @Id
val id: String? = null, val id: String? = null,

View File

@ -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<Rank, String> {
/**
* 특정 gameId에 대해 점수가 높은 순서대로 상위 10개의 랭킹을 조회합니다.
* @param gameId 조회할 게임의 ID
* @return Flux<Rank>
</Rank> */
// 쿼리 메소드 이름 변경 및 파라미터 추가
fun findTop10ByGameIdOrderByScoreDesc(gameId: String): Flux<Rank?> // 👈 수정
}

View File

@ -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로 대체됨.
// */

View File

@ -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<SudokuPuzzle, String> {
// 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로 통합됨.
// */

View File

@ -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;*/
/*}*/

View File

@ -45,15 +45,18 @@ body {
font-family: var(--font-main); font-family: var(--font-main);
background-color: var(--color-bg-page); background-color: var(--color-bg-page);
color: var(--color-text-secondary); color: var(--color-text-secondary);
text-align: center;
margin: 0; margin: 0;
}
/* Create a new class for the game's specific layout */
.game-body-wrapper {
text-align: center;
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
/* (★ 통일) H1 (게임 제목) 스타일 통일 (변수 사용) */ /* (★ 통일) H1 (게임 제목) 스타일 통일 (변수 사용) */
h1 { h1 {
font-size: clamp(2.2em, 8vw, 3.2em); font-size: clamp(2.2em, 8vw, 3.2em);

View File

@ -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 *!*/
/*}*/

View File

@ -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; !* 유지 *!*/
/*}*/

View File

@ -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;*/
/*}*/

View File

@ -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 = '<li>로딩 중...</li>';
// try {
// // (★ 수정) user.js의 공통 fetchRanks 함수 호출
// const rankings = await fetchRanks(currentGameType, currentContextId);
//
// rankingList.innerHTML = ''; // 리스트 비우기
// if (!rankings || rankings.length === 0) {
// rankingList.innerHTML = '<li>등록된 랭킹이 없습니다.</li>';
// return;
// }
//
// rankings.forEach((rank, index) => {
// const li = document.createElement('li');
// // (★ 수정) 공통 모델(GameRank)의 필드명(playerName, primaryScore)을 사용
// li.innerHTML = `<span>${index + 1}. ${rank.playerName}</span><strong>${rank.primaryScore}점</strong>`;
// rankingList.appendChild(li);
// });
// } catch (error) {
// console.error('Error fetching ranks:', error);
// rankingList.innerHTML = '<li>랭킹을 불러올 수 없습니다.</li>';
// }
// }
//
// // ----- 게임 시작 -----
// updateRankingList();
// initializeBoard();
// });

View File

@ -1,53 +1,53 @@
document.addEventListener('DOMContentLoaded', () => { // document.addEventListener('DOMContentLoaded', () => {
const generateBtn = document.getElementById('generate-btn'); // const generateBtn = document.getElementById('generate-btn');
const boardElement = document.getElementById('sudoku-board'); // const boardElement = document.getElementById('sudoku-board');
const statusMessage = document.getElementById('status-message'); // const statusMessage = document.getElementById('status-message');
//
// 스도쿠 보드를 화면에 그리는 함수 // // 스도쿠 보드를 화면에 그리는 함수
function renderBoard(puzzleString) { // function renderBoard(puzzleString) {
boardElement.innerHTML = ''; // 기존 보드 초기화 // boardElement.innerHTML = ''; // 기존 보드 초기화
if (!puzzleString || puzzleString.length !== 81) { // if (!puzzleString || puzzleString.length !== 81) {
statusMessage.textContent = '잘못된 퍼즐 데이터입니다.'; // statusMessage.textContent = '잘못된 퍼즐 데이터입니다.';
return; // return;
} // }
//
for (const char of puzzleString) { // for (const char of puzzleString) {
const cell = document.createElement('div'); // const cell = document.createElement('div');
cell.classList.add('cell'); // cell.classList.add('cell');
cell.textContent = char; // cell.textContent = char;
boardElement.appendChild(cell); // boardElement.appendChild(cell);
} // }
} // }
//
// 생성 버튼 클릭 이벤트 리스너 // // 생성 버튼 클릭 이벤트 리스너
generateBtn.addEventListener('click', async () => { // generateBtn.addEventListener('click', async () => {
// 버튼 비활성화 및 상태 메시지 업데이트 // // 버튼 비활성화 및 상태 메시지 업데이트
generateBtn.disabled = true; // generateBtn.disabled = true;
statusMessage.textContent = '새로운 스도쿠 퍼즐을 생성 중입니다... 🧠'; // statusMessage.textContent = '새로운 스도쿠 퍼즐을 생성 중입니다... 🧠';
boardElement.innerHTML = ''; // 보드 비우기 // boardElement.innerHTML = ''; // 보드 비우기
//
try { // try {
// 백엔드 API 호출 (POST 요청) // // 백엔드 API 호출 (POST 요청)
const response = await fetch('/sudoku/generate', { // const response = await fetch('/sudoku/generate', {
method: 'POST', // method: 'POST',
}); // });
//
if (!response.ok) { // if (!response.ok) {
throw new Error(`서버 오류: ${response.statusText}`); // throw new Error(`서버 오류: ${response.statusText}`);
} // }
//
const result = await response.json(); // const result = await response.json();
//
// 성공 시 보드 렌더링 // // 성공 시 보드 렌더링
renderBoard(result.puzzle); // renderBoard(result.puzzle);
statusMessage.textContent = `✅ 퍼즐 생성 완료! (ID: ${result.puzzleKey})`; // statusMessage.textContent = `✅ 퍼즐 생성 완료! (ID: ${result.puzzleKey})`;
//
} catch (error) { // } catch (error) {
console.error('Error generating Sudoku:', error); // console.error('Error generating Sudoku:', error);
statusMessage.textContent = `❌ 생성 실패: ${error.message}`; // statusMessage.textContent = `❌ 생성 실패: ${error.message}`;
} finally { // } finally {
// 성공/실패 여부와 관계없이 버튼 다시 활성화 // // 성공/실패 여부와 관계없이 버튼 다시 활성화
generateBtn.disabled = false; // generateBtn.disabled = false;
} // }
}); // });
}); // });

View File

@ -541,7 +541,7 @@ function save() {
dataToSend.content = encodeURIComponent(JSON.stringify(quill.getContents())); // Quill 콘텐츠는 JSON 문자열로 변환 후 인코딩 dataToSend.content = encodeURIComponent(JSON.stringify(quill.getContents())); // Quill 콘텐츠는 JSON 문자열로 변환 후 인코딩
dataToSend.category = encodeURIComponent(dataToSend.category || 'none'); dataToSend.category = encodeURIComponent(dataToSend.category || 'none');
dataToSend.tags = encodeURIComponent(dataToSend.tags || ''); dataToSend.tags = encodeURIComponent(dataToSend.tags || '');
dataToSend.posting = document.getElementById('post-published-switch').checked
// 3. 현재 위치 좌표를 '수정 좌표'로 업데이트 // 3. 현재 위치 좌표를 '수정 좌표'로 업데이트
dataToSend.modifyLat = currentLat; dataToSend.modifyLat = currentLat;
dataToSend.modifyLon = currentLon; dataToSend.modifyLon = currentLon;

View File

@ -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 = '<h2>오류: 퍼즐 데이터를 불러올 수 없습니다.</h2>';
// }
// return; // upload.html에서는 여기서 즉시 return됨.
// }
//
// // --- DOM 요소 참조 (게임 페이지 전용) ---
// const modeSelector = document.getElementById('mode-selector');
// const gameBoard = document.getElementById('game-board');
// const pointsDisplay = document.getElementById('points-display');
// const hintBtn = document.getElementById('hint-btn');
// const resultOverlay = document.getElementById('result-overlay');
// const modalTitle = document.getElementById('modal-title');
// const modalMessage = document.getElementById('modal-message');
// const modalButtons = document.getElementById('modal-buttons');
//
//
// // --- (★ 수정) 게임 상태 변수 (타이머 추가) ---
// let currentMode = 'fill';
// let points = 5;
// let isGameFinished = false;
// let gameStartTime = 0; // (★ 신규) 게임 시작 시간 (ms)
//
// let isDragging = false;
// let dragAction = null;
// let startCell = null;
// let lastHoveredCell = null;
// let currentSelection = new Set();
// let affectedRows = new Set();
// let affectedCols = new Set();
//
// // --- 퍼즐 데이터 및 플레이어 진행 상황 ---
// const solution = puzzleData.solutionGrid;
// const numRows = solution.length;
// const numCols = solution[0].length;
// let playerGrid = Array(numRows).fill(0).map(() => Array(numCols).fill(0));
// let lockedRows = Array(numRows).fill(false);
// let lockedCols = Array(numCols).fill(false);
//
//
// function updateMode() {
// currentMode = document.querySelector('input[name="play-mode"]:checked').value;
// }
//
// function calculateCellSize() {
// // ... (셀 크기 계산 로직 - 수정 없음) ...
// const tempContainer = document.createElement('div');
// tempContainer.style.position = 'absolute';
// tempContainer.style.visibility = 'hidden';
// const tempCell = document.createElement('div');
// tempCell.className = 'clue-cell';
// tempCell.textContent = '0';
// tempContainer.appendChild(tempCell);
// document.body.appendChild(tempContainer);
// const fontHeight = tempCell.offsetHeight;
// tempCell.textContent = '10';
// const doubleDigitWidth = tempCell.offsetWidth;
// document.body.removeChild(tempContainer);
// const baseSize = Math.max(fontHeight, doubleDigitWidth, 30);
// return baseSize + 10;
// }
//
// /**
// * (★ 수정) drawBoard (타이머 시작점 추가)
// */
// function drawBoard(cellSize) {
// // ... (모든 보드 그리기 DOM 생성 로직 - 수정 없음) ...
// gameBoard.style.gridTemplateColumns = `${cellSize * 2}px 1fr`;
// gameBoard.style.gridTemplateRows = `${cellSize * 2}px 1fr`;
// const corner = document.createElement('div');
// const colCluesContainer = document.createElement('div');
// colCluesContainer.className = 'col-clues-container';
// const rowCluesContainer = document.createElement('div');
// rowCluesContainer.className = 'row-clues-container';
// const puzzleGridContainer = document.createElement('div');
// puzzleGridContainer.className = 'puzzle-grid-container';
// puzzleGridContainer.style.gridTemplateColumns = `repeat(${numCols}, ${cellSize}px)`;
// puzzleGridContainer.style.gridTemplateRows = `repeat(${numRows}, ${cellSize}px)`;
//
// puzzleData.colClues.forEach((clues, index) => {
// const clueCell = document.createElement('div');
// clueCell.className = 'clue-cell col-clue';
// clueCell.id = `col-clue-${index}`;
// clueCell.style.width = `${cellSize}px`;
// clueCell.innerHTML = clues.join('<br>');
// if ((index + 1) % 5 === 0 && index < numCols - 1) clueCell.classList.add('guide-line-right');
// colCluesContainer.appendChild(clueCell);
// });
// puzzleData.rowClues.forEach((clues, index) => {
// const clueCell = document.createElement('div');
// clueCell.className = 'clue-cell row-clue';
// clueCell.id = `row-clue-${index}`;
// clueCell.style.height = `${cellSize}px`;
// clueCell.textContent = clues.join(' ');
// if ((index + 1) % 5 === 0 && index < numRows - 1) clueCell.classList.add('guide-line-bottom');
// rowCluesContainer.appendChild(clueCell);
// });
// for (let r = 0; r < numRows; r++) {
// for (let c = 0; c < numCols; c++) {
// const cell = document.createElement('div');
// cell.className = 'grid-cell';
// cell.dataset.row = r;
// cell.dataset.col = c;
// if ((c + 1) % 5 === 0 && c < numCols - 1) cell.classList.add('guide-line-right');
// if ((r + 1) % 5 === 0 && r < numRows - 1) cell.classList.add('guide-line-bottom');
// puzzleGridContainer.appendChild(cell);
// }
// }
// gameBoard.appendChild(corner);
// gameBoard.appendChild(colCluesContainer);
// gameBoard.appendChild(rowCluesContainer);
// gameBoard.appendChild(puzzleGridContainer);
//
// // (★ 신규) 보드가 그려지는 시점을 게임 시작 시간으로 기록
// gameStartTime = Date.now();
//
// attachEventListeners(puzzleGridContainer);
// }
//
//
// /**
// * 화면 너비에 맞춰 게임 보드 전체를 비율 그대로 축소/확대합니다.
// */
// function fitBoardToScreen() {
// const viewport = document.getElementById('board-viewport');
// const board = document.getElementById('game-board');
// board.style.transform = 'scale(1)';
// const boardRect = board.getBoundingClientRect();
// const viewportRect = viewport.getBoundingClientRect();
// if (boardRect.width > viewportRect.width) {
// const scale = viewportRect.width / boardRect.width;
// board.style.transform = `scale(${scale})`;
// viewport.style.height = `${boardRect.height * scale}px`;
// } else {
// board.style.transform = 'scale(1)';
// viewport.style.height = `${boardRect.height}px`;
// }
// }
//
//
// /**
// * 셀의 상태를 변경하는 유일한 함수. 모든 사용자 입력은 이 함수를 거칩니다.
// */
// function updateCellState(cell, action) {
// if (isGameFinished) return;
// const row = parseInt(cell.dataset.row);
// const col = parseInt(cell.dataset.col);
// if (lockedRows[row] || lockedCols[col]) return;
// affectedRows.add(row);
// affectedCols.add(col);
// const currentState = playerGrid[row][col];
// let newState = currentState;
// if (action === 'fill') {
// if (solution[row][col] === 0) {
// points--;
// updatePointsDisplay();
// cell.classList.add('incorrect');
// setTimeout(() => cell.classList.remove('incorrect'), 500);
// if (points <= 0) triggerGameOver();
// return;
// }
// newState = 1;
// } else if (action === 'mark') {
// newState = -1;
// } else if (action === 'clear') {
// newState = 0;
// }
// if (currentState !== newState) {
// playerGrid[row][col] = newState;
// cell.classList.toggle('filled', newState === 1);
// cell.classList.toggle('marked', newState === -1);
// }
// }
// // --- (이벤트 리스너 및 드래그/터치 핸들러) ---
// // (★ 수정 없음) attachEventListeners, handleDragStart, handleDragMove, handleDragEnd
// // (★ 수정 없음) updateSelectionVisuals, clearSelectionVisuals
// // --- (모두 동일하게 유지) ---
// function attachEventListeners(grid) {
// grid.addEventListener('mousedown', (e) => handleDragStart(e));
// grid.addEventListener('mouseover', (e) => handleDragMove(e));
// grid.addEventListener('contextmenu', (e) => e.preventDefault());
// grid.addEventListener('touchstart', (e) => handleDragStart(e), { passive: false });
// grid.addEventListener('touchmove', (e) => handleDragMove(e), { passive: false });
// }
// window.addEventListener('mouseup', () => handleDragEnd());
// window.addEventListener('touchend', () => handleDragEnd());
// modeSelector.addEventListener('change', updateMode);
// function handleDragStart(e) {
// if (isGameFinished || !e.target.classList.contains('grid-cell')) return;
// isDragging = true;
// e.preventDefault();
// const cell = e.target;
// const currentState = playerGrid[parseInt(cell.dataset.row)][parseInt(cell.dataset.col)];
// if (e.type === 'mousedown') {
// if (e.button === 0) {
// dragAction = (currentState === 1) ? 'clear' : 'fill';
// document.querySelector('input[name="play-mode"][value="fill"]').checked = true;
// } else if (e.button === 2) {
// dragAction = (currentState === -1) ? 'clear' : 'mark';
// document.querySelector('input[name="play-mode"][value="mark"]').checked = true;
// }
// } else {
// const currentMode = document.querySelector('input[name="play-mode"]:checked').value;
// if (currentMode === 'fill') {
// dragAction = (currentState === 1) ? 'clear' : 'fill';
// } else {
// dragAction = (currentState === -1) ? 'clear' : 'mark';
// }
// }
// startCell = { row: parseInt(cell.dataset.row), col: parseInt(cell.dataset.col) };
// lastHoveredCell = startCell;
// updateSelectionVisuals();
// }
// function handleDragMove(e) {
// if (!isDragging) return;
// e.preventDefault();
// const target = (e.touches)
// ? document.elementFromPoint(e.touches[0].clientX, e.touches[0].clientY)
// : e.target;
// if (target && target.classList.contains('grid-cell')) {
// const row = parseInt(target.dataset.row);
// const col = parseInt(target.dataset.col);
// if (row !== lastHoveredCell.row || col !== lastHoveredCell.col) {
// lastHoveredCell = { row, col };
// updateSelectionVisuals();
// }
// }
// }
// function handleDragEnd() {
// if (!isDragging) return;
// currentSelection.forEach(cell => updateCellState(cell, dragAction));
// clearSelectionVisuals();
// if (dragAction === 'fill' || dragAction === 'clear') {
// checkAndLockCompletedLines(affectedRows, affectedCols);
// }
// checkWinCondition();
// isDragging = false;
// dragAction = null;
// startCell = null;
// lastHoveredCell = null;
// currentSelection.clear();
// affectedRows.clear();
// affectedCols.clear();
// }
// /**
// * 드래그 중인 사각형 영역을 계산하고 시각적으로 업데이트하는 함수
// */
// function updateSelectionVisuals() {
// const newSelection = new Set();
// if (!startCell || !lastHoveredCell) return;
// const r1 = Math.min(startCell.row, lastHoveredCell.row);
// const r2 = Math.max(startCell.row, lastHoveredCell.row);
// const c1 = Math.min(startCell.col, lastHoveredCell.col);
// const c2 = Math.max(startCell.col, lastHoveredCell.col);
// for (let r = r1; r <= r2; r++) {
// for (let c = c1; c <= c2; c++) {
// const cell = document.querySelector(`.grid-cell[data-row='${r}'][data-col='${c}']`);
// if (cell) newSelection.add(cell);
// }
// }
// currentSelection.forEach(cell => {
// if (!newSelection.has(cell)) cell.classList.remove('selecting');
// });
// newSelection.forEach(cell => {
// if (!currentSelection.has(cell)) cell.classList.add('selecting');
// });
// currentSelection = newSelection;
// }
// /**
// * 모든 시각적 피드백을 제거하는 함수
// */
// function clearSelectionVisuals() {
// currentSelection.forEach(cell => cell.classList.remove('selecting'));
// }
//
// /**
// * 특정 행이 '칠해야 할 칸'을 모두 만족했는지 검사
// */
// function isRowComplete(rowIndex) {
// for (let c = 0; c < numCols; c++) {
// if (solution[rowIndex][c] === 1 && playerGrid[rowIndex][c] !== 1) return false;
// }
// return true;
// }
// /**
// * 특정 열이 '칠해야 할 칸'을 모두 만족했는지 검사
// */
// function isColComplete(colIndex) {
// for (let r = 0; r < numRows; r++) {
// if (solution[r][colIndex] === 1 && playerGrid[r][colIndex] !== 1) return false;
// }
// return true;
// }
//
// // --- (게임 완료 체크 로직) ---
// /**
// * 완성된 라인을 확인하고 잠금 처리 및 스타일 변경 (X 자동 완성 없음)
// */
// function checkAndLockCompletedLines(rowsToCheck, colsToCheck) {
// rowsToCheck.forEach(r => {
// if (!lockedRows[r] && isRowComplete(r)) {
// lockedRows[r] = true;
// document.getElementById(`row-clue-${r}`).classList.add('completed');
// for (let c = 0; c < numCols; c++) {
// document.querySelector(`.grid-cell[data-row='${r}'][data-col='${c}']`).classList.add('locked');
// }
// }
// });
// colsToCheck.forEach(c => {
// if (!lockedCols[c] && isColComplete(c)) {
// lockedCols[c] = true;
// document.getElementById(`col-clue-${c}`).classList.add('completed');
// for (let r = 0; r < numRows; r++) {
// document.querySelector(`.grid-cell[data-row='${r}'][data-col='${c}']`).classList.add('locked');
// }
// }
// });
// }
// function checkWinCondition() {
// if (isGameFinished) return;
// for (let r = 0; r < numRows; r++) {
// for (let c = 0; c < numCols; c++) {
// const playerState = (playerGrid[r][c] === 1) ? 1 : 0;
// if (playerState !== solution[r][c]) return;
// }
// }
// triggerGameSuccess();
// }
//
//
// function updatePointsDisplay() {
// pointsDisplay.textContent = points;
// hintBtn.disabled = (points <= 0 || isGameFinished);
// }
//
// /**
// * (★ 신규) 노노그램 랭킹 등록을 처리하는 함수
// * 이 함수는 user.js에 정의된 공통 submitRank 함수를 호출합니다.
// */
// async function submitNonogramRank(completionTime, hintsUsed) {
// const playerName = prompt("랭킹에 등록할 이름을 입력하세요:", "Player");
// if (!playerName || playerName.trim() === "") return;
//
// try {
// // (★ 신규) user.js의 공통 submitRank 함수 호출
// // 주 점수(primaryScore) = 완료 시간(초) (낮을수록 좋음)
// // 보조 점수(secondaryScore) = 사용한 힌트 수(5-남은포인트) (낮을수록 좋음)
// await submitRank(
// 'NONOGRAM', // GameType
// puzzleData.id, // ContextId (퍼즐 고유 ID)
// playerName.trim(), // playerName
// completionTime, // primaryScore (시간)
// hintsUsed // secondaryScore (힌트 사용 횟수)
// );
//
// alert("랭킹이 등록되었습니다!");
// // 랭킹 등록 버튼 비활성화 (중복 제출 방지)
// const submitBtn = document.getElementById('modal-submit-rank-btn');
// if (submitBtn) submitBtn.disabled = true;
//
// } catch (error) {
// console.error("Rank submission failed:", error);
// alert("랭킹 등록에 실패했습니다: " + error.message);
// }
// }
//
// /**
// * (★ 수정) 성공/실패 모달 (랭킹 등록 버튼 추가를 위해 ID 할당 기능 추가)
// */
// function showResultModal(config) {
// modalTitle.textContent = config.title;
// modalMessage.textContent = config.message;
// modalButtons.innerHTML = '';
// config.buttons.forEach(btnInfo => {
// const button = document.createElement('button');
// button.textContent = btnInfo.text;
// button.className = btnInfo.class || '';
// button.onclick = btnInfo.action;
// if (btnInfo.id) button.id = btnInfo.id; // (★ 신규) 버튼 ID 할당 기능
// modalButtons.appendChild(button);
// });
// resultOverlay.classList.remove('hidden');
// setTimeout(() => resultOverlay.classList.add('visible'), 10);
// }
//
//
// /**
// * 게임 실패 처리
// */
// function triggerGameOver() {
// if (isGameFinished) return;
// isGameFinished = true;
// document.querySelector('.puzzle-grid-container').style.pointerEvents = 'none';
// hintBtn.disabled = true;
// showResultModal({
// title: 'Failure', message: '포인트를 모두 사용했습니다.', buttons: [
// { text: '재시도 (Retry)', class: 'primary', action: () => window.location.reload() },
// { text: '홈으로 (Home)', action: () => window.location.href = '/' }
// ]
// });
// }
//
// /**
// * (★ 수정) 게임 성공 처리 (타이머 계산 및 랭킹 버튼 추가)
// */
// function triggerGameSuccess() {
// if (isGameFinished) return;
// isGameFinished = true;
//
// // (★ 신규) 게임 완료 시간 및 힌트 사용량 계산
// const completionTimeSeconds = Math.floor((Date.now() - gameStartTime) / 1000);
// const hintsUsed = 5 - points; // 힌트 사용량 = 5 - 남은 포인트
//
// // --- 요소 참조 및 상호작용 비활성화 ---
// const viewport = document.getElementById('board-viewport');
// const puzzleGridContainer = document.querySelector('.puzzle-grid-container');
// const grayscaleImg = document.getElementById('grayscale-reveal');
// const originalImg = document.getElementById('original-reveal');
// puzzleGridContainer.style.pointerEvents = 'none';
// hintBtn.disabled = true;
//
// // --- 애니메이션 위치 및 크기 계산 ---
// const gridRect = puzzleGridContainer.getBoundingClientRect();
// const viewportRect = viewport.getBoundingClientRect();
// const top = gridRect.top - viewportRect.top;
// const left = gridRect.left - viewportRect.top; // (오타 수정) viewportRect.top -> viewportRect.left
//
// [grayscaleImg, originalImg].forEach(img => {
// img.style.top = `${top}px`;
// img.style.left = `${left}px`;
// img.style.width = `${gridRect.width}px`;
// img.style.height = `${gridRect.height}px`;
// img.src = (img.id === 'grayscale-reveal') ? puzzleData.grayscaleImage : puzzleData.originalImage;
// });
//
// // --- 애니메이션 순차 실행 ---
// setTimeout(() => {
// grayscaleImg.style.opacity = '1';
// setTimeout(() => {
// originalImg.style.opacity = '1';
// setTimeout(() => {
// // (★ 수정) 모달 버튼 설정에 "랭킹 등록" 버튼 추가
// showResultModal({
// title: 'Success! 🎉',
// message: `퍼즐을 완성했습니다! (시간: ${completionTimeSeconds}초, 힌트 사용: ${hintsUsed}개)`,
// buttons: [
// {
// text: '랭킹 등록',
// class: 'primary',
// id: 'modal-submit-rank-btn', // (★ 신규) 랭킹 제출 버튼
// action: () => submitNonogramRank(completionTimeSeconds, hintsUsed)
// },
// { text: '다른 문제 풀기', action: () => window.location.href = '/puzzle/play' },
// { text: '홈으로', action: () => window.location.href = '/' }
// ]
// });
// }, 2000);
// }, 2000);
// }, 500);
// }
//
// // 힌트 버튼 클릭 이벤트 처리
// hintBtn.addEventListener('click', () => {
// if (points <= 0 || isGameFinished) return;
// points--;
// updatePointsDisplay();
// const hintCandidates = [];
// for (let r = 0; r < numRows; r++) {
// for (let c = 0; c < numCols; c++) {
// if (solution[r][c] === 1 && playerGrid[r][c] !== 1) {
// hintCandidates.push({ r, c });
// }
// }
// }
// if (hintCandidates.length > 0) {
// const hint = hintCandidates[Math.floor(Math.random() * hintCandidates.length)];
// const cellToReveal = document.querySelector(`.grid-cell[data-row='${hint.r}'][data-col='${hint.c}']`);
//
// updateCellState(cellToReveal, 'fill');
//
// const hintAffectedRows = new Set([hint.r]);
// const hintAffectedCols = new Set([hint.c]);
// checkAndLockCompletedLines(hintAffectedRows, hintAffectedCols);
// checkWinCondition();
// } else {
// alert("더 이상 사용할 힌트가 없습니다!");
// points++;
// updatePointsDisplay();
// }
// if (points <= 0 && !isGameFinished) {
// triggerGameOver();
// }
// });
//
// // --- 초기 실행 ---
// const optimalCellSize = calculateCellSize();
// drawBoard(optimalCellSize);
// updatePointsDisplay();
// updateMode();
//
// requestAnimationFrame(() => {
// fitBoardToScreen();
// window.addEventListener('resize', fitBoardToScreen);
// });
// });
//
//
// /**
// * ==============================================
// * upload.js (업로드 페이지 로직)
// * (★ 리팩토링: 통합 API 경로 사용)
// * ==============================================
// */
// let currentPuzzleData = null; // 업로드 성공 시 퍼즐 데이터 저장
//
// // (★ 수정 없음) 업로드 페이지용 성공 애니메이션 함수
// function showSuccessAnimation() {
// if (!currentPuzzleData) return;
//
// const puzzleContainer = document.getElementById('puzzle-container');
// const grayscaleImg = document.getElementById('grayscale-reveal');
// const originalImg = document.getElementById('original-reveal');
//
// grayscaleImg.src = currentPuzzleData.grayscaleImage;
// originalImg.src = currentPuzzleData.originalImage;
//
// puzzleContainer.style.transition = 'opacity 0.5s';
// puzzleContainer.style.opacity = '0';
// grayscaleImg.style.opacity = '1';
//
// setTimeout(() => {
// grayscaleImg.style.opacity = '0';
// originalImg.style.opacity = '1';
// }, 2000);
// }
//
// // (★ 수정 없음) 업로드 페이지용 퍼즐 미리보기 그리기 함수
// function drawPuzzle(puzzleData) {
// const container = document.getElementById('puzzle-container');
// container.innerHTML = '';
//
// const { solutionGrid, rowClues, colClues } = puzzleData;
// const numRows = solutionGrid.length;
// const numCols = solutionGrid[0].length;
//
// container.style.gridTemplateColumns = `auto repeat(${numCols}, 1fr)`;
// container.style.gridTemplateRows = `auto repeat(${numRows}, 1fr)`;
//
// // 1. 코너
// const corner = document.createElement('div');
// corner.className = 'grid-cell';
// container.appendChild(corner);
//
// // 2. 열 힌트
// for (const clues of colClues) {
// const clueCell = document.createElement('div');
// clueCell.className = 'clue-cell';
// clueCell.innerHTML = clues.join('<br>');
// container.appendChild(clueCell);
// }
//
// // 3. 행 힌트 및 정답 그리드
// for (let i = 0; i < numRows; i++) {
// const rowClueCell = document.createElement('div');
// rowClueCell.className = 'clue-cell';
// rowClueCell.textContent = rowClues[i].join(' ');
// container.appendChild(rowClueCell);
//
// for (let j = 0; j < numCols; j++) {
// const cell = document.createElement('div');
// cell.className = 'solution-cell';
// if (solutionGrid[i][j] === 1) {
// cell.classList.add('filled');
// } else {
// cell.classList.add('empty');
// }
// container.appendChild(cell);
// }
// }
// }
//
//
// // upload.js의 DOMContentLoaded 리스너
// document.addEventListener('DOMContentLoaded', () => {
//
// const createBtn = document.getElementById('createBtn');
//
// // createBtn이 없는 nonogram.html(게임 페이지)에서는 이 리스너가 아무것도 실행하지 않음.
// if (!createBtn) {
// return;
// }
//
// // (업로드 페이지 전용 로직)
// createBtn.addEventListener('click', async () => {
// const uploader = document.getElementById('imageUploader');
// const statusDiv = document.getElementById('status');
// const puzzleContainer = document.getElementById('puzzle-container');
// const testSuccessBtn = document.getElementById('test-success-btn');
// const deleteBtn = document.getElementById('delete-btn');
// const playBtn = document.getElementById('play-btn');
//
// if (uploader.files.length === 0) {
// statusDiv.textContent = '이미지 파일을 선택해주세요.';
// return;
// }
//
// const imageFile = uploader.files[0];
// const formData = new FormData();
// formData.append('imageFile', imageFile);
//
// statusDiv.textContent = '문제를 생성하는 중...';
// puzzleContainer.innerHTML = '';
//
// try {
// // (★ 수정) API 경로 변경 -> 통합 컨트롤러의 /puzzle/upload.bjx 호출
// const response = await fetch('/puzzle/upload.bjx', {
// method: 'POST',
// body: formData,
// });
//
// if (response.ok) {
// const puzzleData = await response.json();
// statusDiv.textContent = '문제 생성 성공!';
// drawPuzzle(puzzleData); // 미리보기 그리기
//
// currentPuzzleData = puzzleData;
// testSuccessBtn.addEventListener('click', showSuccessAnimation);
//
// testSuccessBtn.style.display = 'inline-block';
// deleteBtn.style.display = 'inline-block';
// playBtn.style.display = 'inline-block';
// } else {
// const errorMessage = await response.text();
// statusDiv.textContent = `생성 실패: ${errorMessage}`;
// }
// } catch (error) {
// console.error('네트워크 오류:', error);
// statusDiv.textContent = '서버와 통신 중 오류가 발생했습니다.';
// }
//
// deleteBtn.addEventListener('click', async () => {
// if (!currentPuzzleData || !currentPuzzleData.id) {
// alert('삭제할 퍼즐이 선택되지 않았습니다.');
// return;
// }
// if (!confirm('정말로 이 퍼즐을 삭제하시겠습니까?')) {
// return;
// }
// try {
// // (★ 수정) API 경로 변경 -> 통합 컨트롤러의 /puzzle/{id}.bjx 호출
// const response = await fetch(`/puzzle/${currentPuzzleData.id}.bjx`, {
// method: 'DELETE',
// });
//
// if (response.ok) {
// statusDiv.textContent = '퍼즐이 성공적으로 삭제되었습니다.';
// puzzleContainer.innerHTML = '';
// // (버그 수정) success-animation-container 내부의 img src를 초기화해야 함
// document.getElementById('grayscale-reveal').src = "";
// document.getElementById('original-reveal').src = "";
//
// testSuccessBtn.style.display = 'none';
// deleteBtn.style.display = 'none';
// playBtn.style.display = 'none';
// currentPuzzleData = null;
// } else {
// statusDiv.textContent = `삭제 실패: 서버 오류 (${response.status})`;
// }
// } catch (error) {
// console.error('삭제 중 네트워크 오류:', error);
// statusDiv.textContent = '삭제 중 오류가 발생했습니다.';
// }
// });
//
// playBtn.addEventListener('click', () => {
// if (currentPuzzleData && currentPuzzleData.id) {
// // (★ 수정 없음) 이 경로는 PuzzleController의 페이지 서빙 경로와 일치하므로 올바름.
// window.location.href = `/puzzle/play/${currentPuzzleData.id}`;
// }
// });
// });
// });

File diff suppressed because it is too large Load Diff

View File

@ -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 = '<li>로딩 중...</li>';
//
// try {
// // (★ 수정) user.js의 공통 fetchRanks 함수 호출 (스도쿠 퍼즐 ID(ContextId) 전달)
// const rankings = await fetchRanks(currentGameType, currentContextId);
//
// rankingList.innerHTML = '';
// if (rankings.length === 0) {
// rankingList.innerHTML = '<li>아직 등록된 랭킹이 없습니다.</li>';
// } else {
// rankings.forEach((rank, index) => {
// const li = document.createElement('li');
// // (★ 수정) 공통 모델 필드(primaryScore)를 시간(초)으로 변환
// const minutes = Math.floor(rank.primaryScore / 60).toString().padStart(2, '0');
// const seconds = (rank.primaryScore % 60).toString().padStart(2, '0');
// li.innerHTML = `<span>${index + 1}위: ${rank.playerName}</span> <span>${minutes}:${seconds}</span>`;
// rankingList.appendChild(li);
// });
// }
// } catch (error) {
// console.error('랭킹 조회 중 오류 발생:', error);
// rankingList.innerHTML = '<li>랭킹을 불러올 수 없습니다.</li>';
// }
// }
//
// /**
// * (★ 수정) user.js의 공통 submitRank 함수를 사용하도록 수정
// */
// submitRankBtn.addEventListener('click', async () => {
// const userName = document.getElementById('username-input').value.trim();
// if (!userName) return alert('이름을 입력해주세요.');
//
// try {
// // (★ 수정) user.js의 공통 submitRank 함수 호출
// // 스도쿠의 주 점수(primaryScore)는 완료 시간(secondsElapsed), 보조 점수는 없음.
// await submitRank(currentGameType, currentContextId, userName, secondsElapsed, null);
//
// alert('랭킹이 성공적으로 등록되었습니다!');
// showRankingModal(); // 랭킹 목록 새로고침
// submitRankBtn.disabled = true; // 중복 등록 방지
// } catch (error) {
// console.error('랭킹 등록 중 오류 발생:', error);
// alert('랭킹 등록에 실패했습니다. 다시 시도해주세요.');
// }
// });
//
// function resetGameView() {
// setupContainer.classList.remove('hidden');
// gameContainer.classList.add('hidden');
// numberInputButtons.classList.add('hidden');
// clearInterval(timerInterval);
// selectedNumber = null;
// focusedCell = null;
// document.querySelectorAll('.cell').forEach(cell => {
// cell.classList.remove('highlight-focused', 'highlight-same-number', 'highlight-selected-number');
// });
// document.querySelectorAll('.num-btn').forEach(btn => btn.classList.remove('selected', 'completed'));
// }
//
// closeModalBtn.addEventListener('click', () => {
// modalOverlay.classList.add('hidden');
// resetGameView();
// });
//
// retryBtn.addEventListener('click', () => {
// gameOverModal.classList.add('hidden');
// resetGameView();
// });
// });₩

View File

@ -33,7 +33,12 @@
<p th:if="${srcPost.writeTime != null and srcPost.writeTime > 0}" <p th:if="${srcPost.writeTime != null and srcPost.writeTime > 0}"
th:text="${#temporals.format(T(java.time.Instant).ofEpochMilli(srcPost.writeTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm:ss')}"></p> th:text="${#temporals.format(T(java.time.Instant).ofEpochMilli(srcPost.writeTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm:ss')}"></p>
</header> </header>
<div style="text-align: right; margin-bottom: 2em;">
<label for="post-published-switch" style="font-weight: bold; color: #555; vertical-align: middle; margin-right: 10px;">
게시물 공개
</label>
<input type="checkbox" id="post-published-switch" name="posting" th:checked="${srcPost.posting}" />
</div>
<div class="write_controllbox" style="margin-top: -1em; margin-bottom: 2em;"> <div class="write_controllbox" style="margin-top: -1em; margin-bottom: 2em;">
<div class="write_option btn-example controlbox-category" to="#popLayer1"> <div class="write_option btn-example controlbox-category" to="#popLayer1">
</div> </div>

View File

@ -23,7 +23,13 @@
</a> </a>
<div class="inner"> <div class="inner">
<h3 style="display: flex; justify-content: space-between; align-items: center;"> <h3 style="display: flex; justify-content: space-between; align-items: center;">
<span>
<span th:if="${!post.posting}" style="background-color: #888; color: white; font-size: 0.7em; padding: 2px 6px; border-radius: 4px; margin-right: 8px; vertical-align: middle;">
비공개
</span>
<span th:text="${post.title != null and not #strings.isEmpty(post.title)} ? ${post.title} : 'untitled[' + ${#temporals.format(T(java.time.Instant).ofEpochMilli(post.writeTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm')} + ']'"></span> <span th:text="${post.title != null and not #strings.isEmpty(post.title)} ? ${post.title} : 'untitled[' + ${#temporals.format(T(java.time.Instant).ofEpochMilli(post.writeTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm')} + ']'"></span>
</span>
<span style="font-size: 0.75em; color: #888; font-weight: normal; white-space: nowrap; margin-left: 1em;"> <span style="font-size: 0.75em; color: #888; font-weight: normal; white-space: nowrap; margin-left: 1em;">
(읽음: <span th:text="${post.readCount}">0</span>) (읽음: <span th:text="${post.readCount}">0</span>)
</span> </span>

View File

@ -7,7 +7,6 @@
> >
<head> <head>
<link th:href="@{/css/common_game_theme.css}" rel="stylesheet" /> <link th:href="@{/css/common_game_theme.css}" rel="stylesheet" />
<script type="text/javascript" th:src="@{/js/user.js}"></script>
<style> <style>
/* ================================= /* =================================
@ -96,19 +95,21 @@
.tile-4 { background-color: #bbdefb; color: #333; } /* #ede0c8 (노란 베이지) -> #bbdefb (파랑) */ .tile-4 { background-color: #bbdefb; color: #333; } /* #ede0c8 (노란 베이지) -> #bbdefb (파랑) */
/* 8부터는 고유 색상이므로 유지 (새 테마와 잘 어울림) */ /* 8부터는 고유 색상이므로 유지 (새 테마와 잘 어울림) */
.tile-8 { background-color: #f2b179; color: #f9f6f2; } .tile-2 { background-color: #E3F2FD; color: #333; } /* 아주 밝은 파랑 */
.tile-16 { background-color: #f59563; color: #f9f6f2; } .tile-4 { background-color: #BBDEFB; color: #333; } /* 밝은 파랑 */
.tile-32 { background-color: #f67c5f; color: #f9f6f2; } .tile-8 { background-color: #90CAF9; color: #fff; } /* 파랑 */
.tile-64 { background-color: #f65e3b; color: #f9f6f2; } .tile-16 { background-color: #64B5F6; color: #fff; } /* 조금 더 짙은 파랑 */
.tile-128 { background-color: #edcf72; color: #f9f6f2; } .tile-32 { background-color: #42A5F5; color: #fff; } /* 짙은 파랑 */
.tile-256 { background-color: #edcc61; color: #f9f6f2; } .tile-64 { background-color: #2196F3; color: #fff; } /* 선명한 파랑 */
.tile-512 { background-color: #edc850; color: #f9f6f2; } .tile-128 { background-color: #1E88E5; color: #fff; } /* 더 선명한 파랑 */
.tile-1024 { background-color: #edc53f; color: #f9f6f2; } .tile-256 { background-color: #1976D2; color: #fff; } /* 깊은 파랑 */
.tile-2048 { background-color: #edc22e; color: #f9f6f2; } .tile-512 { background-color: #1565C0; color: #fff; } /* 아주 깊은 파랑 */
.tile-4096 { background-color: #3c3a32; color: #f9f6f2; } .tile-1024 { background-color: #0D47A1; color: #fff; } /* 남색에 가까운 파랑 */
.tile-8192 { background-color: #ff3333; color: #f9f6f2; } .tile-2048 { background-color: #283593; color: #fff; } /* 남색 */
.tile-16384 { background-color: #0077cc; color: #f9f6f2; } .tile-4096 { background-color: #3F51B5; color: #fff; } /* 인디고 */
.tile-32768 { background-color: #9900cc; color: #f9f6f2; } .tile-8192 { background-color: #673AB7; color: #fff; } /* 딥 퍼플 */
.tile-16384 { background-color: #4527A0; color: #fff; } /* 더 짙은 딥 퍼플 */
.tile-32768 { background-color: #311B92; color: #fff; } /* 가장 짙은 딥 퍼플 */
/* ================================= /* =================================
@ -186,7 +187,8 @@
</head> </head>
<body> <body>
<th:block layout:fragment="content"> <th:block layout:fragment="content">
<h1>2048</h1>
<div class="game-body-wrapper"> <h1>2048</h1>
<p>화살표를 터치하거나 키보드 방향키를 이용해 타일을 합쳐보세요!</p> <p>화살표를 터치하거나 키보드 방향키를 이용해 타일을 합쳐보세요!</p>
<div class="game-container"> <div class="game-container">
<div class="score-container"> <div class="score-container">
@ -208,7 +210,7 @@
<ol id="ranking-list"></ol> <ol id="ranking-list"></ol>
</div> </div>
</div> </div>
</div>
<script type="text/javascript"> <script type="text/javascript">
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// ... (DOM 요소 가져오기 - 동일) // ... (DOM 요소 가져오기 - 동일)

View File

@ -6,7 +6,6 @@
layout:decorate="~{layout/default_layout}" layout:decorate="~{layout/default_layout}"
> >
<th:block layout:fragment="head" id="head"> <th:block layout:fragment="head" id="head">
<script type="text/javascript" th:src="@{/js/user.js}"></script>
<link th:href="@{/css/common_game_theme.css}" rel="stylesheet" /> <link th:href="@{/css/common_game_theme.css}" rel="stylesheet" />
<style> <style>
@ -1065,8 +1064,10 @@
</th:block > </th:block >
<th:block layout:fragment="content"> <th:block layout:fragment="content">
<div class="game-body-wrapper">
<div id="game-container"> <div id="game-container">
<canvas id="gameCanvas"></canvas> <canvas id="gameCanvas"></canvas>
</div> </div>
</div>
</th:block> </th:block>
</html> </html>

View File

@ -7,8 +7,6 @@
> >
<head layout:fragment="head" id="head"> <head layout:fragment="head" id="head">
<link th:href="@{/css/common_game_theme.css}" rel="stylesheet" /> <link th:href="@{/css/common_game_theme.css}" rel="stylesheet" />
<script type="text/javascript" th:src="@{/js/user.js}"></script>
<style> <style>
/* sudoku.css의 내용을 여기에 삽입 */ /* sudoku.css의 내용을 여기에 삽입 */
body { body {
@ -614,7 +612,7 @@
try { try {
// user.js의 공통 fetchRanks 함수 호출 (스도쿠 퍼즐 ID 전달) // user.js의 공통 fetchRanks 함수 호출 (스도쿠 퍼즐 ID 전달)
// currentGameType 변수가 정의되어 있어야 합니다. 예: const currentGameType = 'sudoku'; // currentGameType 변수가 정의되어 있어야 합니다. 예: const currentGameType = 'sudoku';
const currentGameType = 'sudoku'; const currentGameType = 'SUDOKU';
const rankings = await fetchRanks(currentGameType, currentPuzzleId); const rankings = await fetchRanks(currentGameType, currentPuzzleId);
rankingList.innerHTML = ''; rankingList.innerHTML = '';
@ -644,7 +642,7 @@
try { try {
// user.js의 공통 submitRank 함수 호출 // user.js의 공통 submitRank 함수 호출
const currentGameType = 'sudoku'; const currentGameType = 'SUDOKU';
await submitRank(currentGameType, currentPuzzleId, userName, secondsElapsed, null); await submitRank(currentGameType, currentPuzzleId, userName, secondsElapsed, null);
alert('랭킹이 성공적으로 등록되었습니다!'); alert('랭킹이 성공적으로 등록되었습니다!');

View File

@ -6,14 +6,134 @@
layout:decorate="~{layout/default_layout}" layout:decorate="~{layout/default_layout}"
> >
<th:block layout:fragment="head" id="head"> <th:block layout:fragment="head" id="head">
<script type="text/javascript" th:src="@{/js/admin-sudoku.js}"></script> <style>
<!-- <link th:href="@{/css/puzzle.css}" rel="stylesheet" />-->
<link th:href="@{/css/admin-sudoku.css}" rel="stylesheet" />
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
background-color: #f4f7f9;
padding: 20px;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
text-align: center;
}
#generate-btn {
padding: 12px 24px;
font-size: 16px;
cursor: pointer;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
transition: background-color 0.2s;
}
#generate-btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
#generate-btn:hover:not(:disabled) {
background-color: #0056b3;
}
#status-message {
margin-top: 15px;
font-size: 14px;
color: #555;
height: 20px;
}
#sudoku-board {
display: grid;
grid-template-columns: repeat(9, 40px);
grid-template-rows: repeat(9, 40px);
border: 3px solid #333;
margin: 20px auto 0;
width: 366px; /* (40px * 9) + 6px border */
}
.cell {
width: 40px;
height: 40px;
border: 1px solid #ccc;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
font-weight: bold;
color: #333;
}
/* 3x3 구역을 위한 굵은 선 */
.cell:nth-child(3n) { border-right: 2px solid #333; }
.cell:nth-child(9n) { border-right: none; }
.cell:nth-of-type(n+19):nth-of-type(-n+27),
.cell:nth-of-type(n+46):nth-of-type(-n+54) {
border-bottom: 2px solid #333;
}
</style>
<script th:inline="javascript"> <script th:inline="javascript">
document.addEventListener('DOMContentLoaded', () => {
const generateBtn = document.getElementById('generate-btn');
const boardElement = document.getElementById('sudoku-board');
const statusMessage = document.getElementById('status-message');
// 스도쿠 보드를 화면에 그리는 함수
function renderBoard(puzzleString) {
boardElement.innerHTML = ''; // 기존 보드 초기화
if (!puzzleString || puzzleString.length !== 81) {
statusMessage.textContent = '잘못된 퍼즐 데이터입니다.';
return;
}
for (const char of puzzleString) {
const cell = document.createElement('div');
cell.classList.add('cell');
cell.textContent = char;
boardElement.appendChild(cell);
}
}
// 생성 버튼 클릭 이벤트 리스너
generateBtn.addEventListener('click', async () => {
// 버튼 비활성화 및 상태 메시지 업데이트
generateBtn.disabled = true;
statusMessage.textContent = '새로운 스도쿠 퍼즐을 생성 중입니다... 🧠';
boardElement.innerHTML = ''; // 보드 비우기
try {
// 백엔드 API 호출 (POST 요청)
const response = await fetch('/puzzle/sudoku/sudoku_gen');
if (!response.ok) {
throw new Error(`서버 오류: ${response.statusText}`);
}
const result = await response.json();
// 성공 시 보드 렌더링
renderBoard(result.puzzle);
statusMessage.textContent = `✅ 퍼즐 생성 완료! (ID: ${result.puzzleKey})`;
} catch (error) {
console.error('Error generating Sudoku:', error);
statusMessage.textContent = `❌ 생성 실패: ${error.message}`;
} finally {
// 성공/실패 여부와 관계없이 버튼 다시 활성화
generateBtn.disabled = false;
}
});
});
</script> </script>
<script type="text/javascript" th:src="@{/js/user.js}"></script>
</th:block > </th:block >
<th:block layout:fragment="content"> <th:block layout:fragment="content">
<div class="container"> <div class="container">

View File

@ -6,12 +6,183 @@
layout:decorate="~{layout/default_layout}" layout:decorate="~{layout/default_layout}"
> >
<th:block layout:fragment="head" id="head"> <th:block layout:fragment="head" id="head">
<script type="text/javascript" th:src="@{/js/nonogram.js}"></script>
<link th:href="@{/css/nonogram.css}" rel="stylesheet" />
<script th:inline="javascript"> <script th:inline="javascript">
/**
* ==============================================
* upload.js (업로드 페이지 로직)
* (★ 리팩토링: 통합 API 경로 사용)
* ==============================================
*/
let currentPuzzleData = null; // 업로드 성공 시 퍼즐 데이터 저장
// (★ 수정 없음) 업로드 페이지용 성공 애니메이션 함수
function showSuccessAnimation() {
if (!currentPuzzleData) return;
const puzzleContainer = document.getElementById('puzzle-container');
const grayscaleImg = document.getElementById('grayscale-reveal');
const originalImg = document.getElementById('original-reveal');
grayscaleImg.src = currentPuzzleData.grayscaleImage;
originalImg.src = currentPuzzleData.originalImage;
puzzleContainer.style.transition = 'opacity 0.5s';
puzzleContainer.style.opacity = '0';
grayscaleImg.style.opacity = '1';
setTimeout(() => {
grayscaleImg.style.opacity = '0';
originalImg.style.opacity = '1';
}, 2000);
}
// (★ 수정 없음) 업로드 페이지용 퍼즐 미리보기 그리기 함수
function drawPuzzle(puzzleData) {
const container = document.getElementById('puzzle-container');
container.innerHTML = '';
const { solutionGrid, rowClues, colClues } = puzzleData;
const numRows = solutionGrid.length;
const numCols = solutionGrid[0].length;
container.style.gridTemplateColumns = `auto repeat(${numCols}, 1fr)`;
container.style.gridTemplateRows = `auto repeat(${numRows}, 1fr)`;
// 1. 코너
const corner = document.createElement('div');
corner.className = 'grid-cell';
container.appendChild(corner);
// 2. 열 힌트
for (const clues of colClues) {
const clueCell = document.createElement('div');
clueCell.className = 'clue-cell';
clueCell.innerHTML = clues.join('<br>');
container.appendChild(clueCell);
}
// 3. 행 힌트 및 정답 그리드
for (let i = 0; i < numRows; i++) {
const rowClueCell = document.createElement('div');
rowClueCell.className = 'clue-cell';
rowClueCell.textContent = rowClues[i].join(' ');
container.appendChild(rowClueCell);
for (let j = 0; j < numCols; j++) {
const cell = document.createElement('div');
cell.className = 'solution-cell';
if (solutionGrid[i][j] === 1) {
cell.classList.add('filled');
} else {
cell.classList.add('empty');
}
container.appendChild(cell);
}
}
}
// upload.js의 DOMContentLoaded 리스너
document.addEventListener('DOMContentLoaded', () => {
const createBtn = document.getElementById('createBtn');
// createBtn이 없는 nonogram.html(게임 페이지)에서는 이 리스너가 아무것도 실행하지 않음.
if (!createBtn) {
return;
}
// (업로드 페이지 전용 로직)
createBtn.addEventListener('click', async () => {
const uploader = document.getElementById('imageUploader');
const statusDiv = document.getElementById('status');
const puzzleContainer = document.getElementById('puzzle-container');
const testSuccessBtn = document.getElementById('test-success-btn');
const deleteBtn = document.getElementById('delete-btn');
const playBtn = document.getElementById('play-btn');
if (uploader.files.length === 0) {
statusDiv.textContent = '이미지 파일을 선택해주세요.';
return;
}
const imageFile = uploader.files[0];
const formData = new FormData();
formData.append('imageFile', imageFile);
statusDiv.textContent = '문제를 생성하는 중...';
puzzleContainer.innerHTML = '';
try {
// (★ 수정) API 경로 변경 -> 통합 컨트롤러의 /puzzle/upload.bjx 호출
const response = await fetch('/puzzle/upload.bjx', {
method: 'POST',
body: formData,
});
if (response.ok) {
const puzzleData = await response.json();
statusDiv.textContent = '문제 생성 성공!';
drawPuzzle(puzzleData); // 미리보기 그리기
currentPuzzleData = puzzleData;
testSuccessBtn.addEventListener('click', showSuccessAnimation);
testSuccessBtn.style.display = 'inline-block';
deleteBtn.style.display = 'inline-block';
playBtn.style.display = 'inline-block';
} else {
const errorMessage = await response.text();
statusDiv.textContent = `생성 실패: ${errorMessage}`;
}
} catch (error) {
console.error('네트워크 오류:', error);
statusDiv.textContent = '서버와 통신 중 오류가 발생했습니다.';
}
deleteBtn.addEventListener('click', async () => {
if (!currentPuzzleData || !currentPuzzleData.id) {
alert('삭제할 퍼즐이 선택되지 않았습니다.');
return;
}
if (!confirm('정말로 이 퍼즐을 삭제하시겠습니까?')) {
return;
}
try {
// (★ 수정) API 경로 변경 -> 통합 컨트롤러의 /puzzle/{id}.bjx 호출
const response = await fetch(`/puzzle/${currentPuzzleData.id}.bjx`, {
method: 'DELETE',
});
if (response.ok) {
statusDiv.textContent = '퍼즐이 성공적으로 삭제되었습니다.';
puzzleContainer.innerHTML = '';
// (버그 수정) success-animation-container 내부의 img src를 초기화해야 함
document.getElementById('grayscale-reveal').src = "";
document.getElementById('original-reveal').src = "";
testSuccessBtn.style.display = 'none';
deleteBtn.style.display = 'none';
playBtn.style.display = 'none';
currentPuzzleData = null;
} else {
statusDiv.textContent = `삭제 실패: 서버 오류 (${response.status})`;
}
} catch (error) {
console.error('삭제 중 네트워크 오류:', error);
statusDiv.textContent = '삭제 중 오류가 발생했습니다.';
}
});
playBtn.addEventListener('click', () => {
if (currentPuzzleData && currentPuzzleData.id) {
// (★ 수정 없음) 이 경로는 PuzzleController의 페이지 서빙 경로와 일치하므로 올바름.
window.location.href = `/puzzle/play/${currentPuzzleData.id}`;
}
});
});
});
</script> </script>
<script type="text/javascript" th:src="@{/js/user.js}"></script>
</th:block > </th:block >
<th:block layout:fragment="content"> <th:block layout:fragment="content">
<div id="puzzle-container"> <div id="puzzle-container">

View File

@ -12,7 +12,6 @@
onclickJoin([[${enc}]],[[${keyword}]]) onclickJoin([[${enc}]],[[${keyword}]])
} }
</script> </script>
<script type="text/javascript" th:src="@{/js/user.js}"></script>
</th:block > </th:block >
<th:block layout:fragment="content"> <th:block layout:fragment="content">
<table id="main_layer"> <table id="main_layer">

View File

@ -8,7 +8,6 @@
<head> <head>
<title>Spring Boot</title> <title>Spring Boot</title>
<script type="text/javascript" th:src="@{/js/user.js}"></script>
</head>> </head>>
<body onload="checkDebug()"> <body onload="checkDebug()">
<th:block layout:fragment="header" th:include="@{fragments/header}"></th:block> <th:block layout:fragment="header" th:include="@{fragments/header}"></th:block>

View File

@ -53,7 +53,6 @@
readCount: [[${srcPost?.readCount ?: 0}]], readCount: [[${srcPost?.readCount ?: 0}]],
voteCount: [[${srcPost?.voteCount ?: 0}]], voteCount: [[${srcPost?.voteCount ?: 0}]],
unlikeCount: [[${srcPost?.unlikeCount ?: 0}]], unlikeCount: [[${srcPost?.unlikeCount ?: 0}]],
// --- Page-specific (모델 데이터 아님) --- // --- Page-specific (모델 데이터 아님) ---
enc: /*[[${enc ?: ''}]]*/, enc: /*[[${enc ?: ''}]]*/,
keyword: /*[[${keyword ?: ''}]]*/ keyword: /*[[${keyword ?: ''}]]*/

View File

@ -59,7 +59,6 @@
<script th:src="@{/js/jquery.min.js}"></script> <script th:src="@{/js/jquery.min.js}"></script>
<script th:src="@{/js/common.js}"></script> <script th:src="@{/js/common.js}"></script>
<script th:src="@{/js/user}"></script>
<script th:src="@{/js/jquery.dropotron.min.js}"></script> <script th:src="@{/js/jquery.dropotron.min.js}"></script>
<script th:src="@{/js/browser.min.js}"></script> <script th:src="@{/js/browser.min.js}"></script>
<script th:src="@{/js/breakpoints.min.js}"></script> <script th:src="@{/js/breakpoints.min.js}"></script>