글 관리 기능

글 관리 기능

게시글 관리와 관련된 고급 기능들을 구현하는 방법을 학습합니다.

페이지 이동 없는 글 삭제

AJAX를 이용한 글 삭제

<!-- 글 삭제 버튼 -->
<div class="document-actions" cond="$oDocument->isGranted()">
    <button type="button" onclick="deleteDocumentAjax({$oDocument->document_srl})" class="btn-delete">
        삭제
    </button>
</div>

<script>
function deleteDocumentAjax(document_srl) {
    if(!confirm('정말로 이 글을 삭제하시겠습니까?')) return false;

    var params = {
        document_srl: document_srl,
        module: 'board',
        act: 'procBoardDeleteDocument'
    };

    exec_xml('board', 'procBoardDeleteDocument', params, function(ret_obj) {
        if(ret_obj.error != 0) {
            alert(ret_obj.message);
        } else {
            alert('글이 삭제되었습니다.');
            // 목록으로 이동
            location.href = current_url.setQuery('document_srl', '');
        }
    });
}
</script>

목록에서 다중 삭제

<!-- 관리자용 일괄 삭제 -->
<form id="board-list-form" action="./" method="post">
    <input type="hidden" name="module" value="board" />
    <input type="hidden" name="act" value="procBoardDeleteDocuments" />

    <table class="board-list">
        <thead>
            <tr>
                <th cond="$grant->manager">
                    <input type="checkbox" onclick="checkAll(this)" />
                </th>
                <th>번호</th>
                <th>제목</th>
                <th>작성자</th>
                <th>날짜</th>
            </tr>
        </thead>
        <tbody>
            <tr loop="$document_list=>$no,$document">
                <td cond="$grant->manager">
                    <input type="checkbox" name="cart[]" value="{$document->document_srl}" />
                </td>
                <td>{$no}</td>
                <td>{$document->getTitle()}</td>
                <td>{$document->getNickName()}</td>
                <td>{$document->getRegdate('Y.m.d')}</td>
            </tr>
        </tbody>
    </table>

    <div class="admin-actions" cond="$grant->manager">
        <button type="button" onclick="doDeleteDocuments()" class="btn-delete">
            선택 삭제
        </button>
        <button type="button" onclick="doMoveDocuments()" class="btn-move">
            선택 이동
        </button>
    </div>
</form>

<script>
function doDeleteDocuments() {
    var checked = jQuery('input[name="cart[]"]:checked');
    if(checked.length == 0) {
        alert('삭제할 글을 선택해주세요.');
        return false;
    }

    if(!confirm(checked.length + '개의 글을 삭제하시겠습니까?')) return false;

    var params = {
        cart: [],
        module: 'board',
        act: 'procBoardDeleteDocuments'
    };

    checked.each(function() {
        params.cart.push(jQuery(this).val());
    });

    exec_xml('board', 'procBoardDeleteDocuments', params, function(ret_obj) {
        if(ret_obj.error != 0) {
            alert(ret_obj.message);
        } else {
            alert('선택한 글이 삭제되었습니다.');
            location.reload();
        }
    });
}
</script>

비밀글 즉시 변경

글보기에서 비밀글 전환

<!-- 비밀글 전환 버튼 -->
<div class="document-options" cond="$oDocument->isGranted()">
    {@$is_secret = $oDocument->get('is_secret') == 'Y'}

    <button type="button" onclick="toggleSecret({$oDocument->document_srl}, '{$is_secret ? 'N' : 'Y'}')" class="btn-secret">
        <i class="xi-{$is_secret ? 'unlock' : 'lock'}"></i>
        {$is_secret ? '공개글로 변경' : '비밀글로 변경'}
    </button>
</div>

<script>
function toggleSecret(document_srl, is_secret) {
    var message = is_secret == 'Y' ? '비밀글로 변경하시겠습니까?' : '공개글로 변경하시겠습니까?';
    if(!confirm(message)) return false;

    var params = {
        document_srl: document_srl,
        is_secret: is_secret
    };

    // 커스텀 API 호출
    exec_xml('board', 'procBoardUpdateDocumentSecret', params, function(ret_obj) {
        if(ret_obj.error != 0) {
            alert(ret_obj.message);
        } else {
            alert('변경되었습니다.');
            location.reload();
        }
    });
}
</script>

추천/비추천 경고창 없이 동작

즉시 추천 처리

<!-- 추천 버튼 -->
<div class="vote-buttons">
    <button type="button" onclick="voteDocument({$oDocument->document_srl}, 1)" class="btn-vote-up" disabled="disabled"|cond="$oDocument->isVoted()">
        <i class="xi-thumbs-up"></i>
        <span class="vote-count">{$oDocument->get('voted_count')}</span>
    </button>

    <button type="button" onclick="voteDocument({$oDocument->document_srl}, -1)" class="btn-vote-down" disabled="disabled"|cond="$oDocument->isBlamed()">
        <i class="xi-thumbs-down"></i>
        <span class="blame-count">{$oDocument->get('blamed_count')}</span>
    </button>
</div>

<script>
// 경고창 없이 추천/비추천
function voteDocument(document_srl, point) {
    var act = point > 0 ? 'procBoardVoteUp' : 'procBoardVoteDown';

    var params = {
        target_srl: document_srl,
        point: point
    };

    // 버튼 비활성화
    jQuery('.btn-vote-up, .btn-vote-down').prop('disabled', true);

    exec_xml('board', act, params, function(ret_obj) {
        if(ret_obj.error != 0) {
            // 에러 메시지만 표시
            showNotification(ret_obj.message, 'error');
            jQuery('.btn-vote-up, .btn-vote-down').prop('disabled', false);
        } else {
            // 성공 시 카운트 업데이트
            if(point > 0) {
                var count = parseInt(jQuery('.vote-count').text()) + 1;
                jQuery('.vote-count').text(count);
                jQuery('.btn-vote-up').addClass('voted');
            } else {
                var count = parseInt(jQuery('.blame-count').text()) + 1;
                jQuery('.blame-count').text(count);
                jQuery('.btn-vote-down').addClass('voted');
            }

            showNotification('추천했습니다.', 'success');
        }
    });
}

// 알림 표시 함수
function showNotification(message, type) {
    var notification = jQuery('<div class="notification ' + type + '">' + message + '</div>');
    jQuery('body').append(notification);

    notification.fadeIn(300).delay(2000).fadeOut(300, function() {
        jQuery(this).remove();
    });
}
</script>

<style>
.notification {
    position: fixed;
    top: 20px;
    right: 20px;
    padding: 15px 20px;
    background: #333;
    color: white;
    border-radius: 4px;
    display: none;
    z-index: 9999;
}

.notification.success {
    background: #4CAF50;
}

.notification.error {
    background: #f44336;
}

.btn-vote-up.voted,
.btn-vote-down.voted {
    color: #2196F3;
}
</style>

댓글 허용 여부 설정

글 작성시 댓글 허용 옵션

<!-- 글쓰기 폼에 댓글 허용 옵션 추가 -->
<div class="write-option">
    <label>
        <input type="checkbox" name="allow_comment" value="Y" checked="checked"|cond="$oDocument->allowComment()" />
        댓글 허용
    </label>

    <label>
        <input type="checkbox" name="allow_trackback" value="Y" checked="checked"|cond="$oDocument->allowTrackback()" />
        트랙백 허용
    </label>

    <label>
        <input type="checkbox" name="notify_message" value="Y" />
        댓글 알림 받기
    </label>
</div>

<!-- 글 보기에서 댓글 허용 상태 변경 -->
<div class="comment-control" cond="$oDocument->isGranted()">
    <button type="button" onclick="toggleCommentAllow({$oDocument->document_srl})" class="btn-comment-toggle">
        {$oDocument->allowComment() ? '댓글 차단' : '댓글 허용'}
    </button>
</div>

<script>
function toggleCommentAllow(document_srl) {
    var params = {
        document_srl: document_srl,
        allow_comment: window.allow_comment ? 'N' : 'Y'
    };

    exec_xml('board', 'procBoardUpdateCommentAllow', params, function(ret_obj) {
        if(ret_obj.error != 0) {
            alert(ret_obj.message);
        } else {
            location.reload();
        }
    });
}
</script>

글 관리 도구

관리자용 일괄 처리

<!-- 관리 도구 모음 -->
<div class="admin-toolbar" cond="$grant->manager">
    <h3>글 관리 도구</h3>

    <!-- 카테고리 일괄 변경 -->
    <div class="tool-item">
        <select name="target_category">
            <option value="">카테고리 선택</option>
            <option loop="$category_list=>$val" value="{$val->category_srl}">
                {$val->title}
            </option>
        </select>
        <button type="button" onclick="changeCategoryBulk()">
            선택 글 카테고리 변경
        </button>
    </div>

    <!-- 상태 일괄 변경 -->
    <div class="tool-item">
        <select name="target_status">
            <option value="PUBLIC">공개</option>
            <option value="SECRET">비밀</option>
            <option value="TEMP">임시저장</option>
        </select>
        <button type="button" onclick="changeStatusBulk()">
            선택 글 상태 변경
        </button>
    </div>

    <!-- 복사/이동 -->
    <div class="tool-item">
        <select name="target_module_srl">
            <option value="">게시판 선택</option>
            {@
                $board_list = getModel('board')->getBoardList();
            }
            <option loop="$board_list=>$board" value="{$board->module_srl}">
                {$board->browser_title}
            </option>
        </select>
        <button type="button" onclick="copyDocumentsBulk()">복사</button>
        <button type="button" onclick="moveDocumentsBulk()">이동</button>
    </div>
</div>

<script>
// 카테고리 일괄 변경
function changeCategoryBulk() {
    var checked = jQuery('input[name="cart[]"]:checked');
    var category_srl = jQuery('select[name="target_category"]').val();

    if(checked.length == 0) {
        alert('글을 선택해주세요.');
        return false;
    }

    if(!category_srl) {
        alert('카테고리를 선택해주세요.');
        return false;
    }

    var document_srls = [];
    checked.each(function() {
        document_srls.push(jQuery(this).val());
    });

    var params = {
        document_srls: document_srls.join(','),
        category_srl: category_srl
    };

    exec_xml('board', 'procBoardAdminUpdateCategory', params, function(ret_obj) {
        if(ret_obj.error != 0) {
            alert(ret_obj.message);
        } else {
            alert('카테고리가 변경되었습니다.');
            location.reload();
        }
    });
}

// 게시판 간 이동
function moveDocumentsBulk() {
    var checked = jQuery('input[name="cart[]"]:checked');
    var target_module_srl = jQuery('select[name="target_module_srl"]').val();

    if(checked.length == 0) {
        alert('글을 선택해주세요.');
        return false;
    }

    if(!target_module_srl) {
        alert('이동할 게시판을 선택해주세요.');
        return false;
    }

    if(!confirm('선택한 글을 이동하시겠습니까?')) return false;

    var document_srls = [];
    checked.each(function() {
        document_srls.push(jQuery(this).val());
    });

    var params = {
        document_srls: document_srls.join(','),
        module_srl: target_module_srl,
        target_module_srl: target_module_srl
    };

    exec_xml('board', 'procBoardAdminMoveDocument', params, function(ret_obj) {
        if(ret_obj.error != 0) {
            alert(ret_obj.message);
        } else {
            alert('글이 이동되었습니다.');
            location.reload();
        }
    });
}
</script>

확장 변수 활용

확장 변수 출력 및 관리

<!-- 확장 변수 출력 -->
<div class="extra-vars">
    {@$extra_vars = $oDocument->getExtraVars()}
    <dl loop="$extra_vars=>$key,$val">
        <dt>{$val->name}</dt>
        <dd>
            <!-- 텍스트 -->
            <span cond="$val->type == 'text'">{$val->value}</span>

            <!-- 이미지 -->
            <img cond="$val->type == 'image' && $val->value" src="{$val->value}" alt="{$val->name}" />

            <!-- 날짜 -->
            <span cond="$val->type == 'date'">{zdate($val->value, 'Y-m-d')}</span>

            <!-- 주소 -->
            <div cond="$val->type == 'kr_zip' && $val->value">
                {@$addr = unserialize($val->value)}
                {$addr[0]} {$addr[1]} {$addr[2]}
            </div>
        </dd>
    </dl>
</div>

<!-- 확장 변수로 필터링 -->
{@
    // 특정 확장 변수 값으로 필터링
    $filtered_documents = array();
    foreach($document_list as $document) {
        $extra_vars = $document->getExtraVars();
        if($extra_vars->product_type->value == 'premium') {
            $filtered_documents[] = $document;
        }
    }
}

모범 사례

  1. 권한 체크: 모든 관리 기능에 권한 확인 필수
  2. 확인 절차: 중요한 작업은 confirm 필수
  3. 로그 기록: 관리 작업은 로그 남기기
  4. 일괄 처리: 대량 작업시 배치 처리 고려
  5. UI/UX: 직관적인 인터페이스 제공

다음 단계

글 관리 기능을 구현했다면, 갤러리형 게시판을 학습하세요.