회원가입 | 고객센터 |
DESIGNONEX
디자인원엑스
About
Service
Q&A
PR리그
자유게시판N
갤러리
포인트게임
공지사항N
통계
로그인 회원가입
고객센터
3.9 공통 함수 / 유틸

공통 클래스 구조

A Administrator
2026.04.21 01:01(수정됨) 99 0

1. 공통 클래스 전체 구조 개요

DXCMS v8.1.0의 core/ 디렉토리에는 10개의 공통 클래스가 있습니다. 이 클래스들은 모두 싱글턴(Singleton) 패턴 또는 정적 클래스로 구현되어 있으며, PHP 5.6부터 8.x까지 완전 호환됩니다. 직접 new 키워드로 생성하지 않고, getInstance() 또는 정적 메서드로만 사용합니다.


1.1 클래스 로드 순서 및 역할

로드순서 클래스 파일 위치 패턴 핵심 역할
1 functions.php core/functions.php 전역 함수 전역 헬퍼 함수 55개 정의
2 DxCache core/DxCache.php 정적 클래스 Redis·APCu·파일 캐시 멀티 드라이버
3 Database core/db/Database.php 싱글턴 PDO 래퍼, 쿼리 빌더, BIGINT 안전 ID
4 Secure core/Secure.php 싱글턴 세션·CSRF·WAF·Rate Limit·bcrypt
5 HookManager core/hook/HookManager.php 싱글턴 Action·Filter 훅 등록/실행
6 PluginRegistry core/PluginRegistry.php 싱글턴 플러그인 타입 등록소
7 Auth core/auth/Auth.php 싱글턴 로그인·회원가입·Remember Me
8 DxSite core/DxSite.php 싱글턴 멀티사이트 도메인 감지·설정 오버라이드
9 DxTheme core/DxTheme.php 싱글턴 테마 파일 해석·폴백 체인
10 DxContainer core/DxContainer.php 싱글턴 경량 DI 컨테이너
11 DxPoint core/DxPoint.php 정적 클래스 포인트·경험치·레벨 엔진


1.2 싱글턴 패턴 — 공통 구조

Database, Secure, Auth, HookManager, PluginRegistry, DxSite, DxTheme, DxContainer는 모두 동일한 싱글턴 패턴으로 구현됩니다.
 
// 모든 싱글턴 클래스의 공통 패턴
class SomeClass {
    private static $instance = null;
    private function __construct() {}   // 외부 new 불가

    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

// 올바른 사용 — getInstance()로만 접근
$db  = Database::getInstance();
$sec = Secure::getInstance();
$auth = Auth::getInstance();

// 잘못된 사용 — new 키워드 사용 금지
$db = new Database();  // Fatal Error: private constructor


1.3 클래스 의존 관계

functions.php              ← 최초 로드 (클래스 없이 동작)
    │
    ├── DxCache            ← functions.php만 의존
    │
    ├── Database           ← functions.php만 의존
    │                        (오류 시 dx_error() 호출)
    │
    ├── Secure             ← functions.php + Database(옵션)
    │       └── DxCache   (Rate Limit 파일 폴백에서만)
    │
    ├── HookManager        ← 의존 없음 (독립적)
    │
    ├── PluginRegistry     ← HookManager + dx_log()
    │
    ├── Auth               ← Database + Secure + HookManager
    │       └── DxCache   (컬럼 캐싱에서)
    │
    ├── DxSite             ← Database + DxCache + dx_config_set()
    │
    ├── DxTheme            ← dx_config() + DX_THEMES 상수
    │
    ├── DxContainer        ← 독립적
    │
    └── DxPoint            ← Database


2. Database — PDO 래퍼 클래스

Database    MySQL•MariaDB PDO 래퍼. 싱글턴. PHP 5.6~8.x 호환

모든 DB 조작의 진입점입니다. PDO를 직접 사용하지 않고 이 클래스의 메서드를 통해 쿼리합니다. BIGINT 오버플로우 방어, 디버그 쿼리 로그, 동시성 안전 ID 생성이 핵심 기능입니다.


2.1 연결 및 인스턴스

// data/config.php에서 자동 연결 (직접 호출 불필요)
Database::getInstance()->connect('localhost', 'dbname', 'user', 'pass', 'utf8mb4', 'dx_');

// 이후 어디서나 getInstance()로 접근
$db = Database::getInstance();

PDO 설정 핵심
• ERRMODE_EXCEPTION: 오류 시 PDOException 발생 (조용한 실패 방지)
• FETCH_ASSOC: 연관 배열로 결과 반환
• EMULATE_PREPARES = true: PHP 32bit에서 BIGINT(16자리) 오버플로우 방지
  → int 캐스팅 없이 문자열로 수신하여 정밀도 보장
• STRINGIFY_FETCHES = true: DECIMAL•BIGINT 등 숫자형도 문자열로 반환


2.2 기본 쿼리 메서드

메서드 반환 설명
row($sql, $params) array|null SELECT 단일 행 반환. 없으면 null.
rows($sql, $params) array SELECT 전체 행 배열. 없으면 [].
value($sql, $params) mixed SELECT 단일 컬럼 값 반환. COUNT, MAX 등에 사용.
query($sql, $params) int INSERT/UPDATE/DELETE. 영향받은 행 수 반환.
insert($sql, $params) string INSERT 후 마지막 INSERT ID 반환.
 
// 단일 행
$user = $db->row("SELECT * FROM `dx_members` WHERE id=? LIMIT 1", array(42));

// 전체 행
$posts = $db->rows("SELECT * FROM `dx_posts` WHERE board_id=? ORDER BY id DESC", array(1));

// 단일 값 (COUNT, MAX 등)
$count = $db->value("SELECT COUNT(*) FROM `dx_posts` WHERE board_id=?", array(1));

// INSERT/UPDATE/DELETE
$db->query("UPDATE `dx_members` SET login_fail=0 WHERE id=?", array(42));

// INSERT + 마지막 ID
$newId = $db->insert("INSERT INTO `dx_posts` (title) VALUES (?)", array('제목'));


2.3 편의 메서드 (빌더 스타일)

메서드 반환 설명
find($table, $where, $fields) array|null WHERE 조건으로 단일 행 조회. LIMIT 1 자동.
findAll($table, $where, $fields, $order, $limit) array WHERE 조건으로 전체 행 조회.
insertRow($table, $data) string 연관 배열로 INSERT. DX_SHUTTING_DOWN 안전.
updateRow($table, $data, $where) int 연관 배열로 UPDATE.
deleteRow($table, $where) int WHERE 조건으로 DELETE.
exists($table, $where) bool 행 존재 여부 확인. COUNT(*) 사용.
count($table, $where) int 행 수 반환.
 
// find: prefix 자동 적용 (dx_members → members 입력)
$user = $db->find('members', array('id'=>42, 'status'=>1));

// findAll: ORDER BY + LIMIT 지원
$posts = $db->findAll('posts', array('board_id'=>1), '*', 'id DESC', '10');

// insertRow: 연관 배열 자동 SQL 생성
$db->insertRow('posts', array(
    'title'      => '제목',
    'content'    => '내용',
    'created_at' => date('Y-m-d H:i:s'),
));

// updateRow: SET + WHERE 분리
$db->updateRow('members', array('login_fail'=>0), array('id'=>42));

// exists: 중복 확인
if ($db->exists('members', array('login_id'=>'hong'))) {
    dx_json(array('error'=>'이미 사용중인 아이디'));
}


2.4 BIGINT 안전 ID 생성

일반적인 AUTO_INCREMENT 대신 microtime 기반 16자리 BIGINT ID를 생성합니다. 분산 서버 환경과 PHP 32bit 오버플로우를 모두 고려한 설계입니다.
 
// 16자리 ID 구조
// 밀리초 타임스탬프 13자리 + 랜덤 3자리 = 최대 16자리
// 예: 1746345600000 + 042 = 1746345600000042

// generateMicrotimeId: 중복 시 최대 10회 재시도
$id = $db->generateMicrotimeId('posts');  // '1746345600000042' (문자열)

// insertWithMicrotimeId: ID 자동 생성 + INSERT
$id = $db->insertWithMicrotimeId('posts', array(
    'title'      => '제목',
    'content'    => '내용',
    'board_id'   => 1,
    'created_at' => date('Y-m-d H:i:s'),
));
// → ID가 data['id'] 첫 번째에 자동 삽입되어 INSERT

// 주의: 반환값은 문자열 (32bit PHP에서 int 캐스팅 금지)
// (int)$id 는 절대 하지 말 것 — 2147483647 오버플로우 발생


2.5 트랜잭션

$db = Database::getInstance();
$db->begin();
try {
    $db->query("UPDATE `dx_members` SET point=point-? WHERE id=?", array(100, $buyerId));
    $db->query("UPDATE `dx_members` SET point=point+? WHERE id=?", array(100, $sellerId));
    $db->insertRow('transactions', array(
        'buyer_id'  => $buyerId,
        'seller_id' => $sellerId,
        'amount'    => 100,
    ));
    $db->commit();
} catch (Exception $e) {
    $db->rollback();
    dx_log('트랜잭션 실패: ' . $e->getMessage(), 'error');
}


2.6 유틸리티 메서드

메서드 반환 설명
escape($str) string PDO::quote() 래퍼. 따옴표 제거. FULLTEXT 검색 등에 사용.
table($name) string prefix + 테이블명 반환. dx_ + posts = dx_posts.
prefix() string 현재 DB 테이블 prefix 반환. 기본: dx_.
tableExists($name) bool 테이블 존재 여부. SHOW TABLES LIKE 사용.
getQueryCount() int 이번 요청의 쿼리 실행 횟수 (디버그).
getQueryLog() array DX_DEBUG=true 시 쿼리 + 파라미터 로그.
pdo() PDO PDO 인스턴스 직접 접근 (고급 사용).


2.7 DX_SHUTTING_DOWN 플래그

dx_redirect() 호출 시 $GLOBALS['DX_SHUTTING_DOWN'] = true로 설정됩니다. 이 플래그가 켜진 상태에서 DB 오류가 발생하면 dx_error(exit)로 이어지지 않고 조용히 실패합니다. register_shutdown_function 안의 방문자 통계 등이 이 상황에서도 안전하게 동작합니다.


3. DxCache — 멀티 드라이버 캐시

DxCache 정적 클래스. Redis·APCu·파일·none 4단계 자동 선택

모든 메서드가 static입니다. new나 getInstance() 없이 DxCache::get() / DxCache::set()으로 바로 사용합니다.


3.1 드라이버 선택 우선순위

우선순위 드라이버 조건 특징
1 Redis REDIS_SESSION_URL 상수 + Redis 익스텐션 + 연결 성공 다중 서버 공유 가능, 원자적 연산
2 APCu apcu_fetch 함수 존재 + apc.enabled = On PHP-FPM 프로세스 공유 메모리
3 파일 DX_DATA 상수 + data/cache/ 쓰기 가능 저가형 호스팅 기본, 원자적 쓰기(tmp→rename)
4 none 위 모두 실패 캐시 없이 동작. 기능 저하 없음.


3.2 메서드

메서드 반환 설명
DxCache::set($key, $value, $ttl=300) void 캐시 저장. ttl=0이면 영구 저장.
DxCache::get($key, $default=null) mixed 캐시 조회. 없거나 만료 시 $default 반환.
DxCache::delete($key) void 캐시 삭제.
DxCache::deletePrefix($prefix) void 접두어로 시작하는 캐시 전체 삭제. Redis: SCAN 사용.
DxCache::flush() void 전체 캐시 초기화. Redis: dxc:* 패턴 삭제.
DxCache::getDriver() string 현재 드라이버 반환. 'redis'|'apcu'|'file'|'none'.
 
// 기본 사용
DxCache::set('menu_items', $menuArray, 300);  // 300초 TTL
$menu = DxCache::get('menu_items');  // 없으면 null

// 기본값 지정
$cached = DxCache::get('board_list', false);
if ($cached === false) {
    $cached = $db->rows("SELECT * FROM dx_boards WHERE status=1");
    DxCache::set('board_list', $cached, 600);
}

// 접두어 삭제 (게시판 설정 변경 시)
DxCache::deletePrefix('board_');  // board_ 로 시작하는 캐시 전체 삭제

// 드라이버 확인 (관리자 대시보드)
echo '캐시 드라이버: ' . DxCache::getDriver();  // 'redis', 'apcu', 'file', 'none'


3.3 키 안전화

set/get/delete 모두 내부적으로 키를 안전하게 변환합니다. 영문자, 숫자, 언더스코어, 하이픈 이외의 문자는 모두 언더스코어로 치환됩니다.
 
$safeKey = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $key);

// 예시
DxCache::set('site_md5(example.com)', ...) → 키: 'site_md5_example_com_'
// 실제로는 md5()를 이용하여 의미 있는 키를 생성하는 것이 권장됨
DxCache::set('site_' . md5('example.com'), $data, 300);


3.4 파일 캐시 원자적 쓰기

파일 캐시는 동시 쓰기 충돌 방지를 위해 임시 파일에 먼저 쓰고 rename()으로 교체합니다. 이 방식은 OS 레벨에서 원자적이므로 반쯤 쓰인 파일을 읽는 상황이 발생하지 않습니다.
 
// 원자적 쓰기 패턴 (DxCache 내부)
$tmp = $file . '.tmp.' . getmypid();
if (@file_put_contents($tmp, $data) !== false) {
    @rename($tmp, $file);  // OS 레벨 원자적 교체
}


4. Secure — 보안 통합 클래스

Secure 싱글턴. 세션·CSRF·WAF·Rate Limit·bcrypt·파일 업로드 검증

v5.2.2에서 WAF, Rate Limit, Bot 탐지, IP 차단이 추가된 통합 보안 클래스입니다. 모든 하위 호환 메서드를 유지하므로 기존 코드를 수정하지 않아도 됩니다.


4.1 세션 관련 메서드

메서드 반환 설명
initSession($isHttps) void 세션 옵션 설정. 쿠키 lifetime=2시간. SameSite=Lax. PHP 5.6+.
startSession() void 세션 시작. 이미 시작된 경우 스킵. AJAX 요청은 ID 재생성 스킵.
getSessionKey() string 세션 키 이름 반환. 기본: 'dx_user'.

v5.2.2 세션 개선
• AJAX/fetch 요청 시 세션 ID 재생성 스킵: Set-Cookie가 적용되기 전 reload 시 세션 소멸 버그 수정
• 세션 파일을 data/sessions/에 저장: 공유호스팅에서 다른 사이트의 GC 간섭 방지
• Redis 세션 핸들러: REDIS_SESSION_URL 정의 시 자동 활성화
• cookie_lifetime = 7200: 브라우저를 닫아도 2시간 동안 세션 유지


4.2 보안 헤더

sendSecurityHeaders()가 발행하는 HTTP 헤더 목록입니다.
 
헤더 역할
X-Frame-Options SAMEORIGIN 클릭재킹(Clickjacking) 방어
X-Content-Type-Options nosniff MIME 스니핑 방어
X-XSS-Protection 1; mode=block 구형 브라우저 XSS 필터 활성화
Referrer-Policy strict-origin-when-cross-origin Referer 정보 제한
Permissions-Policy camera=(), microphone=(), geolocation=() 카메라·마이크·위치 접근 차단
Content-Security-Policy script-src unsafe-inline https: ... XSS·인젝션 방어 (CDN 허용)
Strict-Transport-Security max-age=31536000 (HTTPS만) HTTPS 강제 (HSTS)
X-Powered-By (제거) PHP 버전 노출 방지


4.3 CSRF 보호

메서드 반환 설명
csrfToken() string CSRF 토큰 반환. 없으면 생성. 있으면 만료 시간만 갱신(토큰 유지).
csrfField() string <input type="hidden" name="_csrf" ...> HTML 반환.
csrfCheck() void 토큰 검증. 실패 시: AJAX→JSON 403, 일반→세션만료 UI 출력.

v6.2.0에서 토큰 재생성을 제거했습니다. 연속 요청에서 이전 토큰이 무효화되는 문제를 해결합니다. 토큰 TTL은 3시간(CSRF_TTL=10800)이며, 활동 감지 시 자동 갱신됩니다.
 
// CSRF 토큰 발급 흐름
// 1. 세션에 토큰 없음 → 신규 64자리 hex 생성
// 2. 세션에 토큰 있음 → 만료 시간만 +3시간 갱신 (토큰값 유지)
// 3. POST에서 $_SESSION[dx_csrf]['token'] 문자열과 타이밍 안전 비교

// 폼 + AJAX 동시 지원
echo dx_csrf_field();  // <input type="hidden" name="_csrf" value="...">

// AJAX: X-CSRF-Token 헤더도 허용
fetch('/api/like', {
    headers: { 'X-CSRF-Token': '<?php echo dx_csrf_token(); ?>' }
});

// POST 처리 시 (검증 실패 → 세션만료 UI 출력 후 exit)
dx_csrf_check();


4.4 WAF (Web Application Firewall)

wafCheck()는 GET 파라미터와 POST 파라미터(content, body 등 본문 필드 제외)를 JSON으로 직렬화하여 4가지 공격 패턴을 검사합니다. 탐지 시 IP를 30분 차단하고 data/security.log에 기록합니다.
 
공격 유형 탐지 패턴 예시
SQL Injection union all select, information_schema, sleep(), benchmark() OR 1=1, UNION SELECT * FROM
XSS <script>, blocked: <script>alert(1)</script>
LFI ../../../, /etc/passwd, php:// ../../../etc/passwd
Command Inj. || ls, && cat, ; whoami ; cat /etc/shadow

WAF 제외 필드: content, body, description, comment, message 등 에디터 입력 필드는 오탐 방지를 위해 WAF 검사에서 제외됩니다.
이 필드들은 에디터 플러그인의 DOMPurify 등으로 클라이언트 측에서도 정제합니다


4.5 Rate Limiting

메서드 반환 설명
rateLimit($key, $limit, $window, $ipLimit) bool 한도 초과 시 false 반환. Redis: 원자적 증가. 파일: LOCK_EX.
 
// 로그인 시도 제한 (30초 내 10회)
if (!Secure::getInstance()->rateLimit('login', 10, 30)) {
    dx_json(array('error'=>'요청이 너무 많습니다. 잠시 후 시도하세요.'), 429);
}

// API 엔드포인트 제한 (60초 내 100회)
if (!Secure::getInstance()->rateLimit('api_comment', 100, 60)) {
    dx_json(array('error'=>'API 한도 초과'), 429);
}

// Redis 없을 경우: data/cache/rl_{md5(key+ip)}.tmp 파일로 폴백


4.6 암호화•해시 정적 메서드

메서드 반환 설명
Secure::randomBytes($length) string PHP 7.0+ random_bytes, OpenSSL, mt_rand 순 폴백.
Secure::randomHex($length=32) string 16진수 난수 문자열. bin2hex(randomBytes) 기반.
Secure::hashEquals($known, $user) bool 타이밍 안전 비교. hash_equals 폴백. 배열이면 token 키 추출.
Secure::bcryptHash($password) string bcrypt 해시. password_hash → crypt 폴백.
Secure::bcryptVerify($pw, $hash) bool bcrypt 검증. password_verify → crypt 폴백.
Secure::sanitize($input, $type) mixed int·float·slug·filename·url·email·html·text 7가지 정제 타입.
Secure::esc($str) string htmlspecialchars ENT_QUOTES UTF-8.
Secure::safeUrl($url) string blocked:·data: 등 위험 프로토콜 차단. # 반환.


4.7 파일 업로드 검증

validateUpload()는 파일 크기, MIME 타입(finfo 우선), 확장자 블랙리스트, 이미지 매직바이트를 4단계로 검증합니다.
 
$result = Secure::validateUpload($_FILES['file']['tmp_name'], $_FILES['file']['name'], 10*1024*1024);
if (!$result['ok']) {
    dx_json(array('error' => $result['error']), 400);
}
// $result: ['ok'=>true, 'mime'=>'image/jpeg', 'size'=>204800, 'ext'=>'jpg']

// 허용 MIME: image/jpeg•png•gif•webp•bmp, application/pdf,
//           docx•xlsx, application/zip, text/plain
// 차단 확장자: php•phtml•phar•asp•aspx•jsp•exe•dll•bat•sh•py•rb 등
// 이미지 매직바이트: JPEG=FFD8FF, PNG=89PNG, GIF=GIF87a/89a, WEBP=RIFF


5. HookManager — 훅 시스템

HookManager 싱글턴. Action·Filter 훅 등록/실행/제거. 우선순위 정렬

Action 훅(dx_add_hook)과 Filter 훅(dx_add_filter)을 관리합니다. 실제로는 동일한 내부 배열에 저장되며 실행 방식만 다릅니다.


5.1 클래스 메서드

메서드 반환 설명
add($name, $callback, $priority=10) void 훅 등록. 등록 즉시 우선순위 정렬.
run($name, $args) void Action 훅 실행. 등록된 콜백들을 priority 순으로 호출.
filter($name, $value, $args) mixed Filter 훅 실행. 값을 콜백 체인에 통과시켜 반환.
remove($name, $callback=null) void 훅 제거. null이면 해당 이름의 전체 훅 제거.
has($name, $callback=null) bool 훅 등록 여부 확인.
count($name) int 특정 훅에 등록된 콜백 수.
getExecuted() array 실행된 훅 이름 목록 (디버그).
getAll() array 등록된 모든 훅 이름 목록 (디버그).


5.2 전역 헬퍼 함수

// Action 훅 (값 반환 없음, 부수효과 목적)
dx_add_hook('dx_bottom', function($ctx) {
    echo '<script src="/my.js"></script>';
}, 10);
dx_run_hook('dx_bottom', $context);

// Filter 훅 (값을 받아 가공 후 반환)
dx_add_filter('dx_post_content', function($content, $args) {
    return str_replace('금칙어', '***', $content);
}, 10);
$content = dx_apply_filter('dx_post_content', $rawContent, array('post_id'=>$id));

// 훅 제거
dx_remove_hook('dx_bottom', 'my_callback_function');  // 특정 콜백만 제거
dx_remove_hook('dx_bottom');                           // 전체 제거

// 훅 존재 확인
if (dx_has_hook('dx_editor_render')) {
    dx_run_hook('dx_editor_render', $args);
}


5.3 표준 훅 포인트 함수

테마 layout/main.php에서 호출하는 세 함수가 내부적으로 타입•슬러그별 훅을 자동 발화합니다.
 
// dx_hook_top($context) 내부 발화 순서
dx_run_hook('dx_top', $context);           // 모든 페이지
dx_run_hook('dx_board_top', $context);     // type='board'인 경우
dx_run_hook('dx_page_notice_top', $context); // slug='notice'인 경우

// dx_hook_middle, dx_hook_bottom도 동일 패턴

// 실전: 특정 게시판에만 배너 삽입
dx_add_hook('dx_board_top', function($ctx) {
    if (isset($ctx['slug']) && $ctx['slug'] === 'notice') {
        echo '<div class="notice-banner">공지사항 배너</div>';
    }
}, 10);


6. PluginRegistry — 플러그인 등록소

PluginRegistry 싱글턴. 에디터·결제·CAPTCHA·SMS 등 교체 가능 기능 모듈 관리


6.1 지원 플러그인 타입

타입 키 레이블 설정 키 설명
editor 에디터 active_editor TinyMCE, CKEditor, Jodit 등 에디터 교체
payment 결제 모듈 active_payment KG이니시스, 토스페이 등 PG사 교체
captcha CAPTCHA active_captcha reCAPTCHA, hCaptcha 등 스팸 방지
sms SMS 발송 active_sms 알리고, NCP, 솔라피 등 SMS 서비스
social_login 소셜 로그인 active_social_login 카카오, 네이버, 구글 로그인
socket 실시간 소켓 active_socket WebSocket 기반 실시간 기능
커스텀 타입 자유 지정 active_{타입} 개발자가 임의 타입 추가 가능


6.2 전역 헬퍼 함수

메서드 반환 설명
dx_register_plugin($info) bool 플러그인 등록. id·type·name 필수.
dx_active_plugin($type) string 현재 활성 플러그인 ID 반환.
dx_active_plugin_info($type) array|null 현재 활성 플러그인 정보 배열 반환.
dx_render_editor($name, $value, $opts) void 에디터 HTML 출력. 미등록 시 textarea 폴백.
dx_render_editor_textarea(...) void textarea 폴백 출력 (내부 헬퍼).
dx_request_payment($paymentData) void 결제 요청. dx_payment_request 훅 발화.
dx_editor_use_comment() bool 활성 에디터가 댓글 에디터 사용하는지 확인.


6.3 플러그인 개발 패턴

// plugins/my-editor/plugin.php
if (!defined('DX_CMS')) exit;

// 1단계: 등록
dx_register_plugin(array(
    'id'          => 'my-editor',
    'type'        => 'editor',
    'name'        => 'My Custom Editor',
    'version'     => '1.0.0',
    'description' => '마크다운 기반 에디터',
    'author'      => '홍길동',
    'priority'    => 10,
    'settings'    => array(
        'height' => array('label'=>'높이(px)', 'type'=>'number'),
    ),
));

// 2단계: 에디터 렌더링 훅 구현
dx_add_hook('dx_editor_render', function($args) {
    if ($args['editor'] !== 'my-editor') return;  // 다른 에디터는 무시
    $name  = htmlspecialchars($args['name'], ENT_QUOTES);
    $value = htmlspecialchars($args['value'], ENT_QUOTES, 'UTF-8');
    echo '<textarea id="my-editor-' . $name . '" name="' . $name . '">' . $value . '</textarea>';
    echo '<script src="/plugins/my-editor/editor.js"></script>';
}, 10);

// 3단계: 게시판 글쓰기 스킨에서 사용
dx_render_editor('content', $existingContent, array('height'=>400));


7. Auth — 인증 시스템

Auth 싱글턴. 로그인·로그아웃·회원가입·Remember Me·세션 토큰 검증

생성자에서 자동으로 세션 또는 Remember Me 쿠키를 읽어 로그인 상태를 복원합니다. 직접 세션을 읽지 않고 Auth::getInstance()를 통해서만 인증 상태를 확인합니다.


7.1 주요 메서드

메서드 반환 설명
isLoggedIn() bool 로그인 여부. 세션 토큰 검증 통과 시 true.
isAdmin() bool 관리자 여부. role='admin' 확인.
user() array|null 현재 로그인 사용자 전체 배열. 비밀번호 필드 제외.
get($field, $default=null) mixed 현재 사용자의 특정 필드 반환.
login($loginId, $password) array 로그인 처리. success·message 배열 반환.
loginById($memberId) bool ID로 직접 로그인 (소셜 로그인용).
logout() void 세션 삭제 + Remember Me 쿠키 삭제 + DB 토큰 삭제.
register($data) array 회원가입. 컬럼 화이트리스트 처리. 자동 로그인 세션 생성.
hashPassword($password) string bcrypt 해시. password_hash → crypt 폴백.
verifyPassword($password, $hash) bool bcrypt 검증. password_verify → crypt 폴백.


7.2 전역 헬퍼 함수 (Auth.php 하단 정의)

메서드 반환 설명
dx_user() array|null Auth::getInstance()->user() 단축.
dx_is_login() bool Auth::getInstance()->isLoggedIn() 단축.
dx_is_admin() bool Auth::getInstance()->isAdmin() 단축.
 
// 로그인 상태 확인
if (!dx_is_login()) {
    dx_redirect(dx_base_url('auth/login'));
}

// 관리자 확인
if (!dx_is_admin()) dx_error('관리자만 접근 가능합니다.', 403);

// 사용자 정보 접근
$user = dx_user();
echo dx_esc($user['name']) . '님 환영합니다.';

// 특정 필드만
$userId = Auth::getInstance()->get('id', 0);
$role   = Auth::getInstance()->get('role', 'guest');

// 로그인 처리
if (dx_method('POST')) {
    dx_csrf_check();
    $result = Auth::getInstance()->login(dx_post('id'), dx_post('pw'));
    if ($result['success']) {
        dx_redirect(dx_base_url());
    } else {
        dx_set_flash($result['message'], 'error');
        dx_redirect(dx_base_url('auth/login'));
    }
}


7.3 세션 토큰 보안

세션 데이터에는 사용자 ID와 HMAC-SHA256 토큰만 저장합니다. 토큰은 user.id + user.join_date(변경 불가) + secret_key 조합으로 생성됩니다. UA나 IP에 의존하지 않아 모바일 네트워크 변경, 다중 탭에서도 세션이 유지됩니다.
 
// $_SESSION 저장 구조
// $_SESSION['dx_user'] = array(
//     'id'    => 42,
//     'token' => hash_hmac('sha256', '42|2026-01-01 00:00:00', 'secret_key...'),
// );

// 토큰 검증 (loadSession() 내부)
$sessionData = $_SESSION[$this->sessionKey()];
$userId = $sessionData['id'];
$user = $db->find('members', array('id'=>$userId, 'status'=>1));
// 1. DB에서 실제 사용자 존재 확인
// 2. 세션 토큰 vs 서버 재생성 토큰 비교
// → 다르면 강제 로그아웃 (세션 하이재킹 방어)


7.4 Remember Me (자동 로그인)

로그인 성공 시 dx_remember 쿠키(userId:token, 24시간)를 발급합니다. 세션 만료 후 다음 요청 시 tryRememberMe()가 자동 실행되어 세션을 복구합니다. 토큰은 매 접근마다 롤링 갱신됩니다.
 
// dx_remember 쿠키 구조
// '{userId}:{64자리 랜덤 hex}'
// 예: '42:a3f8c1d2e4...(64자)'

// 토큰 롤링 갱신 (매 자동 로그인마다)
// 1. 새 토큰 생성 → DB 업데이트
// 2. 새 쿠키 발급 (기존 토큰 무효화)
// → 쿠키 탈취 후 재사용 공격(Replay Attack) 방어

// 컬럼 존재 확인 후 발급 (구버전 DB 호환)
// DxCache로 SHOW COLUMNS 결과 캐싱 (매 요청 DB 쿼리 방지)


8. DxSite — 멀티사이트 관리자

DxSite 싱글턴. 같은 DB로 여러 도메인 운영. 도메인별 테마·메뉴·설정 분리

dx_sites 테이블이 없거나 요청 도메인이 미등록인 경우 dx_settings 기본값을 그대로 사용합니다. 멀티사이트를 사용하지 않는다면 이 클래스의 존재를 의식할 필요가 없습니다.


8.1 주요 메서드

메서드 반환 설명
current() array|null 현재 사이트 DB 행 반환. 미등록이면 null.
isMultiSite() bool 멀티사이트로 등록된 도메인인지 여부.
menuGroup() string 현재 사이트의 메뉴 그룹 반환.
theme() string 현재 사이트의 테마명 반환.
getDomain() string 현재 요청 도메인 반환 (포트 제거).
DxSite::all() array 전체 사이트 목록 반환 (관리자용, 정적).


8.2 전역 헬퍼 함수

메서드 반환 설명
dx_menu_group() string DxSite::getInstance()->menuGroup() 단축.
dx_theme() string DxSite::getInstance()->theme() 단축.


8.3 설정 오버라이드 동작

등록된 도메인의 사이트 설정은 dx_settings 전역 설정보다 우선합니다. DxCache로 캐싱되어 성능 영향이 없습니다.
 
// 오버라이드되는 설정 키
// site_name, site_description, site_url, theme,
// language, timezone, footer_text, menu_group

// extra_config (JSON 컬럼)으로 추가 설정 오버라이드 가능
// {"custom_key": "custom_value", "email_from": "shop@example.com"}

// DxCache TTL: 300초. 관리자에서 변경 시 캐시 삭제 필요
DxCache::deletePrefix('site_');  // 전체 사이트 캐시 삭제


9. DxTheme — 테마/스킨 해석기

DxTheme 싱글턴. 현재 테마→default 테마→null 순의 2단계 폴백 체인

테마 파일 경로를 직접 조합하는 대신 이 클래스를 통해 해석합니다. 현재 테마에 파일이 없으면 자동으로 default 테마를 사용하므로, 커스텀 테마는 변경하고 싶은 파일만 포함하면 됩니다.


9.1 테마 디렉토리 구조

themes/
  default/              ← 기본 테마 (항상 존재, 최종 폴백)
    theme.json          ← 테마 메타정보
    layout/main.php     ← 전체 레이아웃
    board/list.php      ← 게시판 목록 스킨
    board/view.php      ← 게시판 상세 스킨
    board/write.php     ← 게시판 작성 스킨
    page/404.php        ← 404 페이지
    parts/pagination.php← 페이지네이션 파셜
    board_latest/list.php← 홈 최신글 스킨

  my-theme/             ← 커스텀 테마 (없는 파일은 default에서 자동 폴백)
    layout/main.php     ← 오버라이드
    board/list.php      ← 오버라이드


9.2 주요 메서드

메서드 반환 설명
resolve($relPath) string|null 테마 파일 절대경로 반환. 현재테마→default→null 폴백.
resolveBoardSkin($skin, $action) string|null 게시판 스킨 파일 해석. 스킨명/액션→액션 4단계 폴백.
resolvePart($name) string|null 파셜 파일 해석. parts/{name}.php 순으로 탐색.
getTheme() string 현재 테마명 반환.
 
// resolve: 현재 테마 → default 테마 폴백
$layoutFile = DxTheme::getInstance()->resolve('layout/main.php');
// my-theme/layout/main.php 없으면 → default/layout/main.php

// resolveBoardSkin: 4단계 폴백
$skinFile = DxTheme::getInstance()->resolveBoardSkin('gallery', 'list');
// 1. my-theme/board/gallery/list.php
// 2. my-theme/board/list.php
// 3. default/board/gallery/list.php
// 4. default/board/list.php

// resolvePart: 파셜 파일
$paginationFile = DxTheme::getInstance()->resolvePart('pagination');
// my-theme/parts/pagination.php → default/parts/pagination.php

// 테마 파일 포함 패턴 (Dispatcher에서)
if ($file = DxTheme::getInstance()->resolve('page/about.php')) {
    include $file;
} else {
    dx_error('페이지를 찾을 수 없습니다.', 404);
}


10. DxContainer — 경량 DI 컨테이너

DxContainer 싱글턴. 라라벨 Service Container 철학을 PHP 5.6으로 구현

기존 getInstance() 싱글턴은 100% 유지하면서, 플러그인이나 커스텀 서비스를 추가로 등록•주입할 수 있습니다. 교체•목업•테스트가 필요한 서비스에 적합합니다.


10.1 주요 메서드

메서드 반환 설명
bind($abstract, $factory) void 팩토리 함수 바인딩. make() 호출 시마다 새 인스턴스 생성.
singleton($abstract, $factory) void 싱글턴 바인딩. 최초 1회만 생성 후 캐싱.
instance($abstract, $obj) void 이미 생성된 인스턴스를 직접 등록.
make($abstract) mixed 바인딩된 서비스 반환. 별칭도 지원.
alias($alias, $abstract) void 별칭 등록. dx_make('db')로 Database 접근 등.
call($target, $args) mixed 클래스@메서드 또는 callable 실행. 의존성 자동 주입.
has($abstract) bool 바인딩 존재 여부.
forget($abstract) void 바인딩 제거.


10.2 전역 헬퍼 함수

메서드 반환 설명
dx_app() DxContainer 컨테이너 인스턴스 반환.
dx_make($abstract) mixed dx_app()->make($abstract) 단축.
 
// 핵심 서비스 자동 등록 (DxContainer::registerCoreServices())
dx_app()->singleton('db', function() { return Database::getInstance(); });
dx_app()->singleton('cache', function() { return DxCache::class; });
dx_app()->singleton('auth', function() { return Auth::getInstance(); });

// 플러그인에서 커스텀 서비스 등록
dx_app()->singleton('sms', function() {
    return new AlimtalkSMS(dx_config('alimtalk_key'));
});

// 꺼내 쓰기
$sms = dx_make('sms');
$sms->send('010-1234-5678', '인증번호: 123456');

// 별칭 등록
dx_app()->alias('db', 'database');
$db = dx_make('db');    // Database::getInstance()
$db = dx_make('database');  // 동일

// 컨트롤러 자동 실행
dx_app()->call('BoardController@index', array('board_id'=>1));


11. DxPoint — 포인트/경험치/레벨 엔진

DxPoint 정적 클래스. 포인트·경험치 지급, 레벨 계산, DB 기반 레벨 설정

new나 getInstance() 없이 정적 메서드만 사용합니다. 레벨 설정은 dx_level_config 테이블에서 로드하며, 테이블이 없으면 하드코딩된 기본값(15단계)으로 폴백합니다.


11.1 기본 포인트•경험치 규칙

이벤트 포인트 경험치 훅 이름
회원가입 (signup) +10 +20 dx_after_register
로그인 (login) +1 +2 dx_after_login
게시글 작성 (write) +5 +10 dx_after_write
댓글 작성 (comment) +2 +5 dx_after_comment
좋아요 받음 (like_recv) +1 +2 dx_after_like
스크랩 받음 (scrap_recv) +1 - (직접 호출)

규칙은 DB dx_point_config 테이블에서 관리자가 변경 가능합니다. 테이블이 없으면 위 하드코딩 기본값을 사용합니다.


11.2 주요 정적 메서드

메서드 반환 설명
DxPoint::add($userId, $amount, $type, $desc) void 포인트 지급/차감. dx_members.point 업데이트 + dx_point_log INSERT.
DxPoint::addExp($userId, $amount, $type) void 경험치 지급. dx_members.exp 업데이트 + 레벨업 체크.
DxPoint::getLevel($exp) int 경험치로 레벨 계산. 1~15레벨.
DxPoint::getLevelName($level) string 레벨명 반환. 예: '새싹', '고수', '마스터'.
DxPoint::getThresholds() array 레벨별 필요 경험치 배열. DB 우선, 없으면 기본값.
DxPoint::getLevelNames() array 레벨별 이름 배열. DB 우선, 없으면 기본값.
DxPoint::getPointRule($type) int 이벤트 타입별 포인트 규칙 반환.
DxPoint::getExpRule($type) int 이벤트 타입별 경험치 규칙 반환.
 
// 포인트 지급 (직접 호출)
DxPoint::add(42, 100, 'manual', '이벤트 보너스 지급');
DxPoint::add(42, -50, 'penalty', '규정 위반 차감');

// 경험치 지급 + 레벨업 자동 처리
DxPoint::addExp(42, 20, 'signup');

// 레벨 계산
$exp = Auth::getInstance()->get('exp', 0);
$level = DxPoint::getLevel($exp);
$levelName = DxPoint::getLevelName($level);  // '활동'

// 훅을 통한 자동 지급 (index.php에서 _dx_register_point_hooks() 호출)
dx_run_hook('dx_after_write', array('user_id'=>$userId, 'post_id'=>$postId));
// → 내부: DxPoint::add($userId, 5, 'write') + DxPoint::addExp($userId, 10, 'write')


11.3 기본 레벨 체계 (15단계)

레벨 이름 필요 경험치
1 새싹 0
2 초보 50
3 일반 150
4 활동 350
5 열정 700
6 고수 1,200
7 달인 2,000
8 전문가 3,200
9 명인 5,000
10 마스터 8,000
11 그랜드 12,000
12 전설 18,000
13 영웅 26,000
14 신화 36,000
15 전설왕 50,000


12. 실전 활용 패턴 모음


12.1 게시판 핸들러 완성 패턴

// boards/handler.php 또는 API 핸들러
if (!defined('DX_CMS')) exit;

$db   = Database::getInstance();
$auth = Auth::getInstance();

// 인증 확인
if (!$auth->isLoggedIn()) {
    dx_redirect(dx_base_url('auth/login'));
}

// POST 처리
if (dx_method('POST')) {
    // Rate Limit: 10초 내 5회 이하
    if (!Secure::getInstance()->rateLimit('board_write', 5, 10)) {
        dx_json(array('error'=>'요청이 너무 많습니다.'), 429);
    }
    dx_csrf_check();

    $title   = dx_post('title');
    $content = dx_post('content');

    // BIGINT 안전 INSERT
    $postId = $db->insertWithMicrotimeId('posts', array(
        'board_id'   => $boardId,
        'user_id'    => $auth->get('id'),
        'title'      => $title,
        'content'    => $content,
        'created_at' => date('Y-m-d H:i:s'),
    ));

    // 포인트 + 경험치 지급 (훅으로 자동)
    dx_run_hook('dx_after_write', array(
        'user_id' => $auth->get('id'),
        'post_id' => $postId,
    ));

    // 캐시 삭제
    DxCache::deletePrefix('board_' . $boardId . '_');

    dx_set_flash('게시글이 등록되었습니다.', 'success');
    dx_redirect(dx_base_url($boardKey . '/view/' . $postId));
}


12.2 캐시 적용 패턴

// 게시판 목록 캐싱 (TTL 60초)
$cacheKey = 'board_' . $boardId . '_page_' . $page;
$posts = DxCache::get($cacheKey, false);

if ($posts === false) {
    $posts = $db->rows(
        "SELECT * FROM `dx_posts` WHERE board_id=? ORDER BY id DESC LIMIT 15",
        array($boardId)
    );
    DxCache::set($cacheKey, $posts, 60);
}

// 캐시 드라이버에 따른 성능 차이
// Redis:  < 1ms
// APCu:   < 1ms
// 파일:   ~2~5ms (SSD 기준)
// none:   DB 쿼리 실행 (~10ms+)


12.3 플러그인 + 훅 조합 패턴

// 결제 플러그인 사용 패턴
$paymentId = dx_active_plugin('payment');
if (!$paymentId) {
    dx_error('결제 모듈이 설정되지 않았습니다.');
}

dx_request_payment(array(
    'order_id'     => 'ORD-' . date('YmdHis'),
    'amount'       => 29000,
    'product_name' => 'DXCMS Pro 라이선스',
    'buyer_name'   => $auth->get('name'),
    'buyer_email'  => $auth->get('email'),
    'return_url'   => dx_base_url('payment/result'),
));

// 에디터 렌더링 패턴
dx_render_editor('content', $existingContent, array(
    'height' => 500,
    'board'  => $board,    // 게시판별 에디터 오버라이드 지원
));


12.4 멀티사이트 활용 패턴

// extend/top/01_multisite.php
if (!defined('DX_CMS')) exit;

$site = DxSite::getInstance();

if ($site->isMultiSite()) {
    // 현재 사이트의 커스텀 설정 적용
    $menuGroup = $site->menuGroup();  // 사이트별 메뉴 그룹
    $theme     = $site->theme();      // 사이트별 테마

    // 추가 쿠키 설정 등
    dx_set_config('analytics_id', $site->current()['extra_config']);
}


공통 클래스 핵심 원칙 요약

1. Database: $db->row/rows/value/query. insertWithMicrotimeId()로 BIGINT ID 안전 생성.
2. DxCache: 정적 클래스. set/get/delete/deletePrefix. 드라이버 자동 선택 (Redis→APCu→파일→none).
3. Secure: 싱글턴. csrfCheck()는 POST 처음에. validateUpload()로 파일 4단계 검증.
4. HookManager: dx_add_hook/dx_run_hook. Filter는 반드시 return. priority 낮을수록 먼저.
5. PluginRegistry: dx_register_plugin으로 등록. dx_active_plugin으로 활성 ID 조회.
6. Auth: dx_is_login/dx_is_admin/dx_user. 세션 직접 접근 금지.
7. DxSite: 멀티사이트 미사용 시 의식 불필요. $dx_config 자동 오버라이드.
8. DxTheme: resolve/resolveBoardSkin으로 경로 해석. 직접 경로 조합 금지.
9. DxContainer: dx_app()->singleton/bind/make. 플러그인 커스텀 서비스 등록에 활용.
10. DxPoint: 정적 클래스. add/addExp. 훅으로 자동 지급. 레벨은 DB 설정 우선.

댓글0

로그인 후 댓글을 작성할 수 있습니다.
3.9 공통 함수 / 유틸 재사용 방식 2026.04.21 3.9 공통 함수 / 유틸 공통 클래스 구조 2026.04.21 3.9 공통 함수 / 유틸 전역 함수 구조 2026.04.21
30
전체 회원
269
전체 게시글
144
전체 댓글
181
오늘 방문
28,530
전체 방문
1
현재 접속
인기글 7일 이내
최신글
최신댓글
목록