게시판 목록 템플릿 (list.html)
기본 목록 구조
표준 테이블 형식
<include target="_header.html" />
<!-- 게시판 상단 도구 -->
<div class="board-tools">
<div class="board-info">
<span class="total-count">전체 {number_format($total_count)}개</span>
<span class="current-page">({$page}/{$page_navigation->last_page})</span>
</div>
<div class="board-actions">
<!-- 글쓰기 버튼 -->
<a href="{getUrl('act', 'dispBoardWrite')}"
class="btn btn-primary" cond="$grant->write">
<i class="icon-edit"></i> {$lang->cmd_write}
</a>
<!-- 검색 폼 -->
<form action="{getUrl()}" method="get" class="search-form">
<input type="hidden" name="mid" value="{$mid}" />
<input type="hidden" name="category" value="{$category}" />
<select name="search_target">
<option value="title" selected="selected"|cond="$search_target == 'title'">제목</option>
<option value="content" selected="selected"|cond="$search_target == 'content'">내용</option>
<option value="title_content" selected="selected"|cond="$search_target == 'title_content'">제목+내용</option>
<option value="nick_name" selected="selected"|cond="$search_target == 'nick_name'">글쓴이</option>
</select>
<input type="text" name="search_keyword" value="{$search_keyword}" placeholder="검색어 입력" />
<button type="submit" class="btn btn-secondary">검색</button>
</form>
</div>
</div>
<!-- 게시글 목록 테이블 -->
<div class="board-list-wrapper">
<table class="board-list" cond="$document_list">
<thead>
<tr>
<th class="num">번호</th>
<th class="category" cond="$category_list && $skin_vars->show_category == 'Y'">분류</th>
<th class="title">제목</th>
<th class="author">글쓴이</th>
<th class="date">작성일</th>
<th class="hit">조회</th>
<th class="vote" cond="$module_info->use_vote == 'Y'">추천</th>
</tr>
</thead>
<tbody>
<!-- 공지사항 -->
<tr loop="$notice_list => $no, $document" class="notice">
<td class="num">
<span class="notice-icon">공지</span>
</td>
<td class="category" cond="$category_list && $skin_vars->show_category == 'Y'">
<span cond="$document->get('category_srl')">
{$category_list[$document->get('category_srl')]->title}
</span>
</td>
<td class="title">
<a href="{getUrl('document_srl', $document->document_srl)}" class="title-link">
{$document->getTitle()}
</a>
<!-- 댓글 수 -->
<span class="comment-count" cond="$document->getCommentCount() > 0">
[{$document->getCommentCount()}]
</span>
<!-- 첨부파일 아이콘 -->
<span class="attach-icon" cond="$document->getAttachedFileCount() > 0">
<i class="icon-attachment"></i>
</span>
<!-- NEW 아이콘 -->
<span class="new-icon" cond="$document->isNew()">
<i class="icon-new">N</i>
</span>
</td>
<td class="author">
<span class="nick">{$document->getNickName()}</span>
</td>
<td class="date">
{zdate($document->getRegdate(), 'Y.m.d')}
</td>
<td class="hit">
{number_format($document->getReadedCount())}
</td>
<td class="vote" cond="$module_info->use_vote == 'Y'">
{$document->getVotedCount()}
</td>
</tr>
<!-- 일반 게시글 -->
<tr loop="$document_list => $no, $document" class="document">
<td class="num">
{$document->getNumber()}
</td>
<td class="category" cond="$category_list && $skin_vars->show_category == 'Y'">
<span cond="$document->get('category_srl')">
{$category_list[$document->get('category_srl')]->title}
</span>
</td>
<td class="title">
<!-- 답글 들여쓰기 -->
<span class="reply-indent" cond="$document->get('depth') > 0">
<block loop="$i = 0; $i < $document->get('depth'); $i++">
<span class="reply-arrow">└</span>
</block>
</span>
<a href="{getUrl('document_srl', $document->document_srl)}" class="title-link">
{$document->getTitle()}
</a>
<!-- 댓글 수 -->
<span class="comment-count" cond="$document->getCommentCount() > 0">
[{$document->getCommentCount()}]
</span>
<!-- 첨부파일 아이콘 -->
<span class="attach-icon" cond="$document->getAttachedFileCount() > 0">
<i class="icon-attachment"></i>
</span>
<!-- NEW 아이콘 -->
<span class="new-icon" cond="$document->isNew()">
<i class="icon-new">N</i>
</span>
<!-- 비밀글 아이콘 -->
<span class="secret-icon" cond="$document->get('status') == 'SECRET'">
<i class="icon-lock"></i>
</span>
</td>
<td class="author">
<span class="nick">{$document->getNickName()}</span>
<!--@if($document->getMemberSrl() > 0)-->
<span class="member-icon" title="회원">M</span>
<!--@endif-->
</td>
<td class="date">
<!--@if($document->isNew())-->
{zdate($document->getRegdate(), 'H:i')}
<!--@else-->
{zdate($document->getRegdate(), 'm.d')}
<!--@endif-->
</td>
<td class="hit">
{number_format($document->getReadedCount())}
</td>
<td class="vote" cond="$module_info->use_vote == 'Y'">
{$document->getVotedCount()}
</td>
</tr>
</tbody>
</table>
<!-- 게시글이 없을 때 -->
<div class="no-documents" cond="!$document_list">
<p class="message">{$lang->no_documents}</p>
<a href="{getUrl('act', 'dispBoardWrite')}"
class="btn btn-primary" cond="$grant->write">
첫 글을 작성해보세요
</a>
</div>
</div>
<!-- 페이지 네비게이션 -->
<div class="pagination-wrapper" cond="$page_navigation">
<nav class="pagination">
<!-- 이전 페이지 -->
<a href="{getUrl('page', $page_navigation->first_page)}"
class="page-btn first" cond="$page_navigation->first_page < $page_navigation->cur_page">
<i class="icon-first"></i>
</a>
<a href="{getUrl('page', $page_navigation->prev_page)}"
class="page-btn prev" cond="$page_navigation->prev_page">
<i class="icon-prev"></i>
</a>
<!-- 페이지 번호 -->
<span class="page-numbers">
<a loop="$page_navigation->page_list => $page_no"
href="{getUrl('page', $page_no)}"
class="page-num {$page_no == $page_navigation->cur_page ? 'current' : ''}">
{$page_no}
</a>
</span>
<!-- 다음 페이지 -->
<a href="{getUrl('page', $page_navigation->next_page)}"
class="page-btn next" cond="$page_navigation->next_page">
<i class="icon-next"></i>
</a>
<a href="{getUrl('page', $page_navigation->last_page)}"
class="page-btn last" cond="$page_navigation->last_page > $page_navigation->cur_page">
<i class="icon-last"></i>
</a>
</nav>
</div>
</div> <!-- board-container -->
카드형 목록
모던 카드 레이아웃
<div class="board-grid" cond="$document_list">
<article class="document-card" loop="$document_list => $no, $document">
<!-- 썸네일 이미지 -->
<div class="card-thumbnail">
<!--@if($document->getThumbnail())-->
<img src="{$document->getThumbnail()}" alt="{$document->getTitle()}" />
<!--@else-->
<div class="no-thumbnail">
<i class="icon-document"></i>
</div>
<!--@endif-->
<!-- 카테고리 배지 -->
<div class="category-badge" cond="$document->get('category_srl')">
{$category_list[$document->get('category_srl')]->title}
</div>
</div>
<!-- 카드 내용 -->
<div class="card-content">
<h3 class="card-title">
<a href="{getUrl('document_srl', $document->document_srl)}">
{cut_str($document->getTitle(), 50)}
</a>
</h3>
<p class="card-summary">
{cut_str(strip_tags($document->getContent()), 100)}
</p>
<div class="card-meta">
<span class="author">{$document->getNickName()}</span>
<span class="date">{zdate($document->getRegdate(), 'Y.m.d')}</span>
<span class="stats">
<i class="icon-eye"></i> {$document->getReadedCount()}
<i class="icon-comment" cond="$document->getCommentCount() > 0"></i>
{$document->getCommentCount()}
</span>
</div>
</div>
</article>
</div>
갤러리형 목록
이미지 중심 레이아웃
<div class="gallery-list" cond="$document_list">
<div class="gallery-item" loop="$document_list => $no, $document">
<div class="gallery-image">
<a href="{getUrl('document_srl', $document->document_srl)}">
<!--@if($document->getThumbnail())-->
<img src="{$document->getThumbnail()}" alt="{$document->getTitle()}" />
<!--@else-->
<div class="no-image">
<i class="icon-image"></i>
<span>이미지 없음</span>
</div>
<!--@endif-->
</a>
<!-- 오버레이 정보 -->
<div class="image-overlay">
<div class="overlay-info">
<span class="comment-count" cond="$document->getCommentCount() > 0">
<i class="icon-comment"></i> {$document->getCommentCount()}
</span>
<span class="view-count">
<i class="icon-eye"></i> {$document->getReadedCount()}
</span>
</div>
</div>
</div>
<div class="gallery-info">
<h4 class="gallery-title">
<a href="{getUrl('document_srl', $document->document_srl)}">
{cut_str($document->getTitle(), 30)}
</a>
</h4>
<div class="gallery-meta">
<span class="author">{$document->getNickName()}</span>
<span class="date">{zdate($document->getRegdate(), 'm.d')}</span>
</div>
</div>
</div>
</div>
모바일 최적화 목록
리스트 형식
<!-- 모바일 감지 -->
{@ $is_mobile = Mobile::isFromMobilePhone() }
<!--@if($is_mobile)-->
<!-- 모바일용 목록 -->
<div class="mobile-list" cond="$document_list">
<div class="mobile-item" loop="$document_list => $no, $document">
<div class="item-header">
<span class="category" cond="$document->get('category_srl')">
{$category_list[$document->get('category_srl')]->title}
</span>
<span class="date">{zdate($document->getRegdate(), 'm.d')}</span>
</div>
<h3 class="item-title">
<a href="{getUrl('document_srl', $document->document_srl)}">
{$document->getTitle()}
</a>
<span class="badges">
<span class="comment-badge" cond="$document->getCommentCount() > 0">
{$document->getCommentCount()}
</span>
<span class="new-badge" cond="$document->isNew()">N</span>
<span class="file-badge" cond="$document->getAttachedFileCount() > 0">
<i class="icon-attachment"></i>
</span>
</span>
</h3>
<div class="item-meta">
<span class="author">{$document->getNickName()}</span>
<span class="stats">
조회 {$document->getReadedCount()}
<span cond="$module_info->use_vote == 'Y'">
· 추천 {$document->getVotedCount()}
</span>
</span>
</div>
</div>
</div>
<!--@else-->
<!-- 데스크탑용 테이블 목록 (위의 표준 테이블 형식) -->
<!--@endif-->
필터링과 정렬
고급 검색 폼
<div class="advanced-search">
<form action="{getUrl()}" method="get" class="search-form-advanced">
<input type="hidden" name="mid" value="{$mid}" />
<div class="search-row">
<!-- 카테고리 필터 -->
<div class="search-field" cond="$category_list">
<label>분류</label>
<select name="category">
<option value="">전체</option>
<option loop="$category_list => $key, $val"
value="{$key}"
selected="selected"|cond="$category == $key">
{$val->title}
</option>
</select>
</div>
<!-- 검색 대상 -->
<div class="search-field">
<label>검색 대상</label>
<select name="search_target">
<option value="title">제목</option>
<option value="content">내용</option>
<option value="title_content">제목+내용</option>
<option value="nick_name">글쓴이</option>
<option value="user_id">아이디</option>
</select>
</div>
<!-- 검색어 -->
<div class="search-field">
<label>검색어</label>
<input type="text" name="search_keyword" value="{$search_keyword}" />
</div>
</div>
<div class="search-row">
<!-- 기간 검색 -->
<div class="search-field">
<label>작성일</label>
<input type="date" name="start_date" value="{$start_date}" />
<span>~</span>
<input type="date" name="end_date" value="{$end_date}" />
</div>
<!-- 정렬 -->
<div class="search-field">
<label>정렬</label>
<select name="order_type">
<option value="desc">최신순</option>
<option value="asc">오래된순</option>
<option value="title">제목순</option>
<option value="nick_name">글쓴이순</option>
<option value="readed_count">조회순</option>
<option value="voted_count">추천순</option>
</select>
</div>
</div>
<div class="search-actions">
<button type="submit" class="btn btn-primary">검색</button>
<button type="button" class="btn btn-secondary" onclick="location.href='{getUrl('search_keyword', '', 'category', '', 'start_date', '', 'end_date', '')}'">
초기화
</button>
</div>
</form>
</div>
CSS 스타일링
기본 목록 스타일
/* 게시판 목록 테이블 */
.board-list {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.board-list th,
.board-list td {
padding: 12px 8px;
border-bottom: 1px solid #eee;
text-align: center;
}
.board-list th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
}
.board-list .title {
text-align: left;
min-width: 300px;
}
.board-list .title a {
color: #333;
text-decoration: none;
}
.board-list .title a:hover {
color: #007bff;
}
/* 공지사항 스타일 */
.board-list .notice {
background: #fff8e1;
}
.board-list .notice .title a {
font-weight: 600;
color: #f57c00;
}
/* 답글 스타일 */
.reply-indent .reply-arrow {
color: #999;
margin-right: 2px;
}
/* 아이콘들 */
.comment-count {
color: #007bff;
font-size: 12px;
margin-left: 5px;
}
.new-icon {
color: #dc3545;
font-size: 11px;
font-weight: bold;
margin-left: 3px;
}
.attach-icon {
color: #28a745;
margin-left: 3px;
}
.secret-icon {
color: #6c757d;
margin-left: 3px;
}
/* 카드형 목록 */
.board-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.document-card {
border: 1px solid #eee;
border-radius: 8px;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
.document-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.card-thumbnail {
position: relative;
height: 200px;
overflow: hidden;
}
.card-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.no-thumbnail {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background: #f8f9fa;
color: #6c757d;
}
.category-badge {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0,123,255,0.9);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.card-content {
padding: 15px;
}
.card-title {
margin: 0 0 10px;
font-size: 16px;
line-height: 1.4;
}
.card-title a {
color: #333;
text-decoration: none;
}
.card-summary {
color: #666;
font-size: 14px;
line-height: 1.4;
margin: 0 0 15px;
}
.card-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #999;
}
/* 반응형 */
@media (max-width: 768px) {
.board-list {
font-size: 13px;
}
.board-list th,
.board-list td {
padding: 8px 4px;
}
.board-list .hit,
.board-list .vote {
display: none;
}
.board-grid {
grid-template-columns: 1fr;
gap: 15px;
}
}