스케치북 게시판 스킨 분석

스케치북 게시판 스킨 분석

인기 있는 스케치북 게시판 스킨의 구조와 특징을 분석하고 커스터마이징 방법을 학습합니다.

스케치북 스킨 구조

디렉토리 구조

modules/board/skins/sketchbook5/
├── skin.xml                    # 스킨 정보 및 설정
├── board.default.css          # 기본 CSS
├── board.mobile.css           # 모바일 CSS
├── js/
│   ├── board.js              # 메인 JavaScript
│   └── jquery.plugins.js     # jQuery 플러그인
├── img/                       # 이미지 리소스
├── _header.html              # 공통 헤더
├── _footer.html              # 공통 푸터
├── list.html                 # 목록 페이지
├── write_form.html           # 글쓰기 폼
├── view_document.html        # 글 보기
├── comment_form.html         # 댓글 폼
└── _comment.html             # 댓글 목록

주요 특징

  1. 반응형 디자인: 모바일/태블릿/PC 완벽 대응
  2. 다양한 리스트 타입: 웹진형, 갤러리형, FAQ형 등
  3. 풍부한 설정 옵션: 관리자에서 세밀한 커스터마이징 가능
  4. 확장 기능: 무한스크롤, 라이트박스, 소셜 공유 등

리스트 타입 분석

웹진형 리스트

<!-- 웹진형 리스트 구조 -->
<div class="bd_lst_wrp">
    <section class="bd_lst bd_zine">
        <article loop="$document_list=>$no,$document" class="zine-item">
            <!-- 썸네일 영역 -->
            <div class="thumb-area">
                {@
                    $thumb_exist = false;
                    if($document->thumbnailExists()) {
                        $thumb_exist = true;
                        $thumbnail = $document->getThumbnail($mi->thumbnail_width, $mi->thumbnail_height, $mi->thumbnail_type);
                    }
                }

                <a href="{getUrl('document_srl',$document->document_srl)}" class="thumb">
                    <img cond="$thumb_exist" src="{$thumbnail}" alt="" />
                    <span cond="!$thumb_exist" class="no-img">No Image</span>
                </a>

                <!-- 카테고리 뱃지 -->
                <span cond="$document->get('category_srl')" class="cat-badge">
                    {$category_list[$document->get('category_srl')]->title}
                </span>
            </div>

            <!-- 콘텐츠 영역 -->
            <div class="content-area">
                <h3 class="title">
                    <a href="{getUrl('document_srl',$document->document_srl)}">
                        {$document->getTitle($mi->subject_cut_size)}
                        <span class="comment-count" cond="$document->getCommentCount()">
                            [{$document->getCommentCount()}]
                        </span>
                    </a>
                </h3>

                <p class="excerpt">
                    {$document->getSummary($mi->content_cut_size)}
                </p>

                <div class="meta">
                    <span class="author">{$document->getNickName()}</span>
                    <span class="date">{$document->getRegdate('Y.m.d')}</span>
                    <span class="read">조회 {$document->get('readed_count')}</span>
                </div>
            </div>
        </article>
    </section>
</div>

갤러리형 리스트

<!-- 갤러리형 리스트 구조 -->
<section class="bd_lst bd_gallery">
    <ul class="gallery-container" data-columns="{$mi->gallery_cols ?: 4}">
        <li loop="$document_list=>$no,$document" class="gallery-item">
            <div class="item-inner">
                <!-- 썸네일 -->
                <div class="thumb-wrap">
                    <a href="{getUrl('document_srl',$document->document_srl)}">
                        {@$thumbnail = $document->getThumbnail($mi->thumbnail_width, $mi->thumbnail_height)}
                        <img src="{$thumbnail}" alt="" cond="$thumbnail" />
                        <span class="no-img" cond="!$thumbnail">No Image</span>

                        <!-- 호버 오버레이 -->
                        <div class="overlay">
                            <span class="view-more">자세히 보기</span>
                        </div>
                    </a>
                </div>

                <!-- 정보 -->
                <div class="info-wrap">
                    <h4 class="title">
                        <a href="{getUrl('document_srl',$document->document_srl)}">
                            {$document->getTitle()}
                        </a>
                    </h4>

                    <div class="meta">
                        <span class="date">{$document->getRegdate('Y.m.d')}</span>
                        <span class="hit">조회 {$document->get('readed_count')}</span>
                    </div>
                </div>
            </div>
        </li>
    </ul>
</section>

스케치북 고유 기능

무한 스크롤

// 스케치북의 무한 스크롤 구현
(function($) {
    var $container = $('.bd_lst');
    var loading = false;
    var page = 1;
    var total_page = window.total_page || 1;

    function loadMore() {
        if(loading || page >= total_page) return;

        loading = true;
        page++;

        $.ajax({
            url: current_url,
            data: {
                page: page,
                xhr: 1  // AJAX 요청 표시
            },
            success: function(html) {
                var $html = $(html);
                var $items = $html.find('.bd_lst > li, .bd_lst > article');

                $container.append($items);

                // 이미지 레이지 로딩 재초기화
                if(window.lazyLoadInstance) {
                    window.lazyLoadInstance.update();
                }

                loading = false;
            }
        });
    }

    // 스크롤 이벤트
    $(window).scroll(function() {
        var scrollTop = $(window).scrollTop();
        var windowHeight = $(window).height();
        var documentHeight = $(document).height();

        if(scrollTop + windowHeight > documentHeight - 200) {
            loadMore();
        }
    });
})(jQuery);

뷰어 모드

<!-- 스케치북 뷰어 모드 -->
<div class="bd_viewer" cond="$mi->use_viewer == 'Y'">
    <div class="viewer-container">
        <!-- 이전 글 -->
        <a cond="$oDocument->getPrevDocument()" 
           href="{getUrl('document_srl', $oDocument->prev_document)}" 
           class="viewer-prev">
            <i class="xi-angle-left"></i>
        </a>

        <!-- 현재 글 내용 -->
        <div class="viewer-content">
            <h1 class="title">{$oDocument->getTitle()}</h1>

            <div class="content">
                {$oDocument->getContent(false)}
            </div>

            <!-- 첨부 이미지 자동 삽입 -->
            <div class="attached-images" cond="$mi->auto_attach_image == 'Y'">
                {@$uploaded_list = $oDocument->getUploadedFiles()}
                <img loop="$uploaded_list=>$key,$file" 
                     cond="$file->direct_download == 'Y' && preg_match('/\.(jpg|jpeg|gif|png)$/i', $file->source_filename)"
                     src="{$file->download_url}" 
                     alt="" />
            </div>
        </div>

        <!-- 다음 글 -->
        <a cond="$oDocument->getNextDocument()" 
           href="{getUrl('document_srl', $oDocument->next_document)}" 
           class="viewer-next">
            <i class="xi-angle-right"></i>
        </a>
    </div>

    <!-- 키보드 네비게이션 -->
    <script>
    $(document).keydown(function(e) {
        if(e.keyCode == 37 && $('.viewer-prev').length) {
            location.href = $('.viewer-prev').attr('href');
        } else if(e.keyCode == 39 && $('.viewer-next').length) {
            location.href = $('.viewer-next').attr('href');
        }
    });
    </script>
</div>

대댓글 시스템

<!-- 스케치북 대댓글 구조 -->
<div class="comment-list">
    <article loop="$oDocument->getComments()=>$key,$comment" class="comment-item" id="comment_{$comment->comment_srl}">
        <div class="comment-header">
            <!-- 프로필 이미지 -->
            <div class="profile">
                {@$profile_image = $comment->getProfileImage()}
                <img src="{$profile_image->src}" alt="" cond="$profile_image" />
                <span class="no-profile" cond="!$profile_image">{$comment->getNickName()}</span>
            </div>

            <div class="meta">
                <span class="author">{$comment->getNickName()}</span>
                <span class="date">{$comment->getRegdate('Y.m.d H:i')}</span>
            </div>
        </div>

        <div class="comment-body">
            <!-- 대댓글인 경우 원댓글 표시 -->
            <div cond="$comment->parent_srl" class="reply-info">
                {@$parent_comment = $comment->getParentComment()}
                <span class="reply-to">@{$parent_comment->getNickName()}</span>
            </div>

            <div class="content">
                {$comment->getContent(false)}
            </div>
        </div>

        <div class="comment-footer">
            <button type="button" class="reply-btn" onclick="toggleReplyForm({$comment->comment_srl})">
                답글
            </button>

            <div class="comment-actions" cond="$comment->isGranted()">
                <button type="button" onclick="toggleModifyForm({$comment->comment_srl})">수정</button>
                <button type="button" onclick="deleteComment({$comment->comment_srl})">삭제</button>
            </div>
        </div>

        <!-- 대댓글 작성 폼 -->
        <div class="reply-write-form" id="reply_form_{$comment->comment_srl}" style="display:none;">
            <form action="./" method="post" onsubmit="return procFilter(this, insert_comment)">
                <input type="hidden" name="document_srl" value="{$oDocument->document_srl}" />
                <input type="hidden" name="parent_srl" value="{$comment->comment_srl}" />
                <textarea name="content" placeholder="답글을 입력하세요"></textarea>
                <div class="btn-area">
                    <button type="submit">답글 작성</button>
                    <button type="button" onclick="toggleReplyForm({$comment->comment_srl})">취소</button>
                </div>
            </form>
        </div>
    </article>
</div>

스킨 설정 활용

skin.xml 분석

<!-- 스케치북 skin.xml 주요 설정 -->
<skin version="0.2">
    <title xml:lang="ko">스케치북5</title>
    <description xml:lang="ko">반응형 게시판 스킨</description>
    <version>5.0</version>
    <date>2024-01-01</date>
    <author>
        <name>개발자명</name>
        <email>email@example.com</email>
    </author>

    <extra_vars>
        <!-- 리스트 타입 -->
        <var name="list_type" type="select">
            <title xml:lang="ko">목록 타입</title>
            <options value="webzine">웹진형</options>
            <options value="gallery">갤러리형</options>
            <options value="list">리스트형</options>
            <options value="faq">FAQ형</options>
        </var>

        <!-- 썸네일 설정 -->
        <var name="thumbnail_width" type="text">
            <title xml:lang="ko">썸네일 가로 크기</title>
            <default>200</default>
        </var>

        <var name="thumbnail_height" type="text">
            <title xml:lang="ko">썸네일 세로 크기</title>
            <default>150</default>
        </var>

        <!-- 기능 설정 -->
        <var name="use_infinite_scroll" type="select">
            <title xml:lang="ko">무한 스크롤 사용</title>
            <options value="Y">사용</options>
            <options value="N">사용 안함</options>
        </var>

        <var name="use_viewer" type="select">
            <title xml:lang="ko">뷰어 모드 사용</title>
            <options value="Y">사용</options>
            <options value="N">사용 안함</options>
        </var>
    </extra_vars>
</skin>

설정값 활용

<!-- 스킨 설정값 사용 예제 -->
{@
    // 기본값 설정
    if(!$mi->list_type) $mi->list_type = 'webzine';
    if(!$mi->thumbnail_width) $mi->thumbnail_width = 200;
    if(!$mi->thumbnail_height) $mi->thumbnail_height = 150;
    if(!$mi->gallery_cols) $mi->gallery_cols = 4;
}

<!-- 리스트 타입에 따른 분기 -->
<include cond="$mi->list_type == 'webzine'" target="_list_webzine.html" />
<include cond="$mi->list_type == 'gallery'" target="_list_gallery.html" />
<include cond="$mi->list_type == 'list'" target="_list_normal.html" />
<include cond="$mi->list_type == 'faq'" target="_list_faq.html" />

<!-- 반응형 갤러리 컬럼 수 -->
<style cond="$mi->list_type == 'gallery'">
@media (min-width: 768px) {
    .gallery-item { width: {100 / $mi->gallery_cols}%; }
}
@media (max-width: 767px) {
    .gallery-item { width: 50%; }
}
@media (max-width: 480px) {
    .gallery-item { width: 100%; }
}
</style>

커스터마이징 예제

새로운 리스트 타입 추가

<!-- 카드형 리스트 추가 -->
<!-- _list_card.html -->
<section class="bd_lst bd_card">
    <div class="card-container">
        <div loop="$document_list=>$no,$document" class="card-item">
            <div class="card">
                <!-- 카드 헤더 -->
                <div class="card-header">
                    {@$category = $category_list[$document->get('category_srl')]}
                    <span class="category" style="background-color: {$category->color}">
                        {$category->title}
                    </span>
                    <span class="date">{$document->getRegdate('Y.m.d')}</span>
                </div>

                <!-- 카드 바디 -->
                <div class="card-body">
                    <h3 class="title">
                        <a href="{getUrl('document_srl',$document->document_srl)}">
                            {$document->getTitle()}
                        </a>
                    </h3>

                    <p class="excerpt">
                        {$document->getSummary(200)}
                    </p>

                    <!-- 태그 -->
                    <div class="tags" cond="$document->get('tag_list')">
                        <span loop="$document->get('tag_list')=>$tag" class="tag">
                            #{$tag}
                        </span>
                    </div>
                </div>

                <!-- 카드 푸터 -->
                <div class="card-footer">
                    <div class="author">
                        {@$profile = $document->getProfileImage()}
                        <img src="{$profile->src}" alt="" cond="$profile" />
                        <span>{$document->getNickName()}</span>
                    </div>

                    <div class="stats">
                        <span><i class="xi-eye"></i> {$document->get('readed_count')}</span>
                        <span><i class="xi-comment"></i> {$document->getCommentCount()}</span>
                        <span><i class="xi-heart"></i> {$document->get('voted_count')}</span>
                    </div>
                </div>
            </div>
        </div>
    </div>
</section>

<style>
.bd_card .card-container {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 20px;
}

.card {
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    overflow: hidden;
    transition: transform 0.3s;
}

.card:hover {
    transform: translateY(-5px);
    box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
</style>

모범 사례

  1. 설정 활용: skin.xml의 설정을 최대한 활용
  2. 구조 유지: 스케치북의 기본 구조 유지하며 확장
  3. 반응형 고려: 모든 커스터마이징에 반응형 적용
  4. 성능 최적화: 무거운 기능은 옵션으로 제공
  5. 호환성: 라이믹스 버전별 호환성 체크

다음 단계

스케치북 스킨 분석을 완료했다면, 레이아웃 개발로 진행하세요.