회원가입 | 고객센터 |
DESIGNONEX
dxcms.kr
로그인 회원가입
고객센터
8. 플러그인

플러그인 제작

D DX
2026.05.01 01:39(수정됨) 132 0

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/ 폴더 외부 경로 접근 없는지 확인

댓글0

로그인 후 댓글을 작성할 수 있습니다.
1. DX 철학 / 개념 왜 DXCMS를 만들었는가 2026.04.20 1. DX 철학 / 개념 DXCMS란 무엇인가 2026.04.20 DXCMS 활용 (CMS) DXCMS 날코딩•막코딩 완전 허용 2026.04.12
31
전체 회원
503
전체 게시글
775
전체 댓글
442
오늘 방문
33,174
전체 방문
3
현재 접속
인기글 7일 이내
최신글
최신댓글
목록