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

채팅 제작

D DX
2026.05.10 16:05(수정됨) 99 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

다행입니다 ^^

건강 챙기세요

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

로그인 후 댓글을 작성할 수 있습니다.
9. 채팅 채팅 제작 2026.05.10 9. 채팅 채팅 구조 2026.05.10
31
전체 회원
503
전체 게시글
775
전체 댓글
442
오늘 방문
33,174
전체 방문
3
현재 접속
인기글 7일 이내
최신글
최신댓글
목록