게시판 기초

게시판 기초

라이믹스 게시판의 기본 개념과 핵심 구조를 학습합니다.

게시판 모듈 개요

기본 개념

라이믹스의 게시판은 확장 가능한 모듈 시스템으로 구성되어 있습니다.

// 게시판 모듈의 기본 구조
$module_info = array(
    'module' => 'board',           // 모듈명
    'module_srl' => 123,          // 모듈 일련번호
    'mid' => 'notice',            // 모듈 ID
    'browser_title' => '공지사항',  // 브라우저 제목
    'description' => '사이트 공지사항입니다',
    'skin' => 'default',          // 스킨명
    'layout_srl' => 456,          // 레이아웃 일련번호
);

게시판 스키마

핵심 테이블 구조

-- 게시물 테이블
CREATE TABLE documents (
    document_srl BIGINT PRIMARY KEY,     -- 게시물 일련번호
    module_srl INT,                      -- 모듈 일련번호
    category_srl INT,                    -- 카테고리 일련번호
    member_srl INT,                      -- 작성자 일련번호
    title VARCHAR(250),                  -- 제목
    content LONGTEXT,                    -- 내용
    readed_count INT DEFAULT 0,          -- 조회수
    voted_count INT DEFAULT 0,           -- 추천수
    blamed_count INT DEFAULT 0,          -- 비추천수
    comment_count INT DEFAULT 0,         -- 댓글수
    status VARCHAR(10) DEFAULT 'PUBLIC', -- 상태 (PUBLIC, SECRET, TEMP)
    password VARCHAR(60),                -- 비밀번호 (비회원)
    user_id VARCHAR(80),                 -- 사용자 ID
    user_name VARCHAR(40),               -- 사용자명
    nick_name VARCHAR(40),               -- 닉네임
    email_address VARCHAR(250),          -- 이메일
    homepage VARCHAR(250),               -- 홈페이지
    tags TEXT,                          -- 태그
    extra_vars TEXT,                    -- 확장변수
    regdate DATETIME,                   -- 등록일
    last_update DATETIME,               -- 최종수정일
    is_notice CHAR(1) DEFAULT 'N',      -- 공지여부
    is_secret CHAR(1) DEFAULT 'N',      -- 비밀글여부

    INDEX idx_module (module_srl, list_order),
    INDEX idx_category (category_srl),
    INDEX idx_member (member_srl),
    INDEX idx_status (status),
    INDEX idx_regdate (regdate)
);

-- 댓글 테이블
CREATE TABLE comments (
    comment_srl BIGINT PRIMARY KEY,      -- 댓글 일련번호
    module_srl INT,                      -- 모듈 일련번호
    document_srl BIGINT,                 -- 게시물 일련번호
    parent_srl BIGINT DEFAULT 0,         -- 부모 댓글 일련번호
    member_srl INT,                      -- 작성자 일련번호
    content TEXT,                        -- 내용
    password VARCHAR(60),                -- 비밀번호
    user_id VARCHAR(80),                 -- 사용자 ID
    user_name VARCHAR(40),               -- 사용자명
    nick_name VARCHAR(40),               -- 닉네임
    email_address VARCHAR(250),          -- 이메일
    homepage VARCHAR(250),               -- 홈페이지
    regdate DATETIME,                    -- 등록일
    last_update DATETIME,                -- 최종수정일
    head INT DEFAULT 0,                  -- 댓글 그룹 헤드
    arrange INT DEFAULT 0,               -- 댓글 정렬
    depth INT DEFAULT 0,                 -- 댓글 깊이
    status VARCHAR(10) DEFAULT 'PUBLIC', -- 상태

    INDEX idx_document (document_srl, head, arrange),
    INDEX idx_member (member_srl),
    INDEX idx_module (module_srl)
);

-- 카테고리 테이블
CREATE TABLE categories (
    category_srl INT PRIMARY KEY,        -- 카테고리 일련번호
    module_srl INT,                      -- 모듈 일련번호
    parent_srl INT DEFAULT 0,            -- 부모 카테고리
    title VARCHAR(250),                  -- 카테고리명
    description TEXT,                    -- 설명
    color VARCHAR(7),                    -- 색상
    expand CHAR(1) DEFAULT 'Y',          -- 확장여부
    document_count INT DEFAULT 0,        -- 게시물수
    list_order INT DEFAULT 0,            -- 정렬순서
    regdate DATETIME,                    -- 등록일

    INDEX idx_module (module_srl, list_order)
);

MVC 패턴 구조

Controller (컨트롤러)

// modules/board/board.controller.php
class boardController extends board
{
    /**
     * 게시물 등록 처리
     */
    function procBoardInsertDocument()
    {
        // 입력값 검증
        $document_srl = Context::get('document_srl');
        $title = Context::get('title');
        $content = Context::get('content');
        $password = Context::get('password');

        // 권한 확인
        if(!$this->grant->write_document) {
            return new BaseObject(-1, 'msg_not_permitted');
        }

        // 데이터 준비
        $args = new stdClass();
        $args->title = $title;
        $args->content = $content;
        $args->password = $password;
        $args->module_srl = $this->module_srl;

        // 문서 객체 생성
        $oDocumentModel = getModel('document');
        $oDocumentController = getController('document');

        if($document_srl) {
            // 수정
            $oDocument = $oDocumentModel->getDocument($document_srl);
            $output = $oDocumentController->updateDocument($oDocument, $args);
        } else {
            // 등록
            $output = $oDocumentController->insertDocument($args);
        }

        if(!$output->toBool()) {
            return $output;
        }

        // 성공 시 리다이렉트
        $this->add('mid', Context::get('mid'));
        $this->add('document_srl', $output->get('document_srl'));

        if($output->get('document_srl')) {
            $this->setRedirectUrl(getNotEncodedUrl('', 'mid', Context::get('mid'), 'act', 'dispBoardContent', 'document_srl', $output->get('document_srl')));
        }
    }

    /**
     * 댓글 등록 처리
     */
    function procBoardInsertComment()
    {
        $comment_srl = Context::get('comment_srl');
        $document_srl = Context::get('document_srl');
        $content = Context::get('content');
        $password = Context::get('password');
        $parent_srl = Context::get('parent_srl');

        // 권한 확인
        if(!$this->grant->write_comment) {
            return new BaseObject(-1, 'msg_not_permitted');
        }

        // 데이터 준비
        $args = new stdClass();
        $args->comment_srl = $comment_srl;
        $args->document_srl = $document_srl;
        $args->content = $content;
        $args->password = $password;
        $args->parent_srl = $parent_srl;

        // 댓글 등록
        $oCommentController = getController('comment');
        $output = $oCommentController->insertComment($args);

        if(!$output->toBool()) {
            return $output;
        }

        $this->add('mid', Context::get('mid'));
        $this->add('document_srl', $document_srl);
        $this->add('comment_srl', $output->get('comment_srl'));
    }
}

Model (모델)

// modules/board/board.model.php
class boardModel extends board
{
    /**
     * 게시물 목록 조회
     */
    function getDocumentList($obj)
    {
        // 기본 조건 설정
        $args = new stdClass();
        $args->module_srl = $obj->module_srl;
        $args->page = $obj->page ?: 1;
        $args->list_count = $obj->list_count ?: 20;
        $args->page_count = $obj->page_count ?: 10;
        $args->sort_index = $obj->sort_index ?: 'list_order';
        $args->order_type = $obj->order_type ?: 'asc';

        // 검색 조건 추가
        if($obj->search_target && $obj->search_keyword) {
            switch($obj->search_target) {
                case 'title':
                    $args->s_title = $obj->search_keyword;
                    break;
                case 'content':
                    $args->s_content = $obj->search_keyword;
                    break;
                case 'title_content':
                    $args->s_title_content = $obj->search_keyword;
                    break;
                case 'nick_name':
                    $args->s_nick_name = $obj->search_keyword;
                    break;
                case 'user_id':
                    $args->s_user_id = $obj->search_keyword;
                    break;
                case 'tag':
                    $args->s_tags = $obj->search_keyword;
                    break;
            }
        }

        // 카테고리 필터
        if($obj->category_srl) {
            $args->category_srl = $obj->category_srl;
        }

        // 상태 필터 (공개글만)
        $args->statusList = array('PUBLIC');

        // 쿼리 실행
        $output = executeQuery('board.getDocumentList', $args);
        if(!$output->toBool()) {
            return $output;
        }

        // 결과 데이터 처리
        $document_list = array();
        if($output->data) {
            foreach($output->data as $key => $val) {
                $document_list[$val->document_srl] = new documentItem();
                $document_list[$val->document_srl]->setAttribute($val);
            }
        }

        $output->data = $document_list;

        return $output;
    }

    /**
     * 게시물 상세 조회
     */
    function getDocument($document_srl, $is_admin = false)
    {
        $args = new stdClass();
        $args->document_srl = $document_srl;

        $output = executeQuery('board.getDocument', $args);
        if(!$output->toBool() || !$output->data) {
            return new BaseObject(-1, 'msg_not_founded');
        }

        $oDocument = new documentItem();
        $oDocument->setAttribute($output->data);

        // 권한 확인
        if(!$is_admin && !$oDocument->isAccessible()) {
            return new BaseObject(-1, 'msg_not_permitted');
        }

        return $oDocument;
    }

    /**
     * 댓글 목록 조회
     */
    function getCommentList($document_srl, $page = 1, $list_count = 20, $is_admin = false)
    {
        $args = new stdClass();
        $args->document_srl = $document_srl;
        $args->page = $page;
        $args->list_count = $list_count;
        $args->page_count = 10;
        $args->sort_index = 'list_order';
        $args->order_type = 'asc';

        if(!$is_admin) {
            $args->statusList = array('PUBLIC');
        }

        $output = executeQuery('board.getCommentList', $args);
        if(!$output->toBool()) {
            return $output;
        }

        $comment_list = array();
        if($output->data) {
            foreach($output->data as $key => $val) {
                $comment_list[$val->comment_srl] = new commentItem($val);
            }
        }

        $output->data = $comment_list;

        return $output;
    }
}

View (뷰)

// modules/board/board.view.php
class boardView extends board
{
    /**
     * 게시물 목록 화면
     */
    function dispBoardContentList()
    {
        // 템플릿 파일 설정
        $this->setTemplatePath($this->module_path.'skins/'.$this->module_info->skin);
        $this->setTemplateFile('list');

        // 페이지 정보
        $page = Context::get('page');
        if(!$page) $page = 1;

        // 검색 조건
        $search_target = Context::get('search_target');
        $search_keyword = Context::get('search_keyword');
        $category_srl = Context::get('category_srl');

        // 게시물 목록 조회
        $args = new stdClass();
        $args->module_srl = $this->module_srl;
        $args->page = $page;
        $args->list_count = $this->module_info->list_count ?: 20;
        $args->search_target = $search_target;
        $args->search_keyword = $search_keyword;
        $args->category_srl = $category_srl;

        $oDocumentModel = getModel('document');
        $output = $oDocumentModel->getDocumentList($args);

        // 템플릿에 변수 할당
        Context::set('document_list', $output->data);
        Context::set('page_navigation', $output->page_navigation);
        Context::set('total_count', $output->total_count);
        Context::set('total_page', $output->total_page);
        Context::set('page', $output->page);

        // 카테고리 목록
        $category_list = $this->getCategoryList($this->module_srl);
        Context::set('category_list', $category_list);

        // 검색 옵션
        Context::set('search_option', array(
            'target' => $search_target,
            'keyword' => $search_keyword
        ));

        // 브라우저 제목 설정
        if($search_keyword) {
            Context::setBrowserTitle($this->module_info->browser_title . ' : ' . $search_keyword);
        } else {
            Context::setBrowserTitle($this->module_info->browser_title);
        }
    }

    /**
     * 게시물 상세 화면
     */
    function dispBoardContent()
    {
        $document_srl = Context::get('document_srl');
        if(!$document_srl) {
            return $this->dispBoardMessage('msg_invalid_request');
        }

        // 게시물 조회
        $oDocumentModel = getModel('document');
        $oDocument = $oDocumentModel->getDocument($document_srl);

        if(!$oDocument->isExists()) {
            return $this->dispBoardMessage('msg_not_founded');
        }

        // 권한 확인
        if(!$oDocument->isAccessible()) {
            return $this->dispBoardMessage('msg_not_permitted');
        }

        // 조회수 증가
        $oDocument->updateReadedCount();

        // 댓글 목록 조회
        $comment_list = $oDocument->getComments();

        // 템플릿 설정
        $this->setTemplatePath($this->module_path.'skins/'.$this->module_info->skin);
        $this->setTemplateFile('view');

        // 템플릿에 변수 할당
        Context::set('oDocument', $oDocument);
        Context::set('comment_list', $comment_list);

        // 브라우저 제목 설정
        Context::setBrowserTitle($oDocument->getTitleText() . ' : ' . $this->module_info->browser_title);

        // 메타 정보 설정
        Context::addMetaTag('description', $oDocument->getSummary(200));
        Context::addMetaTag('keywords', $oDocument->get('tags'));
    }
}

데이터 객체

documentItem 클래스

class documentItem extends BaseObject
{
    var $document_srl = 0;
    var $variables = array();
    var $components = array();

    /**
     * 생성자
     */
    function __construct($document_srl = 0, $load_extra_vars = true, $columnList = array())
    {
        $this->document_srl = $document_srl;
        $this->_loaded = false;

        if($document_srl) {
            $this->load($load_extra_vars, $columnList);
        }
    }

    /**
     * 게시물 데이터 로드
     */
    function load($load_extra_vars = true, $columnList = array())
    {
        $args = new stdClass();
        $args->document_srl = $this->document_srl;

        $output = executeQuery('document.getDocument', $args, $columnList);
        if(!$output->toBool() || !$output->data) {
            return false;
        }

        $this->setAttribute($output->data, $load_extra_vars);
        $this->_loaded = true;

        return true;
    }

    /**
     * 접근 권한 확인
     */
    function isAccessible()
    {
        // 공개글은 누구나 접근 가능
        if($this->get('status') == 'PUBLIC') {
            return true;
        }

        // 비밀글은 작성자와 관리자만 접근 가능
        if($this->get('status') == 'SECRET') {
            $logged_info = Context::get('logged_info');

            // 관리자
            if($logged_info && $logged_info->is_admin == 'Y') {
                return true;
            }

            // 작성자
            if($logged_info && $this->get('member_srl') == $logged_info->member_srl) {
                return true;
            }

            // 비회원 작성글 - 비밀번호 확인 필요
            if(!$this->get('member_srl')) {
                return $this->isGranted();
            }

            return false;
        }

        return false;
    }

    /**
     * 제목 텍스트 반환
     */
    function getTitleText($cut_size = 0)
    {
        $title = $this->get('title');
        if($cut_size > 0) {
            $title = cut_str($title, $cut_size);
        }
        return $title;
    }

    /**
     * 내용 요약 반환
     */
    function getSummary($cut_size = 200)
    {
        $content = $this->get('content');
        $content = preg_replace('/<[^>]*>/', '', $content);
        $content = str_replace(array("\r\n", "\r", "\n"), ' ', $content);

        if($cut_size > 0) {
            $content = cut_str($content, $cut_size);
        }

        return trim($content);
    }

    /**
     * 첨부파일 목록 반환
     */
    function getUploadedFiles()
    {
        if(!$this->get('uploaded_count')) {
            return array();
        }

        $oFileModel = getModel('file');
        return $oFileModel->getFiles($this->document_srl);
    }
}

확장변수 시스템

확장변수 정의

<!-- modules/board/conf/module.xml -->
<extra_vars>
    <var id="use_category" type="select" default="Y">
        <title xml:lang="ko">카테고리 사용</title>
        <options value="Y">사용</options>
        <options value="N">사용안함</options>
    </var>

    <var id="list_count" type="text" default="20">
        <title xml:lang="ko">목록 수</title>
    </var>

    <var id="search_list_count" type="text" default="20">
        <title xml:lang="ko">검색 결과 수</title>
    </var>

    <var id="page_count" type="text" default="10">
        <title xml:lang="ko">페이지 네비게이션 수</title>
    </var>
</extra_vars>

사용자 정의 필드

// 사용자 정의 필드 추가
function insertExtraVar($module_srl, $var_name, $var_type, $var_desc)
{
    $args = new stdClass();
    $args->module_srl = $module_srl;
    $args->var_name = $var_name;
    $args->var_type = $var_type;
    $args->var_desc = $var_desc;
    $args->var_default = '';
    $args->var_is_required = 'N';
    $args->eid = $var_name;

    return executeQuery('board.insertExtraVar', $args);
}

// 확장변수 값 저장
function setExtraVars($document_srl, $extra_vars)
{
    if(!is_array($extra_vars)) {
        return;
    }

    foreach($extra_vars as $key => $val) {
        $args = new stdClass();
        $args->document_srl = $document_srl;
        $args->var_name = $key;
        $args->var_value = $val;

        executeQuery('board.insertDocumentExtraVar', $args);
    }
}

모범 사례

  1. 데이터 검증: 모든 입력값에 대한 엄격한 검증
  2. 권한 관리: 세밀한 권한 체계 구현
  3. 성능 최적화: 적절한 인덱싱과 쿼리 최적화
  4. 확장성: 모듈화된 구조로 기능 확장 용이
  5. 보안: XSS, SQL Injection 등 취약점 방지

다음 단계

게시판 기초를 이해했다면, 기본 구조에서 실제 구현을 학습하세요.