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 설정 우선.