/* ─── State ─────────────────────────────────────────── */ let ws = null; let currentRoom = "general"; let currentUser = ""; let rooms = {}; let reconnectTimer = null; let reconnectDelay = 1000; /* ─── DOM refs ──────────────────────────────────────── */ const messagesEl = document.getElementById("messages"); const msgInput = document.getElementById("msg-input"); const channelNameEl = document.getElementById("ch-name"); const channelDescEl = document.getElementById("ch-desc"); const channelListEl = document.getElementById("channel-list"); const userDisplayEl = document.getElementById("user-display"); const connStatus = document.getElementById("conn-status"); const userModal = document.getElementById("user-modal-overlay"); const userNameInput = document.getElementById("username-input"); const typingEl = document.getElementById("typing-indicator"); /* ─── Init ──────────────────────────────────────────── */ window.addEventListener("DOMContentLoaded", () => { const saved = sessionStorage.getItem("guardia_user"); if (saved) { currentUser = saved; startApp(); } else { userModal.classList.add("show"); userNameInput.focus(); } }); document.getElementById("username-form").addEventListener("submit", e => { e.preventDefault(); const name = userNameInput.value.trim(); if (!name) return; currentUser = name; sessionStorage.setItem("guardia_user", name); userModal.classList.remove("show"); startApp(); }); function startApp() { userDisplayEl.textContent = currentUser; connect(); } /* ─── WebSocket ─────────────────────────────────────── */ function connect() { const proto = location.protocol === "https:" ? "wss" : "ws"; const url = `${proto}://${location.host}/ws/chat/${currentRoom}/${encodeURIComponent(currentUser)}`; ws = new WebSocket(url); showStatus("reconnecting", "연결 중…"); ws.onopen = () => { showStatus("online", "연결됨"); reconnectDelay = 1000; setTimeout(() => connStatus.classList.remove("show"), 2000); }; ws.onmessage = e => { const data = JSON.parse(e.data); // 타이핑 인디케이터 이벤트 if (data.type === "bot_typing") { showTyping(data.show); return; } if (data.type === "init") { rooms = data.rooms || {}; renderChannelList(); updateChannelHeader(); applyRoomClass(); messagesEl.innerHTML = ""; lastDate = ""; lastSender = ""; if (data.messages?.length) { data.messages.forEach(renderMessage); scrollBottom(); } else { renderWelcome(); } } else { showTyping(false); renderMessage(data); scrollBottom(); } }; ws.onclose = () => { showTyping(false); showStatus("offline", "연결 끊김 — 재연결 중…"); reconnectTimer = setTimeout(() => { reconnectDelay = Math.min(reconnectDelay * 2, 15000); connect(); }, reconnectDelay); }; ws.onerror = () => ws.close(); } function sendMessage() { const text = msgInput.value.trim(); if (!text || !ws || ws.readyState !== WebSocket.OPEN) return; ws.send(JSON.stringify({ content: text })); msgInput.value = ""; msgInput.style.height = "auto"; } /* ─── Typing indicator ──────────────────────────────── */ function showTyping(show) { if (show) { typingEl.innerHTML = `
기재부 예산시스템 WAS 재기동해줘\n예: 서버 상태 알려줘\n예: 도움@bot 도움 을 입력하면 GUARDiA Bot 명령어를 확인할 수 있습니다.";
messagesEl.appendChild(hint);
}
}
let lastDate = "";
let lastSender = "";
function renderMessage(msg) {
const msgDate = new Date(msg.timestamp).toLocaleDateString("ko-KR", {
month: "long", day: "numeric", weekday: "long"
});
if (msgDate !== lastDate) {
const divider = document.createElement("div");
divider.className = "date-divider";
divider.textContent = msgDate;
messagesEl.appendChild(divider);
lastDate = msgDate;
lastSender = "";
}
const isBot = msg.sender_type === "BOT";
const isSame = msg.sender === lastSender && !isBot;
lastSender = msg.sender;
const group = document.createElement("div");
group.className = "msg-group";
group.dataset.msgId = msg.message_id;
const initials = (msg.sender || "?").slice(0, 2).toUpperCase();
const time = new Date(msg.timestamp).toLocaleTimeString("ko-KR", {
hour: "2-digit", minute: "2-digit"
});
const avatarHTML = isSame
? ``
: `${escHtml(code.trim())}`);
text = text.replace(/`([^`]+)`/g, (_, c) => `${escHtml(c)}`);
text = text.replace(/\*\*(.+?)\*\*/g, "$1");
return text;
}
function escHtml(s) {
return String(s ?? "")
.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
}
function escAttr(s) { return String(s ?? "").replace(/'/g, "\\'"); }
function nowTime() {
return new Date().toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit" });
}
/* ─── UI helpers ────────────────────────────────────── */
function scrollBottom() {
requestAnimationFrame(() => { messagesEl.scrollTop = messagesEl.scrollHeight; });
}
function showStatus(cls, text) {
connStatus.className = `show ${cls}`;
connStatus.textContent = text;
}
/* ─── Input events ──────────────────────────────────── */
document.getElementById("send-btn").addEventListener("click", sendMessage);
msgInput.addEventListener("keydown", e => {
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); }
});
msgInput.addEventListener("input", () => {
msgInput.style.height = "auto";
msgInput.style.height = Math.min(msgInput.scrollHeight, 120) + "px";
});