회원가입 | 고객센터 |
DESIGNONEX
dxcms.kr
로그인 회원가입
고객센터
4.1 CMS 아키텍처

데이터 흐름 연결

D DX
2026.04.21 01:04(수정됨) 130 0

1. 데이터 흐름 개요

DXCMS의 데이터는 HTTP 요청이 들어오는 순간부터 HTML 응답이 나가는 순간까지 여러 계층을 거쳐 변환됩니다. 이 문서는 입력(Input) → 검증(Validate) → 처리(Process) → 저장(Persist) → 응답(Output)의 5단계 데이터 흐름을 각 시나리오별로 추적합니다.


1.1 데이터 흐름의 5단계

단계 이름 설명
① Input 입력 수집 HTTP Request에서 데이터 수집. dx_get(), dx_post(), $_FILES, $_SESSION, $_COOKIE. BIGINT ID는 ctype_digit() 검증 후 문자열 유지 (32bit PHP 오버플로우 방지)
② Validate 입력 검증 보안 검증(CSRF, XSS, 권한), 형식 검증(타입 캐스팅, 정규식), 비즈니스 규칙 검증(중복 확인, 잠금 상태). 실패 시 dx_error() 또는 dx_json() 즉시 반환
③ Process 데이터 처리 DxSanitizer로 정제, 비즈니스 로직 실행, DxCache 조회/무효화, 훅(Hook) 발생
④ Persist 저장/읽기 Database(PDO) Prepared Statement로 MySQL 읽기·쓰기. BIGINT PK는 insertWithMicrotimeId()로 생성
⑤ Output 응답 생성 HTML(renderWithLayout), JSON(dx_json), Redirect(dx_redirect), 파일 다운로드. 캐시 무효화 후 응답


1.2 데이터 흐름 전체 지도

HTTP Request
    │
    ▼ ① Input 수집
┌──────────────────────────────────────────────────────────────┐
│  dx_get() / dx_post() / $_FILES / $_SESSION / $_COOKIE      │
│  BIGINT: ctype_digit() 검증 후 문자열 유지 (32bit 안전)     │
└───────────────────────────┬──────────────────────────────────┘
                            │
    ▼ ② Validate 검증
┌──────────────────────────────────────────────────────────────┐
│  Secure::csrfCheck()    — CSRF 토큰 검증 (POST 필수)         │
│  Auth::isLoggedIn()     — 인증 상태 확인                    │
│  Auth::isAdmin()        — 권한 레벨 확인                    │
│  DxSanitizer::text()    — 입력 정제 / XSS 방어              │
│  비즈니스 규칙          — 중복 확인, 잠금 상태 등            │
└───────────────────────────┬──────────────────────────────────┘
                            │
    ▼ ③ Process 처리
┌──────────────────────────────────────────────────────────────┐
│  HookManager::run('dx_board_before_save')  — 저장 전 훅     │
│  DxCache::get()                            — 캐시 조회      │
│  DxCategory / DxSeo / DxPoint              — 서비스 처리    │
│  DxBoardSkin::resolveView()                — 스킨 탐색      │
└───────────────────────────┬──────────────────────────────────┘
                            │
    ▼ ④ Persist 저장/읽기
┌──────────────────────────────────────────────────────────────┐
│  Database::insertWithMicrotimeId()  — BIGINT PK 생성 + INSERT│
│  Database::rows() / row() / value() — SELECT (Prepared)     │
│  Database::updateRow()              — UPDATE                │
│  @unlink()                          — 파일 시스템 삭제       │
│  DxCache::deletePrefix()            — 캐시 무효화           │
└───────────────────────────┬──────────────────────────────────┘
                            │
    ▼ ⑤ Output 응답
┌──────────────────────────────────────────────────────────────┐
│  HTML: _brd_render() → DxTheme → layout/main.php            │
│  JSON: dx_json(array) → Content-Type: application/json      │
│  Redirect: dx_redirect() → Location 헤더 + JS 폴백          │
│  훅 실행: dx_after_write, dx_board_after_save, dx_bottom     │
└──────────────────────────────────────────────────────────────┘
    │
    ▼ HTTP Response


2. 입력(Input) — 데이터 수집 계층

모든 외부 입력은 반드시 헬퍼 함수를 통해 추상화됩니다. raw $_GET/$_POST에 직접 접근하지 않고 타입 캐스팅과 기본값 처리를 함께 수행합니다.


2.1 입력 수집 헬퍼 함수

함수 소스 동작 및 특이사항
dx_get($key, $default, $type) $_GET GET 파라미터 추출. dx_cast()로 타입 캐스팅
dx_post($key, $default, $type) $_POST POST 파라미터 추출. dx_cast()로 타입 캐스팅
dx_request($key, $default, $type) $_REQUEST GET+POST 통합 추출
dx_cast($val, 'bigint') 내부 함수 32bit PHP 오버플로우 방지. ctype_digit() 검증 후 문자열 반환. (int) 캐스팅 절대 금지
dx_ip() $_SERVER Cloudflare/CDN/리버스프록시 헤더 우선 순위로 실제 IP 추출
$_SESSION[sessionKey()] 세션 Secure::getSessionKey()가 반환하는 동적 키 이름 사용 (사이트별 고유)
$_COOKIE['dx_remember'] 쿠키 Remember Me 자동 로그인. member_id:token 형식. 만료 확인 후 토큰 롤링 갱신


2.2 BIGINT ID 처리 — 32bit PHP 안전 패턴

post_id, comment_id 등 BIGINT 컬럼은 16자리 숫자입니다. 32bit PHP에서 (int) 캐스팅 시 오버플로우가 발생하므로 문자열 그대로 사용합니다.
 
// ❌ WRONG — 32bit PHP에서 16자리 BIGINT가 음수로 변환됨
$id = (int)$_GET['id'];   // → 오버플로우 버그

// ✅ CORRECT — ctype_digit() 검증 후 문자열 유지
$id = dx_cast($_GET['id'], 'bigint');  // → '1746000000123456' (문자열)

// Router에서의 처리 (Router.php)
$this->current['id'] = ($third && ctype_digit($third)) ? $third : '0';

// handler.php에서의 처리
$_rawId = isset($GLOBALS['dx_route']['id']) ? $GLOBALS['dx_route']['id'] : '0';
$id = (is_string($_rawId) && ctype_digit($_rawId) && $_rawId !== '0') ? $_rawId : '';

// PDO 바인딩 시 EMULATE_PREPARES=true로 문자열을 BIGINT로 자동 처리
PDO::ATTR_EMULATE_PREPARES  => true,
PDO::ATTR_STRINGIFY_FETCHES => true,  // BIGINT 결과도 문자열로 수신


2.3 세션 키 동적 생성 — 예측 불가 구조

세션 키 이름은 설치 시 생성된 64자리 secret_key를 기반으로 동적으로 생성됩니다. 소스코드가 공개되어도 세션 키 이름을 예측할 수 없습니다.
 
// Secure.php — initSecretKeys()
// secret_key(64자리) → SHA-256 → 앞 16자리를 세션 키 이름으로 사용
$this->sessionKey = 'dx_' . substr(hash('sha256', $secret), 0, 16);
// 결과 예: 'dx_a1b2c3d4e5f6a7b8' (사이트마다 다름)

// Auth.php — 세션 읽기/쓰기
$_SESSION[$this->sessionKey()] = [
    'id'    => $user['id'],
    'token' => $this->makeToken($user),  // HMAC-SHA256
];

// 세션 토큰 생성 (Auth::makeToken)
// user.id + user.join_date(불변값) → secret_key로 HMAC-SHA256
$payload = $user['id'] . '|' . $user['join_date'];
return hash_hmac('sha256', $payload, $secret);


3. 검증(Validate) — 보안 및 규칙 검증

입력된 데이터는 세 겹의 검증을 통과해야 합니다. 보안 검증(CSRF•인증•권한), 형식 검증(타입•정규식), 비즈니스 규칙 검증(중복•잠금) 순서로 실행됩니다. 어느 단계에서든 실패하면 즉시 오류 응답이 반환됩니다.


3.1 CSRF 토큰 검증 흐름

// 모든 POST 요청의 첫 번째 줄 (API, handler.php 공통)
dx_csrf_check();  // 실패 시 403 exit (Secure::csrfCheck() 위임)

// Secure::csrfCheck() 내부 동작
$token  = $_POST['_csrf'] ?? '';                 // 폼에서 전송된 토큰
$stored = $_SESSION[$this->csrfKey];               // 세션에 저장된 토큰
if (!hash_equals($stored, $token)) {
    http_response_code(403); exit('CSRF 토큰 오류');
}
// 검증 후 새 토큰 재발급 (토큰 교체로 CSRF 재사용 공격 방지)
// v5.2.4: AJAX 응답에 new_csrf 포함하여 JS가 갱신 가능


3.2 인증 검증 흐름

검증 함수 내부 동작
Auth::getInstance() 생성자에서 loadSession() 호출. 세션 → DB 회원 조회 → 토큰 검증. 실패 시 자동 logout()
loadSession() 세션 키 확인 → ② DB에서 member status=1 확인 → ③ HMAC 토큰 일치 확인. 셋 중 하나라도 실패 시 로그아웃
tryRememberMe() 세션 없을 때 dx_remember 쿠키 확인. member_id:token 파싱 → DB 토큰 비교(hash_equals) → 만료 확인 → 성공 시 세션 복구 + 토큰 롤링
isLoggedIn() $this->user !== null 반환. O(1) 확인
isAdmin() $this->user['role'] === 'admin' 확인
get($field, $default) 로그인 회원 정보 필드 반환. 비로그인 시 $default 반환


3.3 DxSanitizer — 입력 정제 흐름

모든 사용자 입력은 저장 전에 DxSanitizer를 통해 정제됩니다. 에디터 콘텐츠(HTML)와 일반 텍스트는 다른 정제 방식을 사용합니다.
 
메서드 처리 내용
DxSanitizer::text($val) trim + 허용 태그 모두 제거. 일반 텍스트 입력(제목, 태그, 카테고리 등)에 사용
DxSanitizer::editorContent($html) 에디터 HTML 정제. 허용 태그 화이트리스트, on* 이벤트 핸들러 제거, blocked: 프로토콜 차단. CKEditor/Jodit/TinyMCE 출력물에 적용
dx_esc($str) htmlspecialchars(ENT_QUOTES, UTF-8). 출력 시 XSS 방어. 테마 파일에서 모든 동적 출력에 사용
dx_safe_url($url) blocked:/blocked:/data: 프로토콜 차단. 서브디렉토리 설치 시 절대경로 자동 변환
DxSanitizer::filename($name) 이중확장자(shell.php.jpg) 차단. 특수문자 제거. 업로드 파일명 정규화


3.4 권한 검증 데이터 흐름

// handler.php — 게시글 작성 권한 검증
$wl = (int)$board['write_level'];  // 0=전체, 1=회원, 9=관리자
if ($wl===1 && !$auth->isLoggedIn()) {
    dx_redirect(dx_base_url('auth/login') . '?redirect=' . urlencode(dx_current_url()));
    exit;
}
if ($wl===9 && !$auth->isAdmin()) {
    dx_error('관리자만 글쓰기 가능합니다.', 403);
}

// 게시글 수정 권한 검증 (작성자 또는 관리자)
$ok = $auth->isAdmin()
   || ($auth->isLoggedIn() && $auth->user()['id'] == $editPost['member_id']);
if (!$ok) dx_error('수정 권한이 없습니다.', 403);

// 비밀글 접근 권한
if (!empty($post['is_secret'])) {
    $ok = $auth->isAdmin()
       || ($auth->isLoggedIn() && $auth->user()['id'] == $post['member_id']);
    if (!$ok) dx_error('비밀글입니다.', 403);
}


4. Database — 데이터베이스 읽기/쓰기 흐름

모든 DB 접근은 Database 클래스의 PDO Prepared Statement를 통해 이루어집니다. SQL Injection을 원천 차단하며, BIGINT 타입 안전을 보장합니다.


4.1 쿼리 실행 파이프라인

// Database::execute() — 모든 쿼리의 공통 실행 경로
$this->queryCount++;

if (DX_DEBUG) {
    $this->queryLog[] = ['sql' => $sql, 'params' => $params];
}

try {
    $stmt = $this->pdo->prepare($sql);   // Prepared Statement 생성
    $stmt->execute((array)$params);       // 바인딩 + 실행
    return $stmt;
} catch (PDOException $e) {
    if ($GLOBALS['DX_SHUTTING_DOWN']) throw $e;  // shutdown 중 재throw
    if (DX_DEBUG) dx_error('DB 오류: ' . $sql);
    else          dx_error('데이터베이스 오류가 발생했습니다.');
}

// PDO 연결 옵션 (32bit 안전)
PDO::ATTR_EMULATE_PREPARES  => true,   // BIGINT 오버플로우 방지
PDO::ATTR_STRINGIFY_FETCHES => true,   // 모든 숫자를 문자열로 수신
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,


4.2 BIGINT PK 생성 — insertWithMicrotimeId()

게시글(posts), 댓글(comments) 등 BIGINT PK 테이블은 AUTO_INCREMENT 대신 microtime 기반 고유 ID를 생성합니다. 분산 환경에서 충돌을 방지하고, 삽입 순서를 시간순으로 유지합니다.
 
// Database::generateMicrotimeId()
// 13자리 밀리초 타임스탬프 + 랜덤 3자리 = 최대 16자리
$mt  = microtime(true);
$sec = floor($mt);
$ms  = round(($mt - $sec) * 1000);
$msStr = sprintf('%010d%03d', $sec, $ms);   // 13자리 문자열
$rndStr = sprintf('%03d', rand(0, 999));     // 3자리 랜덤
$id = $msStr . $rndStr;                       // 16자리 문자열 (절대 (int) 금지!)

// 충돌 확인 후 재시도 (최대 10회)
$exists = $db->value("SELECT COUNT(*) FROM `{$tbl}` WHERE id = ?", [$id]);
if (!$exists) return $id;  // 문자열 반환 — 32bit PHP 안전
usleep(1000);  // 1ms 대기 후 재시도


4.3 Database 메서드 호출 패턴

메서드 SQL 사용 시나리오
row($sql, $params) SELECT … LIMIT 1 게시글 단건, 회원 조회, 게시판 설정 로드
rows($sql, $params) SELECT … 목록 조회 (게시글 목록, 댓글 목록, 파일 목록)
value($sql, $params) SELECT 단일값 COUNT(*), 인기점수 계산값, 단일 컬럼
insertRow($table, $data) INSERT 알림, 방문자 로그, 설정 저장 등 AUTO_INCREMENT 테이블
insertWithMicrotimeId($table, $data) INSERT (BIGINT PK) 게시글, 댓글 저장. microtime ID 자동 생성
updateRow($table, $data, $where) UPDATE 조회수 +1, 좋아요 수 갱신, 회원 정보 수정
deleteRow($table, $where) DELETE 단순 조건 삭제 (편의 메서드)
query($sql, $params) DML (영향 행 수) 복잡한 UPDATE/DELETE, popular_score 갱신
begin() / commit() / rollback() 트랜잭션 다중 테이블 원자적 처리
tableExists($name) SHOW TABLES LIKE 마이그레이션 전 컬럼/테이블 존재 여부 확인


4.4 QueryBuilder — 라라벨 스타일 체이닝

기존 Database 클래스와 완전히 하위호환하면서 라라벨 스타일의 체이닝 쿼리를 지원합니다. dx_db() 전역 함수로 어디서나 접근 가능합니다.
 
// 기본 사용법
$posts = dx_db('posts')
    ->where('status', 1)
    ->where('board_id', $boardId)
    ->orderBy('id', 'desc')
    ->limit(10)
    ->get();

// LEFT JOIN (회원 이름 포함)
$posts = dx_db('posts')
    ->leftJoin('members', 'posts.member_id', '=', 'members.id')
    ->select(['posts.*', 'members.name AS member_name'])
    ->where('posts.status', 1)
    ->paginate(20);  // ['data' => rows, 'total' => count, 'pages' => n]

// 복수 조건
$member = dx_db('members')
    ->where('login_id', $loginId)
    ->orWhere('email', $loginId)
    ->where('status', 1)
    ->first();


5. DxCache — 캐시 데이터 흐름

DxCache는 APCu(메모리) 또는 파일 기반 캐시를 자동 선택합니다. 캐시는 READ 경로에서 DB 조회를 건너뛰게 하고, WRITE 경로에서 즉시 무효화됩니다.


5.1 캐시 READ 흐름

// 게시판 목록 캐시 (handler.php)
$_brdCacheKey = 'board_list_' . $board['board_key'] . '_p' . $page;

// 캐시 적용 조건 (비로그인 + 검색/카테고리/태그 없음 + action=list)
$_brdCacheOk = (
    class_exists('DxCache') &&
    !$search && !$cat && !$tag &&
    !$auth->isLoggedIn() &&
    $action !== 'search'
);

if ($_brdCacheOk) {
    $_brdCached = DxCache::get($_brdCacheKey, null);
    if (is_array($_brdCached)) {
        extract($_brdCached, EXTR_SKIP);  // 캐시에서 변수 복원
        _brd_render('list', $ctx);
        break;  // DB 쿼리 완전 생략
    }
}

// 전역 공지는 실시간 확인 필요 → 캐시에서 제외, 항상 DB 조회


5.2 캐시 WRITE/무효화 흐름

무효화 시점 무효화 대상 코드
게시글 작성/수정 board_list_{key}_* DxCache::deletePrefix('board_list_'.$key.'_')
게시글 완전 삭제 board_list_{key}_* _dx_delete_post_completely()에서 호출
설정 저장 전체 캐시 DxCache::flush() (관리자 설정 저장 시)
카테고리 변경 cat_board_{id} DxCache::deletePrefix('cat_board_')
사이트 설정 저장 site_{md5(domain)} DxCache::deletePrefix('site_')
댓글 삭제 (목록 캐시 미영향) comment_count 감소 + popular_score 재계산만


6. 게시글 CRUD — 전체 데이터 흐름

가장 핵심적인 게시판 기능의 데이터 흐름을 단계별로 추적합니다. 각 단계에서 어떤 데이터가 어느 클래스를 거쳐 어디에 저장되는지 명확히 보여줍니다.


6.1 게시글 작성(Write) 전체 데이터 흐름

① HTTP POST /free/write
   │
   ▼ ② 입력 수집 (handler.php)
   $data = [
       'board_id' => (int)$board['id'],
       'title'    => DxSanitizer::text(dx_post('title')),      // XSS 정제
       'content'  => DxSanitizer::editorContent($_POST['content']),  // HTML 정제
       'category' => DxSanitizer::text(dx_post('category')),
       'tags'     => DxSanitizer::text(dx_post('tags')),
       'is_notice'=> $auth->isAdmin() && !empty($_POST['is_notice']) ? 1 : 0,
       'is_secret'=> !empty($_POST['is_secret']) ? 1 : 0,
       'member_id'=> (int)$auth->user()['id'],
       'ip'       => dx_ip(),
       'status'   => 1,
   ];
   │
   ▼ ③ 검증
   dx_csrf_check();              // CSRF 토큰 검증
   Auth::isLoggedIn()            // 로그인 확인
   DxCaptcha::verify() (비회원)  // 캡챠 검증
   tableExists('posts.link')    // 컬럼 존재 확인 (마이그레이션 호환)
   │
   ▼ ④ 저장 전 훅
   dx_run_hook('dx_board_before_save', ['data' => &$data, 'board' => $board]);
   // 플러그인이 $data를 수정 가능
   │
   ▼ ⑤ DB INSERT (Persist)
   $postId = $db->insertWithMicrotimeId('posts', $data);
   // microtime ID: 예) '1746123456789012'  (16자리 문자열)
   │
   ▼ ⑥ 파일 업로드 처리 (조건부)
   if ($board['use_file'] && isset($_FILES['files']))
       dx_board_upload_files($postId, $board, $_FILES['files']);
   │
   ▼ ⑦ 갤러리 썸네일 자동 생성 (조건부)
   if ($board['board_type'] === 'gallery' && class_exists('DxThumb'))
       DxThumb::autoFromPost($postId, $board['board_key'], $thumbOpts);
   │
   ▼ ⑧ 저장 후 훅 실행
   dx_run_hook('dx_after_write',      ['post_id'=>$postId, 'board'=>$board]);
   // → _dx_register_point_hooks: DxPoint::add('write'), addExp('write')
   dx_run_hook('dx_board_after_save', ['post_id'=>$postId, 'board'=>$board]);
   // → 플러그인: 인덱싱, 알림, 외부 API 연동 등
   │
   ▼ ⑨ 실시간 소켓 브로드캐스트 (조건부)
   $_SESSION['dx_post_live_broadcast'] = ['post_id'=>$postId, 'board_key'=>$key];
   // view.php 로드 시 JS가 소켓서버에 post_live 이벤트 전송
   │
   ▼ ⑩ 캐시 무효화
   DxCache::deletePrefix('board_list_' . $board['board_key'] . '_');
   │
   ▼ ⑪ Redirect
   dx_redirect($board['board_key'] . '/view/' . $postId);


6.2 게시글 목록(List) 데이터 흐름

① HTTP GET /free/list?page=1
   │
   ▼ ② 캐시 확인 (비로그인 + 검색없음 + 기본 페이지만)
   $cacheKey = 'board_list_free_p1';
   $cached = DxCache::get($cacheKey, null);
   if ($cached) { _brd_render('list', ctx); break; }  // DB 조회 생략!
   │
   ▼ ③ DB 쿼리 (캐시 미스 시)
   // 공지글 별도 조회
   SELECT * FROM posts WHERE board_id=? AND status=1 AND is_notice=1
   // 일반 글 페이지네이션
   SELECT p.*, m.name AS member_name FROM posts p
   LEFT JOIN members m ON p.member_id=m.id
   WHERE p.board_id=? AND p.status=1 AND p.is_notice=0
   ORDER BY p.id DESC LIMIT 20 OFFSET 0
   // 전체 공지 (실시간 기간 체크, 캐시 제외)
   SELECT * FROM global_notices WHERE status=1 AND start_at<=? AND end_at>=?
   │
   ▼ ④ 카테고리 목록 (캐시)
   DxCategory::getByBoard($boardId, 'list');
   // 캐시키: cat_board_{id}  TTL: 300초
   │
   ▼ ⑤ 컨텍스트 구성 및 훅
   $ctx = compact('board','posts','notices','total','page','perPage',...);
   dx_run_hook('dx_board_list_context', ['context' => &$ctx, 'board' => $board]);
   │
   ▼ ⑥ 캐시 저장
   DxCache::set($cacheKey, [...$ctx], 60);  // TTL 60초
   // 전체 공지는 캐시에서 제외 (실시간 기간 체크 필요)
   │
   ▼ ⑦ 렌더링
   _brd_render('list', $ctx);  // DxBoardSkin 폴백 체인 → layout/main.php


6.3 게시글 보기(View) 데이터 흐름

① HTTP GET /free/view/1746000000123456
   │
   ▼ ② 입력 수집
   $id = '1746000000123456';  // 문자열 (32bit 안전)
   │
   ▼ ③ DB 조회 (LEFT JOIN으로 회원명 포함)
   SELECT p.*, m.name AS member_name, m.profile_img
   FROM posts p LEFT JOIN members m ON p.member_id=m.id
   WHERE p.id='1746000000123456' AND p.board_id=? AND p.status=1
   │
   ▼ ④ 비밀글 접근 권한 확인
   if ($post['is_secret']) { auth->isAdmin() || 작성자만 허용 }
   │
   ▼ ⑤ 조회수 중복 방지
   $vk = 'viewed_post_' . $id;  // 세션 키
   if (!$_SESSION[$vk]) {
       UPDATE posts SET view_count=view_count+1 WHERE id=?
       // popular_score 재계산: (조회×1 + 좋아요×5 + 댓글×3) × 시간감쇠
       $decay = max(0.1, 1.0 - floor($daysPassed/7) * 0.1);
       UPDATE posts SET popular_score=? WHERE id=?
       $_SESSION[$vk] = 1;  // 중복 방지 플래그
   }
   │
   ▼ ⑥ 관련 데이터 조회
   // 첨부파일
   SELECT * FROM post_files WHERE post_id=? ORDER BY id ASC
   // 댓글 (회원명 포함)
   SELECT c.*, m.name AS member_name FROM comments c
   LEFT JOIN members m ON c.member_id=m.id
   WHERE c.post_id=? AND c.status=1 ORDER BY c.id ASC
   // 이전글/다음글
   SELECT id,title FROM posts WHERE board_id=? AND status=1 AND id<? ORDER BY id DESC LIMIT 1
   SELECT id,title FROM posts WHERE board_id=? AND status=1 AND id>? ORDER BY id ASC  LIMIT 1
   │
   ▼ ⑦ SEO 메타 생성
   DxSeo::build('board_view', ['post'=>$post, 'board'=>$board]);
   // title, description(본문앞150자), og:image(첫번째img), JSON-LD Article
   // 비밀글 → noindex=true
   │
   ▼ ⑧ 컨텍스트 구성 및 훅
   dx_run_hook('dx_board_view_context', ['context'=>&$ctx, 'post'=>$post]);
   │
   ▼ ⑨ 렌더링
   _brd_render('view', $ctx);  // DxBoardSkin 폴백 → layout/main.php


6.4 게시글 완전 삭제(Delete) 데이터 흐름

게시글 삭제는 연관 데이터를 모두 정리하는 cascade 삭제입니다. 첨부파일 실제 삭제, 댓글 좋아요, 댓글, 게시글 좋아요, 스크랩, 포인트 로그 순서로 처리됩니다.
 
// _dx_delete_post_completely($db, $postId, $boardKey)
// PDO 직접 사용 — Database::execute()의 dx_error/exit 우회

① 첨부파일 삭제
   SELECT save_path FROM post_files WHERE post_id=?
   → @unlink($realPath)  // 실제 파일 삭제
   → DELETE FROM post_files WHERE post_id=?

② 댓글 좋아요 삭제
   SELECT id FROM comments WHERE post_id=?
   → DELETE FROM likes WHERE target_type='comment' AND target_id=?

③ 댓글 hard delete
   DELETE FROM comments WHERE post_id=?

④ 게시글 좋아요 삭제
   DELETE FROM likes WHERE target_type='post' AND target_id=?

⑤ 스크랩 삭제
   DELETE FROM scraps WHERE post_id=?

⑥ 포인트 로그 삭제
   DELETE FROM point_logs WHERE ref_type='post' AND ref_id=?

⑦ 게시글 hard delete
   DELETE FROM posts WHERE id=?

⑧ 캐시 무효화
   DxCache::deletePrefix('board_list_' . $boardKey . '_');


7. API — AJAX 데이터 흐름

AJAX 요청은 core/api/ 폴더의 PHP 파일로 라우팅됩니다. 모든 API는 JSON을 반환하며, POST 요청은 CSRF 토큰 검증이 필수입니다.


7.1 댓글 등록 데이터 흐름

① HTTP POST /api/comment (AJAX)
   Content-Type: application/x-www-form-urlencoded
   Params: _csrf, post_id, content, parent_id(선택)
   │
   ▼ ② 검증
   dx_csrf_check();
   $postId = dx_post('post_id', '0', 'bigint');  // BIGINT 안전
   $content = DxSanitizer::editorContent($_POST['content']);
   // 게시판 댓글 레벨 확인, 로그인 여부
   │
   ▼ ③ DB INSERT
   $commentId = $db->insertWithMicrotimeId('comments', [
       'post_id'    => $postId,
       'member_id'  => $auth->get('id'),
       'content'    => $content,
       'parent_id'  => $parentId,
       'status'     => 1,
   ]);
   // 게시글 comment_count +1, popular_score 재계산
   UPDATE posts SET comment_count=comment_count+1 WHERE id=?
   │
   ▼ ④ 훅 실행
   dx_run_hook('dx_after_comment', ['comment_id'=>$commentId]);
   // → DxPoint::add('comment'), addExp('comment')
   // → DxNotification::add(게시글 작성자에게 알림)
   │
   ▼ ⑤ JSON 응답
   dx_json([
       'success'   => true,
       'comment_id'=> $commentId,
       'new_csrf'  => Secure::csrfToken(),  // v5.2.4 CSRF 갱신
   ]);


7.2 좋아요 토글 데이터 흐름

① HTTP POST /api/like
   Params: _csrf, post_id
   │
   ▼ ② 검증
   dx_csrf_check();
   $postId = dx_post('post_id', '0', 'bigint');
   // 내 글 좋아요 차단
   $postOwner = $db->value("SELECT member_id FROM posts WHERE id=?", [$postId]);
   if ((int)$postOwner === (int)$memberId) { dx_json(['success'=>false, ...]); }
   │
   ▼ ③ 중복 확인
   // 로그인: member_id로 확인 (IP 오판 방지)
   // 비로그인: ip로만 확인
   $exists = $db->row("SELECT id FROM likes WHERE target_type='post'",
       "AND target_id=? AND member_id=?", [$postId, $memberId]);
   │
   ▼ ④ 토글
   if ($exists) {  // 좋아요 취소
       DELETE FROM likes WHERE id=?
       UPDATE posts SET like_count=GREATEST(0, like_count-1) WHERE id=?
   } else {        // 좋아요 추가
       INSERT INTO likes (target_type, target_id, member_id, ip) VALUES(...)
       UPDATE posts SET like_count=like_count+1 WHERE id=?
       // DxNotification::add(게시글 작성자에게 알림)
       // dx_run_hook('dx_after_like', ['post_id', 'owner_id'])
       // → DxPoint::add(owner, 'like_recv')
   }
   │
   ▼ ⑤ popular_score 재계산
   $raw = view_count*1 + like_count*5 + comment_count*3;
   $decay = max(0.1, 1.0 - floor($daysPassed/7) * 0.1);
   UPDATE posts SET popular_score=? WHERE id=?
   │
   ▼ ⑥ JSON 응답
   dx_json(['success'=>true, 'like_count'=>$newCount, 'liked'=>$liked]);


7.3 파일 업로드 데이터 흐름

① HTTP POST /api/upload (또는 게시글 저장 시 멀티파트)
   Content-Type: multipart/form-data
   │
   ▼ ② 검증 (Secure.php가 전담)
   // MIME 타입 + 확장자 이중 검증
   $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
   $mime = mime_content_type($tmpName);
   // 이중 확장자 차단: shell.php.jpg → 차단
   if (preg_match('/\.php/i', $filename)) → 거부
   // 실제 이미지 검증 (이미지 확장자인 경우)
   $imgInfo = @getimagesize($tmpName);
   if (!$imgInfo) → 거부 (가짜 이미지)
   │
   ▼ ③ 저장 경로 생성
   $savePath = DX_DATA . '/uploads/' . $boardKey . '/' . date('Y/m/');
   @mkdir($savePath, 0755, true);
   $savedName = uniqid() . '.' . $ext;  // 원본 파일명 대신 고유명 사용
   │
   ▼ ④ 파일 이동 + DB 저장
   move_uploaded_file($tmpName, $savePath . $savedName);
   $db->insertRow('post_files', [
       'post_id'   => $postId,
       'orig_name' => $originalName,
       'save_path' => $savePath . $savedName,
       'file_size' => $fileSize,
       'mime_type' => $mime,
   ]);
   │
   ▼ ⑤ 응답 (에디터 인라인 삽입용)
   dx_json(['url' => dx_base_url('api/download?id=' . $fileId)]);


8. 인증 — 로그인/로그아웃/회원가입 데이터 흐름


8.1 로그인 데이터 흐름

① HTTP POST /auth/login
   Params: login_id, password, remember(선택)
   │
   ▼ ② Auth::login($loginId, $password)
   // login_id 또는 email로 조회
   SELECT * FROM members
   WHERE (login_id=? OR email=?) AND status=1 LIMIT 1
   │
   ▼ ③ 비밀번호 검증
   password_verify($password, $user['password'])  // bcrypt
   // 실패 시: login_fail++ (10회 초과 → 계정 잠금)
   UPDATE members SET login_fail=login_fail+1 WHERE id=?
   │
   ▼ ④ 로그인 성공 처리
   UPDATE members SET login_fail=0, last_login=?, last_ip=? WHERE id=?
   // 세션 생성
   $_SESSION[$sessionKey] = [
       'id'    => $user['id'],
       'token' => hash_hmac('sha256', id+join_date, secret_key),
   ];
   // Remember Me 쿠키 발급 (remember_token 컬럼 존재 시)
   UPDATE members SET remember_token=?, remember_expires=? WHERE id=?
   setcookie('dx_remember', $userId.':'.randomHex(64), time()+86400, ...);
   │
   ▼ ⑤ 훅 실행
   dx_run_hook('dx_after_login', ['user' => $user]);
   // → DxPoint::add('login') (오늘 첫 로그인 시만)
   // → DxMemberMonitor::onLogin($userId)
   │
   ▼ ⑥ Redirect → 홈 또는 ?redirect= URL


8.2 회원가입 데이터 흐름

① HTTP POST /auth/register
   Params: login_id, password, password_confirm, name, email, ...
   │
   ▼ ② 검증 (Auth::register())
   // 아이디 형식: /^[a-zA-Z0-9_]{4,20}$/
   // 비밀번호: 8자 이상
   // 중복: $db->exists('members', ['login_id' => $loginId])
   //       $db->exists('members', ['email' => $email])
   │
   ▼ ③ 데이터 준비
   $data['password'] = password_hash($password, PASSWORD_BCRYPT);
   // 화이트리스트: SHOW COLUMNS로 실제 컬럼만 INSERT (SQL Injection 방지)
   $cols = array_column($db->rows("SHOW COLUMNS FROM members"), 'Field');
   $safeData = array_intersect_key($data, array_flip($cols));
   // 가입 경로 수집: IP, UA, device, OS, browser
   │
   ▼ ④ DB INSERT
   $id = $db->insertRow('members', $safeData);
   // 가입과 동시에 자동 로그인 세션 생성
   │
   ▼ ⑤ 훅 실행
   dx_run_hook('dx_after_register', ['user_id' => $id]);
   // → DxPoint::add('signup', 10), addExp('signup', 20)


8.3 Remember Me — 자동 로그인 흐름

// Auth::loadSession() → 세션 없을 때 tryRememberMe() 호출

① dx_remember 쿠키 파싱: "userId:token"
② DB 조회: SELECT * FROM members WHERE id=? AND status=1
③ PHP에서 토큰 비교:
   hash_equals($user['remember_token'], $token)
   // SQL WHERE 절 비교 금지 → 타이밍 공격 방지
④ 만료 확인:
   if (strtotime($user['remember_expires']) < time()) → 쿠키 삭제
⑤ 자동 로그인 성공 → 세션 복구
⑥ 토큰 롤링 갱신:
   $newToken = Secure::randomHex(64);  // 새 토큰 생성
   UPDATE members SET remember_token=?, remember_expires=? WHERE id=?
   setcookie('dx_remember', $userId.':'.newToken, ...);  // 30일 연장
// 매 요청마다 토큰 갱신 → 탈취된 토큰의 사용 기회 최소화


9. 훅(Hook) — 데이터 흐름 연결 메커니즘

훅은 CMS의 각 데이터 처리 단계를 서로 연결하는 핵심 메커니즘입니다. 핵심 파일을 수정하지 않고도 데이터 흐름에 새로운 처리를 삽입할 수 있습니다.


9.1 데이터 흐름 내 훅 발생 지점

훅 이름 유형 데이터 흐름 연결
dx_board_before Action 게시판 핸들러 진입 시. 접근 제한, 로깅 삽입 가능
dx_board_before_save Action (ref) 저장 전 $data 수정 가능. 플러그인이 추가 필드 처리
dx_after_write Action 글 저장 후. → DxPoint::add(write), 외부 인덱싱
dx_board_after_save Action 글 저장 후. 리다이렉트 URL도 변경 가능(ref)
dx_board_list_context Action (ref) 목록 컨텍스트 수정 가능. 광고, 추가 데이터 주입
dx_board_view_context Action (ref) 보기 컨텍스트 수정 가능. 관련글, 추천글 주입
dx_board_before_delete Action 삭제 전. 취소 가능 (exit)
dx_board_after_delete Action 삭제 후. 외부 인덱스 제거
dx_after_comment Action 댓글 저장 후. → DxPoint, DxNotification
dx_after_like Action 좋아요 후. → DxPoint(like_recv), DxNotification
dx_after_login Action 로그인 후. → DxPoint(login), DxMemberMonitor
dx_after_logout Action 로그아웃 후. → DxMemberMonitor
dx_after_register Action 가입 후. → DxPoint(signup)
dx_head Action <head> 내. CSS/JS 추가
dx_body_bottom Action </body> 직전. DxPopup::render(), 스크립트
dx_editor_init Action 에디터 초기화 요청 → dx_editor_render 호출
dx_editor_render Action 에디터 플러그인이 등록. 실제 에디터 HTML 출력


9.2 포인트 데이터 흐름 연결

포인트는 훅을 통해 각 이벤트에 자동으로 연결됩니다. _dx_register_point_hooks()에서 모든 포인트 훅이 일괄 등록됩니다.
 
// STEP 4에서 일괄 등록 (index.php)
_dx_register_point_hooks();

// 각 이벤트 → 포인트/경험치 지급 흐름
dx_after_login    → DxPoint::add(login, +1pt, +2exp)   // 오늘 첫 로그인만
dx_after_write    → DxPoint::add(write, +5pt, +10exp)
dx_after_comment  → DxPoint::add(comment, +2pt, +5exp)
dx_after_like     → DxPoint::add(like_recv, +1pt, +2exp)  // 글 작성자에게
dx_after_register → DxPoint::add(signup, +10pt, +20exp)

// DxPoint::add() 내부 흐름
INSERT INTO point_log (member_id, type, point, ref_type, ref_id, ...)
UPDATE members SET point=point+?, exp=exp+? WHERE id=?
// 레벨업 확인
$newLevel = DxPoint::calcLevel($totalExp);
if ($newLevel > $currentLevel) {
    UPDATE members SET level=? WHERE id=?
}


10. 응답(Output) — 데이터 출력 흐름

처리된 데이터는 세 가지 방식으로 출력됩니다. HTML 렌더링, JSON 응답, Redirect가 있으며 각각 다른 경로를 따릅니다.


10.1 HTML 렌더링 흐름 (_brd_render)

// _brd_render($skinAction, $context)  내부 흐름

① DxBoardSkin::resolveView($skin, $skinAction)
   // 6단계 폴백 체인으로 스킨 파일 탐색

② 스킨 파일 안전 실행 (에러 격리)
   set_error_handler(...);  // notice/warning → 로그만, 계속 실행
   ob_start();
   try {
       extract($context, EXTR_SKIP);  // $context를 변수로 풀기
       include $skinFile;              // 스킨 파일 실행
   } catch (Exception $e) {
       echo '<div class="error">...';  // 에러 박스 표시
   }
   $dx_content = ob_get_clean();  // 스킨 출력 캡처
   restore_error_handler();

③ 레이아웃 파일 실행
   $layoutFile = DxTheme::getInstance()->resolve('layout/main.php');
   extract($context, EXTR_SKIP);  // 레이아웃에 컨텍스트 전달
   require $layoutFile;
   // layout/main.php 내부에서 echo $dx_content; 로 본문 삽입

// layout/main.php 내 데이터 흐름
DxSeo::render()            → <head> SEO 메타 출력
dx_run_hook('dx_head')    → CSS/JS 삽입
echo $dx_content           → 스킨 본문 삽입
dx_run_hook('dx_body_bottom') → 팝업, 분석 스크립트


10.2 JSON 응답 흐름 (dx_json)

// dx_json($data, $code=200)
while (ob_get_level() > 0) ob_end_clean();  // 버퍼 비우기
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;

// 표준 응답 구조
// 성공: {success:true, data:..., new_csrf:'...'(POST 후 CSRF 갱신)}
// 실패: {success:false, message:'...'}, HTTP 4xx/5xx


10.3 Redirect 흐름 (dx_redirect)

// dx_redirect($url, $code=302)
$GLOBALS['DX_SHUTTING_DOWN'] = true;  // DB 오류 시 exit 방지 플래그

// 출력 버퍼 전체 비우기 (카페24 등 공유호스팅 호환)
while (ob_get_level() > 0) ob_end_clean();

// HTTP Location 헤더
if (!headers_sent()) {
    header('Location: ' . $url, true, $code);
}

// 항상 HTML 폴백도 출력 (header() 실패 환경 대응)
echo '<meta http-equiv="refresh" content="0;url=' . $safeUrl . '">';
echo '<script>location.replace(' . json_encode($url) . ');</script>';
exit;


11. 데이터 흐름 연결 전체 매트릭스

주요 시나리오별로 데이터가 어떤 컴포넌트를 거쳐 흐르는지 한눈에 보여주는 매트릭스입니다.
 
시나리오 Input CSRF Auth Sanitize DB Cache Output
게시글 목록 조회 dx_get() isLoggedIn() rows(list,notices) READ HTML
게시글 보기 dx_get(bigint) isLoggedIn() row(post)+JOIN HTML
게시글 작성 dx_post() isLoggedIn() text+editor insertWithId() DELETE Redirect
게시글 삭제 dx_post() isAdmin/owner cascade DELETE DELETE Redirect
댓글 등록 dx_post() isLoggedIn() editorContent insertWithId() JSON
좋아요 토글 dx_post(bigint) 선택 INSERT/DELETE JSON
파일 업로드 $_FILES isLoggedIn() filename insertRow() JSON
로그인 dx_post() text row(member) Redirect
회원가입 dx_post() text insertRow() Redirect
자동 로그인 $_COOKIE tryRememberMe row+UPDATE READ 세션복구

✅  데이터 흐름 핵심 원칙 요약

  1) BIGINT는 문자열로: 모든 BIGINT ID는 (int) 캐스팅 금지, 문자열 유지
  2) 모든 POST는 CSRF 먼저: dx_csrf_check()가 첫 번째 줄
  3) 저장 전 Sanitize: text() 또는 editorContent()로 반드시 정제
  4) 캐시는 Write 즉시 무효화: deletePrefix()로 관련 캐시 일괄 삭제
  5) 훅으로 연결: 포인트•알림•모니터링은 훅으로만 연결, 핵심 파일 수정 없음
  6) 삭제는 cascade: 게시글 삭제 시 파일•댓글•좋아요•스크랩 모두 정리
  7) 에러 격리: 스킨/extend 오류가 전체 응답을 죽이지 않도록 ob_start + try/catch
  8) Redirect 후 DX_SHUTTING_DOWN: 리다이렉트 후 발생하는 DB 오류가 exit로 이어지지 않게 보호

댓글0

로그인 후 댓글을 작성할 수 있습니다.
3.10 모듈 로딩 구조 플러그인 / 확장 로딩 방식 2026.04.21 3.9 공통 함수 / 유틸 재사용 방식 2026.04.21 3.9 공통 함수 / 유틸 공통 클래스 구조 2026.04.21 3.9 공통 함수 / 유틸 전역 함수 구조 2026.04.21 3.8 Extend 구조 실제 적용 흐름 2026.04.21 3.8 Extend 구조 Extend 개념 2026.04.21 3.7 Hook 시스템 Hook 시스템 활용 사례 2026.04.21 3.7 Hook 시스템 실행 타이밍 2026.04.21 3.7 Hook 시스템 Hook 개념 2026.04.21 3.6 데이터 처리 구조 공통 함수 활용 2026.04.21 3.6 데이터 처리 구조 데이터 흐름 상세 기술 2026.04.21 3.6 데이터 처리 구조 DB 접근 방식 2026.04.21 3.5 컨트롤러 구조 컨트롤러 구조 • 데이터 전달 • 실행 방식 • 역할 2026.04.21 3.4 라우팅 시스템 URL 처리 방식 • 라우팅 규칙 • 동적 라이팅 2026.04.21 3.3 실행 흐름 초기 로딩 과정 및 공통 초기화 흐름 2026.04.21 3.2 폴더 구조 install/ — 설치 및 마이그레이션 2026.04.21 3.2 폴더 구조 pages/ — 커스텀 페이지 2026.04.21 3.2 폴더 구조 data/ — 런타임 데이터 2026.04.21 3.2 폴더 구조 extend/ — 코드 자동 삽입 2026.04.21 3.2 폴더 구조 routes/ + controllers/ — 라라벨 스타일 라우팅 2026.04.21
31
전체 회원
503
전체 게시글
770
전체 댓글
441
오늘 방문
33,173
전체 방문
2
현재 접속
인기글 7일 이내
최신글
최신댓글
목록