diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/configs/SecurityConfig.kt b/src/main/kotlin/kr/lunaticbum/back/lun/configs/SecurityConfig.kt index 0002ae0..0c39d50 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/configs/SecurityConfig.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/configs/SecurityConfig.kt @@ -89,7 +89,7 @@ class SecurityConfig( "/spider/new**", "/rank/**","/sudoku/**","/spider/**", "/puzzle/play","/puzzle/2048","/puzzle/play/**","/puzzle/sudoku","/puzzle/spider", - "/css/**", "/js/**", "/images/**", "/webjars/**", "/assets/**").permitAll() + "/webfonts/**", "/css/**", "/js/**", "/images/**", "/webjars/**", "/assets/**").permitAll() .anyRequest().authenticated() }.formLogin { form -> form.loginPage("/user/login.bs") diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/BlogController.kt b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/BlogController.kt index 24a6670..3c148ed 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/BlogController.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/BlogController.kt @@ -42,7 +42,7 @@ import java.util.* @RestController @RequestMapping("/blog") -class BlogController() { +class BlogController(private val commentService : CommentService) { companion object { val TEMPTOKEN = "TEMP_TOKEN_VIBUM" } @@ -58,20 +58,20 @@ class BlogController() { @Autowired lateinit var logService: LogService val WRITE_PERMISSION_KEY = "PERMISSION" - @GetMapping("write/{token}","write.bs") - fun writ(@PathVariable token : String? ) : ResultMV{ - val vm = ResultMV("content/blog/write") - if (token.equals(TEMPTOKEN)) { - vm.modelMap.put(WRITE_PERMISSION_KEY,"OK") - vm.modelMap.put(EncTypeKey, EncType11) - vm.modelMap.put(ApiKeyWordKey,"WRITE") - vm.modelMap.put("title","회원이 들어는 구나~!!") - vm.modelMap.put("defaultTitle","무제(無題) (Untitled, ${SimpleDateFormat("yyyy-MM-dd HH:mm").format(Date())})") - } else { - vm.modelMap.put(WRITE_PERMISSION_KEY,"NO") - } - return vm - } +// @GetMapping("write/{token}","write.bs") +// fun writ(@PathVariable token : String? ) : ResultMV{ +// val vm = ResultMV("content/blog/write") +// if (token.equals(TEMPTOKEN)) { +// vm.modelMap.put(WRITE_PERMISSION_KEY,"OK") +// vm.modelMap.put(EncTypeKey, EncType11) +// vm.modelMap.put(ApiKeyWordKey,"WRITE") +// vm.modelMap.put("title","회원이 들어는 구나~!!") +// vm.modelMap.put("defaultTitle","무제(無題) (Untitled, ${SimpleDateFormat("yyyy-MM-dd HH:mm").format(Date())})") +// } else { +// vm.modelMap.put(WRITE_PERMISSION_KEY,"NO") +// } +// return vm +// } @PostMapping("post.bjx") fun post(httpServletRequest: HttpServletRequest, @RequestBody jsonString: String) : ResponseEntity { @@ -234,18 +234,49 @@ class BlogController() { return vm } - @GetMapping("editor/{postId}") - fun editor(@PathVariable postId : String) : ResultMV{ +// @GetMapping("editor/{postId}") +// fun editor(@PathVariable postId : String) : ResultMV{ +// val vm = ResultMV("content/blog/editor") +// postManager.getPost(postId).block().apply { +// this?.title = URLDecoder.decode(this?.title) +// this?.content = URLDecoder.decode(this?.content) +// vm.modelMap.put("srcPost",this) +// } +// return vm +// } + + @GetMapping(value = ["/edit", "/edit/{postId}"]) + fun editPost(@PathVariable(required = false) postId: String?): ResultMV { + // 뷰는 'editor' 하나만 사용합니다. val vm = ResultMV("content/blog/editor") - postManager.getPost(postId).block().apply { - this?.title = URLDecoder.decode(this?.title) - this?.content = URLDecoder.decode(this?.content) - vm.modelMap.put("srcPost",this) + + if (postId == null) { + // 새 글 작성 (postId가 없는 경우) + // 새 Post 객체를 모델에 추가하여 th:object에서 오류가 나지 않도록 합니다. + val newPost = Post().apply { + title = "무제(無題) (${SimpleDateFormat("yyyy-MM-dd HH:mm").format(Date())})" + content = "" // 내용은 비워둡니다. + } + vm.modelMap["srcPost"] = newPost + vm.modelMap["pageTitle"] = "새 글 작성" // 페이지 제목을 동적으로 설정 + } else { + // 기존 글 수정 (postId가 있는 경우) + postManager.getPost(postId).block()?.apply { + this.title = URLDecoder.decode(this.title) + this.content = URLDecoder.decode(this.content) + vm.modelMap["srcPost"] = this + vm.modelMap["pageTitle"] = "글 수정" // 페이지 제목을 동적으로 설정 + } } + + // 글쓰기 권한 및 기타 필요한 데이터를 모델에 추가합니다. + vm.modelMap.put(WRITE_PERMISSION_KEY,"OK") + vm.modelMap.put(EncTypeKey, EncType11) + vm.modelMap.put(ApiKeyWordKey,"WRITE") + return vm } - @GetMapping("posts") fun posts(pageable: Pageable) : ResultMV{ val vm = ResultMV("content/blog/posts") @@ -338,6 +369,31 @@ class BlogController() { return vm } + @GetMapping("categories.bjx") + fun getCategories(): Mono> { + val resultCode = 0 + val resultMsg = "Success" + // Replace with your actual database query for categories + val categories = listOf("Technology", "Travel", "Food", "Lifestyle") + return Mono.just(ResponseEntity.ok().body(TagResult().apply { + this.resultCode = resultCode + this.resultMsg = resultMsg + this.tags = categories + })) + } + + @GetMapping("hashtags.bjx") + fun getHashtags(): Mono> { + val resultCode = 0 + val resultMsg = "Success" + // Replace with your actual database query for hashtags + val hashtags = listOf("kotlin", "spring", "travelgram", "foodie", "blogging") + return Mono.just(ResponseEntity.ok().body(TagResult().apply { + this.resultCode = resultCode + this.resultMsg = resultMsg + this.tags = hashtags + })) + } @Value("\${image.upload.path}") private val uploadPath: String? = null @@ -424,5 +480,86 @@ class BlogController() { this.thumbnailName = "${uuid}_thumbnail.$extension" }) } +// In BlogController.kt +// Add these new functions to your BlogController class + @GetMapping("posts/{postId}/comments.bjx") + fun getCommentsForPost(@PathVariable postId: String): Mono> { + val resultCode = 0 + val resultMsg = "Success" + return commentService.getCommentsForPost(postId) + .collectList() + .map { commentsList -> + ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(CommentsResult().apply { + this.resultCode = resultCode + this.resultMsg = resultMsg + this.comments = commentsList + }) + } + .onErrorResume { + Mono.just(ResponseEntity.status(500).body(CommentsResult().apply { + this.resultCode = 500 + this.resultMsg = "Error fetching comments" + this.comments = emptyList() + })) + } + } + + @PostMapping("posts/{postId}/comments.bjx") + fun addComment(@PathVariable postId: String, @RequestBody jsonString: String): Mono> { + try { + val decodedBytes: ByteArray = Base64.getDecoder().decode(jsonString) + val requestData = String(decodedBytes) + val comment = Gson().fromJson(requestData, Comment::class.java).apply { + this.postId = postId + this.writeTime = System.currentTimeMillis() + } + + return commentService.addComment(comment) + .map { + ResponseEntity.ok().body(ResponceResult().apply { + resultCode = 0 + resultMsg = "Comment submitted successfully" + }) + } + .onErrorResume { e -> + Mono.just(ResponseEntity.status(500).body(ResponceResult().apply { + resultCode = 500 + resultMsg = "Error submitting comment: ${e.message}" + })) + } + } catch (e: Exception) { + return Mono.just(ResponseEntity.status(400).body(ResponceResult().apply { + resultCode = 400 + resultMsg = "Invalid request data" + })) + } + } + + @GetMapping("comments/{commentId}/replies.bjx") + fun getRepliesForComment(@PathVariable commentId: String): Mono> { + val resultCode = 0 + val resultMsg = "Success" + + return commentService.getRepliesForComment(commentId) + .collectList() + .map { repliesList -> + ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(CommentsResult().apply { + this.resultCode = resultCode + this.resultMsg = resultMsg + this.comments = repliesList + }) + } + .onErrorResume { + Mono.just(ResponseEntity.status(500).body(CommentsResult().apply { + this.resultCode = 500 + this.resultMsg = "Error fetching replies" + this.comments = emptyList() + })) + } + } } \ No newline at end of file diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/SpiderController.kt b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/SpiderController.kt index 68ba14c..5c2c6cf 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/SpiderController.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/SpiderController.kt @@ -33,7 +33,6 @@ class SpiderController(private val spiderService: SpiderService,) { return spiderService.updateGame(game) } - // 랭킹 등록 엔드포인트 @PostMapping("/register") fun registerRank(@RequestBody rank: SpiderRank): Mono> { @@ -55,4 +54,11 @@ class SpiderController(private val spiderService: SpiderService,) { val gameId = request["gameId"] ?: return Mono.error(IllegalArgumentException("Game ID is required.")) return spiderService.dealCardsFromStock(gameId) } + + // 실행 취소 엔드포인트 추가 + @PostMapping("/undo") + fun undo(@RequestBody request: Map): Mono { + val gameId = request["gameId"] ?: return Mono.error(IllegalArgumentException("Game ID is required.")) + return spiderService.undoGame(gameId) + } } \ No newline at end of file diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/model/Post.kt b/src/main/kotlin/kr/lunaticbum/back/lun/model/Post.kt index 5ca69b5..1e490d6 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/model/Post.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/model/Post.kt @@ -74,6 +74,27 @@ class Comment { var writeTime: Long? = null var mentions: List? = null // 언급된 유저 아이디(선택) } + +@Data +@NoArgsConstructor +@AllArgsConstructor +class TagResult { + var resultCode: Int = 0 + var resultMsg: String = "" + var tags: List? = null +} + + +@Data +@NoArgsConstructor +@AllArgsConstructor +class CommentsResult { + var resultCode: Int = 0 + var resultMsg: String = "" + var comments: List? = null +} + + @Repository interface CommentRepository : ReactiveMongoRepository { fun findByPostIdAndParentIdIsNullOrderByWriteTimeAsc(postId: String): Flux // 최상위 댓글 @@ -82,7 +103,9 @@ interface CommentRepository : ReactiveMongoRepository { @Service class CommentService(private val commentRepository: CommentRepository) { - + fun getRepliesForComment(parentId: String): Flux { + return commentRepository.findByParentIdOrderByWriteTimeAsc(parentId) + } fun addComment(comment: Comment): Mono { // 예시: 부모 댓글 존재 여부/권한 검증 등 비즈니스 로직 처리 return commentRepository.save(comment) diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/model/Spider.kt b/src/main/kotlin/kr/lunaticbum/back/lun/model/Spider.kt index 28f56b7..1f02ddc 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/model/Spider.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/model/Spider.kt @@ -1,3 +1,5 @@ +// kr.lunaticbum.back.lun.model.Spider.kt + package kr.lunaticbum.back.lun.model import org.springframework.data.annotation.Id @@ -17,6 +19,8 @@ data class SpiderGame( val foundation: List>, val moves: Int, val isCompleted: Boolean, + val undoCount: Int = 0, // 실행 취소 횟수: 최대 5회 제한 + val undoHistory: List = emptyList(), // 게임 상태 히스토리 저장 val timestamp: Long = System.currentTimeMillis() ) @@ -26,19 +30,34 @@ data class SpiderCard( var isFaceUp: Boolean, ) +// 게임 상태 히스토리를 저장할 데이터 클래스 +data class SpiderGameHistory( + val tableau: List>, + val stock: List, + val foundation: List>, + val moves: Int +) + interface SpiderGameRepository : ReactiveMongoRepository { override fun findById(id: String): Mono } @Service -class SpiderService(private val spiderGameRepository: SpiderGameRepository, - private val spiderRankRepository: SpiderRankRepository ) { - +class SpiderService( + private val spiderGameRepository: SpiderGameRepository, + private val spiderRankRepository: SpiderRankRepository +) { + // 덱 생성, 카드 분배 등 기존 로직은 그대로 유지 private fun createDeck(numSuits: Int): List { val allSuits = listOf("spade", "heart", "club", "diamond") val suits = allSuits.take(numSuits) + val setsPerSuit = when (numSuits) { + 1 -> 8 // 13 ranks * 1 suit * 8 sets = 104 cards + 2 -> 4 // 13 ranks * 2 suits * 4 sets = 104 cards + 4 -> 2 // 13 ranks * 4 suits * 2 sets = 104 cards + else -> throw IllegalArgumentException("Invalid number of suits: $numSuits") + } val deck = mutableListOf() - val setsPerSuit = 8 / numSuits repeat(setsPerSuit) { for (suit in suits) { for (rank in 1..13) { @@ -51,24 +70,31 @@ class SpiderService(private val spiderGameRepository: SpiderGameRepository, private fun dealCards(shuffledCards: List, numCards: String): Pair>, List> { val initialCards = numCards.split(",").map { it.trim().toInt() } - val cardsPerStack = listOf( - initialCards[0], initialCards[0], initialCards[0], initialCards[0], - initialCards[1], initialCards[1], initialCards[1], initialCards[1], initialCards[1], initialCards[1] - ) - - val cards = shuffledCards.toMutableList() - val tableau = mutableListOf>() - - cardsPerStack.forEach { num -> - val stack = mutableListOf() - repeat(num) { - val card = cards.removeFirst() - card.isFaceUp = (it == num - 1) - stack.add(card) - } - tableau.add(stack) + val cardsPerStack = List(10) { index -> + if (index < 4) initialCards[0] else initialCards[1] } - return Pair(tableau, cards) + + val cardsToDeal = shuffledCards.toMutableList() + val tableau = MutableList(10) { mutableListOf() } + + // 각 스택에 초기 카드를 분배합니다. + cardsPerStack.forEachIndexed { stackIndex, count -> + repeat(count) { + if (cardsToDeal.isNotEmpty()) { + val card = cardsToDeal.removeFirst() + tableau[stackIndex].add(card) + } + } + } + + // 각 스택의 맨 위 카드를 앞면으로 설정합니다. + tableau.forEach { stack -> + if (stack.isNotEmpty()) { + stack.last().isFaceUp = true + } + } + + return Pair(tableau, cardsToDeal) } fun newGame(numSuits: Int, numCards: String): Mono { @@ -82,7 +108,9 @@ class SpiderService(private val spiderGameRepository: SpiderGameRepository, stock = stock, foundation = emptyList(), moves = 0, - isCompleted = false + isCompleted = false, + undoCount = 0, + undoHistory = emptyList() ) return spiderGameRepository.save(initialGame) } @@ -91,29 +119,49 @@ class SpiderService(private val spiderGameRepository: SpiderGameRepository, return spiderGameRepository.findById(id) } + // updateGame 메서드: 게임 상태를 업데이트하기 전에 히스토리를 저장합니다. fun updateGame(game: SpiderGame): Mono { - return spiderGameRepository.save(game) + val historyToSave = SpiderGameHistory( + tableau = game.tableau, + stock = game.stock, + foundation = game.foundation, + moves = game.moves + ) + // 기존 히스토리에 현재 상태를 추가하고, 최대 5개까지만 유지합니다. + val updatedHistory = (game.undoHistory + historyToSave).takeLast(5) + val updatedGame = game.copy(undoHistory = updatedHistory) + return spiderGameRepository.save(updatedGame) } + // dealCardsFromStock 메서드: 카드 분배 전에 히스토리를 저장합니다. fun dealCardsFromStock(gameId: String): Mono { return spiderGameRepository.findById(gameId) .flatMap { game -> val stockCards = game.stock.toMutableList() if (stockCards.size >= 10) { - val cardsToDeal = stockCards.take(10) + // 현재 게임 상태를 히스토리에 저장 + val historyToSave = SpiderGameHistory( + tableau = game.tableau, + stock = game.stock, + foundation = game.foundation, + moves = game.moves + ) + val updatedHistory = (game.undoHistory + historyToSave).takeLast(5) + val updatedTableau = game.tableau.toMutableList() val remainingStock = stockCards.drop(10) updatedTableau.forEachIndexed { index, stack -> - val cardToDeal = cardsToDeal[index] - cardToDeal.isFaceUp = true + val cardToDeal = stockCards[index] + cardToDeal.isFaceUp = true // 카드를 추가할 때 앞면으로 뒤집기 (stack as MutableList).add(cardToDeal) } val updatedGame = game.copy( tableau = updatedTableau, stock = remainingStock, - moves = game.moves + 1 + moves = game.moves + 1, + undoHistory = updatedHistory // 히스토리 업데이트 ) spiderGameRepository.save(updatedGame) } else { @@ -122,12 +170,34 @@ class SpiderService(private val spiderGameRepository: SpiderGameRepository, } } - // 🔴 추가: 랭킹 등록 함수 + // undoGame 메서드: 히스토리에서 마지막 상태를 불러와 게임을 되돌립니다. + fun undoGame(gameId: String): Mono { + return spiderGameRepository.findById(gameId) + .flatMap { game -> + if (game.undoHistory.isNotEmpty() && game.undoCount < 5) { + val lastHistory = game.undoHistory.last() + val remainingHistory = game.undoHistory.dropLast(1) + + val updatedGame = game.copy( + tableau = lastHistory.tableau, + stock = lastHistory.stock, + foundation = lastHistory.foundation, + moves = lastHistory.moves, + undoCount = game.undoCount + 1, // 실행 취소 횟수 증가 + undoHistory = remainingHistory // 마지막 히스토리는 제거 + ) + spiderGameRepository.save(updatedGame) + } else { + Mono.error(IllegalArgumentException("Cannot undo. No more history or undo limit reached.")) + } + } + } + + // 랭킹 관련 기존 메서드들은 그대로 유지 fun registerRank(rank: SpiderRank): Mono { return spiderRankRepository.save(rank) } - // 🔴 추가: 랭킹 조회 함수 fun getRanksByGameId(gameId: String): Flux { return spiderRankRepository.findByGameIdOrderByMovesAscCompletionTimeAsc(gameId) } diff --git a/src/main/resources/static/assets/js/util.js b/src/main/resources/static/assets/js/util.js deleted file mode 100644 index bdb8e9f..0000000 --- a/src/main/resources/static/assets/js/util.js +++ /dev/null @@ -1,587 +0,0 @@ -(function($) { - - /** - * Generate an indented list of links from a nav. Meant for use with panel(). - * @return {jQuery} jQuery object. - */ - $.fn.navList = function() { - - var $this = $(this); - $a = $this.find('a'), - b = []; - - $a.each(function() { - - var $this = $(this), - indent = Math.max(0, $this.parents('li').length - 1), - href = $this.attr('href'), - target = $this.attr('target'); - - b.push( - '' + - '' + - $this.text() + - '' - ); - - }); - - return b.join(''); - - }; - - /** - * Panel-ify an element. - * @param {object} userConfig User config. - * @return {jQuery} jQuery object. - */ - $.fn.panel = function(userConfig) { - - // No elements? - if (this.length == 0) - return $this; - - // Multiple elements? - if (this.length > 1) { - - for (var i=0; i < this.length; i++) - $(this[i]).panel(userConfig); - - return $this; - - } - - // Vars. - var $this = $(this), - $body = $('body'), - $window = $(window), - id = $this.attr('id'), - config; - - // Config. - config = $.extend({ - - // Delay. - delay: 0, - - // Hide panel on link click. - hideOnClick: false, - - // Hide panel on escape keypress. - hideOnEscape: false, - - // Hide panel on swipe. - hideOnSwipe: false, - - // Reset scroll position on hide. - resetScroll: false, - - // Reset forms on hide. - resetForms: false, - - // Side of viewport the panel will appear. - side: null, - - // Target element for "class". - target: $this, - - // Class to toggle. - visibleClass: 'visible' - - }, userConfig); - - // Expand "target" if it's not a jQuery object already. - if (typeof config.target != 'jQuery') - config.target = $(config.target); - - // Panel. - - // Methods. - $this._hide = function(event) { - - // Already hidden? Bail. - if (!config.target.hasClass(config.visibleClass)) - return; - - // If an event was provided, cancel it. - if (event) { - - event.preventDefault(); - event.stopPropagation(); - - } - - // Hide. - config.target.removeClass(config.visibleClass); - - // Post-hide stuff. - window.setTimeout(function() { - - // Reset scroll position. - if (config.resetScroll) - $this.scrollTop(0); - - // Reset forms. - if (config.resetForms) - $this.find('form').each(function() { - this.reset(); - }); - - }, config.delay); - - }; - - // Vendor fixes. - $this - .css('-ms-overflow-style', '-ms-autohiding-scrollbar') - .css('-webkit-overflow-scrolling', 'touch'); - - // Hide on click. - if (config.hideOnClick) { - - $this.find('a') - .css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)'); - - $this - .on('click', 'a', function(event) { - - var $a = $(this), - href = $a.attr('href'), - target = $a.attr('target'); - - if (!href || href == '#' || href == '' || href == '#' + id) - return; - - // Cancel original event. - event.preventDefault(); - event.stopPropagation(); - - // Hide panel. - $this._hide(); - - // Redirect to href. - window.setTimeout(function() { - - if (target == '_blank') - window.open(href); - else - window.location.href = href; - - }, config.delay + 10); - - }); - - } - - // Event: Touch stuff. - $this.on('touchstart', function(event) { - - $this.touchPosX = event.originalEvent.touches[0].pageX; - $this.touchPosY = event.originalEvent.touches[0].pageY; - - }) - - $this.on('touchmove', function(event) { - - if ($this.touchPosX === null - || $this.touchPosY === null) - return; - - var diffX = $this.touchPosX - event.originalEvent.touches[0].pageX, - diffY = $this.touchPosY - event.originalEvent.touches[0].pageY, - th = $this.outerHeight(), - ts = ($this.get(0).scrollHeight - $this.scrollTop()); - - // Hide on swipe? - if (config.hideOnSwipe) { - - var result = false, - boundary = 20, - delta = 50; - - switch (config.side) { - - case 'left': - result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta); - break; - - case 'right': - result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta)); - break; - - case 'top': - result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY > delta); - break; - - case 'bottom': - result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY < (-1 * delta)); - break; - - default: - break; - - } - - if (result) { - - $this.touchPosX = null; - $this.touchPosY = null; - $this._hide(); - - return false; - - } - - } - - // Prevent vertical scrolling past the top or bottom. - if (($this.scrollTop() < 0 && diffY < 0) - || (ts > (th - 2) && ts < (th + 2) && diffY > 0)) { - - event.preventDefault(); - event.stopPropagation(); - - } - - }); - - // Event: Prevent certain events inside the panel from bubbling. - $this.on('click touchend touchstart touchmove', function(event) { - event.stopPropagation(); - }); - - // Event: Hide panel if a child anchor tag pointing to its ID is clicked. - $this.on('click', 'a[href="#' + id + '"]', function(event) { - - event.preventDefault(); - event.stopPropagation(); - - config.target.removeClass(config.visibleClass); - - }); - - // Body. - - // Event: Hide panel on body click/tap. - $body.on('click touchend', function(event) { - $this._hide(event); - }); - - // Event: Toggle. - $body.on('click', 'a[href="#' + id + '"]', function(event) { - - event.preventDefault(); - event.stopPropagation(); - - config.target.toggleClass(config.visibleClass); - - }); - - // Window. - - // Event: Hide on ESC. - if (config.hideOnEscape) - $window.on('keydown', function(event) { - - if (event.keyCode == 27) - $this._hide(event); - - }); - - return $this; - - }; - - /** - * Apply "placeholder" attribute polyfill to one or more forms. - * @return {jQuery} jQuery object. - */ - $.fn.placeholder = function() { - - // Browser natively supports placeholders? Bail. - if (typeof (document.createElement('input')).placeholder != 'undefined') - return $(this); - - // No elements? - if (this.length == 0) - return $this; - - // Multiple elements? - if (this.length > 1) { - - for (var i=0; i < this.length; i++) - $(this[i]).placeholder(); - - return $this; - - } - - // Vars. - var $this = $(this); - - // Text, TextArea. - $this.find('input[type=text],textarea') - .each(function() { - - var i = $(this); - - if (i.val() == '' - || i.val() == i.attr('placeholder')) - i - .addClass('polyfill-placeholder') - .val(i.attr('placeholder')); - - }) - .on('blur', function() { - - var i = $(this); - - if (i.attr('name').match(/-polyfill-field$/)) - return; - - if (i.val() == '') - i - .addClass('polyfill-placeholder') - .val(i.attr('placeholder')); - - }) - .on('focus', function() { - - var i = $(this); - - if (i.attr('name').match(/-polyfill-field$/)) - return; - - if (i.val() == i.attr('placeholder')) - i - .removeClass('polyfill-placeholder') - .val(''); - - }); - - // Password. - $this.find('input[type=password]') - .each(function() { - - var i = $(this); - var x = $( - $('
') - .append(i.clone()) - .remove() - .html() - .replace(/type="password"/i, 'type="text"') - .replace(/type=password/i, 'type=text') - ); - - if (i.attr('id') != '') - x.attr('id', i.attr('id') + '-polyfill-field'); - - if (i.attr('name') != '') - x.attr('name', i.attr('name') + '-polyfill-field'); - - x.addClass('polyfill-placeholder') - .val(x.attr('placeholder')).insertAfter(i); - - if (i.val() == '') - i.hide(); - else - x.hide(); - - i - .on('blur', function(event) { - - event.preventDefault(); - - var x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]'); - - if (i.val() == '') { - - i.hide(); - x.show(); - - } - - }); - - x - .on('focus', function(event) { - - event.preventDefault(); - - var i = x.parent().find('input[name=' + x.attr('name').replace('-polyfill-field', '') + ']'); - - x.hide(); - - i - .show() - .focus(); - - }) - .on('keypress', function(event) { - - event.preventDefault(); - x.val(''); - - }); - - }); - - // Events. - $this - .on('submit', function() { - - $this.find('input[type=text],input[type=password],textarea') - .each(function(event) { - - var i = $(this); - - if (i.attr('name').match(/-polyfill-field$/)) - i.attr('name', ''); - - if (i.val() == i.attr('placeholder')) { - - i.removeClass('polyfill-placeholder'); - i.val(''); - - } - - }); - - }) - .on('reset', function(event) { - - event.preventDefault(); - - $this.find('select') - .val($('option:first').val()); - - $this.find('input,textarea') - .each(function() { - - var i = $(this), - x; - - i.removeClass('polyfill-placeholder'); - - switch (this.type) { - - case 'submit': - case 'reset': - break; - - case 'password': - i.val(i.attr('defaultValue')); - - x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]'); - - if (i.val() == '') { - i.hide(); - x.show(); - } - else { - i.show(); - x.hide(); - } - - break; - - case 'checkbox': - case 'radio': - i.attr('checked', i.attr('defaultValue')); - break; - - case 'text': - case 'textarea': - i.val(i.attr('defaultValue')); - - if (i.val() == '') { - i.addClass('polyfill-placeholder'); - i.val(i.attr('placeholder')); - } - - break; - - default: - i.val(i.attr('defaultValue')); - break; - - } - }); - - }); - - return $this; - - }; - - /** - * Moves elements to/from the first positions of their respective parents. - * @param {jQuery} $elements Elements (or selector) to move. - * @param {bool} condition If true, moves elements to the top. Otherwise, moves elements back to their original locations. - */ - $.prioritize = function($elements, condition) { - - var key = '__prioritize'; - - // Expand $elements if it's not already a jQuery object. - if (typeof $elements != 'jQuery') - $elements = $($elements); - - // Step through elements. - $elements.each(function() { - - var $e = $(this), $p, - $parent = $e.parent(); - - // No parent? Bail. - if ($parent.length == 0) - return; - - // Not moved? Move it. - if (!$e.data(key)) { - - // Condition is false? Bail. - if (!condition) - return; - - // Get placeholder (which will serve as our point of reference for when this element needs to move back). - $p = $e.prev(); - - // Couldn't find anything? Means this element's already at the top, so bail. - if ($p.length == 0) - return; - - // Move element to top of parent. - $e.prependTo($parent); - - // Mark element as moved. - $e.data(key, $p); - - } - - // Moved already? - else { - - // Condition is true? Bail. - if (condition) - return; - - $p = $e.data(key); - - // Move element back to its original location (using our placeholder). - $e.insertAfter($p); - - // Unmark element as moved. - $e.removeData(key); - - } - - }); - - }; - -})(jQuery); \ No newline at end of file diff --git a/src/main/resources/static/assets/sass/libs/_breakpoints.scss b/src/main/resources/static/assets/sass/libs/_breakpoints.scss deleted file mode 100644 index c5301d8..0000000 --- a/src/main/resources/static/assets/sass/libs/_breakpoints.scss +++ /dev/null @@ -1,223 +0,0 @@ -// breakpoints.scss v1.0 | @ajlkn | MIT licensed */ - -// Vars. - - /// Breakpoints. - /// @var {list} - $breakpoints: () !global; - -// Mixins. - - /// Sets breakpoints. - /// @param {map} $x Breakpoints. - @mixin breakpoints($x: ()) { - $breakpoints: $x !global; - } - - /// Wraps @content in a @media block targeting a specific orientation. - /// @param {string} $orientation Orientation. - @mixin orientation($orientation) { - @media screen and (orientation: #{$orientation}) { - @content; - } - } - - /// Wraps @content in a @media block using a given query. - /// @param {string} $query Query. - @mixin breakpoint($query: null) { - - $breakpoint: null; - $op: null; - $media: null; - - // Determine operator, breakpoint. - - // Greater than or equal. - @if (str-slice($query, 0, 2) == '>=') { - - $op: 'gte'; - $breakpoint: str-slice($query, 3); - - } - - // Less than or equal. - @elseif (str-slice($query, 0, 2) == '<=') { - - $op: 'lte'; - $breakpoint: str-slice($query, 3); - - } - - // Greater than. - @elseif (str-slice($query, 0, 1) == '>') { - - $op: 'gt'; - $breakpoint: str-slice($query, 2); - - } - - // Less than. - @elseif (str-slice($query, 0, 1) == '<') { - - $op: 'lt'; - $breakpoint: str-slice($query, 2); - - } - - // Not. - @elseif (str-slice($query, 0, 1) == '!') { - - $op: 'not'; - $breakpoint: str-slice($query, 2); - - } - - // Equal. - @else { - - $op: 'eq'; - $breakpoint: $query; - - } - - // Build media. - @if ($breakpoint and map-has-key($breakpoints, $breakpoint)) { - - $a: map-get($breakpoints, $breakpoint); - - // Range. - @if (type-of($a) == 'list') { - - $x: nth($a, 1); - $y: nth($a, 2); - - // Max only. - @if ($x == null) { - - // Greater than or equal (>= 0 / anything) - @if ($op == 'gte') { - $media: 'screen'; - } - - // Less than or equal (<= y) - @elseif ($op == 'lte') { - $media: 'screen and (max-width: ' + $y + ')'; - } - - // Greater than (> y) - @elseif ($op == 'gt') { - $media: 'screen and (min-width: ' + ($y + 1) + ')'; - } - - // Less than (< 0 / invalid) - @elseif ($op == 'lt') { - $media: 'screen and (max-width: -1px)'; - } - - // Not (> y) - @elseif ($op == 'not') { - $media: 'screen and (min-width: ' + ($y + 1) + ')'; - } - - // Equal (<= y) - @else { - $media: 'screen and (max-width: ' + $y + ')'; - } - - } - - // Min only. - @else if ($y == null) { - - // Greater than or equal (>= x) - @if ($op == 'gte') { - $media: 'screen and (min-width: ' + $x + ')'; - } - - // Less than or equal (<= inf / anything) - @elseif ($op == 'lte') { - $media: 'screen'; - } - - // Greater than (> inf / invalid) - @elseif ($op == 'gt') { - $media: 'screen and (max-width: -1px)'; - } - - // Less than (< x) - @elseif ($op == 'lt') { - $media: 'screen and (max-width: ' + ($x - 1) + ')'; - } - - // Not (< x) - @elseif ($op == 'not') { - $media: 'screen and (max-width: ' + ($x - 1) + ')'; - } - - // Equal (>= x) - @else { - $media: 'screen and (min-width: ' + $x + ')'; - } - - } - - // Min and max. - @else { - - // Greater than or equal (>= x) - @if ($op == 'gte') { - $media: 'screen and (min-width: ' + $x + ')'; - } - - // Less than or equal (<= y) - @elseif ($op == 'lte') { - $media: 'screen and (max-width: ' + $y + ')'; - } - - // Greater than (> y) - @elseif ($op == 'gt') { - $media: 'screen and (min-width: ' + ($y + 1) + ')'; - } - - // Less than (< x) - @elseif ($op == 'lt') { - $media: 'screen and (max-width: ' + ($x - 1) + ')'; - } - - // Not (< x and > y) - @elseif ($op == 'not') { - $media: 'screen and (max-width: ' + ($x - 1) + '), screen and (min-width: ' + ($y + 1) + ')'; - } - - // Equal (>= x and <= y) - @else { - $media: 'screen and (min-width: ' + $x + ') and (max-width: ' + $y + ')'; - } - - } - - } - - // String. - @else { - - // Missing a media type? Prefix with "screen". - @if (str-slice($a, 0, 1) == '(') { - $media: 'screen and ' + $a; - } - - // Otherwise, use as-is. - @else { - $media: $a; - } - - } - - } - - // Output. - @media #{$media} { - @content; - } - - } \ No newline at end of file diff --git a/src/main/resources/static/assets/sass/libs/_functions.scss b/src/main/resources/static/assets/sass/libs/_functions.scss deleted file mode 100644 index f563aab..0000000 --- a/src/main/resources/static/assets/sass/libs/_functions.scss +++ /dev/null @@ -1,90 +0,0 @@ -/// Removes a specific item from a list. -/// @author Hugo Giraudel -/// @param {list} $list List. -/// @param {integer} $index Index. -/// @return {list} Updated list. -@function remove-nth($list, $index) { - - $result: null; - - @if type-of($index) != number { - @warn "$index: #{quote($index)} is not a number for `remove-nth`."; - } - @else if $index == 0 { - @warn "List index 0 must be a non-zero integer for `remove-nth`."; - } - @else if abs($index) > length($list) { - @warn "List index is #{$index} but list is only #{length($list)} item long for `remove-nth`."; - } - @else { - - $result: (); - $index: if($index < 0, length($list) + $index + 1, $index); - - @for $i from 1 through length($list) { - - @if $i != $index { - $result: append($result, nth($list, $i)); - } - - } - - } - - @return $result; - -} - -/// Gets a value from a map. -/// @author Hugo Giraudel -/// @param {map} $map Map. -/// @param {string} $keys Key(s). -/// @return {string} Value. -@function val($map, $keys...) { - - @if nth($keys, 1) == null { - $keys: remove-nth($keys, 1); - } - - @each $key in $keys { - $map: map-get($map, $key); - } - - @return $map; - -} - -/// Gets a duration value. -/// @param {string} $keys Key(s). -/// @return {string} Value. -@function _duration($keys...) { - @return val($duration, $keys...); -} - -/// Gets a font value. -/// @param {string} $keys Key(s). -/// @return {string} Value. -@function _font($keys...) { - @return val($font, $keys...); -} - -/// Gets a misc value. -/// @param {string} $keys Key(s). -/// @return {string} Value. -@function _misc($keys...) { - @return val($misc, $keys...); -} - -/// Gets a palette value. -/// @param {string} $keys Key(s). -/// @return {string} Value. -@function _palette($keys...) { - @return val($palette, $keys...); -} - -/// Gets a size value. -/// @param {string} $keys Key(s). -/// @return {string} Value. -@function _size($keys...) { - @return val($size, $keys...); -} \ No newline at end of file diff --git a/src/main/resources/static/assets/sass/libs/_html-grid.scss b/src/main/resources/static/assets/sass/libs/_html-grid.scss deleted file mode 100644 index 7438a8c..0000000 --- a/src/main/resources/static/assets/sass/libs/_html-grid.scss +++ /dev/null @@ -1,149 +0,0 @@ -// html-grid.scss v1.0 | @ajlkn | MIT licensed */ - -// Mixins. - - /// Initializes the current element as an HTML grid. - /// @param {mixed} $gutters Gutters (either a single number to set both column/row gutters, or a list to set them individually). - /// @param {mixed} $suffix Column class suffix (optional; either a single suffix or a list). - @mixin html-grid($gutters: 1.5em, $suffix: '') { - - // Initialize. - $cols: 12; - $multipliers: 0, 0.25, 0.5, 1, 1.50, 2.00; - $unit: 100% / $cols; - - // Suffixes. - $suffixes: null; - - @if (type-of($suffix) == 'list') { - $suffixes: $suffix; - } - @else { - $suffixes: ($suffix); - } - - // Gutters. - $guttersCols: null; - $guttersRows: null; - - @if (type-of($gutters) == 'list') { - - $guttersCols: nth($gutters, 1); - $guttersRows: nth($gutters, 2); - - } - @else { - - $guttersCols: $gutters; - $guttersRows: 0; - - } - - // Row. - display: flex; - flex-wrap: wrap; - box-sizing: border-box; - align-items: stretch; - - // Columns. - > * { - box-sizing: border-box; - } - - // Gutters. - &.gtr-uniform { - > * { - > :last-child { - margin-bottom: 0; - } - } - } - - // Alignment. - &.aln-left { - justify-content: flex-start; - } - - &.aln-center { - justify-content: center; - } - - &.aln-right { - justify-content: flex-end; - } - - &.aln-top { - align-items: flex-start; - } - - &.aln-middle { - align-items: center; - } - - &.aln-bottom { - align-items: flex-end; - } - - // Step through suffixes. - @each $suffix in $suffixes { - - // Suffix. - @if ($suffix != '') { - $suffix: '-' + $suffix; - } - @else { - $suffix: ''; - } - - // Row. - - // Important. - > .imp#{$suffix} { - order: -1; - } - - // Columns, offsets. - @for $i from 1 through $cols { - > .col-#{$i}#{$suffix} { - width: $unit * $i; - } - - > .off-#{$i}#{$suffix} { - margin-left: $unit * $i; - } - } - - // Step through multipliers. - @each $multiplier in $multipliers { - - // Gutters. - $class: null; - - @if ($multiplier != 1) { - $class: '.gtr-' + ($multiplier * 100); - } - - &#{$class} { - margin-top: ($guttersRows * $multiplier * -1); - margin-left: ($guttersCols * $multiplier * -1); - - > * { - padding: ($guttersRows * $multiplier) 0 0 ($guttersCols * $multiplier); - } - - // Uniform. - &.gtr-uniform { - margin-top: $guttersCols * $multiplier * -1; - - > * { - padding-top: $guttersCols * $multiplier; - } - } - - } - - } - - } - - } \ No newline at end of file diff --git a/src/main/resources/static/assets/sass/libs/_mixins.scss b/src/main/resources/static/assets/sass/libs/_mixins.scss deleted file mode 100644 index a331483..0000000 --- a/src/main/resources/static/assets/sass/libs/_mixins.scss +++ /dev/null @@ -1,78 +0,0 @@ -/// Makes an element's :before pseudoelement a FontAwesome icon. -/// @param {string} $content Optional content value to use. -/// @param {string} $category Optional category to use. -/// @param {string} $where Optional pseudoelement to target (before or after). -@mixin icon($content: false, $category: regular, $where: before) { - - text-decoration: none; - - &:#{$where} { - - @if $content { - content: $content; - } - - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - display: inline-block; - font-style: normal; - font-variant: normal; - text-rendering: auto; - line-height: 1; - text-transform: none !important; - - @if ($category == brands) { - font-family: 'Font Awesome 5 Brands'; - } - @elseif ($category == solid) { - font-family: 'Font Awesome 5 Free'; - font-weight: 900; - } - @else { - font-family: 'Font Awesome 5 Free'; - font-weight: 400; - } - - } - -} - -/// Applies padding to an element, taking the current element-margin value into account. -/// @param {mixed} $tb Top/bottom padding. -/// @param {mixed} $lr Left/right padding. -/// @param {list} $pad Optional extra padding (in the following order top, right, bottom, left) -/// @param {bool} $important If true, adds !important. -@mixin padding($tb, $lr, $pad: (0,0,0,0), $important: null) { - - @if $important { - $important: '!important'; - } - - $x: 0.1em; - - @if unit(_size(element-margin)) == 'rem' { - $x: 0.1rem; - } - - padding: ($tb + nth($pad,1)) ($lr + nth($pad,2)) max($x, $tb - _size(element-margin) + nth($pad,3)) ($lr + nth($pad,4)) #{$important}; - -} - -/// Encodes a SVG data URL so IE doesn't choke (via codepen.io/jakob-e/pen/YXXBrp). -/// @param {string} $svg SVG data URL. -/// @return {string} Encoded SVG data URL. -@function svg-url($svg) { - - $svg: str-replace($svg, '"', '\''); - $svg: str-replace($svg, '%', '%25'); - $svg: str-replace($svg, '<', '%3C'); - $svg: str-replace($svg, '>', '%3E'); - $svg: str-replace($svg, '&', '%26'); - $svg: str-replace($svg, '#', '%23'); - $svg: str-replace($svg, '{', '%7B'); - $svg: str-replace($svg, '}', '%7D'); - $svg: str-replace($svg, ';', '%3B'); - - @return url("data:image/svg+xml;charset=utf8,#{$svg}"); - -} \ No newline at end of file diff --git a/src/main/resources/static/assets/sass/libs/_vars.scss b/src/main/resources/static/assets/sass/libs/_vars.scss deleted file mode 100644 index 5eb4ac6..0000000 --- a/src/main/resources/static/assets/sass/libs/_vars.scss +++ /dev/null @@ -1,33 +0,0 @@ -// Misc. - $misc: ( - z-index-base: 10000 - ); - -// Duration. - $duration: ( - navPanel: 0.5s - ); - -// Size. - $size: ( - navPanel: 275px, - radius: 5px - ); - -// Font. - $font: ( - ); - -// Palette. - $palette: ( - bg: #f7f7f7, - fg: #474747, - fg-bold: #4c4c4c, - fg-light: #999, - border: #e0e0e0, - - accent: ( - bg: #37c0fb, - fg: #fff - ) - ); \ No newline at end of file diff --git a/src/main/resources/static/assets/sass/libs/_vendor.scss b/src/main/resources/static/assets/sass/libs/_vendor.scss deleted file mode 100644 index 6599a3f..0000000 --- a/src/main/resources/static/assets/sass/libs/_vendor.scss +++ /dev/null @@ -1,376 +0,0 @@ -// vendor.scss v1.0 | @ajlkn | MIT licensed */ - -// Vars. - - /// Vendor prefixes. - /// @var {list} - $vendor-prefixes: ( - '-moz-', - '-webkit-', - '-ms-', - '' - ); - - /// Properties that should be vendorized. - /// Data via caniuse.com, github.com/postcss/autoprefixer, and developer.mozilla.org - /// @var {list} - $vendor-properties: ( - - // Animation. - 'animation', - 'animation-delay', - 'animation-direction', - 'animation-duration', - 'animation-fill-mode', - 'animation-iteration-count', - 'animation-name', - 'animation-play-state', - 'animation-timing-function', - - // Appearance. - 'appearance', - - // Backdrop filter. - 'backdrop-filter', - - // Background image options. - 'background-clip', - 'background-origin', - 'background-size', - - // Box sizing. - 'box-sizing', - - // Clip path. - 'clip-path', - - // Filter effects. - 'filter', - - // Flexbox. - 'align-content', - 'align-items', - 'align-self', - 'flex', - 'flex-basis', - 'flex-direction', - 'flex-flow', - 'flex-grow', - 'flex-shrink', - 'flex-wrap', - 'justify-content', - 'order', - - // Font feature. - 'font-feature-settings', - 'font-language-override', - 'font-variant-ligatures', - - // Font kerning. - 'font-kerning', - - // Fragmented borders and backgrounds. - 'box-decoration-break', - - // Grid layout. - 'grid-column', - 'grid-column-align', - 'grid-column-end', - 'grid-column-start', - 'grid-row', - 'grid-row-align', - 'grid-row-end', - 'grid-row-start', - 'grid-template-columns', - 'grid-template-rows', - - // Hyphens. - 'hyphens', - 'word-break', - - // Masks. - 'mask', - 'mask-border', - 'mask-border-outset', - 'mask-border-repeat', - 'mask-border-slice', - 'mask-border-source', - 'mask-border-width', - 'mask-clip', - 'mask-composite', - 'mask-image', - 'mask-origin', - 'mask-position', - 'mask-repeat', - 'mask-size', - - // Multicolumn. - 'break-after', - 'break-before', - 'break-inside', - 'column-count', - 'column-fill', - 'column-gap', - 'column-rule', - 'column-rule-color', - 'column-rule-style', - 'column-rule-width', - 'column-span', - 'column-width', - 'columns', - - // Object fit. - 'object-fit', - 'object-position', - - // Regions. - 'flow-from', - 'flow-into', - 'region-fragment', - - // Scroll snap points. - 'scroll-snap-coordinate', - 'scroll-snap-destination', - 'scroll-snap-points-x', - 'scroll-snap-points-y', - 'scroll-snap-type', - - // Shapes. - 'shape-image-threshold', - 'shape-margin', - 'shape-outside', - - // Tab size. - 'tab-size', - - // Text align last. - 'text-align-last', - - // Text decoration. - 'text-decoration-color', - 'text-decoration-line', - 'text-decoration-skip', - 'text-decoration-style', - - // Text emphasis. - 'text-emphasis', - 'text-emphasis-color', - 'text-emphasis-position', - 'text-emphasis-style', - - // Text size adjust. - 'text-size-adjust', - - // Text spacing. - 'text-spacing', - - // Transform. - 'transform', - 'transform-origin', - - // Transform 3D. - 'backface-visibility', - 'perspective', - 'perspective-origin', - 'transform-style', - - // Transition. - 'transition', - 'transition-delay', - 'transition-duration', - 'transition-property', - 'transition-timing-function', - - // Unicode bidi. - 'unicode-bidi', - - // User select. - 'user-select', - - // Writing mode. - 'writing-mode', - - ); - - /// Values that should be vendorized. - /// Data via caniuse.com, github.com/postcss/autoprefixer, and developer.mozilla.org - /// @var {list} - $vendor-values: ( - - // Cross fade. - 'cross-fade', - - // Element function. - 'element', - - // Filter function. - 'filter', - - // Flexbox. - 'flex', - 'inline-flex', - - // Grab cursors. - 'grab', - 'grabbing', - - // Gradients. - 'linear-gradient', - 'repeating-linear-gradient', - 'radial-gradient', - 'repeating-radial-gradient', - - // Grid layout. - 'grid', - 'inline-grid', - - // Image set. - 'image-set', - - // Intrinsic width. - 'max-content', - 'min-content', - 'fit-content', - 'fill', - 'fill-available', - 'stretch', - - // Sticky position. - 'sticky', - - // Transform. - 'transform', - - // Zoom cursors. - 'zoom-in', - 'zoom-out', - - ); - -// Functions. - - /// Removes a specific item from a list. - /// @author Hugo Giraudel - /// @param {list} $list List. - /// @param {integer} $index Index. - /// @return {list} Updated list. - @function remove-nth($list, $index) { - - $result: null; - - @if type-of($index) != number { - @warn "$index: #{quote($index)} is not a number for `remove-nth`."; - } - @else if $index == 0 { - @warn "List index 0 must be a non-zero integer for `remove-nth`."; - } - @else if abs($index) > length($list) { - @warn "List index is #{$index} but list is only #{length($list)} item long for `remove-nth`."; - } - @else { - - $result: (); - $index: if($index < 0, length($list) + $index + 1, $index); - - @for $i from 1 through length($list) { - - @if $i != $index { - $result: append($result, nth($list, $i)); - } - - } - - } - - @return $result; - - } - - /// Replaces a substring within another string. - /// @author Hugo Giraudel - /// @param {string} $string String. - /// @param {string} $search Substring. - /// @param {string} $replace Replacement. - /// @return {string} Updated string. - @function str-replace($string, $search, $replace: '') { - - $index: str-index($string, $search); - - @if $index { - @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace); - } - - @return $string; - - } - - /// Replaces a substring within each string in a list. - /// @param {list} $strings List of strings. - /// @param {string} $search Substring. - /// @param {string} $replace Replacement. - /// @return {list} Updated list of strings. - @function str-replace-all($strings, $search, $replace: '') { - - @each $string in $strings { - $strings: set-nth($strings, index($strings, $string), str-replace($string, $search, $replace)); - } - - @return $strings; - - } - -// Mixins. - - /// Wraps @content in vendorized keyframe blocks. - /// @param {string} $name Name. - @mixin keyframes($name) { - - @-moz-keyframes #{$name} { @content; } - @-webkit-keyframes #{$name} { @content; } - @-ms-keyframes #{$name} { @content; } - @keyframes #{$name} { @content; } - - } - - /// Vendorizes a declaration's property and/or value(s). - /// @param {string} $property Property. - /// @param {mixed} $value String/list of value(s). - @mixin vendor($property, $value) { - - // Determine if property should expand. - $expandProperty: index($vendor-properties, $property); - - // Determine if value should expand (and if so, add '-prefix-' placeholder). - $expandValue: false; - - @each $x in $value { - @each $y in $vendor-values { - @if $y == str-slice($x, 1, str-length($y)) { - - $value: set-nth($value, index($value, $x), '-prefix-' + $x); - $expandValue: true; - - } - } - } - - // Expand property? - @if $expandProperty { - @each $vendor in $vendor-prefixes { - #{$vendor}#{$property}: #{str-replace-all($value, '-prefix-', $vendor)}; - } - } - - // Expand just the value? - @elseif $expandValue { - @each $vendor in $vendor-prefixes { - #{$property}: #{str-replace-all($value, '-prefix-', $vendor)}; - } - } - - // Neither? Treat them as a normal declaration. - @else { - #{$property}: #{$value}; - } - - } \ No newline at end of file diff --git a/src/main/resources/static/assets/sass/main.scss b/src/main/resources/static/assets/sass/main.scss deleted file mode 100644 index fbcc2c1..0000000 --- a/src/main/resources/static/assets/sass/main.scss +++ /dev/null @@ -1,1560 +0,0 @@ -@import 'libs/vars'; -@import 'libs/functions'; -@import 'libs/mixins'; -@import 'libs/vendor'; -@import 'libs/breakpoints'; -@import 'libs/html-grid'; -@import url("fontawesome-all.min.css"); -@import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,300italic,600,600italic"); - -/* - Arcana by HTML5 UP - html5up.net | @ajlkn - Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) -*/ - - -// Breakpoints. - - @include breakpoints(( - wide: ( 1281px, 1680px ), - normal: ( 981px, 1280px ), - narrow: ( 841px, 980px ), - narrower: ( 737px, 840px ), - mobile: ( 481px, 736px ), - mobilep: ( null, 480px ) - )); - -// Reset. -// Based on meyerweb.com/eric/tools/css/reset (v2.0 | 20110126 | License: public domain) - - html, body, div, span, applet, object, - iframe, h1, h2, h3, h4, h5, h6, p, blockquote, - pre, a, abbr, acronym, address, big, cite, - code, del, dfn, em, img, ins, kbd, q, s, samp, - small, strike, strong, sub, sup, tt, var, b, - u, i, center, dl, dt, dd, ol, ul, li, fieldset, - form, label, legend, table, caption, tbody, - tfoot, thead, tr, th, td, article, aside, - canvas, details, embed, figure, figcaption, - footer, header, hgroup, menu, nav, output, ruby, - section, summary, time, mark, audio, video { - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; - } - - article, aside, details, figcaption, figure, - footer, header, hgroup, menu, nav, section { - display: block; - } - - body { - line-height: 1; - } - - ol, ul { - list-style:none; - } - - blockquote, q { - quotes: none; - - &:before, - &:after { - content: ''; - content: none; - } - } - - table { - border-collapse: collapse; - border-spacing: 0; - } - - body { - -webkit-text-size-adjust: none; - } - - mark { - background-color: transparent; - color: inherit; - } - - input::-moz-focus-inner { - border: 0; - padding: 0; - } - - input, select, textarea { - -moz-appearance: none; - -webkit-appearance: none; - -ms-appearance: none; - appearance: none; - } - -/* Basic */ - - // Set box model to border-box. - // Based on css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice - html { - box-sizing: border-box; - } - - *, *:before, *:after { - box-sizing: inherit; - } - - body { - background: _palette(bg) url('images/bg01.png'); - - // Stops initial animations until page loads. - &.is-preload { - *, *:before, *:after { - @include vendor('animation', 'none !important'); - @include vendor('transition', 'none !important'); - } - } - - } - - body, input, select, textarea { - color: _palette(fg); - font-family: 'Source Sans Pro', sans-serif; - font-size: 16pt; - font-weight: 300; - line-height: 1.65em; - } - - a { - @include vendor('transition', 'color 0.2s ease-in-out, border-color 0.2s ease-in-out, opacity 0.2s ease-in-out'); - color: _palette(accent, bg); - text-decoration: none; - border-bottom: dotted 1px; - - &:hover { - color: _palette(accent, bg); - border-bottom-color: transparent; - } - } - - strong, b { - font-weight: 600; - } - - em, i { - font-style: italic; - } - - p, ul, ol, dl, table, blockquote { - margin: 0 0 2em 0; - } - - h1, h2, h3, h4, h5, h6 { - color: inherit; - font-weight: 600; - line-height: 1.75em; - margin-bottom: 1em; - - a { - color: inherit; - text-decoration: none; - } - - em { - font-style: normal; - font-weight: 300; - } - } - - h2 { - font-size: 1.75em; - letter-spacing: -0.025em; - } - - h3 { - font-size: 1.2em; - letter-spacing: -0.025em; - } - - sub { - font-size: 0.8em; - position: relative; - top: 0.5em; - } - - sup { - font-size: 0.8em; - position: relative; - top: -0.5em; - } - - hr { - border-top: solid 1px _palette(border); - border: 0; - margin-bottom: 1.5em; - } - - blockquote { - border-left: solid 0.5em _palette(border); - font-style: italic; - padding: 1em 0 1em 2em; - } - -/* Container */ - - .container { - margin: 0 auto; - max-width: 100%; - width: 1400px; - - @include breakpoint('<=wide') { - width: 1200px; - } - - @include breakpoint('<=normal') { - width: 960px; - } - - @include breakpoint('<=narrow') { - width: 95%; - } - - @include breakpoint('<=narrower') { - width: 95%; - } - - @include breakpoint('<=mobile') { - width: 90%; - } - - @include breakpoint('<=mobilep') { - width: 100%; - } - } - -/* Row */ - - .row { - @include html-grid((50px, 50px)); - - @include breakpoint('<=wide') { - @include html-grid((40px, 40px), 'wide'); - } - - @include breakpoint('<=normal') { - @include html-grid((30px, 30px), 'normal'); - } - - @include breakpoint('<=narrow') { - @include html-grid((30px, 30px), 'narrow'); - } - - @include breakpoint('<=narrower') { - @include html-grid((30px, 30px), 'narrower'); - } - - @include breakpoint('<=mobile') { - @include html-grid((20px, 20px), 'mobile'); - } - - @include breakpoint('<=mobilep') { - @include html-grid((20px, 20px), 'mobilep'); - } - } - -/* Section/Article */ - - section, article { - &.special { - text-align: center; - } - } - - header { - p { - color: _palette(fg-light); - font-size: 1.25em; - position: relative; - margin-top: -1.25em; - margin-bottom: 2.25em; - } - - &.major { - text-align: center; - margin: 0 0 2em 0; - - h2 { - font-size: 2.25em; - } - - p { - position: relative; - border-top: solid 1px _palette(border); - padding: 1em 0 0 0; - margin: 0; - top: -1em; - font-size: 1.5em; - letter-spacing: -0.025em; - } - } - } - - footer { - margin: 0 0 3em 0; - - > :last-child { - margin-bottom: 0; - } - - &.major { - padding-top: 3em; - } - } - -/* Form */ - - input[type="text"], - input[type="password"], - input[type="email"], - textarea { - @include vendor('appearance', 'none'); - @include vendor('transition', 'border-color 0.2s ease-in-out'); - background: #fff; - border: solid 1px _palette(border); - border-radius: _size(radius); - color: inherit; - display: block; - outline: 0; - padding: 0.75em; - text-decoration: none; - width: 100%; - - &:focus { - border-color: _palette(accent, bg); - } - } - - input[type="text"], - input[type="password"], - input[type="email"] { - line-height: 1em; - } - - label { - display: block; - color: inherit; - font-weight: 600; - line-height: 1.75em; - margin-bottom: 0.5em; - } - - ::-webkit-input-placeholder { - color: _palette(fg-light); - position: relative; - top: 3px; - } - - :-moz-placeholder { - color: _palette(fg-light); - } - - ::-moz-placeholder { - color: _palette(fg-light); - } - - :-ms-input-placeholder { - color: _palette(fg-light); - } - -/* Image */ - - .image { - border: 0; - display: inline-block; - position: relative; - border-radius: _size(radius); - - img { - display: block; - border-radius: _size(radius); - } - - &.left { - display: block; - float: left; - margin: 0 2em 2em 0; - position: relative; - top: 0.25em; - - img { - display: block; - width: 100%; - } - } - - &.fit { - display: block; - - img { - display: block; - width: 100%; - } - } - - &.featured { - display: block; - margin: 0 0 2em 0; - - img { - display: block; - width: 100%; - } - } - } - -/* Icon */ - - .icon { - @include icon; - position: relative; - text-decoration: none; - - > .label { - display: none; - } - - &:before { - line-height: inherit; - } - - &.solid { - &:before { - font-weight: 900; - } - } - - &.brands { - &:before { - font-family: 'Font Awesome 5 Brands'; - } - } - - &.major { - text-align: center; - cursor: default; - background-color: _palette(accent, bg); - @include vendor('background-image', ('linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.15))', 'url("images/bg01.png")')); - color: _palette(accent, fg); - border-radius: 100%; - display: inline-block; - width: 5em; - height: 5em; - line-height: 5em; - box-shadow: 0 0 0 7px white, 0 0 0 8px _palette(border); - margin: 0 0 2em 0; - - &:before { - font-size: 36px; - } - } - } - -/* Lists */ - - ol { - list-style: decimal; - padding-left: 1.25em; - - li { - padding-left: 0.25em; - } - } - - ul { - list-style: disc; - padding-left: 1em; - - li { - padding-left: 0.5em; - } - } - -/* Links */ - - ul.links { - list-style: none; - padding-left: 0; - - li { - line-height: 2.5em; - padding-left: 0; - } - } - -/* Icons */ - - ul.icons { - cursor: default; - list-style: none; - padding-left: 0; - - li { - display: inline-block; - line-height: 1em; - padding-left: 1.5em; - - &:first-child { - padding-left: 0; - } - - a, span { - font-size: 2em; - border: 0; - } - } - } - -/* Menu */ - - ul.menu { - list-style: none; - padding-left: 0; - - li { - border-left: solid 1px _palette(border); - display: inline-block; - padding: 0 0 0 1em; - margin: 0 0 0 1em; - - &:first-child - { - border-left: 0; - margin-left: 0; - padding-left: 0; - } - } - } - -/* Actions */ - - ul.actions { - @include vendor('display', 'flex'); - cursor: default; - list-style: none; - margin-left: -1em; - padding-left: 0; - - li { - padding: 0 0 0 1em; - vertical-align: middle; - } - - &.special { - @include vendor('justify-content', 'center'); - width: 100%; - margin-left: 0; - - li { - &:first-child { - padding-left: 0; - } - } - } - - &.stacked { - @include vendor('flex-direction', 'column'); - margin-left: 0; - - li { - padding: 1.25em 0 0 0; - - &:first-child { - padding-top: 0; - } - } - } - - &.fit { - width: calc(100% + 1em); - - li { - @include vendor('flex-grow', '1'); - @include vendor('flex-shrink', '1'); - width: 100%; - - > * { - width: 100%; - } - } - - &.stacked { - width: 100%; - } - } - - @include breakpoint('<=mobile') { - &:not(.fixed) { - @include vendor('flex-direction', 'column'); - margin-left: 0; - width: 100% !important; - - li { - @include vendor('flex-grow', '1'); - @include vendor('flex-shrink', '1'); - padding: 1em 0 0 0; - text-align: center; - width: 100%; - - > * { - width: 100%; - } - - &:first-child { - padding-top: 0; - } - - input[type="submit"], - input[type="reset"], - input[type="button"], - button, - .button { - width: 100%; - - &.icon { - &:before { - margin-left: -0.5em; - } - } - } - } - } - } - } - -/* Tables */ - - table { - width: 100%; - - &.default { - width: 100%; - - tbody { - tr { - border-bottom: solid 1px _palette(border); - } - } - - td { - padding: 0.5em 1em 0.5em 1em; - } - - th { - font-weight: 600; - padding: 0.5em 1em 0.5em 1em; - text-align: left; - } - - thead { - background-color: #555555; - @include vendor('background-image', ('linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.15))', 'url("images/bg01.png")')); - color: #fff; - } - - tfoot { - } - - tbody { - } - } - } - -/* Button */ - - input[type="submit"], - input[type="reset"], - input[type="button"], - button, - .button { - @include vendor('appearance', 'none'); - @include vendor('transition', 'background-color 0.2s ease-in-out, color 0.2s ease-in-out, box-shadow 0.2s ease-in-out'); - @include vendor('background-image', ('linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.15))', 'url("images/bg01.png")')); - background-color: _palette(accent, bg); - border-radius: _size(radius); - border: 0; - color: _palette(accent, fg); - cursor: pointer; - display: inline-block; - padding: 0 1.5em; - line-height: 2.75em; - min-width: 9em; - text-align: center; - text-decoration: none; - font-weight: 600; - letter-spacing: -0.025em; - - &:hover { - background-color: lighten(_palette(accent, bg), 5); - color: _palette(accent, fg) !important; - } - - &:active { - background-color: darken(_palette(accent, bg), 5); - color: _palette(accent, fg); - } - - &.alt { - background-color: #555555; - color: #fff; - - &:hover { - background-color: lighten(#555555, 5); - } - - &:active { - background-color: darken(#555555, 5); - } - } - - &.icon { - &:before { - margin-right: 0.5em; - } - } - - &.fit { - width: 100%; - } - - &.small { - font-size: 0.8em; - } - } - -/* Box */ - - .box { - &.highlight { - text-align: center; - } - - &.post { - position: relative; - margin: 0 0 2em 0; - - &:after { - content: ''; - display: block; - clear: both; - } - - .inner { - margin-left: calc(30% + 2em); - - > :last-child { - margin-bottom: 0; - } - } - - .image { - width: 30%; - margin: 0; - } - } - } - -/* Header */ - - #header { - text-align: center; - padding: 3em 0 0 0; - background-color: #fff; - background-image: url('images/bg02.png'), url('images/bg02.png'), url('images/bg01.png'); - background-position: top left, top left, top left; - background-size: 100% 6em, 100% 6em, auto; - background-repeat: no-repeat, no-repeat, repeat; - - h1 { - padding: 0 0 2.75em 0; - margin: 0; - - a { - font-size: 1.5em; - letter-spacing: -0.025em; - border: 0; - } - } - } - - #nav { - cursor: default; - background-color: #333; - @include vendor('background-image', ('linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.3))', 'url("images/bg01.png")')); - padding: 0; - - &:after { - content: ''; - display: block; - width: 100%; - height: 0.75em; - background-color: _palette(accent, bg); - background-image: url("images/bg01.png"); - } - - > ul { - margin: 0; - - > li { - position: relative; - display: inline-block; - margin-left: 1em; - - a { - color: #c0c0c0; - text-decoration: none; - border: 0; - display: block; - padding: 1.5em 0.5em 1.35em 0.5em; - } - - &:first-child { - margin-left: 0; - } - - &:hover { - a { - color: #fff; - } - } - - &.current { - font-weight: 600; - - &:before { - @include vendor('transform', 'rotateZ(45deg)'); - width: 0.75em; - height: 0.75em; - content: ''; - display: block; - position: absolute; - bottom: -0.5em; - left: 50%; - margin-left: -0.375em; - background-color: _palette(accent, bg); - background-image: url("images/bg01.png"); - } - - a { - color: #fff; - } - } - - &.active { - a { - color: #fff; - } - - &.current { - &:before { - opacity: 0; - } - } - } - - > ul { - display: none; - } - } - } - } - -/* Dropotron */ - - .dropotron { - @include vendor('background-image', ('linear-gradient(top, rgba(0,0,0,0.3), rgba(0,0,0,0))', 'url("images/bg01.png")')); - background-color: #333; - border-radius: _size(radius); - color: #fff; - min-width: 10em; - padding: 1em 0; - text-align: center; - box-shadow: 0 1em 1em 0 rgba(0,0,0,0.5); - list-style: none; - - > li { - line-height: 2em; - padding: 0 1.1em 0 1em; - - > a { - color: #c0c0c0; - text-decoration: none; - border: 0; - } - - &.active, - &:hover { - > a { - color: #fff; - } - } - } - - &.level-0 { - border-radius: 0 0 _size(radius) _size(radius); - font-size: 0.9em; - padding-top: 0; - margin-top: -1px; - } - } - -/* Banner */ - - #banner { - background-image: url('../../images/banner.jpg'); - background-position: center center; - background-size: cover; - height: 28em; - text-align: center; - position: relative; - - header { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - background: #212121; - background: rgba(27,27,27,0.75); - color: #fff; - padding: 1.5em 0; - - h2 { - display: inline-block; - margin: 0; - font-size: 1.25em; - vertical-align: middle; - - em { - opacity: 0.75; - } - - a { - border-bottom-color: transparentize(#fff, 0.5); - - &:hover { - border-bottom-color: transparent; - } - } - } - - .button { - vertical-align: middle; - margin-left: 1em; - } - } - } - -/* Wrapper */ - - .wrapper { - padding: 5em 0 3em 0; - - &.style1 { - background: #fff; - } - - &.style2 { - background-color: #fff; - background-image: url('images/bg02.png'), url('images/bg03.png'), url('images/bg01.png'); - background-position: top left, bottom left, top left; - background-size: 100% 6em, 100% 6em, auto; - background-repeat: no-repeat, no-repeat, repeat; - } - - &.style3 { - background-color: _palette(accent, bg); - @include vendor('background-image', ('linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.15))', 'url("images/bg01.png")')); - color: _palette(accent, fg); - - .button { - background: _palette(accent, fg); - color: _palette(fg); - - &:hover { - color: _palette(accent, bg) !important; - } - } - } - } - -/* CTA */ - - #cta { - text-align: center; - padding: 3.5em 0; - - header { - h2 { - display: inline-block; - vertical-align: middle; - margin: 0; - } - - .button { - vertical-align: middle; - margin-left: 1em; - } - } - } - -/* Footer */ - - #footer { - padding: 4em 0 8em 0; - - a { - color: inherit; - border-bottom-color: transparentize(_palette(fg), 0.75); - - &:hover { - color: _palette(accent, bg); - border-bottom-color: transparent; - } - } - - .container { - margin-bottom: 4em; - } - - .icons { - text-align: center; - margin: 0; - - a { - color: _palette(fg-light); - - &:hover { - color: _palette(fg); - } - } - } - - .copyright { - color: _palette(fg-light); - margin-top: 1.5em; - text-align: center; - font-size: 0.9em; - } - } - -/* Wide */ - - @include breakpoint('<=wide') { - - /* Basic */ - - body, input, select, textarea { - font-size: 14pt; - line-height: 1.5em; - } - - /* Banner */ - - #banner { - height: 24em; - } - - } - -/* Normal */ - - @include breakpoint('<=normal') { - - /* Basic */ - - body, input, select, textarea { - font-size: 13pt; - line-height: 1.5em; - } - - /* Lists */ - - ol { - padding-left: 1.25em; - - li { - padding-left: 0.25em; - } - } - - /* Icons */ - - ul.icons { - li { - a, span { - font-size: 1.5em; - } - } - } - - /* Header */ - - #header { - padding: 2em 0 0 0; - - h1 { - padding: 0 0 1.75em 0; - } - } - - /* Banner */ - - #banner { - height: 20em; - } - - /* Wrapper */ - - .wrapper { - padding: 3em 0 1em 0; - } - - /* CTA */ - - #cta { - padding: 2em 0; - } - - /* Footer */ - - #footer { - padding: 3em 0 3em 0; - - .container { - margin-bottom: 1em; - } - } - - } - -/* Narrow */ - - @include breakpoint('<=narrow') { - - /* Basic */ - - body, input, select, textarea { - font-size: 12pt; - line-height: 1.5em; - } - - } - -/* Narrower */ - - #navPanel, #titleBar { - display: none; - } - - @include breakpoint('<=narrower') { - - /* Basic */ - - html, body { - overflow-x: hidden; - } - - body, input, select, textarea { - font-size: 13pt; - } - - h1, h2, h3, h4, h5, h6 { - margin-bottom: 0.5em; - } - - header { - p { - margin-top: -0.75em; - } - - &.major { - text-align: center; - margin: 0 0 2em 0; - - h2 { - font-size: 1.75em; - } - - p { - top: -0.25em; - font-size: 1.25em; - } - } - } - - /* Box */ - - .box { - &.highlight { - text-align: left; - position: relative; - padding-left: 7em; - - i { - position: absolute; - margin: 0; - left: 0; - top: 0.25em; - } - } - - &.post { - .inner { - margin-left: calc(20% + 2em); - } - - .image { - width: 20%; - } - } - } - - /* Header */ - - #header { - display: none; - } - - /* Banner */ - - #banner { - height: 20em; - - header { - h2 { - display: block; - } - - .button { - margin: 1em 0 0 0; - } - } - } - - /* CTA */ - - #cta { - padding: 1.5em 0; - - header { - h2 { - display: block; - } - - .button { - margin: 1em 0 0 0; - } - } - } - - /* Footer */ - - #footer { - text-align: center; - - .container { - margin-bottom: 4em; - } - - form .actions { - @include vendor('justify-content', 'center'); - width: 100%; - margin-left: 0; - - li { - &:first-child { - padding-left: 0; - } - } - } - } - - /* Nav */ - - #page-wrapper { - @include vendor('backface-visibility', 'hidden'); - @include vendor('transition', 'transform #{_duration(navPanel)} ease'); - padding-bottom: 1px; - padding-top: 44px; - } - - #titleBar { - @include vendor('backface-visibility', 'hidden'); - @include vendor('transition', 'transform #{_duration(navPanel)} ease'); - display: block; - height: 44px; - left: 0; - position: fixed; - top: 0; - width: 100%; - z-index: _misc(z-index-base) + 1; - background-color: #333; - @include vendor('background-image', ('linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.3))', 'url("images/bg01.png")')); - height: 44px; - line-height: 44px; - box-shadow: 0 4px 0 0 _palette(accent, bg); - - .title { - display: block; - position: relative; - font-weight: 600; - text-align: center; - color: #fff; - z-index: 1; - - em { - font-style: normal; - font-weight: 300; - } - } - - .toggle { - @include icon(false, solid); - border: 0; - height: 60px; - left: 0; - position: absolute; - top: 0; - width: 80px; - z-index: 2; - - &:before { - content: '\f0c9'; - display: block; - height: 44px; - line-height: inherit; - text-align: center; - width: 44px; - color: #fff; - opacity: 0.5; - } - - &:active { - &:before { - opacity: 0.75; - } - } - } - } - - #navPanel { - background-color: #1f1f1f; - box-shadow: inset -1px 0 3px 0 rgba(0,0,0,0.5); - @include vendor('background-image', ('linear-gradient(left, rgba(0,0,0,0) 75%, rgba(0,0,0,0.15))', 'url("images/bg01.png")')); - @include vendor('backface-visibility', 'hidden'); - @include vendor('transform', 'translateX(#{_size(navPanel) * -1})'); - @include vendor('transition', ('transform #{_duration(navPanel)} ease')); - display: block; - height: 100%; - left: 0; - overflow-y: auto; - position: fixed; - top: 0; - width: _size(navPanel); - z-index: _misc(z-index-base) + 2; - - .link { - border-bottom: 0; - border-top: solid 1px rgba(255,255,255,0.05); - color: #888; - display: block; - height: 48px; - line-height: 48px; - padding: 0 1em 0 1em; - text-decoration: none; - - &:first-child { - border-top: 0; - } - - &.depth-0 { - color: #fff; - } - - .indent-1 { display: inline-block; width: 1em; } - .indent-2 { display: inline-block; width: 2em; } - .indent-3 { display: inline-block; width: 3em; } - .indent-4 { display: inline-block; width: 4em; } - .indent-5 { display: inline-block; width: 5em; } - } - } - - body { - &.navPanel-visible { - #page-wrapper { - @include vendor('transform', 'translateX(#{_size(navPanel)})'); - } - - #titleBar { - @include vendor('transform', 'translateX(#{_size(navPanel)})'); - } - - #navPanel { - @include vendor('transform', 'translateX(0)'); - } - } - } - - } - -/* Mobile */ - - @include breakpoint('<=mobile') { - - /* Basic */ - - body, input, select, textarea { - font-size: 11pt; - line-height: 1.35em; - } - - h2 { - font-size: 1.25em; - letter-spacing: 0; - line-height: 1.35em; - } - - h3 { - font-size: 1em; - letter-spacing: 0; - line-height: 1.35em; - } - - header { - p { - margin-top: -0.5em; - font-size: 1em; - } - - &.major { - padding: 0 20px; - - h2 { - font-size: 1.25em; - } - - p { - top: 0; - margin-top: 1.25em; - font-size: 1em; - } - } - } - - /* Menu */ - - ul.menu { - li { - border: 0; - padding: 0; - margin: 0; - display: block; - line-height: 2em; - } - } - - /* Banner */ - - #banner { - height: 18em; - } - - /* Wrapper */ - - .wrapper { - padding: 2em 0 1px 0; - } - - } - -/* Mobile (Portrait) */ - - @include breakpoint('<=mobilep') { - - /* Icon */ - - .icon { - &.major { - width: 4em; - height: 4em; - line-height: 4em; - box-shadow: 0 0 0 7px white, 0 0 0 8px _palette(border); - - &:before { - font-size: 24px; - } - } - } - - /* Button */ - - input[type="submit"], - input[type="reset"], - input[type="button"], - button, - .button { - width: 100%; - display: block; - } - - /* Box */ - - .box { - &.highlight { - padding-left: calc(4em + 30px); - } - - &.post { - .inner { - margin-left: calc(30% + 20px); - } - - .image { - width: 30%; - } - } - } - - /* Banner */ - - #banner { - height: 20em; - - header { - padding: 20px; - } - } - - /* Wrapper */ - - .wrapper { - padding: 2em 20px 1px 20px; - } - - /* CTA */ - - #cta { - padding: 20px; - } - - /* Footer */ - - #footer { - padding: 2em 20px; - text-align: left; - } - - } \ No newline at end of file diff --git a/src/main/resources/static/css/blog.css b/src/main/resources/static/css/blog.css deleted file mode 100644 index 77645fe..0000000 --- a/src/main/resources/static/css/blog.css +++ /dev/null @@ -1,233 +0,0 @@ -:root{ - --ButtonWidth:45%; - --ButtonHeight:45px; -} - - -#save { - right: 0; - position: absolute; - width: var(--ButtonWidth); - height: var(--ButtonHeight); -} -.layer > * { - /*height: fit-content;*/ -} -/*input {*/ -/* width: 100%;*/ -/*}*/ -.write_controllbox { - margin: 0; - padding: 0; - display: -webkit-box; - display: -moz-box; - display: -ms-flexbox; - display: -moz-flex; - display: -webkit-flex; - display: flex; - justify-content: space-between; - list-style: none; -} -.write_option { - display: inline-block; - text-align: center; - line-height: 30px; - width: 100%; - padding: 0; - /*margin: 2px;*/ - margin-top: 5px; - margin-bottom: 5px; - align-content: center; - top: 0; - bottom: 0; - color: black; - /*background: #40404564;*/ - position: relative; - border: 1px solid #ccc; - height: var(--ButtonHeight); - border-radius: 10px; - background: #fafbfc; -} - -#location_field { - line-height: initial; - height: initial; -} -/*#title_field {*/ -/*font-size: 20px;*/ -/*}*/ - -.pop_layer .pop_container { - padding: 20px 25px; -} - -.pop_layer p.ctxt { - color: #666; - line-height: 25px; -} - -.pop_layer .btn_r { - width: 100%; - margin: 10px 0 20px; - padding-top: 10px; - border-top: 1px solid #DDD; - text-align: right; -} - -.pop_layer { - display: none; - position: absolute; - top: 50%; - left: 50%; - width: 410px; - height: auto; - background-color: #fff; - border: 5px solid #3571B5; - z-index: 10; -} - -.dim_layer { - display: none; - position: fixed; - _position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 100; -} - -.dim_layer .dimBg { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: #000; - opacity: .5; - filter: alpha(opacity=50); -} - -.dim_layer .pop_layer { - display: block; -} - -a.btn_layerClose { - display: inline-block; - height: 25px; - padding: 0 14px 0; - border: 1px solid #304a8a; - background-color: #3f5a9d; - font-size: 13px; - color: #fff; - line-height: 25px; -} - -a.btn_layerClose:hover { - border: 1px solid #091940; - background-color: #1f326a; - color: #fff; -} -.post_layer { - height: 100%; - place-content: space-between; - place-items: stretch; - display: grid; - gap: 10px; - /*grid-auto-rows: minmax(200px, auto);*/ - grid-template-columns: repeat(auto-fill, minmax(200px, auto)); - width: 100%; - -} -.post_item { - justify-content: space-between; - flex-wrap: wrap; - flex-direction: row; - width: 100%; - border-radius: 10px; - background: #F0F0F524; -} -#postId { - display: none; -} - -#writeDate { - text-align: right; -} -.post_attr { - display: block; - padding: 5px; -} -#content{ - overflow: hidden; - overflow-y: hidden; - overflow-x: hidden; -} -/*#editor {*/ -/* border: 1px solid #393b42;*/ -/* padding: 1px;*/ -/* border-radius: 10px;*/ -/* background: #00000044;*/ -/*}*/ - -.ql-font-sans-serif { font-family: Arial, Helvetica, sans-serif; } -.ql-font-serif { font-family: Georgia, serif; } -.ql-font-monospace { font-family: "Courier New", monospace; } -.ql-font-arial { font-family: Arial, sans-serif; } -.ql-font-georgia { font-family: Georgia, serif; } -.ql-font-comic-sans-ms { font-family: "Comic Sans MS", cursive, sans-serif; } -.ql-font-courier-new { font-family: "Courier New", Courier, monospace; } -.ql-font-roboto { - font-family: 'Roboto', sans-serif; -} -.ql-font-playfair-display { - font-family: 'Playfair Display', serif; -} - - -/* Quill 툴바 기본 정렬 및 스타일 통합 */ -.ql-toolbar.ql-snow { - display: flex; - flex-wrap: wrap; /* 툴바 요소가 넘칠 경우 줄바꿈 */ - align-items: center; - gap: 8px; /* 아이콘과 버튼 간 간격 */ - padding: 8px 12px; - border-radius: 10px 10px 0 0; - background: #fafbfc; -} - -#editor.ql-container.ql-snow { - display: flex; - flex-wrap: wrap; /* 툴바 요소가 넘칠 경우 줄바꿈 */ - align-items: center; - gap: 8px; /* 아이콘과 버튼 간 간격 */ - padding: 8px 12px; - border-radius: 10px 10px 10px 10px; - background: #fafbfc; -} - -/* 툴바 내 각 툴 그룹 사이 구분선 및 여백 */ -.ql-toolbar.ql-snow > span.ql-formats:not(:last-child) { - border-right: 1px solid #ddd; - margin-right: 18px; - padding-right: 10px; -} - -/* 폰트 선택기 및 드롭다운 스타일 */ -.ql-toolbar .ql-picker-label, -.ql-toolbar .ql-picker { - min-width: 2em; - height: 2.2em; - display: flex; - align-items: center; -} - -/* 툴바 버튼 스타일 */ -.ql-toolbar button { - min-width: 32px; - height: 32px; - margin: 0 2px; - display: flex; - align-items: center; - justify-content: center; -} diff --git a/src/main/resources/static/css/common.css b/src/main/resources/static/css/common.css index 035635a..108294b 100644 --- a/src/main/resources/static/css/common.css +++ b/src/main/resources/static/css/common.css @@ -1,198 +1,264 @@ -:root { - --WindowFull : 99vw; - --TopHeight: 160px; - --FooterHeight: 160px; - --ContentVerticalMargin: 5px; - --DEFAULT_LAYER_BACK : #2e2e2eBB - /*background-image: url("data:image/svg+xml,")*/ +/* + * MODIFIED COMMON.CSS + * This file is refactored to inherit styles and variables from main.css. + * It styles custom components (popups, editor, etc.) to match the Arcana theme. + */ + +/* * Removed conflicting global 'html, body' styles. + * The site will now correctly use the font and background from main.css. + */ + +/* + * --- POPUP AND DIM LAYER STYLES --- + * Reworked to use variables and styles from main.css for a consistent look. +*/ +.pop_layer { + display: none; + position: fixed; /* Use fixed for proper viewport centering */ + top: 50%; + left: 50%; + transform: translate(-50%, -50%); /* Modern centering method */ + width: 450px; + max-width: 90%; /* Ensure it's responsive on small screens */ + height: auto; + background-color: var(--pure-white, #fff); /* Use theme's white variable */ + border: 1px solid #e0e0e0; /* Use theme's standard border color */ + box-shadow: 0 0 15px rgba(0, 0, 0, 0.15); /* Add a subtle shadow */ + z-index: 1001; /* Ensure it's above the dim layer */ + border-radius: 5px; /* Use theme's standard border-radius */ } -html { - /*background-image: url("data:image/svg+xml,");*/ - /*margin: 1vh 1vw;*/ - background: black; +.pop_layer .pop_container { + padding: 2em; } -/*#where{*/ -/* table-layout: fixed;*/ -/*}*/ - -/*.where_item {*/ -/* display: table-cell;*/ -/*}*/ -body { - user-select: none; - -webkit-user-select: none; - align-content: center; - /*background: var(--DEFAULT_LAYER_BACK);*/ - /*padding: 1vh 1vw;*/ - /*border-radius: 10px;*/ +.pop_layer .pop_conts h2 { + font-size: 1.75em; /* Match theme's h2 style */ + margin-bottom: 1em; + text-align: center; } -body > *{ - align-content: center; - color: white; - padding: 1vh 1vw; +.pop_layer p.ctxt { + color: var(--font-color_default, #474747); /* Use theme's text color */ + line-height: 1.65em; } -/*#main_layer {*/ -/* width: 100%;*/ -/* margin: 0 auto;*/ -/* position: relative;*/ -/* background: #F0F0F524;*/ -/* border-radius: 10px;*/ -/*}*/ - - -.center_menu { - - display: inline-flex; - justify-content: space-evenly; +.pop_layer .btn_r { + width: 100%; + margin-top: 1.5em; + padding-top: 1em; + border-top: 1px solid #e0e0e0; /* Use theme's divider color */ + text-align: right; } -header { +/* Style the close button to match the theme's alternate button style */ +a.btn_layerClose { + display: inline-block; + padding: 0 1.5em; + line-height: 2.75em; + text-decoration: none; + font-weight: 600; + border-radius: 5px; + cursor: pointer; + background-color: var(--button-alt-default, #555); + color: var(--pure-white, #fff); + transition: background-color 0.2s ease-in-out; +} + +a.btn_layerClose:hover { + background-color: var(--button-alt-hover, #626262); + color: var(--pure-white, #fff) !important; /* Ensure hover color override */ +} + + +.dim_layer { + display: none; + position: fixed; top: 0; - /*background: var(--DEFAULT_LAYER_BACK);*/ - background: var(--DEFAULT_LAYER_BACK); - border-top: #ec914b8f; - border-radius: 5px 30px; - border-width: 1px; - height: 5vh; - min-height: 5vh; - grid-auto-flow: column; - display: flex;; - position: relative; - justify-content: space-around; -} -.user_info { - - padding: 2px; - position: absolute; - display: inline-flex; - flex-direction: column; - right: 0; -} -.login_input { - border-radius: 10px; - border-width: 2px; - border: #F0F0F514; - padding: 5px; - background: #F0F0F524; - color: white; - text-align: center; - margin: 2px; -} -.login_input::placeholder { - color: #ec914b; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + background-color: rgba(0, 0, 0, 0.4); /* Slightly softer dim */ } -.login_input::-webkit-input-placeholder{ - color: #ec914b; +/* * --- LOGIN FORM STYLES --- + * Adapted to use the default form input styles from main.css. +*/ +#loginFormElement input[type="text"], +#loginFormElement input[type="password"] { + margin-bottom: 1em; /* Add spacing between fields */ } -.login_input:-ms-input-placeholder{ - color: #ec914b; +#loginFormElement button { + margin-top: 1em; + width: 100%; } -.hello_to_user { - border-radius: 10px; - border-width: 2px; - border: #F0F0F514; - padding: 10px; - background: #F0F0F524; - color: #ec914b; - text-align: center; - margin: 2px; -} - - -.hello_to_user_txt { - color: #ec914b; -} - - -#bottom { - float: right; - display: inline-block; - justify-content: space-between; - margin-left: auto; - grid-auto-flow: column; - grid-template-columns: 3fr; - position: absolute; - right: 30px; -} - -#top { - - display: inline-block; - justify-content: space-between; - margin-left: auto; - grid-auto-flow: column; - grid-template-columns: 3fr; - position: absolute; - left: 30px; -} - -h2 { - margin: 1vw; -} - -#main_layer { - border-radius: 10px 10px 0px 10px; - padding: 10px; - margin: 1vw 1vh; - position: relative; - overflow-y: auto; - overflow-x: clip; - height: 78vh; - min-height: 8vh; -} - -#main_layer > div { - position: relative; -} - - -#content > * { - -} - -footer { - display: flex; - bottom: 0; - border-top: #ec914b8f; - background: var(--DEFAULT_LAYER_BACK); - border-radius: 5px 30px ; - border-width: 1px; - height: 5vh; - min-height: 5vh; - position: relative; +/* Custom Checkbox Styling */ +#loginFormElement span { + vertical-align: middle; + margin-left: 0.5em; } #rememberMe { - width: 20px; - height: 20px; - background-color: lightblue; /* 체크박스 배경색 */ - border: 2px solid blue; /* 테두리 색 */ - appearance: none; /* 기본 OS 스타일 제거 */ + vertical-align: middle; + width: 22px; + height: 22px; + appearance: none; -webkit-appearance: none; - -moz-appearance: none; + border: 1px solid #e0e0e0; + border-radius: 5px; + background-color: var(--pure-white, #fff); cursor: pointer; position: relative; + top: -2px; } #rememberMe:checked { - background-color: blue; + background-color: var(--point-color, #FFA500); + border-color: var(--point-color, #FFA500); } #rememberMe:checked::after { - content: ""; + content: ''; position: absolute; - top: 3px; + top: 2px; left: 7px; - width: 5px; - height: 10px; - border: solid white; + width: 6px; + height: 12px; + border: solid var(--pure-white, #fff); border-width: 0 2px 2px 0; transform: rotate(45deg); +} + +/* * --- BLOG POST & EDITOR STYLES --- +*/ + +/* Control box below the editor in viewer.html */ +.write_controllbox { + margin-top: 2em; + padding: 0; + display: flex; + justify-content: space-between; + list-style: none; + gap: 15px; /* Use gap for spacing */ +} + +.write_option { + display: flex; /* Use flexbox for centering */ + justify-content: center; + align-items: center; + text-align: center; + width: 100%; + padding: 0.75em; + margin: 0; + background: var(--pure-white, #fff); + border: solid 1px #e0e0e0; + border-radius: 5px; + color: inherit; + min-height: 48px; /* Set a minimum height */ +} + + +/* * --- QUILL EDITOR THEME OVERRIDE --- + * Modified to blend with the Arcana theme's light background. +*/ + +/* Define custom fonts for Quill to match the site */ +.ql-font-source-sans-pro { font-family: 'Source Sans Pro', sans-serif; } +/* Add any other fonts you've whitelisted in common.js */ + +.ql-toolbar.ql-snow { + background: var(--almost-white, #f7f7f7); /* Use theme's light gray */ + border: 1px solid #e0e0e0; + border-bottom: none; /* Connect toolbar to editor visually */ + border-radius: 5px 5px 0 0; + padding: 12px 8px; +} + +.ql-container.ql-snow { + background: var(--pure-white, #fff); + border: 1px solid #e0e0e0; + border-radius: 0 0 5px 5px; + color: var(--font-color_default, #474747); +} + +/* Ensure editor content uses the theme's default font and size */ +.ql-editor { + font-family: 'Source Sans Pro', sans-serif; + font-size: 1rem; /* Base size */ + line-height: 1.65em; +} + +/* + * --- COMMENT SECTION --- + * Styled to fit the main theme. +*/ +.comment-section { + margin-top: 3em; + padding-top: 2em; + border-top: 1px solid #e0e0e0; +} + +#comment-form-container { + display: flex; + flex-direction: column; + margin-bottom: 2em; +} + +/* Inherits styles from main.css 'textarea' selector */ +#comment-input { + min-height: 100px; + margin-bottom: 1em; +} + +#comments-list .comment { + background: var(--almost-white, #f7f7f7); + border-radius: 5px; + padding: 1em 1.5em; + margin-bottom: 1em; + border: 1px solid #e0e0e0; +} + +.comment-header { + font-weight: 600; + color: var(--font-color_default, #474747); + margin-bottom: 0.5em; +} + +.comment-time { + font-size: 0.9em; + color: #999; + margin-left: 0.5em; +} + +.comment-content { + line-height: 1.65em; + white-space: pre-wrap; + word-wrap: break-word; +} + +/* 읽기 모드 컨트롤 박스 내부의 태그 스타일 */ +.write_option .tag-title { + font-weight: 600; + margin-right: 0.5em; + color: #555; +} + +.write_option .tag-item { + display: inline-block; + background-color: var(--almost-white, #f7f7f7); + border: 1px solid #e0e0e0; + border-radius: 15px; /* 둥근 태그 모양 */ + padding: 0.2em 0.8em; + margin-right: 0.5em; + font-size: 0.9em; + color: var(--font-color_default, #474747); +} + +/* 읽기 모드에서는 커서 모양을 기본으로 변경 */ +.write_option.controlbox-category:not(.btn-example), +.write_option.controlbox-hashtag:not(.btn-example) { + cursor: default; } \ No newline at end of file diff --git a/src/main/resources/static/assets/css/fontawesome-all.min.css b/src/main/resources/static/css/fontawesome-all.min.css similarity index 100% rename from src/main/resources/static/assets/css/fontawesome-all.min.css rename to src/main/resources/static/css/fontawesome-all.min.css diff --git a/src/main/resources/static/assets/css/images/bg01.png b/src/main/resources/static/css/images/bg01.png similarity index 100% rename from src/main/resources/static/assets/css/images/bg01.png rename to src/main/resources/static/css/images/bg01.png diff --git a/src/main/resources/static/assets/css/images/bg02.png b/src/main/resources/static/css/images/bg02.png similarity index 100% rename from src/main/resources/static/assets/css/images/bg02.png rename to src/main/resources/static/css/images/bg02.png diff --git a/src/main/resources/static/assets/css/images/bg03.png b/src/main/resources/static/css/images/bg03.png similarity index 100% rename from src/main/resources/static/assets/css/images/bg03.png rename to src/main/resources/static/css/images/bg03.png diff --git a/src/main/resources/static/assets/css/images/card-back.png b/src/main/resources/static/css/images/card-back.png similarity index 100% rename from src/main/resources/static/assets/css/images/card-back.png rename to src/main/resources/static/css/images/card-back.png diff --git a/src/main/resources/static/css/images/tq.png b/src/main/resources/static/css/images/tq.png new file mode 100644 index 0000000..0159beb Binary files /dev/null and b/src/main/resources/static/css/images/tq.png differ diff --git a/src/main/resources/static/assets/css/main.css b/src/main/resources/static/css/main.css similarity index 99% rename from src/main/resources/static/assets/css/main.css rename to src/main/resources/static/css/main.css index 91a47b3..f87b19a 100644 --- a/src/main/resources/static/assets/css/main.css +++ b/src/main/resources/static/css/main.css @@ -2693,7 +2693,7 @@ button.small, /* Banner */ #banner { - background-image: url("../../images/banner.jpg"); + background-image: url("../images/banner.jpg"); background-position: center center; background-size: cover; height: 28em; diff --git a/src/main/resources/static/css/spider.css b/src/main/resources/static/css/spider.css index 478eaca..c6f78e1 100644 --- a/src/main/resources/static/css/spider.css +++ b/src/main/resources/static/css/spider.css @@ -1,40 +1,17 @@ -/* src/main/resources/static/css/spider.css */ - -/* - * 전체 게임 컨테이너 (Canvas를 담는 역할) - * - 기존 블로그 레이아웃을 해치지 않도록 최소한의 스타일만 적용 - */ #game-container { display: flex; - justify-content: center; - align-items: center; + justify-content: center; /* 가로 중앙 정렬 */ + align-items: flex-start; /* 세로 상단 정렬 */ background-color: #008000; width: 100vw; height: 100vh; box-sizing: border-box; } -/* - * 캔버스 스타일 - * - 배경을 초록색으로 설정하고, 테두리를 추가 - * - 화면 크기에 맞춰 비율을 유지하며 최대 크기 제한 - */ #gameCanvas { background-color: #008000; border: 2px solid #fff; - max-width: 90vw; - max-height: 90vh; + width: 95%; + max-height: min(95vw, 95vh); box-sizing: border-box; -} - -/* - * 카드 이동 애니메이션 (Canvas 로직에서는 사용하지 않지만, 향후 재사용을 위해 남겨둠) - */ -@keyframes moveCard { - from { - transform: translate(var(--fromX), var(--fromY)); - } - to { - transform: translate(var(--toX), var(--toY)); - } } \ No newline at end of file diff --git a/src/main/resources/static/index_ex.html b/src/main/resources/static/index_ex.html index 2885466..9e31c99 100644 --- a/src/main/resources/static/index_ex.html +++ b/src/main/resources/static/index_ex.html @@ -9,7 +9,7 @@ Arcana by HTML5 UP - +
@@ -228,12 +228,12 @@
- - - - - - + + + + + + \ No newline at end of file diff --git a/src/main/resources/static/js/blog.js b/src/main/resources/static/js/blog.js deleted file mode 100644 index 1899d95..0000000 --- a/src/main/resources/static/js/blog.js +++ /dev/null @@ -1,335 +0,0 @@ - -var quill = null -function initEditor(useEditor) { - console.log("DOMContentLoaded"); - const editorContainer = document.querySelector('#editor'); - function setEditorHeight() { - const height = Math.max(window.innerHeight * 0.5, 300); - editorContainer.style.height = height + 'px'; - } - - baseData.id = serverData.id; - baseData.title = decodeURIComponent(serverData.title || ''); - baseData.content = decodeURIComponent(serverData.content || ''); - baseData.firstPostLat = serverData.firstPostLat; - baseData.firstPostLon = serverData.firstPostLon; - baseData.writeTime = serverData.writeTime; - baseData.originId = serverData.originId; - - getLocation(); - - setEditorHeight(); - window.addEventListener('resize', setEditorHeight); - try { - var Font = Quill.import('formats/font'); - Font.whitelist = ['sans-serif', 'serif', 'monospace', 'arial', 'georgia', 'comic-sans-ms', 'courier-new', 'roboto', 'playfair-display']; - Quill.register(Font, true); - Quill.register({ 'modules/table-better': QuillTableBetter }, true); - - quill = new Quill(editorContainer, { - theme: 'snow', - modules: useEditor ? { - toolbar: { - container: [ - [{ font: Font.whitelist }], - [{ 'size': ['small', false, 'large', 'huge'] }], - ['bold', 'italic', 'underline', 'strike'], - [{ 'color': [] }, { 'background': [] }], - [{ 'header': 1 }, { 'header': 2 }, 'blockquote', 'code-block'], - [{ 'script': 'sub'}, { 'script': 'super' }], - [{ 'list': 'ordered'}, { 'list': 'bullet' }], - [{ 'indent': '-1'}, { 'indent': '+1' }], - ['link', 'image', 'video'], - ['table-better'], - [{ 'direction': 'rtl' }], - [{ 'align': [] }], - ['clean'] - ], - handlers: { - image: function () { - selectLocalImage(); - }, - video: function () { - selectLocalVideo() - } - } - }, - 'table-better': { - language: 'en_US', - toolbarTable: true - }, - keyboard: { - bindings: QuillTableBetter.keyboardBindings - } - } : {toolbar : false, toolbarTable: false}, - - readOnly: !useEditor - }); - - loadContent(serverData.content); - if (!useEditor) { - editorContainer.classList.add('readonly-mode'); - } else { - editorContainer.classList.remove('readonly-mode'); - } - console.log("quill", quill); - }catch (e) { } - try { - document.querySelector("#title_field").textContent = baseData.title - }catch (e) { } - try { - document.getElementById('location_field').textContent = "Lat: " + baseData.firstPostLat + ", Lon: " + baseData.firstPostLon; - var requestOptions = { - method: 'GET', - }; - - fetch("https://api.geoapify.com/v1/geocode/reverse?lat="+baseData.firstPostLat+"&lon="+baseData.firstPostLon+"&apiKey=2b37a75bb0754086b5a1c4a7c3173ee8", requestOptions) - .then(response => response.json()) - .then(function(result) { - try { - document.getElementById('location_field').textContent = result.features[0].properties.formatted - } catch (e) { - document.getElementById('location_field').innerHTML = "Lat: " + baseData.firstPostLat + "
Lon: " + baseData.firstPostLon; - } - }) - .catch(error => console.log('error', error)); - }catch (e) { } -} - -function parseDelta(content) { - try { - if (typeof content === "string") { - const obj = JSON.parse(content); - if (obj && typeof obj === "object" && Array.isArray(obj.ops)) { - return obj; // 유효한 Delta 객체 - } - } else if (content && typeof content === "object" && Array.isArray(content.ops)) { - return content; // 이미 Delta 객체 형태 - } - } catch (e) { - // JSON 파싱 실패 → HTML로 간주 - } - return null; // Delta 아님 -} -function loadEditor() { - location.href = getMainPath() + '/blog/editor/' + serverData.id; -} -function loadContent(content) { - console.log("content >>> ", content); - const delta = parseDelta(content); - if (delta) { - quill.setContents(delta); - } else { - quill.clipboard.dangerouslyPasteHTML(content); - } -} - - -function save() { - onclickWrite(serverData.enc ,serverData.keyword,JSON.stringify(quill.getContents())) -} - -function parseDeltaContent(deltaString) { - let textContent = ""; - let mediaLinks = []; - - try { - const delta = JSON.parse(deltaString); // Delta 문자열 → 객체 - if (delta && Array.isArray(delta.ops)) { - delta.ops.forEach(op => { - if (op.insert) { - if (typeof op.insert === "string") { - textContent += op.insert; - } else if (typeof op.insert === "object") { - if (op.insert.image) { - mediaLinks.push(op.insert.image); - } - if (op.insert.video) { - mediaLinks.push(op.insert.video); - } - } - } - }); - } - } catch (e) { - console.error("Delta JSON parse error:", e); - } - return { text: textContent.trim(), media: mediaLinks }; -} - - -function selectLocalImage() { - // 이미지 URL 입력 받기 - const url = prompt("이미지 URL을 입력하거나 빈칸으로 두시면 파일 업로드를 합니다."); - - if (url) { - // URL이 입력된 경우 이미지 삽입 - const range = quill.getSelection(true); - quill.insertEmbed(range.index, 'image', url); - quill.setSelection(range.index + 1); - } else { - // URL이 없거나 취소한 경우 파일 업로드 처리 - const input = document.createElement('input'); - input.setAttribute('type', 'file'); - input.setAttribute('accept', 'image/*'); - input.click(); - - input.onchange = async () => { - const file = input.files[0]; - if (file) { - const file = input.files[0]; - console.log("on selectLocalImage File", file); - if (!file || !file.type.startsWith('image/')) { - console.warn('이미지 파일만 업로드 가능합니다.'); - return; - } - uploadImage(file); - } - }; - } -} - -function uploadImage(blob) { - const formData = new FormData(); - formData.append('file', blob); - let uploadUrl = getMainPath() + "/blog/post/imageUpload.bjx"; - let imageUrl = getMainPath() + '/blog/post/images/'; - $.ajax({ - type: 'POST', - enctype: 'multipart/form-data', - url: uploadUrl, - data: formData, - dataType: 'json', - processData: false, - contentType: false, - cache: false, - timeout: 600000, - success: function (data) { - console.log(data); - imageUrl += data.fileName; - insertToEditor(imageUrl); - }, - error: function (e) { - console.error(e); - // callback('image_load_fail'); - } - }); -} - -function selectLocalVideo() { - const input = document.createElement('input'); - input.setAttribute('type', 'file'); - input.setAttribute('accept', 'video/*'); - input.click(); - - input.onchange = () => { - const file = input.files[0]; - if (!file || !file.type.startsWith('video/')) { - alert('동영상 파일만 업로드할 수 있습니다.'); - return; - } - uploadVideo(file); - }; -} -function uploadVideo(file) { - const formData = new FormData(); - formData.append('video', file); - - fetch('/api/upload/video', { - method: 'POST', - body: formData - }) - .then(res => res.json()) - .then(result => { - if (result.url) { - const range = quill.getSelection(true); - quill.insertEmbed(range.index, 'video', result.url); - quill.setSelection(range.index + 1); - } else { - console.error('동영상 업로드 실패', result); - } - }) - .catch(err => { - console.error('업로드 중 오류', err); - }); -} - - -function insertToEditor(url) { - const range = quill.getSelection(true); - quill.insertEmbed(range.index, 'image', url); - quill.setSelection(range.index + 1); -} - -var currentLat = 0.0 -var currentLon = 0.0 - -let baseData = { - 'id' : "", - 'title': "", - 'content': "", - 'firstPostLat': 0.0, - 'firstPostLon': 0.0, - 'category' : "none", - 'hashTags' : "#none", - 'modifyLat' : 0.0, - 'modifyLon' : 0.0, - 'originId' : "", - 'writeTime' : 0, -} - -function goToEditor(element) { - const postId = element.getAttribute('data-post-id'); - if (postId) { - // postId를 이용해 원하는 처리 수행 - console.log("편집할 postId:", postId); - // 예: 페이지 이동, 모달 오픈 등 - location.href = getMainPath() + '/blog/editor/' + postId; - } else { - console.warn("postId가 없네요."); - } -} - - -function onclickWrite(type, keyword, html) { - let title_field = document.getElementById('title_field') - var hasValues = true - if (hasValues) { - baseData.title = encodeURIComponent(title_field.value) - baseData.content = encodeURIComponent(html) - baseData.modifyLat = encodeURIComponent(currentLat) - baseData.modifyLon = encodeURIComponent(currentLon) - } - let uploadUrl = getMainPath() + "/blog/post.bjx"; - if(confirm(JSON.stringify(baseData) + "\n해당 내용으로\n유저 등록 하실??")) { - post(uploadUrl,type,JSON.stringify(baseData),keyword, function (resultData) { - alert(resultData) - }) - } else { - - } -} -function getLocation() { - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition(showPosition); - } else { - x.innerHTML = "Geolocation is not supported by this browser."; - } -} -function showPosition(position) { - currentLat = position.coords.latitude - currentLon = position.coords.longitude - if(baseData.firstPostLat !== 0.0) { - baseData.modifyLat = encodeURIComponent(currentLat) - } else { - baseData.firstPostLat = encodeURIComponent(currentLat) - } - if(baseData.firstPostLon !== 0.0 ) { - baseData.modifyLon = encodeURIComponent(currentLon) - } else { - baseData.firstPostLon = encodeURIComponent(currentLon) - } - - - document.getElementById('location_field').textContent = "Lat: " + position.coords.latitude + ", Lon: " + position.coords.longitude; -} \ No newline at end of file diff --git a/src/main/resources/static/assets/js/breakpoints.min.js b/src/main/resources/static/js/breakpoints.min.js similarity index 100% rename from src/main/resources/static/assets/js/breakpoints.min.js rename to src/main/resources/static/js/breakpoints.min.js diff --git a/src/main/resources/static/assets/js/browser.min.js b/src/main/resources/static/js/browser.min.js similarity index 100% rename from src/main/resources/static/assets/js/browser.min.js rename to src/main/resources/static/js/browser.min.js diff --git a/src/main/resources/static/js/common.js b/src/main/resources/static/js/common.js index 91bc2b7..621906e 100644 --- a/src/main/resources/static/js/common.js +++ b/src/main/resources/static/js/common.js @@ -1,464 +1,546 @@ -var enc = null -var keyword = null -document.addEventListener('DOMContentLoaded', function() { - const loginForm = document.getElementById('loginFormElement'); - loginForm.addEventListener('submit', function(e) { - e.preventDefault(); // 기본 폼 제출 동작 방지 - submitLoginForm(); - }); +/** + * ================================================================================= + * common.js - 블로그 공통 스크립트 (최종 수정본) + * - Quill 에디터 초기화 및 제어 (편집/읽기 모드) + * - 게시물 데이터 관리 (baseData) 및 서버 통신 (save, post) + * - UI 제어 (팝업, 컨트롤 박스 동적 설정) + * - 페이지 이동 및 로그인/로그아웃, 유틸리티 함수 + * ================================================================================= + */ + +// 전역 변수: Quill 에디터 인스턴스와 게시물 기본 데이터를 저장합니다. +var quill = null; +var currentLat = 0.0; +var currentLon = 0.0; +var baseData = { + 'id': "", + 'title': "", + 'content': "", + 'category': "none", + 'tags': "", + 'firstPostLat': 0.0, + 'firstPostLon': 0.0, + 'modifyLat': 0.0, + 'modifyLon': 0.0, + 'originId': "", + 'writeTime': 0, +}; + +// jQuery를 사용하여 문서가 완전히 로드된 후에 함수를 실행합니다. +$(document).ready(function() { + // 뷰어/에디터 페이지가 아닐 수 있으므로, #editor 요소가 있을 때만 initEditor를 호출하도록 방어 코드를 추가하는 것이 좋습니다. + // 현재는 각 페이지에서 직접 호출하므로 이 코드는 참고용입니다. + + // 사이드바의 인기글/최신글 목록을 가져옵니다. if (document.querySelector(".rank_of_view")) { - fetch('blog/rankOfViews.bjx') - .then(response => response.json()) - .then(data => { - const ul = document.querySelector('.rank_of_view'); - ul.innerHTML = ''; // 기존 리스트 지움 - ul.style.listStyle = 'none'; // 불릿 제거 - ul.style.paddingLeft = '0'; // 들여쓰기 제거 (선택사항) - // data가 ['제목1', '제목2', ...] 형식이라고 가정 - data.posts.forEach(item => { - const date = new Date(item.writeTime); - - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - - const formatted = `${year}/${month}/${day}`; - const li = document.createElement('li'); - const a = document.createElement('a'); - a.id = item.id; - a.href = `${getMainPath()}/blog/viewer/${item.id}`; - a.innerHTML = `${item.title}
[${year}/${month}/${day}]` - li.appendChild(a); - ul.appendChild(li); - }); - }) - .catch(error => { - console.error('받아오기 실패:', error); - } - ); + fetchRankOfViews(); } if (document.querySelector(".recent_posts")) { - fetch('blog/recentOfPost.bjx') - .then(response => response.json()) - .then(data => { - const ul = document.querySelector('.recent_posts'); - ul.innerHTML = ''; // 기존 리스트 지움 - ul.style.listStyle = 'none'; // 불릿 제거 - ul.style.paddingLeft = '0'; // 들여쓰기 제거 (선택사항) - // data가 ['제목1', '제목2', ...] 형식이라고 가정 - data.posts.forEach(item => { - const date = new Date(item.writeTime); - - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - - const formatted = `${year}/${month}/${day}`; - const li = document.createElement('li'); - const a = document.createElement('a'); - a.id = item.id; - a.href = `${getMainPath()}/blog/viewer/${item.id}`; - a.innerHTML = `${item.title}
[${year}/${month}/${day}]` - li.appendChild(a); - ul.appendChild(li); - }); - }) - .catch(error => { - console.error('받아오기 실패:', error); - } - ); + fetchRecentPosts(); } + // 팝업 닫기 버튼 이벤트 + $('.btn_layerClose').on('click', function(e) { + e.preventDefault(); + closePopup(); + }); + + // 로그인 폼 제출 이벤트 + $('#loginFormElement').on('submit', function(e) { + e.preventDefault(); + submitLoginForm(); + }); + + // 로그인 팝업 열기 버튼 이벤트 + $('.open-login-popup').on('click', function() { + openPopup(this); + }); }); -onload = function() { - history.replaceState({}, null, location.pathname); - // var accToken = get_cookie("access") - // var refreshToken = get_cookie("refresh") - // console.log("access === " + accToken + " || " + accToken.length); - // console.log("refresh === " + refreshToken + " || " + refreshToken.length); - document.cookie = "access=; expires=Thu, 01 Jan 1970 00:00:01 GMT;" - document.cookie = "refresh=; expires=Thu, 01 Jan 1970 00:00:01 GMT;" - document.cookie = "CLEAR=; expires=Thu, 01 Jan 1970 00:00:01 GMT;" - var currentList = [{"page":["posts"],"id":"menu_posts"},{"page":["licenses"],"id":"menu_drop"},{"page":["licenses"],"id":"menu_drop"}] - if(location.pathname.length > 1) { - // 1. 모든 'current' 클래스를 가진 요소를 선택하고 제거 - // const currentElements = document.querySelectorAll('.current'); - // currentElements.forEach(element => { - // element.classList.remove('current'); - // }); - currentList.forEach(element => { - element.page.forEach((page, index) => { - console.log(location.pathname); - if (location.pathname.includes(page)) { - const targetElement = document.getElementById(element.id); - if (targetElement) { - targetElement.classList.add('current'); - } - } - }) - }) - } else { - const targetElement = document.getElementById('menu_home'); - if (targetElement) { - targetElement.classList.add('current'); + +/** + * [핵심] Quill 에디터를 초기화하는 메인 함수입니다. + * useEditor 파라미터 값에 따라 '편집 모드'와 '읽기 모드'를 동적으로 전환합니다. + * @param {boolean} useEditor - true: 편집기 활성화, false: 읽기 전용 뷰어 활성화 + */ +function initEditor(useEditor = false) { + console.log("### initEditor 함수 실행됨! 편집 모드:", useEditor, "###"); // 이 줄을 추가! + + const editorContainer = document.querySelector('#editor'); + if (!editorContainer) return; + + if (typeof serverData !== 'undefined') { + baseData.id = serverData.id; + baseData.title = decodeURIComponent(serverData.title || ''); + baseData.content = decodeURIComponent(serverData.content || ''); + baseData.category = serverData.category; + baseData.tags = serverData.tags; + baseData.firstPostLat = serverData.firstPostLat; + baseData.firstPostLon = serverData.firstPostLon; + baseData.writeTime = serverData.writeTime; + baseData.originId = serverData.originId; + } + + getLocation(); + + try { + var Font = Quill.import('formats/font'); + Font.whitelist = ['sans-serif', 'serif', 'monospace', 'arial', 'georgia', 'comic-sans-ms', 'courier-new', 'roboto', 'playfair-display']; + Quill.register(Font, true); + Quill.register({ 'modules/table-better': QuillTableBetter }, true); + + const quillOptions = { + theme: 'snow', + modules: useEditor ? { + toolbar: { + container: [ + [{ font: Font.whitelist }], [{ 'size': ['small', false, 'large', 'huge'] }], + ['bold', 'italic', 'underline', 'strike'], [{ 'color': [] }, { 'background': [] }], + [{ 'header': 1 }, { 'header': 2 }, 'blockquote', 'code-block'], + [{ 'script': 'sub'}, { 'script': 'super' }], [{ 'list': 'ordered'}, { 'list': 'bullet' }], + [{ 'indent': '-1'}, { 'indent': '+1' }], ['link', 'image', 'video'], + ['table-better'], [{ 'direction': 'rtl' }], [{ 'align': [] }], ['clean'] + ], + handlers: { image: function() { selectLocalImage(); }, video: function() { selectLocalVideo(); } } + }, + 'table-better': { language: 'en_US', toolbarTable: true }, + keyboard: { bindings: QuillTableBetter.keyboardBindings } + } : { + toolbar: false + }, + readOnly: !useEditor + }; + + quill = new Quill(editorContainer, quillOptions); + + if (baseData.content) { + loadContent(baseData.content); } - } -} -// onbeforeunload = function () { -// var accToken = get_cookie("access") -// var refreshToken = get_cookie("refresh") -// console.log("access === " + accToken + " || " + accToken.length); -// console.log("refresh === " + refreshToken + " || " + refreshToken.length); -// -// if (accToken.length < 1) { -// document.cookie = "refresh="+ window.sessionStorage.getItem("REFRESH") + ";"; -// } -// if (refreshToken.length < 1) { -// window.sessionStorage.setItem("REFRESH",get_cookie("refresh")) -// } -// } -function sendTlg(form, type,keyword) { - console.log(form) - let data = { - 'name': form.querySelector("#name").value, - 'email': form.querySelector("#email").value, - 'message': form.querySelector("#message").value, - } - if (data.name != null && data.email != null && data.message != null && data.message.length > 0) { - if(confirm(JSON.stringify(data) + "\n해당 내용으로\n메시지 보내쉴?")) { - post(getMainPath()+"/tlg/repotToMe.bjx",type,JSON.stringify(data),keyword, function (resultData) { - alert("서버에 전달됨.") - }) + if (!useEditor) { + editorContainer.classList.add('readonly-mode'); } else { - - } - } - return false -} -function get_cookie(name) { - var value = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)'); - return value? value[2] : null; -} -function divider(key) { - return merge(padding(),key,padding()) -} -function padding() { - return "%7C%2A-%2A%7C"; -} -function merge(...args) { - return args.join(""); -} -function unformat(type , data, key) { - var even = []; - var odd = []; - data.split("").forEach(function (v,idx,full) {if(idx % 2 === 0) {even.push(v)} else {odd.push(v)}}); - switch (type) { - case "T0": return merge(odd.join(""),divider(key),even.join("")); - case "T1": return merge(odd.reverse().join(""),divider(key),even.join("")); - case "T2": return merge(odd.join(""),divider(key),even.reverse().join("")); - default: return merge(odd.reverse().join(""),divider(key),even.reverse().join("")); - } -} -function checkDebug(){ - var debug = false - try { - debug = typeof v8debug === 'object' - || /--debug|--inspect/.test(process.execArgv.join(' ')); - alert(debug) - } catch (e) { - - } - - try { - const inspector = require('inspector'); - debug = inspector.url() !== undefined; - alert(debug) - } catch (e) { - - } -} -var acpt_key = "" -function post(target,type, data, key,callBackResult) { - var httpRequest; - /* 통신에 사용 될 XMLHttpRequest 객체 정의 */ - httpRequest = new XMLHttpRequest(); - /* httpRequest의 readyState가 변화했을때 함수 실행 */ - httpRequest.onreadystatechange = () => { - /* readyState가 Done이고 응답 값이 200일 때, 받아온 response로 name과 age를 그려줌 */ - if (httpRequest.readyState === XMLHttpRequest.DONE) { - if (httpRequest.status === 200) { - callBackResult(httpRequest.response) - } else { - alert('Request Error!'); + editorContainer.classList.remove('readonly-mode'); + const titleField = document.querySelector("#title_field"); + if (titleField) { + titleField.value = baseData.title; } } + } catch (e) { + console.error("Quill initialization failed:", e); } - httpRequest.open('POST', target, true); - httpRequest.setRequestHeader("Content-Type", "text/plain"); - var odd = [] - var even = [] - var dataStr = JSON.stringify(data) - var src = dataStr.split("") - src.forEach(function (s,i,a) {if (i % 2 === 0) {even.push(s)} else {odd.push(s)}}) - httpRequest.send(btoa(JSON.stringify({ - 'data': unformat(type,data,key), - 'key':key, - 'type':type, - }))); -} -function postLogin(target,type, data, key,callBackResult) { - var httpRequest; - /* 통신에 사용 될 XMLHttpRequest 객체 정의 */ - httpRequest = new XMLHttpRequest(); - /* httpRequest의 readyState가 변화했을때 함수 실행 */ - httpRequest.onreadystatechange = () => { - /* readyState가 Done이고 응답 값이 200일 때, 받아온 response로 name과 age를 그려줌 */ - if (httpRequest.readyState === XMLHttpRequest.DONE) { - if (httpRequest.status === 200) { - try { - var data = JSON.parse(httpRequest.response) - callBackResult(data) - } catch (e) { - - } - } else { - alert('Request Error!'); - } - } - } - - httpRequest.withCredentials = true - httpRequest.open('POST', target, true); - httpRequest.setRequestHeader("Content-Type", "text/plain"); - var odd = [] - var even = [] - var dataStr = JSON.stringify(data) - var src = dataStr.split("") - src.forEach(function (s,i,a) {if (i % 2 === 0) {even.push(s)} else {odd.push(s)}}) - httpRequest.send(btoa(JSON.stringify({ - 'data': unformat(type,data,key), - 'key':key, - 'type':type, - }))); + setupControlBox(useEditor ? 'edit' : 'view'); } -function mainPath() { - console.log(`location.port >> ${location.port}`) - if ('443' === location.port) { - document.location.replace(location.protocol + "//" + location.hostname + ":" + location.port) +function selectLocalImage() { + // 이미지 URL 입력 받기 + const url = prompt("이미지 URL을 입력하거나 빈칸으로 두시면 파일 업로드를 합니다."); + + if (url) { + // URL이 입력된 경우 이미지 삽입 + const range = quill.getSelection(true); + quill.insertEmbed(range.index, 'image', url); + quill.setSelection(range.index + 1); } else { - document.location.replace(location.protocol + "//" + location.hostname) + // URL이 없거나 취소한 경우 파일 업로드 처리 + const input = document.createElement('input'); + input.setAttribute('type', 'file'); + input.setAttribute('accept', 'image/*'); + input.click(); + + input.onchange = async () => { + const file = input.files[0]; + if (file) { + const file = input.files[0]; + console.log("on selectLocalImage File", file); + if (!file || !file.type.startsWith('image/')) { + console.warn('이미지 파일만 업로드 가능합니다.'); + return; + } + uploadImage(file); + } + }; } } -function gotoWrite() { - document.location.replace(getMainPath()+"/blog/write.bs") +function uploadImage(blob) { + const formData = new FormData(); + formData.append('file', blob); + let uploadUrl = getMainPath() + "/blog/post/imageUpload.bjx"; + let imageUrl = getMainPath() + '/blog/post/images/'; + $.ajax({ + type: 'POST', + enctype: 'multipart/form-data', + url: uploadUrl, + data: formData, + dataType: 'json', + processData: false, + contentType: false, + cache: false, + timeout: 600000, + success: function (data) { + console.log(data); + imageUrl += data.fileName; + insertToEditor(imageUrl); + }, + error: function (e) { + console.error(e); + // callback('image_load_fail'); + } + }); } -function gotoModify() { - document.location.replace(getMainPath()+"/blog/modify.bs") -} +function selectLocalVideo() { + const input = document.createElement('input'); + input.setAttribute('type', 'file'); + input.setAttribute('accept', 'video/*'); + input.click(); -function gotoPuzzleUpload() { - document.location.replace(getMainPath()+"/puzzle/upload.bs") + input.onchange = () => { + const file = input.files[0]; + if (!file || !file.type.startsWith('video/')) { + alert('동영상 파일만 업로드할 수 있습니다.'); + return; + } + uploadVideo(file); + }; +} +function uploadVideo(file) { + const formData = new FormData(); + formData.append('video', file); + + fetch('/api/upload/video', { + method: 'POST', + body: formData + }) + .then(res => res.json()) + .then(result => { + if (result.url) { + const range = quill.getSelection(true); + quill.insertEmbed(range.index, 'video', result.url); + quill.setSelection(range.index + 1); + } else { + console.error('동영상 업로드 실패', result); + } + }) + .catch(err => { + console.error('업로드 중 오류', err); + }); } -function gotoSudoKuGen() { - document.location.replace(getMainPath()+"/puzzle/sudoku_gen.bs") +function insertToEditor(url) { + const range = quill.getSelection(true); + quill.insertEmbed(range.index, 'image', url); + quill.setSelection(range.index + 1); } -function gotoWhere() { - document.location.replace(getMainPath()+"/bums/where.bs") +/** + * 에디터 모드('edit' 또는 'view')에 따라 컨트롤 박스를 설정합니다. + */ +function setupControlBox(mode) { + const categoryBox = document.querySelector('.controlbox-category'); + const hashtagBox = document.querySelector('.controlbox-hashtag'); + + if (!categoryBox || !hashtagBox) return; + + if (mode === 'edit') { + categoryBox.setAttribute('onclick', 'openPopup(this)'); + hashtagBox.setAttribute('onclick', 'openPopup(this)'); + categoryBox.innerText = '카테고리 설정'; + hashtagBox.innerText = '해시태그 편집'; + fetchCategoriesAndHashtags(); + } else { + categoryBox.removeAttribute('onclick'); + hashtagBox.removeAttribute('onclick'); + categoryBox.classList.remove('btn-example'); + hashtagBox.classList.remove('btn-example'); + + categoryBox.innerHTML = `카테고리: ${baseData.category || '지정되지 않음'}`; + + hashtagBox.innerHTML = '태그: '; + if (baseData.tags && baseData.tags.length > 0) { + baseData.tags.split(',').forEach(tag => { + hashtagBox.innerHTML += `#${tag.trim()}`; + }); + } else { + hashtagBox.innerHTML += '없음'; + } + } } +/** + * 백엔드 API를 호출하여 카테고리와 해시태그 목록을 가져와 팝업을 채웁니다. + */ +function fetchCategoriesAndHashtags() { + fetch(`${getMainPath()}/blog/categories.bjx`).then(res => res.json()).then(data => { + if (data.resultCode === 0 && data.tags) { + const list = document.querySelector('#category-list'); + if(list) { + list.innerHTML = ''; + data.tags.forEach(tag => { + const el = document.createElement('span'); + el.className = 'tag-item'; + el.innerText = tag; + list.appendChild(el); + }); + } + } + }).catch(err => console.error('Error fetching categories:', err)); + + fetch(`${getMainPath()}/blog/hashtags.bjx`).then(res => res.json()).then(data => { + if (data.resultCode === 0 && data.tags) { + const list = document.querySelector('#hashtag-list'); + if(list) { + list.innerHTML = ''; + data.tags.forEach(tag => { + const el = document.createElement('span'); + el.className = 'tag-item'; + el.innerText = `#${tag}`; + list.appendChild(el); + }); + } + } + }).catch(err => console.error('Error fetching hashtags:', err)); +} + +/** + * 컨텐츠를 Quill 에디터에 로드합니다. + */ +function loadContent(content) { + try { + const delta = JSON.parse(content); + if (delta && Array.isArray(delta.ops)) { + quill.setContents(delta); + return; + } + } catch (e) { /* HTML 문자열일 경우 아래에서 처리 */ } + quill.clipboard.dangerouslyPasteHTML(content); +} + +/** + * 게시물 수정 페이지로 이동합니다. + */ +function loadEditor() { + if (baseData.id) { + location.href = `${getMainPath()}/blog/edit/${baseData.id}`; + } +} + +/** + * 작성된 게시물을 서버에 저장합니다. + */ +function save() { + const titleField = document.getElementById('title_field'); + if (titleField) { + baseData.title = encodeURIComponent(titleField.value); + } + + baseData.content = encodeURIComponent(JSON.stringify(quill.getContents())); + baseData.modifyLat = currentLat; + baseData.modifyLon = currentLon; + + const uploadUrl = `${getMainPath()}/blog/post.bjx`; + if (confirm("해당 내용으로 저장하시겠습니까?")) { + post(uploadUrl, serverData.enc, JSON.stringify(baseData), serverData.keyword, function(resultData) { + alert("저장되었습니다."); + }); + } +} + +/** + * 사용자의 현재 위치(위도, 경도)를 가져옵니다. + */ +function getLocation() { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(pos => { + currentLat = pos.coords.latitude; + currentLon = pos.coords.longitude; + if (baseData.firstPostLat === 0.0) baseData.firstPostLat = currentLat; + if (baseData.firstPostLon === 0.0) baseData.firstPostLon = currentLon; + baseData.modifyLat = currentLat; + baseData.modifyLon = currentLon; + const locationField = document.getElementById('location_field'); + if (locationField) { + locationField.textContent = `Lat: ${currentLat.toFixed(4)}, Lon: ${currentLon.toFixed(4)}`; + } + }); + } +} + +/** + * 팝업 레이어를 엽니다. + */ +function openPopup(element) { + const targetId = element.getAttribute('to'); + const popup = document.querySelector(targetId); + const overlay = document.querySelector('.dim_layer'); + if (popup && overlay) { + overlay.style.display = 'block'; + popup.style.display = 'block'; + } +} + +/** + * 팝업 레이어를 닫습니다. + */ +function closePopup() { + const overlay = document.querySelector('.dim_layer'); + if(overlay) overlay.style.display = 'none'; + document.querySelectorAll('.pop_layer').forEach(p => p.style.display = 'none'); +} + +/** + * 게시물 상세 보기 페이지로 이동합니다. + */ +function goToViewer(element) { + if (element && element.id) { + location.href = `${getMainPath()}/blog/viewer/${element.id}`; + } +} + +// ================================================================================= +// [복구] 이하 누락되었던 함수들 +// ================================================================================= + +/** + * 인기글 목록을 가져와 UI에 표시합니다. + */ +function fetchRankOfViews() { + fetch(`${getMainPath()}/blog/rankOfViews.bjx`).then(res => res.json()).then(data => { + const ul = document.querySelector('.rank_of_view'); + if (ul && data.posts) { + ul.innerHTML = ''; + data.posts.forEach(item => { + const date = new Date(item.writeTime); + const formattedDate = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`; + ul.innerHTML += `
  • ${item.title}
    [${formattedDate}]
  • `; + }); + } + }).catch(error => console.error('Failed to fetch rank of views:', error)); +} + +/** + * 최신글 목록을 가져와 UI에 표시합니다. + */ +function fetchRecentPosts() { + fetch(`${getMainPath()}/blog/recentOfPost.bjx`).then(res => res.json()).then(data => { + const ul = document.querySelector('.recent_posts'); + if (ul && data.posts) { + ul.innerHTML = ''; + data.posts.forEach(item => { + const date = new Date(item.writeTime); + const formattedDate = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`; + ul.innerHTML += `
  • ${item.title}
    [${formattedDate}]
  • `; + }); + } + }).catch(error => console.error('Failed to fetch recent posts:', error)); +} + +/** + * 로그인 폼 데이터를 서버에 전송합니다. + */ +function submitLoginForm() { + const data = { + 'user_id': $('#loginId').val(), + 'user_pw': $('#loginPassword').val(), + 'rememberMe': $('#rememberMe').is(':checked'), + }; + + postLogin(`${getMainPath()}/user/login.bjx`, serverData.enc, JSON.stringify(data), serverData.keyword, function(response) { + if (response.isOk) { + location.reload(); + } else { + alert(`로그인 실패: ${response.resultMsg}`); + } + }); +} + +// --- 페이지 이동(Navigation) 함수들 --- +function gotoHome() { document.location.replace(`${getMainPath()}/home.bs`); } +function gotoWrite() { document.location.replace(`${getMainPath()}/blog/edit`); } // 수정된 URL +function gotoModify() { document.location.replace(`${getMainPath()}/blog/posts`); } // 수정된 URL +function gotoLogin() { document.location.replace(`${getMainPath()}/login.bs`); } +function gotoJoin() { document.location.replace(`${getMainPath()}/user/join.bs`); } + +/** + * 로그아웃을 처리합니다. + */ function logout() { - // retrieve all cookies - document.cookie = "access=; expires=Thu, 01 Jan 1970 00:00:01 GMT;" - document.cookie = "refresh=; expires=Thu, 01 Jan 1970 00:00:01 GMT;" - - console.log(document.cookie["JSESSIONID"]) - document.cookie = "JSESSIONID=; expires=Thu, 01 Jan 1970 00:00:01 GMT;" - document.cookie = "CLEAR="+Date.now()+""; - let logOutUrl = getMainPath() + "/user/logout.bs"; - - alert("로그아웃 됨요~! 빠염~!") - - // 동적으로 form 생성하여 POST 요청 전송 const form = document.createElement('form'); form.method = 'POST'; - form.action = getMainPath() + '/user/logout.bs'; + form.action = `${getMainPath()}/user/logout.bs`; - // CSRF 토큰을 meta태그 등에서 얻어서 삽입 (예: ) + // Spring Security CSRF 토큰 추가 const csrfToken = document.querySelector('meta[name="_csrf"]').getAttribute('content'); const csrfParam = document.querySelector('meta[name="_csrf_parameter"]').getAttribute('content'); - const csrfInput = document.createElement('input'); - csrfInput.type = 'hidden'; - csrfInput.name = csrfParam; // 예: "_csrf" - csrfInput.value = csrfToken; - form.appendChild(csrfInput); + if (csrfToken && csrfParam) { + const csrfInput = document.createElement('input'); + csrfInput.type = 'hidden'; + csrfInput.name = csrfParam; + csrfInput.value = csrfToken; + form.appendChild(csrfInput); + } document.body.appendChild(form); form.submit(); } -function gotoHome() { - console.log(`location.port >> ${location.port}`) - location.href = getMainPath()+"/home.bs" -} - -function gotoLogin() { - console.log(`location.port >> ${location.port}`) - location.href = getMainPath()+"/login.bs" -} - -function gotoJoin() { - document.location.replace(getMainPath() + "/user/join.bs") -} - -function goToViewer(item) { - location.href = getMainPath() + "/blog/viewer/" + item.id -} - -function goToView(path,id) { - location.href = path + id; -} -function onclickLogin(type, keyword) { - let user_id = document.getElementById('user_id') - let user_pw = document.getElementById('user_pw') - let data = { - 'user_id': user_id.value, - 'user_pw': user_pw.value, - } - postLogin(getMainPath()+"/user/login.bjx",type,JSON.stringify(data),keyword, function (data) { - if (data.isOk) { - - document.cookie = "access=" + data.token.split(";")[0]+";" - // document.cookie = "refresh=" + data.refresh.split(";")[0]+";" - // window.sessionStorage.setItem("ACCESS",data.refresh.split(";")[0]) - window.sessionStorage.setItem("REFRESH",data.refresh.split(";")[0]) - document.location.replace(document.location) - } else { - if (data.resultCode === 7100) { - if(confirm(`너 누구임 정보 없는데?!\n${data.resultMsg}[${data.resultCode}]\n가입 할래!?`)){ - document.location.replace(getMainPath() + "/user/join.bs") - } - } else { - alert(`너 누구임?!\n${data.resultMsg}[${data.resultCode}]`) - } - } - }) -} - +// ================================================================================= +// 서버 통신 및 암호화 관련 유틸리티 함수들 (기존 코드 유지) +// ================================================================================= function getMainPath() { - console.log(`location.port >> ${location.port}`) - if ('443' === location.port) { - return location.protocol + "//" + location.hostname + ":" + location.port - } else { - return location.protocol + "//" + location.hostname - } + return location.protocol + "//" + location.hostname + (location.port ? ':' + location.port : ''); } -function openPopup(a) { - - var $href = $(a).attr('to'); - document.querySelectorAll('[id*=popLayer]').forEach(function (v,k,p) { - $(v).hide(); - }); - layer_popup($href); -} - -function layer_popup(el){ - var $el = $(el); //레이어의 id를 $el 변수에 저장 - - var isDim = true ;//$(document).hasClass('dimBg'); //dimmed 레이어를 감지하기 위한 boolean 변수 - isDim ? $('.dim_layer').fadeIn() : $el.fadeIn(); - $el.show() - var $elWidth = ~~($el.outerWidth()), - $elHeight = ~~($el.outerHeight()), - docWidth = $(document).width(), - docHeight = $(document).height(); - - // 화면의 중앙에 레이어를 띄운다. - if ($elHeight < docHeight || $elWidth < docWidth) { - $el.css({ - marginTop: -$elHeight /2, - marginLeft: -$elWidth/2 - }) - } else { - $el.css({top: 0, left: 0}); - } - - $el.find('a.btn_layerClose').click(function(){ - isDim ? $('.dim_layer').fadeOut() : $el.fadeOut(); // 닫기 버튼을 클릭하면 레이어가 닫힌다. - return false; - }); - - $('.dimBg').click(function(){ - $('.dim_layer').fadeOut(); - return false; - }); -} - -function urldecode(t){ - return decodeURI(t) -} - -function openLoginPopup(formType) { - - document.getElementById('overlay').style.display = 'block'; - document.getElementById('loginForm').style.display = formType === 'login' ? 'block' : 'none'; - document.getElementById('signupForm').style.display = formType === 'signup' ? 'block' : 'none'; - - if(formType === 'login') { - const loginIdInput = document.getElementById('loginId'); - loginIdInput.focus(); - } -} - -function closePopup() { - document.getElementById('overlay').style.display = 'none'; -} - -function submitLoginForm() { - let user_id = document.getElementById('loginId') - let user_pw = document.getElementById('loginPassword') - let rememberMe = document.getElementById('rememberMe') - console.log(rememberMe.value) - let data = { - 'user_id': user_id.value, - 'user_pw': user_pw.value, - 'rememberMe' : rememberMe.value === "on", - } - postLogin(getMainPath()+"/user/login.bjx",user_pw.data,JSON.stringify(data),user_pw.data, function (data) { - closePopup() - alert("data >> " + data) - if (data.isOk) { - document.location.replace(location.href) - } else { - if (data.resultCode === 7100) { - if(confirm(`너 누구임 정보 없는데?!\n${data.resultMsg}[${data.resultCode}]\n가입 할래!?`)){ - document.location.replace(getMainPath() + "/user/join") - } - } else { - alert(`너 누구임?!\n${data.resultMsg}[${data.resultCode}]`) - } +function post(target, type, data, key, callBackResult) { + const httpRequest = new XMLHttpRequest(); + httpRequest.onreadystatechange = () => { + if (httpRequest.readyState === XMLHttpRequest.DONE) { + if (httpRequest.status === 200) callBackResult(httpRequest.response); + else alert('Request Error!'); } - }) + }; + httpRequest.open('POST', target, true); + httpRequest.setRequestHeader("Content-Type", "text/plain"); + httpRequest.send(btoa(JSON.stringify({ + 'data': unformat(type, data, key), 'key': key, 'type': type, + }))); } -function isDelta(content) { - try { - // Delta는 JSON이면서 'ops'라는 키를 포함 - if (typeof content === "string") { - content = JSON.parse(content); +function postLogin(target, type, data, key, callBackResult) { + const httpRequest = new XMLHttpRequest(); + httpRequest.onreadystatechange = () => { + if (httpRequest.readyState === XMLHttpRequest.DONE) { + if (httpRequest.status === 200) { + try { + callBackResult(JSON.parse(httpRequest.response)); + } catch (e) { console.error("Login response parse error:", e); } + } else { alert('Request Error!'); } } - return typeof content === "object" && content.ops !== undefined; - } catch (e) { - return false; // JSON 파싱 실패하면 마크업(HTML)으로 간주 + }; + httpRequest.withCredentials = true; + httpRequest.open('POST', target, true); + httpRequest.setRequestHeader("Content-Type", "text/plain"); + httpRequest.send(btoa(JSON.stringify({ + 'data': unformat(type, data, key), 'key': key, 'type': type, + }))); +} + +function unformat(type, data, key) { + var even = [], odd = []; + data.split("").forEach((v, idx) => (idx % 2 === 0 ? even.push(v) : odd.push(v))); + const dividerStr = ["%7C%2A-%2A%7C", key, "%7C%2A-%2A%7C"].join(""); + switch (type) { + case "T0": + return [odd.join(""), dividerStr, even.join("")].join(""); + case "T1": + return [odd.reverse().join(""), dividerStr, even.join("")].join(""); + case "T2": + return [odd.join(""), dividerStr, even.reverse().join("")].join(""); + default: + return [odd.reverse().join(""), dividerStr, even.reverse().join("")].join(""); } } \ No newline at end of file diff --git a/src/main/resources/static/js/easy.qrcode.min.js b/src/main/resources/static/js/easy.qrcode.min.js new file mode 100644 index 0000000..1ba137c --- /dev/null +++ b/src/main/resources/static/js/easy.qrcode.min.js @@ -0,0 +1,21 @@ +/** + * EasyQRCodeJS + * + * Cross-browser QRCode generator for pure javascript. Support Canvas, SVG and Table drawing methods. Support Dot style, Logo, Background image, Colorful, Title etc. settings. Support Angular, Vue.js, React, Next.js, Svelte framework. Support binary(hex) data mode.(Running with DOM on client side) + * + * Version 4.4.10 + * + * @author [ inthinkcolor@gmail.com ] + * + * @see https://github.com/ushelp/EasyQRCodeJS + * @see http://www.easyproject.cn/easyqrcodejs/tryit.html + * @see https://github.com/ushelp/EasyQRCodeJS-NodeJS + * + * Copyright 2017 Ray, EasyProject + * Released under the MIT license + * + * [Support AMD, CMD, CommonJS/Node.js] + * + */ +!function(){"use strict";function a(a,b){var c,d=Object.keys(b);for(c=0;c1?(b=c,b.width=arguments[0],b.height=arguments[1]):b=a||c,!(this instanceof f))return new f(b);this.width=b.width||c.width,this.height=b.height||c.height,this.enableMirroring=void 0!==b.enableMirroring?b.enableMirroring:c.enableMirroring,this.canvas=this,this.__document=b.document||document,b.ctx?this.__ctx=b.ctx:(this.__canvas=this.__document.createElement("canvas"),this.__ctx=this.__canvas.getContext("2d")),this.__setDefaultStyles(),this.__stack=[this.__getStyleState()],this.__groupStack=[],this.__root=this.__document.createElementNS("http://www.w3.org/2000/svg","svg"),this.__root.setAttribute("version",1.1),this.__root.setAttribute("xmlns","http://www.w3.org/2000/svg"),this.__root.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),this.__root.setAttribute("width",this.width),this.__root.setAttribute("height",this.height),this.__ids={},this.__defs=this.__document.createElementNS("http://www.w3.org/2000/svg","defs"),this.__root.appendChild(this.__defs),this.__currentElement=this.__document.createElementNS("http://www.w3.org/2000/svg","g"),this.__root.appendChild(this.__currentElement)},f.prototype.__createElement=function(a,b,c){void 0===b&&(b={});var d,e,f=this.__document.createElementNS("http://www.w3.org/2000/svg",a),g=Object.keys(b);for(c&&(f.setAttribute("fill","none"),f.setAttribute("stroke","none")),d=0;d0){"path"===this.__currentElement.nodeName&&(this.__currentElementsToStyle||(this.__currentElementsToStyle={element:b,children:[]}),this.__currentElementsToStyle.children.push(this.__currentElement),this.__applyCurrentDefaultPath());var c=this.__createElement("g");b.appendChild(c),this.__currentElement=c}var d=this.__currentElement.getAttribute("transform");d?d+=" ":d="",d+=a,this.__currentElement.setAttribute("transform",d)},f.prototype.scale=function(b,c){void 0===c&&(c=b),this.__addTransform(a("scale({x},{y})",{x:b,y:c}))},f.prototype.rotate=function(b){var c=180*b/Math.PI;this.__addTransform(a("rotate({angle},{cx},{cy})",{angle:c,cx:0,cy:0}))},f.prototype.translate=function(b,c){this.__addTransform(a("translate({x},{y})",{x:b,y:c}))},f.prototype.transform=function(b,c,d,e,f,g){this.__addTransform(a("matrix({a},{b},{c},{d},{e},{f})",{a:b,b:c,c:d,d:e,e:f,f:g}))},f.prototype.beginPath=function(){var a,b;this.__currentDefaultPath="",this.__currentPosition={},a=this.__createElement("path",{},!0),b=this.__closestGroupOrSvg(),b.appendChild(a),this.__currentElement=a},f.prototype.__applyCurrentDefaultPath=function(){var a=this.__currentElement;"path"===a.nodeName?a.setAttribute("d",this.__currentDefaultPath):console.error("Attempted to apply path command to node",a.nodeName)},f.prototype.__addPathCommand=function(a){this.__currentDefaultPath+=" ",this.__currentDefaultPath+=a},f.prototype.moveTo=function(b,c){"path"!==this.__currentElement.nodeName&&this.beginPath(),this.__currentPosition={x:b,y:c},this.__addPathCommand(a("M {x} {y}",{x:b,y:c}))},f.prototype.closePath=function(){this.__currentDefaultPath&&this.__addPathCommand("Z")},f.prototype.lineTo=function(b,c){this.__currentPosition={x:b,y:c},this.__currentDefaultPath.indexOf("M")>-1?this.__addPathCommand(a("L {x} {y}",{x:b,y:c})):this.__addPathCommand(a("M {x} {y}",{x:b,y:c}))},f.prototype.bezierCurveTo=function(b,c,d,e,f,g){this.__currentPosition={x:f,y:g},this.__addPathCommand(a("C {cp1x} {cp1y} {cp2x} {cp2y} {x} {y}",{cp1x:b,cp1y:c,cp2x:d,cp2y:e,x:f,y:g}))},f.prototype.quadraticCurveTo=function(b,c,d,e){this.__currentPosition={x:d,y:e},this.__addPathCommand(a("Q {cpx} {cpy} {x} {y}",{cpx:b,cpy:c,x:d,y:e}))};var j=function(a){var b=Math.sqrt(a[0]*a[0]+a[1]*a[1]);return[a[0]/b,a[1]/b]};f.prototype.arcTo=function(a,b,c,d,e){var f=this.__currentPosition&&this.__currentPosition.x,g=this.__currentPosition&&this.__currentPosition.y;if(void 0!==f&&void 0!==g){if(e<0)throw new Error("IndexSizeError: The radius provided ("+e+") is negative.");if(f===a&&g===b||a===c&&b===d||0===e)return void this.lineTo(a,b);var h=j([f-a,g-b]),i=j([c-a,d-b]);if(h[0]*i[1]==h[1]*i[0])return void this.lineTo(a,b);var k=h[0]*i[0]+h[1]*i[1],l=Math.acos(Math.abs(k)),m=j([h[0]+i[0],h[1]+i[1]]),n=e/Math.sin(l/2),o=a+n*m[0],p=b+n*m[1],q=[-h[1],h[0]],r=[i[1],-i[0]],s=function(a){var b=a[0];return a[1]>=0?Math.acos(b):-Math.acos(b)},t=s(q),u=s(r);this.lineTo(o+q[0]*e,p+q[1]*e),this.arc(o,p,e,t,u)}},f.prototype.stroke=function(){"path"===this.__currentElement.nodeName&&this.__currentElement.setAttribute("paint-order","fill stroke markers"),this.__applyCurrentDefaultPath(),this.__applyStyleToCurrentElement("stroke")},f.prototype.fill=function(){"path"===this.__currentElement.nodeName&&this.__currentElement.setAttribute("paint-order","stroke fill markers"),this.__applyCurrentDefaultPath(),this.__applyStyleToCurrentElement("fill")},f.prototype.rect=function(a,b,c,d){"path"!==this.__currentElement.nodeName&&this.beginPath(),this.moveTo(a,b),this.lineTo(a+c,b),this.lineTo(a+c,b+d),this.lineTo(a,b+d),this.lineTo(a,b),this.closePath()},f.prototype.fillRect=function(a,b,c,d){var e,f;e=this.__createElement("rect",{x:a,y:b,width:c,height:d,"shape-rendering":"crispEdges"},!0),f=this.__closestGroupOrSvg(),f.appendChild(e),this.__currentElement=e,this.__applyStyleToCurrentElement("fill")},f.prototype.strokeRect=function(a,b,c,d){var e,f;e=this.__createElement("rect",{x:a,y:b,width:c,height:d},!0),f=this.__closestGroupOrSvg(),f.appendChild(e),this.__currentElement=e,this.__applyStyleToCurrentElement("stroke")},f.prototype.__clearCanvas=function(){for(var a=this.__closestGroupOrSvg(),b=a.getAttribute("transform"),c=this.__root.childNodes[1],d=c.childNodes,e=d.length-1;e>=0;e--)d[e]&&c.removeChild(d[e]);this.__currentElement=c,this.__groupStack=[],b&&this.__addTransform(b)},f.prototype.clearRect=function(a,b,c,d){if(0===a&&0===b&&c===this.width&&d===this.height)return void this.__clearCanvas();var e,f=this.__closestGroupOrSvg();e=this.__createElement("rect",{x:a,y:b,width:c,height:d,fill:"#FFFFFF"},!0),f.appendChild(e)},f.prototype.createLinearGradient=function(a,c,d,e){var f=this.__createElement("linearGradient",{id:b(this.__ids),x1:a+"px",x2:d+"px",y1:c+"px",y2:e+"px",gradientUnits:"userSpaceOnUse"},!1);return this.__defs.appendChild(f),new g(f,this)},f.prototype.createRadialGradient=function(a,c,d,e,f,h){var i=this.__createElement("radialGradient",{id:b(this.__ids),cx:e+"px",cy:f+"px",r:h+"px",fx:a+"px",fy:c+"px",gradientUnits:"userSpaceOnUse"},!1);return this.__defs.appendChild(i),new g(i,this)},f.prototype.__parseFont=function(){var a=/^\s*(?=(?:(?:[-a-z]+\s*){0,2}(italic|oblique))?)(?=(?:(?:[-a-z]+\s*){0,2}(small-caps))?)(?=(?:(?:[-a-z]+\s*){0,2}(bold(?:er)?|lighter|[1-9]00))?)(?:(?:normal|\1|\2|\3)\s*){0,3}((?:xx?-)?(?:small|large)|medium|smaller|larger|[.\d]+(?:\%|in|[cem]m|ex|p[ctx]))(?:\s*\/\s*(normal|[.\d]+(?:\%|in|[cem]m|ex|p[ctx])))?\s*([-,\'\"\sa-z0-9]+?)\s*$/i,b=a.exec(this.font),c={style:b[1]||"normal",size:b[4]||"10px",family:b[6]||"sans-serif",weight:b[3]||"normal",decoration:b[2]||"normal",href:null};return"underline"===this.__fontUnderline&&(c.decoration="underline"),this.__fontHref&&(c.href=this.__fontHref),c},f.prototype.__wrapTextLink=function(a,b){if(a.href){var c=this.__createElement("a");return c.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",a.href),c.appendChild(b),c}return b},f.prototype.__applyText=function(a,b,e,f){var g=this.__parseFont(),h=this.__closestGroupOrSvg(),i=this.__createElement("text",{"font-family":g.family,"font-size":g.size,"font-style":g.style,"font-weight":g.weight,"text-decoration":g.decoration,x:b,y:e,"text-anchor":c(this.textAlign),"dominant-baseline":d(this.textBaseline)},!0);i.appendChild(this.__document.createTextNode(a)),this.__currentElement=i,this.__applyStyleToCurrentElement(f),h.appendChild(this.__wrapTextLink(g,i))},f.prototype.fillText=function(a,b,c){this.__applyText(a,b,c,"fill")},f.prototype.strokeText=function(a,b,c){this.__applyText(a,b,c,"stroke")},f.prototype.measureText=function(a){return this.__ctx.font=this.font,this.__ctx.measureText(a)},f.prototype.arc=function(b,c,d,e,f,g){if(e!==f){e%=2*Math.PI,f%=2*Math.PI,e===f&&(f=(f+2*Math.PI-.001*(g?-1:1))%(2*Math.PI));var h=b+d*Math.cos(f),i=c+d*Math.sin(f),j=b+d*Math.cos(e),k=c+d*Math.sin(e),l=g?0:1,m=0,n=f-e;n<0&&(n+=2*Math.PI),m=g?n>Math.PI?0:1:n>Math.PI?1:0,this.lineTo(j,k),this.__addPathCommand(a("A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}",{rx:d,ry:d,xAxisRotation:0,largeArcFlag:m,sweepFlag:l,endX:h,endY:i})),this.__currentPosition={x:h,y:i}}},f.prototype.clip=function(){var c=this.__closestGroupOrSvg(),d=this.__createElement("clipPath"),e=b(this.__ids),f=this.__createElement("g");this.__applyCurrentDefaultPath(),c.removeChild(this.__currentElement),d.setAttribute("id",e),d.appendChild(this.__currentElement),this.__defs.appendChild(d),c.setAttribute("clip-path",a("url(#{id})",{id:e})),c.appendChild(f),this.__currentElement=f},f.prototype.drawImage=function(){var a,b,c,d,e,g,h,i,j,k,l,m,n,o,p=Array.prototype.slice.call(arguments),q=p[0],r=0,s=0;if(3===p.length)a=p[1],b=p[2],e=q.width,g=q.height,c=e,d=g;else if(5===p.length)a=p[1],b=p[2],c=p[3],d=p[4],e=q.width,g=q.height;else{if(9!==p.length)throw new Error("Invalid number of arguments passed to drawImage: "+arguments.length);r=p[1],s=p[2],e=p[3],g=p[4],a=p[5],b=p[6],c=p[7],d=p[8]}h=this.__closestGroupOrSvg(),this.__currentElement;var t="translate("+a+", "+b+")";if(q instanceof f){if(i=q.getSvg().cloneNode(!0),i.childNodes&&i.childNodes.length>1){for(j=i.childNodes[0];j.childNodes.length;)o=j.childNodes[0].getAttribute("id"),this.__ids[o]=o,this.__defs.appendChild(j.childNodes[0]);if(k=i.childNodes[1]){var u,v=k.getAttribute("transform");u=v?v+" "+t:t,k.setAttribute("transform",u),h.appendChild(k)}}}else"CANVAS"!==q.nodeName&&"IMG"!==q.nodeName||(l=this.__createElement("image"),l.setAttribute("width",c),l.setAttribute("height",d),l.setAttribute("preserveAspectRatio","none"),l.setAttribute("opacity",this.globalAlpha),(r||s||e!==q.width||g!==q.height)&&(m=this.__document.createElement("canvas"),m.width=c,m.height=d,n=m.getContext("2d"),n.drawImage(q,r,s,e,g,0,0,c,d),q=m),l.setAttribute("transform",t),l.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href","CANVAS"===q.nodeName?q.toDataURL():q.originalSrc),h.appendChild(l))},f.prototype.createPattern=function(a,c){var d,e=this.__document.createElementNS("http://www.w3.org/2000/svg","pattern"),g=b(this.__ids);return e.setAttribute("id",g),e.setAttribute("width",a.width),e.setAttribute("height",a.height),"CANVAS"===a.nodeName||"IMG"===a.nodeName?(d=this.__document.createElementNS("http://www.w3.org/2000/svg","image"),d.setAttribute("width",a.width),d.setAttribute("height",a.height),d.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href","CANVAS"===a.nodeName?a.toDataURL():a.getAttribute("src")),e.appendChild(d),this.__defs.appendChild(e)):a instanceof f&&(e.appendChild(a.__root.childNodes[1]),this.__defs.appendChild(e)),new h(e,this)},f.prototype.setLineDash=function(a){a&&a.length>0?this.lineDash=a.join(","):this.lineDash=null},f.prototype.drawFocusRing=function(){},f.prototype.createImageData=function(){},f.prototype.getImageData=function(){},f.prototype.putImageData=function(){},f.prototype.globalCompositeOperation=function(){},f.prototype.setTransform=function(){},"object"==typeof window&&(window.C2S=f),"object"==typeof module&&"object"==typeof module.exports&&(module.exports=f)}(),function(){"use strict";function a(a,b,c){this.mode=q.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var d=0,e=this.data.length;d65536?(f[0]=240|(1835008&g)>>>18,f[1]=128|(258048&g)>>>12,f[2]=128|(4032&g)>>>6,f[3]=128|63&g):g>2048?(f[0]=224|(61440&g)>>>12,f[1]=128|(4032&g)>>>6,f[2]=128|63&g):g>128?(f[0]=192|(1984&g)>>>6,f[1]=128|63&g):f[0]=g,this.parsedData.push(f)}this.parsedData=Array.prototype.concat.apply([],this.parsedData),c||this.parsedData.length==this.data.length||(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function c(a,b){if(a.length==i)throw new Error(a.length+"/"+b);for(var c=0;cw.length)throw new Error("Too long data. the CorrectLevel."+["M","L","H","Q"][c]+" limit length is "+i);return 0!=b.version&&(d<=b.version?(d=b.version,b.runVersion=d):(console.warn("QR Code version "+b.version+" too small, run version use "+d),b.runVersion=d)),d}function h(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a.length?3:0)}var i,j,k="object"==typeof global&&global&&global.Object===Object&&global,l="object"==typeof self&&self&&self.Object===Object&&self,m=k||l||Function("return this")(),n="object"==typeof exports&&exports&&!exports.nodeType&&exports,o=n&&"object"==typeof module&&module&&!module.nodeType&&module,p=m.QRCode;a.prototype={getLength:function(a){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;b=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b,c){for(var d=-1;d<=7;d++)if(!(a+d<=-1||this.moduleCount<=a+d))for(var e=-1;e<=7;e++)b+e<=-1||this.moduleCount<=b+e||(0<=d&&d<=6&&(0==e||6==e)||0<=e&&e<=6&&(0==d||6==d)||2<=d&&d<=4&&2<=e&&e<=4?(this.modules[a+d][b+e][0]=!0,this.modules[a+d][b+e][2]=c,this.modules[a+d][b+e][1]=-0==d||-0==e||6==d||6==e?"O":"I"):this.modules[a+d][b+e][0]=!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;c<8;c++){this.makeImpl(!0,c);var d=t.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c);this.make();for(var e=0;e>c&1);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3][0]=d}for(var c=0;c<18;c++){var d=!a&&1==(b>>c&1);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)][0]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=t.getBCHTypeInfo(c),e=0;e<15;e++){var f=!a&&1==(d>>e&1);e<6?this.modules[e][8][0]=f:e<8?this.modules[e+1][8][0]=f:this.modules[this.moduleCount-15+e][8][0]=f}for(var e=0;e<15;e++){var f=!a&&1==(d>>e&1);e<8?this.modules[8][this.moduleCount-e-1][0]=f:e<9?this.modules[8][15-e-1+1][0]=f:this.modules[8][15-e-1][0]=f}this.modules[this.moduleCount-8][8][0]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,f=0,g=this.moduleCount-1;g>0;g-=2)for(6==g&&g--;;){for(var h=0;h<2;h++)if(null==this.modules[d][g-h][0]){var i=!1;f>>e&1));var j=t.getMask(b,d,g-h);j&&(i=!i),this.modules[d][g-h][0]=i,e--,-1==e&&(f++,e=7)}if((d+=c)<0||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,f){for(var g=d.getRSBlocks(a,c),h=new e,i=0;i8*k)throw new Error("code length overflow. ("+h.getLengthInBits()+">"+8*k+")");for(h.getLengthInBits()+4<=8*k&&h.put(0,4);h.getLengthInBits()%8!=0;)h.putBit(!1);for(;;){if(h.getLengthInBits()>=8*k)break;if(h.put(b.PAD0,8),h.getLengthInBits()>=8*k)break;h.put(b.PAD1,8)}return b.createBytes(h,g)},b.createBytes=function(a,b){for(var d=0,e=0,f=0,g=new Array(b.length),h=new Array(b.length),i=0;i=0?o.get(p):0}}for(var q=0,l=0;l=0;)b^=t.G15<=0;)b^=t.G18<>>=1;return b},getPatternPosition:function(a){return t.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case s.PATTERN000:return(b+c)%2==0;case s.PATTERN001:return b%2==0;case s.PATTERN010:return c%3==0;case s.PATTERN011:return(b+c)%3==0;case s.PATTERN100:return(Math.floor(b/2)+Math.floor(c/3))%2==0;case s.PATTERN101:return b*c%2+b*c%3==0;case s.PATTERN110:return(b*c%2+b*c%3)%2==0;case s.PATTERN111:return(b*c%3+(b+c)%2)%2==0;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new c([1],0),d=0;d5&&(c+=3+f-5)}for(var d=0;d=256;)a-=255;return u.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},v=0;v<8;v++)u.EXP_TABLE[v]=1<>>7-a%8&1)},put:function(a,b){for(var c=0;c>>b-c-1&1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var w=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],x=function(){return"undefined"!=typeof CanvasRenderingContext2D}()?function(){function a(){if("svg"==this._htOption.drawer){var a=this._oContext.getSerializedSvg(!0);this.dataURL=a,this._el.innerHTML=a}else try{var b=this._elCanvas.toDataURL("image/png");this.dataURL=b}catch(a){console.error(a)}this._htOption.onRenderingEnd&&(this.dataURL||console.error("Can not get base64 data, please check: 1. Published the page and image to the server 2. The image request support CORS 3. Configured `crossOrigin:'anonymous'` option"),this._htOption.onRenderingEnd(this._htOption,this.dataURL))}function b(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&c._fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,void(d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==")}!0===c._bSupportDataURI&&c._fSuccess?c._fSuccess.call(c):!1===c._bSupportDataURI&&c._fFail&&c._fFail.call(c)}if(m._android&&m._android<=2.1){var c=1/window.devicePixelRatio,d=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,b,e,f,g,h,i,j,k){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*c;else void 0===j&&(arguments[1]*=c,arguments[2]*=c,arguments[3]*=c,arguments[4]*=c);d.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=f(),this._el=a,this._htOption=b,"svg"==this._htOption.drawer?(this._oContext={},this._elCanvas={}):(this._elCanvas=document.createElement("canvas"),this._el.appendChild(this._elCanvas),this._oContext=this._elCanvas.getContext("2d")),this._bSupportDataURI=null,this.dataURL=null};return e.prototype.draw=function(a){function b(){d.quietZone>0&&d.quietZoneColor&&(h.lineWidth=0,h.fillStyle=d.quietZoneColor,h.fillRect(0,0,i._elCanvas.width,d.quietZone),h.fillRect(0,d.quietZone,d.quietZone,i._elCanvas.height-2*d.quietZone),h.fillRect(i._elCanvas.width-d.quietZone,d.quietZone,d.quietZone,i._elCanvas.height-2*d.quietZone),h.fillRect(0,i._elCanvas.height-d.quietZone,i._elCanvas.width,d.quietZone))}function c(a){function c(a){var c=Math.round(d.width/3.5),e=Math.round(d.height/3.5);c!==e&&(c=e),d.logoMaxWidth?c=Math.round(d.logoMaxWidth):d.logoWidth&&(c=Math.round(d.logoWidth)),d.logoMaxHeight?e=Math.round(d.logoMaxHeight):d.logoHeight&&(e=Math.round(d.logoHeight));var f,g;void 0===a.naturalWidth?(f=a.width,g=a.height):(f=a.naturalWidth,g=a.naturalHeight),(d.logoMaxWidth||d.logoMaxHeight)&&(d.logoMaxWidth&&f<=c&&(c=f),d.logoMaxHeight&&g<=e&&(e=g),f<=c&&g<=e&&(c=f,e=g));var i=(d.width+2*d.quietZone-c)/2,j=(d.height+d.titleHeight+2*d.quietZone-e)/2,k=Math.min(c/f,e/g),l=f*k,m=g*k;(d.logoMaxWidth||d.logoMaxHeight)&&(c=l,e=m,i=(d.width+2*d.quietZone-c)/2,j=(d.height+d.titleHeight+2*d.quietZone-e)/2),d.logoBackgroundTransparent||(h.fillStyle=d.logoBackgroundColor,h.fillRect(i,j,c,e));var n=h.imageSmoothingQuality,o=h.imageSmoothingEnabled;h.imageSmoothingEnabled=!0,h.imageSmoothingQuality="high",h.drawImage(a,i+(c-l)/2,j+(e-m)/2,l,m),h.imageSmoothingEnabled=o,h.imageSmoothingQuality=n,b(),s._bIsPainted=!0,s.makeImage()}d.onRenderingStart&&d.onRenderingStart(d);for(var i=0;i
    ';g.push(m)}if(b.quietZone&&(h="display:inline-block; width:"+(b.width+2*b.quietZone)+"px; height:"+(b.width+2*b.quietZone)+"px;background:"+b.quietZoneColor+"; text-align:center;"),g.push('
    '),g.push(''),g.push('");for(var p=0;p');for(var q=0;q')}else{var v=b.colorDark;6==p?(v=b.timing_H||b.timing||k,g.push('')):6==q?(v=b.timing_V||b.timing||k,g.push('')):g.push('')}}g.push("")}if(g.push("
    '),b.title){var n=b.titleColor,o=b.titleFont;g.push('
    '+b.title+"
    ")}b.subTitle&&g.push('
    '+b.subTitle+"
    "),g.push("
    "),g.push("
    "),b.logo){var w=new Image;null!=b.crossOrigin&&(w.crossOrigin=b.crossOrigin),w.src=b.logo;var x=b.width/3.5,y=b.height/3.5;x!=y&&(x=y),b.logoWidth&&(x=b.logoWidth),b.logoHeight&&(y=b.logoHeight);var z="position:relative; z-index:1;display:table-cell;top:-"+((b.height-b.titleHeight)/2+y/2+b.quietZone)+"px;text-align:center; width:"+x+"px; height:"+y+"px;line-height:"+x+"px; vertical-align: middle;";b.logoBackgroundTransparent||(z+="background:"+b.logoBackgroundColor),g.push('
    ')}b.onRenderingStart&&b.onRenderingStart(b),c.innerHTML=g.join("");var A=c.childNodes[0],B=(b.width-A.offsetWidth)/2,C=(b.height-A.offsetHeight)/2;B>0&&C>0&&(A.style.margin=C+"px "+B+"px"),this._htOption.onRenderingEnd&&this._htOption.onRenderingEnd(this._htOption,null)},a.prototype.clear=function(){this._el.innerHTML=""},a}();j=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:r.H,dotScale:1,dotScaleTiming:1,dotScaleTiming_H:i,dotScaleTiming_V:i,dotScaleA:1,dotScaleAO:i,dotScaleAI:i,quietZone:0,quietZoneColor:"rgba(0,0,0,0)",title:"",titleFont:"normal normal bold 16px Arial",titleColor:"#000000",titleBackgroundColor:"#ffffff",titleHeight:0,titleTop:30,subTitle:"",subTitleFont:"normal normal normal 14px Arial",subTitleColor:"#4F4F4F",subTitleTop:60,logo:i,logoWidth:i,logoHeight:i,logoMaxWidth:i,logoMaxHeight:i,logoBackgroundColor:"#ffffff",logoBackgroundTransparent:!1,PO:i,PI:i,PO_TL:i,PI_TL:i,PO_TR:i,PI_TR:i,PO_BL:i,PI_BL:i,AO:i,AI:i,timing:i,timing_H:i,timing_V:i,backgroundImage:i,backgroundImageAlpha:1,autoColor:!1,autoColorDark:"rgba(0, 0, 0, .6)",autoColorLight:"rgba(255, 255, 255, .7)",onRenderingStart:i,onRenderingEnd:i,version:0,tooltip:!1,binary:!1,drawer:"canvas",crossOrigin:null,utf8WithoutBOM:!0},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];(this._htOption.version<0||this._htOption.version>40)&&(console.warn("QR Code version '"+this._htOption.version+"' is invalidate, reset to 0"),this._htOption.version=0),(this._htOption.dotScale<0||this._htOption.dotScale>1)&&(console.warn(this._htOption.dotScale+" , is invalidate, dotScale must greater than 0, less than or equal to 1, now reset to 1. "),this._htOption.dotScale=1),(this._htOption.dotScaleTiming<0||this._htOption.dotScaleTiming>1)&&(console.warn(this._htOption.dotScaleTiming+" , is invalidate, dotScaleTiming must greater than 0, less than or equal to 1, now reset to 1. "),this._htOption.dotScaleTiming=1),this._htOption.dotScaleTiming_H?(this._htOption.dotScaleTiming_H<0||this._htOption.dotScaleTiming_H>1)&&(console.warn(this._htOption.dotScaleTiming_H+" , is invalidate, dotScaleTiming_H must greater than 0, less than or equal to 1, now reset to 1. "),this._htOption.dotScaleTiming_H=1):this._htOption.dotScaleTiming_H=this._htOption.dotScaleTiming,this._htOption.dotScaleTiming_V?(this._htOption.dotScaleTiming_V<0||this._htOption.dotScaleTiming_V>1)&&(console.warn(this._htOption.dotScaleTiming_V+" , is invalidate, dotScaleTiming_V must greater than 0, less than or equal to 1, now reset to 1. "),this._htOption.dotScaleTiming_V=1):this._htOption.dotScaleTiming_V=this._htOption.dotScaleTiming,(this._htOption.dotScaleA<0||this._htOption.dotScaleA>1)&&(console.warn(this._htOption.dotScaleA+" , is invalidate, dotScaleA must greater than 0, less than or equal to 1, now reset to 1. "),this._htOption.dotScaleA=1),this._htOption.dotScaleAO?(this._htOption.dotScaleAO<0||this._htOption.dotScaleAO>1)&&(console.warn(this._htOption.dotScaleAO+" , is invalidate, dotScaleAO must greater than 0, less than or equal to 1, now reset to 1. "),this._htOption.dotScaleAO=1):this._htOption.dotScaleAO=this._htOption.dotScaleA,this._htOption.dotScaleAI?(this._htOption.dotScaleAI<0||this._htOption.dotScaleAI>1)&&(console.warn(this._htOption.dotScaleAI+" , is invalidate, dotScaleAI must greater than 0, less than or equal to 1, now reset to 1. "),this._htOption.dotScaleAI=1):this._htOption.dotScaleAI=this._htOption.dotScaleA,(this._htOption.backgroundImageAlpha<0||this._htOption.backgroundImageAlpha>1)&&(console.warn(this._htOption.backgroundImageAlpha+" , is invalidate, backgroundImageAlpha must between 0 and 1, now reset to 1. "),this._htOption.backgroundImageAlpha=1),this._htOption.height=this._htOption.height+this._htOption.titleHeight,"string"==typeof a&&(a=document.getElementById(a)),(!this._htOption.drawer||"svg"!=this._htOption.drawer&&"canvas"!=this._htOption.drawer)&&(this._htOption.drawer="canvas"),this._android=f(),this._el=a,this._oQRCode=null;var d={};for(var c in this._htOption)d[c]=this._htOption[c];this._oDrawing=new x(this._el,d),this._htOption.text&&this.makeCode(this._htOption.text)},j.prototype.makeCode=function(a){this._oQRCode=new b(g(a,this._htOption),this._htOption.correctLevel),this._oQRCode.addData(a,this._htOption.binary,this._htOption.utf8WithoutBOM),this._oQRCode.make(),this._htOption.tooltip&&(this._el.title=a),this._oDrawing.draw(this._oQRCode)},j.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},j.prototype.clear=function(){this._oDrawing.remove()},j.prototype.resize=function(a,b){this._oDrawing._htOption.width=a,this._oDrawing._htOption.height=b,this._oDrawing.draw(this._oQRCode)},j.prototype.noConflict=function(){return m.QRCode===this&&(m.QRCode=p),j},j.CorrectLevel=r,"function"==typeof define&&(define.amd||define.cmd)?define([],function(){return j}):o?((o.exports=j).QRCode=j,n.QRCode=j):m.QRCode=j}.call(this); \ No newline at end of file diff --git a/src/main/resources/static/js/easy.qrcode.min.txt b/src/main/resources/static/js/easy.qrcode.min.txt new file mode 100644 index 0000000..1ba137c --- /dev/null +++ b/src/main/resources/static/js/easy.qrcode.min.txt @@ -0,0 +1,21 @@ +/** + * EasyQRCodeJS + * + * Cross-browser QRCode generator for pure javascript. Support Canvas, SVG and Table drawing methods. Support Dot style, Logo, Background image, Colorful, Title etc. settings. Support Angular, Vue.js, React, Next.js, Svelte framework. Support binary(hex) data mode.(Running with DOM on client side) + * + * Version 4.4.10 + * + * @author [ inthinkcolor@gmail.com ] + * + * @see https://github.com/ushelp/EasyQRCodeJS + * @see http://www.easyproject.cn/easyqrcodejs/tryit.html + * @see https://github.com/ushelp/EasyQRCodeJS-NodeJS + * + * Copyright 2017 Ray, EasyProject + * Released under the MIT license + * + * [Support AMD, CMD, CommonJS/Node.js] + * + */ +!function(){"use strict";function a(a,b){var c,d=Object.keys(b);for(c=0;c1?(b=c,b.width=arguments[0],b.height=arguments[1]):b=a||c,!(this instanceof f))return new f(b);this.width=b.width||c.width,this.height=b.height||c.height,this.enableMirroring=void 0!==b.enableMirroring?b.enableMirroring:c.enableMirroring,this.canvas=this,this.__document=b.document||document,b.ctx?this.__ctx=b.ctx:(this.__canvas=this.__document.createElement("canvas"),this.__ctx=this.__canvas.getContext("2d")),this.__setDefaultStyles(),this.__stack=[this.__getStyleState()],this.__groupStack=[],this.__root=this.__document.createElementNS("http://www.w3.org/2000/svg","svg"),this.__root.setAttribute("version",1.1),this.__root.setAttribute("xmlns","http://www.w3.org/2000/svg"),this.__root.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),this.__root.setAttribute("width",this.width),this.__root.setAttribute("height",this.height),this.__ids={},this.__defs=this.__document.createElementNS("http://www.w3.org/2000/svg","defs"),this.__root.appendChild(this.__defs),this.__currentElement=this.__document.createElementNS("http://www.w3.org/2000/svg","g"),this.__root.appendChild(this.__currentElement)},f.prototype.__createElement=function(a,b,c){void 0===b&&(b={});var d,e,f=this.__document.createElementNS("http://www.w3.org/2000/svg",a),g=Object.keys(b);for(c&&(f.setAttribute("fill","none"),f.setAttribute("stroke","none")),d=0;d0){"path"===this.__currentElement.nodeName&&(this.__currentElementsToStyle||(this.__currentElementsToStyle={element:b,children:[]}),this.__currentElementsToStyle.children.push(this.__currentElement),this.__applyCurrentDefaultPath());var c=this.__createElement("g");b.appendChild(c),this.__currentElement=c}var d=this.__currentElement.getAttribute("transform");d?d+=" ":d="",d+=a,this.__currentElement.setAttribute("transform",d)},f.prototype.scale=function(b,c){void 0===c&&(c=b),this.__addTransform(a("scale({x},{y})",{x:b,y:c}))},f.prototype.rotate=function(b){var c=180*b/Math.PI;this.__addTransform(a("rotate({angle},{cx},{cy})",{angle:c,cx:0,cy:0}))},f.prototype.translate=function(b,c){this.__addTransform(a("translate({x},{y})",{x:b,y:c}))},f.prototype.transform=function(b,c,d,e,f,g){this.__addTransform(a("matrix({a},{b},{c},{d},{e},{f})",{a:b,b:c,c:d,d:e,e:f,f:g}))},f.prototype.beginPath=function(){var a,b;this.__currentDefaultPath="",this.__currentPosition={},a=this.__createElement("path",{},!0),b=this.__closestGroupOrSvg(),b.appendChild(a),this.__currentElement=a},f.prototype.__applyCurrentDefaultPath=function(){var a=this.__currentElement;"path"===a.nodeName?a.setAttribute("d",this.__currentDefaultPath):console.error("Attempted to apply path command to node",a.nodeName)},f.prototype.__addPathCommand=function(a){this.__currentDefaultPath+=" ",this.__currentDefaultPath+=a},f.prototype.moveTo=function(b,c){"path"!==this.__currentElement.nodeName&&this.beginPath(),this.__currentPosition={x:b,y:c},this.__addPathCommand(a("M {x} {y}",{x:b,y:c}))},f.prototype.closePath=function(){this.__currentDefaultPath&&this.__addPathCommand("Z")},f.prototype.lineTo=function(b,c){this.__currentPosition={x:b,y:c},this.__currentDefaultPath.indexOf("M")>-1?this.__addPathCommand(a("L {x} {y}",{x:b,y:c})):this.__addPathCommand(a("M {x} {y}",{x:b,y:c}))},f.prototype.bezierCurveTo=function(b,c,d,e,f,g){this.__currentPosition={x:f,y:g},this.__addPathCommand(a("C {cp1x} {cp1y} {cp2x} {cp2y} {x} {y}",{cp1x:b,cp1y:c,cp2x:d,cp2y:e,x:f,y:g}))},f.prototype.quadraticCurveTo=function(b,c,d,e){this.__currentPosition={x:d,y:e},this.__addPathCommand(a("Q {cpx} {cpy} {x} {y}",{cpx:b,cpy:c,x:d,y:e}))};var j=function(a){var b=Math.sqrt(a[0]*a[0]+a[1]*a[1]);return[a[0]/b,a[1]/b]};f.prototype.arcTo=function(a,b,c,d,e){var f=this.__currentPosition&&this.__currentPosition.x,g=this.__currentPosition&&this.__currentPosition.y;if(void 0!==f&&void 0!==g){if(e<0)throw new Error("IndexSizeError: The radius provided ("+e+") is negative.");if(f===a&&g===b||a===c&&b===d||0===e)return void this.lineTo(a,b);var h=j([f-a,g-b]),i=j([c-a,d-b]);if(h[0]*i[1]==h[1]*i[0])return void this.lineTo(a,b);var k=h[0]*i[0]+h[1]*i[1],l=Math.acos(Math.abs(k)),m=j([h[0]+i[0],h[1]+i[1]]),n=e/Math.sin(l/2),o=a+n*m[0],p=b+n*m[1],q=[-h[1],h[0]],r=[i[1],-i[0]],s=function(a){var b=a[0];return a[1]>=0?Math.acos(b):-Math.acos(b)},t=s(q),u=s(r);this.lineTo(o+q[0]*e,p+q[1]*e),this.arc(o,p,e,t,u)}},f.prototype.stroke=function(){"path"===this.__currentElement.nodeName&&this.__currentElement.setAttribute("paint-order","fill stroke markers"),this.__applyCurrentDefaultPath(),this.__applyStyleToCurrentElement("stroke")},f.prototype.fill=function(){"path"===this.__currentElement.nodeName&&this.__currentElement.setAttribute("paint-order","stroke fill markers"),this.__applyCurrentDefaultPath(),this.__applyStyleToCurrentElement("fill")},f.prototype.rect=function(a,b,c,d){"path"!==this.__currentElement.nodeName&&this.beginPath(),this.moveTo(a,b),this.lineTo(a+c,b),this.lineTo(a+c,b+d),this.lineTo(a,b+d),this.lineTo(a,b),this.closePath()},f.prototype.fillRect=function(a,b,c,d){var e,f;e=this.__createElement("rect",{x:a,y:b,width:c,height:d,"shape-rendering":"crispEdges"},!0),f=this.__closestGroupOrSvg(),f.appendChild(e),this.__currentElement=e,this.__applyStyleToCurrentElement("fill")},f.prototype.strokeRect=function(a,b,c,d){var e,f;e=this.__createElement("rect",{x:a,y:b,width:c,height:d},!0),f=this.__closestGroupOrSvg(),f.appendChild(e),this.__currentElement=e,this.__applyStyleToCurrentElement("stroke")},f.prototype.__clearCanvas=function(){for(var a=this.__closestGroupOrSvg(),b=a.getAttribute("transform"),c=this.__root.childNodes[1],d=c.childNodes,e=d.length-1;e>=0;e--)d[e]&&c.removeChild(d[e]);this.__currentElement=c,this.__groupStack=[],b&&this.__addTransform(b)},f.prototype.clearRect=function(a,b,c,d){if(0===a&&0===b&&c===this.width&&d===this.height)return void this.__clearCanvas();var e,f=this.__closestGroupOrSvg();e=this.__createElement("rect",{x:a,y:b,width:c,height:d,fill:"#FFFFFF"},!0),f.appendChild(e)},f.prototype.createLinearGradient=function(a,c,d,e){var f=this.__createElement("linearGradient",{id:b(this.__ids),x1:a+"px",x2:d+"px",y1:c+"px",y2:e+"px",gradientUnits:"userSpaceOnUse"},!1);return this.__defs.appendChild(f),new g(f,this)},f.prototype.createRadialGradient=function(a,c,d,e,f,h){var i=this.__createElement("radialGradient",{id:b(this.__ids),cx:e+"px",cy:f+"px",r:h+"px",fx:a+"px",fy:c+"px",gradientUnits:"userSpaceOnUse"},!1);return this.__defs.appendChild(i),new g(i,this)},f.prototype.__parseFont=function(){var a=/^\s*(?=(?:(?:[-a-z]+\s*){0,2}(italic|oblique))?)(?=(?:(?:[-a-z]+\s*){0,2}(small-caps))?)(?=(?:(?:[-a-z]+\s*){0,2}(bold(?:er)?|lighter|[1-9]00))?)(?:(?:normal|\1|\2|\3)\s*){0,3}((?:xx?-)?(?:small|large)|medium|smaller|larger|[.\d]+(?:\%|in|[cem]m|ex|p[ctx]))(?:\s*\/\s*(normal|[.\d]+(?:\%|in|[cem]m|ex|p[ctx])))?\s*([-,\'\"\sa-z0-9]+?)\s*$/i,b=a.exec(this.font),c={style:b[1]||"normal",size:b[4]||"10px",family:b[6]||"sans-serif",weight:b[3]||"normal",decoration:b[2]||"normal",href:null};return"underline"===this.__fontUnderline&&(c.decoration="underline"),this.__fontHref&&(c.href=this.__fontHref),c},f.prototype.__wrapTextLink=function(a,b){if(a.href){var c=this.__createElement("a");return c.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",a.href),c.appendChild(b),c}return b},f.prototype.__applyText=function(a,b,e,f){var g=this.__parseFont(),h=this.__closestGroupOrSvg(),i=this.__createElement("text",{"font-family":g.family,"font-size":g.size,"font-style":g.style,"font-weight":g.weight,"text-decoration":g.decoration,x:b,y:e,"text-anchor":c(this.textAlign),"dominant-baseline":d(this.textBaseline)},!0);i.appendChild(this.__document.createTextNode(a)),this.__currentElement=i,this.__applyStyleToCurrentElement(f),h.appendChild(this.__wrapTextLink(g,i))},f.prototype.fillText=function(a,b,c){this.__applyText(a,b,c,"fill")},f.prototype.strokeText=function(a,b,c){this.__applyText(a,b,c,"stroke")},f.prototype.measureText=function(a){return this.__ctx.font=this.font,this.__ctx.measureText(a)},f.prototype.arc=function(b,c,d,e,f,g){if(e!==f){e%=2*Math.PI,f%=2*Math.PI,e===f&&(f=(f+2*Math.PI-.001*(g?-1:1))%(2*Math.PI));var h=b+d*Math.cos(f),i=c+d*Math.sin(f),j=b+d*Math.cos(e),k=c+d*Math.sin(e),l=g?0:1,m=0,n=f-e;n<0&&(n+=2*Math.PI),m=g?n>Math.PI?0:1:n>Math.PI?1:0,this.lineTo(j,k),this.__addPathCommand(a("A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}",{rx:d,ry:d,xAxisRotation:0,largeArcFlag:m,sweepFlag:l,endX:h,endY:i})),this.__currentPosition={x:h,y:i}}},f.prototype.clip=function(){var c=this.__closestGroupOrSvg(),d=this.__createElement("clipPath"),e=b(this.__ids),f=this.__createElement("g");this.__applyCurrentDefaultPath(),c.removeChild(this.__currentElement),d.setAttribute("id",e),d.appendChild(this.__currentElement),this.__defs.appendChild(d),c.setAttribute("clip-path",a("url(#{id})",{id:e})),c.appendChild(f),this.__currentElement=f},f.prototype.drawImage=function(){var a,b,c,d,e,g,h,i,j,k,l,m,n,o,p=Array.prototype.slice.call(arguments),q=p[0],r=0,s=0;if(3===p.length)a=p[1],b=p[2],e=q.width,g=q.height,c=e,d=g;else if(5===p.length)a=p[1],b=p[2],c=p[3],d=p[4],e=q.width,g=q.height;else{if(9!==p.length)throw new Error("Invalid number of arguments passed to drawImage: "+arguments.length);r=p[1],s=p[2],e=p[3],g=p[4],a=p[5],b=p[6],c=p[7],d=p[8]}h=this.__closestGroupOrSvg(),this.__currentElement;var t="translate("+a+", "+b+")";if(q instanceof f){if(i=q.getSvg().cloneNode(!0),i.childNodes&&i.childNodes.length>1){for(j=i.childNodes[0];j.childNodes.length;)o=j.childNodes[0].getAttribute("id"),this.__ids[o]=o,this.__defs.appendChild(j.childNodes[0]);if(k=i.childNodes[1]){var u,v=k.getAttribute("transform");u=v?v+" "+t:t,k.setAttribute("transform",u),h.appendChild(k)}}}else"CANVAS"!==q.nodeName&&"IMG"!==q.nodeName||(l=this.__createElement("image"),l.setAttribute("width",c),l.setAttribute("height",d),l.setAttribute("preserveAspectRatio","none"),l.setAttribute("opacity",this.globalAlpha),(r||s||e!==q.width||g!==q.height)&&(m=this.__document.createElement("canvas"),m.width=c,m.height=d,n=m.getContext("2d"),n.drawImage(q,r,s,e,g,0,0,c,d),q=m),l.setAttribute("transform",t),l.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href","CANVAS"===q.nodeName?q.toDataURL():q.originalSrc),h.appendChild(l))},f.prototype.createPattern=function(a,c){var d,e=this.__document.createElementNS("http://www.w3.org/2000/svg","pattern"),g=b(this.__ids);return e.setAttribute("id",g),e.setAttribute("width",a.width),e.setAttribute("height",a.height),"CANVAS"===a.nodeName||"IMG"===a.nodeName?(d=this.__document.createElementNS("http://www.w3.org/2000/svg","image"),d.setAttribute("width",a.width),d.setAttribute("height",a.height),d.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href","CANVAS"===a.nodeName?a.toDataURL():a.getAttribute("src")),e.appendChild(d),this.__defs.appendChild(e)):a instanceof f&&(e.appendChild(a.__root.childNodes[1]),this.__defs.appendChild(e)),new h(e,this)},f.prototype.setLineDash=function(a){a&&a.length>0?this.lineDash=a.join(","):this.lineDash=null},f.prototype.drawFocusRing=function(){},f.prototype.createImageData=function(){},f.prototype.getImageData=function(){},f.prototype.putImageData=function(){},f.prototype.globalCompositeOperation=function(){},f.prototype.setTransform=function(){},"object"==typeof window&&(window.C2S=f),"object"==typeof module&&"object"==typeof module.exports&&(module.exports=f)}(),function(){"use strict";function a(a,b,c){this.mode=q.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var d=0,e=this.data.length;d65536?(f[0]=240|(1835008&g)>>>18,f[1]=128|(258048&g)>>>12,f[2]=128|(4032&g)>>>6,f[3]=128|63&g):g>2048?(f[0]=224|(61440&g)>>>12,f[1]=128|(4032&g)>>>6,f[2]=128|63&g):g>128?(f[0]=192|(1984&g)>>>6,f[1]=128|63&g):f[0]=g,this.parsedData.push(f)}this.parsedData=Array.prototype.concat.apply([],this.parsedData),c||this.parsedData.length==this.data.length||(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function c(a,b){if(a.length==i)throw new Error(a.length+"/"+b);for(var c=0;cw.length)throw new Error("Too long data. the CorrectLevel."+["M","L","H","Q"][c]+" limit length is "+i);return 0!=b.version&&(d<=b.version?(d=b.version,b.runVersion=d):(console.warn("QR Code version "+b.version+" too small, run version use "+d),b.runVersion=d)),d}function h(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a.length?3:0)}var i,j,k="object"==typeof global&&global&&global.Object===Object&&global,l="object"==typeof self&&self&&self.Object===Object&&self,m=k||l||Function("return this")(),n="object"==typeof exports&&exports&&!exports.nodeType&&exports,o=n&&"object"==typeof module&&module&&!module.nodeType&&module,p=m.QRCode;a.prototype={getLength:function(a){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;b=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b,c){for(var d=-1;d<=7;d++)if(!(a+d<=-1||this.moduleCount<=a+d))for(var e=-1;e<=7;e++)b+e<=-1||this.moduleCount<=b+e||(0<=d&&d<=6&&(0==e||6==e)||0<=e&&e<=6&&(0==d||6==d)||2<=d&&d<=4&&2<=e&&e<=4?(this.modules[a+d][b+e][0]=!0,this.modules[a+d][b+e][2]=c,this.modules[a+d][b+e][1]=-0==d||-0==e||6==d||6==e?"O":"I"):this.modules[a+d][b+e][0]=!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;c<8;c++){this.makeImpl(!0,c);var d=t.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c);this.make();for(var e=0;e>c&1);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3][0]=d}for(var c=0;c<18;c++){var d=!a&&1==(b>>c&1);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)][0]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=t.getBCHTypeInfo(c),e=0;e<15;e++){var f=!a&&1==(d>>e&1);e<6?this.modules[e][8][0]=f:e<8?this.modules[e+1][8][0]=f:this.modules[this.moduleCount-15+e][8][0]=f}for(var e=0;e<15;e++){var f=!a&&1==(d>>e&1);e<8?this.modules[8][this.moduleCount-e-1][0]=f:e<9?this.modules[8][15-e-1+1][0]=f:this.modules[8][15-e-1][0]=f}this.modules[this.moduleCount-8][8][0]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,f=0,g=this.moduleCount-1;g>0;g-=2)for(6==g&&g--;;){for(var h=0;h<2;h++)if(null==this.modules[d][g-h][0]){var i=!1;f>>e&1));var j=t.getMask(b,d,g-h);j&&(i=!i),this.modules[d][g-h][0]=i,e--,-1==e&&(f++,e=7)}if((d+=c)<0||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,f){for(var g=d.getRSBlocks(a,c),h=new e,i=0;i8*k)throw new Error("code length overflow. ("+h.getLengthInBits()+">"+8*k+")");for(h.getLengthInBits()+4<=8*k&&h.put(0,4);h.getLengthInBits()%8!=0;)h.putBit(!1);for(;;){if(h.getLengthInBits()>=8*k)break;if(h.put(b.PAD0,8),h.getLengthInBits()>=8*k)break;h.put(b.PAD1,8)}return b.createBytes(h,g)},b.createBytes=function(a,b){for(var d=0,e=0,f=0,g=new Array(b.length),h=new Array(b.length),i=0;i=0?o.get(p):0}}for(var q=0,l=0;l=0;)b^=t.G15<=0;)b^=t.G18<>>=1;return b},getPatternPosition:function(a){return t.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case s.PATTERN000:return(b+c)%2==0;case s.PATTERN001:return b%2==0;case s.PATTERN010:return c%3==0;case s.PATTERN011:return(b+c)%3==0;case s.PATTERN100:return(Math.floor(b/2)+Math.floor(c/3))%2==0;case s.PATTERN101:return b*c%2+b*c%3==0;case s.PATTERN110:return(b*c%2+b*c%3)%2==0;case s.PATTERN111:return(b*c%3+(b+c)%2)%2==0;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new c([1],0),d=0;d5&&(c+=3+f-5)}for(var d=0;d=256;)a-=255;return u.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},v=0;v<8;v++)u.EXP_TABLE[v]=1<>>7-a%8&1)},put:function(a,b){for(var c=0;c>>b-c-1&1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var w=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],x=function(){return"undefined"!=typeof CanvasRenderingContext2D}()?function(){function a(){if("svg"==this._htOption.drawer){var a=this._oContext.getSerializedSvg(!0);this.dataURL=a,this._el.innerHTML=a}else try{var b=this._elCanvas.toDataURL("image/png");this.dataURL=b}catch(a){console.error(a)}this._htOption.onRenderingEnd&&(this.dataURL||console.error("Can not get base64 data, please check: 1. Published the page and image to the server 2. The image request support CORS 3. Configured `crossOrigin:'anonymous'` option"),this._htOption.onRenderingEnd(this._htOption,this.dataURL))}function b(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&c._fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,void(d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==")}!0===c._bSupportDataURI&&c._fSuccess?c._fSuccess.call(c):!1===c._bSupportDataURI&&c._fFail&&c._fFail.call(c)}if(m._android&&m._android<=2.1){var c=1/window.devicePixelRatio,d=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,b,e,f,g,h,i,j,k){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*c;else void 0===j&&(arguments[1]*=c,arguments[2]*=c,arguments[3]*=c,arguments[4]*=c);d.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=f(),this._el=a,this._htOption=b,"svg"==this._htOption.drawer?(this._oContext={},this._elCanvas={}):(this._elCanvas=document.createElement("canvas"),this._el.appendChild(this._elCanvas),this._oContext=this._elCanvas.getContext("2d")),this._bSupportDataURI=null,this.dataURL=null};return e.prototype.draw=function(a){function b(){d.quietZone>0&&d.quietZoneColor&&(h.lineWidth=0,h.fillStyle=d.quietZoneColor,h.fillRect(0,0,i._elCanvas.width,d.quietZone),h.fillRect(0,d.quietZone,d.quietZone,i._elCanvas.height-2*d.quietZone),h.fillRect(i._elCanvas.width-d.quietZone,d.quietZone,d.quietZone,i._elCanvas.height-2*d.quietZone),h.fillRect(0,i._elCanvas.height-d.quietZone,i._elCanvas.width,d.quietZone))}function c(a){function c(a){var c=Math.round(d.width/3.5),e=Math.round(d.height/3.5);c!==e&&(c=e),d.logoMaxWidth?c=Math.round(d.logoMaxWidth):d.logoWidth&&(c=Math.round(d.logoWidth)),d.logoMaxHeight?e=Math.round(d.logoMaxHeight):d.logoHeight&&(e=Math.round(d.logoHeight));var f,g;void 0===a.naturalWidth?(f=a.width,g=a.height):(f=a.naturalWidth,g=a.naturalHeight),(d.logoMaxWidth||d.logoMaxHeight)&&(d.logoMaxWidth&&f<=c&&(c=f),d.logoMaxHeight&&g<=e&&(e=g),f<=c&&g<=e&&(c=f,e=g));var i=(d.width+2*d.quietZone-c)/2,j=(d.height+d.titleHeight+2*d.quietZone-e)/2,k=Math.min(c/f,e/g),l=f*k,m=g*k;(d.logoMaxWidth||d.logoMaxHeight)&&(c=l,e=m,i=(d.width+2*d.quietZone-c)/2,j=(d.height+d.titleHeight+2*d.quietZone-e)/2),d.logoBackgroundTransparent||(h.fillStyle=d.logoBackgroundColor,h.fillRect(i,j,c,e));var n=h.imageSmoothingQuality,o=h.imageSmoothingEnabled;h.imageSmoothingEnabled=!0,h.imageSmoothingQuality="high",h.drawImage(a,i+(c-l)/2,j+(e-m)/2,l,m),h.imageSmoothingEnabled=o,h.imageSmoothingQuality=n,b(),s._bIsPainted=!0,s.makeImage()}d.onRenderingStart&&d.onRenderingStart(d);for(var i=0;i';g.push(m)}if(b.quietZone&&(h="display:inline-block; width:"+(b.width+2*b.quietZone)+"px; height:"+(b.width+2*b.quietZone)+"px;background:"+b.quietZoneColor+"; text-align:center;"),g.push('
    '),g.push(''),g.push('");for(var p=0;p');for(var q=0;q')}else{var v=b.colorDark;6==p?(v=b.timing_H||b.timing||k,g.push('')):6==q?(v=b.timing_V||b.timing||k,g.push('')):g.push('')}}g.push("")}if(g.push("
    '),b.title){var n=b.titleColor,o=b.titleFont;g.push('
    '+b.title+"
    ")}b.subTitle&&g.push('
    '+b.subTitle+"
    "),g.push("
    "),g.push("
    "),b.logo){var w=new Image;null!=b.crossOrigin&&(w.crossOrigin=b.crossOrigin),w.src=b.logo;var x=b.width/3.5,y=b.height/3.5;x!=y&&(x=y),b.logoWidth&&(x=b.logoWidth),b.logoHeight&&(y=b.logoHeight);var z="position:relative; z-index:1;display:table-cell;top:-"+((b.height-b.titleHeight)/2+y/2+b.quietZone)+"px;text-align:center; width:"+x+"px; height:"+y+"px;line-height:"+x+"px; vertical-align: middle;";b.logoBackgroundTransparent||(z+="background:"+b.logoBackgroundColor),g.push('
    ')}b.onRenderingStart&&b.onRenderingStart(b),c.innerHTML=g.join("");var A=c.childNodes[0],B=(b.width-A.offsetWidth)/2,C=(b.height-A.offsetHeight)/2;B>0&&C>0&&(A.style.margin=C+"px "+B+"px"),this._htOption.onRenderingEnd&&this._htOption.onRenderingEnd(this._htOption,null)},a.prototype.clear=function(){this._el.innerHTML=""},a}();j=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:r.H,dotScale:1,dotScaleTiming:1,dotScaleTiming_H:i,dotScaleTiming_V:i,dotScaleA:1,dotScaleAO:i,dotScaleAI:i,quietZone:0,quietZoneColor:"rgba(0,0,0,0)",title:"",titleFont:"normal normal bold 16px Arial",titleColor:"#000000",titleBackgroundColor:"#ffffff",titleHeight:0,titleTop:30,subTitle:"",subTitleFont:"normal normal normal 14px Arial",subTitleColor:"#4F4F4F",subTitleTop:60,logo:i,logoWidth:i,logoHeight:i,logoMaxWidth:i,logoMaxHeight:i,logoBackgroundColor:"#ffffff",logoBackgroundTransparent:!1,PO:i,PI:i,PO_TL:i,PI_TL:i,PO_TR:i,PI_TR:i,PO_BL:i,PI_BL:i,AO:i,AI:i,timing:i,timing_H:i,timing_V:i,backgroundImage:i,backgroundImageAlpha:1,autoColor:!1,autoColorDark:"rgba(0, 0, 0, .6)",autoColorLight:"rgba(255, 255, 255, .7)",onRenderingStart:i,onRenderingEnd:i,version:0,tooltip:!1,binary:!1,drawer:"canvas",crossOrigin:null,utf8WithoutBOM:!0},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];(this._htOption.version<0||this._htOption.version>40)&&(console.warn("QR Code version '"+this._htOption.version+"' is invalidate, reset to 0"),this._htOption.version=0),(this._htOption.dotScale<0||this._htOption.dotScale>1)&&(console.warn(this._htOption.dotScale+" , is invalidate, dotScale must greater than 0, less than or equal to 1, now reset to 1. "),this._htOption.dotScale=1),(this._htOption.dotScaleTiming<0||this._htOption.dotScaleTiming>1)&&(console.warn(this._htOption.dotScaleTiming+" , is invalidate, dotScaleTiming must greater than 0, less than or equal to 1, now reset to 1. "),this._htOption.dotScaleTiming=1),this._htOption.dotScaleTiming_H?(this._htOption.dotScaleTiming_H<0||this._htOption.dotScaleTiming_H>1)&&(console.warn(this._htOption.dotScaleTiming_H+" , is invalidate, dotScaleTiming_H must greater than 0, less than or equal to 1, now reset to 1. "),this._htOption.dotScaleTiming_H=1):this._htOption.dotScaleTiming_H=this._htOption.dotScaleTiming,this._htOption.dotScaleTiming_V?(this._htOption.dotScaleTiming_V<0||this._htOption.dotScaleTiming_V>1)&&(console.warn(this._htOption.dotScaleTiming_V+" , is invalidate, dotScaleTiming_V must greater than 0, less than or equal to 1, now reset to 1. "),this._htOption.dotScaleTiming_V=1):this._htOption.dotScaleTiming_V=this._htOption.dotScaleTiming,(this._htOption.dotScaleA<0||this._htOption.dotScaleA>1)&&(console.warn(this._htOption.dotScaleA+" , is invalidate, dotScaleA must greater than 0, less than or equal to 1, now reset to 1. "),this._htOption.dotScaleA=1),this._htOption.dotScaleAO?(this._htOption.dotScaleAO<0||this._htOption.dotScaleAO>1)&&(console.warn(this._htOption.dotScaleAO+" , is invalidate, dotScaleAO must greater than 0, less than or equal to 1, now reset to 1. "),this._htOption.dotScaleAO=1):this._htOption.dotScaleAO=this._htOption.dotScaleA,this._htOption.dotScaleAI?(this._htOption.dotScaleAI<0||this._htOption.dotScaleAI>1)&&(console.warn(this._htOption.dotScaleAI+" , is invalidate, dotScaleAI must greater than 0, less than or equal to 1, now reset to 1. "),this._htOption.dotScaleAI=1):this._htOption.dotScaleAI=this._htOption.dotScaleA,(this._htOption.backgroundImageAlpha<0||this._htOption.backgroundImageAlpha>1)&&(console.warn(this._htOption.backgroundImageAlpha+" , is invalidate, backgroundImageAlpha must between 0 and 1, now reset to 1. "),this._htOption.backgroundImageAlpha=1),this._htOption.height=this._htOption.height+this._htOption.titleHeight,"string"==typeof a&&(a=document.getElementById(a)),(!this._htOption.drawer||"svg"!=this._htOption.drawer&&"canvas"!=this._htOption.drawer)&&(this._htOption.drawer="canvas"),this._android=f(),this._el=a,this._oQRCode=null;var d={};for(var c in this._htOption)d[c]=this._htOption[c];this._oDrawing=new x(this._el,d),this._htOption.text&&this.makeCode(this._htOption.text)},j.prototype.makeCode=function(a){this._oQRCode=new b(g(a,this._htOption),this._htOption.correctLevel),this._oQRCode.addData(a,this._htOption.binary,this._htOption.utf8WithoutBOM),this._oQRCode.make(),this._htOption.tooltip&&(this._el.title=a),this._oDrawing.draw(this._oQRCode)},j.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},j.prototype.clear=function(){this._oDrawing.remove()},j.prototype.resize=function(a,b){this._oDrawing._htOption.width=a,this._oDrawing._htOption.height=b,this._oDrawing.draw(this._oQRCode)},j.prototype.noConflict=function(){return m.QRCode===this&&(m.QRCode=p),j},j.CorrectLevel=r,"function"==typeof define&&(define.amd||define.cmd)?define([],function(){return j}):o?((o.exports=j).QRCode=j,n.QRCode=j):m.QRCode=j}.call(this); \ No newline at end of file diff --git a/src/main/resources/static/js/easy_qrcode.zip b/src/main/resources/static/js/easy_qrcode.zip new file mode 100644 index 0000000..e338f54 Binary files /dev/null and b/src/main/resources/static/js/easy_qrcode.zip differ diff --git a/src/main/resources/static/assets/js/jquery.dropotron.min.js b/src/main/resources/static/js/jquery.dropotron.min.js similarity index 100% rename from src/main/resources/static/assets/js/jquery.dropotron.min.js rename to src/main/resources/static/js/jquery.dropotron.min.js diff --git a/src/main/resources/static/assets/js/jquery.min.js b/src/main/resources/static/js/jquery.min.js similarity index 100% rename from src/main/resources/static/assets/js/jquery.min.js rename to src/main/resources/static/js/jquery.min.js diff --git a/src/main/resources/static/assets/js/main.js b/src/main/resources/static/js/main.js similarity index 100% rename from src/main/resources/static/assets/js/main.js rename to src/main/resources/static/js/main.js diff --git a/src/main/resources/static/js/spider.js b/src/main/resources/static/js/spider.js index df1ef63..d7c5e5c 100644 --- a/src/main/resources/static/js/spider.js +++ b/src/main/resources/static/js/spider.js @@ -1,260 +1,546 @@ // src/main/resources/static/js/spider.js document.addEventListener('DOMContentLoaded', () => { - // === 1. 게임 상태 및 Canvas 설정 === + + // ======================================= + // 1. 상수 및 변수 선언 + // ======================================= + console.log("DOM 콘텐츠가 로드되었습니다. 초기 설정 시작."); + + // HTML 요소 const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); + // ** UI 버튼 및 영역의 논리적 위치 정의 (캔버스 내 좌표) ** + const UI_ELEMENTS = {}; + + // ** 비율 상수 정의 ** + const CARD_WIDTH_RATIO = 1 / 11.5; + const CARD_HEIGHT_RATIO = 1.4; + const CARD_GAP_X_RATIO = 0.15; + const CARD_OVERLAP_Y_RATIO = 0.3; + const CARD_RANK_LEFT_PADDING = 0.1; + const CARD_RANK_TOP_PADDING = 0.1; + const CARD_SYMBOL_TOP_PADDING = 0.25; + const CARD_SYMBOL_BOTTOM_PADDING = 0.15; + const FOUNDATION_CARD_SPACING = 0.2; // 카드 겹침 비율 (20%만 보이게) + const FOUNDATION_WIDTH_RATIO = 0.45; // 파운데이션 영역 최대 너비 + + // 게임 상태 및 데이터 let gameId = null; let currentGame = null; + let isGameCompleted = false; // 게임 완료 상태 변수 const API_BASE_URL = '/spider'; + // 동적으로 계산될 레이아웃 변수 let cardWidth = 0; let cardHeight = 0; + let cardGapX = 0; let cardOverlapY = 0; + let totalTableauWidth = 0; + let tableauStartX = 0; + // 드래그 및 애니메이션 관련 변수 let isAnimating = false; let isDragging = false; + let dragStartX = 0; + let dragStartY = 0; + const DRAG_THRESHOLD = 5; let draggedCards = []; let dragOffsetX = 0; let dragOffsetY = 0; - let lastTapTime = 0; - let animatedCard = null; let animationProgress = 0; - let shakeOffset = 0; - let dimOpacity = 0; - let shakenCard = null; + let completedStackCards = []; + let isAnimatingCompletion = false; + // 하단 정렬을 위한 Y 좌표 기준 + const BOTTOM_ROW_Y_RATIO = 0.9; + let dpr = 1; + const MAX_UNDO_COUNT = 5; + + // 카드 뒷면 이미지 로드 const cardBackImage = new Image(); cardBackImage.src = '../assets/css/images/card-back.png'; - let assetsLoaded = false; cardBackImage.onload = () => { assetsLoaded = true; - updateCardCountOptions(); + resizeCanvas(); + draw(); + console.log("카드 뒷면 이미지가 로드되었습니다."); }; - const suitCountSelect = document.getElementById('suitCountSelect'); - const cardCountSelect = document.getElementById('cardCountSelect'); - const startButton = document.getElementById('startButton'); - - // 🔴 수정: 난이도별 카드 분배 옵션을 세분화 + // UI 옵션 데이터 + const suitOptions = [{ value: 1, text: '1개' }, { value: 2, text: '2개' }, { value: 4, text: '4개' }]; const cardDistributionOptions = { - '1': [ // 1 무늬: 맞출 확률이 매우 높으므로 쉬운 난이도만 제공 - { value: '3,3', text: '쉬움 (총 30장)' }, - { value: '4,3', text: '보통 (총 34장)' }, - { value: '5,4', text: '어려움 (총 44장)' } - ], - '2': [ // 2 무늬: 난이도가 높아져 4단계까지 제공 - { value: '3,3', text: '쉬움 (총 30장)' }, - { value: '4,3', text: '보통 (총 34장)' }, - { value: '5,4', text: '어려움 (총 44장)' }, - { value: '5,5', text: '매우 어려움 (총 50장)' } - ], - '4': [ // 4 무늬: 가장 어려우므로 쉬움 난이도는 제외하고 제공 - { value: '4,3', text: '보통 (총 34장)' }, - { value: '5,4', text: '어려움 (총 44장)' }, - { value: '5,5', text: '매우 어려움 (총 50장)' }, - { value: '6,6', text: '극악 (총 60장)' } - ] + '1': [{ value: '4,3', text: '쉬움' }, { value: '5,4', text: '보통' }, { value: '6,5', text: '어려움' }], + '2': [{ value: '5,4', text: '쉬움' }, { value: '6,5', text: '보통' }, { value: '7,6', text: '어려움' }], + '4': [{ value: '6,5', text: '쉬움' }, { value: '7,6', text: '보통' }, { value: '8,7', text: '어려움' }] }; + let selectedSuit = 1; + let selectedCardCount = '4,3'; - /** - * 무늬 수 선택에 따라 카드 수 옵션을 업데이트하는 함수 - */ - function updateCardCountOptions() { - const selectedSuits = suitCountSelect.value; - const options = cardDistributionOptions[selectedSuits] || []; + // ======================================= + // 2. 렌더링 (그리기) 관련 함수 + // ======================================= + console.log("렌더링 관련 함수 정의 시작."); - cardCountSelect.innerHTML = ''; - - options.forEach(option => { - const newOption = document.createElement('option'); - newOption.value = option.value; - newOption.textContent = option.text; - cardCountSelect.appendChild(newOption); - }); - } - - // Canvas 크기 조절 + // 캔버스 크기 조정 및 레이아웃 변수 계산 function resizeCanvas() { - canvas.width = window.innerWidth * 0.9; - canvas.height = window.innerHeight * 0.9; - cardWidth = canvas.width / 12; - cardHeight = cardWidth * 1.4; - cardOverlapY = cardHeight * 0.2; + console.log("resizeCanvas 함수 호출."); + const size = Math.min(window.innerWidth, window.innerHeight) * 0.95; + canvas.style.width = `${size}px`; + canvas.style.height = `${size}px`; + dpr = window.devicePixelRatio || 1; + canvas.width = size * dpr; + canvas.height = size * dpr; + ctx.scale(dpr, dpr); + const logicalWidth = size; + const logicalHeight = size; + cardWidth = logicalWidth * CARD_WIDTH_RATIO; + cardHeight = cardWidth * CARD_HEIGHT_RATIO; + cardGapX = cardWidth * CARD_GAP_X_RATIO; + cardOverlapY = cardHeight * CARD_OVERLAP_Y_RATIO; + totalTableauWidth = cardWidth * 10 + cardGapX * 9; + tableauStartX = (logicalWidth - totalTableauWidth) / 2; + + // ** UI 요소 위치 재계산 (캔버스 내에서) ** + const buttonWidth = logicalWidth * 0.2; + const buttonHeight = logicalHeight * 0.05; + const buttonGap = 10; + const startX = (logicalWidth - (buttonWidth * 3 + buttonGap * 2)) / 2; + const startY = logicalHeight * 0.05; + + // 게임 시작 전 난이도 설정 UI + UI_ELEMENTS.difficultyUI = { + x: logicalWidth / 2 - (buttonWidth * 1.5 + buttonGap), + y: logicalHeight * 0.45, + width: (buttonWidth * 3 + buttonGap * 2), + height: buttonHeight + }; + UI_ELEMENTS.suitSelect = { x: startX, y: startY, width: buttonWidth, height: buttonHeight }; + UI_ELEMENTS.cardCountSelect = { x: startX + buttonWidth + buttonGap, y: startY, width: buttonWidth, height: buttonHeight }; + UI_ELEMENTS.startButton = { x: startX + buttonWidth * 2 + buttonGap * 2, y: startY, width: buttonWidth, height: buttonHeight }; + + // 하단 UI 요소들의 기준 Y 좌표 + const bottomY = logicalHeight * BOTTOM_ROW_Y_RATIO; + const itemSpacing = 20; + + // 파운데이션 영역 + const foundationX = logicalWidth * 0.05; + const foundationAreaWidth = logicalWidth * FOUNDATION_WIDTH_RATIO; // 화면 너비의 45%로 제한 + UI_ELEMENTS.foundationArea = { + x: foundationX, + y: bottomY, + width: foundationAreaWidth, + height: cardHeight + }; + + const undoButtonWidth = cardWidth * 0.8; + const undoButtonHeight = cardHeight * 0.5; + const undoCountDisplayWidth = cardWidth * 0.5; + + // 실행 취소/게임 포기 버튼 위치 + const undoButtonX = logicalWidth * 0.5 - (undoButtonWidth + undoCountDisplayWidth + itemSpacing) / 2; + UI_ELEMENTS.undoButton = { + x: undoButtonX, + y: bottomY + (cardHeight - undoButtonHeight) / 2, + width: undoButtonWidth, + height: undoButtonHeight + }; + + // 취소 횟수 표시 위치 + UI_ELEMENTS.undoCountDisplay = { + x: undoButtonX + undoButtonWidth + itemSpacing, + y: bottomY + (cardHeight - undoButtonHeight) / 2, + width: undoCountDisplayWidth, + height: undoButtonHeight + }; + + // 스톡 영역 + const stockX = logicalWidth * 0.95 - cardWidth; + UI_ELEMENTS.stockArea = { + x: stockX, + y: bottomY, + width: cardWidth, + height: cardHeight + }; + + // 게임 완료 버튼 위치 + UI_ELEMENTS.restartButton = { + x: logicalWidth / 2 - buttonWidth / 2, + y: logicalHeight / 2 + 50, + width: buttonWidth, + height: buttonHeight + }; + + console.log(`캔버스 논리적 크기: ${size}x${size}, 캔버스 물리적 해상도: ${canvas.width}x${canvas.height}`); } + window.addEventListener('resize', resizeCanvas); - resizeCanvas(); - // === 2. 렌더링 및 게임 루프 === + // 메인 그리기 루프 function draw() { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = '#008000'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - if (currentGame && assetsLoaded) { - drawGame(currentGame); - - if (animatedCard) drawAnimatedCard(); - if (shakeOffset !== 0) drawShakingCard(); - if (dimOpacity > 0) drawDimOverlay(); + if (!assetsLoaded) { + requestAnimationFrame(draw); + return; } + ctx.clearRect(0, 0, canvas.width, canvas.height); + + if (currentGame) { + drawGame(currentGame); + } + + drawUI(); + requestAnimationFrame(draw); } - /** - * 🔴 수정: 새로운 데이터 구조에 맞게 게임 전체를 그리는 함수 - */ - function drawGame(game) { - const startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2; - const startY = cardHeight * 0.5; + // UI 요소를 캔버스에 직접 그리는 함수 + function drawUI() { + if (!currentGame) { + // 게임 시작 전 난이도 선택 UI + const suitSelect = UI_ELEMENTS.suitSelect; + ctx.fillStyle = '#f0f0f0'; + ctx.fillRect(suitSelect.x, suitSelect.y, suitSelect.width, suitSelect.height); + ctx.strokeStyle = '#333'; + ctx.strokeRect(suitSelect.x, suitSelect.y, suitSelect.width, suitSelect.height); + ctx.fillStyle = '#000'; + ctx.font = '16px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(`무늬: ${selectedSuit}개`, suitSelect.x + suitSelect.width / 2, suitSelect.y + suitSelect.height / 2); - // 테이블로 스택 그리기 - game.tableau.forEach((stack, stackIndex) => { + const cardCountSelect = UI_ELEMENTS.cardCountSelect; + ctx.fillStyle = '#f0f0f0'; + ctx.fillRect(cardCountSelect.x, cardCountSelect.y, cardCountSelect.width, cardCountSelect.height); + ctx.strokeRect(cardCountSelect.x, cardCountSelect.y, cardCountSelect.width, cardCountSelect.height); + ctx.fillStyle = '#000'; + ctx.fillText(`카드: ${cardDistributionOptions[selectedSuit.toString()].find(opt => opt.value === selectedCardCount).text}`, + cardCountSelect.x + cardCountSelect.width / 2, cardCountSelect.y + cardCountSelect.height / 2); + + const startButton = UI_ELEMENTS.startButton; + ctx.fillStyle = '#4CAF50'; + ctx.fillRect(startButton.x, startButton.y, startButton.width, startButton.height); + ctx.strokeStyle = '#333'; + ctx.strokeRect(startButton.x, startButton.y, startButton.width, startButton.height); + ctx.fillStyle = '#fff'; + ctx.fillText('새 게임 시작', startButton.x + startButton.width / 2, startButton.y + startButton.height / 2); + } else { + // 게임 중 하단 UI + const undoButton = UI_ELEMENTS.undoButton; + const undoCountDisplay = UI_ELEMENTS.undoCountDisplay; + + const isUndoPossible = currentGame.undoHistory.length > 0; + const isUndoEnabled = currentGame.undoCount < MAX_UNDO_COUNT && isUndoPossible; + const isSurrender = currentGame.undoCount >= MAX_UNDO_COUNT; + + if (isUndoEnabled) { + const buttonText = '실행 취소'; + const buttonColor = '#ff9800'; + ctx.fillStyle = buttonColor; + ctx.fillRect(undoButton.x, undoButton.y, undoButton.width, undoButton.height); + ctx.strokeStyle = '#333'; + ctx.strokeRect(undoButton.x, undoButton.y, undoButton.width, undoButton.height); + ctx.fillStyle = '#fff'; + ctx.font = '14px Arial'; + ctx.fillText(buttonText, undoButton.x + undoButton.width / 2, undoButton.y + undoButton.height / 2); + + // 남은 취소 횟수 표시 + const remainingUndos = MAX_UNDO_COUNT - currentGame.undoCount; + ctx.fillStyle = '#fff'; + ctx.font = '20px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(`${remainingUndos}`, undoCountDisplay.x + undoCountDisplay.width / 2, undoCountDisplay.y + undoCountDisplay.height / 2); + } else if (isSurrender) { + const buttonText = '게임 포기'; + const buttonColor = '#f44336'; + ctx.fillStyle = buttonColor; + ctx.fillRect(undoButton.x, undoButton.y, undoButton.width, undoButton.height); + ctx.strokeStyle = '#333'; + ctx.strokeRect(undoButton.x, undoButton.y, undoButton.width, undoButton.height); + ctx.fillStyle = '#fff'; + ctx.font = '14px Arial'; + ctx.fillText(buttonText, undoButton.x + undoButton.width / 2, undoButton.y + undoButton.height / 2); + } + } + } + + // 전체 게임 화면을 그리는 메인 함수 + function drawGame(game) { + drawBackground(); + drawTableau(game.tableau); + drawStockAndFoundation(game.stock, game.foundation); + drawDraggedCards(draggedCards); + drawCompletionAnimation(); + + // 게임 완료 시 메시지 표시 + if (isGameCompleted) { + drawCompletionMessage(); + } + } + + // 게임 완료 메시지 그리기 + function drawCompletionMessage() { + const logicalWidth = canvas.width / dpr; + const logicalHeight = canvas.height / dpr; + + // 반투명한 배경 + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(0, 0, logicalWidth, logicalHeight); + + // 메시지 텍스트 + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 36px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('게임 완료! 축하합니다!', logicalWidth / 2, logicalHeight / 2); + + // '새 게임 시작' 버튼 그리기 + const restartButton = UI_ELEMENTS.restartButton; + ctx.fillStyle = '#4CAF50'; + ctx.fillRect(restartButton.x, restartButton.y, restartButton.width, restartButton.height); + ctx.strokeStyle = '#fff'; + ctx.strokeRect(restartButton.x, restartButton.y, restartButton.width, restartButton.height); + ctx.fillStyle = '#fff'; + ctx.font = '20px Arial'; + ctx.fillText('다시 시작', restartButton.x + restartButton.width / 2, restartButton.y + restartButton.height / 2); + } + + // 게임 배경 그리기 + function drawBackground() { + ctx.fillStyle = '#008000'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + // 테이블 스택 그리기 + function drawTableau(tableau) { + const startY = cardHeight * 0.5 + const draggingCards = isDragging ? new Set(draggedCards) : null; + tableau.forEach((stack, stackIndex) => { stack.forEach((card, cardIndex) => { - // 드래그 중인 카드는 숨김 - if (isDragging && draggedCards.includes(card)) { + if (draggingCards && draggingCards.has(card)) { return; } - const x = startX + stackIndex * (cardWidth + cardWidth * 0.5); + const x = tableauStartX + stackIndex * (cardWidth + cardGapX); const y = startY + cardIndex * cardOverlapY; + card.touchHeight = (cardIndex === stack.length - 1) ? cardHeight : cardOverlapY; drawSingleCard(card, x, y); }); }); - - // 드래그 중인 카드 묶음 그리기 - if (isDragging && draggedCards.length > 0) { - drawDraggedCards(draggedCards); - } - - // 스톡과 파운데이션 그리기 - drawStockAndFoundation(game.stock, game.foundation); } - /** - * 드래그 중인 카드 묶음을 그리는 함수 - */ + // 드래그 중인 카드 묶음 그리기 function drawDraggedCards(cards) { + if (!isDragging || !Array.isArray(cards) || cards.length === 0) { + return; + } cards.forEach((card, index) => { const x = cards[0].x; const y = cards[0].y + index * cardOverlapY; - - ctx.fillStyle = '#ffffff'; - ctx.fillRect(x, y, cardWidth, cardHeight); - ctx.strokeStyle = '#333333'; - ctx.strokeRect(x, y, cardWidth, cardHeight); - - const isRed = (card.suit === 'heart' || card.suit === 'diamond'); - ctx.fillStyle = isRed ? '#ff0000' : '#000000'; - ctx.font = `${cardWidth * 0.25}px Arial`; - ctx.fillText(getRankText(card.rank), x + cardWidth * 0.1, y + cardHeight * 0.25); - ctx.fillText(getSuitSymbol(card.suit), x + cardWidth * 0.1, y + cardHeight * 0.5); + drawSingleCard(card, x, y); }); } - /** - * 단일 카드를 그리는 헬퍼 함수 - */ + // 완성된 스택 애니메이션 그리기 + function drawCompletionAnimation() { + if (isAnimatingCompletion) { + const now = Date.now(); + completedStackCards = completedStackCards.filter(card => { + if (now < card.animEndTime) { + const progress = (now - (card.animEndTime - 500)) / 500; + const currentX = card.animStartX + (card.animTargetX - card.animStartX) * progress; + const currentY = card.animStartY + (card.animTargetY - card.animStartY) * progress; + drawSingleCard(card, currentX, currentY); + return true; + } else { + return false; + } + }); + if (completedStackCards.length === 0) { + isAnimatingCompletion = false; + } + } + } + + // 단일 카드 그리기 (좌표와 크기를 카드 객체에 저장) function drawSingleCard(card, x, y) { + card.x = x; + card.y = y; + card.width = cardWidth; + card.height = cardHeight; if (card.isFaceUp) { ctx.fillStyle = '#ffffff'; ctx.fillRect(x, y, cardWidth, cardHeight); ctx.strokeStyle = '#333333'; ctx.strokeRect(x, y, cardWidth, cardHeight); - const isRed = (card.suit === 'heart' || card.suit === 'diamond'); ctx.fillStyle = isRed ? '#ff0000' : '#000000'; ctx.font = `${cardWidth * 0.25}px Arial`; - - // 🔴 수정: 텍스트의 Y 좌표를 위로 올립니다. - ctx.fillText(getRankText(card.rank), x + cardWidth * 0.1, y + cardHeight * 0.15); - ctx.fillText(getSuitSymbol(card.suit), x + cardWidth * 0.1, y + cardHeight * 0.4); + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + ctx.fillText(getRankText(card.rank), x + cardWidth * CARD_RANK_LEFT_PADDING, y + cardHeight * CARD_RANK_TOP_PADDING); + drawSuitSymbols(card, x, y); } else { ctx.drawImage(cardBackImage, x, y, cardWidth, cardHeight); } } - /** - * 이동 애니메이션 중인 카드를 그리는 로직 - */ - function drawAnimatedCard() { - if (!animatedCard) return; + function drawSuitSymbols(card, x, y) { + const symbol = getSuitSymbol(card.suit); + let symbolSize; + if (card.rank >= 2 && card.rank <= 5) { + symbolSize = cardWidth * 0.2; + } else { + symbolSize = cardWidth * 0.15; + } + ctx.font = `${symbolSize}px Arial`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = (card.suit === 'heart' || card.suit === 'diamond') ? '#ff0000' : '#000000'; + const symbolAreaY = y + cardHeight * CARD_SYMBOL_TOP_PADDING; + const symbolAreaHeight = cardHeight * (1 - CARD_SYMBOL_TOP_PADDING - CARD_SYMBOL_BOTTOM_PADDING); + const symbolAreaMiddleY = symbolAreaY + symbolAreaHeight / 2; + const symbolAreaLeftX = x + cardWidth * 0.25; + const symbolAreaRightX = x + cardWidth * 0.75; + const symbolGapY = symbolAreaHeight / 3; + const positions = { + top: { x: x + cardWidth / 2, y: symbolAreaY + symbolGapY * 0.5 }, + bottom: { x: x + cardWidth / 2, y: symbolAreaY + symbolAreaHeight - symbolGapY * 0.5 }, + center: { x: x + cardWidth / 2, y: symbolAreaMiddleY }, + leftTop: { x: symbolAreaLeftX, y: symbolAreaY + symbolGapY * 0.5 }, + rightTop: { x: symbolAreaRightX, y: symbolAreaY + symbolGapY * 0.5 }, + leftCenter: { x: symbolAreaLeftX, y: symbolAreaMiddleY }, + rightCenter: { x: symbolAreaRightX, y: symbolAreaMiddleY }, + leftBottom: { x: symbolAreaLeftX, y: symbolAreaY + symbolAreaHeight - symbolGapY * 0.5 }, + rightBottom: { x: symbolAreaRightX, y: symbolAreaY + symbolAreaHeight - symbolGapY * 0.5 }, + middleTop: { x: x + cardWidth / 2, y: symbolAreaY + symbolGapY * 0.5 }, + middleBottom: { x: x + cardWidth / 2, y: symbolAreaY + symbolAreaHeight - symbolGapY * 0.5 } + }; - const startX = animatedCard.startX + (animatedCard.endX - animatedCard.startX) * animationProgress; - const startY = animatedCard.startY + (animatedCard.endY - animatedCard.startY) * animationProgress; - - ctx.fillStyle = '#ffffff'; - ctx.fillRect(startX, startY, cardWidth, cardHeight); - ctx.strokeStyle = '#333333'; - ctx.strokeRect(startX, startY, cardWidth, cardHeight); - - const isRed = (animatedCard.card.suit === 'heart' || animatedCard.card.suit === 'diamond'); - ctx.fillStyle = isRed ? '#ff0000' : '#000000'; - ctx.font = `${cardWidth * 0.25}px Arial`; - ctx.fillText(getRankText(animatedCard.card.rank), startX + cardWidth * 0.1, startY + cardHeight * 0.25); - ctx.fillText(getSuitSymbol(animatedCard.card.suit), startX + cardWidth * 0.1, startY + cardHeight * 0.5); - - animationProgress += 0.05; - if (animationProgress >= 1) { - isAnimating = false; - animatedCard = null; + switch (card.rank) { + case 1: case 11: case 12: case 13: + ctx.font = `${cardWidth * 0.6}px Arial`; + ctx.fillText(symbol, x + cardWidth / 2, y + cardHeight / 2); + break; + case 2: + ctx.fillText(symbol, positions.middleTop.x, positions.middleTop.y); + ctx.fillText(symbol, positions.middleBottom.x, positions.middleBottom.y); + break; + case 3: + ctx.fillText(symbol, positions.middleTop.x, positions.middleTop.y); + ctx.fillText(symbol, positions.center.x, positions.center.y); + ctx.fillText(symbol, positions.middleBottom.x, positions.middleBottom.y); + break; + case 4: + ctx.fillText(symbol, positions.leftTop.x, positions.leftTop.y); + ctx.fillText(symbol, positions.rightTop.x, positions.rightTop.y); + ctx.fillText(symbol, positions.leftBottom.x, positions.leftBottom.y); + ctx.fillText(symbol, positions.rightBottom.x, positions.rightBottom.y); + break; + case 5: + ctx.fillText(symbol, positions.leftTop.x, positions.leftTop.y); + ctx.fillText(symbol, positions.rightTop.x, positions.rightTop.y); + ctx.fillText(symbol, positions.center.x, positions.center.y); + ctx.fillText(symbol, positions.leftBottom.x, positions.leftBottom.y); + ctx.fillText(symbol, positions.rightBottom.x, positions.rightBottom.y); + break; + case 6: + ctx.fillText(symbol, positions.leftTop.x, positions.leftTop.y); + ctx.fillText(symbol, positions.rightTop.x, positions.rightTop.y); + ctx.fillText(symbol, positions.leftCenter.x, positions.leftCenter.y); + ctx.fillText(symbol, positions.rightCenter.x, positions.rightCenter.y); + ctx.fillText(symbol, positions.leftBottom.x, positions.leftBottom.y); + ctx.fillText(symbol, positions.rightBottom.x, positions.rightBottom.y); + break; + case 7: + ctx.fillText(symbol, positions.leftTop.x, positions.leftTop.y); + ctx.fillText(symbol, positions.rightTop.x, positions.rightTop.y); + ctx.fillText(symbol, positions.leftCenter.x, positions.leftCenter.y); + ctx.fillText(symbol, positions.rightCenter.x, positions.rightCenter.y); + ctx.fillText(symbol, positions.leftBottom.x, positions.leftBottom.y); + ctx.fillText(symbol, positions.rightBottom.x, positions.rightBottom.y); + ctx.fillText(symbol, positions.center.x, positions.center.y); + break; + case 8: + ctx.fillText(symbol, positions.leftTop.x, positions.leftTop.y); + ctx.fillText(symbol, positions.rightTop.x, positions.rightTop.y); + ctx.fillText(symbol, positions.leftCenter.x, positions.leftCenter.y); + ctx.fillText(symbol, positions.rightCenter.x, positions.rightCenter.y); + ctx.fillText(symbol, positions.leftBottom.x, positions.leftBottom.y); + ctx.fillText(symbol, positions.rightBottom.x, positions.rightBottom.y); + ctx.fillText(symbol, positions.middleTop.x, positions.middleTop.y); + ctx.fillText(symbol, positions.middleBottom.x, positions.middleBottom.y); + break; + case 9: + ctx.fillText(symbol, positions.leftTop.x, positions.leftTop.y); + ctx.fillText(symbol, positions.rightTop.x, positions.rightTop.y); + ctx.fillText(symbol, positions.leftCenter.x, positions.leftCenter.y); + ctx.fillText(symbol, positions.rightCenter.x, positions.rightCenter.y); + ctx.fillText(symbol, positions.leftBottom.x, positions.leftBottom.y); + ctx.fillText(symbol, positions.rightBottom.x, positions.rightBottom.y); + ctx.fillText(symbol, positions.center.x, positions.center.y); + ctx.fillText(symbol, positions.middleTop.x, positions.middleTop.y); + ctx.fillText(symbol, positions.middleBottom.x, positions.middleBottom.y); + break; + case 10: + ctx.fillText(symbol, positions.leftTop.x, positions.leftTop.y); + ctx.fillText(symbol, positions.rightTop.x, positions.rightTop.y); + ctx.fillText(symbol, positions.leftCenter.x, positions.leftCenter.y); + ctx.fillText(symbol, positions.rightCenter.x, positions.rightCenter.y); + ctx.fillText(symbol, positions.leftBottom.x, positions.leftBottom.y); + ctx.fillText(symbol, positions.rightBottom.x, positions.rightBottom.y); + ctx.fillText(symbol, positions.top.x, positions.top.y); + ctx.fillText(symbol, positions.bottom.x, positions.bottom.y); + ctx.fillText(symbol, positions.middleTop.x, (positions.top.y + symbolSize + positions.center.y) / 2 ); + ctx.fillText(symbol, positions.middleBottom.x, ((positions.bottom.y + positions.center.y) - symbolSize) / 2); + break; } } - /** - * 무효화 시 흔들리는 카드를 그리는 로직 - */ - function drawShakingCard() { - if (!shakenCard) return; - - const x = shakenCard.originalX + shakeOffset; - const y = shakenCard.originalY; - - ctx.fillStyle = '#ffffff'; - ctx.fillRect(x, y, cardWidth, cardHeight); - ctx.strokeStyle = '#333333'; - ctx.strokeRect(x, y, cardWidth, cardHeight); - - const isRed = (shakenCard.card.suit === 'heart' || shakenCard.card.suit === 'diamond'); - ctx.fillStyle = isRed ? '#ff0000' : '#000000'; - ctx.font = `${cardWidth * 0.25}px Arial`; - ctx.fillText(getRankText(shakenCard.card.rank), x + cardWidth * 0.1, y + cardHeight * 0.25); - ctx.fillText(getSuitSymbol(shakenCard.card.suit), x + cardWidth * 0.1, y + cardHeight * 0.5); - } - - /** - * 붉은색 딤 오버레이를 그리는 로직 - */ - function drawDimOverlay() { - ctx.fillStyle = `rgba(255, 0, 0, ${dimOpacity})`; - ctx.fillRect(0, 0, canvas.width, canvas.height); - } - - /** - * 🔴 추가: 스톡 더미와 파운데이션 영역을 그리는 로직 - */ + // 스톡 및 파운데이션 그리기 function drawStockAndFoundation(stock, foundation) { - const stockX = canvas.width - cardWidth * 1.5; - const stockY = canvas.height - cardHeight * 1.5; + const logicalCanvasWidth = canvas.width / (window.devicePixelRatio || 1); + const logicalCanvasHeight = canvas.height / (window.devicePixelRatio || 1); + const stockArea = UI_ELEMENTS.stockArea; + const foundationArea = UI_ELEMENTS.foundationArea; + const bottomY = logicalCanvasHeight * BOTTOM_ROW_Y_RATIO; - if (stock.length > 0) { - ctx.drawImage(cardBackImage, stockX, stockY, cardWidth, cardHeight); - } + // 파운데이션 영역을 투명한 색으로 표현 + ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; + ctx.fillRect(foundationArea.x, bottomY, foundationArea.width, foundationArea.height); - const foundationY = canvas.height - cardHeight * 1.5; + // 파운데이션 foundation.forEach((stack, index) => { - const foundationX = cardWidth * 0.5 + index * (cardWidth + 10); + const foundationX = foundationArea.x + index * (cardWidth * FOUNDATION_CARD_SPACING); // 겹쳐지게 그리기 if (stack.length > 0) { + // 완성된 카드 스택을 그립니다. const topCard = stack[stack.length - 1]; - drawSingleCard(topCard, foundationX, foundationY); + drawSingleCard(topCard, foundationX, bottomY); } }); + + // 스톡 + if (stock.length > 0) { + ctx.drawImage(cardBackImage, stockArea.x, bottomY, cardWidth, cardHeight); + + // 스톡 위에 남은 카드 수를 표시합니다. + const remainingDeals = Math.floor(stock.length / 10); + ctx.fillStyle = '#fff'; + ctx.font = '20px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(`${remainingDeals}`, stockArea.x + stockArea.width / 2, stockArea.y + stockArea.height / 2); + } } - // === 3. 이벤트 리스너 연결 === + // ======================================= + // 3. 이벤트 핸들러 및 유틸리티 함수 + // ======================================= + console.log("이벤트 핸들러 등록 시작."); + canvas.addEventListener('mousedown', handlePointerDown); canvas.addEventListener('mousemove', handlePointerMove); canvas.addEventListener('mouseup', handlePointerUp); @@ -263,213 +549,283 @@ document.addEventListener('DOMContentLoaded', () => { canvas.addEventListener('touchend', handlePointerUp); canvas.addEventListener('dblclick', handleDoubleClick); - suitCountSelect.addEventListener('change', updateCardCountOptions); - startButton.addEventListener('click', startNewGame); - function getCanvasCoordinates(event) { const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; const clientX = event.touches ? event.touches[0].clientX : event.clientX; const clientY = event.touches ? event.touches[0].clientY : event.clientY; return { - x: clientX - rect.left, - y: clientY - rect.top + x: (clientX - rect.left) * scaleX / dpr, + y: (clientY - rect.top) * scaleY / dpr }; } - /** - * 🔴 수정: 새로운 데이터 구조를 사용하도록 findCardAt 함수 변경 - */ - function findCardAt(x, y) { - const startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2; - const startY = cardHeight * 0.5; + // 클릭된 위치의 카드 또는 UI 요소 찾기 + function findElementAt(x, y) { + if (isGameCompleted) { + const restartButton = UI_ELEMENTS.restartButton; + if (x >= restartButton.x && x <= restartButton.x + restartButton.width && y >= restartButton.y && y <= restartButton.y + restartButton.height) { + return { type: 'ui', name: 'restartButton' }; + } + } - for (let stackIndex = 9; stackIndex >= 0; stackIndex--) { - const stackCards = currentGame.tableau[stackIndex]; - for (let cardIndex = stackCards.length - 1; cardIndex >= 0; cardIndex--) { - const card = stackCards[cardIndex]; - if (!card.isFaceUp) continue; + // 게임 진행 중일 때만 스톡과 실행 취소 버튼을 감지합니다. + if (currentGame) { + // 스톡 클릭 감지 + const stockArea = UI_ELEMENTS.stockArea; + if (x >= stockArea.x && x <= stockArea.x + stockArea.width && y >= stockArea.y && y <= stockArea.y + stockArea.height) { + return { type: 'stock' }; + } - const cardX = startX + stackIndex * (cardWidth + cardWidth * 0.5); - const cardY = startY + cardIndex * cardOverlapY; - const touchHeight = (cardIndex === stackCards.length - 1) ? cardHeight : cardOverlapY; + // 실행 취소 버튼 클릭 감지 + const undoButton = UI_ELEMENTS.undoButton; + if (x >= undoButton.x && x <= undoButton.x + undoButton.width && y >= undoButton.y && y <= undoButton.y + undoButton.height) { + return { type: 'ui', name: 'undoButton' }; + } + } - if (x >= cardX && x <= cardX + cardWidth && y >= cardY && y <= cardY + touchHeight) { - return { card, stackIndex, cardIndex }; + // 게임 상태와 관계 없이 항상 난이도 선택 UI를 감지합니다. + if (!currentGame) { + // 난이도 선택 UI 클릭 감지 + const suitSelect = UI_ELEMENTS.suitSelect; + if (x >= suitSelect.x && x <= suitSelect.x + suitSelect.width && y >= suitSelect.y && y <= suitSelect.y + suitSelect.height) { + return { type: 'ui', name: 'suitSelect' }; + } + const cardCountSelect = UI_ELEMENTS.cardCountSelect; + if (x >= cardCountSelect.x && x <= cardCountSelect.x + cardCountSelect.width && y >= cardCountSelect.y && y <= cardCountSelect.y + cardCountSelect.height) { + return { type: 'ui', name: 'cardCountSelect' }; + } + const startButton = UI_ELEMENTS.startButton; + if (x >= startButton.x && x <= startButton.x + startButton.width && y >= startButton.y && y <= startButton.y + startButton.height) { + return { type: 'ui', name: 'startButton' }; + } + } + + // 카드 클릭 감지 + if (currentGame) { + for (let stackIndex = 9; stackIndex >= 0; stackIndex--) { + const stackCards = currentGame.tableau[stackIndex]; + for (let cardIndex = stackCards.length - 1; cardIndex >= 0; cardIndex--) { + const card = stackCards[cardIndex]; + if (!card.isFaceUp) continue; + if (x >= card.x && x <= card.x + card.width && y >= card.y && y <= card.y + card.touchHeight) { + return { type: 'card', card, stackIndex, cardIndex }; + } } } } + return null; } - /** - * 🔴 수정: 클릭된 카드 아래의 묶음을 반환 (새로운 데이터 구조용) - */ - function getCardStackForMove(cardData, stackIndex) { + // 이동 가능한 카드 묶음 검사 (기존 로직) + function getCardStackForMove(card, stackIndex, cardIndex) { const stack = currentGame.tableau[stackIndex]; - const startIndex = stack.findIndex(c => c.suit === cardData.suit && c.rank === cardData.rank); - if (startIndex === -1 || !cardData.isFaceUp) return null; - - const movableStack = stack.slice(startIndex); + if (cardIndex === -1 || !card.isFaceUp) { + return null; + } + const movableStack = []; + for (let i = cardIndex; i < stack.length; i++) { + if (stack[i].isFaceUp) { + movableStack.push(stack[i]); + } else { + break; + } + } + if (movableStack.length === 0) { + return null; + } for (let i = 0; i < movableStack.length - 1; i++) { - // 🔴 수정: 랭크와 무늬를 모두 확인하여 유효한 묶음인지 체크 - if (movableStack[i].rank !== movableStack[i+1].rank + 1 || movableStack[i].suit !== movableStack[i+1].suit) { + if (movableStack[i].rank !== movableStack[i + 1].rank + 1 || movableStack[i].suit !== movableStack[i + 1].suit) { return null; } } return movableStack; } + // ======================================= + // 4. 게임 로직 및 상호작용 + // ======================================= + let touchStart = {}; + function handlePointerDown(event) { - // 🔴 로그 추가: 이 로그가 찍히는지 확인하세요. - console.log('--- handlePointerDown 실행 ---'); - - if (isAnimating) { - console.log('애니메이션 중이므로 드래그 시작 불가.'); - return; - } + if (isAnimating || isAnimatingCompletion) return; const coords = getCanvasCoordinates(event); - const clickedCardData = findCardAt(coords.x, coords.y); + touchStart = { x: coords.x, y: coords.y, time: Date.now() }; + const element = findElementAt(coords.x, coords.y); + if (!element) return; - // 🔴 추가: 스톡 더미 클릭 감지 - const stockX = canvas.width - cardWidth * 1.5; - const stockY = canvas.height - cardHeight * 1.5; - - if (coords.x >= stockX && coords.x <= stockX + cardWidth && coords.y >= stockY && coords.y <= stockY + cardHeight) { - handleStockClick(); - return; - } - - if (clickedCardData) { - const { card, stackIndex } = clickedCardData; - const movableStack = getCardStackForMove(card, stackIndex); - if (movableStack) { - isDragging = true; + if (element.type === 'ui') { + switch (element.name) { + case 'startButton': + startNewGame(); + break; + case 'undoButton': + if (currentGame.undoCount < MAX_UNDO_COUNT) { + handleUndo(); + } else { + currentGame = null; + draw(); + } + break; + case 'suitSelect': + selectedSuit = (selectedSuit === 1) ? 2 : (selectedSuit === 2) ? 4 : 1; + selectedCardCount = cardDistributionOptions[selectedSuit.toString()][0].value; + draw(); + break; + case 'cardCountSelect': + const currentOptions = cardDistributionOptions[selectedSuit.toString()]; + const currentIndex = currentOptions.findIndex(opt => opt.value === selectedCardCount); + const nextIndex = (currentIndex + 1) % currentOptions.length; + selectedCardCount = currentOptions[nextIndex].value; + draw(); + break; + case 'restartButton': + currentGame = null; + isGameCompleted = false; + draw(); + break; + } + } else if (element.type === 'card') { + const { card, stackIndex, cardIndex } = element; + const movableStack = getCardStackForMove(card, stackIndex, cardIndex); + if (movableStack && movableStack.length > 0) { draggedCards = movableStack; - draggedCards.sourceStackIndex = stackIndex; // 드래그 시작 스택 인덱스 저장 + draggedCards.sourceStackIndex = stackIndex; const cardPos = getCardPosition(card, stackIndex); dragOffsetX = coords.x - cardPos.x; dragOffsetY = coords.y - cardPos.y; } + } else if (element.type === 'stock') { + handleStockClick(); } } function handlePointerMove(event) { - if (!isDragging || draggedCards.length === 0) return; + if (!draggedCards || draggedCards.length === 0) return; event.preventDefault(); const coords = getCanvasCoordinates(event); - draggedCards[0].x = coords.x - dragOffsetX; - draggedCards[0].y = coords.y - dragOffsetY; + + if (!isDragging) { + const dx = coords.x - touchStart.x; + const dy = coords.y - touchStart.y; + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance > DRAG_THRESHOLD) { + isDragging = true; + } + } + + if (isDragging) { + draggedCards[0].x = coords.x - dragOffsetX; + draggedCards[0].y = coords.y - dragOffsetY; + } + draw(); } function handlePointerUp(event) { - if (!isDragging || draggedCards.length === 0) return; - isDragging = false; + if (!isDragging || draggedCards.length === 0) { + returnToOriginalPosition(); + return; + } const coords = getCanvasCoordinates(event); const dropTargetStackId = findStackAt(coords.x, coords.y); const sourceStackIndex = draggedCards.sourceStackIndex; - - // 🔴 로그 추가: 드래그가 끝났을 때의 상태를 확인 - console.log('--- 드래그 종료 ---'); - console.log('드래그된 카드 묶음:', draggedCards.map(c => `${getRankText(c.rank)} of ${c.suit}`)); - console.log('드롭 대상 스택 ID:', dropTargetStackId); - console.log('------------------'); - if (dropTargetStackId) { const destinationStackIndex = (parseInt(dropTargetStackId.split('-')[1]) || 1) - 1; const isValid = isValidMove(draggedCards, destinationStackIndex); - - // 🔴 로그 추가: isValidMove가 실행될 때의 정보를 확인 - console.log('isValidMove 체크 시작'); - console.log('대상 스택의 맨 위 카드:', currentGame.tableau[destinationStackIndex].length > 0 ? currentGame.tableau[destinationStackIndex][currentGame.tableau[destinationStackIndex].length - 1] : '없음'); - console.log('이동할 묶음의 첫 카드:', draggedCards[0]); - console.log('결과:', isValid); - console.log('------------------'); - if (isValid) { moveCardLocally(draggedCards, sourceStackIndex, destinationStackIndex); + checkCompletedStacks(); updateGameOnServer(currentGame); } else { - draggedCards.forEach(card => { - delete card.x; - delete card.y; - }); + returnToOriginalPosition(); } } else { - draggedCards.forEach(card => { - delete card.x; - delete card.y; - }); + returnToOriginalPosition(); } + isDragging = false; draggedCards = []; draw(); } + function returnToOriginalPosition() { + isDragging = false; + draggedCards = []; + } + function handleStockClick() { - if (!currentGame || isAnimating) return; + if (!currentGame || isAnimating || currentGame.stock.length === 0) return; dealFromStock(); } function handleDoubleClick(event) { const coords = getCanvasCoordinates(event); const clickedCardData = findCardAt(coords.x, coords.y); - if (clickedCardData) { - const { card, stackIndex } = clickedCardData; - const movableStack = getCardStackForMove(card, stackIndex); - + const { card, stackIndex, cardIndex } = clickedCardData; + const movableStack = getCardStackForMove(card, stackIndex, cardIndex); if (movableStack) { const destinationStackId = getBestMoveForStack(movableStack); - if (destinationStackId) { const destinationStackIndex = (parseInt(destinationStackId.split('-')[1]) || 1) - 1; moveCardLocally(movableStack, stackIndex, destinationStackIndex); + checkCompletedStacks(); updateGameOnServer(currentGame); - } else { - animateInvalidMove(card); } - } else { - animateInvalidMove(card); } } } - // === 4. 게임 로직 및 애니메이션 === - - function getBestMoveForStack(cardsToMove) { - if (cardsToMove.length === 0) return null; - const firstCardToMove = cardsToMove[0]; - - for (let i = 0; i < 10; i++) { - const destStackId = `tableau-${i + 1}`; - const destStackCards = currentGame.tableau[i]; - - if (destStackCards.length === 0) { - if (firstCardToMove.rank === 13) { - return destStackId; + function checkCompletedStacks() { + let completedCount = 0; + for (let stackIndex = 0; stackIndex < currentGame.tableau.length; stackIndex++) { + const stack = currentGame.tableau[stackIndex]; + if (stack.length < 13) continue; + const last13Cards = stack.slice(stack.length - 13); + let isCompleted = true; + for (let i = 0; i < 12; i++) { + if (last13Cards[i].rank !== last13Cards[i+1].rank + 1 || last13Cards[i].suit !== last13Cards[i+1].suit) { + isCompleted = false; + break; } } - - const destTopCard = destStackCards[destStackCards.length - 1]; - - if (firstCardToMove.rank === destTopCard.rank - 1) { - return destStackId; + if (isCompleted) { + completedCount++; + isAnimatingCompletion = true; + const cardsToRemove = stack.splice(stack.length - 13, 13); + cardsToRemove.forEach(card => { + const cardPos = getCardPosition(card, stackIndex); + card.animStartX = cardPos.x; + card.animStartY = cardPos.y; + card.animEndTime = Date.now() + 500; + card.animTargetX = (UI_ELEMENTS.foundationArea.x + currentGame.foundation.length * (cardWidth * FOUNDATION_CARD_SPACING)); + card.animTargetY = UI_ELEMENTS.foundationArea.y; + completedStackCards.push(card); + }); + if (stack.length > 0) { + stack[stack.length - 1].isFaceUp = true; + } + // 완성된 스택을 foundation에 추가 + currentGame.foundation.push(cardsToRemove); } } - return null; + // 모든 카드가 foundation으로 이동했는지 확인 + const totalFoundationCards = currentGame.foundation.reduce((sum, stack) => sum + stack.length, 0); + if (totalFoundationCards === 104) { + isGameCompleted = true; + } } function isValidMove(cardsToMove, destinationStackIndex) { if (cardsToMove.length === 0) return false; - const firstCardToMove = cardsToMove[0]; const destStackCards = currentGame.tableau[destinationStackIndex]; - if (destStackCards.length === 0) { return true; } - const destTopCard = destStackCards[destStackCards.length - 1]; if (firstCardToMove.rank === destTopCard.rank - 1) { return true; @@ -477,77 +833,47 @@ document.addEventListener('DOMContentLoaded', () => { return false; } - /** - * 🔴 수정: moveCardLocally 함수를 새로운 데이터 구조에 맞게 변경 - */ function moveCardLocally(cardsToMove, sourceStackIndex, destinationStackIndex) { - // 이동할 카드들을 소스 스택에서 제거 const sourceStack = currentGame.tableau[sourceStackIndex]; const newSourceStack = sourceStack.slice(0, sourceStack.length - cardsToMove.length); - - // 대상 스택에 카드들을 추가 const destinationStack = currentGame.tableau[destinationStackIndex]; const newDestinationStack = [...destinationStack, ...cardsToMove]; - - // 게임 상태 업데이트 const newTableau = [...currentGame.tableau]; newTableau[sourceStackIndex] = newSourceStack; newTableau[destinationStackIndex] = newDestinationStack; - - // 이전 스택의 마지막 카드 뒤집기 - if (newSourceStack.length > 0) { + if (newSourceStack.length > 0 && !newSourceStack[newSourceStack.length - 1].isFaceUp) { newSourceStack[newSourceStack.length - 1].isFaceUp = true; } - - // 서버에 보낼 데이터 구조 업데이트 currentGame.tableau = newTableau; currentGame.moves++; } - function animateCardMove(cardsToMove, destinationStackId) { - isAnimating = true; - animationProgress = 0; - - const startPos = getCardPosition(cardsToMove[0], draggedCards.sourceStackIndex); - const endPos = getCardDestinationPosition(cardsToMove, destinationStackId); - - animatedCard = { - cards: cardsToMove, - startX: startPos.x, - startY: startPos.y, - endX: endPos.x, - endY: endPos.y, - destinationStack: destinationStackId - }; - } - - function animateInvalidMove(card) { - let shakeStartTime = Date.now(); - const shakeDuration = 500; - shakenCard = { - card: card, - originalX: getCardPosition(card, findStackIndexForCard(card)).x, - originalY: getCardPosition(card, findStackIndexForCard(card)).y - }; - - function updateShake() { - const elapsedTime = Date.now() - shakeStartTime; - if (elapsedTime > shakeDuration) { - shakeOffset = 0; - dimOpacity = 0; - shakenCard = null; - return; - } - - shakeOffset = Math.sin(elapsedTime * 0.1) * 5; - dimOpacity = 0.5 * (1 - elapsedTime / shakeDuration); - requestAnimationFrame(updateShake); + // 실행 취소 핸들러 + async function handleUndo() { + if (!currentGame || isAnimating || currentGame.undoCount >= MAX_UNDO_COUNT || currentGame.undoHistory.length === 0) { + console.log("실행 취소 불가: 횟수 초과, 히스토리 없음 또는 애니메이션 진행 중"); + return; + } + try { + const response = await fetch(`${API_BASE_URL}/undo`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ gameId: currentGame.id }) + }); + const newGame = await response.json(); + currentGame = newGame; + draw(); + } catch (error) { + console.error("실행 취소 중 오류 발생:", error); } - updateShake(); } + // ======================================= + // 5. 서버 통신 함수 + // ======================================= + async function dealFromStock() { - if (!currentGame || isAnimating) return; + if (!currentGame || isAnimating || currentGame.stock.length === 0) return; isAnimating = true; try { const response = await fetch(`${API_BASE_URL}/deal`, { @@ -559,7 +885,7 @@ document.addEventListener('DOMContentLoaded', () => { currentGame = newGame; draw(); } catch (error) { - console.error('카드 분배 실패:', error); + console.error("카드 분배 중 오류 발생:", error); } finally { isAnimating = false; } @@ -574,61 +900,76 @@ document.addEventListener('DOMContentLoaded', () => { }); const newGame = await response.json(); currentGame = newGame; + isDragging = false; + draggedCards = []; draw(); } catch (error) { - console.error('게임 업데이트 실패:', error); + console.error("게임 상태 업데이트 중 오류 발생:", error); } } - // === 5. 헬퍼 함수 === + async function startNewGame() { + if (!assetsLoaded) return; + const numSuits = selectedSuit; + const numCards = selectedCardCount; + try { + const response = await fetch(`${API_BASE_URL}/new?numSuits=${numSuits}&numCards=${numCards}`); + currentGame = await response.json(); + gameId = currentGame.id; + isDragging = false; + draggedCards = []; + isGameCompleted = false; // 새 게임 시작 시 완료 상태 초기화 + draw(); + } catch (error) { + console.error("새 게임 시작 중 오류 발생:", error); + } + } + // ======================================= + // 6. 기타 유틸리티 함수 + // ======================================= function findStackAt(x, y) { - const startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2; const startY = cardHeight * 0.5; - for (let i = 0; i < 10; i++) { - const stackX = startX + i * (cardWidth + cardWidth * 0.5); + const stackX = tableauStartX + i * (cardWidth + cardGapX); const stackCards = currentGame.tableau[i]; - const lastCardIndex = stackCards.length > 0 ? stackCards.length - 1 : -1; - const stackY = lastCardIndex >= 0 ? startY + lastCardIndex * cardOverlapY : startY; - const stackHeight = lastCardIndex >= 0 ? cardHeight : cardHeight + (15 * cardOverlapY); - - if (x >= stackX && x <= stackX + cardWidth && y >= stackY && y <= stackY + stackHeight) { + if (stackCards.length === 0) { + if (x >= stackX && x <= stackX + cardWidth && y >= startY) { + return `tableau-${i + 1}`; + } + } + const lastCardIndex = stackCards.length - 1; + const lastCardY = startY + lastCardIndex * cardOverlapY; + if (x >= stackX && x <= stackX + cardWidth && y >= lastCardY) { return `tableau-${i + 1}`; } } return null; } - function findStackIndexForCard(card) { - for(let i = 0; i < currentGame.tableau.length; i++) { - if (currentGame.tableau[i].includes(card)) { - return i; + function findCardAt(x, y) { + for (let stackIndex = 9; stackIndex >= 0; stackIndex--) { + const stackCards = currentGame.tableau[stackIndex]; + for (let cardIndex = stackCards.length - 1; cardIndex >= 0; cardIndex--) { + const card = stackCards[cardIndex]; + if (!card.isFaceUp) continue; + if (x >= card.x && x <= card.x + card.width && y >= card.y && y <= card.y + card.touchHeight) { + return { card, stackIndex, cardIndex }; + } } } - return -1; + return null; } function getCardPosition(card, stackIndex) { - const startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2; const startY = cardHeight * 0.5; const stackCards = currentGame.tableau[stackIndex]; const cardIndexInStack = stackCards.findIndex(c => c.suit === card.suit && c.rank === card.rank); - const x = startX + stackIndex * (cardWidth + cardWidth * 0.5); + const x = tableauStartX + stackIndex * (cardWidth + cardGapX); const y = startY + cardIndexInStack * cardOverlapY; return { x, y }; } - function getCardDestinationPosition(cardsToMove, destinationStackId) { - const startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2; - const startY = cardHeight * 0.5; - const destStackIndex = (parseInt(destinationStackId.split('-')[1]) || 1) - 1; - const destStackCards = currentGame.tableau[destStackIndex]; - const x = startX + destStackIndex * (cardWidth + cardWidth * 0.5); - const y = startY + destStackCards.length * cardOverlapY; - return { x, y }; - } - function getRankText(rank) { if (rank === 1) return 'A'; if (rank === 11) return 'J'; @@ -644,18 +985,25 @@ document.addEventListener('DOMContentLoaded', () => { if (suit === 'diamond') return '♦️'; } - async function startNewGame() { - if (!assetsLoaded) return; - const numSuits = suitCountSelect.value; - const numCards = cardCountSelect.value; - try { - const response = await fetch(`${API_BASE_URL}/new?numSuits=${numSuits}&numCards=${numCards}`); - currentGame = await response.json(); - draw(); - } catch (error) { - console.error('새 게임 시작 실패:', error); + function getBestMoveForStack(cardsToMove) { + if (cardsToMove.length === 0) return null; + const firstCardToMove = cardsToMove[0]; + for (let i = 0; i < 10; i++) { + const destStackId = `tableau-${i + 1}`; + const destStackCards = currentGame.tableau[i]; + if (destStackCards.length === 0) { + return destStackId; + } else { + const destTopCard = destStackCards[destStackCards.length - 1]; + if (firstCardToMove.rank === destTopCard.rank - 1) { + return destStackId; + } + } } + return null; } - updateCardCountOptions(); + // 초기화 + resizeCanvas(); + draw(); }); \ No newline at end of file diff --git a/src/main/resources/static/js/tj.js b/src/main/resources/static/js/tj.js new file mode 100644 index 0000000..200b54e --- /dev/null +++ b/src/main/resources/static/js/tj.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
    ",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0' + + '' + + $this.text() + + '' + ); + }); + + return b.join(''); + + }; + + /** + * 특정 요소를 슬라이드 아웃 패널로 변환합니다. + * @param {object} userConfig 사용자 설정 객체 + * @return {jQuery} jQuery 객체 + */ + $.fn.panel = function(userConfig) { + + // 요소가 없으면 반환 + if (this.length == 0) + return $this; + + // 여러 요소에 적용될 경우, 각각에 대해 재귀적으로 호출 + if (this.length > 1) { + for (var i=0; i < this.length; i++) + $(this[i]).panel(userConfig); + return $this; + } + + // 변수 설정 + var $this = $(this), + $body = $('body'), + $window = $(window), + id = $this.attr('id'), + config; + + // 기본 설정과 사용자 설정을 병합 + config = $.extend({ + delay: 0, // 지연 시간 + hideOnClick: false, // 링크 클릭 시 패널 숨김 여부 + hideOnEscape: false, // ESC 키 누를 시 패널 숨김 여부 + hideOnSwipe: false, // 스와이프 시 패널 숨김 여부 + resetScroll: false, // 숨길 때 스크롤 리셋 여부 + resetForms: false, // 숨길 때 폼 리셋 여부 + side: null, // 패널이 나타날 위치 (top, bottom, left, right) + target: $this, // 패널이 보일 때 클래스가 적용될 대상 + visibleClass: 'visible' // 패널이 보일 때 적용될 클래스 이름 + }, userConfig); + + // 패널 숨기기 내부 함수 + $this._hide = function(event) { + if (!config.target.hasClass(config.visibleClass)) + return; + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + config.target.removeClass(config.visibleClass); + window.setTimeout(function() { + if (config.resetScroll) + $this.scrollTop(0); + if (config.resetForms) + $this.find('form').each(function() { + this.reset(); + }); + }, config.delay); + }; + + // 브라우저 호환성을 위한 CSS 설정 + $this + .css('-ms-overflow-style', '-ms-autohiding-scrollbar') + .css('-webkit-overflow-scrolling', 'touch'); + + // 링크 클릭 시 패널 숨기기 이벤트 핸들러 + if (config.hideOnClick) { + + $this.find('a') + .css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)'); + + $this + .on('click', 'a', function(event) { + + var $a = $(this), + href = $a.attr('href'), + target = $a.attr('target'); + + // --- ⬇️ 수정된 로직 시작 ⬇️ --- + + // 1. 팝업 링크인지 먼저 확인합니다. + // 링크에 'open-login-popup' 클래스가 있으면 메뉴를 닫고, 이 핸들러의 동작은 여기서 종료합니다. + // (팝업을 여는 동작은 common.js에 있는 다른 이벤트 핸들러가 처리합니다.) + if ($a.hasClass('open-login-popup')) { + $this._hide(); + return; + } + + // 2. 기존의 일반 링크(페이지 이동) 처리 로직은 그대로 둡니다. + if (!href || href == '#' || href == '' || href == '#' + id) + return; + + // --- ⬆️ 수정된 로직 끝 ⬆️ --- + + // Cancel original event. + event.preventDefault(); + event.stopPropagation(); + + // Hide panel. + $this._hide(); + + // Redirect to href. + window.setTimeout(function() { + if (target == '_blank') + window.open(href); + else + window.location.href = href; + }, config.delay + 10); + + }); + } + + // 터치 및 스와이프 이벤트 핸들러 + $this.on('touchstart', function(event) { + $this.touchPosX = event.originalEvent.touches[0].pageX; + $this.touchPosY = event.originalEvent.touches[0].pageY; + }); + $this.on('touchmove', function(event) { + if ($this.touchPosX === null || $this.touchPosY === null) return; + var diffX = $this.touchPosX - event.originalEvent.touches[0].pageX, + diffY = $this.touchPosY - event.originalEvent.touches[0].pageY, + th = $this.outerHeight(), + ts = ($this.get(0).scrollHeight - $this.scrollTop()); + + if (config.hideOnSwipe) { + var result = false, boundary = 20, delta = 50; + switch (config.side) { + case 'left': result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta); break; + case 'right': result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta)); break; + case 'top': result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY > delta); break; + case 'bottom': result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY < (-1 * delta)); break; + default: break; + } + if (result) { + $this.touchPosX = null; + $this.touchPosY = null; + $this._hide(); + return false; + } + } + + if (($this.scrollTop() < 0 && diffY < 0) || (ts > (th - 2) && ts < (th + 2) && diffY > 0)) { + event.preventDefault(); + event.stopPropagation(); + } + }); + + // 패널 내부에서 발생하는 이벤트가 상위로 전파되는 것을 방지 + $this.on('click touchend touchstart touchmove', function(event) { + event.stopPropagation(); + }); + + // 패널 ID를 가리키는 링크 클릭 시 패널 숨기기 + $this.on('click', 'a[href="#' + id + '"]', function(event) { + event.preventDefault(); + event.stopPropagation(); + config.target.removeClass(config.visibleClass); + }); + + // body 클릭 시 패널 숨기기 + $body.on('click touchend', function(event) { + $this._hide(event); + }); + + // 패널을 여는 링크(토글)에 대한 이벤트 핸들러 + $body.on('click', 'a[href="#' + id + '"]', function(event) { + event.preventDefault(); + event.stopPropagation(); + config.target.toggleClass(config.visibleClass); + }); + + // ESC 키 누를 시 패널 숨기기 + if (config.hideOnEscape) { + $window.on('keydown', function(event) { + if (event.keyCode == 27) $this._hide(event); + }); + } + + return $this; + }; + + /** + * 구형 브라우저에서 input의 'placeholder' 속성을 지원하기 위한 پلی필(Polyfill)입니다. + * @return {jQuery} jQuery 객체 + */ + $.fn.placeholder = function() { + if (typeof (document.createElement('input')).placeholder != 'undefined') + return $(this); + if (this.length == 0) return $this; + if (this.length > 1) { + for (var i=0; i < this.length; i++) $(this[i]).placeholder(); + return $this; + } + var $this = $(this); + $this.find('input[type=text],textarea').each(function() { + var i = $(this); + if (i.val() == '' || i.val() == i.attr('placeholder')) + i.addClass('polyfill-placeholder').val(i.attr('placeholder')); + }).on('blur', function() { + var i = $(this); + if (i.attr('name').match(/-polyfill-field$/)) return; + if (i.val() == '') + i.addClass('polyfill-placeholder').val(i.attr('placeholder')); + }).on('focus', function() { + var i = $(this); + if (i.attr('name').match(/-polyfill-field$/)) return; + if (i.val() == i.attr('placeholder')) + i.removeClass('polyfill-placeholder').val(''); + }); + $this.find('input[type=password]').each(function() { + var i = $(this); + var x = $($('
    ').append(i.clone()).remove().html().replace(/type="password"/i, 'type="text"').replace(/type=password/i, 'type=text')); + if (i.attr('id') != '') x.attr('id', i.attr('id') + '-polyfill-field'); + if (i.attr('name') != '') x.attr('name', i.attr('name') + '-polyfill-field'); + x.addClass('polyfill-placeholder').val(x.attr('placeholder')).insertAfter(i); + if (i.val() == '') i.hide(); else x.hide(); + i.on('blur', function(event) { + event.preventDefault(); + var x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]'); + if (i.val() == '') { i.hide(); x.show(); } + }); + x.on('focus', function(event) { + event.preventDefault(); + var i = x.parent().find('input[name=' + x.attr('name').replace('-polyfill-field', '') + ']'); + x.hide(); + i.show().focus(); + }).on('keypress', function(event) { + event.preventDefault(); + x.val(''); + }); + }); + $this.on('submit', function() { + $this.find('input[type=text],input[type=password],textarea').each(function(event) { + var i = $(this); + if (i.attr('name').match(/-polyfill-field$/)) i.attr('name', ''); + if (i.val() == i.attr('placeholder')) { + i.removeClass('polyfill-placeholder'); + i.val(''); + } + }); + }).on('reset', function(event) { + event.preventDefault(); + $this.find('select').val($('option:first').val()); + $this.find('input,textarea').each(function() { + var i = $(this), x; + i.removeClass('polyfill-placeholder'); + switch (this.type) { + case 'submit': case 'reset': break; + case 'password': + i.val(i.attr('defaultValue')); + x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]'); + if (i.val() == '') { i.hide(); x.show(); } else { i.show(); x.hide(); } + break; + case 'checkbox': case 'radio': i.attr('checked', i.attr('defaultValue')); break; + case 'text': case 'textarea': + i.val(i.attr('defaultValue')); + if (i.val() == '') { i.addClass('polyfill-placeholder'); i.val(i.attr('placeholder')); } + break; + default: i.val(i.attr('defaultValue')); break; + } + }); + }); + return $this; + }; + + /** + * 특정 조건에 따라 요소의 순서를 부모 요소의 맨 앞으로 이동시키거나 원래 위치로 되돌립니다. + * (주로 반응형 레이아웃에서 요소의 위치를 변경할 때 사용됩니다.) + * @param {jQuery} $elements 이동시킬 요소 + * @param {bool} condition true이면 맨 앞으로, false이면 원래 위치로 이동 + */ + $.prioritize = function($elements, condition) { + var key = '__prioritize'; + if (typeof $elements != 'jQuery') $elements = $($elements); + $elements.each(function() { + var $e = $(this), $p, $parent = $e.parent(); + if ($parent.length == 0) return; + if (!$e.data(key)) { + if (!condition) return; + $p = $e.prev(); + if ($p.length == 0) return; + $e.prependTo($parent); + $e.data(key, $p); + } else { + if (condition) return; + $p = $e.data(key); + $e.insertAfter($p); + $e.removeData(key); + } + }); + }; + +})(jQuery); \ No newline at end of file diff --git a/src/main/resources/static/assets/webfonts/fa-brands-400.eot b/src/main/resources/static/webfonts/fa-brands-400.eot similarity index 100% rename from src/main/resources/static/assets/webfonts/fa-brands-400.eot rename to src/main/resources/static/webfonts/fa-brands-400.eot diff --git a/src/main/resources/static/assets/webfonts/fa-brands-400.svg b/src/main/resources/static/webfonts/fa-brands-400.svg similarity index 100% rename from src/main/resources/static/assets/webfonts/fa-brands-400.svg rename to src/main/resources/static/webfonts/fa-brands-400.svg diff --git a/src/main/resources/static/assets/webfonts/fa-brands-400.ttf b/src/main/resources/static/webfonts/fa-brands-400.ttf similarity index 100% rename from src/main/resources/static/assets/webfonts/fa-brands-400.ttf rename to src/main/resources/static/webfonts/fa-brands-400.ttf diff --git a/src/main/resources/static/assets/webfonts/fa-brands-400.woff b/src/main/resources/static/webfonts/fa-brands-400.woff similarity index 100% rename from src/main/resources/static/assets/webfonts/fa-brands-400.woff rename to src/main/resources/static/webfonts/fa-brands-400.woff diff --git a/src/main/resources/static/assets/webfonts/fa-brands-400.woff2 b/src/main/resources/static/webfonts/fa-brands-400.woff2 similarity index 100% rename from src/main/resources/static/assets/webfonts/fa-brands-400.woff2 rename to src/main/resources/static/webfonts/fa-brands-400.woff2 diff --git a/src/main/resources/static/assets/webfonts/fa-regular-400.eot b/src/main/resources/static/webfonts/fa-regular-400.eot similarity index 100% rename from src/main/resources/static/assets/webfonts/fa-regular-400.eot rename to src/main/resources/static/webfonts/fa-regular-400.eot diff --git a/src/main/resources/static/assets/webfonts/fa-regular-400.svg b/src/main/resources/static/webfonts/fa-regular-400.svg similarity index 100% rename from src/main/resources/static/assets/webfonts/fa-regular-400.svg rename to src/main/resources/static/webfonts/fa-regular-400.svg diff --git a/src/main/resources/static/assets/webfonts/fa-regular-400.ttf b/src/main/resources/static/webfonts/fa-regular-400.ttf similarity index 100% rename from src/main/resources/static/assets/webfonts/fa-regular-400.ttf rename to src/main/resources/static/webfonts/fa-regular-400.ttf diff --git a/src/main/resources/static/assets/webfonts/fa-regular-400.woff b/src/main/resources/static/webfonts/fa-regular-400.woff similarity index 100% rename from src/main/resources/static/assets/webfonts/fa-regular-400.woff rename to src/main/resources/static/webfonts/fa-regular-400.woff diff --git a/src/main/resources/static/assets/webfonts/fa-regular-400.woff2 b/src/main/resources/static/webfonts/fa-regular-400.woff2 similarity index 100% rename from src/main/resources/static/assets/webfonts/fa-regular-400.woff2 rename to src/main/resources/static/webfonts/fa-regular-400.woff2 diff --git a/src/main/resources/static/assets/webfonts/fa-solid-900.eot b/src/main/resources/static/webfonts/fa-solid-900.eot similarity index 100% rename from src/main/resources/static/assets/webfonts/fa-solid-900.eot rename to src/main/resources/static/webfonts/fa-solid-900.eot diff --git a/src/main/resources/static/assets/webfonts/fa-solid-900.svg b/src/main/resources/static/webfonts/fa-solid-900.svg similarity index 100% rename from src/main/resources/static/assets/webfonts/fa-solid-900.svg rename to src/main/resources/static/webfonts/fa-solid-900.svg diff --git a/src/main/resources/static/assets/webfonts/fa-solid-900.ttf b/src/main/resources/static/webfonts/fa-solid-900.ttf similarity index 100% rename from src/main/resources/static/assets/webfonts/fa-solid-900.ttf rename to src/main/resources/static/webfonts/fa-solid-900.ttf diff --git a/src/main/resources/static/assets/webfonts/fa-solid-900.woff b/src/main/resources/static/webfonts/fa-solid-900.woff similarity index 100% rename from src/main/resources/static/assets/webfonts/fa-solid-900.woff rename to src/main/resources/static/webfonts/fa-solid-900.woff diff --git a/src/main/resources/static/assets/webfonts/fa-solid-900.woff2 b/src/main/resources/static/webfonts/fa-solid-900.woff2 similarity index 100% rename from src/main/resources/static/assets/webfonts/fa-solid-900.woff2 rename to src/main/resources/static/webfonts/fa-solid-900.woff2 diff --git a/src/main/resources/templates/content/blog/editor.html b/src/main/resources/templates/content/blog/editor.html index 94f1e5f..fec118f 100644 --- a/src/main/resources/templates/content/blog/editor.html +++ b/src/main/resources/templates/content/blog/editor.html @@ -3,88 +3,92 @@ xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/extras/spring-security" - layout:decorate="~{layout/default_layout}" > - - - - - + + + + + + + + + - - - - - - - - - - - - +
    -

    -

    +

    글 작성/수정

    + +

    + +

    + +

    -
    +
    -

    권한이 없는 뎁쇼?!

    +

    권한이 없는 뎁쇼?!

    - -
    -
    -
    -
    - + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    -

    + -
    +
    - -

    Thank you.
    - Your registration was submitted successfully.
    - Selected invitees will be notified by e-mail on JANUARY 24th.

    - Hope to see you soon! -

    +

    Categories

    +
    + + -
    -
    +
    - -

    - test 002 -

    +

    Hashtags

    +
    + + -
    - - + + \ No newline at end of file diff --git a/src/main/resources/templates/content/blog/modify.html b/src/main/resources/templates/content/blog/modify.html index ab5d904..f4c06da 100644 --- a/src/main/resources/templates/content/blog/modify.html +++ b/src/main/resources/templates/content/blog/modify.html @@ -5,8 +5,6 @@ xmlns:sec="http://www.thymeleaf.org/extras/spring-security" layout:decorate="~{layout/default_layout}"> - - diff --git a/src/main/resources/templates/content/blog/posts.html b/src/main/resources/templates/content/blog/posts.html index c319d66..15c5885 100644 --- a/src/main/resources/templates/content/blog/posts.html +++ b/src/main/resources/templates/content/blog/posts.html @@ -5,102 +5,39 @@ xmlns:sec="http://www.thymeleaf.org/extras/spring-security" layout:decorate="~{layout/default_layout}" > - - - - - - - + + + + +
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    +
    +
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - + + \ No newline at end of file diff --git a/src/main/resources/templates/content/blog/viewer.html b/src/main/resources/templates/content/blog/viewer.html index 73a68d9..2712c15 100644 --- a/src/main/resources/templates/content/blog/viewer.html +++ b/src/main/resources/templates/content/blog/viewer.html @@ -5,57 +5,67 @@ xmlns:sec="http://www.thymeleaf.org/extras/spring-security" layout:decorate="~{layout/default_layout}" > - - - - - + + + + + + + - - - - - - - - - - - - + + + +
    - -
    +
    -

    A gigantic heading you can use for whatever

    +

    게시물 제목이 여기에 표시됩니다

    -
    +
    +
    -
    +
    +
    -
    -
    -
    -
    -
    +
    +
    +
    +
    +
    +
    +
    +
    +

    + +
    +

    Comments

    +
    + + +
    +
    +
    +
    - + + \ No newline at end of file diff --git a/src/main/resources/templates/content/blog/write.html b/src/main/resources/templates/content/blog/write.html deleted file mode 100644 index cb35439..0000000 --- a/src/main/resources/templates/content/blog/write.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    -
    -
    - -

    권한이 없는 뎁쇼?!

    -
    - -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -

    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    - -

    Thank you.
    - Your registration was submitted successfully.
    - Selected invitees will be notified by e-mail on JANUARY 24th.

    - Hope to see you soon! -

    -
    - Close -
    - -
    -
    -
    -
    -
    -
    - -

    - test 002 -

    -
    - Close -
    - -
    -
    -
    -
    - - diff --git a/src/main/resources/templates/content/home.html b/src/main/resources/templates/content/home.html index b941bf1..bad9789 100644 --- a/src/main/resources/templates/content/home.html +++ b/src/main/resources/templates/content/home.html @@ -4,13 +4,6 @@ xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/extras/spring-security" layout:decorate="~{layout/default_layout}"> - - - - -
    +

    글쓰기[Writing]

    오직 주인장 만의 권한 임요. 그냥 내가 쓰기 편하게 여기 놔둔 메뉴임. 님들은 못씀요.
    [Only the owner has the authority. This is just a menu that I put here for my convenience. You can't use it.]

    -
    +