1/** 2 * WP Realtime Secure Chat - íë¡í ìì ¯ 3 * [UX ì ê·¸ë ì´ë] ìí´ë¦ ìë ê²ì¤í¸ ë¡ê·¸ì¸ & ì¦ì ì ì¥ ì ì© 4 * [ì¶ê°] ì본 íë¦ ì ì§ + DB ê¸°ë° ë°© ëª©ë¡ ë° ê³¼ê±° ëí ë´ì ì°ë 5 */ 6(function () { 7 'use strict'; 8 9 const config = window.WPRC_Config || {}; 10 11 /* ì¸ì¦(ë¸ë£¨) ë°°ì§ë chat-client-mobile.js ê° ê·¸ë¦°ë¤ (ëª¨ë¬ ë´ë¶ ë ë ë´ë¹). */ 12 const pluginUrl = (config.pluginUrl || '').replace(/\/$/, ''); 13 const serverUrl = (config.nodeServerUrl || '').replace(/\/$/, ''); 14 const version = config.version || '1'; 15 16 function escHtml(str) { 17 return String(str == null ? '' : str) 18 .replace(/&/g, '&') 19 .replace(/</g, '<') 20 .replace(/>/g, '>') 21 .replace(/"/g, '"') 22 .replace(/'/g, '''); 23 } 24 25 function safeImgUrl(url) { 26 const s = String(url || ''); 27 return /^https?:\/\//i.test(s) ? s : ''; 28 } 29 30 // ââ "ì±í ëê°ê¸°"ë¡ ë ë ë°© ë¸ë¡ë¦¬ì¤í¸ (chat-client-mobile.js ì ëì¼ í¤) ââ 31 // ì구(FastAPI) DB·localStorage ë°±ì ì ë°©ì´ ë¨ì ìì´ë 목ë¡ì ëì´ìëì§ ìê² ì°¨ë¨. 32 function _leftRooms() { 33 try { return JSON.parse(localStorage.getItem('wprc_left_rooms') || '[]'); } catch (e) { return []; } 34 } 35 function _isLeftRoom(roomId) { 36 return _leftRooms().indexOf(String(roomId)) !== -1; 37 } 38 39 const floatBtn = document.getElementById('wprc-float-btn'); 40 const floatModal = document.getElementById('wprc-float-modal'); 41 const iconOpen = document.getElementById('wprc-float-icon-open'); 42 const iconClose = document.getElementById('wprc-float-icon-close'); 43 const badge = document.getElementById('wprc-float-badge'); 44 45 if (!floatBtn || !floatModal) return; 46 47 // ìì½ë ííì´ì§ ì±í íì´ì§ìì ë²í¼ ì¨ê¹ 48 if (document.querySelector('#wprc-chat-app')) { 49 const floatWrap = document.getElementById('wprc-float-wrap'); 50 if (floatWrap) floatWrap.style.display = 'none'; 51 return; 52 } 53 54 let isOpen = false; 55 let isMounted = false; 56 let isMounting = false; 57 let unread = 0; 58 59 function loadCSS(href, id) { 60 if (document.getElementById(id)) return Promise.resolve(); 61 return new Promise((res) => { 62 const link = document.createElement('link'); 63 link.id = id; 64 link.rel = 'stylesheet'; 65 link.href = href; 66 link.onload = res; 67 link.onerror = res; 68 document.head.appendChild(link); 69 setTimeout(res, 1500); 70 }); 71 } 72 73 /* ì¤í¬ë¦½í¸ ë¡ë. 74 â ï¸ ì´ì 구íì onerror ì 2ì´ íìììììë resolve í´ì, 75 ìì§ ë¤ ë°ì§ 못í ì¤í¬ë¦½í¸ë¥¼ "ë°ìë¤"ê³ ì¹ê³ ì§ííë¤. 76 ë린 íì ìì 112KB ì§ë¦¬ chat-client-mobile.js ê° 2ì´ë¥¼ ë기면 77 WPRC_InitMobileChat ì´ ìë ì± mountChat ì´ ì´ì´ì ¸ "ì´ê¸°í ì¤ë¥"ê° ë´ë¤. 78 â ï¸ ë `getElementById(id)` ë§ ë³´ê³ ì¦ì resolve íëë°, íê·¸ê° ìë¤ê³ 79 ë¡ëê° ëë ê²ì ìëë¤(bgConnect ê° socket.io 를 먼ì ì¬ì´ ëë¤). 80 ì§í ì¤ì¸ ë¡ëê° ìì¼ë©´ ê·¸ ì½ìì ê·¸ëë¡ ê¸°ë¤ë¦°ë¤. 81 ì¤í¨íë©´ í그를 ì§ì ë¤ì ìëê° ì²ìë¶í° ë°ì ì ìê² íë¤. */ 82 function loadScript(src, id, timeout) { 83 const existing = document.getElementById(id); 84 if (existing) { 85 return existing._wprcLoad || Promise.resolve(); 86 } 87 88 const s = document.createElement('script'); 89 s.id = id; 90 s.src = src; 91 92 const p = new Promise((res, rej) => { 93 let settled = false; 94 const finish = (ok, err) => { 95 if (settled) return; 96 settled = true; 97 clearTimeout(timer); 98 if (ok) { 99 s._wprcLoad = null; // ìë£ â ë¤ì í¸ì¶ì ê·¸ë¥ íµê³¼ 100 res(); 101 } else { 102 if (s.parentNode) s.parentNode.removeChild(s); 103 rej(err); 104 } 105 }; 106 const timer = setTimeout( 107 () => finish(false, new Error('ìê° ì´ê³¼: ' + src)), 108 timeout || 20000 109 ); 110 s.onload = () => finish(true); 111 s.onerror = () => finish(false, new Error('ë¶ë¬ì¤ê¸° ì¤í¨: ' + src)); 112 }); 113 114 s._wprcLoad = p; 115 document.body.appendChild(s); 116 return p; 117 } 118 119 function buildModalHTML() { 120 // sessionStorageìì ê²ì¤í¸ ëë¤ì ë³µì 121 if (!config.isLoggedIn) { 122 try { 123 const g = JSON.parse(sessionStorage.getItem('wprc_guest') || '{}'); 124 if (g.displayName) config.displayName = g.displayName; 125 } catch(e) {} 126 } 127 const displayName = escHtml(config.displayName || 'ê²ì¤í¸'); 128 const avatarUrl = safeImgUrl(config.avatarUrl); 129 130 floatModal.innerHTML = ` 131 <div class="wprc-is-mobile" style="position:relative;width:100%;height:100%;overflow:hidden;background:#fff;"> 132 <div class="wprc-mobile-wrapper" style="display:flex;flex-direction:column;height:100%;"> 133 134 <header class="wprc-mobile-header" style="position:relative;"> 135 <button id="wprc-float-close-inner" 136 style="position:absolute; right:15px; top:15px; background:none;border:none;cursor:pointer; 137 padding:4px;color:#6b7280;font-size:20px;line-height:1;z-index:10;" aria-label="ë«ê¸°">✕</button> 138 139 <div id="wprc-mob-header-friends" class="wprc-mob-tab-header active"> 140 <h2>ì¹êµ¬</h2> 141 <button id="wprc-friend-search-btn" aria-label="ì¹êµ¬ ê²ì" 142 style="margin-left:auto;background:none;border:none;cursor:pointer; 143 color:#8b5cf6;font-size:20px;padding:4px 8px;line-height:1;"> 144 <i class="fas fa-user-plus"></i> 145 </button> 146 </div> 147 <div id="wprc-mob-header-rooms" class="wprc-mob-tab-header"> 148 <h2>ì±í </h2> 149 <button id="wprc-mobile-action" class="wprc-mob-new-room-btn"> 150 <i class="fas fa-plus"></i>
150 ì ì±í ë°© 151 </button> 152 </div> 153 <div id="wprc-mob-header-channel" class="wprc-mob-tab-header"> 154 <h2>ì±ëí¡</h2> 155 </div> 156 <div id="wprc-mob-header-settings" class="wprc-mob-tab-header"> 157 <h2>ì¤ì </h2> 158 </div> 159 </header> 160 161 <main id="wprc-mobile-main"> 162 <div id="wprc-mob-tab-friends" class="wprc-mobile-tab-content active"> 163 <div class="wprc-mob-my-profile"> 164 ${avatarUrl 165 ? `<img src="${avatarUrl}" class="wprc-mob-my-avatar">` 166 : `<div class="wprc-mob-my-avatar wprc-mob-avatar-placeholder"><i class="fas fa-user"></i></div>`} 167 <div class="wprc-mob-my-profile-info"> 168 <span class="wprc-mob-my-name" id="wprc-mob-my-name-display">${displayName}</span> 169 <span class="wprc-mob-my-status" id="wprc-mob-my-status-display">ìí ë©ìì§ë¥¼ ì ë ¥íì¸ì</span> 170 </div> 171 </div> 172 <button type="button" id="wprc-talk-channel-btn" class="wprc-tc-entry"> 173 <span class="wprc-tc-entry-icon"><i class="fas fa-bullhorn"></i></span> 174 <span class="wprc-tc-entry-text"> 175 <strong>í¡ì±ë</strong> 176 <em>ììë´ì¤ ê³µì ì±ëê³¼ ë°ë¡ ëíí기</em> 177 </span> 178 <i class="fas fa-chevron-right wprc-tc-entry-arrow"></i> 179 </button> 180 <div class="wprc-mob-sort-bar"> 181 <span>ì¹êµ¬ <strong id="wprc-online-count-mob">0</strong>ëª </span> 182 <span class="wprc-mob-sort-options"> 183 <button type="button" class="wprc-sort-btn" data-sort="name">ê°ëë¤ì</button> 184 <span class="wprc-sort-sep" aria-hidden="true">·</span> 185 <button type="button" class="wprc-sort-btn" data-sort="update">ì ë°ì´í¸ì</button> 186 </span> 187 </div>
188 <ul id="wprc-user-list-mob" class="wprc-mobile-list"></ul> 189 </div> 190 191 <div id="wprc-mob-tab-rooms" class="wprc-mobile-tab-content"> 192 <ul id="wprc-room-list-mob" class="wprc-mobile-list"></ul> 193 </div> 194 195 <div id="wprc-mob-tab-channel" class="wprc-mobile-tab-content"> 196 <div id="wprc-channel-body-mob"></div> 197 </div> 198 199 <div id="wprc-mob-tab-settings" class="wprc-mobile-tab-content"> 200 <div class="wprc-mob-settings"> 201 <div class="wprc-mob-setting-row"> 202 <label class="wprc-mob-setting-label">íì ëë¤ì</label> 203 <div class="wprc-mob-setting-control"> 204 <input type="text" id="wprc-setting-nickname-mob" value="${displayName}" maxlength="20" placeholder="ëë¤ì"> 205 <button id="wprc-save-nickname-mob" class="wprc-mob-btn-change">ë³ê²½</button> 206 </div> 207 </div> 208 <div class="wprc-mob-setting-row"> 209 <label class="wprc-mob-setting-label">ìí ë©ìì§</label> 210 <div class="wprc-mob-setting-control"> 211 <input type="text" id="wprc-setting-status-mob" maxlength="60" placeholder="ìí ë©ìì§ë¥¼ ì ë ¥íì¸ì"> 212 <button id="wprc-save-status-mob" class="wprc-mob-btn-change">ë³ê²½</button> 213 </div> 214 <p class="wprc-mob-setting-desc" id="wprc-status-preview">íì¬: <em>ìì</em></p> 215 </div> 216 <div class="wprc-mob-setting-row wprc-mob-setting-row--between"> 217 <div> 218 <label class="wprc-mob-setting-label">ì림ì</label> 219 <p class="wprc-mob-setting-desc">ì ë©ìì§ ì림ì</p> 220 </div> 221 <label class="wprc-mob-toggle"> 222 <input type="checkbox" id="wprc-setting-sound-mob" checked> 223 <span class="wprc-mob-toggle-track"></span> 224 </label> 225 </div> 226 </div> 227 </div> 228 </main> 229 230 <nav class="wprc-mobile-nav" id="wprc-mobile-nav"> 231 <button class="wprc-nav-item active" data-tab="friends"> 232 <i class="fas fa-user"></i><span>ì¹êµ¬</span> 233 </button> 234 <button class="wprc-nav-item" data-tab="rooms"> 235 <i class="fas fa-comment"></i><span>ì±í </span> 236 </button> 237 <button class="wprc-nav-item" data-tab="channel"> 238 <i class="fas fa-headset"></i><span>ì±ëí¡</span> 239 </button> 240 <button class="wprc-nav-item" data-tab="settings"> 241 <i class="fas fa-ellipsis-h"></i><span>ë보기</span> 242 </button> 243 </nav> 244 245 <!-- í¡ì±ë ëª©ë¡ íë©´ â ì±í ì¤ë²ë ì´ë³´ë¤ ìì ë¬ì¼ ëíì°½ì´ ìì ìì¸ë¤ --> 246 <div id="wprc-channel-view" class="wprc-mobile-overlay"> 247 <header class="wprc-mobile-chat-header"> 248 <button id="wprc-channel-back" class="wprc-icon-btn" aria-label="ë¤ë¡"><i class="fas fa-arrow-left"></i></button> 249 <h2>í¡ì±ë</h2>
250 <span class="wprc-tc-header-spacer" aria-hidden="true"></span> 251 </header> 252 <div class="wprc-tc-page"> 253 <p class="wprc-tc-intro">ê¶ê¸í ì ì ê³µì ì±ëë¡ ë¨ê²¨ 주ì¸ì. ë´ë¹ìê° íì¸ í ëµë³ë립ëë¤.</p> 254 <ul id="wprc-channel-list" class="wprc-mobile-list wprc-tc-list"></ul> 255 </div> 256 </div> 257 258 <div id="wprc-mobile-chat-view" class="wprc-mobile-overlay"> 259 <header class="wprc-mobile-chat-header"> 260 <button id="wprc-mobile-back" class="wprc-icon-btn"><i class="fas fa-arrow-left"></i></button> 261 <h2 id="wprc-mobile-chat-name">ì±í ë°©</h2> 262 <button id="wprc-mobile-info" class="wprc-icon-btn"><i class="fas fa-bars"></i></button> 263 </header> 264 <div id="wprc-messages-mob" class="wprc-messages-mobile-container"></div> 265 <div class="wprc-mobile-input-area"> 266 <div id="wprc-mob-img-preview" class="wprc-mob-img-preview" style="display:none;"> 267 <div class="wprc-mob-img-preview-inner"> 268 <img id="wprc-mob-img-preview-img" src="" alt="미리보기"> 269 <button id="wprc-mob-img-cancel" class="wprc-mob-img-cancel" aria-label="ì·¨ì"> 270 <i class="fas fa-times"></i> 271 </button> 272 </div> 273 </div> 274 <div class="wprc-mobile-input-row"> 275 <input type="file" id="wprc-mob-file-input" accept="image/jpeg,image/png,image/gif,image/webp" style="display:none;"> 276 <button id="wprc-mob-attach-btn" class="wprc-mob-attach-btn" aria-label="ì¬ì§ 첨ë¶"><i class="fas fa-image"></i></button> 277 <input type="text" id="wprc-message-input-mob" placeholder="ë©ìì§ë¥¼ ì ë ¥íì¸ì (enter ì ì ì¡)" autocomplete="off" maxlength="2000"> 278 <button id="wprc-send-btn-mob" aria-label="ì ì¡"><i class="fas fa-paper-plane"></i></button> 279 </div> 280 </div> 281 </div> 282 283 </div> 284 </div>`; 285 286 document.getElementById('wprc-float-close-inner')?.addEventListener('click', closeModal); 287 } 288 289 function showSpinner(message) { 290 floatModal.innerHTML = ` 291 <div style="position:relative;width:100%;height:100%;background:#fff;display:flex;flex-direction:column; 292 align-items:center;justify-content:center;"> 293 <button id="wprc-float-close-spinner" aria-label="ë«ê¸°" 294 style="position:absolute;right:15px;top:15px;background:none;border:none;cursor:pointer; 295 padding:4px;color:#94a3b8;font-size:24px;line-height:1;">✕</button> 296 <div style="width:40px;height:40px;border:4px solid #f3e8ff;border-top:4px solid #8b5cf6; 297 border-radius:50%;animation:wprc-spin 1s linear infinite;"></div> 298 <style>@keyframes wprc-spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}</style> 299 <p style="margin-top:20px;color:#64748b;font-size:15px;font-weight:600;">${escHtml(message || 'ì±í ë°©ì ì ì ì¤ì ëë¤...')}</p> 300 </div>`; 301 document.getElementById('wprc-float-close-spinner')?.addEventListener('click', closeModal); 302 } 303 304 /* ì´ê¸°í ì¤í¨ íë©´. ìì ìë ì¤ë¥ ë¬¸êµ¬ë§ ëì°ê³ ëì´ë¼ ìë¡ê³ 침 ë§ê³ ë ë°©ë²ì´ ììë¤. 305 ì´ì [ë¤ì ìë] ë¡ ê·¸ ì리ìì 복구ëë¤. */ 306 function showMountError(message, retry) { 307 floatModal.innerHTML = ` 308 <div style="position:relative;width:100%;height:100%;background:#fff;display:flex;flex-direction:column; 309 align-items:center;justify-content:center;padding:24px;box-sizing:border-box;text-align:center;"> 310 <button id="wprc-float-close-err" aria-label="ë«ê¸°" 311 style="position:absolute;right:15px;top:15px;background:none;border:none;cursor:pointer; 312 padding:4px;color:#94a3b8;font-size:24px;line-height:1;">✕</button> 313 <div style="font-size:34px;line-height:1;margin-bottom:14px;" aria-hidden="true">📶</div> 314 <p style="margin:0 0 6px;color:#1f2937;font-size:16px;font-weight:700;">
314${escHtml(message || 'ì±í ì ë¶ë¬ì¤ì§ 못íìµëë¤')}</p> 315 <p style="margin:0 0 18px;color:#64748b;font-size:13px;line-height:1.6;"> 316 ì°ê²°ì´ ë리거ë ì ì ëê²¼ì ì ììµëë¤.<br>ì ì í ë¤ì ìëí´ ì£¼ì¸ì.</p> 317 <button id="wprc-float-retry" type="button" 318 style="background:#8b5cf6;color:#fff;border:none;border-radius:20px;padding:10px 22px; 319 font-size:14px;font-weight:600;cursor:pointer;">ë¤ì ìë</button> 320 </div>`; 321 document.getElementById('wprc-float-close-err')?.addEventListener('click', closeModal); 322 document.getElementById('wprc-float-retry')?.addEventListener('click', () => { 323 if (typeof retry === 'function') retry(); 324 }); 325 } 326 327 function generateGuestNickname() { 328 const adj = ['ì©ì©í','ì¬ë¹ 른','ì©ê°í','ëê¸í','ëª ëí','ì ëë','ìë±í','ê·ì¬ì´']; 329 const noun = ['ìê³í ë¼','ì¹í','ë¶ê·¹ê³°','íë¤','ì¤ë¦¬ë구리','ì½ìë¼','미ì´ìº£','ì¹´í¼ë°ë¼']; 330 return adj[Math.floor(Math.random() * adj.length)] + ' ' + noun[Math.floor(Math.random() * noun.length)]; 331 } 332 333 async function autoGuestLogin() { 334 showSpinner(); 335 const autoNickname = generateGuestNickname(); 336 try { 337 const resp = await fetch(config.restUrl + 'guest-token', { 338 method: 'POST', 339 headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce }, 340 body: JSON.stringify({ nickname: autoNickname }), 341 }); 342 const data = await resp.json(); 343 344 if (!data.success) { 345 showMountError('ìë ì ìì ì¤í¨íìµëë¤', () => autoGuestLogin()); 346 return; 347 } 348 349 sessionStorage.setItem('wprc_guest', JSON.stringify({ token: data.token, displayName: autoNickname })); 350 config.displayName = autoNickname; 351 await mountChat(); 352 } catch (err) { 353 console.error('[WPRC Float] autoGuestLogin ì¤í¨:', err); 354 showMountError('ìë²ì ì°ê²°íì§ ëª»íìµëë¤', () => autoGuestLogin()); 355 } 356 } 357 358 async function mountChat() {
359 // â ï¸ isMounted 를 try ììì true ë¡ ì¸ì°ë©´, í ë² ì¤í¨íì ë ì구í ê³ ì°©ëë¤ 360 // (openModal ì `if (isMounted) return;` ì ê±¸ë ¤ ë«ìë¤ ì´ì´ë ì¤ë¥ íë©´ ê·¸ëë¡). 361 // ì±ê³µíì ëë§ ì¸ì°ê³ , ì¤ë³µ ì§ì ì ë³ë íëê·¸ë¡ ë§ëë¤. 362 if (isMounted || isMounting) return; 363 isMounting = true; 364 365 showSpinner('ì±í ì ì¤ë¹íê³ ììµëë¤...'); 366 367 try { 368 // íìí ê²ë¤ì 먼ì ë¤ ë°ì ë¤ì íë©´ì ë§ë ë¤. 369 // (ìì ìë HTML ì 먼ì ê·¸ë ¤ì, ë린 íì ìì ë¹ ê»ë°ê¸°ê° ì¤ë ë³´ìë¤) 370 await Promise.all([ 371 loadCSS('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css', 'wprc-fa-css'), 372 loadCSS(pluginUrl + '/assets/css/chat.css?v=' + version, 'wprc-chat-css'), 373 ]); 374 375 if (typeof io !== 'function') { 376 await loadScript(serverUrl + '/socket.io/socket.io.js', 'wprc-socketio'); 377 } 378 if (typeof io !== 'function') { 379 throw new Error('socket.io 를 ë¶ë¬ì¤ì§ 못íìµëë¤.'); 380 } 381 382 if (typeof WPRC_InitMobileChat !== 'function') { 383 await loadScript(pluginUrl + '/assets/js/chat-client-mobile.js?v=' + version, 'wprc-mobile-js'); 384 } 385 if (typeof WPRC_InitMobileChat !== 'function') { 386 throw new Error('ì±í 모ëì ë¶ë¬ì¤ì§ 못íìµëë¤.'); 387 } 388 389 // HTML ì½ì 390 buildModalHTML(); 391 const root = floatModal.querySelector('.wprc-is-mobile'); 392 if (!root) { 393 throw new Error('ì±í íë©´ì ë§ë¤ì§ 못íìµëë¤.'); 394 } 395 396 // ì본 í´ë¼ì´ì¸í¸ JS ì´ê¸°í ì¤í (í, UI ì í, ì´ë²¤í¸ ë± ëª¨ë ì´ ììì ì²ë¦¬ë¨) 397 // 398 // â ï¸ ë°© ëª©ë¡ ë³µì(localStorage ë°±ì + DB)·과거 ëí ë¡ë·방 ë¶í(room:restore)ì 399 // chat-client-mobile.js ìì¼ë¡ ì®ê²¼ë¤. ì¬ê¸°ì ë íë©´ 400 // â ë°© 목ë¡ì ì§ì DOM ì ê½ìë í´ë¼ì´ì¸í¸ì renderRoomList() ê° ë®ì´ì°ê³ 401 // â¡ ë°©ì ì´ ë room:join / DB íì¤í 리 ìì²ì´ ë ë²ì© ëê°ë¤. 402 // ìì½ë íì´ì§ì ëìì ì¼ì¹ìí¤ê¸° ìí´ìë í ê³³ììë§ ì²ë¦¬í´ì¼ íë¤. 403 WPRC_InitMobileChat(root); 404 405 isMounted = true; 406 407 } catch (e) { 408 console.error('[WPRC Float] ì´ê¸°í ì¤í¨:', e); 409 showMountError('ì±í ì ë¶ë¬ì¤ì§ 못íìµëë¤', () => mountChat()); 410 } finally { 411 isMounting = false; 412 } 413 } 414 415 function openModal() { 416 isOpen = true; 417 floatModal.style.display = 'flex'; 418 iconOpen.style.display = 'none'; 419 iconClose.style.display = 'block'; 420 resetBadge(); 421 422 const noticeBar = document.getElementById('site-notice-bar'); 423 if (noticeBar) { 424 noticeBar._wprcWasVisible = noticeBar.style.display !== 'none'; 425 noticeBar.style.setProperty('display', 'none', 'important'); 426 document.documentElement.style.setProperty('--top-banner-height', '0px'); 427 } 428 429 if (isMounted) return; 430 431 const isLogged = !!config.isLoggedIn && config.isLoggedIn !== '0' && config.isLoggedIn !== 'false'; 432 let hasGuestToken = false; 433 try { 434 const g = JSON.parse(sessionStorage.getItem('wprc_guest') || '{}'); 435 if (g.token) hasGuestToken = true; 436 } catch (e) {} 437 438 if (!isLogged && !hasGuestToken) { 439 autoGuestLogin(); 440 } else { 441 if (!isLogged) { 442 try { 443 const g = JSON.parse(sessionStorage.getItem('wprc_guest') || '{}'); 444 if (g.displayName) config.displayName = g.displayName; 445 } catch(e) {} 446 } 447 mountChat(); 448 } 449 } 450 451 function closeModal() { 452 isOpen = false;
453 floatModal.style.display = 'none'; 454 iconOpen.style.display = 'block'; 455 iconClose.style.display = 'none'; 456 457 const noticeBar = document.getElementById('site-notice-bar'); 458 if (noticeBar && noticeBar._wprcWasVisible) { 459 noticeBar.style.removeProperty('display'); 460 const h = noticeBar.offsetHeight; 461 document.documentElement.style.setProperty('--top-banner-height', h + 'px'); 462 } 463 } 464 465 floatBtn.addEventListener('click', () => isOpen ? closeModal() : openModal()); 466 467 let bgSocket = null; 468 469 // ð ê²ì¤í¸ í í° íë³´: sessionStorage ì ìì¼ë©´ ì¬ì¬ì©, ìì¼ë©´ ìë ë°ê¸(ë©ì¸ íì´ì§ ìë ì ìì©). 470 async function ensureGuestToken() { 471 try { 472 const g = JSON.parse(sessionStorage.getItem('wprc_guest') || '{}'); 473 if (g.token) { 474 if (g.displayName) config.displayName = g.displayName; 475 return g.token; 476 } 477 } catch (e) {} 478 479 if (!config.allowGuests) return ''; 480 481 const nickname = generateGuestNickname(); 482 try { 483 const resp = await fetch(config.restUrl + 'guest-token', { 484 method: 'POST', 485 headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce }, 486 body: JSON.stringify({ nickname }), 487 }); 488 const data = await resp.json(); 489 if (data && data.success && data.token) { 490 sessionStorage.setItem('wprc_guest', JSON.stringify({ token: data.token, displayName: nickname })); 491 config.displayName = nickname; 492 return data.token; 493 } 494 } catch (e) { 495 console.warn('[WPRC BG] ê²ì¤í¸ ìë ì ì ì¤í¨:', e); 496 } 497 return ''; 498 } 499 500 async function bgConnect() { 501 if (bgSocket && bgSocket.connected) return; 502 503 const isLogged = !!config.isLoggedIn && config.isLoggedIn !== '0' && config.isLoggedIn !== 'false'; 504 let token = isLogged ? (config.token || '') : ''; 505 506 if (!isLogged) { 507 try { 508 const g = JSON.parse(sessionStorage.getItem('wprc_guest') || '{}'); 509 if (g.token) { 510 token = g.token; 511 if (g.displayName) config.displayName = g.displayName; 512 } 513 } catch(e) {} 514 515 // ð ë©ì¸ íì´ì§ ì§ì ì: ê²ì¤í¸ í í°ì´ ìì¼ë©´ ìë ë°ê¸ë°ì 백그ë¼ì´ë ì ì 516 // (í ë² ë°ê¸ëë©´ sessionStorage ì ë¨ì ì¸ì ë´ ë¤ë¥¸ íì´ì§ììë ì¬ì¬ì©ë¨) 517 if (!token && config.isFrontPage) { 518 token = await ensureGuestToken(); 519 } 520 521 if (!token) return; // ê²ì¤í¸ í í° ë¯¸ë°ê¸(ë©ì¸ ì¸ íì´ì§ ë±) ì ì ìíì§ ìì 522 } 523 524 if (!token) { console.warn('[WPRC BG] í í° ìì, ì°ê²° ì·¨ì'); return; } 525 if (!config.nodeServerUrl) { console.warn('[WPRC BG] nodeServerUrl ìì'); return; } 526 527 // loadScript ë ì¤í¨ ì reject íë¤(§ì´ê¸°í ìì ) â ì¬ê¸°ì ì¼ì¼ì¼ 528 // ì²ë¦¬ëì§ ìì ê±°ë¶ê° ë¨ì§ ìëë¤. ë°°ê²½ ì ìì ì¤í¨í´ë ì¡°ì©í ëì´ê°ë¤. 529 if (typeof io !== 'function') { 530 try { 531 await loadScript(serverUrl + '/socket.io/socket.io.js', 'wprc-socketio'); 532 } catch (e) { 533 console.warn('[WPRC BG] socket.io ë¡ë ì¤í¨:', e); 534 return; 535 } 536 } 537 if (typeof io !== 'function') { console.warn('[WPRC BG] socket.io ìì, ì°ê²° ì·¨ì'); return; } 538 539 bgSocket = io(config.nodeServerUrl, { 540 auth: { token }, 541 transports: ['websocket', 'polling'], 542 reconnectionAttempts: 10, 543 reconnectionDelay: 3000, 544 }); 545 546 bgSocket.on('connect', () => { 547 console.log('[WPRC BG] ìì¼ ì°ê²° ìë£ â'); 548 floatBtn.classList.add('wprc-float-online'); 549 }); 550 551 bgSocket.on('connect_error', (err) => { 552 console.warn('[WPRC BG] ì°ê²° ì¤ë¥:', err.message); 553 }); 554
555 bgSocket.on('disconnect', () => { 556 floatBtn.classList.remove('wprc-float-online'); 557 }); 558 559 // â ï¸ ë°© ë°±ì ·DB ì ì¥(rooms/save)·room:flag ë chat-client-mobile.js ë¡ ì®ê²¼ë¤. 560 // ì´ ë°±ê·¸ë¼ì´ë ìì¼ì room:created 를 io.emit ë¸ë¡ëìºì¤í¸ë¡ ë°ì¼ë¯ë¡ 561 // (= ë¨ì´ ë§ë ë°©ê¹ì§ ë ìì¨ë¤) ì¬ê¸°ì ì ì¥íë©´ ë´ ë°© 목ë¡ì´ ì¤ì¼ëë¤. 562 563 // ð ì ìì ì ì ë°ì´í¸ (ì´ì ìì²) 564 bgSocket.on('users:list', (userList) => { 565 const countSpan = document.getElementById('wprc-banner-online-count'); 566 if (countSpan && Array.isArray(userList)) { 567 countSpan.textContent = Number(userList.length).toLocaleString(); 568 } 569 }); 570 571 // ð [ì¶ê°] 백그ë¼ì´ëìì ë´ ë°© ì ì²´ì join â ë«í ìì´ë ì ë©ìì§ë¥¼ ì¤ìê° ìì 572 // (ìë²ë join í ìì¼ìê²ë§ message:receive 를 ë³´ë´ë¯ë¡ ì¹´ì´í¸íë ¤ë©´ join íì) 573 const _bgJoined = new Set(); 574 function bgJoinRooms(list) { 575 if (!Array.isArray(list)) return; 576 list.forEach((r) => { 577 const rid = r && (r.id || r.room_id); 578 if (rid && !_bgJoined.has(rid) && !_isLeftRoom(rid)) { 579 _bgJoined.add(rid); 580 bgSocket.emit('room:join', { roomId: rid }); 581 } 582 }); 583 } 584 bgSocket.on('rooms:list', (list) => bgJoinRooms(list)); 585 // ìë¡ ë§ë¤ì´ì§ ë°©ë ì¦ì 구ë 586 bgSocket.on('room:created', (room) => { if (room) bgJoinRooms([room]); }); 587 588 // ð [ì¶ê°] ì ë©ìì§ ìì ì 미ì½ì ì¹´ì´í¸ ì¦ê° (모ë¬ì´ ë«í ìì ëë§) 589 bgSocket.on('message:receive', (msg) => { 590 if (!msg) return; 591 if (isOpen) return; // ë³´ê³ ìë ì¤ì´ë©´ ì¹´ì´í¸íì§ ìì 592 const myId = String(config.userId || ''); 593 if (myId && String(msg.userId) === myId) return; // ë´ê° ë³´ë¸ ë©ìì§ ì ì¸ 594 document.dispatchEvent(new CustomEvent('wprc:unread', { detail: { count: 1 } })); 595 596 // ì ë©ìì§ ì림 â ì°ì¸¡ ìë¨ í ì¤í¸ + ë¹íì (í´ë¦íë©´ ì±í ì°½ì´ ì´ë¦°ë¤) 597 if (window.WPRC_Notify) { 598 window.WPRC_Notify.notify({ 599 title: msg.displayName || 'ì ë©ìì§', 600 body: msg.image ? '(ì¬ì§)' : msg.text, 601 room: msg.roomName || '', 602 avatar: msg.avatarUrl || '', 603 onClick: () => { try { openModal(); } catch (e) { /* 무ì */ } }, 604 }); 605 } 606 }); 607 // ============================================================== 608 } 609 610 if (document.readyState === 'loading') { 611 document.addEventListener('DOMContentLoaded', () => setTimeout(bgConnect, 1000)); 612 } else { 613 setTimeout(bgConnect, 1000); 614 } 615 616 function resetBadge() { 617 unread = 0; 618 if (badge) badge.style.display = 'none'; 619 } 620 621 document.addEventListener('wprc:unread', (e) => { 622 if (isOpen) return; 623 unread += (e.detail?.count || 1); 624 if (badge) { 625 badge.textContent = unread > 99 ? '99+' : unread; 626 badge.style.display = 'block'; 627 } 628 }); 629 630 // â ï¸ loadRoomsFromDB / loadHistoryFromDB / backupRoomToLocal ì 631 // chat-client-mobile.js ë¡ ì´ê´ë¨ (ìì½ë íì´ì§ì ëì ì¼ì¹, ì¤ë³µ ìì² ì ê±°). 632 633})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.