...
This commit is contained in:
parent
e8355b3048
commit
7397d403d4
@ -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<Post> = postManager.find8().awaitSingleOrNull() ?: emptyList()
|
||||
vm.modelMap["Posts"] = postsList.map { processPostForView(it) }
|
||||
vm.modelMap["searchQuery"] = q // HTML에 hidden으로 숨겨둠
|
||||
// val postsList: List<Post> = postManager.find8().awaitSingleOrNull() ?: emptyList()
|
||||
// vm.modelMap["Posts"] = postsList.map { processPostForView(it) }
|
||||
vm.modelMap["path"] = "/blog/viewer/"
|
||||
|
||||
} catch (ex: Exception) {
|
||||
|
||||
@ -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<Post, String> {
|
||||
@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<Post>
|
||||
|
||||
@Aggregation(pipeline = [
|
||||
"{ \$sort: { modifyTime: -1 } }",
|
||||
|
||||
@ -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<WebBookmark, String> {
|
||||
|
||||
|
||||
// [검색용] 키워드가 제목, 코멘트, 태그, 설명 중 포함 + 공개된 북마크 + 커서 적용
|
||||
@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<String>, keyword: String, maxTime: Long, pageable: Pageable): Flux<WebBookmark>
|
||||
// WebBookmarkRepository 인터페이스 내부에 추가
|
||||
// savedAt이 특정 시간(?1)보다 작은 것들 중 최신순 조회
|
||||
fun findByVisibilityInAndSavedAtLessThanOrderBySavedAtDesc(
|
||||
|
||||
@ -18,20 +18,18 @@ class FeedService(
|
||||
private val postRepository: PostRepository,
|
||||
private val bookmarkRepository: WebBookmarkRepository
|
||||
) {
|
||||
/**
|
||||
* @param cursorTime 클라이언트가 가지고 있는 마지막 글의 시간 (첫 요청시엔 현재시간 or 아주 큰 값)
|
||||
* @param size 한 번에 불러올 개수 (예: 10개)
|
||||
*/
|
||||
fun getGlobalFeed(cursorTime: Long?, size: Int): Mono<FeedResponse> {
|
||||
// 커서가 없으면 현재 시간으로 설정 (첫 로딩)
|
||||
val lastTime = cursorTime ?: System.currentTimeMillis()
|
||||
|
||||
// 각 저장소에서 'size' 만큼만 가져옴 (부하 최소화)
|
||||
// 기존 메서드 시그니처 변경: keyword: String? 추가
|
||||
fun getGlobalFeed(cursorTime: Long?, size: Int, keyword: String? = null): Mono<FeedResponse> {
|
||||
val lastTime = cursorTime ?: System.currentTimeMillis()
|
||||
val pageable = PageRequest.of(0, size)
|
||||
|
||||
// 1. Post 조회 (lastTime 이전 글)
|
||||
val postsFlux = postRepository.findFeedPostsBefore(lastTime, pageable)
|
||||
.map { post ->
|
||||
// 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("<.*?>"), "") ?: "" // 태그 제거
|
||||
|
||||
@ -47,10 +45,14 @@ class FeedService(
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Bookmark 조회 (lastTime 이전 글)
|
||||
val bookmarksFlux = bookmarkRepository.findByVisibilityInAndSavedAtLessThanOrderBySavedAtDesc(
|
||||
// 2. Bookmark 조회
|
||||
val bookmarksFlux = if (!keyword.isNullOrBlank()) {
|
||||
bookmarkRepository.searchBookmarks(listOf("PUBLIC"), keyword, lastTime, pageable)
|
||||
} else {
|
||||
bookmarkRepository.findByVisibilityInAndSavedAtLessThanOrderBySavedAtDesc(
|
||||
listOf("PUBLIC"), lastTime, pageable
|
||||
).map { bookmark ->
|
||||
)
|
||||
}.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<FeedResponse> {
|
||||
// // 커서가 없으면 현재 시간으로 설정 (첫 로딩)
|
||||
// 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)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
12
src/main/resources/static/js/imagesloaded.pkgd.min.js
vendored
Normal file
12
src/main/resources/static/js/imagesloaded.pkgd.min.js
vendored
Normal file
@ -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}));
|
||||
9
src/main/resources/static/js/masonry.pkgd.min.js
vendored
Normal file
9
src/main/resources/static/js/masonry.pkgd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -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}">
|
||||
<head>
|
||||
<script th:src="@{/js/imagesloaded.pkgd.min.js}"></script>
|
||||
<script th:src="@{/js/masonry.pkgd.min.js}"></script>
|
||||
<style>
|
||||
.feed-grid {
|
||||
/* 그리드 컨테이너 */
|
||||
width: 100%;
|
||||
}
|
||||
.grid-sizer, .feed-item {
|
||||
/* 반응형 너비 설정 (기존 col-4 col-12-medium 등과 호환되게 조정) */
|
||||
width: 33.333%;
|
||||
}
|
||||
|
||||
/* 모바일 화면에서는 1단으로 변경 */
|
||||
@media screen and (max-width: 980px) {
|
||||
.grid-sizer, .feed-item {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 736px) {
|
||||
.grid-sizer, .feed-item {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.feed-item {
|
||||
padding: 0 1em 2em 1em; /* 카드 간 간격 */
|
||||
float: left; /* Masonry 필수 */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<th:block layout:fragment="content" id="content">
|
||||
<section id="banner"
|
||||
th:styleappend="${randomBannerImage != null} ? |background-image: url('${apiBaseUrl}${randomBannerImage}');| : ''">
|
||||
@ -16,7 +47,20 @@
|
||||
<a href="#" class="button">더 읽으쉴?!<br>[Read More Gibberish]</a>
|
||||
</header>
|
||||
</section>
|
||||
<section class="wrapper style1" style="padding: 2em 0 1em 0;">
|
||||
<div class="container">
|
||||
<form action="/" method="get" id="search-form" style="display: flex; justify-content: center; gap: 10px;">
|
||||
<input type="text" name="q" th:value="${searchQuery}" placeholder="관심 있는 내용을 검색해보세요 (예: Spring, 일상...)"
|
||||
style="width: 50%; min-width: 300px; padding: 0.75em;" />
|
||||
<button type="submit" class="button icon solid fa-search">검색</button>
|
||||
</form>
|
||||
|
||||
<div th:if="${searchQuery}" style="text-align: center; margin-top: 1em;">
|
||||
<h3>'<span th:text="${searchQuery}" style="color: #e44c65;"></span>' 검색 결과</h3>
|
||||
<a th:href="@{/}" class="button small alt">전체 목록 돌아가기</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- <section class="wrapper style2">-->
|
||||
<!-- <div class="container">-->
|
||||
<!-- <header class="major">-->
|
||||
@ -35,7 +79,7 @@
|
||||
|
||||
<section class="wrapper style1">
|
||||
<div class="container">
|
||||
<article id="feed-container">
|
||||
<article id="feed-container" class="feed-grid">
|
||||
<section th:each="item, iterStat : ${feedItems}" class="feed-item">
|
||||
|
||||
<div th:if="${item.type.name() == 'POST'}" class="box post"
|
||||
@ -179,9 +223,11 @@
|
||||
// UI 상태 변경 (로딩 중)
|
||||
btn.style.display = 'none';
|
||||
spinner.style.display = 'inline-block';
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const query = urlParams.get('q') || '';
|
||||
|
||||
// API 호출
|
||||
fetch(`/api/feed?cursor=${cursor}`)
|
||||
// API 호출 시 q 파라미터 포함
|
||||
fetch(`/api/feed?cursor=${cursor}&q=${encodeURIComponent(query)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// 데이터가 없으면 버튼 숨기고 종료
|
||||
@ -272,5 +318,24 @@
|
||||
return '';
|
||||
}
|
||||
</script>
|
||||
/* home.html 하단 스크립트 */
|
||||
<script th:inline="javascript">
|
||||
var $grid; // Masonry 인스턴스 저장 변수
|
||||
|
||||
// 1. 페이지 로드 완료 후 초기화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var gridElement = document.querySelector('.feed-grid');
|
||||
|
||||
// 이미지 로딩이 완료된 후 레이아웃 실행 (겹침 방지)
|
||||
imagesLoaded(gridElement, function() {
|
||||
$grid = new Masonry(gridElement, {
|
||||
itemSelector: '.feed-item',
|
||||
columnWidth: '.grid-sizer',
|
||||
percentPosition: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user