게시판 기초¶
라이믹스 게시판의 기본 개념과 핵심 구조를 학습합니다.
게시판 모듈 개요¶
기본 개념¶
라이믹스의 게시판은 확장 가능한 모듈 시스템으로 구성되어 있습니다.
// 게시판 모듈의 기본 구조
$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);
}
}
모범 사례¶
- 데이터 검증: 모든 입력값에 대한 엄격한 검증
- 권한 관리: 세밀한 권한 체계 구현
- 성능 최적화: 적절한 인덱싱과 쿼리 최적화
- 확장성: 모듈화된 구조로 기능 확장 용이
- 보안: XSS, SQL Injection 등 취약점 방지
다음 단계¶
게시판 기초를 이해했다면, 기본 구조에서 실제 구현을 학습하세요.