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 스킨 폴더 생성
- plugins/dx-socket/chats/ 폴더 안에 새 폴더 생성
- 폴더명 규칙: 영소문자•숫자•언더스코어•하이픈만 사용 (예: my-chat, dark-glass)
- 폴더 안에 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">
×
</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">×</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"
×
</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 스킨 등록 및 선택 단계
- plugins/dx-socket/chats/{스킨폴더}/ 에 chat.json, chat.php 업로드
- 관리자 → 실시간 소켓 → 모니터 & 설정 탭 클릭
- "채팅 세부 설정" 섹션 확인
- "채팅 스킨" 드롭다운에서 새 스킨 선택
- "저장" 버튼 클릭
- settings 테이블에 plugin_dx-socket_chat_skin = "{스킨폴더명}" 저장
- 사이트에서 채팅 버튼 확인 — 새 스킨 즉시 적용
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 테스트 체크
- [ ] 관리자에서 스킨 드롭다운에 새 스킨 표시 확인
- [ ] 채팅 버튼 클릭 → 채팅창 열리는지 확인
- [ ] 메시지 입력 후 전송 → 채팅창에 표시 확인
- [ ] 다른 브라우저/탭에서 동시 접속 → 실시간 메시지 수신 확인
- [ ] 채팅창 닫기 → 배지 표시 후 열면 초기화 확인
- [ ] 모바일에서 키보드 올려도 입력창 가려지지 않는지 확인