PageSourceSearch

https://www.swn.kr/wp-content/plugins/wp-realtime-chat/assets/js/chat-float.js?ver=1790251083

js swn.kr collected 2026-09-24 12:06:11 UTC 32,214 bytes, 633 lines download raw bytes

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, '&lt;')
20            .replace(/>/g, '&gt;')
21            .replace(/"/g, '&quot;')
22            .replace(/'/g, '&#039;');
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="닫기">&#x2715;</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;">&#x2715;</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;">&#x2715;</button>
313                <div style="font-size:34px;line-height:1;margin-bottom:14px;" aria-hidden="true">&#128246;</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.