커스텀 갤러리 게시판 만들기¶
개요¶
이미지 중심의 갤러리형 게시판을 커스터마이징하는 방법을 설명합니다.
디렉토리 구조¶
skins/board/custom_gallery/
├── board.default.css
├── board.default.js
├── list.html
├── view_document.html
├── write_form.html
├── comment.html
├── info.xml
├── img/
│ └── no-image.png
└── css/
└── gallery.css
info.xml 설정¶
<?xml version="1.0" encoding="UTF-8"?>
<skin version="0.2">
<title xml:lang="ko">커스텀 갤러리</title>
<extra_vars>
<var id="columns" type="select">
<title xml:lang="ko">한 줄에 표시할 이미지 수</title>
<options value="3">3개</options>
<options value="4">4개</options>
<options value="5">5개</options>
<options value="6">6개</options>
<default>4</default>
</var>
<var id="thumbnail_width" type="text">
<title xml:lang="ko">썸네일 너비 (px)</title>
<default>300</default>
</var>
<var id="thumbnail_height" type="text">
<title xml:lang="ko">썸네일 높이 (px)</title>
<default>300</default>
</var>
<var id="thumbnail_type" type="select">
<title xml:lang="ko">썸네일 타입</title>
<options value="crop">크롭 (정사각형)</options>
<options value="ratio">비율 유지</options>
<default>crop</default>
</var>
<var id="hover_effect" type="select">
<title xml:lang="ko">호버 효과</title>
<options value="none">없음</options>
<options value="zoom">확대</options>
<options value="overlay">오버레이</options>
<default>zoom</default>
</var>
<var id="show_info" type="checkbox">
<title xml:lang="ko">이미지 정보 표시</title>
<default>Y</default>
</var>
</extra_vars>
</skin>
list.html - 갤러리 목록¶
{@
// 설정값 가져오기
$columns = $module_info->columns ?: 4;
$thumb_width = $module_info->thumbnail_width ?: 300;
$thumb_height = $module_info->thumbnail_height ?: 300;
$thumb_type = $module_info->thumbnail_type ?: 'crop';
$hover_effect = $module_info->hover_effect ?: 'zoom';
$show_info = $module_info->show_info == 'Y';
}
<load target="css/gallery.css" />
<load target="board.default.js" />
<div class="board-gallery" data-columns="{$columns}">
<!-- 카테고리 필터 -->
<!--@if($module_info->use_category == 'Y' && $category_list)-->
<div class="category-filter">
<a href="{getUrl('category','')}" class="<!--@if(!$category)-->active<!--@end-->">전체</a>
<!--@foreach($category_list as $val)-->
<a href="{getUrl('category',$val->category_srl)}"
class="<!--@if($category == $val->category_srl)-->active<!--@end-->">
{$val->title} ({$val->document_count})
</a>
<!--@end-->
</div>
<!--@end-->
<!-- 갤러리 그리드 -->
<div class="gallery-grid columns-{$columns} effect-{$hover_effect}">
<!--@foreach($document_list as $no => $document)-->
{@
// 썸네일 추출
$thumbnail = '';
if($document->thumbnailExists()) {
$thumbnail = $document->getThumbnail($thumb_width, $thumb_height, $thumb_type);
} else {
// 본문에서 이미지 추출
preg_match('/<img[^>]+src="([^"]+)"/', $document->getContent(false), $matches);
if($matches[1]) {
$thumbnail = $matches[1];
}
}
if(!$thumbnail) {
$thumbnail = $skin_path . 'img/no-image.png';
}
}
<div class="gallery-item">
<a href="{$document->getPermanentUrl()}" class="gallery-link">
<div class="gallery-image">
<img src="{$thumbnail}" alt="{$document->getTitleText()}" />
<!--@if($hover_effect == 'overlay')-->
<div class="overlay">
<div class="overlay-content">
<h4>{$document->getTitleText()}</h4>
<p>{$document->getSummary(100)}</p>
</div>
</div>
<!--@end-->
</div>
<!--@if($show_info)-->
<div class="gallery-info">
<h3 class="title">{$document->getTitleText()}</h3>
<div class="meta">
<span class="author">{$document->getNickName()}</span>
<span class="date">{$document->getRegdate('Y.m.d')}</span>
<span class="views">조회 {$document->get('readed_count')}</span>
</div>
</div>
<!--@end-->
</a>
<!-- 추가 액션 -->
<div class="gallery-actions">
<!--@if($document->get('comment_count'))-->
<span class="comments" title="댓글">
<i class="xi-message-o"></i> {$document->get('comment_count')}
</span>
<!--@end-->
<!--@if($document->get('voted_count'))-->
<span class="votes" title="추천">
<i class="xi-heart-o"></i> {$document->get('voted_count')}
</span>
<!--@end-->
</div>
</div>
<!--@end-->
</div>
<!-- 페이지 네비게이션 -->
<div class="pagination">
<a href="{getUrl('page','','module_srl','')}" class="direction">
<i class="xi-angle-left"></i><i class="xi-angle-left"></i>
</a>
<!--@if($page_navigation->first_page > 1)-->
<a href="{getUrl('page',$page_navigation->first_page-1)}" class="direction">
<i class="xi-angle-left"></i>
</a>
<!--@end-->
<!--@foreach($page_navigation->page_list as $page_no)-->
<a href="{getUrl('page',$page_no)}" class="<!--@if($page == $page_no)-->active<!--@end-->">
{$page_no}
</a>
<!--@end-->
<!--@if($page_navigation->last_page < $page_navigation->total_page)-->
<a href="{getUrl('page',$page_navigation->last_page+1)}" class="direction">
<i class="xi-angle-right"></i>
</a>
<!--@end-->
<a href="{getUrl('page',$page_navigation->last_page)}" class="direction">
<i class="xi-angle-right"></i><i class="xi-angle-right"></i>
</a>
</div>
<!-- 글쓰기 버튼 -->
<!--@if($grant->write_document)-->
<div class="write-button">
<a href="{getUrl('act','dispBoardWrite')}" class="btn btn-primary">
<i class="xi-pen"></i> 글쓰기
</a>
</div>
<!--@end-->
</div>
gallery.css - 스타일시트¶
/* 갤러리 기본 스타일 */
.board-gallery {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 카테고리 필터 */
.category-filter {
margin-bottom: 30px;
text-align: center;
}
.category-filter a {
display: inline-block;
padding: 8px 16px;
margin: 0 5px;
border: 1px solid #ddd;
border-radius: 20px;
color: #666;
text-decoration: none;
transition: all 0.3s;
}
.category-filter a.active,
.category-filter a:hover {
background: #333;
color: #fff;
border-color: #333;
}
/* 갤러리 그리드 */
.gallery-grid {
display: grid;
gap: 20px;
margin-bottom: 40px;
}
.gallery-grid.columns-3 {
grid-template-columns: repeat(3, 1fr);
}
.gallery-grid.columns-4 {
grid-template-columns: repeat(4, 1fr);
}
.gallery-grid.columns-5 {
grid-template-columns: repeat(5, 1fr);
}
.gallery-grid.columns-6 {
grid-template-columns: repeat(6, 1fr);
}
/* 갤러리 아이템 */
.gallery-item {
position: relative;
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
transition: transform 0.3s, box-shadow 0.3s;
}
.gallery-link {
display: block;
text-decoration: none;
color: inherit;
}
/* 이미지 컨테이너 */
.gallery-image {
position: relative;
width: 100%;
padding-bottom: 100%; /* 1:1 비율 */
overflow: hidden;
}
.gallery-image img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s;
}
/* 호버 효과 - 확대 */
.gallery-grid.effect-zoom .gallery-item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0,0,0,0.2);
}
.gallery-grid.effect-zoom .gallery-item:hover img {
transform: scale(1.1);
}
/* 호버 효과 - 오버레이 */
.gallery-grid.effect-overlay .overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
color: #fff;
opacity: 0;
transition: opacity 0.3s;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.gallery-grid.effect-overlay .gallery-item:hover .overlay {
opacity: 1;
}
.overlay-content {
text-align: center;
}
.overlay-content h4 {
font-size: 18px;
margin-bottom: 10px;
}
.overlay-content p {
font-size: 14px;
line-height: 1.5;
}
/* 갤러리 정보 */
.gallery-info {
padding: 15px;
}
.gallery-info .title {
font-size: 16px;
margin: 0 0 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gallery-info .meta {
font-size: 12px;
color: #999;
}
.gallery-info .meta span {
margin-right: 10px;
}
/* 갤러리 액션 */
.gallery-actions {
position: absolute;
top: 10px;
right: 10px;
display: flex;
gap: 10px;
}
.gallery-actions span {
background: rgba(0,0,0,0.6);
color: #fff;
padding: 5px 10px;
border-radius: 20px;
font-size: 12px;
}
/* 반응형 디자인 */
@media (max-width: 1200px) {
.gallery-grid.columns-6 {
grid-template-columns: repeat(5, 1fr);
}
}
@media (max-width: 992px) {
.gallery-grid.columns-5,
.gallery-grid.columns-6 {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 768px) {
.gallery-grid.columns-4,
.gallery-grid.columns-5,
.gallery-grid.columns-6 {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 576px) {
.gallery-grid {
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
}
/* 페이지네이션 */
.pagination {
text-align: center;
margin: 40px 0;
}
.pagination a {
display: inline-block;
padding: 8px 12px;
margin: 0 2px;
border: 1px solid #ddd;
color: #666;
text-decoration: none;
transition: all 0.3s;
}
.pagination a.active,
.pagination a:hover {
background: #333;
color: #fff;
border-color: #333;
}
/* 글쓰기 버튼 */
.write-button {
text-align: right;
margin-top: 20px;
}
.btn {
display: inline-block;
padding: 10px 20px;
background: #333;
color: #fff;
text-decoration: none;
border-radius: 4px;
transition: background 0.3s;
}
.btn:hover {
background: #555;
}
JavaScript 인터랙션¶
// board.default.js
jQuery(function($) {
// 이미지 지연 로딩
const images = document.querySelectorAll('.gallery-image img');
const imageOptions = {
threshold: 0,
rootMargin: '0px 0px 50px 0px'
};
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
const src = img.getAttribute('data-src');
if (src) {
img.src = src;
img.removeAttribute('data-src');
}
observer.unobserve(img);
}
});
}, imageOptions);
images.forEach(img => imageObserver.observe(img));
// 라이트박스 기능
$('.gallery-image').on('click', function(e) {
e.preventDefault();
const imgSrc = $(this).find('img').attr('src');
const lightbox = $('<div class="lightbox">')
.append($('<img>').attr('src', imgSrc))
.append($('<span class="close">×</span>'))
.appendTo('body');
lightbox.on('click', function() {
$(this).remove();
});
});
// 무한 스크롤 (선택적)
if ($('.board-gallery').data('infinite-scroll')) {
let loading = false;
let page = 2;
$(window).on('scroll', function() {
if (loading) return;
const scrollHeight = $(document).height();
const scrollPosition = $(window).height() + $(window).scrollTop();
if ((scrollHeight - scrollPosition) < 200) {
loading = true;
$.ajax({
url: request_uri,
type: 'GET',
data: {
page: page,
mid: current_mid
},
success: function(data) {
const items = $(data).find('.gallery-item');
$('.gallery-grid').append(items);
page++;
loading = false;
}
});
}
});
}
});
반응형 이미지 처리¶
<!-- write_form.html 일부 -->
<script>
// 이미지 업로드 시 자동 리사이징
$(function() {
$('#fileUpload').on('change', function(e) {
const files = e.target.files;
Array.from(files).forEach(file => {
if (!file.type.match('image.*')) return;
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 최대 크기 설정
const maxWidth = 1920;
const maxHeight = 1080;
let width = img.width;
let height = img.height;
if (width > maxWidth || height > maxHeight) {
const ratio = Math.min(maxWidth / width, maxHeight / height);
width *= ratio;
height *= ratio;
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(function(blob) {
// 리사이징된 이미지 업로드
const formData = new FormData();
formData.append('file', blob, file.name);
// 업로드 처리...
}, file.type, 0.9);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
});
});
</script>