커스텀 갤러리 게시판 만들기

커스텀 갤러리 게시판 만들기

개요

이미지 중심의 갤러리형 게시판을 커스터마이징하는 방법을 설명합니다.

디렉토리 구조

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">&times;</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>

관련 문서