1. 전역 함수 라이브러리 개요
core/functions.php는 DXCMS 전체에서 가장 먼저 로드되는 공통 함수 라이브러리입니다. 이 파일이 다른 클래스들보다 먼저 실행되어야 하기 때문에, Database, HookManager, Auth 등 클래스를 직접 인스턴스화하지 않고 순수 함수 정의만 담습니다. 대신 클래스가 로드된 후 호출되면 클래스 기능을 위임(delegate)합니다.
1.1 로드 순서와 역할
// index.php STEP 1에서 require_once 순서
require_once DX_CORE . '/functions.php'; // ← 1번째: 전역 함수 정의
require_once DX_CORE . '/DxCache.php'; // 2번째
require_once DX_CORE . '/hook/HookManager.php'; // 3번째
require_once DX_CORE . '/PluginRegistry.php'; // 4번째
// ...
// functions.php가 먼저 로드되어야 하는 이유:
// • dx_config(), dx_log() 등이 다른 클래스 __construct에서 사용됨
// • dx_random_bytes()가 Secure.php 세션 초기화에 필요
// • dx_is_https()가 세션 쿠키 Secure 플래그에 필요
1.2 함수 그룹 전체 지도
| 그룹 |
함수 수 |
주요 함수 |
특이사항 |
| 보안 난수 |
2 |
dx_random_bytes, dx_random_hex |
PHP 5.6~8.x 폴백 체인 |
| 경로 유틸 |
2 |
dx_realpath, dx_path_inside |
윈도우 백슬래시 처리 |
| HTTPS 감지 |
1 |
dx_is_https |
Cloudflare·IIS·리버스프록시 지원 |
| URL 생성 |
5 |
dx_base_url, dx_static_url, dx_request_uri, dx_current_url, dx_redirect |
이중슬래시 제거 자동화 |
| 요청 헬퍼 |
5 |
dx_get, dx_post, dx_request, dx_method, dx_cast |
타입 캐스팅 내장 |
| IP 주소 |
1 |
dx_ip |
CF·프록시 우선순위 체인 |
| UA / 디바이스 |
4 |
dx_ua, dx_device, dx_os, dx_browser |
mobile·tablet·pc 구분 |
| 설정 |
3 |
dx_config, dx_set_config, dx_config_set |
$dx_config 전역 배열 래퍼 |
| 보안 |
4 |
dx_esc, dx_safe_url, dx_csrf_token, dx_csrf_field, dx_csrf_check |
Secure.php 위임 |
| 응답 |
2 |
dx_error, dx_json |
HTTP 상태코드 + exit |
| 플래시 메시지 |
3 |
dx_set_flash, dx_flash, dx_get_flash |
세션 기반 1회성 메시지 |
| 문자열 |
2 |
dx_substr, dx_mb_substr |
mb_* 폴백 |
| 날짜 |
1 |
dx_date |
타임스탬프·문자열 양쪽 지원 |
| 업로드 |
2 |
dx_upload_url, dx_upload_exists |
data/uploads/ 기준 |
| 배열·기타 |
3 |
dx_array_column, dx_time_ago, dx_filesize |
PHP 5.4 폴백 |
| 페이지네이션 |
1 |
dx_pagination |
HTML 문자열 반환 |
| 로그 |
1 |
dx_log |
info/debug는 기록 안 함 |
| AJAX |
1 |
dx_is_ajax |
X-Requested-With 감지 |
| 게시판 헬퍼 |
2 |
dx_board_posts, dx_board_latest |
홈 제작 전용 |
| 테마 자산 |
2 |
dx_head_assets, dx_font_css |
Pretendard + Space Grotesk |
2. 보안 난수 함수
암호화에 사용할 수 있는 예측 불가능한 난수 바이트를 생성합니다. PHP 버전과 서버 환경에 따라 자동으로 최적 방법을 선택하는 폴백 체인이 핵심입니다.
2.1 dx_random_bytes($length)
| dx_random_bytes(int $length) |
string |
암호화 안전 난수 바이트 생성 |
| 매개변수 |
타입 |
기본값 |
설명 |
| $length |
int |
(필수) |
생성할 바이트 수 |
PHP 버전과 서버 환경에 따라 4단계 폴백 체인을 사용합니다.
// 폴백 체인 (성능 + 보안 최우선순)
// 1단계: PHP 7.0+ 내장 (CSPRNG, 가장 안전)
if (function_exists('random_bytes')) {
return random_bytes($length);
}
// 2단계: PHP 5.x + OpenSSL (CSPRNG 수준)
if (function_exists('openssl_random_pseudo_bytes')) {
$strong = false;
$bytes = openssl_random_pseudo_bytes($length, $strong);
if ($bytes !== false) return $bytes;
}
// 3단계: PHP 5.x + mcrypt (구형 서버)
if (function_exists('mcrypt_create_iv')) {
$bytes = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
if ($bytes !== false && strlen($bytes) === $length) return $bytes;
}
// 4단계: mt_rand 폴백 (보안 취약 — 최후 수단)
$bytes = '';
for ($i = 0; $i < $length; $i++) { $bytes .= chr(mt_rand(0, 255)); }
return $bytes;
2.2 dx_random_hex($length)
| dx_random_hex(int $length = 32) |
string |
지정 길이의 16진수 난수 문자열 반환 |
| 매개변수 |
타입 |
기본값 |
설명 |
| $length |
int |
32 |
반환할 16진수 문자열 길이 |
// 사용 예
$token = dx_random_hex(32); // 32자리 hex 문자열 (예: 'a3f8c1...')
$key = dx_random_hex(64); // 64자리 hex 문자열
// 내부: bin2hex(dx_random_bytes(ceil(length/2)))
// 바이트를 hex로 변환 → 길이가 2배 → ceil(32/2)=16바이트 생성
// 실제 활용: CSRF 토큰, API 키, 세션 ID 등
$csrf = dx_random_hex(32); // CSRF 토큰
$apiKey = dx_random_hex(48); // API 키
3. 경로 유틸리티 함수
파일 경로를 안전하게 처리합니다. Windows(백슬래시)와 Linux(슬래시)를 동시에 지원하며, 경로 탈출(path traversal) 공격을 방어합니다.
3.1 dx_realpath($path)
| dx_realpath(string $path) |
string|false |
실제 절대경로 반환 (백슬래시 정규화) |
PHP 내장 realpath()를 래핑하여 Windows 백슬래시를 슬래시로 정규화합니다. 실제 존재하지 않는 경로는 false를 반환합니다.
// PHP 내장 realpath() + 백슬래시 정규화
$path = str_replace('\\\\', '/', $path);
$real = realpath($path);
return $real ? str_replace('\\\\', '/', $real) : false;
// 예시
$real = dx_realpath('/var/www/html/extend/../top/01_maint.php');
// → '/var/www/html/top/01_maint.php'
// Windows 경로도 동일하게 처리
$real = dx_realpath('C:\\inetpub\\wwwroot\\extend\\top\\file.php');
// → 'C:/inetpub/wwwroot/extend/top/file.php'
3.2 dx_path_inside($path, $base)
| dx_path_inside(string $path, string $base) |
bool |
$path가 $base 디렉토리 내부인지 검증 |
| 매개변수 |
타입 |
기본값 |
설명 |
| $path |
string |
(필수) |
검사할 파일/디렉토리 경로 |
| $base |
string |
(필수) |
기준 베이스 디렉토리 경로 |
경로 탈출(path traversal) 공격 방어를 위해 DxExtend, Dispatcher 등 파일 로더가 실행 전 반드시 호출합니다.
// ① 두 경로 모두 realpath()로 정규화
$path = dx_realpath($path);
$base = dx_realpath($base);
if ($path === false || $base === false) return false;
// ② base 경계 검증
$base = rtrim($base, '/');
// ③ Windows는 대소문자 무시 비교 (NTFS 특성)
if (DIRECTORY_SEPARATOR === '\\\\') {
return stripos($path, $base . '/') === 0 || strtolower($path) === strtolower($base);
}
return strpos($path, $base . '/') === 0 || $path === $base;
// 실제 사용 예 (Dispatcher.php)
$fullPath = DX_PAGES . '/' . ltrim($filePath, '/');
if (!dx_path_inside($fullPath, DX_PAGES)) {
$this->dispatch404(); return;
}
// ../../../etc/passwd 같은 경로 탈출 시도를 원천 차단
4. URL 생성 함수
루트 설치, 서브디렉토리 설치, IIS URL Rewrite 없는 환경, Cloudflare 등 다양한 환경에서 올바른 URL을 생성합니다. 이중 슬래시(//) 제거와 자동 도메인 감지가 핵심입니다.
4.1 내부 헬퍼: _dx_resolve_base()
모든 URL 생성 함수가 공유하는 내부 헬퍼입니다. site_url 설정값과 실제 요청 도메인을 비교하여 불일치 시 자동 감지로 전환합니다. static 변수로 캐싱하여 요청당 1회만 계산합니다.
// static 캐싱으로 요청당 1회만 계산
static $_cached = null;
if ($_cached !== null) return $_cached;
// 1단계: site_url 설정값 읽기
$base = dx_config('site_url', '');
// 2단계: 도메인 불일치 감지
if ($base && isset($_SERVER['HTTP_HOST'])) {
$configHost = strtolower(parse_url($base, PHP_URL_HOST));
$requestHost = strtolower(preg_replace('/:\\d+$/', '', $_SERVER['HTTP_HOST']));
if ($configHost !== $requestHost) $base = ''; // 자동 감지로 전환
}
// 3단계: 자동 감지 (site_url 없거나 불일치 시)
if (!$base) {
$scheme = dx_is_https() ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'];
$dir = dirname($_SERVER['SCRIPT_NAME']); // 서브디렉토리 추출
$base = $scheme . '://' . $host . $dir;
}
// 4단계: 이중 슬래시 제거 후 캐싱
$base = preg_replace_callback('#^(https?://)(.*)$#i', function($m) {
return $m[1] . preg_replace('#/+#', '/', $m[2]);
}, $base);
$_cached = rtrim($base, '/');
4.2 dx_base_url($path)
| dx_base_url(string $path = '') |
string |
내부 절대 URL 생성 |
| 매개변수 |
타입 |
기본값 |
설명 |
| $path |
string |
'' |
'' 또는 '/'이면 루트 URL, 그 외 경로 문자열 |
// 기본 사용
echo dx_base_url(); // https://example.com/
echo dx_base_url('notice'); // https://example.com/notice
echo dx_base_url('auth/login'); // https://example.com/auth/login
// 서브디렉토리 설치 시
echo dx_base_url('notice'); // https://example.com/cms/notice
// URL Rewrite 없는 IIS 환경
// dx_config('url_rewrite', '1') === '0' 인 경우
echo dx_base_url('notice'); // https://example.com/index.php?_url=/notice
// 테마, extend, 플러그인 파일에서 공통으로 사용
$url = dx_base_url($boardKey . '/view/' . $postId);
$redirect = dx_base_url('auth/login') . '?redirect=' . urlencode(dx_current_url());
4.3 dx_static_url($path)
| dx_static_url(string $path = '') |
string |
정적 자산(CSS/JS/이미지) URL 생성 |
dx_base_url과 동일한 베이스를 사용하지만 URL Rewrite 폴백 없이 항상 직접 경로를 반환합니다. CDN 분리가 필요한 경우 이 함수만 오버라이드하면 됩니다.
echo dx_static_url('assets/css/main.css'); // https://example.com/assets/css/main.css
echo dx_static_url('assets/js/app.js'); // https://example.com/assets/js/app.js
// 테마 파일에서
echo '<link rel="stylesheet" href="' . dx_static_url('themes/default/assets/style.css') . '">';
4.4 dx_request_uri()
| dx_request_uri() |
string |
이중 슬래시 정규화된 REQUEST_URI 반환 |
$_SERVER['REQUEST_URI']를 직접 사용하면 //로 시작하는 URI가 들어올 수 있습니다. 이 함수는 이중 슬래시를 정규화하여 반환합니다. Router, Dispatcher 등 모든 URI 처리에서 이 함수를 사용합니다.
// 직접 접근 대신 항상 이 함수 사용
// $_SERVER['REQUEST_URI'] = '//notice//view/42' (비정상 요청)
echo dx_request_uri(); // '/notice/view/42' (정규화됨)
// extend/ 파일에서 현재 경로 확인
$uri = dx_request_uri();
if (strpos($uri, '/admin') === 0) return; // 관리자 경로 건너뜀
4.5 dx_current_url()
| dx_current_url() |
string |
현재 요청의 전체 URL (scheme + host + URI) |
// 현재 URL 전체 반환 (쿼리스트링 포함)
// 현재 요청: GET https://example.com/notice/view/42?page=1
echo dx_current_url(); // 'https://example.com/notice/view/42?page=1'
// 활용: 로그인 후 원래 페이지로 돌아가기
$redirect = dx_base_url('auth/login') . '?redirect=' . urlencode(dx_current_url());
dx_redirect($redirect);
4.6 dx_redirect($url, $code) / dx_redirect_back($fallback)
| dx_redirect(string $url, int $code = 302) |
void (exit) |
리다이렉트 후 exit |
ob_start 버퍼를 모두 비운 후 header()를 시도하고, 실패 시 HTML meta/JS로 폴백합니다. 카페24 등 일부 공유호스팅에서 header()가 실패하는 환경을 대응합니다.
// 기본 302 리다이렉트
dx_redirect(dx_base_url('notice'));
// 301 영구 리다이렉트
dx_redirect(dx_base_url('notice'), 301);
// 이전 페이지로 돌아가기
dx_redirect_back('/'); // Referer 없으면 '/'로
// 내부 동작:
// 1. $GLOBALS['DX_SHUTTING_DOWN'] = true (shutdown 에러 방지)
// 2. ob_end_clean() 반복으로 버퍼 모두 비움
// 3. header('Location: ...') 시도
// 4. HTML meta refresh + JS location.replace 출력 (폴백)
// 5. exit
5. 요청 헬퍼 함수
GET, POST, REQUEST 데이터를 안전하게 읽고 타입 캐스팅합니다. 직접 $_GET/$_POST에 접근하는 대신 이 함수들을 사용하면 타입 안전성과 기본값 처리가 자동으로 이루어집니다.
5.1 dx_get / dx_post / dx_request
| dx_get(string $key, mixed $default = '', string $type = 'string') |
mixed |
$_GET 값을 타입 캐스팅하여 반환 |
| dx_post(string $key, mixed $default = '', string $type = 'string') |
mixed |
$_POST 값을 타입 캐스팅하여 반환 |
| dx_request(string $key, mixed $default = '', string $type = 'string') |
mixed |
$_REQUEST 값을 타입 캐스팅하여 반환 |
| 매개변수 |
타입 |
기본값 |
설명 |
| $key |
string |
(필수) |
읽을 파라미터 키 |
| $default |
mixed |
'' |
키가 없을 때 반환할 기본값 |
| $type |
string |
'string' |
string | int | float | bool | array | bigint |
// 타입별 사용 예
$page = dx_get('page', 1, 'int'); // 없으면 1, int로 변환
$keyword = dx_get('q', ''); // 없으면 빈 문자열
$postId = dx_post('id', 0, 'bigint'); // BIGINT 안전 처리 (32bit 오버플로우 방지)
$active = dx_post('active', false, 'bool');
$tags = dx_post('tags', array(), 'array');
// bigint 타입의 특별 처리
// PHP 32bit에서 (int) 캐스팅 시 2147483647 초과값 오버플로우 → 문자열로 반환
$id = dx_post('post_id', '0', 'bigint'); // '12345678901234' (문자열)
if (ctype_digit($id) && $id !== '0') { /* 유효한 BIGINT */ }
5.2 dx_method($method)
| dx_method(string $method) |
bool |
현재 HTTP 메서드 확인 |
if (dx_method('POST')) {
// POST 요청 처리
dx_csrf_check();
$title = dx_post('title');
}
if (dx_method('DELETE')) {
// DELETE 요청 처리 (REST API)
}
5.3 dx_cast($val, $type) — 내부 타입 캐스터
| 타입 문자열 |
변환 방식 |
특이사항 |
| 'int' |
(int)$val |
정수 변환 |
| 'float' |
(float)$val |
실수 변환 |
| 'bool' |
(bool)$val |
불리언 변환 |
| 'array' |
(array)$val |
배열 변환 |
| 'bigint' |
ctype_digit 검증 후 문자열 반환 |
32bit PHP 오버플로우 방지, 양의 정수만 |
| 'string' |
trim($val) — 문자열+앞뒤공백제거 |
기본 타입, 문자열이 아닌 값은 그대로 반환 |
6. IP 주소 및 User-Agent 함수
6.1 dx_ip()
| dx_ip() |
string |
실제 클라이언트 IP 반환 (Cloudflare·프록시 우선) |
Cloudflare, 리버스 프록시, 로드밸런서 뒤에서 실제 클라이언트 IP를 정확히 가져옵니다. 다음 순서로 검사하여 유효한 IP를 반환합니다.
| 우선순위 |
$_SERVER 키 |
설명 |
| 1 |
HTTP_CF_CONNECTING_IP |
Cloudflare가 추가하는 실제 방문자 IP |
| 2 |
HTTP_X_REAL_IP |
Nginx 리버스 프록시 |
| 3 |
HTTP_X_FORWARDED_FOR |
L4/L7 로드밸런서 (첫 번째 IP 사용) |
| 4 |
HTTP_CLIENT_IP |
일부 프록시 서버 |
| 5 |
REMOTE_ADDR |
직접 연결 IP (폴백) |
$ip = dx_ip(); // '203.0.113.42'
// 내부 로직: filter_var()로 유효성 검증
foreach ($keys as $k) {
if (!empty($_SERVER[$k])) {
$ips = explode(',', $_SERVER[$k]); // X-Forwarded-For: ip1, ip2, ip3
$ip = trim($ips[0]); // 첫 번째 IP만 사용
if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
}
}
return '0.0.0.0'; // 폴백
// 활용: IP 차단, 방문자 로그, 중복 방지
dx_add_hook('dx_top', function() {
$blocked = array('192.168.1.100', '10.0.0.1');
if (in_array(dx_ip(), $blocked)) { http_response_code(403); exit; }
});
6.2 dx_ua() / dx_device() / dx_os() / dx_browser()
| dx_ua() |
string |
User-Agent 문자열 반환 (최대 500자) |
| dx_device() |
string |
디바이스 타입 반환: 'mobile' | 'tablet' | 'pc' |
| dx_os() |
string |
OS 정보 반환: 'Windows 10/11', 'Android 13', 'iOS 17.0' 등 |
| dx_browser() |
string |
브라우저 반환: 'Chrome 120', 'Firefox 121', 'Edge', 'Safari' 등 |
dx_device()는 태블릿 감지를 모바일보다 먼저 수행합니다. Android 태블릿은 UA에 'mobile'이 없는 경우가 많으므로 android(?!.*mobile) 패턴으로 감지합니다.
// 디바이스별 분기 예시
$device = dx_device(); // 'mobile' | 'tablet' | 'pc'
if ($device === 'mobile') {
// 모바일 전용 레이아웃
include DX_THEMES . '/' . $theme . '/layout/mobile.php';
} else {
include DX_THEMES . '/' . $theme . '/layout/main.php';
}
// 방문자 통계에서 활용
$os = dx_os(); // 'Android 13'
$browser = dx_browser(); // 'Chrome 120'
$device = dx_device(); // 'mobile'
// dx_device() 내부: 태블릿 먼저 체크 (순서 중요)
if (preg_match('/ipad|tablet|kindle|playbook|silk|(android(?!.*mobile))/i', $ua))
return 'tablet';
if (preg_match('/mobile|iphone|ipod|android|blackberry|.../i', $ua))
return 'mobile';
return 'pc';
7. 설정 함수
$dx_config 전역 배열을 읽고 쓰는 래퍼 함수입니다. data/config.php가 로드된 후 DB의 dx_settings 테이블 값들이 이 배열에 채워집니다.
7.1 dx_config($key, $default)
| dx_config(string $key, mixed $default = '') |
mixed |
$dx_config 배열에서 설정값 반환 |
| 매개변수 |
타입 |
기본값 |
설명 |
| $key |
string |
(필수) |
설정 키 이름 |
| $default |
mixed |
'' |
키가 없을 때 반환할 기본값 |
// 기본 사용
$theme = dx_config('theme', 'default');
$siteName = dx_config('site_name', 'DXCMS');
$siteUrl = dx_config('site_url', '');
// 주요 설정 키 목록
dx_config('site_name') // 사이트 이름
dx_config('site_url') // 사이트 URL
dx_config('theme') // 현재 테마명
dx_config('url_rewrite') // URL Rewrite 사용 여부 ('1'/'0')
dx_config('secret_key') // 시크릿 키 (64자리 랜덤)
dx_config('editor') // 활성 에디터 플러그인 ID
dx_config('socket_enabled') // 소켓 활성화 여부
// 내부 구현: global $dx_config 배열 직접 접근
function dx_config($key, $default = '') {
global $dx_config;
if (isset($dx_config) && is_array($dx_config) && array_key_exists($key, $dx_config)) {
return $dx_config[$key];
}
return $default;
}
7.2 dx_set_config($key, $value) / dx_config_set($key, $value)
| dx_set_config(string $key, mixed $value) |
void |
$dx_config 배열에 런타임 값 설정 |
두 함수는 동일합니다. dx_config_set은 DxSite.php 등 내부 클래스에서 사용하는 별칭입니다. 런타임에만 반영되며 DB를 변경하지 않습니다.
// 멀티사이트: DxSite가 도메인별 설정을 오버라이드할 때 사용
dx_set_config('theme', 'shop-theme'); // 현재 요청의 테마 변경
dx_set_config('site_name', 'Shop Site'); // 현재 요청의 사이트명 변경
// extend/top/ 파일에서 조건부 설정 변경
if (strpos(dx_request_uri(), '/shop') === 0) {
dx_set_config('theme', 'shop-theme');
}
// 주의: 이 함수는 DB를 변경하지 않음
// DB에 영구 저장하려면 Database::getInstance()를 통해 dx_settings 테이블 직접 수정
8. 보안 함수
XSS 방어, URL 안전 처리, CSRF 토큰 발급•검증을 담당합니다. 보안 핵심 로직은 Secure.php에 위임하며, functions.php의 함수들은 편의 래퍼 역할을 합니다.
8.1 dx_esc($str)
| dx_esc(string $str) |
string |
HTML 특수문자 이스케이프 (XSS 방어) |
htmlspecialchars($str, ENT_QUOTES, 'UTF-8')의 단축 함수입니다. 테마 파일에서 사용자 입력을 출력할 때 반드시 이 함수를 거쳐야 합니다.
// 잘못된 방법 (XSS 취약)
echo $user['name'];
echo $_GET['keyword'];
// 올바른 방법
echo dx_esc($user['name']);
echo dx_esc(dx_get('keyword'));
// HTML 속성에서도 동일하게 사용
echo '<input value="' . dx_esc($value) . '">';
echo '<a href="' . dx_safe_url($link) . '">' . dx_esc($title) . '</a>';
ENT_HTML5는 PHP 5.4+에서만 지원합니다.
일부 저가형 호스팅의 패치 미적용 환경을 위해 ENT_QUOTES를 사용합니다.
PHP 5.6 최소 요구사항이지만 패치 수준이 낮은 환경도 커버합니다.
8.2 dx_safe_url($url)
| dx_safe_url(string $url) |
string |
XSS·프로토콜 인젝션 방어 URL 반환 |
blocked:, blocked:, data: 등 위험 프로토콜을 차단하고, 상대 경로를 절대 URL로 변환합니다.
// 위험 URL 차단
echo dx_safe_url('blocked:alert(1)'); // '#'
echo dx_safe_url('data:text/html,...'); // '#'
echo dx_safe_url('blocked:...'); // '#'
// 상대 경로 → 절대 URL 변환 (서브디렉토리 안전)
echo dx_safe_url('/notice'); // 'https://example.com/notice'
echo dx_safe_url('/notice/view/42'); // 'https://example.com/notice/view/42'
// 테마에서 메뉴 링크 출력 시
foreach ($menus as $menu) {
echo '<a href="' . dx_safe_url($menu['url']) . '">' . dx_esc($menu['name']) . '</a>';
}
8.3 CSRF 토큰 함수
| dx_csrf_token() |
string |
CSRF 토큰 반환 (없으면 생성) |
| dx_csrf_field() |
string |
<input type="hidden" name="_csrf" ...> HTML 반환 |
| dx_csrf_check() |
void |
CSRF 토큰 검증 실패 시 403 exit |
세 함수 모두 Secure::getInstance()에 위임합니다. Secure가 로드되지 않은 환경(설치 화면 등)에서는 세션 기반 폴백을 사용합니다.
// 폼에 CSRF 필드 삽입
<form method="POST" action="<?php echo dx_base_url('auth/register'); ?>">
<?php echo dx_csrf_field(); ?>
<!-- 출력: <input type="hidden" name="_csrf" value="a3f8c1..."> -->
<input type="text" name="name">
<button type="submit">가입</button>
</form>
// POST 처리 시 검증 (검증 실패 → 403 exit)
if (dx_method('POST')) {
dx_csrf_check();
// 이 아래 코드는 CSRF 검증 통과한 요청만 실행
$name = dx_post('name');
}
// AJAX에서 토큰 가져오기
fetch('/api/comment', {
method: 'POST',
headers: { 'X-CSRF-Token': '<?php echo dx_csrf_token(); ?>' },
body: JSON.stringify({ content: '...' })
});
9. 응답 함수
9.1 dx_error($msg, $code)
| dx_error(string $msg, int $code = 500) |
void (exit) |
HTTP 에러 응답 출력 후 종료 |
| 매개변수 |
타입 |
기본값 |
설명 |
| $msg |
string |
(필수) |
에러 메시지 |
| $code |
int |
500 |
HTTP 상태 코드 |
DX_DEBUG 모드 여부에 따라 출력 내용이 달라집니다. 항상 data/error.log에 스택 트레이스를 기록합니다.
// DX_DEBUG = true 시: 상세 에러 출력
// <pre class="text-red-600 ..."> [DX Error 500] 상세 메시지 </pre>
// DX_DEBUG = false 시 (운영): 일반 에러 페이지
// <div> 500 / 오류가 발생했습니다. </div>
// 403은 "접근이 거부되었습니다." 메시지
// 활용 예
if (!dx_is_admin()) dx_error('관리자만 접근 가능합니다.', 403);
if (!$board) dx_error('게시판을 찾을 수 없습니다.', 404);
// 로그 기록: data/error.log에 자동 기록
// [DXCMS DX_ERROR] 2026-05-01 12:00:00 | code=403 | msg=... | trace=...
9.2 dx_json($data, $code)
| dx_json(mixed $data, int $code = 200) |
void (exit) |
JSON 응답 출력 후 종료 |
| 매개변수 |
타입 |
기본값 |
설명 |
| $data |
mixed |
(필수) |
JSON으로 인코딩할 데이터 |
| $code |
int |
200 |
HTTP 상태 코드 |
// API 핸들러에서 성공 응답
dx_json(array('success' => true, 'id' => 42));
// 에러 응답
dx_json(array('error' => '권한이 없습니다.'), 403);
dx_json(array('error' => '찾을 수 없습니다.'), 404);
// 내부 동작:
// 1. http_response_code($code)
// 2. header('Content-Type: application/json; charset=utf-8')
// 3. json_encode($data, JSON_UNESCAPED_UNICODE)
// 4. exit
// 한글 JSON 출력 예
dx_json(array('message' => '댓글이 등록되었습니다.'));
// {"message":"댓글이 등록되었습니다."} ← JSON_UNESCAPED_UNICODE로 한글 유지
10. 플래시 메시지 함수
HTTP 리다이렉트를 거쳐 다음 요청에서 1회 표시되는 알림 메시지를 세션에 저장하고 읽습니다.
10.1 dx_set_flash / dx_flash / dx_get_flash
| dx_set_flash(string $message, string $type = 'success') |
void |
플래시 메시지 세션 저장 |
| dx_flash(string $message, string $type = 'success') |
void |
dx_set_flash의 별칭 |
| dx_get_flash() |
array|null |
플래시 메시지 읽고 세션에서 삭제 (1회성) |
| $type 값 |
의미 |
테마에서 활용 |
| 'success' |
성공 (초록) |
게시글 작성 완료, 로그인 성공 등 |
| 'error' |
오류 (빨강) |
인증 실패, 권한 없음 등 |
| 'warning' |
경고 (노랑) |
입력값 오류, 주의사항 등 |
| 'info' |
정보 (파랑) |
안내 메시지 등 |
// 1. 처리 후 메시지 저장 → 리다이렉트
// core/api/post.php 또는 boards/handler.php
dx_set_flash('게시글이 등록되었습니다.', 'success');
dx_redirect(dx_base_url($boardKey . '/view/' . $postId));
// 로그인 실패 시
dx_flash('아이디 또는 비밀번호가 올바르지 않습니다.', 'error');
dx_redirect(dx_base_url('auth/login'));
// 2. 테마 레이아웃에서 표시
// themes/default/layout/main.php
$flash = dx_get_flash();
if ($flash) {
echo '<div class="flash flash-' . dx_esc($flash['type']) . '">'
. dx_esc($flash['message']) . '</div>';
// dx_get_flash()는 읽는 즉시 세션에서 삭제됨 (1회성)
}
11. 문자열 • 날짜 • 파일 유틸리티 함수
11.1 문자열 함수
| dx_substr(string $str, int $length, string $suffix = '...') |
string |
멀티바이트 안전 문자열 자르기 |
| dx_mb_substr(string $str, int $start, int $length = null, string $enc = 'UTF-8') |
string |
mb_substr 폴백 래퍼 |
// 제목 길이 제한 (한글 포함)
$title = dx_substr($post['title'], 30);
// "안녕하세요 이것은 매우 긴 게시글..." → "안녕하세요 이것은 매우 긴 게시..."
// 커스텀 접미사
$excerpt = dx_substr($content, 100, ' •••');
// mb_substr 폴백: mb_string 확장 없는 환경에서도 동작
$sub = dx_mb_substr('안녕하세요', 2, 3); // '하세요'
11.2 dx_date($datetime, $format)
| dx_date(mixed $datetime, string $format = 'Y-m-d') |
string |
날짜 포맷 변환 (타임스탬프·문자열 양쪽 지원) |
| 매개변수 |
타입 |
기본값 |
설명 |
| $datetime |
mixed |
(필수) |
Unix 타임스탬프(int) 또는 날짜 문자열 |
| $format |
string |
'Y-m-d' |
PHP date() 포맷 문자열 |
// 날짜 문자열 → 포맷 변환
echo dx_date('2026-05-01 12:30:00', 'Y.m.d'); // '2026.05.01'
echo dx_date('2026-05-01 12:30:00', 'H:i'); // '12:30'
echo dx_date('2026-05-01 12:30:00', 'Y년 m월 d일'); // '2026년 05월 01일'
// 타임스탬프도 직접 지원
echo dx_date(time(), 'Y-m-d'); // '2026-05-01'
// 빈 값, 유효하지 않은 날짜 → 빈 문자열 반환 (오류 없음)
echo dx_date(''); // ''
echo dx_date('invalid'); // ''
// 게시판에서 활용
echo dx_date($post['created_at'], 'm.d'); // '05.01' (짧은 날짜)
echo dx_date($post['created_at'], 'Y-m-d'); // '2026-05-01'
11.3 파일 업로드 함수
| dx_upload_url(string $path = '') |
string |
data/uploads/ 기준 URL 생성 |
| dx_upload_exists(string $path = '') |
bool |
data/uploads/ 기준 파일 존재 여부 확인 |
// 업로드 파일 URL 생성
echo dx_upload_url('2026/05/photo.jpg');
// 'https://example.com/data/uploads/2026/05/photo.jpg'
// 썸네일 이미지 출력 (파일 존재 확인 후)
if (dx_upload_exists($post['thumbnail'])) {
echo '<img src="' . dx_upload_url($post['thumbnail']) . '">';
} else {
echo '<img src="' . dx_static_url('assets/img/no-image.png') . '">';
}
12. 배열 • 시간 • 용량 유틸리티 함수
12.1 dx_array_column($arr, $columnKey, $indexKey)
| dx_array_column(array $arr, string $columnKey, string $indexKey = null) |
array |
PHP 5.4 이하 array_column() 폴백 |
// PHP 5.5+에서는 내장 array_column()으로 자동 위임
// PHP 5.4 이하에서는 수동 구현으로 폴백
$rows = array(
array('id'=>1, 'name'=>'홍길동'),
array('id'=>2, 'name'=>'김철수'),
);
// name만 추출
$names = dx_array_column($rows, 'name'); // ['홍길동', '김철수']
// id를 인덱스로 사용
$byId = dx_array_column($rows, 'name', 'id'); // [1=>'홍길동', 2=>'김철수']
12.2 dx_time_ago($datetime)
| dx_time_ago(string $datetime) |
string |
상대 시간 문자열 반환 ('방금 전', 'N분 전' 등) |
| 경과 시간 |
반환값 |
| 60초 미만 |
'방금 전' |
| 1시간 미만 |
'N분 전' (예: '30분 전') |
| 24시간 미만 |
'N시간 전' (예: '3시간 전') |
| 7일 미만 |
'N일 전' (예: '2일 전') |
| 7일 이상 |
'm/d' (예: '04/28') |
echo dx_time_ago('2026-05-01 11:58:00'); // '방금 전'
echo dx_time_ago('2026-05-01 11:00:00'); // '58분 전'
echo dx_time_ago('2026-05-01 09:00:00'); // '3시간 전'
echo dx_time_ago('2026-04-30 12:00:00'); // '1일 전'
echo dx_time_ago('2026-04-20 12:00:00'); // '04/20'
// 게시판 목록에서 활용
echo dx_time_ago($post['created_at']);
12.3 dx_filesize($bytes)
| dx_filesize(int $bytes) |
string |
파일 크기를 읽기 쉬운 형식으로 변환 |
echo dx_filesize(512); // '512B'
echo dx_filesize(1536); // '1.5KB'
echo dx_filesize(1572864); // '1.5MB'
echo dx_filesize(1610612736); // '1.5GB'
// 첨부파일 목록에서 활용
echo dx_filesize($file['file_size']);
13. 페이지네이션 함수
HTML 페이지네이션 링크를 생성합니다. 테마 파일에서 바로 echo할 수 있는 HTML 문자열을 반환합니다.
13.1 dx_pagination($total, $perPage, $current, $urlPattern, $range)
| dx_pagination(int $total, int $perPage, int $current, string $urlPattern, int $range = 5) |
string |
페이지네이션 HTML 반환 |
| 매개변수 |
타입 |
기본값 |
설명 |
| $total |
int |
(필수) |
전체 레코드 수 |
| $perPage |
int |
(필수) |
페이지당 표시 수 |
| $current |
int |
(필수) |
현재 페이지 번호 (1부터 시작) |
| $urlPattern |
string |
(필수) |
{page} 플레이스홀더를 포함한 URL 패턴 |
| $range |
int |
5 |
표시할 페이지 번호 수 |
// 게시판 목록 페이지에서
$page = dx_get('page', 1, 'int');
$perPage = 15;
$total = $db->value("SELECT COUNT(*) FROM posts WHERE board_id = ?", array($boardId));
// {page} 플레이스홀더 사용
$urlPattern = dx_base_url($boardKey) . '?page={page}';
echo dx_pagination($total, $perPage, $page, $urlPattern, 5);
// 출력 예:
// <a href="...?page=2" class="dx-page-btn">‹</a>
// <a href="...?page=1" class="dx-page-btn">1</a>
// <a href="...?page=2" class="dx-page-btn dx-page-active">2</a>
// <a href="...?page=3" class="dx-page-btn">3</a>
// <a href="...?page=4" class="dx-page-btn">4</a>
// <a href="...?page=5" class="dx-page-btn">5</a>
// <a href="...?page=3" class="dx-page-btn">›</a>
// 반환 조건: 전체 데이터가 perPage 이하이거나 totalPage가 1이면 빈 문자열 반환
스타일 커스터마이징
dx_pagination()이 생성하는 링크의 CSS 클래스는 dx-page-btn과 dx-page-active입니다.
테마의 CSS에서 이 클래스를 스타일링하거나, 테마에서 dx_pagination() 대신
직접 페이지네이션 HTML을 구현할 수도 있습니다.
14. 로그 및 AJAX 감지 함수
14.1 dx_log($message, $level)
| dx_log(string $message, string $level = 'info') |
void |
data/error.log에 로그 기록 |
| 매개변수 |
타입 |
기본값 |
설명 |
| $message |
string |
(필수) |
기록할 메시지 |
| $level |
string |
'info' |
'info' | 'debug' | 'warning' | 'error' |
info와 debug 레벨은 기록하지 않습니다. warning과 error만 data/error.log에 기록됩니다. 호출 파일명과 줄 번호가 자동으로 추적되어 함께 기록됩니다.
// info/debug는 기록되지 않음 (성능 최적화)
dx_log('사용자 접속', 'info'); // 기록 안 됨
dx_log('디버그 정보', 'debug'); // 기록 안 됨
// warning/error는 기록됨
dx_log('IP 차단 시도: ' . dx_ip(), 'warning');
dx_log('DB 연결 실패: ' . $e->getMessage(), 'error');
// 로그 파일 형식 (data/error.log)
// [2026-05-01 12:00:00][WARNING][extend/top/02_ip_block.php:25] IP 차단 시도: 10.0.0.1
// [2026-05-01 12:00:01][ERROR][core/db/Database.php:88] DB 연결 실패: ...
// 내부: debug_backtrace()로 호출 파일/줄 자동 추적
// functions.php 자신은 스킵하고 실제 호출자 위치를 기록
14.2 dx_is_ajax()
| dx_is_ajax() |
bool |
AJAX 요청인지 확인 |
// X-Requested-With: XMLHttpRequest 헤더 감지
if (dx_is_ajax()) {
// AJAX 응답: JSON
dx_json(array('success' => true, 'data' => $result));
} else {
// 일반 페이지 응답: HTML 리다이렉트
dx_set_flash('완료되었습니다.', 'success');
dx_redirect(dx_base_url('notice'));
}
// 주의: fetch API는 기본적으로 이 헤더를 보내지 않음
// fetch 사용 시 명시적으로 헤더 추가 필요:
fetch('/api/like', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json'
}
});
15. 게시판 헬퍼 함수 (홈 제작용)
홈페이지 레이아웃에서 최신 게시글을 가져오거나 스킨으로 출력하기 위한 함수입니다. 테마의 홈 페이지 파일에서 가장 많이 사용됩니다.
15.1 dx_board_posts($boardKey, $limit, $withNotice, $titleLen, $excerptLen)
| dx_board_posts(string $boardKey, int $limit = 5, bool $withNotice = false, int $titleLen = 0, int $excerptLen = 100) |
array |
게시판 최신글 배열 반환 |
| 매개변수 |
타입 |
기본값 |
설명 |
| $boardKey |
string |
(필수) |
게시판 키 (board_key) |
| $limit |
int |
5 |
가져올 글 수 |
| $withNotice |
bool |
false |
true면 공지글 포함 |
| $titleLen |
int |
0 |
제목 최대 길이 (0이면 전체) |
| $excerptLen |
int |
100 |
본문 발췌 길이 (0이면 발췌 없음) |
반환 배열 각 항목의 키 목록입니다.
| 키 |
타입 |
예시 값 |
설명 |
| 'id' |
int |
42 |
게시글 ID |
| 'title' |
string |
'공지사항입니다' |
제목 ($titleLen 적용) |
| 'author' |
string |
'홍길동' |
작성자 이름 |
| 'date' |
string |
'2026-05-01' |
날짜 (Y-m-d) |
| 'date_short' |
string |
'05.01' |
짧은 날짜 (m.d) |
| 'url' |
string |
'https://...view/42' |
게시글 URL |
| 'board_key' |
string |
'notice' |
게시판 키 |
| 'view_count' |
int |
127 |
조회수 |
| 'comment_count' |
int |
5 |
댓글 수 |
| 'excerpt' |
string |
'내용 요약...' |
본문 발췌 ($excerptLen 적용) |
// 홈 페이지 파일 (pages/home.php)에서 활용
$posts = dx_board_posts('notice', 5);
foreach ($posts as $post) {
echo '<li>';
echo ' <a href="' . dx_esc($post['url']) . '">';
echo ' ' . dx_esc($post['title']);
echo ' </a>';
echo ' <span>' . $post['date_short'] . '</span>';
echo '</li>';
}
// 본문 발췌 포함 (카드형 레이아웃)
$posts = dx_board_posts('free', 6, false, 30, 80);
foreach ($posts as $post) {
echo '<div class="card">';
echo ' <h3>' . dx_esc($post['title']) . '</h3>';
echo ' <p>' . dx_esc($post['excerpt']) . '</p>';
echo ' <span>' . dx_time_ago($post['date']) . '</span>';
echo '</div>';
}
15.2 dx_board_latest($boardKey, $limit, $skin, $title, $icon, $titleLen, $showExcerpt)
| dx_board_latest(string $boardKey, int $limit = 5, string $skin = 'list', ...) |
void |
스킨 파일로 최신글 목록 출력 |
| 매개변수 |
타입 |
기본값 |
설명 |
| $boardKey |
string |
(필수) |
게시판 키 |
| $limit |
int |
5 |
표시할 글 수 |
| $skin |
string |
'list' |
스킨명: 'list', 'card', 'simple' 등 |
| $title |
string |
'' |
섹션 제목 (없으면 게시판 이름 자동 사용) |
| $icon |
string |
'' |
아이콘 이모지 |
| $titleLen |
int |
0 |
제목 최대 길이 |
| $showExcerpt |
bool |
false |
본문 발췌 표시 여부 |
스킨 파일은 DxTheme 폴백 체인으로 결정됩니다. 스킨 파일 내에서 사용 가능한 변수 목록입니다.
| 변수 |
타입 |
설명 |
| $board_key |
string |
게시판 키 (board_key) |
| $board_name |
string |
게시판 이름 (DB) |
| $title |
string |
표시 제목 ($title 파라미터 또는 $board_name) |
| $icon |
string |
아이콘 이모지 |
| $more_url |
string |
더보기 URL (게시판 목록 URL) |
| $posts |
array |
dx_board_posts() 반환 배열 |
| $show_excerpt |
bool |
발췌 표시 여부 |
// 스킨 폴백 체인
// 1. themes/{현재테마}/board_latest/{스킨명}.php ← 최우선
// 2. themes/default/board_latest/{스킨명}.php ← 폴백
// 3. themes/default/board_latest/list.php ← 최종 폴백
// 홈 페이지에서 사용 예
dx_board_latest('notice', 5, 'list', '공지사항', '📢');
dx_board_latest('free', 6, 'card', '자유게시판', '💬');
dx_board_latest('gallery', 8, 'grid', '갤러리', '🖼️', 0, false);
16. 테마 자산 함수
모든 테마에 공통으로 필요한 폰트, 아이콘, CSS 엔진을 자동으로 로드합니다. 테마 파일의 <head> 영역에서 한 번만 호출하면 됩니다.
16.1 dx_head_assets($opts)
| dx_head_assets(array $opts = array()) |
void |
Pretendard + Space Grotesk + Font Awesome 6 + dxb-css 로드 |
| 매개변수 |
타입 |
기본값 |
설명 |
| opts['fa'] |
bool |
true |
Font Awesome 6 CSS 로드 여부 |
| opts['pretend'] |
bool |
true |
Pretendard 폰트 로드 여부 |
| opts['grotesk'] |
bool |
true |
Space Grotesk 폰트 로드 여부 |
| opts['dxb'] |
bool |
true |
dxb-css.js 로드 여부 |
<!-- themes/{테마}/layout/main.php -->
<head>
<meta charset="UTF-8">
<title><?php echo dx_esc(dx_config('site_name')); ?></title>
<?php dx_head_assets(); ?>
<!-- 위 한 줄로 아래 항목 모두 자동 로드:
- Pretendard 폰트 (CDN)
- Space Grotesk 폰트 (Google Fonts)
- Font Awesome 6 (CDN)
- dxb-css.js (DXB 유틸리티 CSS 엔진) -->
<link rel="stylesheet" href="<?php echo dx_static_url('themes/default/assets/style.css'); ?>">
</head>
// 개별 옵션 비활성화
<?php dx_head_assets(array('fa' => false)); ?> // Font Awesome 제외
<?php dx_head_assets(array('dxb' => false)); ?> // dxb-css.js 제외
16.2 dx_font_css()
| dx_font_css() |
void |
공통 body font CSS 인라인 출력 |
CSS 변수로 폰트 패밀리를 정의하고 기본 리셋을 적용합니다. 테마 파일의 <style> 블록 직전에 호출합니다.
<!-- 출력되는 CSS 요약 -->
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0 }
html { scroll-behavior: smooth }
:root {
--font-body: 'Pretendard', -apple-system, ..., sans-serif;
--font-head: 'Space Grotesk', 'Pretendard', sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', ..., monospace;
}
body { font-family: var(--font-body); -webkit-font-smoothing: antialiased; }
a { text-decoration: none; color: inherit }
img { max-width: 100%; height: auto; display: block }
/* 스크롤바, 텍스트 셀렉션 스타일 등 */
.sg { font-family: var(--font-head) } /* Space Grotesk 단축 클래스 */
</style>
17. 내부 전용 함수 (_dx_ 접두사)
앞에 언더스코어(_)가 붙은 함수는 내부 용도로, 직접 호출하지 않는 것을 권장합니다. 대신 공개 함수를 통해 간접 사용합니다.
17.1 _dx_resolve_base()
dx_base_url(), dx_static_url()이 내부적으로 사용하는 베이스 URL 계산 함수입니다. static 캐싱으로 요청당 1회만 계산합니다. (4장 URL 생성 섹션에 상세 설명)
17.2 _dx_inject_dxb_css()
load_plugins() 호출 시 자동으로 실행되어 DXB CSS 엔진(dxb-css.js)을 모든 페이지에 자동 주입합니다. dx_head 훅과 dx_body_bottom 훅에 콜백을 등록하는 방식으로 동작합니다.
// load_plugins() 내에서 자동 호출
_dx_inject_dxb_css();
// 내부: 두 개의 훅 등록
dx_add_hook('dx_head', function() {
// dxb-css.js 파일 존재 시에만 로드
// 중복 로드 방지 (data-dxb 속성 확인)
}, 1); // priority=1: 다른 dx_head 훅보다 먼저 실행
dx_add_hook('dx_body_bottom', function() {
// 1차 rescan() 즉시 실행
// 2차: requestAnimationFrame으로 브라우저 렌더링 직전 스캔
}, 1);
// 직접 호출하지 않아야 하는 이유:
// • load_plugins()가 이미 호출하므로 중복 등록 발생
// • dx_head, dx_body_bottom 훅이 2배로 등록됨
17.3 _dx_register_point_hooks()
포인트•경험치 시스템의 훅을 등록합니다. index.php에서 초기화 완료 후 1회 호출됩니다.
| 훅 이름 |
이벤트 |
처리 내용 |
| dx_after_write |
게시글 작성 |
DxPoint::add + DxPoint::addExp (write 타입) |
| dx_after_comment |
댓글 작성 |
DxPoint::add + DxPoint::addExp (comment 타입) |
| dx_after_login |
로그인 |
오늘 첫 로그인인지 확인 후 출석 포인트 지급 |
| dx_after_register |
회원 가입 |
가입 축하 포인트 + 경험치 지급 |
| dx_after_like |
좋아요 받음 |
작성자에게 like_recv 포인트 + 경험치 |
17.4 _dxAvaColor($name)
회원 이름을 해시하여 아바타 배경색을 결정합니다. 같은 이름은 항상 같은 색이 나옵니다.
// 사용 예 (테마 파일에서)
$color = _dxAvaColor($user['name']);
echo '<div style="background:' . $color . ';...">';
echo $user['name'][0]; // 이름 첫 글자
echo '</div>';
// 8가지 색상 중 해시로 선택
// #ef4444 (빨강), #f97316 (주황), #eab308 (노랑), #22c55e (초록)
// #06b6d4 (청록), #3b82f6 (파랑), #8b5cf6 (보라), #ec4899 (핑크)
18. 실전 활용 패턴 모음
18.1 게시판 핸들러 패턴
// boards/handler.php 또는 pages/*.php에서
// ① GET 파라미터 안전하게 읽기
$page = dx_get('page', 1, 'int');
$keyword = dx_get('q', '');
$postId = dx_get('id', 0, 'bigint');
// ② POST 처리
if (dx_method('POST')) {
dx_csrf_check();
$title = dx_post('title');
$content = dx_post('content');
if (empty($title)) {
dx_json(array('error' => '제목을 입력해주세요.'), 422);
}
// ... DB 저장 ...
dx_json(array('success' => true, 'id' => $newId));
}
// ③ 리다이렉트
dx_set_flash('게시글이 등록되었습니다.', 'success');
dx_redirect(dx_base_url($boardKey . '/view/' . $newId));
18.2 API 핸들러 패턴
// core/api/my_api.php
if (!defined('DX_CMS')) exit;
// 인증 확인
if (!Auth::getInstance()->isLoggedIn()) {
dx_json(array('error' => '로그인이 필요합니다.'), 401);
}
// AJAX 여부 확인
if (!dx_is_ajax()) {
dx_json(array('error' => 'AJAX 요청만 허용됩니다.'), 400);
}
// CSRF 검증
dx_csrf_check();
// 요청 처리
$targetId = dx_post('id', 0, 'bigint');
if (!ctype_digit($targetId) || $targetId === '0') {
dx_json(array('error' => '잘못된 요청입니다.'), 400);
}
// ... 처리 로직 ...
dx_json(array('success' => true, 'message' => '처리되었습니다.'));
18.3 extend/ 파일 패턴
// extend/middle/02_my_feature.php
if (!defined('DX_CMS')) exit;
// 라우트 정보 (middle에서만 사용 가능)
$route = isset($GLOBALS['dx_route']) ? $GLOBALS['dx_route'] : array();
$type = isset($route['type']) ? $route['type'] : '';
$slug = isset($route['slug']) ? $route['slug'] : '';
// admin/api 제외
if (in_array($type, array('admin', 'api'))) return;
// 설정 읽기
$enabled = dx_config('my_feature_enabled', '0');
if ($enabled !== '1') return;
// IP 기반 처리
$ip = dx_ip();
$device = dx_device();
// 응답 후 처리
register_shutdown_function(function() use ($ip, $type, $slug) {
if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();
$db = Database::getInstance();
$db->query("INSERT INTO my_log...", array($ip, $type, $slug));
});
18.4 테마 파일 패턴
<!-- themes/{테마}/layout/main.php -->
<?php if (!defined('DX_CMS')) exit; ?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo dx_esc(dx_config('site_name')); ?></title>
<?php dx_head_assets(); ?>
<?php dx_font_css(); ?>
</head>
<body>
<?php
// 플래시 메시지 표시
$flash = dx_get_flash();
if ($flash) {
echo '<div class="flash flash-' . dx_esc($flash['type']) . '">'
. dx_esc($flash['message']) . '</div>';
}
?>
<main>
<?php echo $dx_content; ?>
</main>
</body>
</html>
19. 전체 함수 빠른 참조 (Quick Reference)
| 함수 |
반환 |
설명 |
| dx_random_bytes($length) |
string |
암호화 안전 난수 바이트 |
| dx_random_hex($length=32) |
string |
16진수 난수 문자열 |
| dx_realpath($path) |
string|false |
백슬래시 정규화 realpath |
| dx_path_inside($path, $base) |
bool |
경로 탈출 방어 검증 |
| dx_is_https() |
bool |
HTTPS 감지 (CF·IIS 포함) |
| dx_base_url($path='') |
string |
내부 절대 URL 생성 |
| dx_static_url($path='') |
string |
정적 자산 URL |
| dx_request_uri() |
string |
이중슬래시 정규화 URI |
| dx_current_url() |
string |
현재 전체 URL |
| dx_redirect($url, $code=302) |
void/exit |
리다이렉트 후 exit |
| dx_redirect_back($fallback='/') |
void/exit |
이전 페이지로 리다이렉트 |
| dx_get($key, $default='', $type='string') |
mixed |
$_GET 안전 읽기 |
| dx_post($key, $default='', $type='string') |
mixed |
$_POST 안전 읽기 |
| dx_request($key, $default='', $type='string') |
mixed |
$_REQUEST 안전 읽기 |
| dx_method($method) |
bool |
HTTP 메서드 확인 |
| dx_cast($val, $type) |
mixed |
타입 캐스팅 |
| dx_ip() |
string |
클라이언트 IP |
| dx_ua() |
string |
User-Agent 문자열 |
| dx_device() |
string |
디바이스: 'mobile'|'tablet'|'pc' |
| dx_os() |
string |
OS 정보 |
| dx_browser() |
string |
브라우저 정보 |
| dx_config($key, $default='') |
mixed |
설정값 조회 |
| dx_set_config($key, $value) |
void |
런타임 설정 변경 |
| dx_config_set($key, $value) |
void |
dx_set_config 별칭 |
| dx_esc($str) |
string |
HTML 특수문자 이스케이프 |
| dx_safe_url($url) |
string |
XSS 방어 URL |
| dx_csrf_token() |
string |
CSRF 토큰 반환/생성 |
| dx_csrf_field() |
string |
CSRF hidden 필드 HTML |
| dx_csrf_check() |
void |
CSRF 검증 (실패 시 403) |
| dx_error($msg, $code=500) |
void/exit |
HTTP 에러 응답 |
| dx_json($data, $code=200) |
void/exit |
JSON 응답 |
| dx_set_flash($msg, $type='success') |
void |
플래시 메시지 저장 |
| dx_flash($msg, $type='success') |
void |
dx_set_flash 별칭 |
| dx_get_flash() |
array|null |
플래시 메시지 읽기(1회) |
| dx_substr($str, $len, $suffix='...') |
string |
멀티바이트 안전 자르기 |
| dx_mb_substr($str, $start, $len=null) |
string |
mb_substr 폴백 래퍼 |
| dx_date($datetime, $format='Y-m-d') |
string |
날짜 포맷 변환 |
| dx_upload_url($path='') |
string |
data/uploads/ URL |
| dx_upload_exists($path='') |
bool |
업로드 파일 존재 확인 |
| dx_array_column($arr, $col, $idx=null) |
array |
array_column PHP 5.4 폴백 |
| dx_time_ago($datetime) |
string |
'N분 전' 등 상대 시간 |
| dx_filesize($bytes) |
string |
'1.5MB' 등 파일 크기 |
| dx_pagination($total, $per, $cur, $url, $range=5) |
string |
페이지네이션 HTML |
| dx_log($msg, $level='info') |
void |
data/error.log 기록 |
| dx_is_ajax() |
bool |
AJAX 요청 감지 |
| dx_board_posts($key, $limit=5, ...) |
array |
게시판 최신글 배열 |
| dx_board_latest($key, $limit=5, $skin, ...) |
void |
스킨으로 최신글 출력 |
| dx_head_assets($opts) |
void |
공통 폰트/아이콘 로드 |
| dx_font_css() |
void |
공통 body font CSS 출력 |
핵심 원칙 요약
1. functions.php는 가장 먼저 로드된다. 클래스를 직접 생성하지 않고 함수 정의만 담는다.
2. $_GET/$_POST 직접 접근 금지. dx_get(), dx_post()를 항상 사용한다.
3. 사용자 입력 출력 시 dx_esc()로 필수 이스케이프. URL은 dx_safe_url().
4. POST 처리 시 dx_csrf_check()를 맨 먼저 호출한다.
5. 리다이렉트는 dx_redirect(). JSON은 dx_json(). 에러는 dx_error().
6. IP는 dx_ip()로만 읽는다. Cloudflare•프록시 환경에서도 실제 IP 보장.
7. 설정은 dx_config(). 런타임 변경은 dx_set_config(). DB 변경 아님.
8. 로그는 dx_log($msg, 'warning') 또는 dx_log($msg, 'error'). info는 기록 안 됨.