1장. 댓글 및 답글 시스템 개요
DXCMS의 댓글 시스템은 REST 방식의 JSON API와 PHP 서버사이드 렌더링을 결합한 하이브리드 구조입니다. 댓글 등록•수정•삭제는 AJAX(fetch) 요청으로 처리되고, 결과는 페이지 새로고침을 통해 서버 렌더링된 HTML로 표시됩니다. 대댓글은 parent_id + depth 트리 구조로 무한 계층을 지원하며, 실시간 소켓 알림과 연동됩니다.
1.1 핵심 설계 원칙
- 하이브리드 처리 — AJAX API로 데이터 처리 → 완료 후 페이지 reload (서버 렌더링 품질 유지)
- 무한 계층 트리 — parent_id + depth 구조로 댓글→대댓글→대대댓글 무제한 지원
- CSRF 토큰 순환 — 댓글 등록/수정/삭제 시마다 새 CSRF 토큰을 응답에 포함, JS가 자동 갱신
- 이중 입력 지원 — AJAX(fetch) 요청과 일반 form POST 모두 처리 (JavaScript 비활성 환경 대응)
- 에디터 통합 — 플러그인으로 등록된 에디터(CKEditor4 등)를 댓글 폼에도 선택적으로 적용
- hard delete — 댓글 삭제 시 DB에서 완전 삭제 (soft delete 없음), 좋아요도 함께 삭제
- 인기점수 연동 — 댓글 등록/삭제 시 게시글 popular_score 자동 재계산
1.2 관련 파일 구성
| 파일 경로 |
역할 |
| core/api/comment.php |
댓글 등록·수정·삭제·단건조회 API (메인 처리) |
| core/api/comment_delete.php |
레거시 삭제 API (comment.php로 통합 권장) |
| themes/default/board/view.php |
댓글 목록 렌더링 + JS 댓글 동작 전체 |
| boards/skins/gallery/view.php |
갤러리 스킨의 댓글 뷰 (동일 구조) |
| core/DxNotification.php |
댓글 등록 시 실시간 알림 발송 |
| install/schema.sql |
dx_comments, dx_likes 테이블 정의 |
2장. DB 스키마 상세
2.1 dx_comments — 댓글 테이블
모든 게시판의 댓글과 대댓글이 이 하나의 테이블에 저장됩니다. 계층 구조는 parent_id와 depth 두 컬럼으로 표현됩니다.
| 컬럼명 |
타입 |
기본값 |
설명 |
| id |
BIGINT UNSIGNED |
밀리초ID |
PK — 밀리초 타임스탬프로 자동 생성 |
| post_id |
BIGINT UNSIGNED |
- |
소속 게시글 ID (dx_posts.id 참조) |
| parent_id |
BIGINT UNSIGNED |
0 |
부모 댓글 ID. 0이면 최상위 댓글, >0이면 대댓글 |
| member_id |
INT UNSIGNED |
0 |
작성 회원 ID. 0이면 비회원 |
| author_name |
VARCHAR(100) |
NULL |
비회원 작성자명 (회원이면 members.name 조인) |
| author_pass |
VARCHAR(255) |
NULL |
비회원 비밀번호 (bcrypt 해시, 미사용 시 NULL) |
| content |
TEXT |
- |
댓글 본문 (일반 텍스트 또는 에디터 HTML) |
| ip |
VARCHAR(45) |
NULL |
작성자 IP (IPv4/IPv6 모두 지원) |
| depth |
TINYINT(3) |
0 |
계층 깊이. 0=최상위 댓글, 1=대댓글, 2=대대댓글 ... |
| status |
TINYINT(1) |
1 |
상태. 1=정상, 0=삭제(현재 hard delete 방식) |
| created_at |
DATETIME |
- |
작성일시 |
💡 밀리초 타임스탬프 ID
id는 PHP insertWithMicrotimeId() 함수로 생성됩니다.
형식: microtime(true) × 1000 + 랜덤 2자리 (예: 1746887234562_47)
이 방식으로 ID 순서 = 시간 순서가 보장되어 ORDER BY id ASC로 시간순 정렬이 됩니다.
2.2 dx_likes — 좋아요 테이블
게시글과 댓글의 좋아요를 모두 이 테이블에서 관리합니다. target_type으로 대상 유형을 구분합니다.
| 컬럼명 |
타입 |
기본값 |
설명 |
| id |
INT UNSIGNED |
AUTO_INCREMENT |
PK |
| target_type |
ENUM('post','comment') |
- |
'post' = 게시글 좋아요, 'comment' = 댓글 좋아요 |
| target_id |
BIGINT UNSIGNED |
- |
대상 게시글 또는 댓글의 ID |
| member_id |
INT UNSIGNED |
0 |
좋아요한 회원 ID |
| ip |
VARCHAR(45) |
NULL |
비회원 좋아요 시 IP |
| created_at |
DATETIME |
- |
좋아요 일시 |
UNIQUE KEY uk_like (target_type, target_id, member_id) — 같은 회원이 같은 대상에 중복 좋아요를 누를 수 없습니다.
2.3 dx_notifications — 실시간 알림 테이블
댓글 등록 시 게시글 작성자와 부모 댓글 작성자에게 알림이 생성됩니다.
| 컬럼명 |
타입 |
설명 |
| id |
BIGINT UNSIGNED |
PK (AUTO_INCREMENT) |
| to_member_id |
INT UNSIGNED |
알림을 받을 회원 ID |
| from_member_id |
INT UNSIGNED |
알림을 보낸 회원 ID |
| type |
VARCHAR(30) |
알림 유형 (예: 'comment', 'reply', 'like') |
| message |
VARCHAR(255) |
알림 본문 (예: "홍길동님이 댓글을 달았습니다.") |
| url |
VARCHAR(500) |
클릭 시 이동 URL (게시글 view + #comment-{id} 앵커) |
| is_read |
TINYINT(1) |
읽음 여부. 0=미읽음, 1=읽음 |
| created_at |
DATETIME |
알림 생성일시 |
3장. 댓글 트리 구조 (parent_id + depth)
댓글과 대댓글은 부모-자식 관계를 parent_id로 연결하고, 계층 깊이를 depth로 표현합니다. 화면에서 대댓글은 왼쪽에 들여쓰기(depth × 24px)와 파란 세로선으로 시각적으로 구분됩니다.
3.1 데이터 구조 예시
아래는 게시글 하나에 댓글 4개가 달린 경우의 DB 레코드 예시입니다.
| id |
post_id |
parent_id |
depth |
내용 |
| 1001 |
500 |
0 |
0 |
안녕하세요! (최상위 댓글) |
| 1002 |
500 |
0 |
0 |
저도 궁금합니다. (최상위 댓글) |
| 1003 |
500 |
1001 |
1 |
1001번 댓글의 대댓글 |
| 1004 |
500 |
1003 |
2 |
1003번의 대대댓글 |
3.2 화면 렌더링 결과
위 데이터가 화면에서 아래와 같이 렌더링됩니다:
┌─ [댓글 1001] 안녕하세요! (depth=0, 들여쓰기 없음)
│ └─ [댓글 1003] 1001번 댓글의 대댓글 (depth=1, 들여쓰기 24px + 파란선)
│ └─ [댓글 1004] 1003번의 대대댓글 (depth=2, 들여쓰기 48px + 파란선)
└─ [댓글 1002] 저도 궁금합니다. (depth=0, 들여쓰기 없음)
3.3 PHP 트리 렌더링 알고리즘
view.php는 아래 단계로 댓글 목록을 트리 형태로 렌더링합니다:
- DB에서 해당 게시글의 모든 댓글을 id 오름차순(작성 순)으로 조회
- $_cmtMap[댓글id] = 댓글데이터 — ID로 댓글 데이터에 빠르게 접근
- $_cmtChildren[parent_id][] = 댓글id — 부모별 자식 id 목록 구성
- parent_id='0'인 최상위 댓글부터 순서대로 _dnx_render_comment() 재귀 호출
- 각 댓글 렌더 후 해당 댓글의 자식들을 재귀적으로 렌더링
- depth가 5를 초과하면 min(depth, 5)로 최대 120px까지만 들여쓰기
🔍 렌더링 핵심 코드 구조
// 댓글 맵과 자식 목록 구성
foreach ($comments as $_c) {
$_cmtMap[$_c['id']] = $_c;
$_cmtChildren[$_c['parent_id']][] = $_c['id'];
}
// 최상위 댓글부터 재귀 렌더
foreach ($_cmtChildren['0'] as $_rootId) {
_dnx_render_comment($_rootId, $_cmtMap, $_cmtChildren, ...);
}
4장. 댓글 API 상세 (core/api/comment.php)
모든 댓글 작업은 /api/comment 단일 엔드포인트로 처리됩니다. _sub 파라미터로 작업 유형을 구분합니다.
| HTTP 메서드 + _sub |
기능 |
설명 |
| GET ?_sub=get&id=N |
단건 댓글 조회 |
실시간 DOM 삽입용. status=1인 댓글만 반환 |
| POST (없음) |
댓글 등록 |
post_id + content + parent_id(0 또는 부모ID) 전송 |
| POST _sub=update |
댓글 수정 |
comment_id + content 전송. 본인 또는 관리자만 가능 |
| POST _sub=delete |
댓글 삭제 |
comment_id 전송. hard delete + 좋아요 삭제 |
4.1 댓글 등록 처리 흐름
- CSRF 토큰 검증 (dx_csrf_check())
- post_id, parent_id, content 파라미터 수신 (content 또는 comment_content 둘 다 지원)
- XSS 필터링 — 에디터 사용 시 DxSanitizer::editorContent(), 아니면 DxSanitizer::text()
- 게시글 존재 확인 (status=1)
- 게시판의 use_comment=1 및 comment_level 권한 확인
- depth 계산 — parent_id의 댓글 depth + 1 (최상위 댓글은 0)
- 비회원 처리 — author_name 필수, author_pass bcrypt 해시 저장
- depth 컬럼 존재 여부 확인 (구버전 DB 대응, 없으면 data에서 제거)
- insertWithMicrotimeId() 로 밀리초 ID 생성 후 댓글 INSERT
- 게시글 comment_count + 1 업데이트
- 인기점수(popular_score) 재계산 (조회×1 + 좋아요×5 + 댓글×3 × 시간감쇠)
- dx_after_comment 훅 실행
- 게시판 목록 캐시 무효화
- DxNotification::onComment() 로 글 작성자 + 부모 댓글 작성자에게 알림
- AJAX 요청: JSON 반환 (id, depth, csrf_token 포함) / 일반 POST: 원글 페이지로 redirect
4.2 댓글 삭제 처리 흐름
- CSRF 토큰 검증
- comment_id로 댓글 조회 (status 무관 — 이미 soft-delete된 것도 처리)
- 권한 확인 — 본인(member_id 일치) 또는 관리자만 삭제 가능
- dx_likes 테이블에서 해당 댓글의 모든 좋아요 hard delete
- dx_comments 테이블에서 댓글 hard delete
- 게시글 comment_count GREATEST(0, count-1) 감소 (0 이하로 내려가지 않음)
- 인기점수(popular_score) 재계산
- JSON 응답 반환 — comment_id, post_id, notif_type, 새 CSRF 토큰 포함
⚠️ 주의: 대댓글 삭제 시 고아(orphan) 문제
현재 구현에서 부모 댓글을 삭제해도 자식(대댓글)은 DB에 남아 있습니다.
화면에서는 부모 댓글 DOM이 즉시 제거되지만 자식은 parent_id로 연결되어 있어
다음 페이지 reload 시 _cmtChildren에 등록되나 _cmtMap에 부모가 없어 렌더되지 않습니다.
(안전한 완전 삭제를 원한다면 훅 dx_board_before_delete 활용 권장)
4.3 댓글 수정 처리 흐름
- CSRF 토큰 검증
- comment_id, content 파라미터 수신 및 유효성 검사
- 댓글 조회 (status=1)
- 권한 확인 — 본인 또는 관리자
- XSS 필터링 (에디터 사용 여부에 따라 다른 새니타이저 적용)
- dx_comments 테이블 content 컬럼만 UPDATE
- JSON 응답 반환 — comment_id, post_id, content, notif_type, 새 CSRF 토큰 포함
4.4 API 응답 형식
등록 성공 시 JSON 응답 예시:
{
"success": true,
"id": "1746887234562", // 새 댓글 ID (밀리초 타임스탬프)
"depth": 1, // 댓글 계층 깊이
"message": "댓글이 등록되었습니다.",
"notify_targets": ["user1"], // 알림 받을 login_id 배열
"csrf_token": "abc123..." // 새 CSRF 토큰 (JS가 자동 갱신)
}
5장. 댓글 권한 체계
5.1 댓글 작성 권한 (comment_level)
게시판의 comment_level 컬럼 값에 따라 댓글 작성 가능 여부가 결정됩니다.
| comment_level 값 |
허용 대상 |
미충족 시 처리 |
| 0 |
비회원 포함 누구나 |
제한 없음 |
| 1 (기본값) |
로그인 회원만 |
JSON: 403 + 로그인 유도 메시지 반환 |
| 9 |
관리자만 |
JSON: 403 + 관리자 전용 메시지 반환 |
5.2 댓글 수정/삭제 권한
| 조건 |
결과 |
| 관리자 (isAdmin() = true) |
모든 댓글 수정/삭제 가능 |
| 로그인 회원 + member_id 일치 |
본인 댓글만 수정/삭제 가능 |
| 그 외 (비회원, 타인) |
403 Forbidden — "삭제/수정 권한이 없습니다." 반환 |
5.3 비회원 댓글
comment_level=0인 게시판에서는 비회원도 댓글을 작성할 수 있습니다.
- author_name 필수 — 이름을 입력하지 않으면 "이름을 입력해주세요." 오류 반환
- author_pass 선택 — 입력 시 bcrypt 해시로 저장 (삭제 시 비밀번호 확인 용도)
- member_id = 0 — DB에 0으로 저장, 회원과 구분
- ip 기록 — 작성자 IP 저장 (관리자 확인용)
6장. JavaScript 동작 상세
6.1 주요 JS 함수 목록
| 함수명 |
역할 |
| dxFetch(url, body) |
fetch 공통 래퍼 — CSRF 헤더 자동 포함, 응답의 csrf_token 자동 갱신 |
| dxSubmitComment(e, postId) |
메인 댓글 등록 — 폼 submit 핸들러, parent_id=0으로 전송 |
| dxSubmitReply(e, parentId, postId) |
답글 등록 — parent_id=부모댓글ID로 전송 |
| dxDeleteComment(commentId, postId) |
댓글 삭제 — confirm 후 API 호출, 성공 시 DOM 제거 |
| dxEditCommentToggle(commentId) |
수정 폼 토글 — 보기모드 ↔ 수정모드 전환 |
| dxUpdateComment(commentId, postId) |
댓글 수정 저장 — CKEditor 또는 textarea 내용 추출 후 API 전송 |
| dxApplyCommentUpdate(commentId, content) |
수정 내용을 DOM에 즉시 반영 (본인 + 소켓 수신 측 공용) |
| dxApplyCommentDelete(commentId) |
댓글 DOM 제거 + 카운트 감소 (본인 + 소켓 수신 측 공용) |
| dxReplyToggle(id) |
답글 폼 열기/닫기, 메인 댓글 폼 숨기기/복원, CKEditor 초기화 |
| dxCmtMenuToggle(id) |
⋮ 드롭다운 메뉴 열기/닫기 |
| dxGetEditorVal(form, name) |
textarea에서 값 추출 (CKEditor 인스턴스 있으면 getData() 호출) |
6.2 댓글 등록 흐름 (JS → API → 화면)
- 사용자가 댓글 폼에 내용 입력 후 "댓글 등록" 버튼 클릭
- dxSubmitComment() 호출 → dxGetEditorVal()로 내용 추출
- submit 버튼 disabled 처리 (중복 제출 방지)
- dxFetch("/api/comment", "post_id=...&content=...&parent_id=0") 호출
- 성공 시 dxSockNotify(d) 로 소켓 알림 브로드캐스트
- 80ms 후 location.href = 현재URL + "#comment-{새ID}" 설정 후 reload
- 페이지 reload 후 #comment-{id} 앵커로 해당 댓글로 자동 스크롤
- 2초 후 배경 노란색 하이라이트 효과 제거
6.3 답글 폼 동작 (dxReplyToggle)
답글 버튼 클릭 시 아래 동작이 순서대로 실행됩니다:
- 이미 열린 다른 답글 폼 모두 닫기 (동시에 하나만 열림)
- 현재 댓글의 답글 폼 열기 (hidden 클래스 토글)
- 메인 댓글 입력 폼(#dx-cmt-form) 숨기기 → 답글 폼이 열린 동안 메인 폼 비노출
- 에디터 사용 환경이면 80ms 후 CKEditor를 답글 textarea에 초기화
- 답글 폼 닫을 때: CKEditor 인스턴스 파괴, 메인 댓글 폼 복원
6.4 댓글 수정 흐름
- ⋮ 메뉴에서 "수정" 클릭 → dxEditCommentToggle(commentId) 호출
- 댓글 내용 div 숨기기 + 수정 폼 표시
- 에디터 사용 환경이면 CKEditor를 수정 textarea에 초기화 (기존 내용 로드)
- "저장" 버튼 클릭 → dxUpdateComment(commentId, postId) 호출
- CKEditor.getData() 또는 textarea.value로 내용 추출
- dxFetch("/api/comment", "_sub=update&comment_id=...&content=...")
- 성공 시 CKEditor 인스턴스 파괴, dxApplyCommentUpdate()로 DOM 즉시 반영
- 수정 폼 닫기 (보기 모드로 복귀)
6.5 CSRF 토큰 자동 갱신
댓글 API의 모든 응답에는 새 CSRF 토큰이 포함됩니다. dxFetch()가 이를 자동으로 처리합니다:
- 응답 JSON의 csrf_token 값을 DX_CSRF 전역 변수에 저장
- <meta name="csrf-token"> 태그의 content 속성 갱신
- 페이지 내 모든 <input name="_csrf"> hidden 필드 값 동기화
이 방식으로 SPA처럼 단일 페이지에서 연속 댓글 작성 시 CSRF 토큰 만료 문제를 방지합니다.
7장. 댓글 에디터 연동
7.1 에디터 사용 여부 결정
댓글 폼에 에디터를 사용할지 여부는 dx_editor_use_comment() 함수가 결정합니다:
function dx_editor_use_comment() {
$editorId = dx_active_plugin("editor");
if (!$editorId) return false;
$key = "plugin_" . $editorId . "_use_comment_editor";
return dx_config($key, "0") === "1";
}
관리자 → 플러그인 설정 → 에디터 플러그인 → "댓글 에디터 사용" 옵션을 ON으로 설정하면 활성화됩니다.
7.2 에디터 모드별 동작 차이
| 구분 |
에디터 OFF (기본) |
에디터 ON (CKEditor4 등) |
| 댓글 입력 UI |
일반 textarea (resize 가능) |
CKEditor 경량 에디터 (toolbar 포함) |
| 내용 새니타이즈 |
DxSanitizer::text() — 태그 제거 |
DxSanitizer::editorContent() — 허용 태그 필터 |
| 저장 형식 |
일반 텍스트 |
HTML (볼드, 링크, 표 등 가능) |
| 화면 출력 |
nl2br(htmlspecialchars()) — 줄바꿈 → <br> |
HTML 그대로 출력 (XSS 필터 후) |
| 답글 에디터 |
없음 (textarea) |
폼 열릴 때 CKEditor 동적 초기화 |
| 수정 에디터 |
없음 (textarea) |
수정 폼 열릴 때 CKEditor 초기화 |
7.3 CKEditor 댓글용 설정
댓글 및 답글의 CKEditor는 게시글용보다 경량화된 툴바를 사용합니다:
- 높이: 150px (게시글은 400px)
- 툴바: Bold, Italic, Underline, Strike, RemoveFormat, TextColor, BGColor, 목록, 정렬, 링크, 표, 자동번역, 소스
- enterMode: ENTER_BR (줄바꿈 시 <br> 삽입, 게시글과 동일)
- 다크모드 자동 동기화: body.classList에 dark 클래스 감지
8장. 실시간 알림 및 소켓 연동
8.1 댓글 알림 대상
새 댓글이 등록되면 아래 두 대상에게 알림이 발송됩니다:
| 알림 수신 대상 |
조건 및 설명 |
| 게시글 작성자 |
댓글 작성자와 다른 회원일 때만 알림. 비회원 게시글은 알림 없음 |
| 부모 댓글 작성자 (대댓글 시) |
대댓글 시 부모 댓글 작성자에게 추가 알림. 게시글 작성자와 동일인이면 중복 발송 안함 |
8.2 알림 발송 흐름
- 댓글 등록 완료 후 DxNotification::onComment($newId, $postId, $fromId, $parentId) 호출
- 게시글 작성자의 login_id를 DB에서 조회
- parent_id가 있으면 부모 댓글 작성자의 login_id도 조회
- notify_targets 배열에 login_id 수집 (중복 제거)
- dx_notifications 테이블에 알림 레코드 INSERT
- JSON 응답에 notify_targets 포함 → JS의 dxSockNotify(d) 호출
- WebSocket 서버로 알림 이벤트 브로드캐스트 → 수신자 화면에 실시간 벨 알림 표시
8.3 소켓 이벤트 유형
| 이벤트 유형 |
발생 시점 및 동작 |
| comment (신규 댓글) |
댓글 등록 후 dxSockNotify() 호출 → 알림 대상 화면에 배지 표시 |
| comment_delete |
댓글 삭제 후 dxSockCommentAction() → 같은 글을 보는 다른 사람 화면에서도 해당 댓글 DOM 제거 |
| comment_update |
댓글 수정 후 dxSockCommentAction() → 같은 글을 보는 사람 화면의 댓글 내용 즉시 갱신 |
9장. 댓글 UI 구성 요소
9.1 댓글 섹션 전체 구조
댓글 섹션은 아래 순서로 구성됩니다:
┌ <section> 댓글 섹션 컨테이너 (use_comment=1일 때만 표시)
│ ├ <header> 댓글 수 표시
│ ├ [댓글 목록] — PHP 재귀 렌더링
│ │ ├ 댓글1 (depth=0)
│ │ │ ├ 작성자 아바타 + 이름 + 날짜
│ │ │ ├ ⋮ 드롭다운 (답글/수정/삭제)
│ │ │ ├ 댓글 내용 (보기 모드)
│ │ │ ├ 수정 폼 (숨김, 수정 시 표시)
│ │ │ └ 답글 폼 (숨김, 답글 버튼 클릭 시 표시)
│ │ └ 대댓글 (depth=1, 들여쓰기 + 파란선)
│ └ [댓글 작성 폼] — 권한 충족 시 표시
└ 미로그인 시: "로그인 후 댓글 작성" 안내
└
9.2 ⋮ 드롭다운 메뉴
각 댓글 우측에는 ⋮(ellipsis) 버튼이 있어 클릭 시 드롭다운 메뉴가 표시됩니다. 메뉴 항목은 권한에 따라 다르게 표시됩니다:
| 메뉴 항목 |
표시 조건 |
| 답글 |
댓글 작성 권한(canCmt)이 있는 사용자 모두 |
| 수정 |
본인 댓글이거나 관리자 |
| 삭제 |
본인 댓글이거나 관리자 (빨간색으로 표시) |
화면 다른 곳 클릭 시 document click 이벤트로 모든 드롭다운 자동 닫힘
9.3 댓글 앵커 및 스크롤
댓글 등록 후 페이지가 reload되면 URL에 #comment-{id} 해시가 포함됩니다. 페이지 로드 후 아래 동작이 수행됩니다:
- id="comment-{id}" 요소를 scrollIntoView({ behavior: "smooth", block: "center" })
- 해당 댓글 배경을 노란색(#fffbeb)으로 하이라이트
- 1.5초 전환 효과(CSS transition) 후 원래 배경색으로 복귀
9.4 댓글 작성 폼 UI
| 상태 |
UI 표시 |
| 로그인 + 작성 권한 있음 |
textarea(또는 에디터) + "댓글 등록" 버튼 |
| 미로그인 (comment_level=1 이상) |
자물쇠 아이콘 + "로그인 후 댓글을 작성할 수 있습니다." + 로그인 링크 |
| use_comment=0 (댓글 비활성화) |
댓글 섹션 자체가 표시되지 않음 |
10장. 게시글 지표 자동 갱신
10.1 comment_count 자동 갱신
댓글 등록/삭제 시 게시글의 comment_count 컬럼이 자동으로 갱신됩니다:
- 등록 시 — UPDATE posts SET comment_count = comment_count + 1, updated_at = ? WHERE id = ?
- 삭제 시 — UPDATE posts SET comment_count = GREATEST(0, comment_count - 1) WHERE id = ?
GREATEST(0, ...) 함수로 0 이하로 내려가지 않도록 보호합니다.
10.2 popular_score 재계산
댓글 등록 또는 삭제 후 인기점수를 즉시 재계산합니다. 계산 공식:
기본 점수 = 조회수 × 1 + 좋아요 × 5 + 댓글수 × 3
시간 감쇠 = max(0.1, 1.0 - floor(경과일수 / 7) × 0.1)
popular_score = round(기본점수 × 시간감쇠)
이 점수로 ?sf=popular 정렬 시 최신 인기글이 상위에 노출됩니다.
11장. 댓글 훅(Hook) 포인트
플러그인이나 테마에서 댓글 동작을 커스터마이징할 때 사용하는 훅입니다.
| 훅 이름 |
발생 시점 및 전달 인자 |
| dx_after_comment |
댓글 등록 완료 직후 — comment_id, post_id, board 전달 |
💡 훅 활용 예시 — 댓글 등록 후 외부 알림
// 댓글 등록 시 슬랙 웹훅 발송 예시
dx_add_hook('dx_after_comment', function($args) {
$commentId = $args['comment_id'];
$postId = $args['post_id'];
// 슬랙 웹훅 또는 외부 알림 처리...
});
12장. 관리자 사용방법
12.1 게시판별 댓글 설정
- 관리자 → 게시판 관리 → 해당 게시판 수정
- 댓글 사용 여부 — use_comment ON/OFF
- 댓글 권한 설정 — comment_level: 0=전체 / 1=회원 / 9=관리자
- 저장 후 즉시 적용
12.2 관리자의 댓글 관리
관리자는 게시글 뷰 페이지에서 모든 댓글의 ⋮ 메뉴를 통해 아래 작업을 수행할 수 있습니다:
- 수정 — 본인 여부와 무관하게 모든 댓글 내용 수정 가능
- 삭제 — 모든 댓글 삭제 가능 (hard delete, 좋아요도 함께 삭제)
대댓글이 달린 부모 댓글을 삭제해도 대댓글은 그대로 남습니다. 연결된 대댓글까지 모두 정리하려면 직접 삭제해야 합니다.
12.3 댓글 에디터 활성화
- 관리자 → 플러그인 관리 → 에디터 플러그인 (CKEditor4 등) 설정
- "댓글 에디터 사용" 옵션 ON
- 저장 후 게시글 뷰 페이지의 댓글 입력란이 에디터로 교체됨
에디터 ON 상태에서는 댓글에 볼드, 이탤릭, 링크, 표, 이미지 삽입 등이 가능합니다.
12.4 댓글 알림 확인
- 상단 알림 벨 아이콘 클릭
- 미읽음 알림 목록에서 댓글 알림 확인
- 알림 클릭 시 해당 게시글 view + #comment-{id} 앵커로 이동
- 알림 읽음 처리 — 클릭 시 is_read = 1 업데이트
13장. 스킨에서 댓글 커스터마이징
13.1 뷰 파일에서 사용 가능한 댓글 변수
| 변수 |
설명 |
| $comments |
댓글 배열 — dx_comments + 작성자 정보 (LEFT JOIN members). id 오름차순 |
| $board['use_comment'] |
댓글 활성화 여부 (1=활성, 0=비활성) |
| $board['comment_level'] |
댓글 작성 권한 (0/1/9) |
| $_useCommentEditor |
dx_editor_use_comment() 반환값 — 에디터 사용 여부 (bool) |
| $post['comment_count'] |
게시글의 댓글 수 (DB 캐시 값, 실제 count와 다를 수 있음) |
13.2 댓글 목록 데이터 구조
$comments 배열의 각 요소는 아래 필드를 포함합니다:
id // 댓글 ID
post_id // 소속 게시글 ID
parent_id // 부모 댓글 ID (0=최상위)
depth // 계층 깊이 (0, 1, 2 ...)
member_id // 작성 회원 ID (0=비회원)
member_name // 작성 회원 이름 (LEFT JOIN)
member_profile_img // 프로필 이미지 경로
author_name // 비회원 작성자명
content // 댓글 내용
ip // 작성자 IP
status // 상태 (1=정상)
created_at // 작성일시
13.3 커스텀 댓글 렌더링 예시
스킨의 view.php에서 기본 댓글 구조를 직접 구현하는 최소 예시:
<?php
// 트리 구조로 변환
$cmtMap = array();
$cmtChildren = array();
foreach ($comments as $c) {
$cmtMap[$c['id']] = $c;
$cmtChildren[$c['parent_id']][] = $c['id'];
}
// 최상위 댓글부터 재귀 렌더
function renderCmt($id, $map, $children) {
if (!isset($map[$id])) return;
$c = $map[$id];
$indent = (int)$c['depth'] * 20;
echo "<div style='margin-left:{$indent}px'>";
echo "<b>" . htmlspecialchars($c['member_name'] ?: $c['author_name']) . "</b>: ";
echo htmlspecialchars($c['content']);
echo "</div>";
foreach ($children[$id] ?? [] as $childId) {
renderCmt($childId, $map, $children);
}
}
if (!empty($cmtChildren['0'])) {
foreach ($cmtChildren['0'] as $rootId) {
renderCmt($rootId, $cmtMap, $cmtChildren);
}
}
14장. 오류 처리 및 디버깅
14.1 자주 발생하는 오류와 해결 방법
| 증상 |
원인 및 해결 |
| 댓글 등록 후 아무 반응 없음 |
JS 오류 확인 (브라우저 콘솔). CSRF 토큰 만료 또는 content 값이 비어있는 경우 |
| "댓글 내용을 입력하세요." 오류 |
CKEditor 내용이 비어있거나 comment_content POST 값이 빈 문자열. DX_DEBUG=true로 서버 로그 확인 |
| depth 컬럼 없음 오류 |
migrate.php 미실행. 오류 발생 시 자동으로 depth 컬럼 없이 저장 (대댓글 들여쓰기 불가) |
| 삭제 후 comment_count 불일치 |
하드 삭제는 됐으나 count 갱신 실패. data/error.log 확인 후 직접 UPDATE로 수정 |
| 대댓글 답글 폼이 열리지 않음 |
dxReplyToggle() 호출 시 요소 ID 불일치. reply-{id} 요소 존재 여부 DOM 확인 |
14.2 data/error.log 로그 패턴
댓글 관련 오류는 아래 접두사로 로그에 기록됩니다:
- [comment_api_delete] — 댓글 삭제 API 오류
- [Comment] — 댓글 등록/수정 오류
- [comment_delete] — 레거시 삭제 API 오류
💡 댓글 시스템 디버깅 체크리스트
1. 브라우저 DevTools → Network 탭에서 /api/comment 요청/응답 확인
2. data/error.log 파일에서 [Comment] 또는 [comment_api] 로그 확인
3. DX_DEBUG=true 설정 시 PHP 경고가 화면 하단에 표시됨
4. dx_comments 테이블에 INSERT 여부 직접 DB 쿼리로 확인
5. CSRF 토큰 문제: 브라우저 쿠키/세션 초기화 후 재시도