회원가입 | 고객센터 |
DESIGNONEX
디자인원엑스
About
Service
Q&A
PR리그
자유게시판N
갤러리
포인트게임
공지사항N
통계
로그인 회원가입
고객센터
4.1 CMS 아키텍처

데이터 흐름 연결

A Administrator
2026.04.21 01:04(수정됨) 93 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

로그인 후 댓글을 작성할 수 있습니다.
4.1 CMS 아키텍처 데이터 흐름 연결 2026.04.21 4.1 CMS 아키텍처 DX 위에 CMS가 올라가는 구조 2026.04.21
30
전체 회원
269
전체 게시글
144
전체 댓글
181
오늘 방문
28,530
전체 방문
1
현재 접속
인기글 7일 이내
최신글
최신댓글
목록