회원가입 | 고객센터 |
DESIGNONEX
dxcms.kr
로그인 회원가입
고객센터
9. 채팅

채팅 제작

D DX
2026.05.10 16:05(수정됨) 98 0 3

1장. 채팅 제작 개요

DXCMS 채팅 제작은 크게 세 가지 방향이 있습니다. 첫째, 채팅 스킨(chat.php/style.css) 제작으로 UI만 교체합니다. 둘째, 전역 JS API(sendEvent, dxSockNotify 등)를 활용해 기존 채팅에 새 기능을 연결합니다. 셋째, 소켓 이벤트 핸들러를 직접 작성해 완전히 새로운 실시간 기능을 구현합니다.


1.1 세 가지 제작 방향

방향 설명 및 난이도
채팅 스킨 제작 chat.php + style.css로 채팅 UI 교체. HTML/CSS 지식만으로 가능. 난이도 ★☆☆
전역 JS API 활용 sendEvent(), dxSockNotify() 등 기존 함수를 호출해 채팅 연동. JS 기본 지식 필요. 난이도 ★★☆
소켓 이벤트 확장 socket-core.js.php의 ws.onmessage 흐름에 새 type 핸들러 연결. JS + WebSocket 이해 필요. 난이도 ★★★


1.2 제작 전 필수 체크

  • dx-socket 플러그인 활성화 — 관리자 → 플러그인 → 소켓 타입에서 "DX 실시간 소켓" 선택
  • API 키(dxk_) 발급 — 관리자 → 실시간 소켓 → API 키 관리 탭에서 발급
  • 기능 활성화 — 관리자 → 실시간 소켓 → chat 기능 ON
  • 소켓 서버 연결 확인 — 브라우저 DevTools → Network → WS 탭에서 wss:// 연결 상태 확인


2장. 채팅 스킨 제작 완전 가이드


2.1 스킨 폴더 생성

  1. plugins/dx-socket/chats/ 폴더 안에 새 폴더 생성
  2. 폴더명 규칙: 영소문자•숫자•언더스코어•하이픈만 사용 (예: my-chat, dark-glass)
  3. 폴더 안에 chat.json, chat.php (필수), style.css (권장) 파일 생성
plugins/dx-socket/chats/
  ├── default/         ← 기본 다크 스킨
  ├── bubble/          ← 말풍선 스킨
  └── my-chat/         ← 내가 만드는 스킨
       ├── chat.json   ← 스킨 메타 (필수)
       ├── chat.php    ← 채팅 UI HTML (필수)
       └── style.css   ← 전용 스타일시트 (권장)


2.2 chat.json — 스킨 메타 정보

{
    "name":        "내 채팅 스킨",
    "description": "커스텀 채팅 UI 설명",
    "version":     "1.0.0",
    "author":      "개발자명",
    "preview":     "우측 하단 커스텀 채팅창"
}

💡 관리자 드롭다운 자동 등록
chat.json이 있는 폴더만 관리자 → 실시간 소켓 → 채팅 스킨 드롭다운에 표시됩니다.
관리자 저장 후 즉시 적용됩니다. 서버 재시작 불필요.


2.3 chat.php — 사용 가능 PHP 변수

plugin.php의 dx_body_bottom 훅에서 chat.php를 include할 때 아래 변수가 주입됩니다.
 
변수 타입 설명
$pos string "right:20px" 또는 "left:20px". 관리자 위치 설정에서 결정
$canChat bool 메시지 전송 가능 여부. 로그인 전용 설정 + 로그인 상태 종합
$loginOnly bool 로그인 전용 여부. chat_login_only 설정값
$mbId string 현재 로그인 아이디. 비로그인이면 빈 문자열("")


2.4 chat.php — 필수 HTML ID 7개

아래 7개 ID는 socket-core.js.php의 JS 함수가 직접 참조합니다. 하나라도 빠지면 해당 기능이 작동하지 않습니다.
 
HTML ID JS에서 사용하는 방식
id="dx-chat-wrap" 전체 래퍼. position:fixed, z-index:99999 필수. 모바일 전체화면 시 JS가 이 안의 box를 조정
id="dx-chat-box" 채팅창 div. display:none으로 시작. dxChatToggle()이 display:flex로 전환
id="dx-chat-messages" 메시지 목록 컨테이너. addMsg()가 메시지 div를 appendChild. scrollTop 자동 조정
id="dx-chat-input" 텍스트 입력 input. dxChatSend()가 value를 읽어 전송 후 초기화. 없으면 전송 불가
id="dx-chat-btn" 채팅 토글 버튼. features_update로 chat OFF 시 JS가 display:none으로 숨김
id="dx-chat-online" 접속자 수 표시 요소. updCount()가 "N명 접속중" 텍스트 자동 갱신
id="dx-chat-badge" 미읽음 배지. display:none으로 시작. updBadge()가 숫자 표시/숨김 자동 처리


2.5 chat.php — 필수 JS 이벤트 연결

이벤트 연결 역할
onclick="dxChatToggle()" 채팅 버튼 또는 헤더에 연결. 채팅창 열기/닫기 토글
onclick="dxChatSend()" 전송 버튼에 연결. 입력값 소켓으로 전송 후 초기화
onkeydown="if(event.key==='Enter')dxChatSend()" 입력 필드에 연결. Enter 키로 전송


2.6 chat.php 완전 구현 예시 — 미니멀 스킨

<?php
// plugins/dx-socket/chats/my-chat/chat.php
if (!defined("DX_CMS")) exit;

// CSS 로드 (선택)
echo '<link rel="stylesheet" href="' . dx_base_url("plugins/dx-socket/chats/my-chat/style.css") . '?v=' . DX_VERSION . '">';
?>

<!-- ★ 전체 래퍼: dx-chat-wrap 필수 -->
<div id="dx-chat-wrap" style="position:fixed;bottom:20px;<?php echo $pos; ?>;z-index:99999;
     font-family:-apple-system,BlinkMacSystemFont,'Malgun Gothic',sans-serif">

  <!-- ★ 채팅창: dx-chat-box 필수, display:none으로 시작 -->
  <div id="dx-chat-box" style="display:none;width:320px;height:440px;
       background:#fff;border-radius:16px;
       box-shadow:0 8px 40px rgba(0,0,0,.15);
       flex-direction:column;overflow:hidden;margin-bottom:10px;
       border:1px solid #e2e8f0">

    <!-- 헤더 -->
    <div style="background:linear-gradient(135deg,#3b82f6,#6366f1);
                padding:14px 16px;display:flex;
                justify-content:space-between;align-items:center">
      <div style="display:flex;align-items:center;gap:8px">
        <!-- 접속 표시 점 (애니메이션은 style.css에서 @keyframes dxPulse 정의) -->
        <span style="width:8px;height:8px;background:#a7f3d0;border-radius:50%;
                     animation:dxPulse 2s infinite;display:inline-block"></span>
        <span style="color:#fff;font-weight:700;font-size:14px">채팅</span>
        <!-- ★ 접속자 수: dx-chat-online 필수 -->
        <span id="dx-chat-online" style="color:rgba(255,255,255,.7);font-size:11px">
          접속중...
        </span>
      </div>
      <!-- 닫기 버튼: dxChatToggle() 필수 -->
      <button
              style="background:rgba(255,255,255,.2);border:none;color:#fff;
                     cursor:pointer;font-size:18px;line-height:1;padding:2px 8px;border-radius:6px">
        &times;
      </button>
    </div>

    <!-- ★ 메시지 목록: dx-chat-messages 필수 -->
    <div id="dx-chat-messages" style="flex:1;overflow-y:auto;padding:12px;
         display:flex;flex-direction:column;gap:4px;background:#f8fafc">
      <!-- addMsg()가 여기에 메시지 div를 삽입 -->
    </div>

    <!-- 입력 영역 -->
    <?php if ($canChat): ?>
    <div style="padding:10px 12px;background:#fff;border-top:1px solid #e2e8f0;display:flex;gap:8px">
      <!-- ★ 입력창: dx-chat-input 필수 -->
      <input id="dx-chat-input" type="text"
             placeholder="메시지 입력 (Enter)" maxlength="200"
             style="flex:1;border:1px solid #e2e8f0;border-radius:10px;
                    padding:8px 12px;font-size:13px;outline:none;box-sizing:border-box">
      <!-- 전송 버튼: dxChatSend() 필수 -->
      <button
              style="background:#3b82f6;border:none;border-radius:10px;
                     padding:8px 16px;color:#fff;font-size:13px;
                     font-weight:700;cursor:pointer">
        전송
      </button>
    </div>
    <?php elseif ($loginOnly && empty($mbId)): ?>
    <!-- 로그인 유도 메시지 (로그인 전용 + 비로그인 상태) -->
    <div style="padding:12px;text-align:center;color:#64748b;font-size:12px;
                background:#fff;border-top:1px solid #e2e8f0">
      <a href="<?php echo dx_base_url('auth/login'); ?>"
         style="color:#3b82f6;font-weight:700">로그인</a>
      후 채팅 가능합니다.
    </div>
    <?php endif; ?>
  </div>

  <!-- ★ 채팅 버튼: dx-chat-btn 필수 + dxChatToggle() 연결 -->
  <button id="dx-chat-btn"
          style="width:54px;height:54px;border-radius:50%;
                 background:linear-gradient(135deg,#3b82f6,#6366f1);border:none;
                 box-shadow:0 4px 20px rgba(59,130,246,.5);cursor:pointer;
                 display:flex;align-items:center;justify-content:center;position:relative">
    <!-- 채팅 아이콘 SVG -->
    <svg width="22" height="22" fill="white" viewBox="0 0 24 24">
      <path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/>
    </svg>
    <!-- ★ 미읽음 배지: dx-chat-badge 필수, display:none으로 시작 -->
    <span id="dx-chat-badge"
          style="display:none;position:absolute;top:-2px;right:-2px;
                 background:#ef4444;color:#fff;border-radius:50%;
                 width:18px;height:18px;font-size:10px;font-weight:700;
                 align-items:center;justify-content:center">
      0
    </span>
  </button>

</div><!-- /dx-chat-wrap -->


2.7 style.css — 권장 필수 CSS

/* plugins/dx-socket/chats/my-chat/style.css */

/* ① 접속 표시 점 애니메이션 — plugin.php에서 기본 dxPulse가 없을 수 있으므로 */  @keyframes dxPulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0.4; }
}

/* ② 스크롤바 커스텀 */  #dx-chat-messages::-webkit-scrollbar { width: 4px; }
#dx-chat-messages::-webkit-scrollbar-thumb {
  background: #c7d2fe;
  border-radius: 2px;
}

/* ③ 입력창 포커스 효과 */  #dx-chat-input:focus {
  border-color: #3b82f6 !important;
  background: #fff !important;
  box-shadow: 0 0 0 3px rgba(59,130,246,.1);
}

/* ④ 모바일 전체화면 보완 규칙 */
@media (max-width: 768px) {
  #dx-chat-box {
    box-sizing: border-box;
    -webkit-overflow-scrolling: touch;
  }
  #dx-chat-messages {
    overscroll-behavior: contain; /* 스크롤 체이닝 방지 */
  }
  #dx-chat-input {
    font-size: 16px !important; /* iOS 자동 줌 방지 */
  }
}


3장. 메시지 렌더링 구조 이해

socket-core.js.php의 addMsg() 함수가 채팅 메시지를 DOM에 추가합니다. 이 함수의 동작 방식을 이해하면 커스텀 스킨에서 메시지 스타일을 원하는 대로 변경할 수 있습니다.


3.1 addMsg() 동작 방식

// socket-core.js.php 내부 addMsg() 핵심 로직

// 1. 내 메시지 판별: UUID 기반 (같은 IP여도 다른 브라우저면 상대방 말풍선)
var mine = (d.uuid && d.uuid === CLIENT_UUID);

// 2. 시간 포맷 (HH:MM:SS)
var t = d.time ? new Date(d.time * 1000) : new Date();

// 3. 컨테이너 div (내 메시지: 오른쪽 정렬, 상대: 왼쪽 정렬)
el.style.cssText = "display:flex;flex-direction:column;margin-bottom:4px;"
  + (mine ? "align-items:flex-end" : "align-items:flex-start");

// 4. 작성자 이름 (내 메시지는 이름 없음)
var nameHtml = mine ? "" : "<span>..." + escHtml(d.mb_id || "GUEST") + "...</span>";

// 5. 말풍선 (내 메시지: 파란 배경, 상대: 어두운 배경)
// mine=true:  background:#3b82f6;color:#fff;border-bottom-right-radius:4px
// mine=false: background:#334155;color:#e2e8f0;border-bottom-left-radius:4px

// 6. chatMsgs 배열 관리 (최대 MAX_MSG=100개)
chatMsgs.push(el);
if (chatMsgs.length > MAX_MSG) {
  var old = chatMsgs.shift();
  if (old.parentNode) old.parentNode.removeChild(old);
}

// 7. dx-chat-messages에 appendChild
document.getElementById("dx-chat-messages").appendChild(el);


3.2 메시지 데이터 구조

필드 설명
type "chat" — 오픈 채팅 메시지
mb_id 발신자 로그인 아이디. 비로그인이면 "GUEST"
uuid 발신 브라우저 고유값. "u" + Date.now() + 랜덤6자. 내 메시지 판별용
message 메시지 내용. 최대 200자. escHtml로 XSS 방어 후 표시
ip 발신자 IP (서버에서 전달)
is_mobile bool — 모바일 접속 여부
time Unix timestamp (초). 없으면 Date.now() 사용


3.3 스킨에서 메시지 스타일 오버라이드

addMsg()는 인라인 스타일로 말풍선을 만듭니다. style.css에서 !important로 덮어쓸 수 없습니다. 메시지 스타일을 완전히 바꾸려면 addMsg() 함수를 오버라이드해야 합니다.
<!-- chat.php 하단 또는 style.css 로드 후 -->
<script>
// addMsg()를 오버라이드하여 커스텀 말풍선 구현
// socket-core.js.php가 로드된 후 실행되어야 함
document.addEventListener("DOMContentLoaded", function() {
  // socket-core.js.php는 즉시 실행 함수라 addMsg는 클로저 내부에 있음
  // 대신 dx-chat-messages에 MutationObserver를 걸어 후처리하는 방법:
  var mc = document.getElementById("dx-chat-messages");
  if (!mc) return;
  var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(m) {
      m.addedNodes.forEach(function(node) {
        if (node.nodeType !== 1) return;
        // 내 메시지 vs 상대방 메시지 판별
        var isMine = node.style.alignItems === "flex-end";
        // 말풍선 span 선택
        var bubble = node.querySelector("span[style]");
        if (bubble && isMine) {
          // 내 메시지 커스텀 스타일
          bubble.style.background = "linear-gradient(135deg,#6366f1,#8b5cf6)";
          bubble.style.borderRadius = "18px";
          bubble.style.borderBottomRightRadius = "4px";
        } else if (bubble) {
          // 상대 메시지 커스텀 스타일
          bubble.style.background = "#ffffff";
          bubble.style.color = "#1e293b";
          bubble.style.border = "1px solid #e2e8f0";
          bubble.style.borderRadius = "18px";
          bubble.style.borderBottomLeftRadius = "4px";
        }
      });
    });
  });
  observer.observe(mc, { childList: true });
});
</script>


4장. 전역 JS API 완전 레퍼런스

socket-core.js.php가 전역(window)에 노출하는 함수와 변수들입니다. 테마 파일, 게시판 뷰, 플러그인의 JS 어디서든 호출할 수 있습니다.


4.1 채팅 제어 함수

함수 설명 및 사용법
window.dxChatToggle() 채팅창 열기/닫기 토글. 채팅 버튼 onclick에 연결
window.dxChatSend() 입력창(dx-chat-input)의 값을 소켓으로 전송 후 초기화. 최대 200자


4.2 소켓 이벤트 전송 함수

함수 설명 및 사용법
window.sendEvent(jsonData) WebSocket으로 JSON 전송. 연결 중이면 최대 3초(200ms×15회) 대기 후 재시도
window._dxWsReady() WebSocket 연결 상태 확인. true/false 반환
 
// sendEvent() 사용 예시

// 채팅 메시지 직접 전송
window.sendEvent({
  type:      "chat",
  mb_id:     "user123",
  uuid:      "u" + Date.now(),  // 고유값
  message:   "안녕하세요!",
  is_mobile: false
});

// 접속 상태 확인 후 전송
if (window._dxWsReady()) {
  window.sendEvent({ type: "chat", message: "준비됨" });
} else {
  console.log("소켓 연결 안됨");
}


4.3 알림•댓글•게시글 연동 함수

함수 설명 및 사용법
window.dxSockNotify(apiResp) 댓글/좋아요 API 응답을 받아 notify + comment_live 이벤트 발송. apiResp에는 notify_targets 배열 필요
window.dxSockCommentAction(apiResp) 댓글 수정(comment_update) 또는 삭제(comment_delete) 브로드캐스트. notif_type + comment_id 필요
window.dxSockPostNew(postId, boardKey) 새 게시글 등록 시 post_live 브로드캐스트. 글 저장 완료 후 호출
 
// 댓글 등록 후 호출 예시 (view.php의 JS에서)
fetch("/api/comment", { method: "POST", ... })
  .then(r => r.json())
  .then(function(apiResp) {
    if (apiResp.success) {
      // 소켓으로 알림 + 실시간 댓글 전파
      if (typeof window.dxSockNotify === "function") {
        window.dxSockNotify(apiResp);
      }
    }
  });

// 글 저장 완료 후 호출 (write.php의 JS에서)
if (typeof window.dxSockPostNew === "function") {
  window.dxSockPostNew(postId, boardKey);
}


4.4 알림 패널 제어 함수

함수 설명
window.dxNotifToggle() 알림 패널 열기/닫기. 열 때 /api/notification 조회
window.dxNotifReceive(data) 소켓 notify 이벤트 수신 시 호출. 벨 배지 증가 + 토스트 표시
window.dxNotifRead(id, el, e) 알림 개별 읽음 처리. /api/notification POST
window.dxNotifReadAll() 전체 알림 읽음 처리


4.5 DM(쪽지) 관련 함수

함수 설명
window.dxDmBadgeIncr() DM 수신 시 메모 탭 배지 + 알림 벨 증가. 소켓 dm 이벤트 수신 시 자동 호출
window.dxDmModal(fromName, preview, fromId) "새 메모가 도착했습니다" 팝업 모달 표시
window.dxDmInviteModal(fromName, fromLoginId, roomId) 1:1 채팅 초대 수신 시 수락/거절 모달 표시
window.dxOpenDmChat(loginId, name, roomId) 1:1 채팅창 열기. 초대 수락 시 자동 호출
window.dxDmChatReceive(data) 1:1 채팅 메시지 수신 핸들러. dm_chat 이벤트 처리


4.6 접속자 정보 전역 변수

전역 변수 설명
window.DX_SOCKET wsUrl, group, mbId, userIp, initUnread, features 포함. PHP가 head에서 주입
window.DX_ACTIVE_USERS Map 객체. IP를 키로 접속자 정보 저장. tracker ON일 때만 갱신
window.DX_MY_NAME 현재 로그인 사용자의 이름 문자열. 채팅 초대에 활용
window.DX_MB_ID 현재 로그인 아이디. DX_SOCKET.mbId와 동일
window.dxSocketUpdateWidget 접속자 변경 시 호출되는 콜백. 외부에서 정의하면 자동 호출됨
window.dxOnPostLive post_live 이벤트 수신 시 호출. 목록 페이지에서 정의
window.dxOnPostUpdate post_update 이벤트 수신 시 호출
window.dxOnPostDelete post_delete 이벤트 수신 시 호출


4.7 로깅 함수

함수 설명
window.dxLog(level, message, data) data/logs/js-YYYY-MM-DD.log에 기록. level: error/warn/info/debug
window.dxLogToServer(level, message) PHP error.log에 JS 이벤트 기록. 소켓 디버깅용


5장. DX_SOCKET 변수와 features 활용


5.1 DX_SOCKET 전역 변수 구조

// PHP dx_head 훅에서 주입되는 전역 변수
var DX_SOCKET = {
  wsUrl:       "wss://designonex.com:14147",  // 소켓 서버 URL
  group:       "dxk_abc123_...",               // 소켓 그룹 = API 키
  mbId:        "user123",                       // 로그인 아이디 (비로그인="")
  userIp:      "1.2.3.4",                       // 클라이언트 IP
  initUnread:  3,                               // 초기 미읽음 알림 수
  features: {
    tracker:      true,    // 접속자 추적
    admin_widget: true,    // 관리자 위젯
    notify:       true,    // 실시간 알림
    comment_live: true,    // 댓글 실시간
    post_live:    true,    // 게시글 목록 실시간
    chat:         true,    // 오픈 채팅
    dm:           true,    // 1:1 쪽지
  }
};

var DX_MY_NAME = "홍길동";  // 현재 로그인 이름
var DX_MB_ID   = "user123"; // 현재 로그인 아이디


5.2 features 기반 조건 분기

커스텀 스킨이나 테마 JS에서 features 값으로 기능 활성화 여부를 확인할 수 있습니다.
// features 활성화 여부 확인
if (typeof DX_SOCKET !== "undefined" && DX_SOCKET.features) {
  if (DX_SOCKET.features.chat) {
    // 채팅 기능 활성화 상태
    document.getElementById("dx-chat-btn").style.display = "flex";
  }
  if (DX_SOCKET.features.notify) {
    // 알림 기능 활성화 상태
    console.log("실시간 알림 활성화됨");
  }
}

// 관리자가 features_update로 실시간 변경 시 자동 반영됨
// (socket-core.js.php의 features_update 핸들러에서 DX_SOCKET.features를 직접 갱신)


5.3 접속자 목록 활용 (DX_ACTIVE_USERS)

// 현재 접속자 목록 조회 (tracker ON일 때만 갱신됨)
if (typeof window.DX_ACTIVE_USERS !== "undefined") {
  window.DX_ACTIVE_USERS.forEach(function(user, ip) {
    console.log("접속자:", user.mb_id || "GUEST", "URL:", user.url, "모바일:", user.is_mobile);
  });
  console.log("총 접속자:", window.DX_ACTIVE_USERS.size);
}

// 접속자 변경 시 콜백 등록
window.dxSocketUpdateWidget = function(activeUsers) {
  var count = activeUsers.size;
  document.getElementById("my-online-count").textContent = count + "명";
};


6장. 새 소켓 이벤트 핸들러 추가

socket-core.js.php에서 ws.onmessage의 타입별 핸들러는 미정의 type을 무시합니다. window.dxOn{Type} 콜백 패턴이나 기존 핸들러 확장 방식으로 새 이벤트를 처리할 수 있습니다.


6.1 dxSocketUpdateWidget — 접속자 변경 콜백

// 테마 layout/main.php 또는 별도 JS 파일에서
// 접속자 수가 변경될 때마다 자동 호출됨
window.dxSocketUpdateWidget = function(activeUsers) {
  // activeUsers: Map<ip, {mb_id, url, is_mobile, last_activity}>
  var total   = activeUsers.size;
  var members = 0;
  var guests  = 0;
  var mobiles = 0;
  activeUsers.forEach(function(u) {
    if (u.mb_id) members++;
    else         guests++;
    if (u.is_mobile) mobiles++;
  });

  // UI 업데이트
  var el = document.getElementById("my-visitor-count");
  if (el) el.textContent = total + "명 접속중 (회원:" + members + " 비회원:" + guests + ")";
};


6.2 dxOnPostLive — 게시글 목록 실시간

// boards/skins/ 또는 테마 board/list.php의 JS에서
// 다른 사람이 새 글을 올리면 자동 호출됨
window.dxOnPostLive = function(postId, boardKey) {
  // 현재 페이지가 해당 게시판 목록인지 확인
  var url = location.pathname;
  if (url.indexOf("/" + boardKey + "/") === -1 && url.indexOf("/" + boardKey) !== url.length - boardKey.length - 1) {
    return; // 현재 게시판이 아님
  }

  // 서버에서 새 글 HTML 조각 가져오기
  fetch("/api/board_post_card?post_id=" + postId + "&board_key=" + boardKey, {
    credentials: "same-origin"
  })
  .then(function(r) { return r.text(); })
  .then(function(html) {
    var list = document.getElementById("board-post-list");
    if (!list) return;
    // 목록 맨 위에 새 글 카드 삽입
    var div = document.createElement("div");
    div.innerHTML = html;
    div.firstElementChild.style.animation = "dxFadeIn .4s ease";
    list.insertBefore(div.firstElementChild, list.firstElementChild);
  });
};


6.3 커스텀 소켓 이벤트 정의 및 처리

완전히 새로운 이벤트 타입을 만들어 서버와 주고받을 수 있습니다. 소켓 서버가 알 수 없는 type은 브로드캐스트로 그룹 전체에 전달합니다.


Step 1: 커스텀 이벤트 전송

// 예: 특정 사용자에게 "리액션" 이벤트 전송
window.sendEvent({
  type:       "my_reaction",     // 커스텀 type
  to_mb_id:   "target_user",
  emoji:      "❤️",
  post_id:    "12345",
  from_mb_id: DX_MB_ID
});


Step 2: 커스텀 이벤트 수신 핸들러

socket-core.js.php의 ws.onmessage에서 직접 처리하기 어려우므로, window.onmessage 기반 래핑 방식을 사용합니다.
// chat.php 하단 <script> 또는 별도 JS 파일에서
// socket-core.js.php 로드 완료 후 실행되어야 함

// 방법: WebSocket 원본 onmessage 후 추가 처리
// socket-core.js.php는 즉시실행함수(IIFE) 내에서 ws를 클로저로 관리하므로
// 외부에서 ws에 직접 접근 불가.
// 대신 CustomEvent를 활용한 인터셉터 패턴 사용:

document.addEventListener("DOMContentLoaded", function() {
  // 소켓 연결 대기 후 커스텀 이벤트 리스너 등록
  var checkReady = setInterval(function() {
    if (typeof window._dxWsReady === "function" && window._dxWsReady()) {
      clearInterval(checkReady);
      initMySocketHandlers();
    }
  }, 300);
});

function initMySocketHandlers() {
  // DXCMS는 알 수 없는 type 메시지를 브로드캐스트하므로
  // 클라이언트가 수신하면 standard EventTarget으로 전달하는 방법:
  // chat.php에서 MutationObserver 대신 커스텀 이벤트 버스 활용
  window.DX_SOCK_BUS = window.DX_SOCK_BUS || {};
  window.DX_SOCK_BUS["my_reaction"] = function(data) {
    if (!data.to_mb_id || data.to_mb_id !== DX_MB_ID) return;
    showReactionToast(data.emoji, data.from_mb_id);
  };
}

// socket-core.js.php에 아래 코드를 추가하면 BUS 연동 가능:
// ws.onmessage 핸들러의 마지막 else 분기에:
// if (window.DX_SOCK_BUS && window.DX_SOCK_BUS[data.type]) {
//   window.DX_SOCK_BUS[data.type](data);
// }

💡 권장 방법
socket-core.js.php의 ws.onmessage 끝 부분에 아래 코드를 추가하면
커스텀 이벤트 버스를 공식 지원할 수 있습니다:

// ws.onmessage 내 모든 분기 처리 후 마지막에:
if (window.DX_SOCK_BUS && typeof window.DX_SOCK_BUS[data.type] === "function") {
  window.DX_SOCK_BUS[data.type](data);
}


7장. 1:1 채팅창(DM Chat) 제작

dxOpenDmChat() 함수가 호출되면 1:1 채팅창 UI가 열립니다. 이 함수는 사용자가 직접 정의해야 합니다. socket-core.js.php는 초대 수락 시 이 함수를 호출합니다.


7.1 dxOpenDmChat() 구현

// chat.php 하단 <script> 또는 별도 JS 파일에서 정의
// 이 함수가 없으면 1:1 채팅 초대 수락 후 채팅창이 열리지 않음

window.dxOpenDmChat = function(targetLoginId, targetName, roomId) {
  // 이미 열려있으면 포커스만
  var existing = document.getElementById("dx-dm-chat-" + roomId);
  if (existing) { existing.querySelector("input").focus(); return; }

  // 채팅 그룹 키 생성 (두 사용자 ID의 MD5) — 서버와 일치해야 함
  // 실제로는 roomId를 그대로 사용하거나 서버에서 group 키를 받아야 함
  var chatGroup = roomId;

  // 1:1 채팅창 UI 생성
  var panel = document.createElement("div");
  panel.id = "dx-dm-chat-" + roomId;
  panel.style.cssText = [
    "position:fixed;bottom:90px;right:90px;z-index:88888",
    "width:300px;height:400px",
    "background:#1e293b;border-radius:16px",
    "box-shadow:0 8px 40px rgba(0,0,0,.5)",
    "display:flex;flex-direction:column;overflow:hidden",
    "border:1px solid #334155"
  ].join(";");

  panel.innerHTML = [
    "<!-- 헤더 -->",
    '<div style="background:#0f172a;padding:12px 14px;display:flex;align-items:center;justify-content:space-between">',
      '<div style="display:flex;align-items:center;gap:8px">',
        '<div style="width:28px;height:28px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#8b5cf6);",',
             '"display:flex;align-items:center;justify-content:center;color:#fff;font-size:12px;font-weight:700">',
          targetName.charAt(0),
        '</div>',
        '<span style="color:#f1f5f9;font-size:13px;font-weight:700">' + escHtml(targetName) + '</span>',
      '</div>',
      '<button + roomId + "').remove()" ',
             'style="background:none;border:none;color:#94a3b8;cursor:pointer;font-size:18px">&times;</button>',
    '</div>',
    "<!-- 메시지 영역 -->",
    '<div id="dm-msgs-" + roomId style="flex:1;overflow-y:auto;padding:10px;display:flex;flex-direction:column;gap:6px"></div>',
    "<!-- 입력 -->",
    '<div style="padding:10px;background:#0f172a;border-top:1px solid #334155;display:flex;gap:6px">',
      '<input id="dm-input-" + roomId type="text" placeholder="메시지..." maxlength="500"',
             'onkeydown="if(event.key==='Enter')dxSendDmChat(' + JSON.stringify(roomId) + ',' + JSON.stringify(targetLoginId) + ')" ',
             'style="flex:1;background:#1e293b;border:1px solid #334155;border-radius:8px;',
                    'padding:7px 10px;color:#f1f5f9;font-size:12px;outline:none">',
      '<button + JSON.stringify(roomId) + ',' + JSON.stringify(targetLoginId) + ')" ',
             'style="background:#3b82f6;border:none;border-radius:8px;padding:7px 12px;color:#fff;font-size:12px;cursor:pointer">전송</button>',
    '</div>',
  ].join("");

  document.body.appendChild(panel);

  // 기존 메시지 로드
  loadDmHistory(roomId);

  var inp = document.getElementById("dm-input-" + roomId);
  if (inp) inp.focus();
};


7.2 DM 메시지 전송 및 수신 구현

// DM 메시지 전송
window.dxSendDmChat = function(roomId, targetLoginId) {
  var inp = document.getElementById("dm-input-" + roomId);
  if (!inp) return;
  var msg = inp.value.trim();
  if (!msg || msg.length > 2000) return;

  // 1. 소켓으로 실시간 전송
  window.sendEvent({
    type:       "dm_chat",
    room_id:    roomId,
    to_login_id: targetLoginId,
    message:    msg,
    from_mb_id: DX_MB_ID
  });

  // 2. DB에 저장 (비접속 시에도 확인 가능)
  var csrf = document.querySelector('meta[name="csrf-token"]');
  fetch("/api/chat_dm", {
    method: "POST",
    credentials: "same-origin",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      "X-Requested-With": "XMLHttpRequest"
    },
    body: "_csrf=" + (csrf ? csrf.content : "") + "&group=" + encodeURIComponent(roomId) + "&message=" + encodeURIComponent(msg)
  });

  inp.value = "";
  // 내 메시지 즉시 표시
  appendDmMsg(roomId, { message: msg, is_mine: true, time_str: new Date().toLocaleTimeString("ko") });
};

// DM 메시지 수신 핸들러 (socket-core.js.php가 호출)
window.dxDmChatReceive = function(data) {
  var roomId = data.room_id;
  var panel  = document.getElementById("dx-dm-chat-" + roomId);
  if (!panel) return; // 채팅창이 열려있지 않으면 무시
  appendDmMsg(roomId, {
    message:      data.message,
    is_mine:      false,
    member_name:  data.from_mb_id,
    time_str:     new Date().toLocaleTimeString("ko")
  });
};

// 메시지 DOM 추가
function appendDmMsg(roomId, msg) {
  var container = document.getElementById("dm-msgs-" + roomId);
  if (!container) return;
  var el = document.createElement("div");
  el.style.cssText = "display:flex;flex-direction:column;margin-bottom:4px;"
    + (msg.is_mine ? "align-items:flex-end" : "align-items:flex-start");
  if (!msg.is_mine && msg.member_name) {
    var nameEl = document.createElement("span");
    nameEl.style.cssText = "font-size:10px;color:#94a3b8;margin-bottom:2px";
    nameEl.textContent = msg.member_name;
    el.appendChild(nameEl);
  }
  var row = document.createElement("div");
  row.style.cssText = "display:flex;align-items:flex-end;gap:4px;flex-direction:"
    + (msg.is_mine ? "row-reverse" : "row");
  var bubble = document.createElement("span");
  bubble.style.cssText = "max-width:180px;padding:7px 11px;border-radius:14px;font-size:12px;word-break:break-word;"
    + (msg.is_mine ? "background:#3b82f6;color:#fff;border-bottom-right-radius:3px"
                   : "background:#334155;color:#e2e8f0;border-bottom-left-radius:3px");
  bubble.textContent = msg.message;
  var time = document.createElement("span");
  time.style.cssText = "font-size:9px;color:#475569;white-space:nowrap";
  time.textContent = msg.time_str || "";
  row.appendChild(bubble); row.appendChild(time);
  el.appendChild(row);
  container.appendChild(el);
  container.scrollTop = container.scrollHeight;
}

// 기존 DM 히스토리 로드
function loadDmHistory(roomId) {
  fetch("/api/chat_dm?group=" + encodeURIComponent(roomId) + "&after=0", {
    credentials: "same-origin"
  })
  .then(function(r) { return r.json(); })
  .then(function(d) {
    if (!d.success) return;
    d.messages.forEach(function(m) {
      appendDmMsg(roomId, {
        message: m.message, is_mine: m.is_mine,
        member_name: m.member_name, time_str: m.time_str
      });
    });
  });
}


8장. 실전 예제 — 그라스모피즘 채팅 스킨

흐림 효과(backdrop-filter)를 활용한 유리 질감의 채팅 스킨 완전 구현 예시입니다.


8.1 chat.json

{
    "name":        "Glass 채팅",
    "description": "그라스모피즘 반투명 채팅창",
    "version":     "1.0.0",
    "author":      "내 이름",
    "preview":     "반투명 유리 질감 채팅창"
}


8.2 style.css

/* plugins/dx-socket/chats/glass-chat/style.css */
@keyframes dxPulse { 0%,100%{opacity:1} 50%{opacity:.4} }
@keyframes glassIn { from{opacity:0;transform:scale(.95) translateY(8px)} to{opacity:1;transform:scale(1) translateY(0)} }

#dx-chat-box {
  animation: glassIn .25s cubic-bezier(.32,1.1,.53,1);
}
#dx-chat-messages::-webkit-scrollbar { width: 3px; }
#dx-chat-messages::-webkit-scrollbar-thumb { background: rgba(255,255,255,.2); border-radius: 2px; }

#dx-chat-input:focus {
  border-color: rgba(255,255,255,.5) !important;
  background: rgba(255,255,255,.15) !important;
}
@media (max-width: 768px) {
  #dx-chat-box { box-sizing: border-box; -webkit-overflow-scrolling: touch; }
  #dx-chat-messages { overscroll-behavior: contain; }
  #dx-chat-input { font-size: 16px !important; }
}


8.3 chat.php (그라스모피즘 완전 구현)

<?php
if (!defined("DX_CMS")) exit;
echo '<link rel="stylesheet" href="' . dx_base_url("plugins/dx-socket/chats/glass-chat/style.css") . '?v=' . DX_VERSION . '">';
?>

<div id="dx-chat-wrap" style="position:fixed;bottom:20px;<?php echo $pos; ?>;z-index:99999;
     font-family:-apple-system,BlinkMacSystemFont,'Malgun Gothic',sans-serif">

  <div id="dx-chat-box" style="display:none;width:330px;height:460px;
       background:rgba(15,23,42,.75);
       backdrop-filter:blur(20px) saturate(180%);
       -webkit-backdrop-filter:blur(20px) saturate(180%);
       border:1px solid rgba(255,255,255,.12);
       border-radius:20px;
       box-shadow:0 8px 40px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.1);
       flex-direction:column;overflow:hidden;margin-bottom:12px">

    <!-- 헤더 -->
    <div style="padding:14px 16px;
                background:rgba(255,255,255,.06);
                border-bottom:1px solid rgba(255,255,255,.08);
                display:flex;justify-content:space-between;align-items:center">
      <div style="display:flex;align-items:center;gap:8px">
        <span style="width:7px;height:7px;background:#4ade80;border-radius:50%;
                     animation:dxPulse 2s infinite;box-shadow:0 0 6px rgba(74,222,128,.5)"></span>
        <span style="color:#f1f5f9;font-weight:700;font-size:13px;letter-spacing:-.01em">실시간 채팅</span>
        <span id="dx-chat-online" style="color:rgba(255,255,255,.45);font-size:11px">접속중...</span>
      </div>
      <button
              style="background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.1);
                     color:rgba(255,255,255,.6);cursor:pointer;
                     font-size:16px;line-height:1;padding:3px 8px;border-radius:6px;
                     transition:all .15s"
        &times;
      </button>
    </div>

    <!-- 메시지 목록 -->
    <div id="dx-chat-messages" style="flex:1;overflow-y:auto;padding:14px;
         display:flex;flex-direction:column;gap:6px"></div>

    <!-- 입력 영역 -->
    <?php if ($canChat): ?>
    <div style="padding:10px 12px;
                background:rgba(255,255,255,.04);
                border-top:1px solid rgba(255,255,255,.08);
                display:flex;gap:8px">
      <input id="dx-chat-input" type="text"
             placeholder="메시지 입력 (Enter)" maxlength="200"
             style="flex:1;background:rgba(255,255,255,.1);
                    border:1px solid rgba(255,255,255,.15);border-radius:10px;
                    padding:8px 12px;color:#f1f5f9;font-size:13px;
                    outline:none;box-sizing:border-box;
                    transition:border-color .2s,background .2s"
             placeholder-color="rgba(255,255,255,.4)">
      <button
              style="background:linear-gradient(135deg,#3b82f6,#6366f1);
                     border:none;border-radius:10px;
                     padding:8px 14px;color:#fff;font-size:13px;
                     font-weight:700;cursor:pointer;
                     box-shadow:0 2px 10px rgba(99,102,241,.4);
                     transition:filter .15s"
        전송
      </button>
    </div>
    <?php elseif ($loginOnly && empty($mbId)): ?>
    <div style="padding:12px;text-align:center;
                color:rgba(255,255,255,.5);font-size:12px;
                background:rgba(255,255,255,.04);
                border-top:1px solid rgba(255,255,255,.08)">
      <a href="<?php echo dx_base_url('auth/login'); ?>"
         style="color:#60a5fa;font-weight:700">로그인</a>
      후 채팅 가능합니다.
    </div>
    <?php endif; ?>
  </div>

  <!-- 채팅 버튼 -->
  <button id="dx-chat-btn"
          style="width:54px;height:54px;border-radius:50%;
                 background:rgba(15,23,42,.8);
                 backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
                 border:1px solid rgba(255,255,255,.15);
                 box-shadow:0 4px 20px rgba(0,0,0,.4),inset 0 1px 0 rgba(255,255,255,.1);
                 cursor:pointer;
                 display:flex;align-items:center;justify-content:center;position:relative;
                 transition:transform .15s,box-shadow .15s"
          >
    <svg width="22" height="22" fill="white" viewBox="0 0 24 24" opacity=".9">
      <path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/>
    </svg>
    <span id="dx-chat-badge"
          style="display:none;position:absolute;top:-3px;right:-3px;
                 background:linear-gradient(135deg,#ef4444,#dc2626);
                 color:#fff;border-radius:50%;
                 width:19px;height:19px;font-size:10px;font-weight:700;
                 align-items:center;justify-content:center;
                 box-shadow:0 2px 6px rgba(239,68,68,.5)">
      0
    </span>
  </button>
</div>


9장. 관리자에서 채팅 스킨 적용


9.1 스킨 등록 및 선택 단계

  1. plugins/dx-socket/chats/{스킨폴더}/ 에 chat.json, chat.php 업로드
  2. 관리자 → 실시간 소켓 → 모니터 & 설정 탭 클릭
  3. "채팅 세부 설정" 섹션 확인
  4. "채팅 스킨" 드롭다운에서 새 스킨 선택
  5. "저장" 버튼 클릭
  6. settings 테이블에 plugin_dx-socket_chat_skin = "{스킨폴더명}" 저장
  7. 사이트에서 채팅 버튼 확인 — 새 스킨 즉시 적용


9.2 자주 발생하는 문제

증상 원인 및 해결
드롭다운에 스킨이 안 보임 chat.json 없거나 JSON 파싱 오류. 파일 내용 확인
채팅창이 열리지 않음 dx-chat-box ID 누락 또는 dxChatToggle() 미연결. 필수 ID 확인
메시지가 표시되지 않음 dx-chat-messages ID 누락. 또는 display:flex 미설정 (채팅창 열릴 때 JS가 flex로 변경)
전송 후 채팅창에 내 메시지 안 나옴 소켓 연결 안 됨. _dxWsReady() 확인. API 키 발급 여부 확인
모바일에서 키보드가 채팅창 가림 style.css에 모바일 보완 규칙 누락. font-size:16px!important 추가
채팅 버튼이 숨겨짐 features_update로 chat=false가 적용됨. 관리자에서 chat 기능 ON 확인


10장. 채팅 스킨 제작 체크리스트


10.1 파일 체크

  • [ ] plugins/dx-socket/chats/{스킨명}/chat.json 생성 (name 필드 필수)
  • [ ] plugins/dx-socket/chats/{스킨명}/chat.php 생성
  • [ ] plugins/dx-socket/chats/{스킨명}/style.css 생성 (권장)


10.2 HTML ID 체크 (7개 필수)

  • [ ] id="dx-chat-wrap" — position:fixed, bottom/right or left, z-index:99999
  • [ ] id="dx-chat-box" — display:none으로 시작, flex-direction:column
  • [ ] id="dx-chat-messages" — overflow-y:auto, flex:1
  • [ ] id="dx-chat-input" — type="text", maxlength="200"
  • [ ] id="dx-chat-btn" — 채팅 토글 버튼
  • [ ] id="dx-chat-online" — 접속자 수 표시
  • [ ] id="dx-chat-badge" — display:none으로 시작


10.3 JS 이벤트 연결 체크

  • [ ] 채팅 버튼:>
  • [ ] 전송 버튼:>
  • [ ] 입력창:>


10.4 CSS 체크

  • [ ] @keyframes dxPulse 정의 (접속 표시 점 애니메이션)
  • [ ] 모바일: #dx-chat-input { font-size: 16px !important } — iOS 자동 줌 방지
  • [ ] 모바일: #dx-chat-messages { overscroll-behavior: contain }
  • [ ] 모바일: #dx-chat-box { box-sizing: border-box; -webkit-overflow-scrolling: touch }


10.5 테스트 체크

  • [ ] 관리자에서 스킨 드롭다운에 새 스킨 표시 확인
  • [ ] 채팅 버튼 클릭 → 채팅창 열리는지 확인
  • [ ] 메시지 입력 후 전송 → 채팅창에 표시 확인
  • [ ] 다른 브라우저/탭에서 동시 접속 → 실시간 메시지 수신 확인
  • [ ] 채팅창 닫기 → 배지 표시 후 열면 초기화 확인
  • [ ] 모바일에서 키보드 올려도 입력창 가려지지 않는지 확인

댓글3

안졸리니졸리 2026.05.19 14:12

소켓 채팅 빨리 사용해 보고싶네요

기존 CMS에서 없던 "실시간" 이라는 기능이 너무 대단한듯합니다
저도 "실시간"에 완전 끌려서 여기까지 오게 되었네요

몸 너무 상하지 않게 건강 챙기면서 하시기 바랍니다

감사합니다 ^^

D
DX 2026.05.19 14:41
^^ 감사합니다. 힘이 납니다. 아자아자
안졸리니졸리 2026.05.19 15:10

다행입니다 ^^

건강 챙기세요

"~~~~ 힘내세요~~~~  우리가~~~ 있잖아요~~~"

로그인 후 댓글을 작성할 수 있습니다.
5. 관리자 기능 사용법 회원 랭킹 2026.04.21 5. 관리자 기능 사용법 포인트샵 2026.04.21 5. 관리자 기능 사용법 레벨 관리 2026.04.21 5. 관리자 기능 사용법 포인트 관리 2026.04.21 5. 관리자 기능 사용법 문자 서비스 2026.04.21 5. 관리자 기능 사용법 메일 보내기 2026.04.21 5. 관리자 기능 사용법 회원 관리 2026.04.21 5. 관리자 기능 사용법 메뉴 관리 2026.04.21 5. 관리자 기능 사용법 인기글 2026.04.21 5. 관리자 기능 사용법 카테고리 2026.04.21 5. 관리자 기능 사용법 게시판 그룹 2026.04.21 5. 관리자 기능 사용법 페이지 관리 2026.04.21 5. 관리자 기능 사용법 전체 공지 2026.04.21 5. 관리자 기능 사용법 팝업 관리 2026.04.21 5. 관리자 기능 사용법 게시판 관리 2026.04.21 4.2 관리자 시스템 구조 관리자 UI 구조 2026.04.21 4.2 관리자 시스템 구조 관리자 라우팅 2026.04.21 4.1 CMS 아키텍처 데이터 흐름 연결 2026.04.21 4.1 CMS 아키텍처 DX 위에 CMS가 올라가는 구조 2026.04.21 3.10 모듈 로딩 구조 자동 로딩 구조 2026.04.21
31
전체 회원
503
전체 게시글
770
전체 댓글
441
오늘 방문
33,173
전체 방문
2
현재 접속
인기글 7일 이내
최신글
최신댓글
목록