1. 공통함수 라이브러리 개요
1.1 파일 위치 및 역할
core/functions.php는 DXCMS 프레임워크 전체에서 공통으로 사용되는 순수 함수(Pure Function)들의 집합입니다.이 파일은 HookManager, PluginRegistry, Database 클래스보다 먼저 로드되며, 클래스 인스턴스를 생성하지 않고 함수 정의만 담습니다. PHP 5.6부터 최신 PHP까지, Windows•Linux•IIS•Apache•Nginx, HTTP•HTTPS 환경을 완전히 지원합니다.
📌 로드 순서 원칙
core/functions.php 는 index.php 부팅 가장 초기에 로드됩니다.
이 파일 안에서는 Database::getInstance(), Auth::getInstance() 등 클래스 인스턴스를 직접 호출하지 않습니다.
단, dx_board_posts() 처럼 내부적으로 Database를 사용하는 함수는 반드시 클래스 로드 이후에 호출하세요.
1.2 함수 카테고리 전체 구조
| 카테고리 | 주요 함수 | 설명 |
|---|---|---|
| 보안 난수 | dx_random_bytes(), dx_random_hex() |
PHP 버전별 자동 폴백 보안 난수 생성 |
| 경로 유틸 | dx_realpath(), dx_path_inside() |
크로스 플랫폼 파일 경로 처리 |
| HTTPS 감지 | dx_is_https() | 프록시•CDN 환경 포함 HTTPS 정확 감지 |
| URL 생성 | dx_base_url(), dx_static_url() |
서브디렉토리•URL Rewrite 자동 처리 |
| 요청 처리 | dx_get(), dx_post(), dx_request() |
타입 캐스팅 포함 안전 입력 처리 |
| IP / UA | dx_ip(), dx_device(), dx_os() |
Cloudflare 포함 실제 IP•디바이스 감지 |
| 설정 관리 | dx_config(), dx_set_config() |
전역 설정값 읽기/쓰기 |
| 보안 | dx_esc(), dx_safe_url(), dx_csrf_*() |
XSS 방어•CSRF 토큰 처리 |
| 응답 | dx_error(), dx_json() |
HTTP 오류•JSON API 응답 |
| 플래시 메시지 | dx_flash(), dx_get_flash() |
세션 기반 일회성 메시지 |
| 문자열 | dx_substr(), dx_time_ago() |
UTF-8 안전 문자열•상대 시간 |
| 날짜 | dx_date() | 날짜 포맷 통일 |
| 파일 업로드 | dx_upload_url(), dx_upload_exists() |
업로드 URL/존재 여부 확인 |
| 페이지네이션 | dx_pagination() | HTML 페이지 버튼 자동 생성 |
| 게시판 헬퍼 | dx_board_posts(), dx_board_latest() |
홈/위젯에서 게시글 출력 |
| 에셋 출력 | dx_head_assets(), dx_font_css() |
폰트•아이콘 CDN 자동 삽입 |
| 로그 | dx_log() | 에러/경고 파일 로그 기록 |
| 플러그인 | load_plugins() | 플러그인 자동 로드 |
2. 보안 난수 생성
2.1 dx_random_bytes($length)
PHP 버전과 서버 환경에 따라 가장 안전한 방법을 자동으로 선택하여 보안 난수 바이트를 반환합니다.
// PHP 7.0+ → random_bytes() 사용 (가장 안전)
// PHP 5.x + OpenSSL → openssl_random_pseudo_bytes()
// PHP 5.x + mcrypt → mcrypt_create_iv()
// 최후 폴백 → mt_rand() (저가형 호스팅 대응)
$bytes = dx_random_bytes(16); // 16바이트 보안 난수 반환
2.2 dx_random_hex($length = 32)
보안 난수를 16진수 문자열로 반환합니다. CSRF 토큰, 세션키, 임시 비밀번호 생성에 사용합니다.
$token = dx_random_hex(32); // 32자리 16진수 토큰 생성
$tempPwd = dx_random_hex(16); // 16자리 임시 비밀번호
// 내부적으로 dx_random_bytes((int)ceil($length/2))를 호출 후 bin2hex() 적용
3. 경로 및 URL 처리
3.1 경로 유틸리티
dx_realpath($path)
Windows 백슬래시를 슬래시로 정규화한 실제 절대 경로를 반환합니다. realpath() 실패 시 false를 반환합니다.
$real = dx_realpath('../data/uploads');
// 결과: '/var/www/html/data/uploads' (\를 /로 자동 변환)
dx_path_inside($path, $base)
$path가 $base 디렉토리 내부에 있는지 확인합니다. 경로 탈출(Path Traversal) 공격 방어에 사용합니다.
$base = DX_DATA . '/uploads';
$file = dx_realpath($userInput);
if (!dx_path_inside($file, $base)) {
dx_error('허용되지 않은 경로입니다.', 403);
}
// Windows 환경: 대소문자 구분 없이 stripos() 사용 (자동 처리)
3.2 HTTPS 감지
dx_is_https()
Cloudflare, AWS ALB, nginx 리버스 프록시 등 다양한 환경에서 정확하게 HTTPS를 감지합니다.
// 다음 헤더를 순서대로 확인:
// 1. $_SERVER['HTTPS']
// 2. HTTP_X_FORWARDED_PROTO (nginx 프록시)
// 3. HTTP_X_FORWARDED_SSL
// 4. HTTP_FRONT_END_HTTPS
// 5. SERVER_PORT === 443
if (dx_is_https()) {
// SSL 환경에서만 실행할 코드
}
3.3 URL 생성 함수
dx_base_url($path = '')
사이트 절대 URL을 생성합니다. 내부적으로 _dx_resolve_base() 헬퍼를 통해 site_url 설정값을 확인하고, 도메인 불일치 시 자동 감지 모드로 전환합니다. 이중 슬래시(//)를 자동 제거하며 URL Rewrite 미적용 환경(IIS 등)도 지원합니다.
echo dx_base_url(); // https://example.com/
echo dx_base_url('board/notice'); // https://example.com/board/notice
echo dx_base_url('admin/settings'); // https://example.com/admin/settings
// URL Rewrite 비활성화 환경 (dx_config('url_rewrite') === '0')
echo dx_base_url('board/notice');
// https://example.com/index.php?_url=/board/notice
dx_static_url($path = '')
CSS, JS, 이미지 등 정적 자산의 URL을 생성합니다. dx_base_url()과 동일한 베이스를 사용하되, URL Rewrite 영향을 받지 않습니다.
echo dx_static_url('assets/css/dx.css');
// https://example.com/assets/css/dx.css
echo dx_static_url('assets/js/dx.js');
// https://example.com/assets/js/dx.js
dx_request_uri() / dx_current_url()
// REQUEST_URI의 이중 슬래시(//) 정규화
$uri = dx_request_uri(); // '/board/notice/1'
// 현재 페이지 완전 URL 반환
$url = dx_current_url();
// 'https://example.com/board/notice/1?page=2'
dx_redirect($url, $code = 302) / dx_redirect_back($fallback = '/')
// 지정 URL로 리다이렉트 (기본 302)
dx_redirect(dx_base_url('member/login'));
dx_redirect(dx_base_url('board/notice'), 301); // 영구 이동
// 이전 페이지로 돌아가기 (HTTP_REFERER 없으면 fallback)
dx_redirect_back('/board/notice');
4. 요청 데이터 처리
4.1 안전 입력 수신 함수
GET/POST/REQUEST 데이터를 수신할 때 반드시 dx_get() • dx_post() • dx_request() 를 사용합니다. 세 함수 모두 내부적으로 dx_cast() 를 거쳐 타입 변환 및 trim을 적용합니다.함수 시그니처
dx_get($key, $default='', $type='string')
dx_post($key, $default='', $type='string')
dx_request($key, $default='', $type='string')
$type 파라미터 종류
| $type 값 | 변환 방식 | 사용 예시 |
|---|---|---|
| string (기본) | trim() 처리된 문자열 | dx_get('keyword') |
| int | (int) 캐스팅 | dx_get('page', 1, 'int') |
| float | (float) 캐스팅 | dx_post('price', 0.0, 'float') |
| bool | (bool) 캐스팅 | dx_post('agree', false, 'bool') |
| array | (array) 캐스팅 | dx_post('ids', [], 'array') |
| bigint | 32bit PHP 오버플로우 방지 정수 문자열 | dx_post('post_id', '0', 'bigint') |
실전 사용 예
// 게시판 목록 파라미터
$page = dx_get('page', 1, 'int'); // GET page, 기본값 1, 정수 변환
$keyword = dx_get('keyword', ''); // GET keyword, 기본값 빈문자열
$catId = dx_get('cat', 0, 'int'); // GET cat, 기본값 0
// 게시글 작성 POST 데이터
$title = dx_post('title'); // POST title, string+trim
$content = dx_post('content'); // POST content
$postId = dx_post('post_id', '0', 'bigint'); // 큰 ID 안전 처리
// HTTP 메서드 확인
if (dx_method('POST')) {
// POST 요청 처리
}
4.2 dx_cast($val, $type)
dx_cast()는 dx_get/post/request 내부에서 호출되지만, 직접 사용할 수도 있습니다.
$raw = ' 123 ';
$num = dx_cast($raw, 'int'); // → 123
$str = dx_cast($raw, 'string'); // → '123' (trim 적용)
$big = dx_cast('9999999999', 'bigint'); // 32bit PHP에서도 안전한 정수 문자열
5. IP 주소 및 디바이스 감지
5.1 dx_ip()
Cloudflare(CF-Connecting-IP), X-Real-IP, X-Forwarded-For 등 프록시 헤더를 순서대로 확인하여 실제 클라이언트 IP를 반환합니다.
// 확인 우선순위:
// 1. HTTP_CF_CONNECTING_IP (Cloudflare)
// 2. HTTP_X_REAL_IP (nginx)
// 3. HTTP_X_FORWARDED_FOR (일반 프록시)
// 4. HTTP_CLIENT_IP
// 5. REMOTE_ADDR (직접 연결)
$ip = dx_ip(); // '203.0.113.45'
// 활용: 접속 로그 기록
$db->query(
"INSERT INTO `{$db->table('access_log')}` (ip, created_at) VALUES (?, NOW())",
[dx_ip()]
);
5.2 디바이스•OS•브라우저 감지
// 디바이스 타입: 'mobile' | 'tablet' | 'pc'
$device = dx_device();
// OS 정보: 'Windows 10/11', 'Android 13', 'iOS 17.0' 등
$os = dx_os();
// 브라우저: 'Chrome 124', 'Firefox 125', 'Edge', 'Safari' 등
$browser = dx_browser();
// 활용: 모바일 전용 레이아웃 분기
if (dx_device() === 'mobile') {
include $skinDir . '/mobile/list.php';
} else {
include $skinDir . '/list.php';
}
// 활용: 회원 접속 통계 저장
$db->query(
"INSERT INTO `{$db->table('member_access')}` (member_id, device, os, browser, ip)
VALUES (?, ?, ?, ?, ?)",
[$memberId, dx_device(), dx_os(), dx_browser(), dx_ip()]
);
6. 설정 관리
6.1 dx_config($key, $default = '')
config.php 가 로드한 전역 $dx_config 배열에서 설정값을 읽습니다. 키가 없으면 $default를 반환합니다.
// 기본 읽기
$siteName = dx_config('site_name', 'DXCMS');
$adminMail = dx_config('admin_email');
$siteUrl = dx_config('site_url', '');
// 활용: 기능 on/off 플래그 확인
if (dx_config('use_sms') === '1') {
// SMS 기능 사용 중
}
// 활용: URL Rewrite 여부 확인
if (dx_config('url_rewrite', '1') === '0') {
// index.php?_url= 방식 사용
}
6.2 dx_set_config($key, $value) / dx_config_set()
런타임에 설정값을 동적으로 변경합니다. dx_config_set()은 dx_set_config()의 별칭입니다.
// 런타임 설정 변경 (DB에는 저장되지 않음, 현재 요청에서만 유효)
dx_set_config('site_name', '테스트 사이트');
dx_config_set('debug_mode', '1');
// 플러그인에서 임시 설정 주입
dx_set_config('theme', 'dark');
7. 보안 관련 함수
7.1 dx_esc($str) — XSS 방어 출력
HTML 특수문자를 엔티티로 변환합니다. 모든 사용자 입력값은 반드시 이 함수를 거쳐 출력하세요. htmlspecialchars(ENT_QUOTES, 'UTF-8') 의 래퍼입니다.
// 잘못된 출력 (XSS 취약)
echo $_GET['name']; // 위험!
// 올바른 출력
echo dx_esc($_GET['name']); // 안전
// 게시글 제목 출력
echo dx_esc($row['title']);
// input value 출력
echo '<input type="text" value="' . dx_esc($value) . '">';
7.2 dx_safe_url($url) — URL XSS 방어
blocked:, blocked:, data: 프로토콜을 차단하고 안전한 URL을 반환합니다. 메뉴•링크 href 출력에 사용합니다. 상대경로(/path) 는 자동으로 dx_base_url()로 변환됩니다.
echo dx_safe_url($menu['url']); // 메뉴 링크 안전 출력
// 위험 URL 처리
echo dx_safe_url('blocked:alert(1)'); // → '#' (차단)
echo dx_safe_url('data:text/html,<h1>'); // → '#' (차단)
// 상대경로 자동 변환
echo dx_safe_url('/board/notice');
// → 'https://example.com/board/notice'
// 링크 태그 사용 예
echo '<a href="' . dx_safe_url($link) . '">' . dx_esc($label) . '</a>';
7.3 CSRF 보호 함수
내부적으로 Secure 클래스에 위임합니다. Secure 미로드 환경에서는 세션 기반 폴백으로 동작합니다.
// 1) 폼에 CSRF 토큰 필드 삽입
echo dx_csrf_field();
// → <input type="hidden" name="_csrf" value="abc123...">
// 2) 토큰 값만 가져오기 (Ajax 요청 헤더 등에 사용)
$token = dx_csrf_token();
// 3) POST 처리 시 검증 (실패 시 403 반환 후 exit)
dx_csrf_check();
// 폼 전체 예시
// <form method="POST" action="...">
// <?php echo dx_csrf_field(); ?>
// ...
// </form>
// POST 핸들러 예시
// dx_csrf_check(); // 반드시 첫 줄에 호출
// $title = dx_post('title');
// ...
⚠️ CSRF 보호 필수 적용
POST 요청을 처리하는 모든 핸들러 파일(게시글 작성/수정/삭제, 회원정보 변경 등) 상단에 반드시 dx_csrf_check()를 호출하세요.
AJAX 요청의 경우 요청 헤더에 X-CSRF-Token: <?php echo dx_csrf_token(); ?> 를 포함시켜야 합니다.
8. 응답 처리 함수
8.1 dx_error($msg, $code = 500)
HTTP 상태코드를 설정하고 오류 화면을 출력한 후 종료합니다. DX_DEBUG 상수가 true면 상세 오류 메시지를, false면 사용자 친화적 메시지를 출력합니다.
// 403 접근 거부
dx_error('권한이 없습니다.', 403);
// 404 not found
dx_error('페이지를 찾을 수 없습니다.', 404);
// 500 서버 오류 (기본)
dx_error('오류가 발생했습니다.');
// 권한 체크 패턴
if (!Auth::getInstance()->isLoggedIn()) {
dx_redirect(dx_base_url('member/login'));
}
if ($post['member_id'] !== $loginUser['id']) {
dx_error('본인 게시글만 수정할 수 있습니다.', 403);
}
8.2 dx_json($data, $code = 200) — API/Ajax 응답
Content-Type: application/json; charset=utf-8 헤더를 설정하고 데이터를 JSON으로 출력 후 종료합니다. 한글 등 유니코드를 이스케이프 없이 출력합니다.
// 성공 응답
dx_json(['success' => true, 'message' => '저장되었습니다.']);
// 데이터 포함 응답
dx_json([
'success' => true,
'data' => ['id' => $postId, 'title' => $title],
]);
// 실패 응답 (422 Unprocessable Entity)
dx_json(['success' => false, 'message' => '제목을 입력하세요.'], 422);
// 인증 실패 (401)
if (!Auth::getInstance()->isLoggedIn()) {
dx_json(['success' => false, 'message' => '로그인이 필요합니다.'], 401);
}
8.3 플래시 메시지 함수
세션에 일회성 메시지를 저장하고, 다음 페이지 로드 시 꺼내어 사용합니다. 리다이렉트 후 알림 표시에 활용합니다.
// 메시지 저장 (리다이렉트 전)
dx_flash('게시글이 저장되었습니다.', 'success');
dx_flash('삭제되었습니다.', 'warning');
dx_flash('오류가 발생했습니다.', 'error');
// dx_set_flash()는 dx_flash()의 동일한 기능 (별칭)
dx_set_flash('처리되었습니다.', 'success');
// 메시지 읽기 (읽으면 세션에서 자동 삭제)
$flash = dx_get_flash();
if ($flash) {
echo '<div class="alert alert-' . dx_esc($flash['type']) . '">';
echo dx_esc($flash['message']);
echo '</div>';
}
9. 문자열•날짜•파일 유틸리티
9.1 문자열 처리
dx_substr($str, $length, $suffix = '...')
UTF-8 한글을 안전하게 지정 길이로 자르고 말줄임 문자를 붙입니다. mb_string 미설치 환경도 지원합니다.
$title = '안녕하세요 반갑습니다 DXCMS 입니다';
$short = dx_substr($title, 10); // '안녕하세요 반갑습니다...'
$custom = dx_substr($title, 10, ' [더보기]'); // 커스텀 suffix
// 게시판 목록 제목 자르기
echo dx_esc(dx_substr($row['title'], 30));
dx_time_ago($datetime)
DB에 저장된 datetime 문자열을 "방금 전", "5분 전", "3시간 전", "2일 전" 형태로 반환합니다.
echo dx_time_ago($row['created_at']);
// 결과 예: '방금 전' | '15분 전' | '3시간 전' | '2일 전' | '04/15'
// 기준: 7일 미만이면 상대시간, 이상이면 'm/d' 형식 날짜 반환
dx_array_column($arr, $columnKey, $indexKey = null)
PHP 5.4 이하 환경에서도 array_column() 기능을 사용할 수 있는 폴백 함수입니다.
$posts = $db->rows("SELECT id, title FROM `{$db->table('posts')}`");
$titles = dx_array_column($posts, 'title'); // 제목 배열 추출
$indexed = dx_array_column($posts, 'title', 'id'); // id를 키로 사용
9.2 날짜 처리
dx_date($datetime, $format = 'Y-m-d')
timestamp 또는 날짜 문자열을 지정 포맷으로 반환합니다. 유효하지 않은 값이면 빈 문자열을 반환합니다.
echo dx_date($row['created_at']); // '2025-04-24'
echo dx_date($row['created_at'], 'Y년 m월 d일'); // '2025년 04월 24일'
echo dx_date($row['created_at'], 'Y-m-d H:i'); // '2025-04-24 14:30'
// timestamp도 처리 가능
echo dx_date(time(), 'H:i:s'); // '14:30:45'
// null/빈값은 빈 문자열 반환
echo dx_date(null); // ''
echo dx_date(''); // ''
9.3 파일 크기 및 업로드
dx_filesize($bytes)
echo dx_filesize(512); // '512B'
echo dx_filesize(2048); // '2KB'
echo dx_filesize(1536000); // '1.5MB'
echo dx_filesize(2147483648); // '2GB'
dx_upload_url($path) / dx_upload_exists($path)
// 업로드 파일 URL 생성
echo dx_upload_url('2025/04/image.jpg');
// 'https://example.com/data/uploads/2025/04/image.jpg'
// 업로드 파일 존재 확인
if (dx_upload_exists('2025/04/image.jpg')) {
echo '<img src="' . dx_upload_url('2025/04/image.jpg') . '">';
}
10. 페이지네이션
10.1 dx_pagination($total, $perPage, $current, $urlPattern, $range = 5)
전체 데이터 수, 페이지당 수, 현재 페이지, URL 패턴을 받아 HTML 페이지 버튼 문자열을 반환합니다.| 파라미터 | 타입 | 설명 |
|---|---|---|
| $total | int | 전체 데이터 수 |
| $perPage | int | 페이지당 표시 수 |
| $current | int | 현재 페이지 번호 (1부터 시작) |
| $urlPattern | string | {page} 플레이스홀더를 포함한 URL 패턴 |
| $range | int | 표시할 페이지 버튼 수 (기본 5) |
$total = 253; // 전체 게시글 수
$perPage = 20; // 페이지당 20개
$page = dx_get('page', 1, 'int');
// {page} 가 페이지 번호로 치환됨
$pager = dx_pagination(
$total,
$perPage,
$page,
dx_base_url('board/notice?page={page}'),
5 // 페이지 버튼 5개 표시
);
echo '<div class="dx-pagination">' . $pager . '</div>';
// 출력 결과 HTML 예시:
// <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=3" class="dx-page-btn">›</a>
11. 게시판 데이터 헬퍼
11.1 dx_board_posts($boardKey, ...) — 게시글 배열 반환
홈페이지 메인 위젯•사이드바에서 특정 게시판의 최신 게시글을 배열로 가져옵니다.파라미터
| 파라미터 | 기본값 | 설명 |
|---|---|---|
| $boardKey | (필수) | 게시판 고유 키 (예: notice, free, gallery) |
| $limit | 5 | 가져올 게시글 수 |
| $withNotice | false | true이면 공지글 포함 |
| $titleLen | 0 | 제목 최대 글자수 (0이면 자르지 않음) |
| $excerptLen | 100 | 본문 요약 글자수 (0이면 요약 없음) |
반환 배열 구조
// 반환 배열 각 항목
$post = [
'id' => 123, // 게시글 ID
'title' => '게시글 제목', // 제목 (titleLen 적용)
'author' => '홍길동', // 작성자 (익명이면 '익명')
'date' => '2025-04-24', // 작성일 (Y-m-d)
'date_short' => '04.24', // 작성일 짧게 (m.d)
'url' => 'https://...', // 게시글 상세 URL
'board_key' => 'notice', // 게시판 키
'view_count' => 42, // 조회수
'comment_count' => 5, // 댓글수
'excerpt' => '본문 요약...', // 요약 (excerptLen > 0 이면)
];
사용 예
// 공지사항 최신 5개
$notices = dx_board_posts('notice');
// 자유게시판 10개 (공지 포함, 제목 30자, 요약 150자)
$posts = dx_board_posts('free', 10, true, 30, 150);
// 갤러리 6개
$gallery = dx_board_posts('gallery', 6);
// 출력
foreach ($posts as $post) {
echo '<a href="' . dx_safe_url($post['url']) . '">';
echo dx_esc($post['title']) . '</a>';
echo '<span>' . dx_esc($post['date_short']) . '</span>';
}
11.2 dx_board_latest($boardKey, ...) — 스킨으로 직접 출력
게시글을 가져와 지정한 스킨 파일로 바로 렌더링합니다. dx_board_posts()와 달리 HTML을 직접 echo합니다.
스킨 파일 탐색 순서
- 1순위: themes/{현재테마}/board_latest/{스킨명}.php
- 2순위: themes/default/board_latest/{스킨명}.php
- 3순위: themes/default/board_latest/list.php (최종 폴백)
스킨 파일 내부에서 사용 가능한 변수
| 변수명 | 설명 |
|---|---|
| $board_key | 게시판 키 (예: notice) |
| $board_name | DB에 저장된 게시판 이름 |
| $title | 섹션 표시 제목 (지정 없으면 board_name) |
| $icon | 아이콘 이모지 |
| $more_url | 더보기 링크 URL |
| $posts | dx_board_posts() 반환 배열 |
| $show_excerpt | 요약 표시 여부 (bool) |
사용 예
// 기본 list 스킨으로 공지사항 5개 출력
dx_board_latest('notice');
// card 스킨, 섹션 제목과 아이콘 지정
dx_board_latest('free', 5, 'card', '자유게시판', '💬');
// 제목 25자 자르기 + 요약 포함
dx_board_latest('gallery', 6, 'simple', '갤러리', '🖼️', 25, true);
// 테마 파일(themes/default/home.php) 안에서 사용
// <?php dx_board_latest('notice', 5, 'list', '공지사항', '📢'); ?>
12. 에셋 및 폰트 출력 함수
12.1 dx_head_assets($opts = [])
모든 레이아웃 <head> 에서 호출하면 Pretendard, Space Grotesk, Font Awesome 6, dxb-css.js를 자동 삽입합니다.
// 기본 (모두 로드)
dx_head_assets();
// 선택적 로드
dx_head_assets([
'pretend' => true, // Pretendard 폰트
'grotesk' => true, // Space Grotesk 폰트
'fa' => true, // Font Awesome 6
'dxb' => true, // dxb-css.js (DXB 유틸리티 CSS 엔진)
]);
// Font Awesome 제외
dx_head_assets(['fa' => false]);
// 레이아웃 파일 head 섹션 예시
// <head>
// <meta charset="UTF-8">
// <?php dx_head_assets(); ?>
// <?php dx_font_css(); ?>
// </head>
12.2 dx_font_css()
CSS 리셋 + Pretendard/Space Grotesk 폰트 변수 정의 + 스크롤바 스타일을 <style> 태그로 출력합니다.
// 레이아웃 <head> 또는 <style> 바로 다음에 호출
dx_font_css();
// 출력 내용:
// * 박스 모델 리셋 (box-sizing: border-box)
// * :root CSS 변수: --font-body, --font-head, --font-mono
// * body font-family 설정
// * a 기본 스타일 제거
// * img 반응형 설정
// * 스크롤바 스타일 (webkit)
// * 텍스트 선택 색상
// * .sg 클래스 (Space Grotesk 단축)
12.3 _dxAvaColor($name) — 아바타 색상 헬퍼
회원 이름을 해싱하여 8가지 고정 색상 중 하나를 반환합니다. 이미지 없는 회원의 텍스트 아바타 배경색 생성에 사용합니다.
$color = _dxAvaColor($member['name']);
// → '#3b82f6' (이름에 따라 항상 동일한 색상 반환)
// 아바타 출력 예시
$bg = _dxAvaColor($member['name']);
$initial = mb_substr($member['name'], 0, 1, 'UTF-8');
echo '<div class="avatar" style="background:' . $bg . '">' . dx_esc($initial) . '</div>';
13. 로그•Ajax•플러그인 로더
13.1 dx_log($message, $level = 'info')
에러와 경고만 data/error.log 에 기록합니다. info/debug 레벨은 기록하지 않습니다. 호출 파일과 라인 번호가 자동 기록됩니다.
// 에러 기록 (기록됨)
dx_log('DB 쿼리 실패: ' . $e->getMessage(), 'error');
// 경고 기록 (기록됨)
dx_log('파일 업로드 실패: ' . $filename, 'warning');
// info/debug는 기록 안 됨 (성능 보호)
dx_log('게시글 조회: ' . $postId, 'info'); // 기록 안 됨
// 로그 파일 형식:
// [2025-04-24 14:30:45][ERROR][boards/handler.php:123] DB 쿼리 실패: ...
13.2 dx_is_ajax()
현재 요청이 XMLHttpRequest(Ajax) 인지 확인합니다. X-Requested-With: XMLHttpRequest 헤더를 기준으로 판단합니다.
if (dx_is_ajax()) {
// Ajax 요청 → JSON 응답
dx_json(['success' => true, 'html' => $html]);
} else {
// 일반 요청 → 페이지 리다이렉트
dx_redirect_back();
}
13.3 load_plugins()
DX_PLUGINS 디렉토리에서 각 플러그인의 plugin.php를 자동으로 require_once합니다. 플러그인 로드 후 DXB CSS 엔진 주입과 에디터 훅 브릿지를 자동 설정합니다.
// index.php 부팅 시 자동 호출됨 (직접 호출 불필요)
load_plugins();
// 플러그인 디렉토리 구조:
// plugins/
// my_plugin/
// plugin.php ← 이 파일이 자동 로드됨
// another_plugin/
// plugin.php
// DXB CSS 엔진 자동 주입 (테마 수정 불필요):
// - dx_head 훅 (priority=1): dxb-css.js 로드 + 1차 rescan
// - dx_body_bottom 훅 (priority=1): 2차 rescan (rAF 포함)
// - dx_admin_top 훅 (priority=1): 관리자 영역 rescan
14. 포인트•경험치 훅 자동 등록
14.1 _dx_register_point_hooks()
게시글 작성, 댓글, 로그인, 가입, 좋아요 등의 이벤트에 포인트/경험치를 자동 부여하는 훅을 등록합니다. DxPoint 클래스 로드 후 자동 실행됩니다.| 훅 이름 | 발동 시점 | 지급 포인트 종류 |
|---|---|---|
| dx_after_write | 게시글 작성 완료 | write (글쓰기 포인트 + 경험치) |
| dx_after_comment | 댓글 작성 완료 | comment (댓글 포인트 + 경험치) |
| dx_after_login | 로그인 성공 (일 1회) | login (출석 포인트 + 경험치) |
| dx_after_register | 신규 회원 가입 | signup (가입 축하 포인트 + 경험치) |
| dx_after_like | 좋아요 받음 | like_recv (좋아요 받기 포인트 + 경험치) |
포인트 설정
실제 지급 포인트 수량은 관리자 > 포인트 설정에서 각 항목별로 설정합니다.
함수 내 두 번째 인자 0은 설정값을 DB에서 자동으로 읽어오기 위한 플레이스홀더입니다.
로그인 포인트는 오늘 이미 받은 경우 point_log 테이블을 확인하여 중복 지급을 방지합니다.
15. 실전 통합 활용 패턴
15.1 게시글 목록 페이지 전체 패턴
<?php
// 1. 입력값 안전 수신
$page = dx_get('page', 1, 'int');
$keyword = dx_get('keyword');
$catId = dx_get('cat', 0, 'int');
$perPage = 20;
// 2. 데이터 조회
$db = Database::getInstance();
$total = (int)$db->value(
"SELECT COUNT(*) FROM posts WHERE board_id=? AND status=1",
[$boardId]
);
$offset = ($page - 1) * $perPage;
$rows = $db->rows(
"SELECT * FROM posts WHERE board_id=? AND status=1
ORDER BY id DESC LIMIT {$perPage} OFFSET {$offset}",
[$boardId]
);
// 3. 페이지네이션 생성
$pager = dx_pagination(
$total, $perPage, $page,
dx_base_url($boardKey . '?page={page}&keyword=' . urlencode($keyword))
);
// 4. 날짜•제목 포맷
foreach ($rows as &$row) {
$row['date_f'] = dx_date($row['created_at'], 'Y-m-d');
$row['date_ago'] = dx_time_ago($row['created_at']);
$row['title_s'] = dx_substr($row['title'], 30);
}
// 5. 출력 (스킨 파일에서)
foreach ($rows as $row) {
echo '<a href="' . dx_safe_url(dx_base_url($boardKey.'/'.$row['id'])) . '">';
echo dx_esc($row['title_s']);
echo '</a> ' . dx_esc($row['date_ago']);
}
echo '<div class="pagination">' . $pager . '</div>';
15.2 Ajax POST 처리 핸들러 패턴
<?php
// Ajax 전용 핸들러 파일
// 1. CSRF 검증 (가장 먼저)
dx_csrf_check();
// 2. 로그인 확인
if (!Auth::getInstance()->isLoggedIn()) {
dx_json(['success' => false, 'message' => '로그인이 필요합니다.'], 401);
}
// 3. 입력값 수신
$postId = dx_post('post_id', '0', 'bigint');
$comment = dx_post('comment');
// 4. 유효성 검사
if (empty($comment)) {
dx_json(['success' => false, 'message' => '댓글 내용을 입력하세요.'], 422);
}
// 5. DB 처리
try {
$db = Database::getInstance();
$id = $db->insert($db->table('comments'), [
'post_id' => $postId,
'member_id' => Auth::getInstance()->user()['id'],
'content' => DxSanitizer::cleanText($comment),
'ip' => dx_ip(),
'created_at' => date('Y-m-d H:i:s'),
]);
dx_json(['success' => true, 'comment_id' => $id]);
} catch (Exception $e) {
dx_log('댓글 저장 실패: ' . $e->getMessage(), 'error');
dx_json(['success' => false, 'message' => '저장 중 오류가 발생했습니다.'], 500);
}
15.3 홈페이지 메인 위젯 패턴
<?php // themes/default/home.php ?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<?php dx_head_assets(); ?>
<?php dx_font_css(); ?>
</head>
<body>
<!-- 공지사항 위젯 (list 스킨) -->
<?php dx_board_latest('notice', 5, 'list', '공지사항', '📢'); ?>
<!-- 자유게시판 위젯 (card 스킨, 제목 25자, 요약 포함) -->
<?php dx_board_latest('free', 6, 'card', '자유게시판', '💬', 25, true); ?>
<!-- PHP 배열로 받아서 직접 처리 -->
<?php
$gallery = dx_board_posts('gallery', 8, false, 20);
foreach ($gallery as $p) {
echo '<div class="card">';
echo ' <a href="' . dx_safe_url($p['url']) . '">' . dx_esc($p['title']) . '</a>';
echo ' <span>' . dx_esc($p['date_short']) . '</span>';
echo '</div>';
}
?>
</body>
</html>
16. 전체 공통함수 빠른 참조표
| 함수명 | 카테고리 | 반환 타입 | 비고 |
|---|---|---|---|
| dx_random_bytes($length) | 보안 난수 | string (bytes) | PHP 5.6+ 자동 폴백 |
| dx_random_hex($length=32) | 보안 난수 | string | 16진수 토큰 생성 |
| dx_realpath($path) | 경로 | string|false | 역슬래시 → 슬래시 |
| dx_path_inside($path, $base) | 경로 | bool | 경로 탈출 방어 |
| dx_is_https() | HTTPS | bool | 프록시/CDN 포함 |
| dx_base_url($path='') | URL | string | URL Rewrite 자동 |
| dx_static_url($path='') | URL | string | 정적 자산 URL |
| dx_request_uri() | URL | string | 이중슬래시 정규화 |
| dx_current_url() | URL | string | 현재 완전 URL |
| dx_redirect($url, $code=302) | URL | void+exit | Location 헤더 |
| dx_redirect_back($fallback='/') | URL | void+exit | Referer 기반 |
| dx_get($key, $default, $type) | 요청 | mixed | $_GET 안전 수신 |
| dx_post($key, $default, $type) | 요청 | mixed | $_POST 안전 수신 |
| dx_request($key, $default, $type) | 요청 | mixed | $_REQUEST 안전 수신 |
| dx_method($method) | 요청 | bool | HTTP 메서드 비교 |
| dx_cast($val, $type) | 타입 | mixed | 타입 변환 헬퍼 |
| dx_ip() | IP | string | Cloudflare 포함 |
| dx_ua() | UA | string | UA 문자열 (500자) |
| dx_device() | 디바이스 | string | mobile|tablet|pc |
| dx_os() | 디바이스 | string | OS 이름 |
| dx_browser() | 디바이스 | string | 브라우저 이름 |
| dx_config($key, $default) | 설정 | mixed | $dx_config 읽기 |
| dx_set_config($key, $value) | 설정 | void | 런타임 설정 변경 |
| dx_config_set($key, $value) | 설정 | void | dx_set_config 별칭 |
| dx_esc($str) | 보안 | string | XSS 방어 출력 |
| dx_safe_url($url) | 보안 | string | URL XSS 방어 |
| dx_csrf_token() | 보안 | string | CSRF 토큰 반환 |
| dx_csrf_field() | 보안 | string | hidden input 반환 |
| dx_csrf_check() | 보안 | void | 검증 실패 시 403 |
| dx_error($msg, $code=500) | 응답 | void+exit | HTTP 오류 출력 |
| dx_json($data, $code=200) | 응답 | void+exit | JSON API 응답 |
| dx_flash($msg, $type) | 플래시 | void | 세션 메시지 저장 |
| dx_set_flash($msg, $type) | 플래시 | void | dx_flash 별칭 |
| dx_get_flash() | 플래시 | array|null | 읽고 세션 삭제 |
| dx_substr($str, $len, $suffix) | 문자열 | string | UTF-8 안전 |
| dx_mb_substr($str, $start, $len) | 문자열 | string | mb_substr 폴백 |
| dx_time_ago($datetime) | 날짜 | string | '방금 전' 형식 |
| dx_date($datetime, $format) | 날짜 | string | 날짜 포맷 |
| dx_filesize($bytes) | 파일 | string | B/KB/MB/GB 표시 |
| dx_upload_url($path) | 파일 | string | uploads URL |
| dx_upload_exists($path) | 파일 | bool | 파일 존재 확인 |
| dx_array_column($arr, $key, $idx) | 배열 | array | PHP 5.4 폴백 |
| dx_pagination(...) | 페이지 | string (HTML) | 페이지 버튼 HTML |
| dx_board_posts($key, ...) | 게시판 | array | 게시글 배열 반환 |
| dx_board_latest($key, ...) | 게시판 | void (echo) | 스킨으로 직접 출력 |
| dx_head_assets($opts) | 에셋 | void (echo) | CDN 폰트/아이콘 |
| dx_font_css() | 에셋 | void (echo) | CSS 리셋+폰트 변수 |
| _dxAvaColor($name) | 에셋 | string (#hex) | 아바타 색상 |
| dx_log($message, $level) | 로그 | void | error/warning만 기록 |
| dx_is_ajax() | Ajax | bool | XHR 요청 감지 |
| load_plugins() | 플러그인 | void | plugin.php 자동 로드 |