애드온 예제¶
실용적인 애드온 예제들을 통해 애드온 개발을 학습합니다.
🔧 예제 1: 로그인 필수 체크 애드온¶
특정 페이지에 로그인하지 않은 사용자의 접근을 제한하는 애드온입니다.
info.xml¶
<?xml version="1.0" encoding="UTF-8"?>
<addon version="0.2">
<title xml:lang="ko">로그인 필수 체크</title>
<title xml:lang="en">Login Required Check</title>
<description xml:lang="ko">특정 페이지에 로그인 필수 설정</description>
<version>1.0.0</version>
<date>2024-01-01</date>
<author email_address="admin@example.com">
<name xml:lang="ko">관리자</name>
</author>
</addon>
login_required.addon.php¶
<?php
if(!defined("__XE__")) exit();
function login_required($obj)
{
// 관리자는 제외
if(Context::get('is_logged')) return;
// 로그인이 필요한 mid 목록
$required_mids = array('notice', 'qna', 'download');
$current_mid = Context::get('mid');
if(in_array($current_mid, $required_mids))
{
// 로그인 페이지로 리다이렉트
$url = getNotEncodedUrl('', 'act', 'dispMemberLoginForm', 'success_return_url', Context::getRequestUri());
header('Location: ' . $url);
exit();
}
}
?>
🔧 예제 2: 방문자 통계 애드온¶
페이지별 방문자 수를 기록하는 애드온입니다.
visitor_stats.addon.php¶
<?php
if(!defined("__XE__")) exit();
function visitor_stats($obj)
{
// 봇이나 관리자 제외
if(Context::get('is_logged') && Context::get('logged_info')->is_admin == 'Y') return;
$current_mid = Context::get('mid');
if(!$current_mid) return;
$today = date('Y-m-d');
$client_ip = \RX_CLIENT_IP;
// 같은 날 같은 IP는 중복 제거
$args = new stdClass();
$args->mid = $current_mid;
$args->visit_date = $today;
$args->ip_address = $client_ip;
$output = executeQuery('addon.visitor_stats.getVisit', $args);
if(!$output->data)
{
// 새로운 방문 기록
$args->reg_date = date('Y-m-d H:i:s');
executeQuery('addon.visitor_stats.insertVisit', $args);
}
}
?>
🔧 예제 3: 게시글 자동 태그 애드온¶
게시글 제목에서 자동으로 태그를 추출하여 설정하는 애드온입니다.
auto_tag.addon.php¶
<?php
if(!defined("__XE__")) exit();
function auto_tag($obj)
{
// 게시글 등록/수정 시에만 동작
if(!in_array($obj->act, array('procBoardInsertDocument', 'procBoardUpdateDocument'))) return;
$title = Context::get('title');
if(!$title) return;
// 한글 키워드 추출 (2글자 이상)
preg_match_all('/[가-힣]{2,}/u', $title, $matches);
if($matches[0])
{
$tags = array_unique($matches[0]);
$tag_string = implode(',', array_slice($tags, 0, 5)); // 최대 5개
Context::set('tags', $tag_string);
}
}
?>
🔧 예제 4: 금지어 필터 애드온¶
게시글과 댓글에서 금지어를 체크하여 등록을 차단하는 애드온입니다.
word_filter.addon.php¶
<?php
if(!defined("__XE__")) exit();
function word_filter($obj)
{
$filtered_acts = array(
'procBoardInsertDocument',
'procBoardUpdateDocument',
'procBoardInsertComment',
'procBoardUpdateComment'
);
if(!in_array($obj->act, $filtered_acts)) return;
// 금지어 목록
$banned_words = array('스팸', '광고', '도배');
$content = Context::get('content');
$title = Context::get('title');
$check_text = $title . ' ' . $content;
foreach($banned_words as $word)
{
if(strpos($check_text, $word) !== false)
{
return new BaseObject(-1, sprintf('금지어 "%s"가 포함되어 있습니다.', $word));
}
}
}
?>
📚 추가 리소스¶
설정 파일 활용¶
config.php 파일을 통해 애드온 설정을 관리할 수 있습니다.
<?php
// config.php
$config = new stdClass();
$config->enabled_mids = array('notice', 'qna');
$config->excluded_ips = array('127.0.0.1');
$config->notification_email = 'admin@example.com';
?>
언어팩 지원¶
다국어 지원을 위한 언어팩 파일 구성:
<?php
// lang/ko.php
$lang->addon_name = '애드온명';
$lang->addon_description = '애드온 설명';
$lang->error_message = '오류가 발생했습니다.';
?>
🔧 예제 5: 고급 캐싱 애드온¶
페이지 캐싱을 통해 성능을 향상시키는 애드온입니다.
page_cache.addon.php¶
<?php
if(!defined("__XE__")) exit();
class PageCacheAddon
{
private $cache_dir;
private $cache_time = 3600; // 1시간
public function __construct()
{
$this->cache_dir = _XE_PATH_ . 'files/cache/page_cache/';
if(!is_dir($this->cache_dir)) {
FileHandler::makeDir($this->cache_dir);
}
}
public function getCacheKey()
{
$uri = $_SERVER['REQUEST_URI'];
$user_agent = $_SERVER['HTTP_USER_AGENT'];
$is_mobile = Mobile::isMobile() ? 'mobile' : 'desktop';
return md5($uri . $user_agent . $is_mobile);
}
public function isCacheable()
{
// GET 요청만 캐싱
if($_SERVER['REQUEST_METHOD'] !== 'GET') return false;
// 로그인 사용자는 캐싱하지 않음
if(Context::get('is_logged')) return false;
// 관리자 페이지는 캐싱하지 않음
if(Context::get('module') === 'admin') return false;
return true;
}
public function getCache()
{
if(!$this->isCacheable()) return null;
$cache_key = $this->getCacheKey();
$cache_file = $this->cache_dir . $cache_key . '.html';
if(file_exists($cache_file)) {
$cache_time = filemtime($cache_file);
if(time() - $cache_time < $this->cache_time) {
return file_get_contents($cache_file);
}
}
return null;
}
public function setCache($content)
{
if(!$this->isCacheable()) return;
$cache_key = $this->getCacheKey();
$cache_file = $this->cache_dir . $cache_key . '.html';
// 캐시에 타임스탬프 추가
$cached_content = $content . '<!-- Cached at ' . date('Y-m-d H:i:s') . ' -->';
file_put_contents($cache_file, $cached_content);
}
}
$page_cache = new PageCacheAddon();
switch($called_position) {
case 'before_module_proc':
// 캐시된 페이지가 있으면 즉시 출력
$cached_content = $page_cache->getCache();
if($cached_content) {
echo $cached_content;
exit;
}
// 출력 버퍼링 시작
if($page_cache->isCacheable()) {
ob_start();
}
break;
case 'before_display_content':
// 출력 버퍼 내용을 캐시에 저장
if($page_cache->isCacheable() && ob_get_level()) {
$content = ob_get_contents();
if($content) {
$page_cache->setCache($content);
}
}
break;
}
?>
🔧 예제 6: SEO 최적화 애드온¶
검색엔진 최적화를 위한 메타태그와 구조화된 데이터를 자동으로 생성하는 애드온입니다.
seo_optimizer.addon.php¶
<?php
if(!defined("__XE__")) exit();
class SEOOptimizer
{
public function generateMetaTags()
{
$oDocument = Context::get('oDocument');
$module_info = Context::get('module_info');
$meta_tags = array();
if($oDocument && $oDocument->isExists()) {
// 게시물 페이지
$title = $oDocument->getTitleText();
$description = $this->getDescription($oDocument->get('content'));
$image = $this->getFirstImage($oDocument->get('content'));
$url = getFullUrl('', 'document_srl', $oDocument->document_srl);
// 구조화된 데이터
$structured_data = array(
'@context' => 'https://schema.org',
'@type' => 'Article',
'headline' => $title,
'description' => $description,
'author' => array(
'@type' => 'Person',
'name' => $oDocument->getNickName()
),
'datePublished' => date('c', $oDocument->get('regdate')),
'dateModified' => date('c', $oDocument->get('last_update'))
);
if($image) {
$structured_data['image'] = $image;
}
} else {
// 목록 페이지
$title = $module_info->browser_title;
$description = $module_info->description;
$url = getFullUrl('', 'mid', Context::get('mid'));
}
// Open Graph 태그
$meta_tags[] = '<meta property="og:title" content="' . htmlspecialchars($title) . '">';
$meta_tags[] = '<meta property="og:description" content="' . htmlspecialchars($description) . '">';
$meta_tags[] = '<meta property="og:url" content="' . $url . '">';
$meta_tags[] = '<meta property="og:type" content="website">';
if($image) {
$meta_tags[] = '<meta property="og:image" content="' . $image . '">';
}
// Twitter Card
$meta_tags[] = '<meta name="twitter:card" content="summary_large_image">';
$meta_tags[] = '<meta name="twitter:title" content="' . htmlspecialchars($title) . '">';
$meta_tags[] = '<meta name="twitter:description" content="' . htmlspecialchars($description) . '">';
// 구조화된 데이터
if(isset($structured_data)) {
$meta_tags[] = '<script type="application/ld+json">' . json_encode($structured_data, JSON_UNESCAPED_UNICODE) . '</script>';
}
return implode("\n", $meta_tags);
}
private function getDescription($content, $length = 160)
{
$text = strip_tags($content);
$text = preg_replace('/\s+/', ' ', $text);
$text = trim($text);
if(mb_strlen($text) > $length) {
$text = mb_substr($text, 0, $length) . '...';
}
return $text;
}
private function getFirstImage($content)
{
preg_match('/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $content, $matches);
if($matches[1]) {
$image_url = $matches[1];
// 상대 경로를 절대 경로로 변환
if(strpos($image_url, 'http') !== 0) {
$image_url = getFullUrl() . ltrim($image_url, '/');
}
return $image_url;
}
return null;
}
}
$seo_optimizer = new SEOOptimizer();
if($called_position == 'before_display_content') {
// 게시물이나 게시판 페이지에서만 동작
if(in_array(Context::get('act'), array('dispBoardContent', 'dispBoardContentView'))) {
$meta_tags = $seo_optimizer->generateMetaTags();
Context::addHtmlHeader($meta_tags);
}
}
?>
🔧 예제 7: 실시간 알림 애드온¶
WebSocket을 사용한 실시간 알림 시스템 애드온입니다.
realtime_notification.addon.php¶
<?php
if(!defined("__XE__")) exit();
class RealtimeNotification
{
private $redis;
private $enabled = false;
public function __construct()
{
// Redis 연결 확인
if(class_exists('Redis')) {
$this->redis = new Redis();
try {
$this->redis->connect('127.0.0.1', 6379);
$this->enabled = true;
} catch(Exception $e) {
$this->enabled = false;
}
}
}
public function sendNotification($type, $data)
{
if(!$this->enabled) return false;
$notification = array(
'type' => $type,
'data' => $data,
'timestamp' => time()
);
// Redis 채널에 발행
$this->redis->publish('notifications', json_encode($notification));
return true;
}
public function getClientScript()
{
if(!$this->enabled) return '';
$logged_info = Context::get('logged_info');
if(!$logged_info) return '';
return '
<script>
(function() {
var socket = new WebSocket("ws://localhost:8080");
socket.onopen = function(e) {
console.log("실시간 알림 연결됨");
// 사용자 인증
socket.send(JSON.stringify({
action: "auth",
member_srl: ' . $logged_info->member_srl . '
}));
};
socket.onmessage = function(event) {
var notification = JSON.parse(event.data);
showNotification(notification);
};
socket.onclose = function(event) {
console.log("실시간 알림 연결 종료");
};
function showNotification(notification) {
// 브라우저 알림
if(Notification.permission === "granted") {
new Notification(notification.title, {
body: notification.message,
icon: "/common/img/notification-icon.png"
});
}
// 페이지 내 알림 표시
var alertContainer = document.getElementById("notification-container");
if(!alertContainer) {
alertContainer = document.createElement("div");
alertContainer.id = "notification-container";
alertContainer.style.cssText = "position:fixed;top:20px;right:20px;z-index:9999;";
document.body.appendChild(alertContainer);
}
var alertDiv = document.createElement("div");
alertDiv.className = "alert alert-info";
alertDiv.innerHTML = notification.message;
alertDiv.style.cssText = "margin-bottom:10px;padding:10px;background:#d1ecf1;border:1px solid #bee5eb;border-radius:4px;";
alertContainer.appendChild(alertDiv);
// 5초 후 자동 제거
setTimeout(function() {
alertDiv.remove();
}, 5000);
}
// 알림 권한 요청
if(Notification.permission === "default") {
Notification.requestPermission();
}
})();
</script>';
}
}
$realtime_notification = new RealtimeNotification();
switch($called_position) {
case 'after_module_proc':
// 새 댓글 알림
if(Context::get('act') === 'procBoardInsertComment') {
$comment_srl = Context::get('comment_srl');
$document_srl = Context::get('document_srl');
if($comment_srl && $document_srl) {
// 원본 글 작성자에게 알림
$oDocumentModel = getModel('document');
$oDocument = $oDocumentModel->getDocument($document_srl);
if($oDocument->isExists()) {
$realtime_notification->sendNotification('new_comment', array(
'target_member_srl' => $oDocument->get('member_srl'),
'title' => '새 댓글 알림',
'message' => '회원님의 글에 새 댓글이 달렸습니다.',
'document_srl' => $document_srl,
'comment_srl' => $comment_srl
));
}
}
}
// 새 게시물 알림 (특정 게시판)
if(Context::get('act') === 'procBoardInsertDocument') {
$document_srl = Context::get('document_srl');
$module_srl = Context::get('module_srl');
// 공지사항 게시판인 경우 전체 알림
if($module_srl == 123) { // 공지사항 게시판 module_srl
$realtime_notification->sendNotification('new_notice', array(
'type' => 'broadcast',
'title' => '새 공지사항',
'message' => '새로운 공지사항이 등록되었습니다.',
'document_srl' => $document_srl
));
}
}
break;
case 'before_display_content':
// 로그인한 사용자에게만 실시간 알림 스크립트 추가
if(Context::get('logged_info')) {
$script = $realtime_notification->getClientScript();
Context::addHtmlFooter($script);
}
break;
}
?>
💡 고급 개발 팁¶
- 클래스 기반 설계: 객체지향 프로그래밍으로 코드 구조화
- 의존성 주입: 외부 서비스(Redis, DB 등) 연결 시 에러 처리
- 이벤트 기반: 비동기 처리를 위한 이벤트 시스템 활용
- 캐싱 전략: 성능 향상을 위한 다층 캐싱 구현
- 모니터링: 애드온 동작 상태를 추적할 수 있는 로깅 시스템
🔍 디버깅과 테스트¶
애드온 디버깅 도구¶
<?php
class AddonDebugger
{
public static function log($message, $data = null)
{
if(__DEBUG__) {
$log_entry = array(
'time' => date('Y-m-d H:i:s'),
'message' => $message,
'data' => $data,
'trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)
);
file_put_contents(
_XE_PATH_ . 'files/debug/addon_debug.log',
json_encode($log_entry) . "\n",
FILE_APPEND
);
}
}
}
// 사용 예시
AddonDebugger::log('애드온 시작', array('act' => Context::get('act')));
?>
📦 배포와 관리¶
버전 관리¶
- 시맨틱 버저닝 사용 (1.0.0, 1.1.0, 2.0.0)
- 변경사항 문서화 (CHANGELOG.md)
- 업데이트 스크립트 제공
성능 모니터링¶
- 실행 시간 측정
- 메모리 사용량 추적
- 데이터베이스 쿼리 최적화
다음 단계¶
애드온 예제들을 학습했다면, 고급 애드온 기법을 통해 더 복잡한 기능을 구현해보세요.