애드온 개발 기초¶
애드온이란?¶
애드온(Addon)은 Rhymix/XE의 모든 실행 과정에 개입하여 기능을 추가하거나 수정할 수 있는 확장 시스템입니다.
애드온의 특징¶
- 전역적: 모든 페이지에서 실행 가능
- 이벤트 기반: 특정 시점에 실행되는 훅(Hook) 시스템
- 비침투적: 기존 코드 수정 없이 기능 확장
- 독립적: 모듈과 독립적으로 작동
애드온 구조¶
기본 파일 구조¶
addons/
└── my_addon/
├── conf/
│ └── info.xml # 애드온 정보 (필수)
├── lang/
│ ├── ko.php # 한국어 언어팩
│ └── en.php # 영어 언어팩
├── tpl/
│ └── config.html # 설정 화면 (선택)
├── my_addon.addon.php # 메인 애드온 파일 (필수)
├── my_addon.class.php # 애드온 클래스 (선택)
└── README.md # 설명서 (선택)
info.xml 작성¶
<?xml version="1.0" encoding="UTF-8"?>
<addon version="0.2">
<title xml:lang="ko">나의 애드온</title>
<title xml:lang="en">My Addon</title>
<description xml:lang="ko">
사용자 정의 기능을 추가하는 애드온입니다.
</description>
<description xml:lang="en">
An addon that adds custom functionality.
</description>
<version>1.0.0</version>
<date>2024-01-01</date>
<author email="user@example.com" link="https://example.com">
<name xml:lang="ko">개발자명</name>
<name xml:lang="en">Developer Name</name>
</author>
<!-- 의존성 -->
<depends>
<module name="board" />
<module name="member" />
</depends>
<!-- 설정 변수 -->
<extra_vars>
<var name="enable_feature" type="select">
<title xml:lang="ko">기능 활성화</title>
<description xml:lang="ko">특정 기능을 활성화합니다</description>
<options value="Y">
<title xml:lang="ko">사용</title>
</options>
<options value="N">
<title xml:lang="ko">사용안함</title>
</options>
</var>
<var name="api_key" type="text">
<title xml:lang="ko">API 키</title>
<description xml:lang="ko">외부 API 연동을 위한 키</description>
</var>
<var name="target_modules" type="text">
<title xml:lang="ko">대상 모듈</title>
<description xml:lang="ko">쉼표로 구분된 모듈 ID 목록</description>
<default>board,page</default>
</var>
</extra_vars>
</addon>
기본 애드온 파일¶
my_addon.addon.php¶
<?php
/**
* 나의 애드온
*
* @author 개발자명
* @version 1.0.0
*/
// 애드온 실행 함수
function my_addon($called_position, $obj)
{
// 애드온 설정 가져오기
$addon_config = AddonModel::getInstance()->getAddonConfig('my_addon');
// 기능이 비활성화된 경우 종료
if ($addon_config->enable_feature !== 'Y') {
return;
}
// 대상 모듈 체크
$target_modules = explode(',', $addon_config->target_modules ?? 'board,page');
$current_module = Context::get('module');
if (!in_array($current_module, $target_modules)) {
return;
}
// 호출 위치별 처리
switch ($called_position) {
case 'before_module_init':
handleBeforeModuleInit($obj);
break;
case 'after_module_proc':
handleAfterModuleProc($obj);
break;
case 'before_display_content':
handleBeforeDisplayContent($obj);
break;
case 'after_display_content':
handleAfterDisplayContent($obj);
break;
}
}
/**
* 모듈 초기화 전 처리
*/
function handleBeforeModuleInit($obj)
{
// Context에 변수 추가
Context::set('addon_active', true);
// 추가 CSS/JS 로드
Context::loadFile([
'./addons/my_addon/assets/my_addon.css',
'./addons/my_addon/assets/my_addon.js'
], true);
}
/**
* 모듈 실행 후 처리
*/
function handleAfterModuleProc($obj)
{
// 결과 데이터 수정
if ($obj instanceof BaseObject) {
// 성공적인 처리 후 추가 작업
if ($obj->toBool()) {
// 로그 기록, 알림 발송 등
writeCustomLog('Module processed successfully');
}
}
}
/**
* 콘텐츠 출력 전 처리
*/
function handleBeforeDisplayContent($obj)
{
// 헤더에 메타태그 추가
Context::addHtmlHeader('<meta name="addon-version" content="1.0.0">');
// 특정 조건에서 리다이렉트
$act = Context::get('act');
if ($act === 'dispSpecialPage' && !isAuthorized()) {
header('Location: ' . getNotEncodedUrl('', 'mid', 'login'));
exit;
}
}
/**
* 콘텐츠 출력 후 처리
*/
function handleAfterDisplayContent($obj)
{
// 페이지 하단에 추가 스크립트 삽입
$additional_script = '
<script>
jQuery(function($) {
// 애드온 관련 스크립트
console.log("My Addon is active");
// 특정 기능 초기화
initCustomFeatures();
});
</script>';
$content = Context::get('content');
$content .= $additional_script;
Context::set('content', $content);
}
/**
* 권한 체크 함수
*/
function isAuthorized()
{
$logged_info = Context::get('logged_info');
return $logged_info && $logged_info->is_admin === 'Y';
}
/**
* 커스텀 로그 작성
*/
function writeCustomLog($message)
{
$log_file = './files/_logs/my_addon.log';
$log_entry = date('Y-m-d H:i:s') . " - " . $message . PHP_EOL;
file_put_contents($log_file, $log_entry, FILE_APPEND | LOCK_EX);
}
/**
* 초기화 함수 (설치 시 실행)
*/
function initCustomFeatures()
{
// 초기화 작업
}
?>
애드온 클래스 방식¶
my_addon.class.php¶
<?php
/**
* 애드온 클래스
*/
class MyAddon
{
private $config;
private static $instance;
/**
* 싱글톤 인스턴스 반환
*/
public static function getInstance()
{
if (!self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 생성자
*/
private function __construct()
{
$this->config = AddonModel::getInstance()->getAddonConfig('my_addon');
}
/**
* 애드온 실행
*/
public function triggerFunction($called_position, $obj)
{
if (!$this->isEnabled()) {
return;
}
$method_name = 'handle' . ucfirst(str_replace('_', '', $called_position));
if (method_exists($this, $method_name)) {
$this->$method_name($obj);
}
}
/**
* 애드온 활성화 여부
*/
private function isEnabled()
{
return $this->config->enable_feature === 'Y';
}
/**
* 대상 모듈 체크
*/
private function isTargetModule()
{
$target_modules = explode(',', $this->config->target_modules ?? '');
$current_module = Context::get('module');
return in_array($current_module, array_map('trim', $target_modules));
}
/**
* 모듈 초기화 전
*/
public function handleBeforemoduleinit($obj)
{
if (!$this->isTargetModule()) {
return;
}
// 특정 액션에서만 실행
$act = Context::get('act');
$allowed_acts = ['dispBoardContent', 'dispBoardWrite', 'dispBoardView'];
if (in_array($act, $allowed_acts)) {
$this->loadAssets();
$this->setContextVariables();
}
}
/**
* 에셋 로드
*/
private function loadAssets()
{
Context::loadFile([
'./addons/my_addon/assets/style.css',
'./addons/my_addon/assets/script.js'
], true);
}
/**
* Context 변수 설정
*/
private function setContextVariables()
{
Context::set('my_addon_config', $this->config);
Context::set('my_addon_active', true);
}
/**
* API 요청 처리
*/
public function handleApiRequest($data)
{
if (!$this->config->api_key) {
return ['error' => 'API key not configured'];
}
// 외부 API 호출
$response = $this->callExternalAPI($data);
return $response;
}
/**
* 외부 API 호출
*/
private function callExternalAPI($data)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.example.com/endpoint');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->config->api_key
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code === 200) {
return json_decode($response, true);
}
return ['error' => 'API request failed'];
}
}
// 애드온 함수에서 클래스 사용
function my_addon($called_position, $obj)
{
$addon = MyAddon::getInstance();
$addon->triggerFunction($called_position, $obj);
}
?>
설정 화면¶
tpl/config.html¶
<!-- 애드온 설정 화면 -->
<form action="{getUrl()}" method="post" class="addon-config-form">
<input type="hidden" name="module" value="admin" />
<input type="hidden" name="act" value="procAdminInsertAddon" />
<input type="hidden" name="addon" value="my_addon" />
<input type="hidden" name="xe_validator_id" value="addon_config" />
<div class="config-section">
<h3>기본 설정</h3>
<div class="form-group">
<label for="enable_feature">기능 활성화</label>
<select name="enable_feature" id="enable_feature">
<option value="Y" selected="selected"|cond="$addon_config->enable_feature == 'Y'">
사용
</option>
<option value="N" selected="selected"|cond="$addon_config->enable_feature == 'N'">
사용안함
</option>
</select>
<p class="help-text">애드온의 주요 기능을 활성화하거나 비활성화합니다.</p>
</div>
<div class="form-group">
<label for="api_key">API 키</label>
<input type="text" name="api_key" id="api_key"
value="{$addon_config->api_key}"
class="form-control" />
<p class="help-text">외부 서비스 연동을 위한 API 키를 입력하세요.</p>
</div>
<div class="form-group">
<label for="target_modules">대상 모듈</label>
<input type="text" name="target_modules" id="target_modules"
value="{$addon_config->target_modules}"
placeholder="board,page,comment"
class="form-control" />
<p class="help-text">
애드온이 작동할 모듈을 쉼표로 구분하여 입력하세요.
<br>예: board,page,comment
</p>
</div>
</div>
<div class="config-section">
<h3>고급 설정</h3>
<div class="form-group">
<label>
<input type="checkbox" name="debug_mode" value="Y"
checked="checked"|cond="$addon_config->debug_mode == 'Y'" />
디버그 모드
</label>
<p class="help-text">개발 중에만 활성화하세요. 로그가 더 자세히 기록됩니다.</p>
</div>
<div class="form-group">
<label for="cache_timeout">캐시 유지 시간 (초)</label>
<input type="number" name="cache_timeout" id="cache_timeout"
value="{$addon_config->cache_timeout ?: 3600}"
min="0" max="86400"
class="form-control" />
<p class="help-text">0으로 설정하면 캐시를 사용하지 않습니다.</p>
</div>
</div>
<div class="config-actions">
<button type="submit" class="btn btn-primary">설정 저장</button>
<button type="button" class="btn btn-secondary" onclick="testAddon()">
연결 테스트
</button>
</div>
</form>
<script>
function testAddon() {
var apiKey = document.getElementById('api_key').value;
if (!apiKey) {
alert('API 키를 먼저 입력하세요.');
return;
}
// AJAX로 연결 테스트
jQuery.ajax({
url: request_uri,
type: 'POST',
data: {
module: 'addon',
act: 'procTestAddon',
addon: 'my_addon',
api_key: apiKey
},
success: function(response) {
if (response.error === '0') {
alert('연결 테스트 성공!');
} else {
alert('연결 실패: ' + response.message);
}
},
error: function() {
alert('테스트 중 오류가 발생했습니다.');
}
});
}
</script>
<style>
.addon-config-form {
max-width: 600px;
}
.config-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #eee;
border-radius: 8px;
}
.config-section h3 {
margin: 0 0 20px;
color: #333;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.form-control {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
.help-text {
margin-top: 5px;
font-size: 13px;
color: #666;
}
.config-actions {
text-align: right;
padding-top: 20px;
border-top: 1px solid #eee;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-left: 10px;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
</style>
애드온 설치 및 관리¶
설치 스크립트¶
<?php
/**
* 애드온 설치 시 실행되는 스크립트
* my_addon.install.php
*/
function my_addon_install()
{
// 필요한 디렉토리 생성
$directories = [
'./files/_logs',
'./files/_cache/my_addon'
];
foreach ($directories as $dir) {
if (!is_dir($dir)) {
FileHandler::makeDir($dir);
}
}
// 기본 설정 저장
$config = new stdClass();
$config->enable_feature = 'Y';
$config->target_modules = 'board,page';
$config->cache_timeout = 3600;
$oAddonAdminController = getAdminController('addon');
$oAddonAdminController->makeCacheFile('my_addon', $config);
return new BaseObject();
}
/**
* 애드온 제거 시 실행되는 스크립트
* my_addon.uninstall.php
*/
function my_addon_uninstall()
{
// 생성된 파일들 정리
$files_to_remove = [
'./files/_logs/my_addon.log',
'./files/_cache/my_addon'
];
foreach ($files_to_remove as $file) {
if (file_exists($file)) {
if (is_dir($file)) {
FileHandler::removeDir($file);
} else {
FileHandler::removeFile($file);
}
}
}
return new BaseObject();
}
?>
유용한 패턴들¶
조건부 실행¶
function my_addon($called_position, $obj)
{
// 관리자 페이지에서는 실행하지 않음
if (Context::get('module') === 'admin') {
return;
}
// 모바일에서만 실행
if (!Mobile::isFromMobilePhone()) {
return;
}
// 특정 액션에서만 실행
$act = Context::get('act');
$allowed_acts = ['dispBoardContent', 'dispBoardView'];
if (!in_array($act, $allowed_acts)) {
return;
}
// 로그인 사용자에게만
$logged_info = Context::get('logged_info');
if (!$logged_info) {
return;
}
}
성능 최적화¶
// 정적 변수로 중복 실행 방지
function my_addon($called_position, $obj)
{
static $executed = false;
if ($executed) {
return;
}
// 처리 로직
$executed = true;
}
// 캐시 활용
function getCachedData($key)
{
static $cache = [];
if (!isset($cache[$key])) {
$cache[$key] = expensive_operation($key);
}
return $cache[$key];
}