그누보드 개발자를 위한 DXCMS 직접 개발 가이드 — 이 페이지 하나로 바로 시작할 수 있습니다
// 경로 자동 탐색 $path = __DIR__; while (!file_exists($path.'/common.php')) { $p = dirname($path); if ($p === $path) break; $path = $p; } include_once $path.'/common.php';
// 방법 1: dx_load.php가 같은 폴더 require_once 'dx_load.php'; // 방법 2: 경로 자동 탐색 (어느 폴더에서든) $p = __DIR__; while (!file_exists($p.'/dx_load.php')) { $pp = dirname($p); if ($pp === $p) break; $p = $pp; } require_once $p.'/dx_load.php';
../../ 없이 동작합니다. dx_load.php 는 DXCMS 루트에 한 번만 놓으면 됩니다.is_login() // 로그인 여부 is_admin() // 관리자 여부 $member['mb_id'] $member['mb_name'] $member['mb_email'] $member['mb_point'] $member['mb_level']
dx_is_login() // 로그인 여부 dx_is_admin() // 관리자 여부 $auth = Auth::getInstance(); $user = $auth->user(); $user['id'] // BIGINT 문자열! $user['name'] $user['email'] $user['point'] $user['level']
// 로그인 필수 페이지 패턴 if (!dx_is_login()) { header('Location: ' . dx_base_url('auth/login') . '?redirect=' . urlencode(dx_current_url())); exit; } // 관리자 필수 페이지 패턴 if (!dx_is_admin()) { http_response_code(403); exit('접근 권한이 없습니다.'); }
(int) 캐스팅 절대 금지. 문자열 그대로 사용하세요.// 여러 행 $res = sql_query("SELECT * FROM ..."); while($row=sql_fetch_array($res)){} // 단일 행 $row = sql_fetch("SELECT * FROM ..."); // 값 변경 sql_query("INSERT INTO ...");
// 여러 행 $db = Database::getInstance(); $rows = $db->rows("SELECT * FROM `{$db->table('posts')}`"); // 단일 행 (파라미터 바인딩) $row = $db->row( "SELECT * FROM `{$db->table('posts')}` WHERE id=?", array($id) ); // INSERT / UPDATE / DELETE $db->insertRow('my_table', array(...)); $db->updateRow('my_table', array(...), array(...)); $db->deleteRow('my_table', array('id'=>$id));
// ─── 전체 메서드 목록 ─────────────────────────────────────────────── $db->rows($sql, $params) // 여러 행 → 2차원 배열 (없으면 빈 배열) $db->row($sql, $params) // 단일 행 → 1차원 배열 (없으면 null) $db->value($sql, $params) // 단일 값 (COUNT 등) $db->query($sql, $params) // INSERT/UPDATE/DELETE 직접 실행 $db->insertRow('테이블명', $data) // 편의 INSERT → 새 ID 반환 $db->updateRow('테이블명', $set, $where) // 편의 UPDATE $db->deleteRow('테이블명', $where) // 편의 DELETE $db->table('posts') // → 'dx_posts' (prefix 자동) $db->pdo() // PDO 직접 접근 (DDL, 트랜잭션 등) // ─── 안전 패턴 (항상 try/catch 감싸기) ───────────────────────────── $rows = array(); try { $rows = $db->rows("SELECT id, title FROM `{$db->table('posts')}` WHERE status=1 LIMIT 10"); } catch (Exception $e) { // 테이블 없거나 컬럼명 다른 경우 대비 }
| 제목 | 날짜 |
|---|---|
| 자 오늘도 화이팅하세요. | 2026-06-10 |
| 모아님 보.고.시.퍼.요 | 2026-06-10 |
| 다 좋았는데 다 웃었는데 | 집에 가는 길이 유난히 허전한 밤 [AI Song] | 2026-06-10 |
| CMS관련 문의드립니다 | 2026-06-09 |
| 서비스 장애 복구 완료 안내 | 2026-06-09 |
G5_URL // 사이트 루트 URL G5_PATH // 루트 절대경로 G5_DATA_PATH // data 폴더 G5_BBS_URL // /bbs URL
dx_base_url() // 사이트 루트 URL dx_base_url('notice/list') // 내부 URL dx_current_url() // 현재 페이지 URL DX_ROOT // 루트 절대경로 DX_DATA // /data 폴더 DX_CORE // /core 폴더 DX_PLUGINS // /plugins 폴더
// ─── 설정 조회 ($config 대체) ──────────────────────────────────────── dx_config('site_name', '기본값') // 사이트명 dx_config('admin_email', '') // 관리자 이메일 dx_config('theme', 'default') // 현재 테마 dx_config('upload_max_size', '') // 업로드 최대 크기 // ─── URL / 경로 헬퍼 ──────────────────────────────────────────────── dx_base_url('board/list') // 내부 URL 생성 dx_current_url() // 현재 페이지 전체 URL dx_redirect($url) // 리다이렉트 후 exit dx_redirect_back('/') // 이전 페이지로 (fallback 지정) // ─── 입력값 처리 ──────────────────────────────────────────────────── dx_get('key', '기본값') // $_GET 안전하게 가져오기 dx_post('key', '기본값') // $_POST 안전하게 가져오기 dx_method('POST') // 요청 메서드 확인 // ─── 파일/이미지 ──────────────────────────────────────────────────── dx_filesize($bytes) // 파일 크기를 읽기 좋게 (예: "1.2 MB") dx_base_url('data/uploads/파일.jpg') // 업로드 파일 URL DxThumb::url('data/uploads/파일.jpg', 300) // 300px 썸네일 URL // ─── 기타 ──────────────────────────────────────────────────────────── dx_ip() // 접속자 IP (프록시 고려) dx_date($datetime, 'Y.m.d') // 날짜 형식 변환 dx_is_mobile() // 모바일 여부 dx_mb_substr($str, 0, 20) // 멀티바이트 안전 substr dx_json(array('success'=>true)) // JSON 응답 + exit dx_csrf_check() // CSRF 검증 (실패 시 자동 종료) dx_csrf_field() // <input type="hidden" name="_csrf" ...> dx_csrf_token() // 토큰 문자열만
// ─── POST 처리 전형 패턴 ──────────────────────────────────────────── if (dx_method('POST')) { dx_csrf_check(); // ← 반드시 첫 줄! 실패 시 자동 종료 $title = dx_post('title', ''); $db->insertRow('my_table', array('title' => $title)); dx_redirect(dx_base_url('direct/index.php')); } // ─── HTML 폼 ──────────────────────────────────────────────────────── // <form method="post"> // <?php echo dx_csrf_field(); ?> ← 이 한 줄만 추가 // <input type="text" name="title"> // <button type="submit">저장</button> // </form> // ─── API (Ajax JSON) 처리 ─────────────────────────────────────────── if (dx_method('POST')) { dx_csrf_check(); // 처리 후 dx_json(array('success' => true, 'message' => '저장됨')); // dx_json은 내부적으로 exit 호출 }
insert_point( $mb_id, $point, '사유', 'board', $board_id );
DxPoint::add($userId, $amount, '사유'); DxPoint::subtract($userId, $amount, '사유');
$user['id'] 를 그대로 전달하세요. (int) 캐스팅 금지.// ─── 훅 등록 + 콜백 ───────────────────────────────────────────────── // ⚠ 클로저(익명함수) 금지! 반드시 일반 함수 사용 (PHP 5.6 호환) dx_add_hook('dx_after_login', 'my_login_hook'); function my_login_hook($args) { $userId = $args['user']['id']; // BIGINT 문자열 DxPoint::add($userId, 10, '로그인 포인트'); } // ─── 내장 훅 목록 ─────────────────────────────────────────────────── // dx_after_login 로그인 후 → $args['user'] // dx_after_logout 로그아웃 후 → $args['user'] // dx_after_register 가입 후 → $args['user_id'] // dx_board_after_save 글 저장 후 → $args['post_id'], $args['board_key'] // dx_board_after_delete 글 삭제 후 → $args['post_id'] // ─── 커스텀 훅 실행 ───────────────────────────────────────────────── dx_run_hook('my_custom_event', array('data' => '값'));
// ─── routes/myapp.php 파일 생성 ───────────────────────────────────── // DXCMS 루트/routes/ 폴더에 파일을 추가하면 자동 로드됩니다. // URL: 도메인/myapp/... 형태로 접근 가능해집니다. // routes/myapp.php if (!defined('DX_CMS')) exit(); require_once DX_ROOT . '/myapp/controllers/MyController.php'; DxRouter::group(array('prefix' => '/myapp'), function() { DxRouter::get('/', 'MyController@index'); DxRouter::get('/list', 'MyController@list'); DxRouter::get('/view/{id}', 'MyController@view'); // 미들웨어 체이닝 DxRouter::get('/mypage', 'MyController@mypage')->middleware('auth'); DxRouter::post('/save', 'MyController@save')->middleware(array('auth', 'csrf')); DxRouter::any('/admin', 'MyController@admin')->middleware('admin'); }); // ─── 미들웨어 종류 ────────────────────────────────────────────────── // 'auth' → 로그인 필수 (비로그인 시 로그인 페이지로) // 'admin' → 관리자 필수 (아니면 403) // 'csrf' → POST CSRF 토큰 검증 // 'guest' → 비로그인 전용 (로그인 상태면 메인으로) // 'json' → Content-Type: application/json 자동 설정
// ─── 컨트롤러 구조 (myapp/controllers/MyController.php) ───────────── if (!defined('DX_CMS')) exit(); class MyController { private $_db; private $_auth; public function __construct() { $this->_db = Database::getInstance(); $this->_auth = Auth::getInstance(); } public function index($params) { $data = array('title' => '메인'); $this->render('main', $data); } public function view($params) { $id = isset($params['id']) ? $params['id'] : ''; // URL: /myapp/view/123 → $id = '123' $this->render('view', array('id' => $id)); } private function render($view, $data = array()) { extract($data); require DX_ROOT . '/myapp/views/' . $view . '.php'; } }
// ─── extend/top/ 폴더에 파일 추가 → 모든 요청에서 자동 실행 ───────── // 파일명 숫자 순으로 실행됩니다 (10_xxx.php → 20_xxx.php → ...) // extend/top/10_my_middleware.php if (!defined('DX_CMS')) exit(); // 예: 모든 페이지에서 방문자 카운트 function _my_visit_count($args) { $db = Database::getInstance(); try { $db->query("UPDATE `{$db->table('settings')}` SET setting_value=setting_value+1 WHERE setting_key='visit_count'"); } catch (Exception $e) {} } dx_add_hook('dx_page_start', '_my_visit_count'); // ─── extend/top/ 용도별 파일 예시 ─────────────────────────────────── // 10_board_fields.php 게시판 여분 필드 자동 로드 // 20_my_plugin.php 커스텀 플러그인 // 50_market_guard.php 마켓 다운로드 보호 // 90_maintenance.php 점검 모드 // ─── DX_DIRECT 플래그 (dx_load.php 로 실행 시 true) ───────────────── if (defined('DX_DIRECT') && DX_DIRECT) { // Direct 방식에서만 실행할 코드 }
DxNotification::send($toMemberId, array( 'type' => 'system', // 알림 타입 'message' => '내용', 'url' => dx_base_url('direct/index.php'), )); // 타입 종류: 'system', 'comment', 'like', 'mention', 'memo'
// ❌ 틀림 (PHP 7+ 전용) $name = $user['name'] ?? '기본값'; // ?? 연산자 $arr = ['key' => 'value']; // 배열 단축 문법 $uid = (int)$user['id']; // BIGINT 캐스팅 dx_add_hook('event', function($a){}); // 클로저 훅 // ✅ 맞음 (PHP 5.6 호환) $name = isset($user['name']) ? $user['name'] : '기본값'; $arr = array('key' => 'value'); $uid = $user['id']; // 문자열 그대로 dx_add_hook('event', 'my_function_name'); // 일반 함수 // ✅ 출력은 항상 htmlspecialchars echo htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); // ✅ DB 쿼리는 try/catch 감싸기 try { $rows = $db->rows($sql); } catch (Exception $e) { $rows = array(); }
7f3a71b54fb1936f63ce...dx_csrf_check() 검증 → 통과 시 처리
// ═══ 방법 A: 테마 레이아웃(헤더+콘텐츠+푸터) 완전 적용 ══════════════ require_once 'dx_load.php'; ob_start(); // ← 출력 캡처 시작 ?> <div>내 HTML 콘텐츠</div> <?php $dx_content = ob_get_clean(); // ← 캡처 완료 // $context: main.php 가 사이드바/SEO 판단에 사용 // Direct 방식은 'direct' 로 설정 → 사이드바 없이 전체 폭 $context = array('type' => 'direct'); $_layout = dx_theme_file('layout/main.php'); if ($_layout && file_exists($_layout)) { include $_layout; // ← 테마가 $dx_content 를 감싸서 출력 } else { echo $dx_content; } exit;
// ═══ 방법 B: 테마 없이 내 HTML만 출력 (Direct 독립 페이지) ══════════ require_once 'dx_load.php'; ?> <!DOCTYPE html> <html lang="ko"> <head><meta charset="UTF-8"><title>내 페이지</title></head> <body> <?php echo dx_is_login() ? '로그인 중' : '비로그인'; ?> </body></html>
// ═══ 기타 dx_theme_file 활용 ════════════════════════════════════════ dx_theme_file('layout/main.php') // 테마 레이아웃 절대경로 dx_theme_file('board/basic/list.php') // 테마 내 특정 파일 dx_theme_asset('css/custom.css') // 테마 에셋 URL dx_theme_option('primary_color', '#1a73e8') // 테마 옵션
// ─── 방법 1: 배열로 받아서 직접 출력 ──────────────────────────────── // dx_board_posts(게시판키, 개수, 공지포함, 제목길이, 요약길이) $posts = dx_board_posts('free', 5); // 반환 배열 구조: id(문자열), title, created_at, view_count, // board_key, author, thumbnail, excerpt 등 foreach ($posts as $p) { $postId = $p['id']; // BIGINT 문자열 — (int) 금지! $title = $p['title']; $date = substr($p['created_at'], 0, 10); $viewUrl = dx_base_url('free/view/' . $postId); echo '<a href="' . $viewUrl . '">' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '</a>'; } // ─── 방법 2: 위젯 HTML 바로 출력 (스킨 적용) ─────────────────────── // dx_board_latest(게시판키, 개수, 스킨, 제목텍스트, 아이콘) dx_board_latest('notice', 5, 'list', '공지사항', ''); dx_board_latest('free', 5, 'list', '자유게시판', ''); // ─── 방법 3: DB 직접 조회 (커스텀 조건) ──────────────────────────── $db = Database::getInstance(); $boardRow = $db->row( "SELECT id FROM `{$db->table('boards')}` WHERE board_key=? AND status=1", array('free') ); if ($boardRow) { $posts = $db->rows( "SELECT id, title, created_at FROM `{$db->table('posts')}` WHERE board_id=? AND status=1 AND is_notice=0 ORDER BY created_at DESC LIMIT 10", array($boardRow['id']) ); }
$p['id'] 그대로 URL에 넣으세요. (int) 캐스팅 금지.// ─── dx_pagination 기본 사용법 ────────────────────────────────────── // dx_pagination($total, $perPage, $current, $urlPattern, $range=5) $page = max(1, (int)dx_get('page', 1)); $perPage = 10; // 전체 건수 조회 $total = (int)$db->value( "SELECT COUNT(*) FROM `{$db->table('posts')}` WHERE status=1" ); // {page} 가 실제 페이지 번호로 자동 치환됩니다 $pagingHtml = dx_pagination( $total, // 전체 건수 $perPage, // 페이지당 행 수 $page, // 현재 페이지 '?page={page}', // URL 패턴 5 // 표시할 버튼 수 (기본 5) ); // 검색 파라미터가 있는 경우 $keyword = dx_get('keyword', ''); $pagingHtml = dx_pagination( $total, $perPage, $page, '?keyword=' . urlencode($keyword) . '&page={page}' ); // ─── 출력 ──────────────────────────────────────────────────────────── echo '<div class="my-page-wrap">' . $pagingHtml . '</div>'; // ─── CSS (페이지 버튼 스타일) ──────────────────────────────────────── // dx_pagination 이 생성하는 클래스: .dx-page-btn / .dx-page-active
// ─── OFFSET 계산 (LIMIT 쿼리와 세트로 사용) ────────────────────────── $offset = ($page - 1) * $perPage; $rows = $db->rows( "SELECT id, title FROM `{$db->table('posts')}` WHERE status=1 ORDER BY created_at DESC LIMIT {$perPage} OFFSET {$offset}" ); // posts.id 는 BIGINT — (int) 캐스팅 금지! foreach ($rows as $r) { $id = $r['id']; // 문자열 그대로 $title = $r['title']; }