From 7397d403d4a2ac7f9964fcd6bacc10c51ac6eb3c Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Fri, 26 Dec 2025 18:00:05 +0900 Subject: [PATCH] ... --- .../controllers/view/PostViewController.kt | 19 ++- .../back/lun/repository/PostRepository.kt | 12 ++ .../lun/repository/WebBookmarkRepository.kt | 15 ++- .../back/lun/service/FeedService.kt | 122 +++++++++++++----- .../static/js/imagesloaded.pkgd.min.js | 12 ++ .../resources/static/js/masonry.pkgd.min.js | 9 ++ .../resources/templates/content/home.html | 71 +++++++++- 7 files changed, 218 insertions(+), 42 deletions(-) create mode 100644 src/main/resources/static/js/imagesloaded.pkgd.min.js create mode 100644 src/main/resources/static/js/masonry.pkgd.min.js diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/view/PostViewController.kt b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/view/PostViewController.kt index 37e44aa..3783581 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/view/PostViewController.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/view/PostViewController.kt @@ -127,13 +127,18 @@ class PostViewController( @GetMapping("/api/feed") @ResponseBody - suspend fun getFeedMore(@RequestParam cursor: Long): FeedResponse { - return feedService.getGlobalFeed(cursor, 10).awaitSingle() + suspend fun getFeedMore( + @RequestParam cursor: Long, + @RequestParam(required = false) q: String? // 더보기 할 때도 검색어 유지 필요 + ): FeedResponse { + return feedService.getGlobalFeed(cursor, 10, q).awaitSingle() } // --- View Endpoints --- @GetMapping("/", "/home.bs") - suspend fun home(request: jakarta.servlet.http.HttpServletRequest): ResultMV { + suspend fun home( + @RequestParam(required = false) q: String?, + request: jakarta.servlet.http.HttpServletRequest): ResultMV { visitorLogService.recordVisit(request).subscribe() val vm = ResultMV("content/home") val defaultBannerImage = "/api/images/0e2bf8b1-1848-4650-b084-5b52d0815be9.jpg?type=banner" @@ -156,12 +161,14 @@ class PostViewController( vm.modelMap["gibberish"] = URLDecoder.decode(randomGibberish.content, "UTF-8") vm.modelMap["gibberishId"] = randomGibberish.id } - val feedData = feedService.getGlobalFeed(null, 10).awaitSingle() + val feedData = feedService.getGlobalFeed(null, 10, q).awaitSingle() vm.modelMap["feedItems"] = feedData.items + vm.modelMap["nextCursor"] = feedData.nextCursor // HTML에 hidden으로 숨겨둠 - val postsList: List = postManager.find8().awaitSingleOrNull() ?: emptyList() - vm.modelMap["Posts"] = postsList.map { processPostForView(it) } + vm.modelMap["searchQuery"] = q // HTML에 hidden으로 숨겨둠 +// val postsList: List = postManager.find8().awaitSingleOrNull() ?: emptyList() +// vm.modelMap["Posts"] = postsList.map { processPostForView(it) } vm.modelMap["path"] = "/blog/viewer/" } catch (ex: Exception) { diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/repository/PostRepository.kt b/src/main/kotlin/kr/lunaticbum/back/lun/repository/PostRepository.kt index 6acd908..6ee3d14 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/repository/PostRepository.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/repository/PostRepository.kt @@ -4,6 +4,7 @@ import kr.lunaticbum.back.lun.model.AggregationCount import kr.lunaticbum.back.lun.model.Post import org.springframework.data.domain.Pageable import org.springframework.data.mongodb.repository.Aggregation +import org.springframework.data.mongodb.repository.Query import org.springframework.data.mongodb.repository.ReactiveMongoRepository import org.springframework.stereotype.Repository import reactor.core.publisher.Flux @@ -11,6 +12,17 @@ import reactor.core.publisher.Mono @Repository interface PostRepository : ReactiveMongoRepository { + @Query("{ " + + " '\$and': [ " + + " { 'posting': true, 'isBlocked': false, 'modifyTime': { '\$lt': ?1 } }, " + // 기본 필터 (공개여부, 커서) + " { '\$or': [ " + + " { 'title': { '\$regex': ?0, '\$options': 'i' } }, " + + " { 'content': { '\$regex': ?0, '\$options': 'i' } }, " + + " { 'tags': { '\$regex': ?0, '\$options': 'i' } } " + + " ] } " + + " ] " + + "}") + fun searchPosts(keyword: String, maxTime: Long, pageable: Pageable): Flux @Aggregation(pipeline = [ "{ \$sort: { modifyTime: -1 } }", diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/repository/WebBookmarkRepository.kt b/src/main/kotlin/kr/lunaticbum/back/lun/repository/WebBookmarkRepository.kt index 198237a..b140aa3 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/repository/WebBookmarkRepository.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/repository/WebBookmarkRepository.kt @@ -3,6 +3,7 @@ package kr.lunaticbum.back.lun.repository import kr.lunaticbum.back.lun.model.WebBookmark import org.springframework.data.domain.Pageable import org.springframework.data.mongodb.repository.Aggregation +import org.springframework.data.mongodb.repository.Query import org.springframework.data.mongodb.repository.ReactiveMongoRepository import org.springframework.stereotype.Repository import reactor.core.publisher.Flux @@ -11,7 +12,19 @@ import reactor.core.publisher.Mono @Repository interface WebBookmarkRepository : ReactiveMongoRepository { - + // [검색용] 키워드가 제목, 코멘트, 태그, 설명 중 포함 + 공개된 북마크 + 커서 적용 + @Query("{ " + + " '\$and': [ " + + " { 'visibility': { '\$in': ?0 }, 'savedAt': { '\$lt': ?2 } }, " + + " { '\$or': [ " + + " { 'title': { '\$regex': ?1, '\$options': 'i' } }, " + + " { 'userComment': { '\$regex': ?1, '\$options': 'i' } }, " + + " { 'tags': { '\$regex': ?1, '\$options': 'i' } }, " + + " { 'description': { '\$regex': ?1, '\$options': 'i' } } " + + " ] } " + + " ] " + + "}") + fun searchBookmarks(visibilities: List, keyword: String, maxTime: Long, pageable: Pageable): Flux // WebBookmarkRepository 인터페이스 내부에 추가 // savedAt이 특정 시간(?1)보다 작은 것들 중 최신순 조회 fun findByVisibilityInAndSavedAtLessThanOrderBySavedAtDesc( diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/service/FeedService.kt b/src/main/kotlin/kr/lunaticbum/back/lun/service/FeedService.kt index cad67e9..b708101 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/service/FeedService.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/service/FeedService.kt @@ -18,39 +18,41 @@ class FeedService( private val postRepository: PostRepository, private val bookmarkRepository: WebBookmarkRepository ) { - /** - * @param cursorTime 클라이언트가 가지고 있는 마지막 글의 시간 (첫 요청시엔 현재시간 or 아주 큰 값) - * @param size 한 번에 불러올 개수 (예: 10개) - */ - fun getGlobalFeed(cursorTime: Long?, size: Int): Mono { - // 커서가 없으면 현재 시간으로 설정 (첫 로딩) - val lastTime = cursorTime ?: System.currentTimeMillis() - // 각 저장소에서 'size' 만큼만 가져옴 (부하 최소화) + // 기존 메서드 시그니처 변경: keyword: String? 추가 + fun getGlobalFeed(cursorTime: Long?, size: Int, keyword: String? = null): Mono { + val lastTime = cursorTime ?: System.currentTimeMillis() val pageable = PageRequest.of(0, size) - // 1. Post 조회 (lastTime 이전 글) - val postsFlux = postRepository.findFeedPostsBefore(lastTime, pageable) - .map { post -> - val type = if (post.postType == "GIBBERISH") ContentType.GIBBERISH else ContentType.POST - val rawContent = post.content?.replace(Regex("<.*?>"), "") ?: "" // 태그 제거 + // 1. Post 조회 (검색어가 있으면 searchPosts, 없으면 findFeedPostsBefore) + val postsFlux = if (!keyword.isNullOrBlank()) { + postRepository.searchPosts(keyword, lastTime, pageable) + } else { + postRepository.findFeedPostsBefore(lastTime, pageable) + }.map { post -> + val type = if (post.postType == "GIBBERISH") ContentType.GIBBERISH else ContentType.POST + val rawContent = post.content?.replace(Regex("<.*?>"), "") ?: "" // 태그 제거 - FeedItemDto( - id = post.id, - type = type, - title = post.title, - content = if (type == ContentType.GIBBERISH) post.content else rawContent, - thumbnail = post.thumb, - createdAt = post.modifyTime, - writer = post.writer, - url = "/blog/viewer/${post.id}" - ) - } + FeedItemDto( + id = post.id, + type = type, + title = post.title, + content = if (type == ContentType.GIBBERISH) post.content else rawContent, + thumbnail = post.thumb, + createdAt = post.modifyTime, + writer = post.writer, + url = "/blog/viewer/${post.id}" + ) + } - // 2. Bookmark 조회 (lastTime 이전 글) - val bookmarksFlux = bookmarkRepository.findByVisibilityInAndSavedAtLessThanOrderBySavedAtDesc( - listOf("PUBLIC"), lastTime, pageable - ).map { bookmark -> + // 2. Bookmark 조회 + val bookmarksFlux = if (!keyword.isNullOrBlank()) { + bookmarkRepository.searchBookmarks(listOf("PUBLIC"), keyword, lastTime, pageable) + } else { + bookmarkRepository.findByVisibilityInAndSavedAtLessThanOrderBySavedAtDesc( + listOf("PUBLIC"), lastTime, pageable + ) + }.map { bookmark -> FeedItemDto( id = bookmark.id, type = ContentType.BOOKMARK, @@ -63,15 +65,71 @@ class FeedService( ) } - // 3. 병합 후 다시 정렬하고 size 만큼 자르기 + // 3. 병합 및 정렬 (기존 동일) return Flux.merge(postsFlux, bookmarksFlux) - .sort(Comparator.comparing(FeedItemDto::createdAt).reversed()) // 최신순 정렬 - .take(size.toLong()) // 전체 중 상위 size 개만 선택 + .sort(Comparator.comparing(FeedItemDto::createdAt).reversed()) + .take(size.toLong()) .collectList() .map { items -> - // 마지막 아이템의 시간을 다음 커서로 설정 val nextCursor = if (items.isNotEmpty()) items.last().createdAt else null FeedResponse(items, nextCursor) } } + + /** + * @param cursorTime 클라이언트가 가지고 있는 마지막 글의 시간 (첫 요청시엔 현재시간 or 아주 큰 값) + * @param size 한 번에 불러올 개수 (예: 10개) + */ +// fun getGlobalFeed(cursorTime: Long?, size: Int): Mono { +// // 커서가 없으면 현재 시간으로 설정 (첫 로딩) +// val lastTime = cursorTime ?: System.currentTimeMillis() +// +// // 각 저장소에서 'size' 만큼만 가져옴 (부하 최소화) +// val pageable = PageRequest.of(0, size) +// +// // 1. Post 조회 (lastTime 이전 글) +// val postsFlux = postRepository.findFeedPostsBefore(lastTime, pageable) +// .map { post -> +// val type = if (post.postType == "GIBBERISH") ContentType.GIBBERISH else ContentType.POST +// val rawContent = post.content?.replace(Regex("<.*?>"), "") ?: "" // 태그 제거 +// +// FeedItemDto( +// id = post.id, +// type = type, +// title = post.title, +// content = if (type == ContentType.GIBBERISH) post.content else rawContent, +// thumbnail = post.thumb, +// createdAt = post.modifyTime, +// writer = post.writer, +// url = "/blog/viewer/${post.id}" +// ) +// } +// +// // 2. Bookmark 조회 (lastTime 이전 글) +// val bookmarksFlux = bookmarkRepository.findByVisibilityInAndSavedAtLessThanOrderBySavedAtDesc( +// listOf("PUBLIC"), lastTime, pageable +// ).map { bookmark -> +// FeedItemDto( +// id = bookmark.id, +// type = ContentType.BOOKMARK, +// title = bookmark.title ?: bookmark.url, +// content = bookmark.userComment ?: bookmark.description, +// thumbnail = bookmark.displayImageUrl, +// createdAt = bookmark.savedAt, +// writer = bookmark.userId, +// url = bookmark.url ?: "" +// ) +// } +// +// // 3. 병합 후 다시 정렬하고 size 만큼 자르기 +// return Flux.merge(postsFlux, bookmarksFlux) +// .sort(Comparator.comparing(FeedItemDto::createdAt).reversed()) // 최신순 정렬 +// .take(size.toLong()) // 전체 중 상위 size 개만 선택 +// .collectList() +// .map { items -> +// // 마지막 아이템의 시간을 다음 커서로 설정 +// val nextCursor = if (items.isNotEmpty()) items.last().createdAt else null +// FeedResponse(items, nextCursor) +// } +// } } \ No newline at end of file diff --git a/src/main/resources/static/js/imagesloaded.pkgd.min.js b/src/main/resources/static/js/imagesloaded.pkgd.min.js new file mode 100644 index 0000000..8ea8958 --- /dev/null +++ b/src/main/resources/static/js/imagesloaded.pkgd.min.js @@ -0,0 +1,12 @@ +/*! + * imagesLoaded PACKAGED v5.0.0 + * JavaScript is all like "You images are done yet or what?" + * MIT License + */ +!function(t,e){"object"==typeof module&&module.exports?module.exports=e():t.EvEmitter=e()}("undefined"!=typeof window?window:this,(function(){function t(){}let e=t.prototype;return e.on=function(t,e){if(!t||!e)return this;let i=this._events=this._events||{},s=i[t]=i[t]||[];return s.includes(e)||s.push(e),this},e.once=function(t,e){if(!t||!e)return this;this.on(t,e);let i=this._onceEvents=this._onceEvents||{};return(i[t]=i[t]||{})[e]=!0,this},e.off=function(t,e){let i=this._events&&this._events[t];if(!i||!i.length)return this;let s=i.indexOf(e);return-1!=s&&i.splice(s,1),this},e.emitEvent=function(t,e){let i=this._events&&this._events[t];if(!i||!i.length)return this;i=i.slice(0),e=e||[];let s=this._onceEvents&&this._onceEvents[t];for(let n of i){s&&s[n]&&(this.off(t,n),delete s[n]),n.apply(this,e)}return this},e.allOff=function(){return delete this._events,delete this._onceEvents,this},t})), + /*! + * imagesLoaded v5.0.0 + * JavaScript is all like "You images are done yet or what?" + * MIT License + */ + function(t,e){"object"==typeof module&&module.exports?module.exports=e(t,require("ev-emitter")):t.imagesLoaded=e(t,t.EvEmitter)}("undefined"!=typeof window?window:this,(function(t,e){let i=t.jQuery,s=t.console;function n(t,e,o){if(!(this instanceof n))return new n(t,e,o);let r=t;var h;("string"==typeof t&&(r=document.querySelectorAll(t)),r)?(this.elements=(h=r,Array.isArray(h)?h:"object"==typeof h&&"number"==typeof h.length?[...h]:[h]),this.options={},"function"==typeof e?o=e:Object.assign(this.options,e),o&&this.on("always",o),this.getImages(),i&&(this.jqDeferred=new i.Deferred),setTimeout(this.check.bind(this))):s.error(`Bad element for imagesLoaded ${r||t}`)}n.prototype=Object.create(e.prototype),n.prototype.getImages=function(){this.images=[],this.elements.forEach(this.addElementImages,this)};const o=[1,9,11];n.prototype.addElementImages=function(t){"IMG"===t.nodeName&&this.addImage(t),!0===this.options.background&&this.addElementBackgroundImages(t);let{nodeType:e}=t;if(!e||!o.includes(e))return;let i=t.querySelectorAll("img");for(let t of i)this.addImage(t);if("string"==typeof this.options.background){let e=t.querySelectorAll(this.options.background);for(let t of e)this.addElementBackgroundImages(t)}};const r=/url\((['"])?(.*?)\1\)/gi;function h(t){this.img=t}function d(t,e){this.url=t,this.element=e,this.img=new Image}return n.prototype.addElementBackgroundImages=function(t){let e=getComputedStyle(t);if(!e)return;let i=r.exec(e.backgroundImage);for(;null!==i;){let s=i&&i[2];s&&this.addBackground(s,t),i=r.exec(e.backgroundImage)}},n.prototype.addImage=function(t){let e=new h(t);this.images.push(e)},n.prototype.addBackground=function(t,e){let i=new d(t,e);this.images.push(i)},n.prototype.check=function(){if(this.progressedCount=0,this.hasAnyBroken=!1,!this.images.length)return void this.complete();let t=(t,e,i)=>{setTimeout((()=>{this.progress(t,e,i)}))};this.images.forEach((function(e){e.once("progress",t),e.check()}))},n.prototype.progress=function(t,e,i){this.progressedCount++,this.hasAnyBroken=this.hasAnyBroken||!t.isLoaded,this.emitEvent("progress",[this,t,e]),this.jqDeferred&&this.jqDeferred.notify&&this.jqDeferred.notify(this,t),this.progressedCount===this.images.length&&this.complete(),this.options.debug&&s&&s.log(`progress: ${i}`,t,e)},n.prototype.complete=function(){let t=this.hasAnyBroken?"fail":"done";if(this.isComplete=!0,this.emitEvent(t,[this]),this.emitEvent("always",[this]),this.jqDeferred){let t=this.hasAnyBroken?"reject":"resolve";this.jqDeferred[t](this)}},h.prototype=Object.create(e.prototype),h.prototype.check=function(){this.getIsImageComplete()?this.confirm(0!==this.img.naturalWidth,"naturalWidth"):(this.proxyImage=new Image,this.img.crossOrigin&&(this.proxyImage.crossOrigin=this.img.crossOrigin),this.proxyImage.addEventListener("load",this),this.proxyImage.addEventListener("error",this),this.img.addEventListener("load",this),this.img.addEventListener("error",this),this.proxyImage.src=this.img.currentSrc||this.img.src)},h.prototype.getIsImageComplete=function(){return this.img.complete&&this.img.naturalWidth},h.prototype.confirm=function(t,e){this.isLoaded=t;let{parentNode:i}=this.img,s="PICTURE"===i.nodeName?i:this.img;this.emitEvent("progress",[this,s,e])},h.prototype.handleEvent=function(t){let e="on"+t.type;this[e]&&this[e](t)},h.prototype.onload=function(){this.confirm(!0,"onload"),this.unbindEvents()},h.prototype.onerror=function(){this.confirm(!1,"onerror"),this.unbindEvents()},h.prototype.unbindEvents=function(){this.proxyImage.removeEventListener("load",this),this.proxyImage.removeEventListener("error",this),this.img.removeEventListener("load",this),this.img.removeEventListener("error",this)},d.prototype=Object.create(h.prototype),d.prototype.check=function(){this.img.addEventListener("load",this),this.img.addEventListener("error",this),this.img.src=this.url,this.getIsImageComplete()&&(this.confirm(0!==this.img.naturalWidth,"naturalWidth"),this.unbindEvents())},d.prototype.unbindEvents=function(){this.img.removeEventListener("load",this),this.img.removeEventListener("error",this)},d.prototype.confirm=function(t,e){this.isLoaded=t,this.emitEvent("progress",[this,this.element,e])},n.makeJQueryPlugin=function(e){(e=e||t.jQuery)&&(i=e,i.fn.imagesLoaded=function(t,e){return new n(this,t,e).jqDeferred.promise(i(this))})},n.makeJQueryPlugin(),n})); \ No newline at end of file diff --git a/src/main/resources/static/js/masonry.pkgd.min.js b/src/main/resources/static/js/masonry.pkgd.min.js new file mode 100644 index 0000000..53386ae --- /dev/null +++ b/src/main/resources/static/js/masonry.pkgd.min.js @@ -0,0 +1,9 @@ +/*! + * Masonry PACKAGED v4.2.2 + * Cascading grid layout library + * https://masonry.desandro.com + * MIT License + * by David DeSandro + */ + +!function(t,e){"function"==typeof define&&define.amd?define("jquery-bridget/jquery-bridget",["jquery"],function(i){return e(t,i)}):"object"==typeof module&&module.exports?module.exports=e(t,require("jquery")):t.jQueryBridget=e(t,t.jQuery)}(window,function(t,e){"use strict";function i(i,r,a){function h(t,e,n){var o,r="$()."+i+'("'+e+'")';return t.each(function(t,h){var u=a.data(h,i);if(!u)return void s(i+" not initialized. Cannot call methods, i.e. "+r);var d=u[e];if(!d||"_"==e.charAt(0))return void s(r+" is not a valid method");var l=d.apply(u,n);o=void 0===o?l:o}),void 0!==o?o:t}function u(t,e){t.each(function(t,n){var o=a.data(n,i);o?(o.option(e),o._init()):(o=new r(n,e),a.data(n,i,o))})}a=a||e||t.jQuery,a&&(r.prototype.option||(r.prototype.option=function(t){a.isPlainObject(t)&&(this.options=a.extend(!0,this.options,t))}),a.fn[i]=function(t){if("string"==typeof t){var e=o.call(arguments,1);return h(this,t,e)}return u(this,t),this},n(a))}function n(t){!t||t&&t.bridget||(t.bridget=i)}var o=Array.prototype.slice,r=t.console,s="undefined"==typeof r?function(){}:function(t){r.error(t)};return n(e||t.jQuery),i}),function(t,e){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",e):"object"==typeof module&&module.exports?module.exports=e():t.EvEmitter=e()}("undefined"!=typeof window?window:this,function(){function t(){}var e=t.prototype;return e.on=function(t,e){if(t&&e){var i=this._events=this._events||{},n=i[t]=i[t]||[];return-1==n.indexOf(e)&&n.push(e),this}},e.once=function(t,e){if(t&&e){this.on(t,e);var i=this._onceEvents=this._onceEvents||{},n=i[t]=i[t]||{};return n[e]=!0,this}},e.off=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){var n=i.indexOf(e);return-1!=n&&i.splice(n,1),this}},e.emitEvent=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){i=i.slice(0),e=e||[];for(var n=this._onceEvents&&this._onceEvents[t],o=0;oe;e++){var i=h[e];t[i]=0}return t}function n(t){var e=getComputedStyle(t);return e||a("Style returned "+e+". Are you running this code in a hidden iframe on Firefox? See https://bit.ly/getsizebug1"),e}function o(){if(!d){d=!0;var e=document.createElement("div");e.style.width="200px",e.style.padding="1px 2px 3px 4px",e.style.borderStyle="solid",e.style.borderWidth="1px 2px 3px 4px",e.style.boxSizing="border-box";var i=document.body||document.documentElement;i.appendChild(e);var o=n(e);s=200==Math.round(t(o.width)),r.isBoxSizeOuter=s,i.removeChild(e)}}function r(e){if(o(),"string"==typeof e&&(e=document.querySelector(e)),e&&"object"==typeof e&&e.nodeType){var r=n(e);if("none"==r.display)return i();var a={};a.width=e.offsetWidth,a.height=e.offsetHeight;for(var d=a.isBorderBox="border-box"==r.boxSizing,l=0;u>l;l++){var c=h[l],f=r[c],m=parseFloat(f);a[c]=isNaN(m)?0:m}var p=a.paddingLeft+a.paddingRight,g=a.paddingTop+a.paddingBottom,y=a.marginLeft+a.marginRight,v=a.marginTop+a.marginBottom,_=a.borderLeftWidth+a.borderRightWidth,z=a.borderTopWidth+a.borderBottomWidth,E=d&&s,b=t(r.width);b!==!1&&(a.width=b+(E?0:p+_));var x=t(r.height);return x!==!1&&(a.height=x+(E?0:g+z)),a.innerWidth=a.width-(p+_),a.innerHeight=a.height-(g+z),a.outerWidth=a.width+y,a.outerHeight=a.height+v,a}}var s,a="undefined"==typeof console?e:function(t){console.error(t)},h=["paddingLeft","paddingRight","paddingTop","paddingBottom","marginLeft","marginRight","marginTop","marginBottom","borderLeftWidth","borderRightWidth","borderTopWidth","borderBottomWidth"],u=h.length,d=!1;return r}),function(t,e){"use strict";"function"==typeof define&&define.amd?define("desandro-matches-selector/matches-selector",e):"object"==typeof module&&module.exports?module.exports=e():t.matchesSelector=e()}(window,function(){"use strict";var t=function(){var t=window.Element.prototype;if(t.matches)return"matches";if(t.matchesSelector)return"matchesSelector";for(var e=["webkit","moz","ms","o"],i=0;is?"round":"floor";r=Math[a](r),this.cols=Math.max(r,1)},n.getContainerWidth=function(){var t=this._getOption("fitWidth"),i=t?this.element.parentNode:this.element,n=e(i);this.containerWidth=n&&n.innerWidth},n._getItemLayoutPosition=function(t){t.getSize();var e=t.size.outerWidth%this.columnWidth,i=e&&1>e?"round":"ceil",n=Math[i](t.size.outerWidth/this.columnWidth);n=Math.min(n,this.cols);for(var o=this.options.horizontalOrder?"_getHorizontalColPosition":"_getTopColPosition",r=this[o](n,t),s={x:this.columnWidth*r.col,y:r.y},a=r.y+t.size.outerHeight,h=n+r.col,u=r.col;h>u;u++)this.colYs[u]=a;return s},n._getTopColPosition=function(t){var e=this._getTopColGroup(t),i=Math.min.apply(Math,e);return{col:e.indexOf(i),y:i}},n._getTopColGroup=function(t){if(2>t)return this.colYs;for(var e=[],i=this.cols+1-t,n=0;i>n;n++)e[n]=this._getColGroupY(n,t);return e},n._getColGroupY=function(t,e){if(2>e)return this.colYs[t];var i=this.colYs.slice(t,t+e);return Math.max.apply(Math,i)},n._getHorizontalColPosition=function(t,e){var i=this.horizontalColIndex%this.cols,n=t>1&&i+t>this.cols;i=n?0:i;var o=e.size.outerWidth&&e.size.outerHeight;return this.horizontalColIndex=o?i+t:this.horizontalColIndex,{col:i,y:this._getColGroupY(i,t)}},n._manageStamp=function(t){var i=e(t),n=this._getElementOffset(t),o=this._getOption("originLeft"),r=o?n.left:n.right,s=r+i.outerWidth,a=Math.floor(r/this.columnWidth);a=Math.max(0,a);var h=Math.floor(s/this.columnWidth);h-=s%this.columnWidth?0:1,h=Math.min(this.cols-1,h);for(var u=this._getOption("originTop"),d=(u?n.top:n.bottom)+i.outerHeight,l=a;h>=l;l++)this.colYs[l]=Math.max(d,this.colYs[l])},n._getContainerSize=function(){this.maxY=Math.max.apply(Math,this.colYs);var t={height:this.maxY};return this._getOption("fitWidth")&&(t.width=this._getContainerFitWidth()),t},n._getContainerFitWidth=function(){for(var t=0,e=this.cols;--e&&0===this.colYs[e];)t++;return(this.cols-t)*this.columnWidth-this.gutter},n.needsResizeLayout=function(){var t=this.containerWidth;return this.getContainerWidth(),t!=this.containerWidth},i}); \ No newline at end of file diff --git a/src/main/resources/templates/content/home.html b/src/main/resources/templates/content/home.html index b7defbe..303030d 100644 --- a/src/main/resources/templates/content/home.html +++ b/src/main/resources/templates/content/home.html @@ -4,6 +4,37 @@ xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/extras/spring-security" layout:decorate="~{layout/default_layout}"> + + + + + +
+
+
+ + +
+
+

'' 검색 결과

+ 전체 목록 돌아가기 +
+
+
@@ -35,7 +79,7 @@
-
+
response.json()) .then(data => { // 데이터가 없으면 버튼 숨기고 종료 @@ -272,5 +318,24 @@ return ''; } + /* home.html 하단 스크립트 */ + \ No newline at end of file