1장. 플러그인 제작 준비
DXCMS 플러그인은 PHP 파일 두 개(plugin.php, manifest.php)만 있으면 즉시 동작합니다. 빌드 도구나 패키지 매니저 없이 파일을 만들고 서버에 올리면 다음 요청부터 자동으로 로드됩니다.
1.1 필요한 사전 지식
- PHP 기본 — if/foreach/echo, 클로저(function($args){}), 배열
- 훅 개념 이해 — dx_add_hook() 으로 등록 → dx_run_hook() 발동 시 실행
- DXCMS 구조 — plugins/ 폴더 위치, dx_config() 함수, dx_base_url() 함수
1.2 플러그인 폴더 생성
- plugins/ 폴더 아래에 새 폴더 생성
- 폴더명 규칙: 영소문자•숫자•하이픈•언더스코어만 사용 (예: my-plugin, slack-notifier)
- 폴더명이 플러그인의 고유 ID가 됩니다 — 중복 없이 의미있는 이름 선택
plugins/
my-plugin/ ← 새 폴더 생성
plugin.php ← 생성 (메인 실행 파일)
manifest.php ← 생성 (메타 정보)
💡 개발 팁
example-plugin 폴더를 그대로 복사해서 시작하면 가장 빠릅니다.
결제 플러그인은 custom-payment-template을 복사하여 수정하세요.
DX_DEBUG=true를 설정하면 PHP 오류가 화면에 표시됩니다.
2장. manifest.php 작성
manifest.php는 플러그인의 신분증입니다. 관리자 → 플러그인 목록에 표시될 이름•버전•설명이 여기서 결정됩니다. 마켓에 공유할 때도 이 파일의 정보가 자동으로 사용됩니다.
2.1 manifest.php 전체 형식
<?php
// plugins/my-plugin/manifest.php
// 이 파일은 배열을 return 합니다.
return array(
"name" => "My Plugin", // 관리자 목록 표시 이름 ★필수
"version" => "1.0.0", // 버전 ★필수
"description" => "플러그인 기능 설명", // 관리자 목록 설명
"author" => "개발자명",
"author_url" => "https://mysite.com",
"type" => "utility", // 플러그인 분류 타입
"min_version" => "8.0.0", // 최소 DXCMS 버전
"tags" => "알림,슬랙,연동", // 쉼표 구분 검색 태그
"category" => "utility", // 카테고리
"homepage" => "https://...", // 홈페이지 URL
"license" => "MIT",
);
2.2 type 필드 선택 기준
| type 값 |
사용 시점 |
| "editor" |
글쓰기 에디터를 제공하는 플러그인 (CKEditor, TinyMCE 등) |
| "payment" |
결제 PG사 연동 플러그인 (토스, 카카오페이 등) |
| "captcha" |
CAPTCHA 서비스 연동 (reCAPTCHA, hCaptcha) |
| "sms" |
SMS 발송 서비스 연동 |
| "socket" |
WebSocket 실시간 기능 |
| "social_login" |
소셜 로그인 연동 |
| "utility" |
기능 추가, 알림, 분석 등 기타 모든 플러그인 |
3장. plugin.php 단계별 구현
3.1 파일 시작 — 보안 체크
모든 plugin.php의 첫 줄은 반드시 직접 접근 차단 코드여야 합니다.
<?php
/**
* My Plugin — DesignOneX CMS
* 플러그인 설명을 여기에 작성하세요.
*/
// 직접 접근 차단 — 반드시 첫 번째 실행 코드로 작성
if (!defined("DX_CMS")) exit("Direct access not allowed.");
3.2 dx_register_plugin() — 타입 등록
에디터•결제•캡챠처럼 관리자가 선택할 수 있는 타입에 속한 플러그인만 이 함수를 호출합니다. 단순 기능 추가 플러그인은 이 단계를 생략해도 됩니다.
// PluginRegistry에 등록 — 관리자 드롭다운에 표시됨
dx_register_plugin(array(
"id" => "my-plugin", // 폴더명과 일치 권장
"type" => "editor", // PluginRegistry 타입
"name" => "My Plugin", // 드롭다운 표시 이름
"version" => "1.0.0",
"description" => "설명",
"author" => "개발자명",
"priority" => 10, // 드롭다운 정렬 (낮을수록 위)
// 관리자 설정 필드 정의
"settings" => array(
"api_key" => array(
"label" => "API 키",
"type" => "text",
"required" => true,
"placeholder" => "sk_live_...",
"description" => "서비스 대시보드에서 발급",
"default" => "",
),
),
// 관리자 화면 사용 안내
"usage_guide" => array(
"what" => "이 플러그인이 하는 일",
"steps" => array(
"1. 관리자 > 플러그인 > 모듈 설정에서 선택",
"2. API 키를 입력하고 저장",
),
"note" => "테스트 키는 test_으로 시작합니다.",
),
));
3.3 활성화 조건 체크 패턴
editor•payment•captcha 타입은 관리자가 하나를 선택합니다. 내 플러그인이 선택된 경우에만 훅을 등록해야 무거운 기능이 불필요하게 실행되지 않습니다.
// ── 패턴 1: 활성화 체크 후 return (가장 일반적) ──────────
if (dx_active_plugin("editor") !== "my-plugin") return;
// 이 줄 이후는 내 플러그인이 활성화된 경우만 실행
// ── 패턴 2: 콜백 내부에서 체크 ──────────────────────────
dx_add_hook("dx_editor_render", function($args) {
if (dx_active_plugin("editor") !== "my-plugin") return;
// 에디터 출력
});
// ── 패턴 3: 설정값 체크 ─────────────────────────────────
$enabled = dx_config("plugin_my-plugin_enabled", "1");
if ($enabled !== "1") return;
3.4 훅 등록 — 실제 기능 구현
dx_add_hook()으로 훅 포인트에 콜백을 등록합니다. 콜백은 클로저, 함수명 문자열, 배열([객체, 메서드]) 모두 지원합니다.
// ── 클로저 (가장 일반적) ─────────────────────────────────
dx_add_hook("dx_head", function($ctx) {
echo '<link rel="stylesheet" href="' . dx_base_url("plugins/my-plugin/assets/style.css") . '">';
}, 10);
// ── 전역 함수명 문자열 ───────────────────────────────────
function my_plugin_head($ctx) {
echo '<link rel="stylesheet" href="...">';
}
dx_add_hook("dx_head", "my_plugin_head", 10);
// ── 클래스 메서드 배열 ───────────────────────────────────
class MyPlugin {
public function onHead($ctx) { ... }
}
$plugin = new MyPlugin();
dx_add_hook("dx_head", array($plugin, "onHead"), 10);
// ── 정적 메서드 ──────────────────────────────────────────
dx_add_hook("dx_head", array("MyPlugin", "staticOnHead"), 10);
4장. settings — 관리자 설정 필드 완전 가이드
dx_register_plugin()의 settings 배열에 필드를 정의하면 관리자 → 플러그인 설정 UI가 자동으로 생성됩니다. 설정값은 settings 테이블에 plugin_{id}_{key} 형식으로 저장됩니다.
4.1 settings 필드 완전 정의
"settings" => array(
// ── text: 단일 줄 텍스트 입력 ────────────────────────
"api_key" => array(
"label" => "API 키", // 입력란 위에 표시되는 레이블
"type" => "text",
"required" => true, // true면 빈값 경고 표시
"placeholder" => "sk_live_...", // 입력란 힌트
"description" => "대시보드에서 발급", // 레이블 아래 회색 설명
"default" => "", // 저장값 없을 때 기본값
),
// ── password: 비밀번호 입력 (마스킹) ─────────────────
"secret_key" => array(
"label" => "시크릿 키",
"type" => "password",
),
// ── select: 드롭다운 선택 ────────────────────────────
"mode" => array(
"label" => "운영 모드",
"type" => "select",
"options" => array(
"test" => "테스트 (실결제 없음)",
"live" => "실서비스",
),
"default" => "test",
),
// ── textarea: 여러 줄 텍스트 ─────────────────────────
"webhook_urls" => array(
"label" => "웹훅 URL 목록",
"type" => "textarea",
"placeholder" => "URL을 한 줄에 하나씩 입력",
),
// ── checkbox_group: 다중 체크박스 ────────────────────
// 선택된 값들이 쉼표 구분 문자열로 저장됨
"methods" => array(
"label" => "지원 결제 수단",
"type" => "checkbox_group",
"options" => array(
"card" => "신용카드",
"bank" => "계좌이체",
"phone" => "휴대폰",
),
"default" => "card,bank",
),
),
4.2 settings 값 읽기
// plugin_{플러그인ID}_{설정키} 형식으로 저장됨
$apiKey = dx_config("plugin_my-plugin_api_key", ""); // 기본값 ""
$mode = dx_config("plugin_my-plugin_mode", "test"); // 기본값 "test"
$enabled = dx_config("plugin_my-plugin_enabled", "1") === "1"; // bool
// checkbox_group: 쉼표 구분 문자열로 저장됨
$methodsStr = dx_config("plugin_my-plugin_methods", "card");
$methods = array_filter(array_map("trim", explode(",", $methodsStr)));
// 결과: ["card", "bank"]
// 결제 플러그인 전용 단축 함수 (dx-payment-helper.php)
// require_once dirname(__FILE__) . "/../dx-payment-helper.php"; 후 사용
$apiKey = dx_pay_cfg("my-plugin", "api_key");
// 내부적으로 dx_config("plugin_my-plugin_api_key") 와 동일
⚠️ 보안 주의
password 타입의 시크릿 키는 반드시 서버 측 코드에서만 사용하세요.
클라이언트(JavaScript)로 절대 전달하면 안 됩니다.
dx_config()는 서버 PHP에서만 호출하므로 안전합니다
5장. 전체 훅 포인트 레퍼런스
5.1 레이아웃 훅 (모든 페이지)
| 훅 이름 |
위치 및 전달 인자 |
| dx_head |
<head> 안쪽. CSS·meta·JSON-LD 삽입. $context 배열 전달 |
| dx_top |
body 최상단 (dx_hook_top). $context 전달 |
| dx_{type}_top |
페이지 타입별 top. 예: dx_board_top, dx_auth_top |
| dx_middle |
콘텐츠 영역 중간 (dx_hook_middle). $context 전달 |
| dx_{type}_middle |
페이지 타입별 middle. 예: dx_board_middle |
| dx_bottom |
body 하단 (dx_hook_bottom). $context 전달 |
| dx_{type}_bottom |
페이지 타입별 bottom |
| dx_footer_scripts |
</body> 직전. JS 파일 삽입용 |
| dx_body_bottom |
</body> 최하단. 분석·채팅 위젯 등 |
5.2 게시판 훅
| 훅 이름 |
발동 시점 및 인자 |
| dx_board_before |
핸들러 진입. board, action, skin, id 전달 |
| dx_board_list_context |
목록 $ctx 확정 직전. context 참조(&)로 전달 → 변경 가능 |
| dx_board_view_context |
상세 $ctx 확정 직전. context 참조(&)로 전달 |
| dx_board_write_context |
글쓰기 $ctx 확정 직전 |
| dx_board_before_save |
글 저장 직전. data 참조(&) 전달 → 커스텀 필드 추가 가능 |
| dx_after_write |
글 저장 완료 후. post_id, board, data |
| dx_board_after_save |
저장 후. redirect URL 변경 가능 |
| dx_board_before_delete |
글 삭제 직전. post, board_key |
| dx_board_after_delete |
글 삭제 완료 후. post_id, board_key |
| dx_after_comment |
댓글 등록 후. comment_id, post_id, board |
| dx_board_after |
핸들러 처리 완료 후 |
5.3 회원 훅
| 훅 이름 |
발동 시점 및 인자 |
| dx_after_login |
로그인 성공 후. user 배열 전달 |
| dx_after_logout |
로그아웃 후. user 배열 전달 |
| dx_after_register |
회원가입 완료 후. user_id, data 전달 |
5.4 포인트•좋아요•친구 훅
| 훅 이름 |
발동 시점 및 인자 |
| dx_after_point |
포인트 변동 후. member_id, type, point, balance |
| dx_levelup |
회원 레벨 상승 시. member_id, old_level, new_level |
| dx_after_like |
좋아요 등록 후. post_id, owner_id |
| dx_add_friend |
친구 추가 후. member_id, target_id |
5.5 에디터•결제•캡챠•관리자 훅
| 훅 이름 |
발동 시점 및 인자 |
| dx_editor_render |
에디터 HTML 출력. name, value, options, editor |
| dx_editor_init |
dx_render_editor() 내부 브릿지. 에디터 실행 진입점 |
| dx_payment_request |
dx_request_payment() 호출 시. 결제창 HTML/JS 출력 |
| dx_captcha_drivers |
CAPTCHA 드라이버 등록 시 |
| dx_sms_drivers |
SMS 드라이버 등록 시 |
| dx_admin_dashboard_widgets |
관리자 대시보드 위젯 삽입 |
| dx_shop_after_purchase |
쇼핑몰 결제 완료 후. order 정보 |
5.6 Filter 훅 — 값 변형
Filter 훅은 값을 받아 가공 후 반환하는 방식입니다. 동일 필터에 여러 플러그인이 연결되면 체인으로 처리됩니다.
// 필터 등록 — 글 내용에 광고 자동 삽입 예시
dx_add_filter("post_content", function($content, $args) {
$adHtml = '<div class="ad-box">광고</div>';
return $content . $adHtml;
}, 10);
// 게시글 제목 필터 등록
dx_add_filter("post_title", function($title, $args) {
// 금칙어 필터
return str_replace("금칙어", "***", $title);
});
// 필터 실행 — CMS 또는 테마에서
$content = dx_apply_filter("post_content", $rawContent, $args);
6장. 에디터 플러그인 제작
에디터 플러그인은 게시글•댓글 글쓰기 폼에 WYSIWYG 에디터를 제공합니다. 관리자가 에디터 드롭다운에서 선택하면 dx_render_editor() 호출 시 내 에디터가 렌더링됩니다.
6.1 에디터 렌더링 흐름
// 글쓰기 폼(write.php)에서
dx_render_editor("content", $existingContent, ["board"=>$board]);
↓
// dx_render_editor() 내부에서
$editorId = dx_active_plugin("editor"); // "my-editor"
dx_run_hook("dx_editor_render", ["name"=>$name,"value"=>$value,...]);
↓
// 내 플러그인의 dx_editor_render 훅 콜백 실행
6.2 에디터 플러그인 전체 구현
<?php
// plugins/my-editor/plugin.php
if (!defined("DX_CMS")) exit;
// 1. editor 타입으로 등록
dx_register_plugin(array(
"id" => "my-editor",
"type" => "editor",
"name" => "My Editor",
"version" => "1.0.0",
"settings" => array(
"height" => array(
"label" => "에디터 높이 (px)",
"type" => "text",
"default" => "400",
),
"use_comment_editor" => array(
"label" => "댓글에도 에디터 사용",
"type" => "select",
"options" => array("1"=>"사용","0"=>"사용 안 함"),
"default" => "1",
),
),
));
// 2. 내 에디터가 활성화된 경우만 실행
if (dx_active_plugin("editor") !== "my-editor") return;
// 3. 에디터 렌더 훅 등록
dx_add_hook("dx_editor_render", function($args) {
$name = isset($args["name"]) ? $args["name"] : "content";
$value = isset($args["value"]) ? $args["value"] : "";
$height = (int)dx_config("plugin_my-editor_height", "400");
// CDN 중복 로드 방지
static $loaded = false;
if (!$loaded) {
$loaded = true;
echo '<script src="https://cdn.example.com/my-editor.min.js"></script>' . " ";
}
// textarea + 에디터 초기화
$safeId = "editor_" . preg_replace("/[^a-zA-Z0-9_]/", "_", $name);
echo '<textarea id="' . $safeId . '" name="' . htmlspecialchars($name, ENT_QUOTES) . '"'
. ' style="display:none">' . htmlspecialchars($value, ENT_QUOTES, "UTF-8") . '</textarea>';
echo '<div id="' . $safeId . '_container" style="height:' . $height . 'px"></div>';
echo '<script>'
. 'MyEditor.create(document.getElementById("' . $safeId . '_container"), {'
. ' initialData: ' . json_encode($value) . ','
. ' height: ' . $height
. '}).then(editor => {'
. ' editor.model.document.on("change:data", () => {'
. ' document.getElementById("' . $safeId . '").value = editor.getData();'
. ' });'
. '});';
echo '</script>';
}, 10);
💡 use_comment_editor 설정의 의미
"use_comment_editor"=>"1" 로 저장하면 댓글 폼에도 에디터가 표시됩니다.
이 값은 dx_editor_use_comment() 함수가 자동으로 읽습니다:
$useEditor = dx_editor_use_comment(); // bool
설정 키 형식: plugin_{에디터ID}_use_comment_editor
7장. 결제 플러그인 제작
결제 플러그인은 dx_request_payment() 호출 시 결제창 HTML/JS를 출력하고, 결제 완료 후 서버에서 금액을 검증하는 함수를 제공합니다. dx-payment-helper.php에 cURL/HTTP 유틸리티가 내장되어 있습니다.
7.1 결제 흐름
[고객 "결제하기" 버튼 클릭]
↓
dx_request_payment(array(
"order_id" => "order_123",
"amount" => 29000,
"product_name" => "DXCMS 라이선스",
"buyer_name" => "홍길동",
"buyer_email" => "user@example.com",
"return_url" => dx_base_url("payment/success"),
"fail_url" => dx_base_url("payment/fail"),
))
↓ dx_payment_request 훅 발동
[내 플러그인 → PG사 결제창 HTML/JS 출력]
↓ 고객이 결제 완료
[PG사 → return_url 리다이렉트 (PaymentKey, orderId, amount)]
↓ return_url 핸들러 (pages/ 또는 boards/)에서
$result = my_pg_confirm($_GET); // 서버 검증
if ($result["success"]) {
// 주문 DB 업데이트, 포인트 지급 등
}
7.2 결제 플러그인 전체 구현
<?php
// plugins/my-pg-payment/plugin.php
if (!defined("DX_CMS")) exit;
// dx-payment-helper.php 로드 (HTTP 요청 유틸)
require_once dirname(__FILE__) . "/../dx-payment-helper.php";
// 1. payment 타입으로 등록
dx_register_plugin(array(
"id" => "my-pg-payment",
"type" => "payment",
"name" => "나의 결제사",
"version" => "1.0.0",
"settings" => array(
"client_key" => array(
"label" => "클라이언트 키", "type" => "text", "required" => true,
),
"secret_key" => array(
"label" => "시크릿 키", "type" => "password", "required" => true,
),
"mode" => array(
"label" => "운영 모드", "type" => "select",
"options" => array("test"=>"테스트","live"=>"실서비스"),
"default" => "test",
),
),
));
// 2. 활성화 체크 — 내 플러그인이 아니면 종료
if (dx_active_plugin("payment") !== "my-pg-payment") return;
// 3. 결제창 훅 등록
dx_add_hook("dx_payment_request", function($data) {
$clientKey = dx_pay_cfg("my-pg-payment", "client_key");
$mode = dx_pay_cfg("my-pg-payment", "mode", "test");
if (!$clientKey) {
dx_payment_error_html("결제 키가 설정되지 않았습니다.");
return;
}
$orderId = isset($data["order_id"]) ? $data["order_id"] : dx_payment_order_id("MY");
$amount = isset($data["amount"]) ? (int)$data["amount"] : 0;
$productName = isset($data["product_name"]) ? $data["product_name"] : "결제";
$returnUrl = isset($data["return_url"]) ? $data["return_url"] : dx_base_url("payment/success");
$failUrl = isset($data["fail_url"]) ? $data["fail_url"] : dx_base_url("payment/fail");
dx_payment_wrap_open("나의 결제사"); // 결제창 래퍼 열기
?>
<script src="https://your-pg.com/sdk.js"></script>
<script>
YourPG.request({
clientKey: <?php echo json_encode($clientKey); ?>,
orderId: <?php echo json_encode($orderId); ?>,
amount: <?php echo $amount; ?>,
orderName: <?php echo json_encode($productName); ?>,
successUrl: <?php echo json_encode($returnUrl); ?>,
failUrl: <?php echo json_encode($failUrl); ?>,
});
</script>
<?php
dx_payment_wrap_close(); // 결제창 래퍼 닫기
});
// 4. 서버 검증 함수
function my_pg_confirm(array $params) {
$secretKey = dx_pay_cfg("my-pg-payment", "secret_key");
$mode = dx_pay_cfg("my-pg-payment", "mode", "test");
$paymentKey = isset($params["paymentKey"]) ? $params["paymentKey"] : "";
$orderId = isset($params["orderId"]) ? $params["orderId"] : "";
$amount = isset($params["amount"]) ? (int)$params["amount"]: 0;
if (!$paymentKey || !$orderId || $amount <= 0) {
return dx_payment_fail("필수 파라미터 누락");
}
// PG사 API 서버 검증
$apiUrl = $mode === "live"
? "https://api.your-pg.com/v1/payments/confirm"
: "https://test-api.your-pg.com/v1/payments/confirm";
$resp = dx_payment_http_post(
$apiUrl,
json_encode(array(
"paymentKey" => $paymentKey,
"orderId" => $orderId,
"amount" => $amount,
)),
array("Authorization: Basic " . base64_encode($secretKey . ":"))
);
$result = json_decode($resp, true);
if (!isset($result["orderId"])) {
$msg = isset($result["message"]) ? $result["message"] : "결제 검증 실패";
return dx_payment_fail($msg);
}
return dx_payment_ok(array(
"tid" => isset($result["paymentKey"]) ? $result["paymentKey"] : $paymentKey,
"order_id" => $orderId,
"amount" => $amount,
"raw" => $result,
));
}
7.3 dx-payment-helper.php 제공 함수
| 함수 |
설명 |
| dx_payment_http_post($url, $body, $headers) |
cURL 우선 POST 요청. file_get_contents 폴백 |
| dx_payment_http_get($url, $headers) |
cURL 우선 GET 요청 |
| dx_pay_cfg($pluginId, $key, $default="") |
dx_config("plugin_{id}_{key}") 단축 함수 |
| dx_payment_order_id($prefix="DX") |
고유 주문 ID 생성 (prefix+날짜+밀리초) |
| dx_payment_wrap_open($title) |
결제창 래퍼 div 열기 HTML 출력 |
| dx_payment_wrap_close() |
결제창 래퍼 닫기 HTML 출력 |
| dx_payment_ok($data) |
성공 배열 반환: ["success"=>true, "tid"=>..., ...] |
| dx_payment_fail($message) |
실패 배열 반환: ["success"=>false, "message"=>...] |
| dx_payment_error_html($msg) |
오류 메시지 HTML 출력 |
8장. 유틸리티 플러그인 실전 제작
8.1 성능 디버그 플러그인
DX_DEBUG 모드일 때 화면 우측 하단에 실행 시간과 DB 쿼리 수를 표시합니다.
plugin.php
<?php
if (!defined("DX_CMS")) exit;
if (defined("DX_DEBUG") && DX_DEBUG) {
dx_add_hook("dx_body_bottom", function() {
$time = round((microtime(true) - DX_START_TIME) * 1000, 2);
$qc = Database::getInstance()->getQueryCount();
echo '<div style="position:fixed;bottom:10px;right:10px;'
. 'background:rgba(15,23,42,.85);color:#fff;padding:8px 14px;'
. 'border-radius:10px;font-size:11px;z-index:9999;font-family:monospace">'
. '⚡ ' . $time . 'ms | DB: ' . $qc . '쿼리'
. '</div>';
}, 999);
}
8.2 글 등록 시 이메일 알림 플러그인
새 게시글이 등록될 때 관리자 이메일로 알림을 발송합니다.
<?php
if (!defined("DX_CMS")) exit;
dx_register_plugin(array(
"id" => "email-notifier",
"type" => "utility",
"name" => "이메일 알림",
"version" => "1.0.0",
"settings" => array(
"notify_email" => array(
"label" => "알림 받을 이메일",
"type" => "text",
"placeholder" => "admin@example.com",
),
"board_keys" => array(
"label" => "알림 게시판 (비우면 전체, 쉼표 구분)",
"type" => "text",
"placeholder" => "notice,free",
),
),
));
dx_add_hook("dx_after_write", function($args) {
$notifyEmail = dx_config("plugin_email-notifier_notify_email", "");
if (!$notifyEmail) return;
// 게시판 필터
$boardKeysStr = dx_config("plugin_email-notifier_board_keys", "");
if ($boardKeysStr) {
$keys = array_map("trim", explode(",", $boardKeysStr));
$boardKey = isset($args["board"]["board_key"]) ? $args["board"]["board_key"] : "";
if (!in_array($boardKey, $keys)) return;
}
$postId = isset($args["post_id"]) ? $args["post_id"] : "";
$board = isset($args["board"]) ? $args["board"] : array();
$data = isset($args["data"]) ? $args["data"] : array();
$title = isset($data["title"]) ? $data["title"] : "(제목 없음)";
$boardName = isset($board["board_name"]) ? $board["board_name"] : "";
$boardKey = isset($board["board_key"]) ? $board["board_key"] : "";
$postUrl = dx_base_url($boardKey . "/view/" . $postId);
$subject = "[새 글] [{$boardName}] {$title}";
$message = "새 게시글이 등록되었습니다. "
. "제목: {$title} "
. "게시판: {$boardName} "
. "URL: {$postUrl}";
@mail($notifyEmail, $subject, $message,
"From: noreply@" . (isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"] : "localhost"));
}, 10);
8.3 글쓰기 커스텀 필드 플러그인
게시글에 "출처 URL" 커스텀 필드를 추가하고, 뷰 페이지에 표시하는 예시입니다. dx_board_before_save 훅과 dx_board_view_context 훅을 함께 활용합니다.
plugin.php
<?php
if (!defined("DX_CMS")) exit;
dx_register_plugin(array(
"id" => "source-url-field",
"type" => "utility",
"name" => "출처 URL 필드",
"version" => "1.0.0",
));
// 1. 글쓰기 폼에 필드 추가 (write_context 훅)
dx_add_hook("dx_board_write_context", function($args) {
// $args["context"] 참조로 접근 가능
// 글쓰기 폼 하단에 필드 출력은 dx_bottom 훅 활용
});
// 2. 글쓰기 폼 하단에 HTML 필드 주입
dx_add_hook("dx_board_bottom", function($ctx) {
if (!isset($ctx["type"]) || ($ctx["type"] !== "write" && $ctx["type"] !== "edit")) return;
$editPost = isset($ctx["editPost"]) ? $ctx["editPost"] : array();
$curVal = isset($editPost["source_url"]) ? $editPost["source_url"] : "";
?>
<div style="margin-top:12px">
<label style="display:block;font-size:.8rem;font-weight:700;color:#64748b;margin-bottom:6px">출처 URL</label>
<input type="url" name="source_url"
value="<?php echo htmlspecialchars($curVal, ENT_QUOTES, "UTF-8"); ?>"
placeholder="https://..."
style="width:100%;padding:9px 14px;border:1px solid #e2e8f0;border-radius:10px;font-size:.875rem">
</div>
<?php
});
// 3. 저장 전 데이터에 추가
// ※ dx_board_before_save의 data는 실제 DB 컬럼이어야 함
// posts 테이블에 source_url 컬럼이 없으면 settings나 별도 테이블 사용
// 여기서는 content에 메타 태그로 포함하는 방식 예시:
dx_add_hook("dx_board_before_save", function($args) {
$sourceUrl = isset($_POST["source_url"]) ? trim($_POST["source_url"]) : "";
if (!$sourceUrl) return;
// URL 검증
if (!filter_var($sourceUrl, FILTER_VALIDATE_URL)) return;
// content에 출처 정보 메타 추가 (data 참조 변경)
$args["data"]["source_url"] = $sourceUrl; // posts 테이블에 컬럼 필요
});
// 4. 뷰 페이지에 출처 URL 표시
dx_add_hook("dx_board_view_context", function($args) {
// $args["context"]["post"]에 source_url이 있으면 뷰에서 출력
// 실제 표시는 테마의 board/view.php에서 $post["source_url"] 사용
});
9장. DxContainer (DI 컨테이너) 활용
DxContainer는 라라벨 스타일 DI 컨테이너입니다. 플러그인이 제공하는 서비스(SMS 발송기, 이메일 클라이언트 등)를 등록하면 다른 플러그인이나 테마에서 가져다 쓸 수 있습니다.
9.1 서비스 등록 (plugin.php에서)
// 싱글턴 등록 — 한 번 생성 후 재사용
dx_app()->singleton("sms", function() {
return new MySmsSender(
dx_config("plugin_my-sms_api_key"),
dx_config("plugin_my-sms_sender") // 발신번호
);
});
// 팩토리 등록 — 호출마다 새 인스턴스
dx_app()->bind("mailer", function() {
return new MyMailer(dx_config("smtp_host"));
});
9.2 서비스 사용 (다른 곳에서)
// 훅 콜백, 테마 파일, boards handler 등 어디서든
$sms = dx_make("sms");
$sms->send("01012345678", "인증번호: 123456");
// dx_make() = dx_app()->make() 단축 함수
$mailer = dx_make("mailer");
$mailer->send("user@example.com", "제목", "내용");
// 등록 여부 확인
if (dx_app()->bound("sms")) {
$sms = dx_make("sms");
}
10장. DxRouter — 플러그인 전용 URL 등록
플러그인이 독립 URL(예: /my-plugin/dashboard, /api/my-plugin/data)을 처리해야 할 때 DxRouter를 사용합니다. 기존 파일 기반 라우팅보다 깔끔하게 URL을 관리할 수 있습니다.
10.1 기본 라우트 등록
// plugin.php에서
// 단순 클로저 라우트
DxRouter::get("/my-plugin/dashboard", function() {
if (!dx_is_login()) dx_redirect(dx_base_url("auth/login"));
$db = Database::getInstance();
$data = $db->rows("SELECT * FROM ...");
// 렌더링
$dxth = DxTheme::getInstance();
ob_start();
include __DIR__ . "/views/dashboard.php";
$dx_content = ob_get_clean();
$layoutFile = $dxth->resolve("layout/main.php");
if ($layoutFile) require $layoutFile;
else echo $dx_content;
})->middleware("auth");
// POST 라우트
DxRouter::post("/api/my-plugin/save", function() {
dx_csrf_check();
$data = dx_post("data", "");
dx_json(array("success" => true, "message" => "저장됨"));
})->middleware(array("auth", "csrf"));
// 그룹 라우트
DxRouter::group(array("prefix" => "/my-plugin", "middleware" => "auth"), function() {
DxRouter::get("/list", function() { /* 목록 */ });
DxRouter::get("/view", function() { /* 상세 */ });
DxRouter::post("/save", function() { /* 저장 */ })->middleware("csrf");
});
10.2 미들웨어 옵션
| 미들웨어 |
동작 |
| "auth" |
비로그인 시 /auth/login 으로 리다이렉트 |
| "admin" |
비관리자 시 403 오류 |
| "csrf" |
POST 요청 시 CSRF 토큰 검증. 실패 시 403 |
| 배열로 여러 개 |
->middleware(["auth", "csrf"]) 순서대로 모두 실행 |
11장. 관리자 패널•위젯 추가
11.1 관리자 대시보드 위젯 추가
dx_admin_dashboard_widgets 훅으로 관리자 대시보드에 위젯을 삽입할 수 있습니다.
// plugin.php에서
dx_add_hook("dx_admin_dashboard_widgets", function() {
$db = Database::getInstance();
$count = (int)$db->value("SELECT COUNT(*) FROM `{$db->table("posts")}`");
?>
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:20px;">
<h3 style="margin:0 0 8px;font-size:.9rem;font-weight:700">내 플러그인 통계</h3>
<p style="margin:0;font-size:1.5rem;font-weight:800;color:#1a73e8">
<?php echo number_format($count); ?>
<span style="font-size:.8rem;color:#64748b">총 게시글</span>
</p>
</div>
<?php
});
11.2 관리자 전용 설정 페이지 (DxRouter 활용)
// plugin.php에서
DxRouter::get("/admin/my-plugin/settings", function() {
if (!dx_is_admin()) { http_response_code(403); exit; }
ob_start();
include __DIR__ . "/admin/settings.php"; // admin/settings.php
$dx_content = ob_get_clean();
$layout = DxTheme::getInstance()->resolve("layout/main.php");
if ($layout) require $layout;
else echo $dx_content;
})->middleware("admin");
DxRouter::post("/admin/my-plugin/settings", function() {
if (!dx_is_admin()) { dx_json(array("success"=>false),403); }
dx_csrf_check();
$db = Database::getInstance();
$val = dx_post("some_setting", "");
$db->query(
"INSERT INTO `{$db->table("settings")}` (setting_key,setting_value,updated_at)
VALUES (?,?,NOW()) ON DUPLICATE KEY UPDATE setting_value=?,updated_at=NOW()",
array("plugin_my-plugin_some_setting", $val, $val)
);
if (class_exists("DxCache")) DxCache::flush();
dx_json(array("success"=>true));
})->middleware(array("admin","csrf"));
12장. 디버깅 및 배포 체크리스트
12.1 개발 시 디버깅 방법
// config.php에서 디버그 모드 활성화
define("DX_DEBUG", true); // 오류가 화면에 표시됨
// 훅 실행 로그 확인 (DX_DEBUG=true 시)
$executed = HookManager::getInstance()->getExecuted();
error_log(print_r($executed, true));
// 등록된 훅 목록 확인
$allHooks = HookManager::getInstance()->getAll();
// 플러그인 설정값 확인
var_dump(dx_config("plugin_my-plugin_api_key"));
// 오류는 data/error.log 에 기록됨
dx_log("내 플러그인 초기화", "debug"); // info/debug: 기록 안 됨
dx_log("설정 오류", "error"); // error: data/error.log 기록
12.2 자주 발생하는 문제
| 증상 |
원인 및 해결 |
| 플러그인이 목록에 안 보임 |
manifest.php 없거나 return array() 형식 오류. PHP 구문 오류 확인 |
| 훅이 실행되지 않음 |
훅 이름 오타. dx_run_hook 실행 위치 확인. DX_DEBUG=true로 실행 로그 출력 |
| settings 값이 저장 안 됨 |
설정 키 오타 (plugin_{id}_{key} 형식 확인). DxCache::flush() 후 재확인 |
| 활성화해도 기능이 동작 안 함 |
dx_active_plugin("타입") !== "내ID" 체크 위치 확인. return 구문 위치 확인 |
| 중복 실행됨 |
static $loaded 변수로 초기화 중복 방지. 전역 상수로도 가능 |
| PHP 버전 오류 |
PHP 5.6 호환: []대신 array(), 단순 클로저 사용 |
12.3 배포 체크리스트
- [ ] manifest.php에 name, version 작성 완료
- [ ] plugin.php 첫 줄: if (!defined("DX_CMS")) exit 포함
- [ ] 모든 사용자 입력값 htmlspecialchars() 처리
- [ ] POST 처리 시 dx_csrf_check() 호출
- [ ] 파일 업로드 처리 시 확장자•크기 검증
- [ ] dx_config()로 설정값 읽기 (하드코딩 금지)
- [ ] 에러 발생 시 dx_log("오류 내용", "error") 로 로깅
- [ ] DX_DEBUG=false 상태에서 최종 동작 확인
- [ ] PHP 5.6 호환 코드 작성 확인 ([] → array(), 화살표 함수 금지)
- [ ] data/ 폴더나 plugins/ 폴더 외부 경로 접근 없는지 확인