PageSourceSearch

https://aya-club.pages.dev/

html aya-club.pages.dev collected 2026-10-03 06:32:41 UTC 191,154 bytes, 3,749 lines download raw bytes

1<!DOCTYPE html>
2<html lang="ko">
3
4<head>
5  
5<script src="js/anti-devtools.js"></script>
5
6  <base target="_top">
7  <meta charset="UTF-8">
8  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
9
10  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⛪</text></svg>">
11  <link rel="manifest" href="manifest.json">
12  <meta name="theme-color" content="#5d4037" id="meta-theme-color">
13  <meta name="mobile-web-app-capable" content="yes">
14  <meta name="apple-mobile-web-app-capable" content="yes">
15  <meta name="apple-mobile-web-app-status-bar-style" content="default">
16  <meta name="apple-mobile-web-app-title" content="HOLY SPARK">
17  <link rel="apple-touch-icon" sizes="180x180" href="icons/icon-180.png">
18  <link rel="stylesheet" href="css/style.css">
19  <title>HOLY SPARK</title>
20  <meta name="description" content="HOLY SPARK - 교회 청년부 커뮤니티 플랫폼">
21  <meta property="og:title" content="HOLY SPARK">
22  <meta property="og:description" content="교회 청년부를 위한 소셜 커뮤니티 및 게임 플랫폼">
23  <meta property="og:type" content="website">
24
25  <!-- Jspreadsheet CE & Jsuites: lazy-loaded on demand (see loadSpreadsheet()) -->
26</head>
27
28<body>
29
30  <!-- 인앱 브라우저(카카오톡 등) 감지 및 외부 브라우저 유도 -->
31  <div id="inapp-browser-overlay" style="
32    display:none; position:fixed; top:0; left:0; width:100%; height:100%;
33    background:rgba(255,255,255,0.97); z-index:999999;
34    flex-direction:column; align-items:center; justify-content:center;
35    padding:30px; box-sizing:border-box; text-align:center; font-family:sans-serif;">
36    <div style="max-width:360px;">
37      <div style="font-size:60px; margin-bottom:16px;">🔒</div>
38      <h2 id="txt-inapp-title" style="margin:0 0 12px; color:#333; font-size:20px; border:none;">외부 브라우저에서 열어주세요</h2>
39      <p id="txt-inapp-desc" style="color:#666; font-size:14px; line-height:1.6; margin-bottom:20px;">
40        <span id="txt-inapp-desc-line1">카카오톡 등 인앱 브라우저에서는 Google 로그인이 차단됩니다.</span><br>
41        <span id="txt-inapp-desc-line2">아래 버튼을 눌러 <strong>Chrome / Safari</strong>에서 열어주세요.</span>
42      </p>
43      <button id="btn-open-external" onclick="openInExternalBrowser()" style="
44        background:#4285F4; color:#fff; border:none; padding:14px 28px;
45        border-radius:8px; font-size:16px; font-weight:bold; cursor:pointer;
46        box-shadow:0 2px 8px rgba(66,133,244,0.4); margin-bottom:14px; width:100%;">
47        <span id="txt-open-external">외부 브라우저로 열기</span>
48      </button>
49      <p id="txt-inapp-alt" style="color:#999; font-size:12px; line-height:1.5; margin-top:8px;">
50        <span id="txt-inapp-alt-line1">또는 우측 상단 <strong>⋮</strong> 메뉴에서</span><br>
51        <span id="txt-inapp-alt-line2"><strong>"다른 브라우저로 열기"</strong>를 선택해주세요.</span>
52      </p>
53      <button id="btn-just-browse" onclick="document.getElementById('inapp-browser-overlay').style.display='none'" style="
54        background:none; border:1px solid #ccc; padding:10px 20px; border-radius:6px;
55        color:#888; font-size:13px; cursor:pointer; margin-top:10px;">
56        <span id="txt-just-browse">그냥 둘러보기 (로그인 불가)</span>
57      </button>
58    </div>
59  </div>
60
61  
61<script>
62  // 인앱 브라우저 감지 (카카오톡, 인스타그램, 페이스북, 네이버, 라인 등)
63  function isInAppBrowser() {
64    var ua = navigator.userAgent || navigator.vendor || '';
65    return /KAKAOTALK|FBAN|FBAV|Instagram|NAVER|Line\/|SamsungBrowser\/.*CrossApp/i.test(ua);
66  }
67
68  // 외부 브라우저로 열기
69  function openInExternalBrowser() {
70    var url = window.location.href;
71    var ua = navigator.userAgent || '';
72
73    if (/KAKAOTALK/i.test(ua)) {
74      // 카카오톡 전용: kakaotalk 스킴으로 외부 브라우저 열기
75      window.location.href = 'kakaotalk://web/openExternal?url=' + encodeURIComponent(url);
76      return;
77    }
78
79    if (/android/i.test(ua)) {
80      // Android: Intent 스킴으로 Chrome 열기
81      var intentUrl = 'intent://' + url.replace(/https?:\/\//, '') +
82        '#Intent;scheme=https;action=android.intent.action.VIEW;end';
83      window.location.href = intentUrl;
84      return;
85    }
86
87    // iOS 등 기타: Safari에서 열기 시도
88    window.open(url, '_system');
89  }
90
91  // Google 로그인 시도 시 인앱 브라우저 체크
92  var _originalHandleGoogleLogin;
93  window.addEventListener('DOMContentLoaded', function() {
94    if (typeof window.handleGoogleLogin === 'function') {
95      _originalHandleGoogleLogin = window.handleGoogleLogin;
96    }
97  });
98
99  function checkInAppBeforeLogin() {
100    if (isInAppBrowser()) {
101      document.getElementById('loginModal').style.display = 'none';
102      document.getElementById('inapp-browser-overlay').style.display = 'flex';
103      return true;
104    }
105    return false;
106  }
107
108  // 페이지 로드 시 인앱 브라우저이면 안내 표시
109  if (isInAppBrowser()) {
110    document.addEventListener('DOMContentLoaded', function() {
111      document.getElementById('inapp-browser-overlay').style.display = 'flex';
112    });
113  }
114  </script>
114
115
116  <div class="ticker-container" id="noticeTicker" style="display:none;">
117    <div class="ticker-content" id="tickerText"></div>
118  </div>
119
120  <div id="langModal" class="modal" style="display:none;">
121    <div class="modal-content retro-box" style="text-align:center;">
122      <span class="close" onclick="closeLangModal()">X</span>
123      <h2 style="border:none;">🌐 Language Select</h2>
124      <button class="lang-btn lang-ko" onclick="setLanguage('ko')">한국어</button>
125      <button class="lang-btn lang-en" onclick="setLanguage('en')">English</button>
126    </div>
127  </div>
128
129  <div class="nav-bar">
130    <div class="nav-top-row">
131      <div class="app-title secret-trigger" onclick="triggerSecret()">
132        <div class="church-icon">⛪</div>
133        <span id="txt-app-title">HOLY SPARK</span>
134      </div>
135
136      <!-- 불씨 위젯 -->
137      <div id="emberWidget" class="ember-container auth-hidden" onclick="emberSystem.showDetail()" style="display:none;" title="나의 불씨"></div>
138
139      <div class="login-bar">
140        <span id="header-profile-img" class="header-avatar auth-hidden" onclick="openUserProfile()"
141          style="display:none; cursor:pointer; margin-right:5px; vertical-align:middle; width:30px; height:30px; background:#ddd; border-radius:50%; border:1px solid #ccc; text-align:center; line-height:30px; font-size:18px; overflow:hidden;">👤</span>
142        <span id="welcome-msg" class="user-name-link auth-hidden" onclick="openUserProfile()" style="display:none;"></span>
143        <!-- Row 1: 알림, 접속자, 앱설치, 로그인/로그아웃 -->
144        <button id="btn-notif" class="nav-dropdown-item auth-hidden login-row1" onclick="openNotificationCenter()"
145          style="display:none; position:relative;" title="알림센터">
146          <span>🔔</span><span class="txt-nav-notif" id="txt-nav-notif">알림</span><span id="notifBadge" class="notif-badge" style="display:none;">0</span>
147        </button>
148        <button id="btn-online" class="nav-dropdown-item login-row1" onclick="toggleOnlinePanel()" title="접속자 목록">
149          <span>👥</span><span id="onlineCount">0</span>
150        </button>
151        <button id="pwa-install-btn" class="nav-dropdown-item login-row1" onclick="promptPWAInstall()" title="앱 설치" style="display:none;">
152          <span>📲</span><span class="txt-nav-install" id="txt-nav-install">설치</span>
153        </button>
154        <button id="btn-login" class="nav-dropdown-item login-row1" onclick="document.getElementById('loginModal').style.display='flex'" title="로그인">
155          <span>🔑</span><span id="btn-login-text">로그인</span>
156        </button>
157        <button id="btn-logout" class="nav-dropdown-item auth-hidden login-row1" onclick="doLogout()" style="display:none;" title="로그아웃">
158          <span>🚪</span><span id="btn-logout-text">로그아웃</span>
159        </button>
160        <button id="btn-stealth-mobile" class="nav-dropdown-item mobile-direct-btn login-row1 auth-hidden" onclick="toggleStealth()" title="숨기기" style="display:none;">
161          <span>👻</span><span class="txt-nav-stealth">숨기기</span>
162        </button>
163        <!-- 설정 드롭다운 (데스크탑 전용) -->
164        <div class="nav-dropdown-wrap" id="navDropdownWrap">
165          <button class="nav-dropdown-item nav-dropdown-trigger" onclick="toggleNavDropdown()" title="설정">
166            <span>⚙️</span><span class="txt-nav-settings">설정</span>
167          </button>
168          <div class="nav-dropdown-menu" id="navDropdownMenu">
169            <button id="btn-weather" class="nav-dropdown-item" onclick="toggleWeather(); toggleNavDropdown();" title="날씨 효과">
170              <span>🌤️</span><span class="txt-nav-weather">날씨</span>
171            </button>
172            <button id="btn-pet-toggle" class="nav-dropdown-item" onclick="togglePetSystem(); toggleNavDropdown();" title="펫 켜기/끄기">
173              <span>🐣</span><span class="txt-nav-pet">펫</span>
174            </button>
175            <button id="btn-darkmode" class="nav-dropdown-item" onclick="toggleDarkMode(); toggleNavDropdown();" title="다크 모드">
176              <span>🌙</span><span class="txt-nav-dark">다크</span>
177            </button>
178            <button id="btn-stealth" class="nav-dropdown-item auth-hidden" onclick="toggleStealth(); toggleNavDropdown();" title="숨기기" style="display:none;">
179              <span>👻</span><span class="txt-nav-stealth">숨기기</span>
180            </button>
181            <button id="btn-recap-dropdown" class="nav-dropdown-item auth-hidden" onclick="if(typeof showWeeklyRecap==='function') showWeeklyRecap(); toggleNavDropdown();" title="주간리캡" style="display:none;">
182              <span>📊</span><span class="txt-nav-recap">리캡</span>
183            </button>
184            <button id="btn-lang-dropdown" class="nav-dropdown-item" onclick="openLangModal(); toggleNavDropdown();" title="언어선택">
185              <span>🌐</span><span class="txt-nav-lang">언어</span>
186            </button>
187          </div>
188        </div>
189        <!-- Row 2: 날씨, 펫, 다크모드 (모바일 직접 표시) -->
190        <button class="nav-dropdown-item mobile-direct-btn login-row2" onclick="toggleWeather()" title="날씨 효과">
191          <span>🌤️</span><span class="txt-nav-weather">날씨</span>
192        </button>
193        <button class="nav-dropdown-item mobile-direct-btn login-row2" onclick="togglePetSystem()" title="펫 켜기/끄기">
194          <span>🐣</span><span class="txt-nav-pet">펫</span>
195        </button>
196        <button class="nav-dropdown-item mobile-direct-btn login-row2" onclick="openLangModal()" title="언어선택">
197          <span>🌐</span><span class="txt-nav-lang">언어</span>
198        </button>
199        <button class="nav-dropdown-item mobile-direct-btn login-row2" onclick="toggleDarkMode()" title="다크 모드">
200          <span>🌙</span><span class="txt-nav-dark">다크</span>
201        </button>
202      </div>
203    </div>
204
205    <div class="menu-grid">
206      <button class="pixel-btn btn-visit" onclick="openModal('visitModal')"><span class="menu-icon">🏃</span><span
207          id="txt-menu-visit">심방신청</span></button>
208      <button class="pixel-btn btn-prayer" onclick="openPrayerBoard()"><span class="menu-icon">🙏</span><span
209          id="txt-menu-prayer">기도나눔</span></button>
210      <button class="pixel-btn btn-ride" onclick="openModal('rideModal')"><span class="menu-icon">🚗</span><span
211          id="txt-menu-ride">라이드</span></button>
212      <button class="pixel-btn btn-member" onclick="if(window.bibleReader) bibleReader.open();"><span class="menu-icon">📖</span><span
213          id="txt-menu-member">성경읽기</span></button>
214      <button class="pixel-btn btn-gallery" onclick="openArchiveModal()">
215        <span class="menu-icon">🏡</span><span id="txt-menu-archive">나눔터</span>
216      </button>
217      <button class="pixel-btn btn-qt" onclick="openQTModal()">
218        <span class="menu-icon">📖</span><span id="txt-menu-qt">QT나눔</span>
219      </button>
220      <button class="pixel-btn btn-bible-world" onclick="openBibleWorld()">
221        <span class="menu-icon">📖</span><span id="txt-menu-play">바이블 월드</span>
222      </button>
223      <button class="pixel-btn btn-worship" onclick="openWorshipModal()">
224        <span class="menu-icon">📖</span><span id="txt-menu-worship">말씀과 교재</span>
225      </button>
226      <button class="pixel-btn btn-talent" onclick="openTalent()">
227        <span class="menu-icon">💰</span><span id="txt-menu-talent">달란트</span>
228      </button>
229      <button class="pixel-btn btn-mission" onclick="if(window.missionSystem) missionSystem.show();">
230        <span class="menu-icon">📋</span><span id="txt-menu-mission">일일 미션</span>
231      </button>
232      <button class="pixel-btn btn-prayerroom" onclick="if(window.prayerRoom) prayerRoom.open();">
233        <span class="menu-icon">🕯️</span><span id="txt-menu-prayerroom">기도실</span>
234      </button>
235      <div class="btn-challenge-wrap">
236        <button class="pixel-btn btn-challenge" onclick="if(window.challengeSystem) challengeSystem.show();">
237          <span class="menu-icon">⚔️</span><span id="txt-menu-challenge">도전장</span>
238        </button>
239        <span id="challengeBadge" class="challenge-badge" style="display:none;"></span>
240      </div>
241    </div>
242  </div>
243
244  <div class="main-layout-wrapper">
245    <div id="mainNoticeWrapper" class="main-notice-wrapper">
246      <div class="main-notice-box" onclick="toggleMainNoticeContent()">
247        <div class="main-notice-header">
248          <span class="main-notice-badge" id="txt-notice-badge">필독</span>
249          <span id="mainNoticeTitleDisplay">공지사항 제목 로딩중...</span>
250          <span style="margin-left:auto; font-size:12px; color:#888;">▼</span>
251        </div>
252        <div id="mainNoticeContentDisplay" class="main-notice-content"></div>
253      </div>
254    </div>
255
256    <!-- 🏠 싸이월드 미니홈피 (Mini-Homepage) -->
257    <div id="minihp-container"
258      style="max-width:920px; margin:20px auto; text-align:center; position:relative; z-index:1; overflow:visible;">
259
260      <!-- ★ NEW: 스크롤 패스스루 오버레이 & 안내 메시지 -->
261      <div id="minihp-overlay"
262        style="position:absolute; top:0; left:0; width:100%; height:100%; z-index:2; cursor:pointer; display:flex; align-items:center; justify-content:center; transition: background 0.3s;">
263        <div id="minihp-hint"
264          style="background:rgba(0,0,0,0.6); color:#fff; padding:8px 16px; border-radius:30px; font-size:13px; pointer-events:none; opacity:0; transition:opacity 0.3s;">
265          <span id="txt-minihp-hint">🖱️ 클릭하여 조작하기</span></div>
266      </div>
267
268      <iframe id="minihp-frame"
269        style="width:100%; height:560px; border:none; border-radius:15px 15px 0 0; box-shadow:0 8px 20px rgba(0,0,0,0.15); background:transparent; position:relative; z-index:1;"
270        src="about:blank" title="Mini-Homepage" scrolling="no" allow="autoplay"></iframe>
271
272      <!-- ✅ iframe 높이 드래그 리사이즈 핸들 -->
273      <div id="iframeResizeHandle" class="feed-resize-handle" style="max-width:100%; border-radius:0 0 15px 15px; position:relative; z-index:3;">
274        <div class="feed-resize-grip">⋯</div>
275      </div>
276
277    </div>
278
279    
279<script>
280      /**
281       * ★ NEW: 미니홈피 iframe 스크롤 패스스루 로직
282       */
283      (function () {
284        var container = document.getElementById('minihp-container');
285        var overlay = document.getElementById('minihp-overlay');
286        var hint = document.getElementById('minihp-hint');
287        var iframe = document.getElementById('minihp-frame');
288        if (!overlay || !iframe) return;
289
290        var hideTimeout = null;
291        var isActive = false; // iframe 조작 모드 여부
292        var _mainFeedMode = false; // 메인피드 모드일 때 deactivateIframe 차단
293        var ACTIVE_TIMEOUT_PC = 10000;     // PC: 10초 후 자동 복구
294        var ACTIVE_TIMEOUT_MOBILE = 10000; // 모바일: 10초 후 자동 복구
295
296        // ── 유틸리티 ──
297        function isMobile() {
298          return window.innerWidth <= 768 || 'ontouchstart' in window;
299        }
300
301        function getTimeout() {
302          return isMobile() ? ACTIVE_TIMEOUT_MOBILE : ACTIVE_TIMEOUT_PC;
303        }
304
305        // ── 활성화: iframe 조작 모드 ──
306        function activateIframe() {
307          if (isActive) return;
308          isActive = true;
309
310          overlay.style.pointerEvents = 'none';
311          overlay.style.background = 'transparent';
312          hint.style.opacity = '0';
313
314          // iframe이 터치 이벤트를 받을 수 있도록 설정
315          iframe.style.pointerEvents = 'auto';
316
317          // 자동 복구 타이머 시작
318          resetAutoRestore();
319
320          // 활성 상태 시각적 피드백 (미세한 테두리)
321          iframe.style.boxShadow = '0 0 0 3px rgba(108, 155, 207, 0.5), 0 8px 20px rgba(0,0,0,0.15)';
322        }
323
324        // ── 비활성화: 스크롤 모드로 복구 ──
325        function deactivateIframe() {
326          if (_mainFeedMode) return; // 메인피드 모드에서는 비활성화 차단
327          if (!isActive) return;
328          isActive = false;
329
330          overlay.style.pointerEvents = '';
331          overlay.style.background = '';
332          iframe.style.pointerEvents = 'none'; // 터치 이벤트 차단
333
334          // 시각적 피드백 제거
335          iframe.style.boxShadow = '0 8px 20px rgba(0,0,0,0.15)';
336
337          clearTimeout(hideTimeout);
338        }
339
340        // ── 자동 복구 타이머 ──
341        function resetAutoRestore() {
342          clearTimeout(hideTimeout);
343          hideTimeout = setTimeout(function () {
344            deactivateIframe();
345            // 복구 시 부드러운 힌트 표시
346            if (isMobile()) {
347              var _cl3 = localStorage.getItem('selectedLang') || 'ko';
348              showMobileHint(_cl3 === 'en' ? 'Switched to scroll mode. Tap to interact again.' : '스크롤 모드로 전환되었습니다. 탭하여 다시 조작하세요.');
349            }
350          }, getTimeout());
351        }
352
353        // ── 모바일 힌트 메시지 ──
354        function showMobileHint(msg) {
355          hint.textContent = msg;
356          hint.style.opacity = '1';
357          setTimeout(function () {
358            hint.style.opacity = '0';
359            // 원래 힌트 텍스트 복구
360            setTimeout(function () {
361              var _cl = localStorage.getItem('selectedLang') || 'ko';
362              hint.textContent = isMobile() ? (_cl === 'en' ? '👆 Tap to interact' : '👆 탭하여 미니홈피 조작하기') : (_cl === 'en' ? '🖱️ Click to interact' : '🖱️ 클릭하여 조작하기');
363            }, 300);
364          }, 2000);
365        }
366
367        // ══════════════════════════════
368        // 초기 상태: iframe 터치 차단
369        // ══════════════════════════════
370        iframe.style.pointerEvents = 'none';
371
372        // 모바일용 힌트 텍스트 설정
373        if (isMobile()) {
374          var _cl2 = localStorage.getItem('selectedLang') || 'ko';
375          hint.textContent = _cl2 === 'en' ? '👆 Tap to interact' : '👆 탭하여 미니홈피 조작하기';
376        }
377
378        // ══════════════════════════════
379        // PC: 마우스 이벤트
380        // ══════════════════════════════
381        container.addEventListener('mouseenter', function () {
382          if (!isActive) {
383            hint.style.opacity = '1';
384          }
385        });
386
387        container.addEventListener('mouseleave', function () {
388          hint.style.opacity = '0';
389          deactivateIframe();
390        });
391
392        // PC: 오버레이 클릭 → 활성화
393        overlay.addEventListener('click', function (e) {
394          // 모바일 터치에서 발생하는 click은 무시 (touchend에서 처리)
395          if (e.sourceCapabilities && e.sourceCapabilities.firesTouchEvents) return;
396          activateIframe();
397        });
398
399        // ══════════════════════════════
400        // 모바일: 터치 이벤트 (핵심)
401        // ══════════════════════════════
402        var touchStartY = 0;
403        var touchStartX = 0;
404        var touchStartTime = 0;
405        var touchMoved = false;
406        var MOVE_THRESHOLD = 10;
406 // px - 이 이상 이동하면 스크롤로 판정
407
408        overlay.addEventListener('touchstart', function (e) {
409          touchStartY = e.touches[0].clientY;
410          touchStartX = e.touches[0].clientX;
411          touchStartTime = Date.now();
412          touchMoved = false;
413        }, { passive: true });
414
415        overlay.addEventListener('touchmove', function (e) {
416          var deltaY = Math.abs(e.touches[0].clientY - touchStartY);
417          var deltaX = Math.abs(e.touches[0].clientX - touchStartX);
418
419          if (deltaY > MOVE_THRESHOLD || deltaX > MOVE_THRESHOLD) {
420            touchMoved = true;
421          }
422
423          // ★ 핵심: 스크롤 의도일 때 오버레이가 터치를 가로채고 있으므로
424          // 부모 스크롤이 자연스럽게 동작함 (passive: true이므로 preventDefault 안 함)
425        }, { passive: true });
426
427        overlay.addEventListener('touchend', function (e) {
428          var elapsed = Date.now() - touchStartTime;
429
430          if (!touchMoved && elapsed < 300) {
431            // ── 탭 (짧은 터치, 이동 없음) → 활성화 ──
432            e.preventDefault(); // 탭 시에만 기본 동작 방지
433            activateIframe();
434
435            // 활성화 피드백
436            var _cl4 = localStorage.getItem('selectedLang') || 'ko';
437            showMobileHint(_cl4 === 'en' ? 'Interactive mode!' : '미니홈피 조작 모드!');
438          }
439          // 드래그(스크롤)인 경우: 아무것도 안 함 → 부모 스크롤 유지
440        });
441
442        // ══════════════════════════════
443        // iframe 밖 터치/클릭 → 비활성화
444        // ══════════════════════════════
445        document.addEventListener('touchstart', function (e) {
446          if (!isActive) return;
447
448          // iframe/container 영역 밖을 터치하면 비활성화
449          if (!container.contains(e.target)) {
450            deactivateIframe();
451          }
452        }, { passive: true });
453
454        document.addEventListener('click', function (e) {
455          if (!isActive) return;
456          if (!container.contains(e.target)) {
457            deactivateIframe();
458          }
459        });
460
461        // ══════════════════════════════
462        // iframe 내부 활동 감지 → 타이머 리셋
463        // ══════════════════════════════
464        // iframe 내부에서 메시지가 오면 타이머 리셋 (활발히 사용 중)
465        window.addEventListener('message', function (e) {
466          if (isActive && e.source === iframe.contentWindow) {
467            resetAutoRestore();
468          }
469        });
470
471        // ★ iframe 위에서의 터치 활동도 감지 (pointerdown으로)
472        // iframe이 활성 상태일 때 container 영역 내 터치 → 타이머 리셋
473        container.addEventListener('touchstart', function (e) {
474          if (isActive) {
475            resetAutoRestore();
476          }
477        }, { passive: true });
478
479        // ══════════════════════════════
480        // ESC 키 → 비활성화 (PC)
481        // ══════════════════════════════
482        document.addEventListener('keydown', function (e) {
483          if (e.key === 'Escape' && isActive) {
484            deactivateIframe();
485            hint.style.opacity = '0';
486          }
487        });
488
489        // ══════════════════════════════
490        // 화면 회전/리사이즈 대응
491        // ══════════════════════════════
492        window.addEventListener('resize', function () {
493          if (isActive) {
494            deactivateIframe();
495          }
496          // 힌트 텍스트 업데이트
497          var _cl5 = localStorage.getItem('selectedLang') || 'ko';
498          hint.textContent = isMobile() ? (_cl5 === 'en' ? '👆 Tap to interact' : '👆 탭하여 미니홈피 조작하기') : (_cl5 === 'en' ? '🖱️ Click to interact' : '🖱️ 클릭하여 조작하기');
499        });
500
501        // ══════════════════════════════
502        // 페이지 가시성 변경 시 비활성화
503        // ══════════════════════════════
504        document.addEventListener('visibilitychange', function () {
505          if (document.hidden && isActive) {
506            deactivateIframe();
507          }
508        });
509
510        // ══════════════════════════════
511        // ★ 메인피드 모드: 오버레이 완전 비활성화
512        // ══════════════════════════════
513        window._setIframeInteractive = function(active) {
514          if (active) {
515            // 메인피드 모드: 오버레이 숨기고 iframe 항상 터치 가능
516            _mainFeedMode = true;
517            clearTimeout(hideTimeout);
518            isActive = true;
519            // CSS에 !important가 있으므로 setProperty로 강제 적용
520            overlay.style.setProperty('display', 'none', 'important');
521            overlay.style.pointerEvents = 'none';
522            hint.style.setProperty('display', 'none', 'important');
523            iframe.style.setProperty('pointer-events', 'auto', 'important');
524            iframe.style.boxShadow = '0 8px 20px rgba(0,0,0,0.15)';
525          } else {
526            // 미니홈피 모드: 오버레이 복원
527            _mainFeedMode = false;
528            overlay.style.removeProperty('display');
529            overlay.style.pointerEvents = '';
530            hint.style.removeProperty('display');
531            iframe.style.removeProperty('pointer-events');
532            deactivateIframe();
533          }
534        };
535
536      })();
537    </script>
537
538
539    
539<script>
540      // ✅ [Fix] ReferenceError: setLanguage & GAS URL
541      // 이 함수는 javascript.html 로드 전에도 실행될 수 있으므로 전역에 선언합니다.
542      window.setLanguage = function (lang) {
543        document.getElementById('langModal').style.display = 'none';
544        localStorage.setItem('selectedLang', lang);
545
546        // javascript.html이 로드된 후라면 실제 번역 적용
547        if (typeof setLanguage === 'function' && window.setLanguage !== this) {
548          setLanguage(lang);
549        } else {
550          // 로드 전이라면 간단한 UI 처리만 (임시)
551          const titleEl = document.getElementById('txt-app-title');
552          if (titleEl) {
553            if (lang === 'ko') titleEl.textContent = '나눔의 장';
554            else titleEl.textContent = '나눔의 장';
555          }
556        }
557      };
558
559      // 초기 언어 설정 확인
560      (function () {
561        const savedLang = localStorage.getItem('selectedLang');
562        if (savedLang) {
563          document.getElementById('langModal').style.display = 'none';
564        }
565      })();
566    </script>
566
567
568    <div id="homeFeedWrapper" class="home-feed-wrapper">
569      <div class="home-feed-box">
570        <div class="home-feed-header">
571          <span class="home-feed-badge" id="txt-feed-badge">새로운 글</span>
572
573          <div class="feed-nav-group">
574            <button id="btnFeedOld" class="feed-nav-btn" onclick="moveFeed(1)" title="이전 글(Old)">◀</button>
575            <button id="btnFeedNew" class="feed-nav-btn" onclick="moveFeed(-1)" title="최신 글(New)">▶</button>
576            <button class="feed-nav-btn" onclick="loadHomeFeed()" title="새로고침" style="margin-left:5px;">⟳</button>
577          </div>
578        </div>
579        <div id="homeFeedList" class="home-feed-list" style="min-height: 50px;">Loading...</div>
580      </div>
581    </div>
582
583    <!-- Chat Container (Moved Outside & Collapsible) -->
584    <div id="chatWrapper" class="home-feed-wrapper" style="margin-bottom: 25px;">
585      <div class="home-feed-box">
586        <div class="home-feed-header" onclick="toggleChatContent()" style="cursor:pointer;">
587          <span class="home-feed-badge" style="background:#ff9800;" id="txt-chat-badge">💬 실시간 채팅</span>
588          <span style="font-size:12px; color:#666; margin-left:10px;" id="txt-chat-badge-sub">(Live Chat)</span>
589          <span id="chatToggleIcon" style="margin-left:auto; font-size:12px; color:#888;">▼</span>
590        </div>
591
592        <div id="chatContentArea">
593          <div id="chat-messages" class="chat-messages-box" style="height: 200px; padding: 10px; background: #fffbf0;">
594            <div class="chat-msg system-msg" style="color:#888; text-align:center; margin-top:50px;">
595              <span id="txt-chat-loading">불러오는 중... (Loading...)</span>
596            </div>
597          </div>
598
599          <!-- Modified Layout: Name | Send Button, then Message Input below -->
600          <form id="chat-form" class="chat-form"
601            style="padding: 10px; background: #eee; border-top: 2px solid #ddd; display:block;">
602            <div style="display:flex; justify-content:space-between; align-items:center; gap:5px; margin-bottom:5px;">
603              <input type="text" id="chat-input-name" placeholder="이름 (Guest)" data-placeholder-ko="이름 (Guest)" data-placeholder-en="Name (Guest)" style="width:100%; font-weight:bold;"
604                required>
605              <button type="submit" class="pixel-btn" id="txt-chat-send"
606                style="width:80px; background:var(--primary-color); margin:0; height:38px; white-space:nowrap;">전송</button>
607            </div>
608            <div style="display:flex; align-items:center; gap:5px;">
609              <input type="text" id="chat-input-msg" placeholder="메시지를 입력하세요..." data-placeholder-ko="메시지를 입력하세요..." data-placeholder-en="Enter a message..."
610                style="flex:1; box-sizing:border-box;" required>
611              <button type="button" id="chatEmojiBtn" class="chat-tool-btn" title="이모티콘">😊</button>
612              <button type="button" id="chatFileBtn" class="chat-tool-btn" title="파일 첨부">📎</button>
613              <input type="file" id="chatFileInput" accept="image/*,.pdf,.doc,.docx,.hwp" style="display:none;">
614            </div>
615            <!-- ✅ 파일 미리보기 -->
616            <div id="chatFilePreview" class="chat-file-preview" style="display:none;"></div>
617            <!-- ✅ 이모티콘 피커 -->
618            <div id="chatEmojiPicker" class="emoji-picker" style="display:none;"></div>
619          </form>
620          <!-- ✅ 드래그 리사이즈 핸들 (하단) -->
621          <div id="chatResizeHandle" class="chat-resize-handle">
622            <div class="chat-resize-grip">⋯</div>
623          </div>
624        </div>
625      </div>
626    </div>
627
628    <div class="container">
629      <div class="retro-box floating" id="calendar-container">
630        <div style="display:flex; justify-content:flex-end; gap:15px; margin-bottom:15px;">
631          <label class="toggle-label">
632            <input type="checkbox" id="toggleVisit" checked onchange="renderCalendar(
632)">
633            <span class="custom-checkbox"></span> <span id="txt-chk-visit">심방 보기</span>
634          </label>
635          <label class="toggle-label">
636            <input type="checkbox" id="toggleBirth" checked onchange="renderCalendar()">
637            <span class="custom-checkbox"></span> <span id="txt-chk-birth">생일 보기</span>
638          </label>
639        </div>
640        <div id="calendar"></div>
641      </div>
642    </div>
643  </div>
644
645  <div id="userProfileModal" class="modal">
646    <div class="modal-content retro-box">
647      <span class="close" onclick="closeModal('userProfileModal')">X</span>
648      <h2 id="txt-profile-title">👤 내 정보 (My Profile)</h2>
649
650      <div style="display:flex; align-items:center; gap:15px; margin-bottom:15px;">
651        <div id="up-photo"
652          style="width:60px; height:60px; background:#ddd; border-radius:10px; border:2px solid #5d4037; overflow:hidden; display:flex; align-items:center; justify-content:center; font-size:30px;">
653          👤</div>
654        <div>
655          <div style="font-size:20px; font-weight:bold; margin-bottom:5px;">
656            <span id="up-name">Loading...</span> <span id="txt-profile-nim">님</span>
657          </div>
658          <div style="color:#ff9800; font-weight:bold;">
659            <span id="txt-profile-talent-label">보유 달란트:</span> <span id="up-point">0</span> P
660          </div>
661        </div>
662      </div>
663      <div style="display:flex; gap:5px; flex-wrap:wrap; margin-top:5px;">
664        <button class="pixel-btn" onclick="openNameChangePrompt()"
665          style="background:var(--secondary-color); font-size:12px; padding:5px 10px; width:auto; display:inline-block;"><span id="txt-profile-changename">이름 변경</span></button>
666        <button class="pixel-btn" onclick="openPersonalInfoEdit()"
667          style="background:#9c27b0; color:white; font-size:12px; padding:5px 10px; width:auto; display:inline-block;">📝 개인정보입력</button>
668        <button class="pixel-btn" onclick="openModal('encourageListModal'); loadMyEncouragements('encourageList')"
669          style="background:var(--green-color); font-size:12px; padding:5px 10px; width:auto; display:inline-block;"><span id="txt-profile-encourage">💌 격려 메시지</span></button>
670        <button class="pixel-btn" onclick="openStatsModal()"
671          style="background:var(--blue-color); font-size:12px; padding:5px 10px; width:auto; display:inline-block;"><span id="txt-profile-stats">📊 통계</span></button>
672      </div>
673
674      <!-- QT 스트릭 -->
675      <div id="profileQTStreak" style="margin-top:15px;"></div>
676
677      <div style="margin-bottom:15px; margin-top:15px;">
678        <h3 style="border-bottom:2px solid #ddd; padding-bottom:5px;" id="txt-profile-comments">🔔 내 글에 달린 댓글</h3>
679        <div id="myCommentList" class="notification-list">
680          Loading...
681        </div>
682      </div>
683
684      <div>
685        <div
686          style="display:flex; justify-content:space-between; align-items:center; border-bottom:2px solid #ddd; padding-bottom:5px; margin-bottom:10px;">
687          <h3 style="margin:0;" id="txt-profile-qt-attend">📅 이번 달 QT 개근</h3>
688          <span id="qtMonthLabel" style="font-size:14px; color:#666;">Loading...</span>
689        </div>
690        <div id="qtAttendanceGrid" class="attendance-grid"></div>
691        <div id="qtRewardArea" style="margin-top:10px; text-align:center;"></div>
692      </div>
693    </div>
694  </div>
695
696  <div id="archiveModal" class="modal">
697    <div class="modal-content retro-box">
698      <span class="close" onclick="closeModal('archiveModal')">X</span>
699      <h2 id="txt-arch-title">🏡 나눔터</h2>
700      <div class="tab-container">
701        <button id="btn-share-notice" class="tab-btn" onclick="switchShareTab('notice', this)"><span id="txt-tab-notice">공지사항</span></button>
702        <button id="btn-share-board" class="tab-btn active" onclick="switchShareTab('board', this)"><span id="txt-tab-board">자유게시판</span></button>
703        <button id="btn-share-archive" class="tab-btn" onclick="switchShareTab('archive', this)"><span id="txt-tab-archive">자료실</span></button>
704        <button id="btn-share-gallery" class="tab-btn" onclick="switchShareTab('gallery', this)"><span id="txt-tab-gallery">갤러리</span></button>
705      </div>
706
707      <div id="shareNoticeView" style="display:none;">
708        <div id="noticeAdminArea" style="margin-bottom:10px; display:none; text-align:right;">
709          <button class="pixel-btn" onclick="promptWriteNotice()" style="background:var(--pink-color);"><span id="txt-admin-write-notice">📢 공지 작성 (Admin)</span></button>
710        </div>
711        <div id="noticeList" class="archive-list">Loading...</div>
712      </div>
713
714      <div id="shareBoardView">
715        <div class="tab-container" style="border:none; margin-bottom:5px;">
716          <button id="btn-share-write" class="pixel-btn" style="background:var(--blue-color); width:100%;"
717            onclick="openFreeWrite()"><span id="txt-btn-freewrite">글쓰기 (Write)</span></button>
718        </div>
719        <div id="freeBoardList" class="archive-list">Loading...</div>
720      </div>
721
722      <div id="shareArchiveView" style="display:none;">
723        <div id="btn-arch-upload-wrapper" style="text-align:right; margin-bottom:10px; display:none;">
724          <button id="btn-share-upload" class="pixel-btn"
725            onclick="document.getElementById('archiveUploadForm').style.display='block'"
726            style="background:var(--blue-color); width:auto; padding: 10px 20px;"><span id="txt-btn-upload">+ 자료 등록 (Upload)</span></button>
727        </div>
728        <div id="archiveUploadForm" style="display:none; background:#eee; padding:10px; margin-bottom:10px;">
729          <form onsubmit="handleArchiveSubmit(event)">
730            <input type="text" name="title" placeholder="제목" id="archive-input-title" data-placeholder-ko="제목" data-placeholder-en="Title" required>
731            <input type="text" name="link" placeholder="링크 (URL)" id="archive-input-link" data-placeholder-ko="링크 (URL)" data-placeholder-en="Link (URL)" required>
732            <input type="text" name="desc" placeholder="설명" id="archive-input-desc" data-placeholder-ko="설명" data-placeholder-en="Description">
733            <button class="pixel-btn" style="width:100%; margin-top:5px; background:var(--primary-color);"><span id="txt-btn-register">등록</span></button>
734          </form>
735        </div>
736        <div id="archiveList" class="archive-list">Loading...</div>
737      </div>
738
739      <div id="shareGalleryView" style="display:none;">
740        <div
741          style="background:#fffbf0; padding:10px; margin-bottom:15px; border:3px solid var(--border-color); text-align:center;">
742          <a href="https://www.antiochurch.org/bbs/board.php?bo_table=young_people_gallery" target="_blank"
743            style="text-decoration:none; color:#000; font-weight:bold; font-size:1.1em;"><span id="txt-gallery-church-link">🔗 교회 홈페이지 갤러리 이동 (Click)</span></a>
744        </div>
745        <div style="text-align:right; margin-bottom:10px;">
746          <button class="pixel-btn" onclick="toggleGalleryUpload()"
747            style="background:var(--green-color); width:100%;"><span id="txt-btn-gallery-upload">📸 사진/영상 올리기</span></button>
748        </div>
749        <div id="galleryUploadArea"
750          style="display:none; background:#fffbf0; padding:10px; border:3px solid var(--border-color); margin-bottom:10px;">
751          <form id="galleryForm" onsubmit="handleGalleryUploadMulti(event)">
752            <input type="text" id="galTitle" placeholder="제목 (Title)" required>
753            <div id="fileListContainer" class="file-input-wrapper">
754              <div style="display:flex; align-items:center; gap:5px;">
755                <input type="file" class="gal-file-input" accept="image/*,video/*" required>
756              </div>
757            </div>
758            <button type="button" class="pixel-btn" onclick="addFileSlot()"
759              style="background:var(--light-blue); width:100%; margin-top:5px;" id="txt-gallery-add-btn">+ 추가 (Add Photo/Video)</button>
760            <button type="submit" class="pixel-btn"
761              style="width:100%; margin-top:10px; background:var(--primary-color);" id="txt-gallery-upload-start">업로드 Start</button>
762          </form>
763        </div>
764        <div id="galleryGrid" class="gallery-grid"></div>
765        <div id="galleryPagination" class="pagination"></div>
766      </div>
767    </div>
768  </div>
769
770  <div id="qtModal" class="modal">
771    <div class="modal-content retro-box">
772      <span class="close" onclick="closeModal('qtModal')">X</span>
773      <h2 id="txt-qt-title">📖 QT나눔</h2>
774      <div class="tab-container" style="margin-bottom:10px;">
775        <button id="btn-qt-comic" class="tab-btn" onclick="switchQTTab('comic', this)"><span id="txt-tab-comic">4컷 만화</span></button>
776        <button id="btn-qt-pastor" class="tab-btn" onclick="switchQTTab('pastor', this)"><span id="txt-tab-pastor">목사 QT</span></button>
777        <button id="btn-qt-youth" class="tab-btn" onclick="switchQTTab('youth', this)"><span id="txt-tab-youth">청년 QT</span></button>
778      </div>
779      <div id="qtHomeView">
780        <div id="qtTodayAdminArea" style="margin-bottom:10px; display:none; text-align:right;">
781          <button class="pixel-btn" onclick="promptEditTodayQT()" style="background:var(--pink-color); width:100%;"><span id="txt-admin-qt-edit">✏️ 오늘의 QT 작성/수정 (Admin)</span></button>
782        </div>
783
784        <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
785          <button class="nav-arrow-btn" onclick="navigateTodayQT(1)">◀</button>
786          <h3 style="margin:0;" id="txt-qt-today-title">오늘의 QT 구절</h3>
787          <button class="nav-arrow-btn" onclick="navigateTodayQT(-1)">▶</button>
788        </div>
789
790        <div id="todayQtBox" class="retro-box" style="padding:12px; background:#fff; min-height:150px;">
791          <div id="todayQtTitle" style="font-weight:bold; margin-bottom:6px;">Loading...</div>
792          <div id="todayQtContent" style="white-space:pre-wrap; line-height:1.4;"></div>
793          <div id="todayQtDate" style="font-size:12px;
793 color:#777; margin-top:8px;"></div>
794        </div>
795      </div>
796      <div id="qtComicView" style="display:none;">
797        <button class="pixel-btn qt-back-btn" style="width:100%; margin-bottom:10px;" onclick="switchQTTab('home')"><span class="txt-btn-back">⬅️ 돌아가기</span></button>
798        <div id="qtComicAdminArea" style="margin-bottom:10px; display:none; text-align:right;">
799          <button class="pixel-btn" onclick="promptUploadQTComic()" style="background:var(--pink-color); width:100%;">4ì»·
800            만화 업로드 (Admin)</button>
801        </div>
802        <div id="qtComicList" class="gallery-grid">Loading...</div>
803        <div id="qtComicPagination" class="pagination"></div>
804      </div>
805      <div id="qtPastorView" style="display:none;">
806        <button class="pixel-btn qt-back-btn" style="width:100%; margin-bottom:10px;" onclick="switchQTTab('home')"><span class="txt-btn-back">⬅️ 돌아가기</span></button>
807        <div id="pastorQTAdminArea" style="margin-bottom:10px; display:none; text-align:right;">
808          <button class="pixel-btn" onclick="promptWritePastorQT()" style="background:var(--pink-color); width:100%;">✍️
809            목사 QT 글쓰기 (Admin)</button>
810        </div>
811        <div id="pastorQTList" class="archive-list">Loading...</div>
812        <div id="pastorQTPagination" class="pagination"></div>
813      </div>
814      <div id="qtYouthView" style="display:none;">
815        <button class="pixel-btn qt-back-btn" style="width:100%; margin-bottom:10px;" onclick="switchQTTab('home')"><span class="txt-btn-back">⬅️ 돌아가기</span></button>
816        <div class="tab-container" style="border:none; margin-bottom:5px;">
817          <button class="pixel-btn" style="background:var(--blue-color); width:100%;" onclick="openYouthQTWrite()"><span id="txt-btn-qt-write">글쓰기 (Write)</span></button>
818        </div>
819        <div id="youthQTList" class="archive-list">Loading...</div>
820        <div id="youthQTPagination" class="pagination"></div>
821      </div>
822    </div>
823  </div>
824
825  <div id="visitModal" class="modal">
826    <div class="modal-content retro-box">
827      <span class="close" onclick="closeModal('visitModal')">X</span>
828      <h2 id="txt-visit-title">🏃 심방 신청서</h2>
829      <form onsubmit="handleVisitSubmit(event)">
830        <label id="lbl-name">성함</label><input type="text" name="name" required>
831        <label id="lbl-date">날짜</label><input type="date" name="date" required>
832        <label id="lbl-time">시간</label><input type="time" name="time" required>
833        <label id="lbl-place">장소</label><input type="text" name="place" required>
834        <label id="lbl-visit-email">이메일 (선택 - 신청확인용)</label><input type="email" name="email" placeholder="[email protected]">
835        <button type="submit" class="pixel-btn btn-submit-form" id="btn-submit-visit">🚀 신청서 제출</button>
836      </form>
837    </div>
838  </div>
839
840  <div id="rideModal" class="modal">
841    <div class="modal-content retro-box">
842      <span class="close" onclick="closeModal('rideModal')">X</span>
843      <h2 id="txt-ride-title">🚗 라이드 요청</h2>
844      <form onsubmit="handleRideSubmit(event)">
845        <label class="lbl-name" id="lbl-ride-name">성함</label><input type="text" name="name" required>
846        <label class="lbl-date" id="lbl-ride-date">날짜</label><input type="date" name="date" required>
847        <label class="lbl-time" id="lbl-ride-time">시간</label><input type="time" name="time" required>
848        <label id="lbl-pickup">출발지</label><input type="text" name="pickup" required>
849        <label id="lbl-dest">도착지</label><input type="text" name="dest" required>
850        <label id="lbl-ride-email">이메일 (선택 - 신청확인용)</label><input type="email" name="email" placeholder="[email protected]">
851        <button type="submit" class="pixel-btn btn-submit-form" style="background:var(--green-color);"
852          id="btn-submit-ride">🚙 라이드 요청</button>
853      </form>
854    </div>
855  </div>
856
857  <div id="prayerModal" class="modal">
858    <div class="modal-content retro-box">
859      <span class="close" onclick="closeModal('prayerModal')">X</span>
860      <h2 id="txt-prayer-title">🙏 기도제목 나눔</h2>
861      <div class="tab-container">
862        <button class="tab-btn active" onclick="switchPrayerTab('list', this)" id="btn-tab-board"><span id="txt-tab-prayer-board">게시판</span></button>
863        <button class="tab-btn" onclick="switchPrayerTab('write', this)" id="btn-tab-write"><span id="txt-tab-prayer-write">글쓰기</span></button>
864      </div>
865      <div id="prayerListView">
866        <div id="prayerList" style="margin-top:10px;"></div>
867      </div>
868      <div id="prayerWriteView" style="display:none;">
869        <form onsubmit="handlePrayerSubmit(event)">
870          <label id="lbl-public">공개 설정</label>
871          <select name="type" onchange="toggleSecretMsg(this)">
872            <option value="public" id="opt-public">📢 함께 나눔 (Share with all)</option>
873            <option value="members" id="opt-members">👥 맴버들과 나눔 (Share with members)</option>
874            <option value="secret" id="opt-secret">🔒 목사님께만 나눔 (Only share to Rev.Lee) (이메일)</option>
875          </select>
876          <p id="secretMsg" style="display:none; color:red; margin:5px;"><span id="txt-secret-msg">* 이메일로만 전송됩니다.</span></p>
877          <label class="lbl-writer" id="lbl-prayer-writer">작성자</label><input type="text" name="writer" required>
878          <label class="lbl-title" id="lbl-prayer-title">제목</label><input type="text" name="title" required>
879          <label class="lbl-content" id="lbl-prayer-content">내용</label><textarea name="content" rows="5" required></textarea>
880          <div class="anon-prayer-toggle" style="display:flex; align-items:center; justify-content:space-between; padding:10px 0; margin:8px 0; border-top:1px dashed #ccc;">
881            <span style="font-size:14px; color:#5d4037;" id="txt-anon-prayer">🔒 익명으로 기도 요청</span>
882            <div class="anon-toggle-switch" id="anonPrayerToggle" onclick="this.classList.toggle('active')"
883              style="width:44px; height:24px; background:#ccc; border-radius:12px; position:relative; cursor:pointer; transition:background 0.3s;">
884              <div style="width:20px; height:20px; background:#fff; border-radius:50%; position:absolute; top:2px; left:2px; transition:transform 0.3s; box-shadow:0 1px 3px rgba(0,0,0,0.3);"></div>
885            </div>
886          </div>
887          <style>
888            .anon-toggle-switch.active { background: #4caf50 !important; }
889            .anon-toggle-switch.active > div { transform: translateX(20px); }
890          </style>
891          <button type="submit" class="pixel-btn btn-submit-form" style="background:var(--blue-color);"
892            id="btn-submit-prayer"><span id="txt-btn-submit-prayer">등록하기</span></button>
893        </form>
894      </div>
895      <div id="prayerDetailView" style="display:none;">
896        <button onclick="switchPrayerTab('list')" class="pixel-btn btn-back">
897          < Back</button>
898            <div style="background:#fff; border:3px solid var(--border-color); padding:15px; margin-top:10px;">
899              <h3 id="detailTitle" style="margin:0;"></h3>
900              <div id="detailMeta"
901                style="color:var(--secondary-color); font-size:14px; margin:10px 0; border-bottom:2px dashed #8d6e63;">
902              </div>
903              <div id="detailContent" style="white-space:pre-wrap; margin-bottom:20px;"></div>
904
905              <div style="text-align:center; margin-bottom:20px;">
906                <button class="btn-prayer-like-new" id="btn-prayer-like" onclick="handlePrayerLike()">
907                  🙏 <span id="txt-prayer-like-text">기도하겠습니다</span> <span id="txt-prayer-like-cnt">0</span>
908                </button>
909              </div>
910
911              <div style="display:flex; justify-content:flex-end;">
912                <span style="color:var(--secondary-color); cursor:pointer; text-decoration:underline;"
913                  onclick="promptEditPrayer()">✏️ Edit</span>
914              </div>
915            </div>
916            <div style="margin-top:15px;">
917              <div style="display:flex; gap:5px; margin-bottom:5px;">
918                <input type="text" id="commentName" placeholder="이름" data-placeholder-ko="이름" data-placeholder-en="Name" style="width:80px;">
919                <input type="text" id="commentInput" placeholder="내용..." data-placeholder-ko="내용..." data-placeholder-en="Comment..." style="flex:1;">
920              </div>
921              <button class="pixel-btn" style="background:var(--secondary-color); width:100%; padding:5px;"
922                onclick="submitComment()"><span id="txt-btn-comment-submit">댓글 등록</span></button>
923              <div id="commentList" style="margin-top:10px;"></div>
924            </div>
925      </div>
926    </div>
927  </div>
928
929  <div id="memberModal" class="modal">
930    <div class="modal-content retro-box">
931      <span class="close" onclick="closeModal('memberModal')">X</span>
932      <h2 id="txt-member-title">👥 청년부 멤버</h2>
933      <div class="tab-container">
934        <button class="tab-btn active" onclick="switchMemberTab('search', this)" id="btn-tab-search" style="display:none;"><span id="txt-tab-member-search">조회</span></button>
935        <!-- [Fix] 등록 버튼 숨김 — Google 계정 기반 가입으로 전환됨 -->
936        <button class="tab-btn" onclick="switchMemberTab('register', this)" id="btn-tab-reg" style="display:none;"><span id="txt-tab-member-reg">등록</span></button>
937      </div>
938
939      <div id="memberSearchView">
940        <div style="display:flex; gap:5px; margin-bottom:15px;">
941          <input type="text" id="memberSearchInput" placeholder="Name" onkeyup="if(event.key==='Enter') searchMember()">
942          <button class="pixel-btn" onclick="searchMember()" style="background:var(--primary-color); width:80px;"
943            id="btn-search">Search</button>
944        </div>
945        <div id="memberListArea" style="max-height:300px; overflow-y:auto; border:2px solid #eee; display:none;"></div>
946        <div id="memberDetailCard" style="display:none; margin-top:10px;"></div>
947      </div>
948
949      <div id="memberRegisterView" style="display:none;">
950        <form onsubmit="handleMemberRegister(event)">
951          <label class="lbl-name" id="lbl-reg-name">이름</label><input type="text" name="name" required>
952          <div class="file-input-wrapper"><label class="lbl-photo" id="lbl-reg-photo">프로필 사진 (선택)</label><input type="file"
953              id="memberPhoto" accept="image/*"></div>
954          <label class="lbl-birth" id="lbl-reg-birth">생년월일</label><input type="date" name="birth">
955          <label class="lbl-phone" id="lbl-reg-phone">전화번호</label><input type="tel" name="phone">
956          <label class="lbl-address" id="lbl-reg-address">주소</label><input type="text" name="address">
957          <label class="lbl-email" id="lbl-reg-email">이메일</label><input type="email" name="email">
958          <label class="lbl-hobbies" id="lbl-reg-hobbies">취미</label><textarea name="hobbies"></textarea>
959          <label class="lbl-interests" id="lbl-reg-interests">관심분야</label><textarea name="interests"></textarea>
960          <label class="lbl-favMedia" id="lbl-reg-favMedia">영화/애니</label><textarea name="favMedia"></textarea>
961          <label class="lbl-favStar" id="lbl-reg-favStar">배우/가수</label><textarea name="favStar"></textarea>
962          <label class="lbl-churchReason" id="lbl-reg-churchReason">교회 계기</label><textarea name="churchReason"></textarea>
963          <label class="lbl-jesusReason" id="lbl-reg-jesusReason">신앙 계기</label><textarea name="jesusReason"></textarea>
964          <label class="lbl-bibleLevel" id="lbl-reg-bibleLevel">성경지식</label>
965          <select name="bibleLevel">
966            <option value="1" id="opt-bible-1">1 (거의 모름)</option>
967            <option value="2">2</option>
968            <option value="3">3</option>
969            <option value="4">4</option>
970            <option value="5" id="opt-bible-5">5 (매우 잘 앎)</option>
971          </select>
972          <label class="lbl-favVerse" id="lbl-reg-favVerse">성경구절</label><textarea name="favVerse"></textarea>
973          <label class="lbl-intro" id="lbl-reg-intro">자기소개</label><textarea name="intro"></textarea>
974          <label class="lbl-family" id="lbl-reg-family">가족</label><textarea name="family"></textarea>
975          <label class="lbl-dream" id="lbl-reg-dream">꿈</label><textarea name="dream"></textarea>
976          <label class="lbl-prayerReq" id="lbl-reg-prayerReq">기도제목</label><textarea name="prayer"></textarea>
977          <label class="lbl-wishes" id="lbl-reg-wishes">바라는점</label><textarea name="wishes"></textarea>
978          <label class="lbl-freeTalk" id="lbl-reg-freeTalk">하고픈말</label><textarea name="freeTalk"></textarea>
979          <button type="submit" class="pixel-btn btn-submit-form" id="bt
979n-submit-member"><span id="txt-btn-submit-member">등록하기</span></button>
980        </form>
981      </div>
982    </div>
983  </div>
984
985  <div id="adminModal" class="modal">
986    <div class="modal-content retro-box">
987      <span class="close" onclick="closeModal('adminModal')">X</span>
988      <h2>🛠 Admin</h2>
989
990      <!-- 탭 버튼 확장 -->
991      <div class="tab-container">
992        <button class="tab-btn active" onclick="showAdminForm('event', this)"><span id="txt-admin-tab-event">일정</span></button>
993        <button class="tab-btn" onclick="showAdminForm('birth', this)"><span id="txt-admin-tab-birth">생일</span></button>
994        <button class="tab-btn" onclick="showAdminForm('talent', this)">💰 달란트</button>
995        <button class="tab-btn" onclick="showAdminForm('excel', this)"
996          style="background:var(--green-color); color:white;">📊 Excel</button>
997        <button class="tab-btn" onclick="showAdminForm('memberList', this)" style="background:#9c27b0; color:white;">👥
998          멤버조회</button>
999        <button class="tab-btn" onclick="showAdminForm('push', this)" style="background:#e91e63; color:white;">📢
1000          푸시알림</button>
1001        <button class="tab-btn" onclick="if(typeof openBossImageAdmin==='function') openBossImageAdmin();" style="background:#FF4500; color:white;">🐉
1002          보스</button>
1003      </div>
1004
1005      <!-- Sync Container -->
1006      <div
1007        style="background:#e3f2fd; padding:10px; margin-bottom:15px; border:2px solid #2196f3; border-radius:8px; display:flex; gap:10px; align-items:center;">
1008        <span style="font-weight:bold; color:#0d47a1;">🔄 Universal:</span>
1009        <select id="syncSheetSelect" style="padding:5px; border:1px solid #1976d2; border-radius:4px;">
1010          <option value="Members">Members (회원)</option>
1011          <option value="Notices">Notices (공지)</option>
1012          <option value="QT_Today">QT_Today</option>
1013          <option value="QT_Pastor">QT_Pastor</option>
1014          <option value="QT_Youth">QT_Youth</option>
1015          <option value="QT_Comic">QT_Comic</option>
1016          <option value="FreeBoard">FreeBoard</option>
1017          <option value="Archive">Archive</option>
1018          <option value="PrayerBoard">PrayerBoard</option>
1019          <option value="Schedule">Schedule</option>
1020          <option value="GalleryInternal">Gallery</option>
1021          <option value="BibleStudy">BibleStudy</option>
1022          <option value="QuizLog">QuizLog</option>
1023          <option value="Votes">Votes</option>
1024          <option value="WorshipSettings">WorshipSettings</option>
1025          <option value="RSVP">RSVP</option>
1026
1027          <option value="MissionFund">MissionFund</option>
1028          <option value="QuizResults">QuizResults</option>
1029          <option value="Pets">Pets (펫)</option>
1030          <option value="PetGuestbook">PetGuestbook</option>
1031          <option value="PetGameLog">PetGameLog</option>
1032          <option value="PetEvolution">PetEvolution</option>
1033          <option value="UserPoints">UserPoints</option>
1034          <option value="BalloonLimits">BalloonLimits</option>
1035          <option value="PointLogs">PointLogs</option>
1036        </select>
1037        <button class="pixel-btn" onclick="window.handleGenericSync()"
1038          style="background:#2196f3; color:white; padding:5px 10px; font-size:12px;">Sync Selected</button>
1039      </div>
1040
1041      <!-- 기존 일정 폼 -->
1042      <div id="adminEventForm">
1043        <form onsubmit="handleAdminAdd(event, '행사')">
1044          <label>Title</label><input type="text" name="title" required>
1045          <label>Date</label><input type="date" name="date" required>
1046          <label>Time</label><input type="time" name="time">
1047          <label>Detail</label><textarea name="detail"></textarea>
1048          <label>Link</label><input type="text" name="link">
1049          <div style="margin: 15px 0; text-align: left;">
1050            <label style="display:flex; align-items:center; gap:8px; cursor:pointer;">
1051              <input type="checkbox" name="useRsvp" style="width:20px; height:20px; display:inline-block; margin:0;">
1052              <span style="font-size:16px; color:var(--text-color); font-weight:bold;">🗳️ 참석 투표(RSVP) 시작</span>
1053            </label>
1054          </div>
1055          <button type="submit" class="pixel-btn btn-submit-form" style="background:var(--blue-color);">Add
1056            Event</button>
1057        </form>
1058      </div>
1059
1060      <!-- 기존 생일 폼 -->
1061      <div id="adminBirthForm" style="display:none;">
1062        <form onsubmit="handleAdminAdd(event, '생일')">
1063          <label>Name</label><input type="text" name="title" required>
1064          <label>Date</label><input type="date" name="date" required>
1065          <button type="submit" class="pixel-btn btn-submit-form" style="background:var(--pink-color);">Add
1066            Birthday</button>
1067        </form>
1068      </div>
1069
1070      <!-- 새로 추가: 달란트 로그 뷰어 -->
1071      <div id="adminTalentForm" style="display:none;">
1072        <div style="margin-bottom:15px; display:flex; gap:10px; align-items:end; flex-wrap:wrap;">
1073          <div style="flex:1; min-width:120px;">
1074            <label style="display:block; margin-bottom:5px; font-size:14px; font-weight:bold;">🔍 전화번호 필터</label>
1075            <input type="text" id="logFilterPhone" placeholder="01012345678"
1076              style="width:100%; padding:8px; border:2px solid #ddd; font-family:monospace;">
1077          </div>
1078
1079          <div style="min-width:100px;">
1080            <label style="display:block; margin-bottom:5px; font-size:14px; font-weight:bold;">📅 기간</label>
1081            <select id="logFilterDays" style="padding:8px; border:2px solid #ddd; width:100%;">
1082              <option value="1">오늘</option>
1083              <option value="7">최근 7일</option>
1084              <option value="30" selected>최근 30일</option>
1085              <option value="90">최근 90일</option>
1086            </select>
1087          </div>
1088
1089          <button class="pixel-btn" onclick="loadPointLogs()"
1090            style="background:var(--blue-color); padding:8px 16px; white-space:nowrap; height:38px;">
1091            🔍 조회
1092          </button>
1093
1094          <button class="pixel-btn" onclick="exportPointLogs()"
1095            style="background:var(--green-color); padding:8px 16px; white-space:nowrap; height:38px;">
1096            📊 내보내기
1097          </button>
1098        </div>
1099
1100        <!-- 요약 통계 -->
1101        <div id="pointLogSummary"
1102          style="display:none; background:#fffbf0; padding:15px; margin-bottom:15px; border:3px solid #ff9800; border-radius:8px;">
1103          <h4 style="margin:0 0 10px 0; color:#f57c00;">📈 요약 통계</h4>
1104          <div id="summaryContent" style="font-size:14px; color:#333;"></div>
1105        </div>
1106
1107        <!-- 로그 테이블 -->
1108        <div id="pointLogContainer"
1109          style="max-height:350px; overflow-y:auto; border:3px solid #5d4037; background:#fff; border-radius:8px;">
1110          <table style="width:100%; border-collapse:collapse; font-size:12px;">
1111            <thead>
1112              <tr style="background:#d7ccc8; position:sticky; top:0; border-bottom:2px solid #5d4037;">
1113                <th style="padding:8px; text-align:left; min-width:130px;">⏰ 시간</th>
1114                <th style="padding:8px; text-align:left; min-width:80px;">👤 이름</th>
1115                <th style="padding:8px; text-align:center; min-width:60px;">💰 포인트</th>
1116                <th style="padding:8px; text-align:left; min-width:100px;">📍 출처</th>
1117                <th style="padding:8px; text-align:left;">📝 상세</th>
1118                <th style="padding:8px; text-align:right; min-width:70px;">💳 잔액</th>
1119              </tr>
1120            </thead>
1121            <tbody id="pointLogTableBody">
1122              <tr>
1123                <td colspan="6" style="text-align:center; padding:20px; color:#999;">조회 버튼을 눌러주세요</td>
1124              </tr>
1125            </tbody>
1126          </table>
1127        </div>
1128
1129        <div style="margin-top:10px; font-size:11px; color:#666; text-align:center; line-height:1.4;">
1130          💡 <strong>의심스러운 활동 감지 기준:</strong><br>
1131          • 짧은 시간 내 대량 적립 (1분 내 5회 이상)<br>
1132          • 동일 출처 반복 (같은 활동 10회 이상)<br>
1133          • 비정상적 고액 (한 번에 50P 이상)
1134        </div>
1135      </div>
1136
1137
1138      <!-- Excel Editor Form -->
1139      <div id="adminExcelForm" style="display:none; text-align:left;">
1140
1141        <!-- Header & Tabs -->
1142        <div style="margin-bottom:15px; background:#f5f5f5; padding:10px; border-radius:8px; border:1px solid #ddd;">
1143          <div style="font-weight:bold; color:#3e2723; margin-bottom:8px; font-size:16px;">📊 데이터 관리 (Data Management)
1144          </div>
1145
1146          <div class="excel-tab-group" style="display:flex; flex-wrap:wrap; gap:5px;">
1147            <button class="pixel-btn excel-tab active" data-sheet="QT_Today"
1148              onclick="switchExcelSheet('QT_Today', this)">📖 오늘의 QT</button>
1149            <button class="pixel-btn excel-tab" data-sheet="QT_Comic" onclick="switchExcelSheet('QT_Comic', this)">🎨
1150              4ì»·</button>
1151            <button class="pixel-btn excel-tab" data-sheet="QT_Pastor" onclick="switchExcelSheet('QT_Pastor', this)">⛪
1152              목사님QT</button>
1153            <button class="pixel-btn excel-tab" data-sheet="QT_Youth" onclick="switchExcelSheet('QT_Youth', this)"
1154              style="background:#fff3e0;">🧑‍🎓 청년QT</button>
1155            <button class="pixel-btn excel-tab" data-sheet="BibleStudy"
1156              onclick="switchExcelSheet('BibleStudy', this)">✝️ 성경공부</button>
1157            <button class="pixel-btn excel-tab" data-sheet="PrayerBoard"
1158              onclick="switchExcelSheet('PrayerBoard', this)">🙏 기도제목</button>
1159            <button class="pixel-btn excel-tab" data-sheet="Notices" onclick="switchExcelSheet('Notices', this)">📢
1160              공지사항</button>
1161            <button class="pixel-btn excel-tab" data-sheet="FreeBoard" onclick="switchExcelSheet('FreeBoard', this)">📝
1162              자유게시판</button>
1163            <button class="pixel-btn excel-tab" data-sheet="Archive" onclick="switchExcelSheet('Archive', this)">📦
1164              자료실</button>
1165            <button class="pixel-btn excel-tab" data-sheet="GalleryInternal"
1166              onclick="switchExcelSheet('GalleryInternal', this)">📸 갤러리</button>
1167            <button class="pixel-btn excel-tab" data-sheet="Schedule" onclick="switchExcelSheet('Schedule', this)">🗓️
1168              일정</button>
1169            <button class="pixel-btn excel-tab" data-sheet="Pets" onclick="switchExcelSheet('Pets', this)">🐾 펫</button>
1170            <button class="pixel-btn excel-tab" data-sheet="PetGuestbook"
1171              onclick="switchExcelSheet('PetGuestbook', this)">📖 펫방명록</button>
1172            <button class="pixel-btn excel-tab" data-sheet="PetGameLog"
1173              onclick="switchExcelSheet('PetGameLog', this)">🎮 펫게임로그</button>
1174            <button class="pixel-btn excel-tab" data-sheet="PetEvolution"
1175              onclick="switchExcelSheet('PetEvolution', this)">🧬 펫진화</button>
1176            <button class="pixel-btn excel-tab" data-sheet="WisdomEvolution"
1177              onclick="switchExcelSheet('WisdomEvolution', this)" style="background:#EDE7F6; color:#4A148C; border-color:#B39DDB;">✨ 지혜진화</button>
1178            <button class="pixel-btn excel-tab" data-sheet="Members" onclick="switchExcelSheet('Members', this)">👥
1179              회원관리</button>
1180            <button class="pixel-btn excel-tab" data-sheet="Requests" onclick="switchExcelSheet('Requests', this)" style="background:#e8f5e9; color:#2e7d32; border-color:#a5d6a7;">📋 가입신청</button>
1181            <button class="pixel-btn excel-tab" data-sheet="UserPoints"
1182              onclick="switchExcelSheet('UserPoints', this)">💰 포인트현황</button>
1183            <button class="pixel-btn excel-tab" data-sheet="PointLogs" onclick="switchExcelSheet('PointLogs', this)">📜
1184              포인트로그</button>
1185
1186            <button class="pixel-btn excel-tab" data-sheet="MissionFund"
1187              onclick="switchExcelSheet('MissionFund', this)">⚓ 선교후원</button>
1188            <button class="pixel-btn excel-tab" data-sheet="QuizLog" onclick="switchExcelSheet('QuizLog', this)">📝
1189              퀴즈로그</button>
1190            <button class="pixel-btn excel-tab" data-sheet="QuizResults"
1191              onclick="switchExcelSheet('QuizResults', this)">📊 퀴즈결과</button>
1192            <button class="pixel-btn excel-tab" data-sheet="Votes" onclick="switchExcelSheet('Votes', this)">🗳️
1193              투표</button>
1194            <button class="pixel-btn excel-tab" data-sheet="RSVP" onclick="switchExcelSheet('RSVP', this)">📅
1195              참석여부</button>
1196            <button class="pixel-btn excel-tab" data-sheet="WorshipSettings"
1197              onclick="switchExcelSheet('WorshipSettings', this)">⚙️ 예배설정</button>
1198            <button class="pixel-btn excel-tab" data-sheet="BalloonLimits"
1199              onclick="switchExcelSheet('BalloonLimits', this)">🎈 풍선제한</button>
1200          </div>
1201        </div>
1202
1203        <!-- Action Buttons & Spreadsheet -->
1204        <div style="display:flex; justify-content:space-between; margin-bottom:5px; flex-wrap:wrap; gap:5px;">
1205          <button class="pixel-btn" onclick="addExcelRow()" style="background:#ff9800; font-size:12px;">➕ 행 추가 (Add
1206            Row)</button>
1207          <button class="pixel-btn" onclick="saveQTSpreadsheet()"
1208            style="background:var(--blue-color); font-size:12px;">💾 변경사항 저장 (Save to Firestore)</button>
1209          <button class="pixel-btn" onclick="linkAllUsersGoogleUID()"
1210            style="background:#ff5722; color:white; font-size:12px;">🔗 GoogleUID 일괄 연동</button>
1211          <button class="pixel-btn" onclick="givePointsManual()"
1212            style="background:#4caf50; color:white; font-size:12px;">💰 포인트 수동 지급</button>
1213        </div>
1214
1215        <div id="spreadsheet"
1216          style="width:100%; height:60vh; overflow:auto; background:#fff; border:2px solid #5d4037;">
1217        </div>
1218
1219        <div style="margin-top:5px; font-size:11px; color:#666; text-align:right;">
1220          * 엑셀처럼 편집 후 [저장] 버튼을 꼭 눌러주세요. (Auto-save 없음)
1221        </div>
1222      </div>
1223
1224      <!-- 관리자 멤버 조회 폼 -->
1225      <div id="adminMemberListForm" style="display:none; text-align:left;">
1226        <div style="background:#f3e5f5; padding:15px; border-radius:8px; border:2px solid #9c27b0; margin-bottom:15px;">
1227          <h3 style="margin:0 0 10px 0; color:#6a1b9a;">👥 전체 멤버 조회</h3>
1228          <p style="font-size:12px; color:#666; margin:0;">멤버를 클릭하면 개인정보를 확인할 수 있습니다.</p>
1229        </div>
1230        <div style="margin-bottom:10px;">
1231          <input type="text" id="adminMemberSearchInput" placeholder="멤버 이름 검색..."
1232            oninput="filterAdminMemberList(this.value)"
1233            style="width:100%; padding:10px; border:2px solid #ce93d8; border-radius:8px; box-sizing:border-box; font-family:'DungGeunMo',monospace;">
1234        </div>
1235        <div id="adminMemberListContainer"
1236          style="background:#fff; border:3px solid #ce93d8; border-radius:8px; padding:10px; min-height:200px; max-height:55vh; overflow-y:auto;">
1237          <div style="text-align:center; color:#999; padding:30px;">로딩 중...</div>
1238        </div>
1239        <div id="adminMemberDetailView" style="display:none; margin-top:15px;"></div>
1240      </div>
1241
1242      <!-- 진화 관리 폼 (Excel 내부로 이동 - 기존 유지) -->
1243      <div id="adminEvolutionForm" style="display:none; text-align:left;">
1244        <div style="background:#f3e5f5; padding:15px; border-radius:8px; border:2px solid #9c27b0; margin-bottom:15px;">
1245          <h3 style="margin:0 0 10px 0; color:#6a1b9a;">🧬 펫 진화 트리 관리</h3>
1246          <p style="font-size:12px; color:#666; margin:0;">진화 레벨, 이미지, 확률, 분기를 설정합니다.</p>
1247        </div>
1248
1249        <div id="evolutionTreeContainer"
1250          style="background:#fff; border:3px solid #ce93d8; border-radius:8px; padding:15px; min-height:200px; max-height:55vh; overflow-y:auto;">
1251          <div style="text-align:center; color:#999; padding:30px;">로딩 중...</div>
1252        </div>
1253
1254        <div style="display:flex; gap:10px; margin-top:15px;">
1255          <button class="pixel-btn" onclick="addEvolutionRootLevel()" style="flex:1; background:#9c27b0; color:white;">➕
1256            루트 진화 레벨 추가</button>
1257          <button class="pixel-btn" onclick="saveEvolutionConfigToServer()"
1258            style="flex:1; background:#4caf50; color:white;">💾 저장</button>
1259        </div>
1260
1261        <div style="margin-top:10px; font-size:11px; color:#666; text-align:center;">
1262          💡 각 레벨에 PNG 이미지를 추가하고 확률을 설정하세요. 확률의 합은 100%여야 합니다.
1263        </div>
1264      </div>
1265
1266      <!-- ✨ 지혜 특별 진화 관리 폼 -->
1267      <div id="adminWisdomEvoForm" style="display:none; text-align:left;">
1268        <div style="background:#EDE7F6; padding:15px; border-radius:8px; border:2px solid #7B1FA2; margin-bottom:15px;">
1269          <h3 style="margin:0 0 10px 0; color:#4A148C;">✨ 지혜 특별 진화 관리</h3>
1270          <p style="font-size:12px; color:#666; margin:0;">바이블 월드에서 지혜 MAX(1000) 달성 시 진화 가능한 특별 펫을 관리합니다.</p>
1271        </div>
1272
1273        <div id="wisdomEvoContainer" style="background:#fff; border:3px solid #B39DDB; border-radius:8px; padding:15px; min-height:200px; max-height:55vh; overflow-y:auto;">
1274          <div style="text-align:center; color:#999; padding:30px;">로딩 중...</div>
1275        </div>
1276
1277        <div style="display:flex; gap:10px; margin-top:15px;">
1278          <button class="pixel-btn" onclick="addWisdomEvolution()" style="flex:1; background:#7B1FA2; color:white;">➕ 특별 진화 추가</button>
1279          <button class="pixel-btn" onclick="saveWisdomEvolutionConfig()" style="flex:1; background:#4caf50; color:white;">💾 저장</button>
1280        </div>
1281
1282        <div style="margin-top:10px; font-size:11px; color:#666; text-align:center;">
1283          💡 이모지 또는 이미지 URL을 설정하고, 확률(%)을 지정하세요. 합계 100%를 권장합니다.
1284        </div>
1285      </div>
1286
1287      <!-- 푸시 알림 발송 폼 -->
1288      <div id="adminPushForm" style="display:none; text-align:left;">
1289        <div style="background:#fce4ec; padding:15px; border-radius:8px; border:2px solid #e91e63; margin-bottom:15px;">
1290          <h3 style="margin:0 0 10px 0; color:#c2185b;">📢 푸시 알림 발송</h3>
1291          <p style="font-size:12px; color:#666; margin:0;">
1292            앱을 설치한 사용자(PWA)에게 푸시 알림을 보냅니다.<br>
1293            알림 권한을 허용한 사용자만 수신합니다.
1294          </p>
1295        </div>
1296
1297        <!-- 등록된 토큰 현황 -->
1298        <div style="background:#fff; border:2px solid #e0e0e0; border-radius:8px; padding:12px; margin-bottom:15px;">
1299          <div style="display:flex; justify-content:space-between; align-items:center;">
1300            <span style="font-weight:bold; color:#333;">🔔 등록된 기기 수:</span>
1301            <span id="pushTokenCount" style="font-size:20px; font-weight:bold; color:#e91e63;">-</span>
1302          </div>
1303          <button class="pixel-btn" onclick="loadPushTokenCount()" style="margin-top:8px; background:#2196f3; font-size:12px; width:100%;">🔄 새로고침</button>
1304        </div>
1305
1306        <!-- 알림 작성 -->
1307        <form onsubmit="handleSendPush(event)">
1308          <label style="font-weight:bold; display:block; margin-bottom:5px;">제목</label>
1309          <input type="text" id="pushTitle" placeholder="알림 제목 (예: 이번 주 예배 안내)" required
1310            style="width:100%; padding:10px; border:2px solid #ddd; border-radius:6px; margin-bottom:12px; box-sizing:border-box;">
1311
1312          <label style="font-weight:bold; display:block; margin-bottom:5px;">내용</label>
1313          <textarea id="pushBody" placeholder="알림 내용을 입력하세요" required rows="4"
1314            style="width:100%; padding:10px; border:2px solid #ddd; border-radius:6px; margin-bottom:12px; box-sizing:border-box; resize:vertical;"></textarea>
1315
1316          <label style="font-weight:bold; display:block; margin-bottom:5px;">링크 URL (선택)</label>
1317          <input type="url" id="pushUrl" placeholder="https://aya-club.pages.dev/"
1318            style="width:100%; padding:10px; border:2px solid #ddd; border-radius:6px; margin-bottom:15px; box-sizing:border-box;">
1319
1320          <button type="submit" class="pixel-btn" style="background:#e91e63; color:white; width:100%; padding:12px; font-size:16px;">
1321            📤 푸시 알림 보내기
1322          </button>
1323        </form>
1324
1325        <div style="margin-top:12px; font-size:11px; color:#666; text-align:center; line-height:1.5;">
1326          💡 사용자가 알림 권한을 허용해야 수신 가능합니다.<br>
1327          💡 알림은 앱이 백그라운드에 있거나 닫혀 있어도 수신됩니다.
1328        </div>
1329      </div>
1330    </div>
1331  </div>
1332
1333  <div id="eventDetailModal" class="modal">
1334    <div class="modal-content retro-box" style="text-align:center;">
1335      <span class="close" onclick="closeModal('eventDetailModal')">X</span>
1336      <h2 id="evtDetailTitle" style="border:none;"></h2>
1337      <div style="background:#fff; border:3px solid var(--border-color); padding:15px; text-align:left;">
1338        <p id="evtDetailDate" style="color:var(--secondary-color);"></p>
1339        <p id="evtDetailDesc" style="white-space:pre-wrap;"></p>
1340        <a id="evtDetailLink" href="#" target="_blank" class="pixel-btn" style="display:none; width:auto; background:var(--blue-color); color:#fff;">🔗 Link</a>
1341
1342        <div id="evtRSVPArea" class="evt-rsvp-area" style="margin-top:12px; text-align:left; display:none;">
1343          <div class="evt-rsvp-title">🗳️ 참석 투표 (RSVP)</div>
1344          <div id="evtRSVPStatus" class="evt-rsvp-status"></div>
1345          <div class="evt-rsvp-buttons">
1346            <button class="pixel-btn" onclick="submitRSVP('ATTEND')" id="txt-rsvp-attend">참석</button>
1347            <button class="pixel-btn" onclick="submitRSVP('MAYBE')" id="txt-rsvp-maybe">미정</button>
1348            <button class="pixel-btn" onclick="submitRSVP('DECLINE')" id="txt-rsvp-decline">불참</button>
1349          </div>
1350          <div id="evtRSVPAdminArea" style="margin-top:10px; display:none;">
1351            <button class="pixel-btn" onclick="openRSVPAdminList()" id="txt-rsvp-admin-list">👑 투표 명단 보기</button>
1352          </div>
1353        </div>
1354      </div>
1355      <div id="evtVisitEditArea" style="display:none; margin-top:10px;">
1356        <button onclick="promptEditVisit()" class="pixel-btn" style="background:var(--primary-color); width:100%;"><span id="txt-btn-edit-visit">✏️ 내 신청 수정하기</span></button>
1357      </div>
1358      <div id="evtEditArea" style="display:none; margin-top:10px;">
1359        <button onclick="promptEditEvent()" class="pixel-btn" style="background:var(--blue-color); width:100%;">✏️ <span id="txt-btn-edit-event">일정 수정하기</span></button>
1360      </div>
1361      <div id="evtAdminArea" style="display:none; margin-top:10px;">
1362        <button onclick="deleteCurrentEvent()" class="pixel-btn" style="background:var(--pink-color); width:100%;">🗑
1363          Delete</button>
1364      </div>
1365    </div>
1366  </div>
1367
1368  <div id="loginModal" class="modal">
1369    <div class="modal-content retro-box" style="text-align:center;">
1370      <span class="close" onclick="document.getElementById('loginModal').style.display='none'">X</span>
1371      <h2 id="login-title">🔑 로그인</h2>
1372
1373      <div class="modal-body" style="text-align:center; padding: 20px 0;">
1374        <p style="margin-bottom:20px; color:#5d4037; font-size:14px; line-height:1.5;">
1375          <span id="txt-login-desc"><strong>구글 계정</strong>으로 로그인해주세요.</span><br>
1376          <span style="font-size:12px; color:#8d6e63;" id="txt-login-desc2">(기존 회원은 전화번호 연동을 통해 데이터를 이어받을 수 있습니다)</span>
1377        </p>
1378
1379        <button onclick="if(!checkInAppBeforeLogin()) handleGoogleLogin()" style="
1380            background-color: #fff;
1381            color: #757575;
1382            border: 2px solid #ddd;
1383            padding: 12px 24px;
1384            border-radius: 4px;
1385            font-size: 16px;
1386            font-weight: bold;
1387            cursor: pointer;
1388            display: inline-flex;
1389            align-items: center;
1390            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
1391            transition: all 0.2s;
1392        " onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 12px rgba(0,0,0,0.15)';"
1393          onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 4px 6px rgba(0,0,0,0.1)';">
1394          <img src="https://upload.wikimedia.org/wikipedia/commons/5/53/Google_%22G%22_Logo.svg" alt="Google"
1395            style="width:20px; height:20px; margin-right:12px;">
1396          <span id="txt-google-login-btn">구글 계정으로 로그인</span>
1397        </button>
1398
1399        <div style="margin-top:25px; font-size:12px; color:#999;">
1400          <span id="txt-login-note">* 최초 1회 전화번호 인증이 필요할 수 있습니다.</span>
1401        </div>
1402      </div>
1403    </div>
1404  </div>
1405
1406  <!-- Phone Verification Modal -->
1407  <div id="phoneVerificationModal" class="modal">
1408    <div class="modal-content retro-box" style="text-align:center; max-width: 400px;">
1409      <h2 style="margin-bottom:10px;" id="txt-phone-verify-title">📞 전화번호 연동</h2>
1410      <div class="modal-body">
1411        <p style="margin-bottom:20px; color:#5d4037; font-size:14px; line-height:1.6;" id="txt-phone-verify-desc">
1412          기존 데이터(미니홈피, 펫 등)를 불러오기 위해<br>
1413          사용하시던 <strong>전화번호</strong>를 입력해주세요.
1414        </p>
1415
1416        <div class="retro-input-group" style="margin-bottom: 10px;">
1417          <input type="text" id="v-name" placeholder="이름 (실명)" data-placeholder-ko="이름 (실명)" data-placeholder-en="Name (real name)" class="swal2-input"
1418            style="text-align:center; font-size:16px;">
1419        </div>
1420        <div class="retro-input-group" style="margin-bottom: 20px;">
1421          <input type="tel" id="v-ph" placeholder="전화번호 (예: 01012345678)" data-placeholder-ko="전화번호 (예: 01012345678)" data-placeholder-en="Phone number (e.g. 01012345678)" class="swal2-input"
1422            style="text-align:center; font-size:18px; letter-spacing:1px;"
1423            onkeypress="if(event.keyCode==13) submitPhoneVerification()">
1424        </div>
1425
1426        <div class="modal-footer" style="display:flex; gap:10px; justify-content:center;">
1427          <button class="pixel-btn" style="background:#bdbdbd;"
1428            onclick="document.getElementById('phoneVerificationModal').style.display='none'; signOut(auth);"><span id="txt-phone-cancel">취소</span></button>
1429          <button class="pixel-btn" onclick="submitPhoneVerification()"
1430            style="background:var(--primary-color);"><span id="txt-phone-confirm">확인</span></button>
1431        </div>
1432      </div>
1433    </div>
1434  </div>
1435
1436  <div id="findModal" class="modal">
1437    <div class="modal-content retro-box" style="text-align:center;">
1438      <span class="close" onclick="document.getElementById('findModal').style.display='none'">X</span>
1439      <h2 id="find-pw-title">비밀번호 찾기</h2>
1440      <p style="margin:20px 0; color:#5d4037; font-size:15px;">Google 로그인을 사용하세요.</p>
1441      <p style="color:#888; font-size:13px;">비밀번호 복구 기능은 더 이상 지원되지 않습니다.<br>Google 계정으로 로그인해 주세요.</p>
1442    </div>
1443  </div>
1444
1445  <div id="talentModal" class="modal">
1446    <div class="modal-content retro-box" style="text-align:center;">
1447      <span class="close" onclick="document.getElementById('talentModal').style.display='none'">X</span>
1448
1449      <div class="talent-header"
1450        style="display:flex; justify-content:space-between; align-items:center; border-bottom:3px dashed #5d4037; padding-bottom:10px; margin-bottom:15px;">
1451        <h2 id="tal-title" style="margin:0; border:none; font-size:24px;">💰 달란트</h2>
1452
1453        <div class="talent-btn-group" style="margin-right: 35px; display: flex; gap: 5px;">
1454          <button id="btn-rank" class="pixel-btn" style="padding:5px 10px; font-size:12px; background:#5d4037;"
1455            onclick="openRanking()"><span id="txt-btn-ranking">🏆 섬김랭킹</span></button>
1456          <button id="btn-rule" class="pixel-btn" style="padding:5px 10px; font-size:12px; background:#8d6e63;"
1457            onclick="showPointRules()"><span id="txt-btn-rules">❓ 적립방법</span></button>
1458        </div>
1459      </div>
1460
1461      <div class="coin-jar">
1462        <div id="j-fill" class="jar-fill" style="height:0%"></div>
1463        <div id="j-txt" class="jar-txt">0P</div>
1464      </div>
1465
1466      <div style="margin:5px;"><span id="txt-total-label">청년들이 누적한 후원금액:</span> <b id="t-dol"
1467          style="color:red; font-size:22px;">$0</b></div>
1468      <div id="txt-rate" style="font-size:12px; color:#555; margin-bottom:15px;">(100 Point = $1 Donation)</div>
1469
1470      <div style="background:#fffbf0; padding:15px; border:2px solid #5d4037; border-radius:5px;">
1471        <div id="txt-my-pt-label" style="font-size:14px; color:#5d4037;">나의 보유 포인트</div>
1472        <div style="font-size:28px; font-weight:bold; margin:5px 0;"><span id="m-pt">0</span> P</div>
1473        <button id="btn-donate" class="pixel-btn btn-brown-pop" onclick="doDonate()"><span id="txt-btn-donate">선교헌금 적립(기부)</span></button>
1474        <button id="btn-shop" class="pixel-btn" style="display:none; margin-top:8px; background:#e65100; color:white; font-size:14px; padding:8px 20px;" onclick="openShop()">🛒 상점구입</button>
1475        <button id="btn-shop-admin" class="pixel-btn" style="display:none; margin-top:8px; background:#b71c1c; color:white; font-size:12px; padding:6px 14px;" onclick="openShopAdmin()">⚙️ 상품관리</button>
1476      </div>
1477    </div>
1478  </div>
1479
1480  <!-- 상점 인라인 모달 -->
1481  <div id="shopModal" class="shop-modal-overlay">
1482    <div class="shop-header">
1483      <button class="shop-back-btn" onclick="closeShop()"><span id="txt-shop-back">← 돌아가기</span></button>
1484      <h1 id="txt-shop-title">🛒 달란트 상점</h1>
1485      <span class="shop-points-badge" id="shopPointsBadge">0 P</span>
1486    </div>
1487
1488    <div class="shop-admin-toggle-wrap" id="shopAdminToggleWrap">
1489      <button class="shop-btn" style="width:auto; background:#b71c1c; color:white; font-size:13px; padding:6px 15px;"
1490        onclick="shopToggleAdminPanel()">⚙️ 상품 관리</button>
1491    </div>
1492
1493    <div class="shop-admin-panel" id="shopAdminPanel">
1494      <h3 id="txt-shop-admin-title">⚙️ 새 상품 등록</h3>
1495      <label id="txt-shop-type-label">상품 유형</label>
1496      <select id="shopAdminType">
1497        <option value="group" id="txt-shop-opt-group">단체구입</option>
1498        <option value="individual" id="txt-shop-opt-individual">개인구입</option>
1499      </select>
1500      <label id="txt-shop-name-label">상품명</label>
1501      <input type="text" id="shopAdminName" placeholder="예: 치킨 파티" data-placeholder-ko="예: 치킨 파티" data-placeholder-en="e.g. Chicken Party">
1502      <label id="txt-shop-desc-label">설명</label>
1503      <textarea id="shopAdminDesc" placeholder="상품에 대한 설명을 입력하세요" data-placeholder-ko="상품에 대한 설명을 입력하세요" data-placeholder-en="Enter product description"></textarea>
1504      <label id="txt-shop-price-label">가격 (P)</label>
1505      <input type="number" id="shopAdminPrice" min="1" placeholder="100">
1506      <label id="txt-shop-stock-label">수량 제한 (개인구입만)</label>
1507      <input type="number" id="shopAdminStock" min="1" value="1" placeholder="3">
1508      <label id="txt-shop-image-label">상품 이미지</label>
1509      <input type="file" id="shopAdminImage" accept="image/*" onchange="shopPreviewImage(this)">
1510      <img class="shop-img-preview" id="shopImgPreview">
1511      <button class="shop-btn contribute" style="margin-top:12px;" onclick="shopSubmitProduct()"><span id="txt-shop-submit">📦 상품 등록</span></button>
1512      <hr style="margin:15px 0; border-color:#e0e0e0;">
1513      <h3 id="txt-shop-product-list-title">📋 등록된 상품 목록</h3>
1514      <div id="shopAdminProductList"></div>
1515    </div>
1516
1517    <div class="shop-tab-container">
1518      <button class="shop-tab-btn active" onclick="shopSwitchTab('group')" id="shopTabGroup"><span id="txt-shop-tab-group">🤝 단체구입</span></button>
1519      <button class="shop-tab-btn" onclick="shopSwitchTab('individual')" id="shopTabIndividual"><span id="txt-shop-tab-individual">👤 개인구입</span></button>
1520      <button class="shop-tab-btn" onclick="shopSwitchTab('history')" id="shopTabHistory"><span id="txt-shop-tab-history">📋 내역</span></button>
1521    </div>
1522
1523    <div class="shop-tab-content active" id="shopContentGroup">
1524      <div class="shop-product-grid" id="shopGroupGrid">
1525        <div class="shop-loading txt-shop-loading">상품을 불러오는 중</div>
1526      </div>
1527    </div>
1528
1529    <div class="shop-tab-content" id="shopContentIndividual">
1530      <div class="shop-product-grid" id="shopIndividualGrid">
1531        <div class="shop-loading txt-shop-loading">상품을 불러오는 중</div>
1532      </div>
1533    </div>
1534
1535    <div class="shop-tab-content" id="shopContentHistory">
1536      <div id="shopHistoryContent">
1537        <div class="shop-loading" id="txt-shop-history-loading">내역을 불러오는 중</div>
1538      </div>
1539    </div>
1540  </div>
1541
1542  
1542<script src='https://cdn.jsdelivr.net/npm/[email protected]/index.global.min.js' defer></script>
1542
1543  
1543<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11" defer></script>
1543
1544
1545  
1545<script>
1546    // GAS URL is now loaded from config.js
1547    var REAL_SCRIPT_URL = window.GAS_API_URL || window.GAS_SCRIPT_URL || "";
1548  </script>
1548
1549  <div id="notificationModal" class="modal">
1550    <div class="modal-content retro-box" style="text-align:center; max-width:750px;">
1551      <span class="close" onclick="closeModal('notificationModal')">X</span>
1552      <h2 style="border:none;" id="txt-notif-title">🔔 알림 센터</h2>
1553      <div
1554        style="text-align:left; background:#fff; border:3px solid var(--border-color); padding:12px; max-height:60vh; overflow:auto;">
1555        <div style="display:flex; gap:10px; margin-bottom:10px;">
1556          <button class="tab-btn active" id="notifTabFeed" onclick="switchNotifTab('feed')"><span id="txt-notif-tab-feed">새로운 글</span></button>
1557          <button class="tab-btn" id="notifTabComment" onclick="switchNotifTab('comment')"><span id="txt-notif-tab-comment">내 댓글</span></button>
1558        </div>
1559        <div id="notifFeedView">
1560          <div id="notifFeedList">Loading...</div>
1561        </div>
1562        <div id="notifCommentView" style="display:none;">
1563          <div id="notifCommentList">Loading...</div>
1564        </div>
1565      </div>
1566    </div>
1567  </div>
1568
1569  <!-- Bible World is rendered as fullscreen overlay by bible-writing-game.js -->
1570
1571  <div id="worshipModal" class="modal">
1572    <div class="modal-content retro-box" style="text-align:center; max-width:400px;">
1573      <span class="close" onclick="closeModal('worshipModal')">X</span>
1574      <h2 id="txt-worship-title">📖 말씀과 교재</h2>
1575      <div style="display:flex; flex-direction:column; gap:15px; margin-top:20px;">
1576        <button class="pixel-btn" onclick="openWorshipService()"
1577          style="background:#4caf50; font-size:18px; padding:20px;">
1578          <span id="txt-btn-worship">🙏 예배</span>
1579        </button>
1580        <button class="pixel-btn" onclick="openBibleStudyBoard()"
1581          style="background:#2196f3; font-size:18px; padding:20px;">
1582          <span id="txt-btn-biblestudy">📚 성경공부</span>
1583        </button>
1584      </div>
1585    </div>
1586  </div>
1587
1588  <div id="worshipServiceModal" class="modal">
1589    <div class="modal-content retro-box">
1590      <span class="close" onclick="closeModal('worshipServiceModal')">X</span>
1591      <h2 id="txt-worship-service-title">🙏 예배</h2>
1592
1593      <div id="worshipNoticeArea"
1594        style="background:#fffbf0; padding:15px; margin-bottom:15px; border:3px solid var(--border-color); border-radius:8px;">
1595        <div style="font-weight:bold; margin-bottom:10px; color:var(--secondary-color);" id="txt-worship-notice-label">📢 예배 안내</div>
1596        <div id="worshipNoticeContent" style="white-space:pre-wrap; line-height:1.6; text-align:left;">
1597          <span id="txt-worship-not-time">예배시간이 아닙니다</span>
1598        </div>
1599      </div>
1600
1601      <div id="worshipAdminArea" style="display:none;">
1602        <div style="background:#fff; padding:15px; border:3px dashed #ff9800; border-radius:8px; margin-bottom:15px;">
1603          <h4 style="margin-top:0; color:#ff9800;">👑 관리자 설정</h4>
1604
1605          <label style="display:block; margin-bottom:5px; font-weight:bold;">예배 공지 수정:</label>
1606          <textarea id="adminWorshipNotice" style="width:100%; height:100px; margin-bottom:10px;"
1607            placeholder="예배 안내 메시지를 입력하세요"></textarea>
1608
1609          <label style="display:block; margin-bottom:5px; font-weight:bold;">Zoom 링크:</label>
1610          <input type="text" id="adminZoomLink" style="width:100%; margin-bottom:10px;"
1611            placeholder="https://zoom.us/j/...">
1612
1613          <button class="pixel-btn" onclick="saveWorshipSettings()"
1614            style="width:100%; background:var(--primary-color);">
1615            💾 저장
1616          </button>
1617        </div>
1618      </div>
1619
1620      <div id="worshipUserArea" style="display:none;">
1621        <button class="pixel-btn" id="joinWorshipBtn" onclick="joinWorshipService()"
1622          style="width:100%; background:#4caf50; font-size:18px; padding:15px;">
1623          <span id="txt-join-worship">📹 예배 접속하기</span>
1624        </button>
1625      </div>
1626    </div>
1627  </div>
1628
1629  <div id="bibleStudyModal" class="modal">
1630    <div class="modal-content retro-box">
1631      <span class="close" onclick="closeModal('bibleStudyModal')">X</span>
1632      <h2 id="txt-biblestudy-title">📚 성경공부</h2>
1633
1634      <div class="tab-container">
1635        <button class="tab-btn active" onclick="switchBibleStudyTab('list', this)" id="txt-bs-tab-list">게시판</button>
1636        <button class="tab-btn" onclick="switchBibleStudyTab('write', this)" id="txt-bs-tab-write">글쓰기</button>
1637      </div>
1638
1639      <div id="bibleStudyListView">
1640        <div style="background:#f5f5f5; padding:10px; margin-bottom:10px; border-radius:8px;">
1641          <div style="display:flex; gap:5px; margin-bottom:8px;">
1642            <input type="text" id="bibleStudySearch" placeholder="🔍 검색..." data-placeholder-ko="🔍 검색..." data-placeholder-en="🔍 Search..." style="flex:1;">
1643            <button class="pixel-btn" onclick="searchBibleStudy()"
1644              style="background:var(--blue-color); width:auto; padding:10px 20px;"><span id="txt-bs-search-btn">검색</span></button>
1645          </div>
1646
1647          <div style="display:flex; gap:8px; flex-wrap:wrap;">
1648            <button class="filter-btn active" onclick="filterBibleStudy('all', this)" id="txt-bs-filter-all">전체</button>
1649            <button class="filter-btn" onclick="filterBibleStudy('구약', this)" id="txt-bs-filter-ot">구약</button>
1650            <button class="filter-btn" onclick="filterBibleStudy('신약', this)" id="txt-bs-filter-nt">신약</button>
1651            <button class="filter-btn" onclick="filterBibleStudy('기타', this)" id="txt-bs-filter-etc">기타</button>
1652          </div>
1653
1654          <div style="display:flex; gap:8px; margin-top:8px;">
1655            <button class="sort-btn active" onclick="sortBibleStudy('latest', this)" id="txt-bs-sort-latest">최신순</button>
1656            <button class="sort-btn" onclick="sortBibleStudy('popular', this)" id="txt-bs-sort-popular">인기순</button>
1657          </div>
1658        </div>
1659
1660        <div id="bibleStudyList" class="archive-list">
1661          Loading...
1662        </div>
1663      </div>
1664
1665      <div id="bibleStudyWriteView" style="display:none;">
1666        <form id="bibleStudyForm" onsubmit="submitBibleStudy(event)">
1667          <label id="txt-bs-category-label">카테고리</label>
1668          <select name="category" required>
1669            <option value="구약" id="txt-bs-opt-ot">구약</option>
1670            <option value="신약" id="txt-bs-opt-nt">신약</option>
1671            <option value="기타" id="txt-bs-opt-etc">기타</option>
1672          </select>
1673
1674          <label id="txt-bs-title-label">제목</label>
1675          <input type="text" name="title" required>
1676
1677          <label id="txt-bs-content-label">내용</label>
1678          <textarea name="content" rows="8" required></textarea>
1679
1680          <label id="txt-bs-video-label">영상 링크 (선택)</label>
1681          <input type="url" name="videoLink" placeholder="https://youtube.com/...">
1682
1683          <div class="file-input-wrapper">
1684            <label id="txt-bs-photo-label">📷 사진 첨부 (최대 5장)</label>
1685            <div id="bsMultiPhotoContainer">
1686              <div style="display:flex; align-items:center; gap:5px; margin-bottom:5px;">
1687                <input type="file" class="bs-multi-photo-input" accept="image/*">
1688              </div>
1689            </div>
1690            <button type="button" class="pixel-btn" onclick="addBibleStudyPhotoSlot()"
1691              style="background:#4caf50; width:100%; margin-top:5px;">
1692              <span id="txt-bs-add-photo">➕ 사진 추가하기</span>
1693            </button>
1694            <div style="font-size:11px; color:#666; margin-top:5px;" id="txt-bs-photo-note">
1695              ※ 각 사진은 1280px로 자동 리사이징됩니다
1696            </div>
1697          </div>
1698
1699          <!-- 🔒 게시글 보기 비밀번호 설정 (새로 추가되는 섹션) -->
1700          <div style="background:#fff3e0; padding:15px; border:3px dashed #ff9800; border-radius:8px; margin:15px 0;">
1701            <label style="color:#e65100; font-weight:bold;" id="txt-bs-pw-label">🔒 게시글 보기 비밀번호 (선택)</label>
1702            <div style="font-size:12px; color:#666; margin-bottom:10px;" id="txt-bs-pw-desc">
1703              설정하면 이 비밀번호를 입력해야 내용을 볼 수 있습니다. (퀴즈 미리 풀기 방지용)
1704            </div>
1705            <input type="text" name="viewPassword" placeholder="예: 1234 (비워두면 모든 사람이 볼 수 있음)" data-placeholder-ko="예: 1234 (비워두면 모든 사람이 볼 수 있음)" data-placeholder-en="e.g., 1234 (leave empty for public access)"
1706              style="text-align:center; font-size:16px; letter-spacing:2px;">
1707            <div style="font-size:11px; color:#999; margin-top:5px;" id="txt-bs-admin-note">
1708              ※ 관리자는 비밀번호 없이 모든 글을 볼 수 있습니다
1709            </div>
1710          </div>
1711
1712          <div style="margin:15px 0; padding:15px; background:#e3f2fd; border-radius:8px;">
1713            <label style="display:flex; align-items:center; gap:8px; cursor:pointer;">
1714              <input type="checkbox" id="enableQuiz" onchange="toggleQuizArea()" style="width:20px; height:20px;">
1715              <span style="font-weight:bold;" id="txt-bs-quiz-toggle">📝 성경퀴즈 포함하기</span>
1716            </label>
1717          </div>
1718
1719          <div id="quizArea"
1720            style="display:none; background:#fff; padding:15px; border:3px dashed #2196f3; border-radius:8px; margin-bottom:15px;">
1721            <h4 style="margin-top:0; color:#2196f3;" id="txt-bs-quiz-area-title">📝 퀴즈 작성 (최대 40문제, 문제당 20초)</h4>
1722            <div id="quizQuestions"></div>
1723            <button type="button" class="pixel-btn" onclick="addQuizQuestion()"
1724              style="width:100%; background:#2196f3; margin-top:10px;" id="txt-bs-add-question">
1725              ➕ 문제 추가
1726            </button>
1727          </div>
1728
1729
1730
1731          <button type="submit" class="pixel-btn btn-submit-form" style="background:var(--primary-color);">
1732            <span id="txt-bs-submit">📤 게시글 등록</span>
1733          </button>
1734        </form>
1735      </div>
1736
1737      <div id="bibleStudyDetailView" style="display:none;">
1738        <button class="pixel-btn btn-back" onclick="switchBibleStudyTab('list')"><span id="txt-bs-back-list">◀ 목록</span></button>
1739
1740        <div id="bibleStudyDetailContent" style="margin-top:15px;">
1741        </div>
1742
1743        <div id="bibleStudyQuizArea" style="display:none; margin-top:20px;">
1744        </div>
1745
1746        <div style="margin-top:20px; border-top:3px dashed var(--secondary-color); padding-top:15px;">
1747          <h4 id="txt-bs-comments-title">💬 댓글</h4>
1748          <div id="bibleStudyComments" style="margin-bottom:10px;"></div>
1749          <div style="display:flex; gap:5px;">
1750            <input type="text" id="bsCommentName" placeholder="이름" data-placeholder-ko="이름" data-placeholder-en="Name" style="width:80px;">
1751            <input type="text" id="bsCommentMsg" placeholder="댓글 내용..." data-placeholder-ko="댓글 내용..." data-placeholder-en="Comment..." style="flex:1;">
1752          </div>
1753          <button class="pixel-btn" onclick="submitBibleStudyComment()"
1754            style="width:100%; margin-top:5px; background:var(--secondary-color);">
1755            <span id="txt-bs-comment-submit">댓글 등록</span>
1756          </button>
1757        </div>
1758      </div>
1759    </div>
1760  </div>
1761
1762  <!-- Jspreadsheet & JSuites for Excel-like Admin -->
1763
1764
1765  <!-- 접속자 슬라이드 패널 -->
1766  <!-- 통계 대시보드 모달 -->
1767  <div id="statsModal" class="modal">
1768    <div class="modal-content retro-box" style="max-width:600px;">
1769      <span class="close" onclick="closeModal('statsModal')">X</span>
1770      <h2 id="txt-stats-title">📊 청년부 통계</h2>
1771      <div id="statsContent">Loading...</div>
1772    </div>
1773  </div>
1774
1775  <!-- 격려 메시지 보기 모달 -->
1776  <div id="encourageListModal" class="modal">
1777    <div class="modal-content retro-box" style="max-width:500px;">
1778      <span class="close" onclick="closeModal('encourageListModal')">X</span>
1779      <h2 id="txt-encourage-title">💌 받은 격려 메시지</h2>
1780      <div id="encourageList">Loading...</div>
1781    </div>
1782  </div>
1783
1784  <!-- 불씨 스트릭 모달 -->
1785  <div id="emberModal" class="modal">
1786    <div class="modal-content retro-box" style="max-width:450px;">
1787      <span class="close" onclick="closeModal('emberModal')">X</span>
1788      <h2 id="txt-ember-title">🔥 나의 불씨</h2>
1789      <div class="ember-detail">
1790        <div class="ember-big-fire" id="emberBigFire">🔥</div>
1791        <div id="emberLevelText" style="font-size:18px; font-weight:bold; margin-bottom:8px;"></div>
1792        <div class="ember-stats">
1793          <div class="ember-stat-box">
1794            <div class="ember-stat-value" id="emberCurrentStreak">0</div>
1795            <div class="ember-stat-label" id="txt-ember-streak">연속 일수</div>
1796          </div>
1797          <div class="ember-stat-box">
1798            <div class="ember-stat-value" id="emberMaxStreak">0</div>
1799            <div class="ember-stat-label" id="txt-ember-best">최고 기록</div>
1800          </div>
1801          <div class="ember-stat-box">
1802            <div class="ember-stat-value" id="emberTotalDays">0</div>
1803            <div class="ember-stat-label" id="txt-ember-total">총 활동일</div>
1804          </div>
1805          <div class="ember-stat-box">
1806            <div class="ember-stat-value" id="emberLevel">1</div>
1807            <div class="ember-stat-label" id="txt-ember-level">불씨 레벨</div>
1808          </div>
1809        </div>
1810        <div class="ember-community-bar">
1811          <div style="font-size:14px;" id="txt-ember-community">🏘️ 공동체 불꽃</div>
1812          <div style="font-size:12px; opacity:0.7;" id="emberCommunityText">오늘 0명 참여</div>
1813          <div class="ember-community-progress">
1814            <div class="ember-community-fill" id="emberCommunityFill" style="width:0%"></div>
1815          </div>
1816        </div>
1817        <div class="ember-heatmap" id="emberHeatmap"></div>
1818      </div>
1819    </div>
1820  </div>
1821
1822  <!-- QT 카드 빌더 모달 -->
1823  <div id="qtCardModal" class="modal">
1824    <div class="modal-content retro-box" style="max-width:400px;">
1825      <span class="close" onclick="closeModal('qtCardModal')">X</span>
1826      <h2 id="txt-qtcard-title">🎨 QT 카드 만들기</h2>
1827      <div class="qt-card-builder">
1828        <div style="font-size:13px; margin-bottom:6px;" id="txt-qtcard-bg-select">배경 선택:</div>
1829        <div class="qt-card-bg-selector" id="qtCardBgSelector"></div>
1830        <div class="qt-card-preview" id="qtCardPreview">
1831          <div class="qt-card-date" id="qtCardDate"></div>
1832          <div id="qtCardContent" style="font-size:16px; line-height:1.8;"></div>
1833          <div class="qt-card-verse" id="qtCardVerse"></div>
1834          <div class="qt-card-logo">⛪ HOLY SPARK</div>
1835        </div>
1836        <div class="qt-card-actions">
1837          <button class="pixel-btn" onclick="downloadQTCard()" style="background:var(--primary-color);">📥 저장</button>
1838          <button class="pixel-btn" onclick="shareQTCard()" style="background:var(--green-color);">📤 공유</button>
1839        </div>
1840      </div>
1841    </div>
1842  </div>
1843
1844  <div id="onlinePanel" class="online-panel">
1845    <div class="online-panel-header">
1846      <span id="txt-online-title">👥 현재 접속자</span>
1847      <span class="online-panel-close" onclick="toggleOnlinePanel()">✕</span>
1848    </div>
1849    <div id="onlineList" class="online-list"></div>
1850  </div>
1851  <div id="onlineOverlay" class="online-overlay" onclick="toggleOnlinePanel()"></div>
1852
1853  
1853<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js" defer></script>
1853
1854  
1854<script src="config/config.js"></script>
1854
1855  
1855<script src="js/api.js"></script>
1855
1856  <!-- Game scripts: lazy-loaded on demand via loadGameScript() -->
1857  
1857<script>
1858  /**
1859   * Lazy Script Loader
1860   * Loads JS/CSS files on demand and caches the promise to avoid duplicate loads.
1861   */
1862  (function() {
1863    'use strict';
1864    var _cache = {};
1865
1866    /**
1867     * Load a single script on demand. Returns a Promise.
1868     * Subsequent calls with the same src return the cached promise.
1869     */
1870    window.loadGameScript = function(src) {
1871      if (_cache[src]) return _cache[src];
1872      _cache[src] = new Promise(function(resolve, reject) {
1873        var s = document.createElement('script');
1874        s.src = src;
1875        s.async = true;
1876        s.onload = resolve;
1877        s.onerror = function() { delete _cache[src]; reject(new Error('Failed to load: ' + src)); };
1878        document.head.appendChild(s);
1879      });
1880      return _cache[src];
1881    };
1882
1883    /**
1884     * Load a CSS stylesheet on demand. Returns a Promise.
1885     */
1886    window.loadStylesheet = function(href) {
1887      if (_cache[href]) return _cache[href];
1888      _cache[href] = new Promise(function(resolve, reject) {
1889        var link = document.createElement('link');
1890        link.rel = 'stylesheet';
1891        link.type = 'text/css';
1892        link.href = href;
1893        link.onload = resolve;
1894        link.onerror = function() { delete _cache[href]; reject(new Error('Failed to load: ' + href)); };
1895        document.head.appendChild(link);
1896      });
1897      return _cache[href];
1898    };
1899
1900    /**
1901     * Load all game scripts (called once when user opens a game).
1902     * Returns a Promise that resolves when all are loaded.
1903     */
1904    var _allGamesPromise = null;
1905    window.loadAllGameScripts = function() {
1906      if (_allGamesPromise) return _allGamesPromise;
1907      _allGamesPromise = Promise.all([
1908        window.loadGameScript('js/dodge-game.js'),
1909        window.loadGameScript('js/galaga-game.js'),
1910        window.loadGameScript('js/pet-flappy.js'),
1911        window.loadGameScript('js/pet-snake.js'),
1912        window.loadGameScript('js/pet-breakout.js'),
1913        window.loadGameScript('js/pet-runner.js'),
1914        window.loadGameScript('js/pet-defense-game.js'),
1915        window.loadGameScript('js/tetris-battle.js'),
1916        window.loadGameScript('js/bible-card-battle.js'),
1917        window.loadGameScript('js/bible-typing-battle.js'),
1918        window.loadGameScript('js/bible-quiz-battle.js')
1919      ]);
1920      return _allGamesPromise;
1921    };
1922
1923    /**
1924     * Load Jspreadsheet CE + Jsuites on demand (for Excel editor).
1925     * Returns a Promise that resolves when both JS + CSS are loaded.
1926     */
1927    var _spreadsheetPromise = null;
1928    window.loadSpreadsheet = function() {
1929      if (_spreadsheetPromise) return _spreadsheetPromise;
1930      // If already available globally, resolve immediately
1931      if (typeof window.jspreadsheet !== 'undefined' || typeof window.jexcel !== 'undefined') {
1932        _spreadsheetPromise = Promise.resolve();
1933        return _spreadsheetPromise;
1934      }
1935      _spreadsheetPromise = Promise.all([
1936        window.loadStylesheet('https://jsuites.net/v4/jsuites.css'),
1937        window.loadStylesheet('https://bossanova.uk/jspreadsheet/v4/jexcel.css'),
1938        window.loadGameScript('https://jsuites.net/v4/jsuites.js').then(function() {
1939          // jexcel depends on jsuites, so load sequentially
1940          return window.loadGameScript('https://bossanova.uk/jspreadsheet/v4/jexcel.js');
1941        })
1942      ]);
1943      return _spreadsheetPromise;
1944    };
1945  })();
1946  </script>
1946
1947  
1947<script src="js/bible-writing-data.js"></script>
1947
1948  
1948<script src="js/pet-system.js"></script>
1948
1949  
1949<script src="js/colosseum-system.js"></script>
1949
1950  
1950<script src="js/bible-writing-game.js"></script>
1950
1951  
1951<script src="js/bible-reader.js"></script>
1951
1952  
1952<script src="js/script.js"></script>
1952
1953
1954  
1954<script type="module">
1955    // Import the functions you need from the SDKs you need
1956    import { initializeApp } from "https://www.gstatic.com/firebasejs/12.8.0/firebase-app.js";
1957    import {
1958      getFirestore,
1959      collection,
1960      addDoc,
1961      updateDoc, //
1962      deleteDoc,
1963      getDocs,
1964      getDoc,
1965      setDoc,
1966      doc,
1967      where,
1968      query,
1969      orderBy,
1970      limit,
1971      onSnapshot,
1972      serverTimestamp,
1973      arrayUnion,
1974      increment,
1975      deleteField,
1976      runTransaction
1977    } from "https://www.gstatic.com/firebasejs/12.8.0/firebase-firestore.js";
1978
1979    //
1980    import {
1981      getStorage,
1982      ref,
1983      uploadBytes,
1984      getDownloadURL,
1985      getBlob,
1986      deleteObject
1987    } from "https://www.gstatic.com/firebasejs/12.8.0/firebase-storage.js";
1988
1989    // ✅ Add Auth Import
1990    import {
1991      getAuth,
1992      GoogleAuthProvider,
1993      signInWithPopup,
1994      signOut,
1995      setPersistence,
1996      browserLocalPersistence
1997    } from "https://www.gstatic.com/firebasejs/12.8.0/firebase-auth.js";
1998
1999    // ✅ Add Realtime Database Import
2000    import {
2001      getDatabase,
2002      ref as rtdbRef,
2003      child as rtdbChild,
2004      set,
2005      update as rtdbUpdate,
2006      get as rtdbGet,
2007      push as rtdbPush,
2008      onDisconnect,
2009      onValue,
2010      onChildAdded as rtdbOnChildAdded,
2011      off as rtdbOff,
2012      remove,
2013      serverTimestamp as rtdbServerTimestamp
2014    } from "https://www.gstatic.com/firebasejs/12.8.0/firebase-database.js";
2015
2016    // ✅ Add Cloud Messaging Import (Push Notifications)
2017    import {
2018      getMessaging,
2019      getToken,
2020      onMessage
2021    } from "https://www.gstatic.com/firebasejs/12.8.0/firebase-messaging.js";
2022
2023    // config.js에서 window.firebaseConfig로 이미 설정됨
2024    const app = initializeApp(window.firebaseConfig);
2025    const db = getFirestore(app);
2026    const rtdb = getDatabase(app); // ✅ Initialize RTDB
2027    const storage = getStorage(app); //
2028    const chatCollection = collection(db, "chat_messages"); //
2029
2030    // ✅ Initialize Cloud Messaging (Push Notifications)
2031    let fcmMessaging = null;
2032    try {
2033      fcmMessaging = getMessaging(app);
2034    } catch (e) {
2035      console.warn('[FCM] Messaging init failed (not supported?):', e.message);
2036    } 
2037
2038    // ✅ Initialize Auth and Sign In Anonymously
2039    // const auth = getAuth(app);
2040    // signInAnonymously(auth)
2041    //   .then(() => {
2042    //     console.log("Firebase: Signed in anonymously");
2043    //   })
2044    //   .catch((error) => {
2045    //     console.error("Firebase Auth Error:", error);
2046    //   });
2047    const auth = getAuth(app);
2048    // ✅ 세션 지속성 명시적 설정 (LOCAL)
2049    setPersistence(auth, browserLocalPersistence)
2050      .then(() => {
2051        console.log("Firebase Persistence set to LOCAL");
2052      })
2053      .catch((error) => {
2054        console.error("Persistence Error:", error);
2055      });
2056
2057    // auth 상태 변화 감지 → window.auth.currentUser 동기화
2058    // [보안] 익명(anonymous) 로그인 금지 — 모든 사용자는 Google 계정으로만 인증
2059    // [Fix] minihp iframe의 Firebase Auth 인스턴스가 공유 IndexedDB를 통해
2060    // 부모의 auth 상태를 일시적으로 null로 만드는 문제 방지
2061    let _authEverConfirmed = false;
2062    let _pendingLogoutTimer = null;
2063    let _logoutInProgress = false;
2064
2065    auth.onAuthStateChanged((user) => {
2066      window.auth = auth;
2067      window.authInitialized = true; // presence 등에서 초기화 완료 확인용
2068
2069      if (user) {
2070        if (user.isAnonymous) {
2071          // 익명 세션 감지 → 즉시 로그아웃 (보안 정책)
2072          console.warn("Firebase Auth: 익명 세션 감지 → 강제 로그아웃");
2073          signOut(auth);
2074          return;
2075        }
2076        _authEverConfirmed = true;
2077        // ★ 인증 복구됨 — 대기 중인 로그아웃 타이머 취소
2078        if (_pendingLogoutTimer) {
2079          clearTimeout(_pendingLogoutTimer);
2080          _pendingLogoutTimer = null;
2081          console.log("Firebase Auth: 세션 복구됨, 로그아웃 취소");
2082        }
2083        console.log("Firebase Auth: Google 로그인됨 UID=", user.uid);
2084
2085        // ★ [Fix] Auth 확정 후 Presence 등록 (타이밍 경합 해결)
2086        setTimeout(function() {
2087          if (typeof initPresence === 'function') initPresence();
2088        }, 500);
2089      } else {
2090        // 미로그인 — 읽기 전용 모드 (익명 로그인 없음)
2091        console.log("Firebase Auth: 미로그인 (읽기 전용 모드)");
2092
2093        // ★ 의도적 로그아웃 중이면 즉시 처리 (타이머 불필요)
2094        if (_logoutInProgress) return;
2095
2096        // ★ 이전에 인증 확인된 적이 있고 localStorage에 유저가 있을 때만
2097        // 로그아웃 고려 (초기 로딩 중 일시적 null 방지)
2098        const savedUser = localStorage.getItem('myUser');
2099        if (savedUser && _authEverConfirmed) {
2100          // 기존 타이머 취소 후 새로 설정
2101          if (_pendingLogoutTimer) clearTimeout(_pendingLogoutTimer);
2102          _pendingLogoutTimer = setTimeout(() => {
2103            _pendingLogoutTimer = null;
2104            // ★ 5초 후에도 여전히 인증 없으면 → 세션이 진짜 만료된 것
2105            if (!auth.currentUser) {
2106              console.warn("Firebase 세션 확인 불가 → UI 로그아웃 처리");
2107              localStorage.removeItem('myUser');
2108              if (window.myUser) window.myUser = null;
2109              if (typeof window.uiLogin === 'function') window.uiLogin();
2110            }
2111          }, 5000); // 2초 → 5초로 증가 (iframe 간섭 대기)
2112        }
2113      }
2114    });
2115
2116    // ★ 의도적 로그아웃 플래그 노출 (doLogout에서 사용)
2117    window._setLogoutInProgress = function(val) { _logoutInProgress = val; };
2118
2119    // 
2120    window.db = db;
2121    window.rtdb = rtdb; // ✅ Expose RTDB
2122    window.rtdbRef = rtdbRef;
2123    window.rtdbSet = set;
2124    window.rtdbOnDisconnect = onDisconnect;
2125    window.rtdbOnValue = onValue;
2126    window.rtdbRemove = remove;
2127    window.rtdbGet = rtdbGet;
2128    window.rtdbPush = rtdbPush;
2129    window.rtdbUpdate = rtdbUpdate;
2130    window.rtdbOff = rtdbOff;
2131    window.rtdbChild = rtdbChild;
2132    window.rtdbOnChildAdded = rtdbOnChildAdded;
2133    window.rtdbServerTimestamp = rtdbServerTimestamp;
2134
2135    // ========== Firebase compat shim (for tetris-battle.js etc.) ==========
2136    var _fbCompatListeners = [];
2137    function _wrapSnap(snap) {
2138      return {
2139        val: function() { return snap.val(); },
2140        exists: function() { return snap.exists(); },
2141        ref: _wrapRef(snap.ref),
2142        key: snap.key
2143      };
2144    }
2145    function _wrapRef(rawRef) {
2146      var w = {
2147        _raw: rawRef,
2148        child: function(p) { return _wrapRef(rtdbChild(rawRef, p)); },
2149        set: function(v) { return set(rawRef, v); },
2150        update: function(v) { return rtdbUpdate(rawRef, v); },
2151        remove: function() { return remove(rawRef); },
2152        push: function(v) {
2153          var nr = rtdbPush(rawRef);
2154          var wrappedNr = _wrapRef(nr);
2155          if (v !== undefined && v !== null) {
2156            var p = set(nr, v);
2157            // Return a thenable ref (like Firebase v8 ThenableReference)
2158            wrappedNr.then = function(resolve, reject) { return p.then(resolve, reject); };
2159            wrappedNr.catch = function(reject) { return p.catch(reject); };
2160          }
2161          return wrappedNr;
2162        },
2163        once: function() {
2164          return rtdbGet(rawRef).then(function(s) { return _wrapSnap(s); });
2165        },
2166        on: function(evt, cb) {
2167          var wrapped = function(s) { cb(_wrapSnap(s)); };
2168          var unsub;
2169          if (evt === 'value') unsub = onValue(rawRef, wrapped);
2170          else if (evt === 'child_added') unsub = rtdbOnChildAdded(rawRef, wrapped);
2171          var path = rawRef.toString();
2172          _fbCompatListeners.push({ path: path, event: evt, callback: cb, unsub: unsub });
2173          return cb;
2174        },
2175        off: function(evt, cb) {
2176          var path = rawRef.toString();
2177          for (var i = _fbCompatListeners.length - 1; i >= 0; i--) {
2178            var l = _fbCompatListeners[i];
2179            if (l.path === path && l.event === evt && l.callback === cb) {
2180              if (typeof l.unsub === 'function') l.unsub();
2181              _fbCompatListeners.splice(i, 1);
2182              return;
2183            }
2184          }
2185        },
2186        onDisconnect: function() { return onDisconnect(rawRef); }
2187      };
2188      return w;
2189    }
2190    window.firebase = {
2191      auth: function() { return window.auth; },
2192      database: function() {
2193        return { ref: function(p) { return _wrapRef(rtdbRef(rtdb, p)); } };
2194      }
2195    };
2196    window.firebase.database.ServerValue = { TIMESTAMP: rtdbServerTimestamp() };
2197    // ========== End compat shim ==========
2198
2199    window.storage = storage; // 
2200    window.collection = collection;
2201    window.addDoc = addDoc;
2202    window.updateDoc = updateDoc; //
2203    window.setDoc = setDoc;
2204    window.getDoc = getDoc;
2205    window.getDocs = getDocs;
2206    window.deleteDoc = deleteDoc;
2207    window.doc = doc;
2208    window.where = where;
2209    window.query = query;
2210    window.orderBy = orderBy;
2211    window.limit = limit;
2212    window.serverTimestamp = serverTimestamp;
2213    window.arrayUnion = arrayUnion;
2214    window.onSnapshot = onSnapshot;
2215    window.increment = increment;
2216    window.deleteField = deleteField;
2217    window.runTransaction = runTransaction;
2218    window.auth = auth;
2219    window.GoogleAuthProvider = GoogleAuthProvider;
2220    window.signInWithPopup = signInWithPopup;
2221    window.signOut = signOut;
2222
2223    window.ref = ref;
2224    window.uploadBytes = uploadBytes;
2225    window.getDownloadURL = getDownloadURL;
2226    window.getBlob = getBlob;
2227    window.deleteObject = deleteObject;
2228
2229    // ✅ Expose FCM functions
2230    window.fcmMessaging = fcmMessaging;
2231    window.fcmGetToken = getToken;
2232    window.fcmOnMessage = onMessage;
2233
2234    // ✅ FCM 포그라운드 메시지 수신 (앱이 활성 상태일 때)
2235    if (fcmMessaging) {
2236      onMessage(fcmMessaging, (payload) => {
2237        console.log('[FCM] Foreground message:', payload);
2238        var data = payload.data || {};
2239        var notif = payload.notification || {};
2240        var title = notif.title || data.title || 'AYA 알림';
2241        var body = notif.body || data.body || '';
2242
2243        // SweetAlert2 토스트로 표시
2244        if (typeof Swal !== 'undefined') {
2245          Swal.fire({
2246            toast: true,
2247            position: 'top-end',
2248            icon: 'info',
2249            title: title,
2250            text: body,
2251            timer: 5000,
2252            showConfirmButton: false,
2253            customClass: { popup: 'retro-swal' }
2254          });
2255        }
2256
2257        // 알림 배지 업데이트
2258        if (typeof updateNotifBadge === 'function') updateNotifBadge();
2259      });
2260    }
2261
2262    const chatForm = document.getElementById('chat-form');
2263    const msgList = document.getElementById('chat-messages');
2264    const nameInput = document.getElementById('chat-input-name');
2265    const msgInput = document.getElementById('chat-input-msg');
2266
2267    // 
2268    const q = query(chatCollection, orderBy("createdAt", "desc"), limit(50));
2269
2270    if (firebaseConfig.apiKey === "YOUR_API_KEY") {
2271      msgList.innerHTML = '<div class="chat-msg" style="color:red; text-align:center; padding-top:20px;">⚠️ Firebase 연결이 필요합니다.<br>index.html 파일 하단의<br>firebaseConfig를 설정해주세요.</div>';
2272    } else {
2273      const unsubscribe = onSnapshot(q, (snapshot) => {
2274        const msgs = [];
2275        snapshot.forEach((doc) => {
2276          msgs.push({ id: doc.id, ...doc.data() });
2277        });
2278        renderMessages(msgs.reverse());
2279      }, (error) => {
2280        console.error("Chat Error:", error);
2281      });
2282    }
2283
2284    function renderMessages(messages) {
2285      msgList.innerHTML = "";
2286      if (messages.length === 0) {
2287        var _cl = localStorage.getItem('selectedLang') || 'ko';
2288        msgList.innerHTML = '<div style="text-align:center; padding:20px; color:#999;">' + (_cl === 'en' ? 'Be the first to leave a message!' : '첫 번째 메시지를 남겨보세요!') + '</div>';
2289        return;
2290      }
2291
2292      messages.forEach(msg => {
2293        const div = document.createElement("div");
2294        div.className = "chat-msg";
2295
2296        var _cl6 = localStorage.getItem('selectedLang') || 'ko';
2297        const safeName = escapeHtml(msg.name || (_cl6 === 'en' ? 'Anonymous' : '익명'));
2298        const safeText = escapeHtml(msg.text || "");
2299
2300        let timeStr = "";
2301        if (msg.createdAt && msg.createdAt.seconds) {
2302          const date = new Date(msg.createdAt.seconds * 1000);
2303          timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
2304        }
2305
2306        // ✅ 첨부 파일 렌더링
2307        var fileHtml = '';
2308        if (msg.fileUrl) {
2309          var isImage = msg.fileType && msg.fileType.startsWith('image/');
2310          if (isImage) {
2311            fileHtml = '<div class="chat-file-attachment">' +
2312              '<a href="' + escapeHtml(msg.fileUrl) + '" target="_blank" title="클릭하여 원본 보기">' +
2313              '<img src="' + escapeHtml(msg.fileUrl) + '" class="chat-attached-img" alt="' + escapeHtml(msg.fileName || '이미지') + '">' +
2314              '</a></div>';
2315          } else {
2316            var sizeStr = msg.fileSize ? ' (' + (msg.fileSize / 1024).toFixed(1) + 'KB)' : '';
2317            fileHtml = '<div class="chat-file-attachment">' +
2318              '<a href="' + escapeHtml(msg.fileUrl) + '" target="_blank" download="' + escapeHtml(msg.fileName || 'file') + '" class="chat-file-link">' +
2319              '📎 ' + escapeHtml(msg.fileName || '파일') + sizeStr +
2320              '</a></div>';
2321          }
2322        }
2323
2324        div.innerHTML = `
2325                <div style="display:flex; align-items:flex-start; margin-bottom: 6px;">
2326                    <div style="font-weight:bold; color:#5d4037; margin-right:6px; min-width:50px; text-align:right;">${safeName}</div>
2327                    <div style="background:#fff; padding:6px 10px; border-radius:8px; border:2px solid #5d4037; position:relative; max-width:75%;">
2328                        ${safeText ? '<div class="chat-text">' + safeText + '</div>' : ''}
2329                        ${fileHtml}
2330                        <div class="chat-time" style="position:absolute; bottom:-16px; right:0; font-size:10px; color:#aaa; width:max-content;">${timeStr}</div>
2331                    </div>
2332                </div>
2333            `;
2334        msgList.appendChild(div);
2335      });
2336      msgList.scrollTop = msgList.scrollHeight;
2337    }
2338
2339    //
2340    // [New] Mini Homepy Login State Handler
2341    let lastMiniHpLoginState = null; // 'loggedIn' or 'loggedOut'
2342
2343    function updateMiniHpLoginState() {
2344      const currentUser = window.myUser;
2345      const minihpContainer = document.getElementById('minihp-container');
2346      const minihpFrame = document.getElementById('minihp-frame');
2347
2348      if (!minihpContainer || !minihpFrame) return;
2349
2350      const minihpOverlay = document.getElementById('minihp-overlay'); // Interaction Hint Overlay
2351
2352      if (currentUser) {
2353        // Logged In
2354        if (lastMiniHpLoginState === 'loggedIn') return; // Debounce
2355
2356        // Remove overlay if exists
2357        const overlay = document.getElementById('minihp-login-overlay');
2358        if (overlay) overlay.remove();
2359
2360        // Show iframe
2361        minihpFrame.style.display = 'block';
2362        // 주의: 오버레이는 _setIframeInteractive에서 관리하므로 여기서 설정하지 않음
2363
2364        minihpContainer.style.height = 'auto'; // Reset height
2365
2366        // Load content if blank (Force reload to ensure clean state)
2367        // ★ 로그인 시 통합 메인 피드 페이지를 먼저 표시
2368        if (minihpFrame.src === 'about:blank' || minihpFrame.src === '') {
2369          var feedUrl = 'main-feed.html';
2370          if (currentUser && currentUser.phone) {
2371            feedUrl += '?vname=' + encodeURIComponent(currentUser.name || '');
2372            feedUrl += '&vphone=' + encodeURIComponent(currentUser.phone);
2373            if (currentUser.isAdmin) feedUrl += '&vadmin=true';
2374          }
2375          minihpFrame.src = feedUrl;
2376          var savedH = localStorage.getItem('iframeHeight');
2377          minihpFrame.style.height = (savedH && parseInt(savedH,10) >= 300) ? savedH + 'px' : '700px';
2378          // 메인피드 로드 시 오버레이 비활성화
2379          if (window._setIframeInteractive) window._setIframeInteractive(true);
2380        }
2381
2382        lastMiniHpLoginState = 'loggedIn';
2383
2384      } else {
2385        // Not Logged In
2386        if (lastMiniHpLoginState === 'loggedOut') return; // Debounce
2387
2388        // Create overlay if not exists
2389        if (!document.getElementById('minihp-login-overlay')) {
2390          const overlay = document.createElement('div');
2391          overlay.id = 'minihp-login-overlay';
2392          overlay.style.position = 'absolute';
2393          overlay.style.top = '0';
2394          overlay.style.left = '0';
2395          overlay.style.width = '100%';
2396          overlay.style.height = '100%';
2397          overlay.style.zIndex = '10'; // Higher than iframe
2398
2399          // Fallback color + Image (Using thumbnail API for reliability)
2400          overlay.style.backgroundColor = '#8d6e63';
2401          overlay.style.backgroundImage = "url('https://lh3.googleusercontent.com/d/1Mg_nppHvgFhzXC_mJsOY2FRORefEzvUL')";
2402          overlay.style.backgroundSize = 'cover';
2403          overlay.style.backgroundPosition = 'center';
2404          overlay.style.borderRadius = '15px'; // Match container radius
2405          overlay.style.display = 'flex';
2406          overlay.style.flexDirection = 'column';
2407          overlay.style.alignItems = 'center';
2408          overlay.style.justifyContent = 'center';
2409          overlay.style.color = '#fff';
2410
2411          overlay.innerHTML = `
2412            <div style="
2413              background: rgba(0, 0, 0, 0.4); 
2414              backdrop-filter: blur(8px); 
2415              -webkit-backdrop-filter: blur(8px);
2416              padding: 40px 60px; 
2417              border-radius: 20px; 
2418              border: 1px solid rgba(255, 255, 255, 0.3); 
2419              box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
2420              text-align: center;
2421              animation: fadeIn 0.5s ease-out;
2422            ">
2423              <div style="font-size: 48px; margin-bottom: 20px; filter: drop-shadow(0 0 10px rgba(255,255,255,0.5));">✨</div>
2424              <h2 style="
2425                margin: 0; 
2426                font-size: 28px; 
2427                font-weight: bold; 
2428                color: #fff; 
2429                text-shadow: 0 2px 10px rgba(0,0,0,0.5);
2430                font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
2431                letter-spacing: -1px;
2432              ">
2433                ` + ((localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Login Required' : '로그인이 필요합니다') + `
2434              </h2>
2435              <p style="
2436                margin-top: 15px;
2437                font-size: 16px;
2438                color: rgba(255, 255, 255, 0.9);
2439                line-height: 1.6;
2440                margin-bottom: 30px;
2441                font-weight: 300;
2442              ">
2443                ` + ((localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Log in to access<br><b style="color:#fff; font-weight:600;">our exclusive youth group space</b>.' : '로그인 하시면<br><b style="color:#fff; font-weight:600;">청년부만의 특별한 공간</b>이 열립니다.') + `
2444              </p>
2445              
2446              <button onclick="document.getElementById('loginModal').style.display='flex'" style="
2447                background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%);
2448                border: none;
2449                padding: 12px 35px;
2450                border-radius: 50px;
2451                color: #5d4037;
2452                font-size: 16px;
2453                font-weight: bold;
2454                cursor: pointer;
2455                box-shadow: 0 4px 15px rgba(255, 154, 158, 0.5);
2456                transition: transform 0.2s, box-shadow 0.2s;
2457              " 
2458              onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 20px rgba(255, 154, 158, 0.7)'"
2459              onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 4px 15px rgba(255, 154, 158, 0.5)'"
2460              >
2461                ` + ((localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Log in & Enter ➜' : '로그인 하고 입장하기 ➜') + `
2462              </button>
2463
2464              <div style="margin-top:16px;">
2465                <span onclick="enterAsGuest()" style="
2466                  color: rgba(255, 255, 255, 0.7);
2467                  font-size: 13px;
2468                  cursor: pointer;
2469                  text-decoration: underline;
2470                  text-underline-offset: 3px;
2471                  transition: color 0.2s;
2472                " 
2473                onmouseover="this.style.color='#fff'"
2474                onmouseout="this.style.color='rgba(255,255,255,0.7)'"
2475                >` + ((localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Preview as Guest' : '손님으로 미리보기') + `</span>
2476              </div>
2477            </div>
2478            <style>
2479              @keyframes fadeIn {
2480                from { opacity: 0; transform: translateY(20px); }
2481                to { opacity: 1; transform: translateY(0); }
2482              }
2483            </style>
2484          `;
2485
2486          minihpContainer.appendChild(overlay);
2487        }
2488
2489        // Hide iframe & Interaction Hint to clean up UI
2490        minihpFrame.style.display = 'none';
2491        if (minihpOverlay) minihpOverlay.style.display = 'none';
2492
2493        // Prevent container collapse
2494        minihpContainer.style.height = '560px';
2495
2496        lastMiniHpLoginState = 'loggedOut';
2497      }
2498    }
2499
2500    function monitorLoginStatus() {
2501      let currentUser = window.myUser; // Global scope user
2502
2503      // [New] Update Mini Homepy State
2504      updateMiniHpLoginState();
2505
2506      if (currentUser && currentUser.name) {
2507        //
2508        if (nameInput.value !== currentUser.name) {
2509          nameInput.value = currentUser.name;
2510        }
2511        //
2512        nameInput.disabled = true;
2513        nameInput.style.backgroundColor = "#e0e0e0";
2514        nameInput.style.color = "#555";
2515        var _cl7 = localStorage.getItem('selectedLang') || 'ko';
2516        nameInput.title = _cl7 === 'en' ? 'Logged-in name (read-only)' : '로그인된 이름 (수정불가)';
2517
2518      } else {
2519        // 
2520        let guestId = localStorage.getItem("chatGuestId");
2521        if (!guestId) {
2522          //
2523          guestId = Math.floor(Math.random() * 9999) + 1;
2524          localStorage.setItem("chatGuestId", guestId);
2525        }
2526
2527        var _cl8 = localStorage.getItem('selectedLang') || 'ko';
2528        const guestName = (_cl8 === 'en' ? 'Guest' : '손님') + guestId;
2529
2530        if (nameInput.value !== guestName) {
2531          nameInput.value = guestName;
2532        }
2533
2534        // Fix the name (Disable input)
2535        nameInput.disabled = true;
2536        nameInput.style.backgroundColor = "#fff3e0"; // Different color for Guest
2537        nameInput.style.color = "#e65100";
2538        nameInput.title = _cl8 === 'en' ? 'Guest nickname (fixed)' : '손님 닉네임 (고정됨)';
2539      }
2540    }
2541
2542    // 
2543    const loginMonitorInterval = setInterval(monitorLoginStatus, 1000);
2544
2545    // 
2546    setTimeout(monitorLoginStatus, 1000);
2547
2548    // ★ 손님 미리보기 함수
2549    window.enterAsGuest = function () {
2550      var minihpFrame = document.getElementById('minihp-frame');
2551      var minihpContainer = document.getElementById('minihp-container');
2552      var minihpOverlay = document.getElementById('minihp-overlay');
2553      var loginOverlay = document.getElementById('minihp-login-overlay');
2554
2555      if (loginOverlay) loginOverlay.remove();
2556      if (minihpFrame) {
2557        minihpFrame.style.display = 'block';
2558        try {
2559          sessionStorage.setItem('minihp_target_phone', 'demo');
2560          sessionStorage.setItem('minihp_guest_mode', 'true');
2561        } catch (e) { }
2562        minihpFrame.src = 'minihp.html?phone=demo&guest=true';
2563      }
2564      if (minihpOverlay) minihpOverlay.style.display = 'flex';
2565      if (minihpContainer) minihpContainer.style.height = 'auto';
2566    };
2567
2568    // 
2569    async function cleanupOldMessages() {
2570      try {
2571        console.log("Chat: Checking for old messages to clean up...");
2572        const retentionPeriod = 2; // days
2573        const cutoffDate = new Date();
2574        cutoffDate.setDate(cutoffDate.getDate() - retentionPeriod);
2575
2576        const oldMsgQuery = query(chatCollection, where("createdAt", "<", cutoffDate));
2577        const snapshot = await getDocs(oldMsgQuery);
2578
2579        if (!snapshot.empty) {
2580          let count = 0;
2581          const deletePromises = [];
2582          snapshot.forEach((doc) => {
2583            deletePromises.push(
2584              deleteDoc(doc.ref).then(() => { count++; }).catch(err => {
2585                console.warn("Failed to delete msg", err);
2586              })
2587            );
2588          });
2589          await Promise.all(deletePromises);
2590          console.log(`Chat: Cleaned up ${count} old messages.`);
2591        } else {
2592          console.log("Chat: No old messages to clean up.");
2593        }
2594      } catch (error) {
2595        console.error("Cleanup error:", error);
2596      }
2597    }
2598
2599    // 
2600    setTimeout(cleanupOldMessages, 3000);
2601
2602    //
2603    window.toggleChatContent = function () {
2604      const content = document.getElementById('chatContentArea');
2605      const icon = document.getElementById('chatToggleIcon');
2606      if (!content) return;
2607
2608      if (content.style.display === "none") {
2609        content.style.display = "block";
2610        if (icon) icon.innerText = "▼";
2611      } else {
2612        content.style.display = "none";
2613        if (icon) icon.innerText = "▲";
2614      }
2615    };
2616    // 
2617    if (chatForm) {
2618      chatForm.addEventListener('submit', async (e) => {
2619        e.preventDefault();
2620        const currentUser = window.myUser;
2621        let name = nameInput.value.trim();
2622
2623        //
2624        if (currentUser && currentUser.name) {
2625          name = currentUser.name;
2626        }
2627
2628        const text = msgInput.value.trim();
2629
2630        if (!name || (!text && !window._chatPendingFile)) return;
2631
2632        if (firebaseConfig.apiKey === "YOUR_API_KEY") {
2633          alert("Firebase 설정이 완료되지 않았습니다.");
2634          return;
2635        }
2636
2637        try {
2638          var msgData = {
2639            name: myUser ? myUser.name : ((localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Anonymous' : '익명'),
2640            phone: myUser ? myUser.phone : '',
2641            uid: auth.currentUser ? auth.currentUser.uid : null,
2642            text: text,
2643            createdAt: serverTimestamp()
2644          };
2645
2646          // ✅ 첨부 파일이 있으면 포함
2647          if (window._chatPendingFile) {
2648            msgData.fileUrl = window._chatPendingFile.url;
2649            msgData.fileName = window._chatPendingFile.name;
2650            msgData.fileType = window._chatPendingFile.type;
2651            msgData.fileSize = window._chatPendingFile.size;
2652            window._chatPendingFile = null;
2653            var preview = document.getElementById('chatFilePreview');
2654            if (preview) { preview.style.display = 'none'; preview.innerHTML = ''; }
2655          }
2656
2657          await addDoc(chatCollection, msgData);
2658          msgInput.value = ""; // Clear input
2659        } catch (error) {
2660          console.error("Error sending message: ", error);
2661          var _clErr = localStorage.getItem('selectedLang') || 'ko';
2662          alert((_clErr === 'en' ? 'Message send failed: ' : '메시지 전송 실패: ') + error.message);
2663        }
2664      });
2665    }
2666
2667    // escapeHtml은 js/script.js에서 전역으로 제공됨 (중복 제거)
2668
2669    // ✅ ===== 공유 이모티콘 피커 유틸리티 =====
2670    var _emojiCategories = {
2671      [((localStorage.getItem('selectedLang') || 'ko') === 'en' ? '😊 Face' : '😊 표정')]: ['😀','😁','😂','🤣','😃','😄','😅','😆','😉','😊','😋','😎','🤩','😍','🥰','😘','😗','🙂','🤗','🤔','🙄','😏','😥','😮','😯','😪','😫','😴','😌','😛','😜','😝','😒','😔','🙃','😲','😇','🥳','🥺','😈'],
2672      [((localStorage.getItem('selectedLang') || 'ko') === 'en' ? '👍 Hand' : '👍 손')]: ['👍','👎','👌','✌️','🤞','🤟','🤘','🤙','👈','👉','👆','👇','☝️','✋','🤚','🖖','👋','🤝','👏','🙏','💪'],
2673      [((localStorage.getItem('selectedLang') || 'ko') === 'en' ? '❤️ Heart' : '❤️ 하트')]: ['❤️','🧡','💛','💚','💙','💜','🖤','🤍','💔','❣️','💕','💞','💓','💗','💖','💘','💝'],
2674      [((localStorage.getItem('selectedLang') || 'ko') === 'en' ? '🎉 Etc' : '🎉 기타')]: ['🎉','🎊','🎈','🎁','🏆','🥇','⚽','🎮','🎵','🎶','💐','🌸','🌹','🌻','✨','🔥','💯','⭐','🌈','☀️','🌙','❄️','🍕','🍔','☕','🍰','🎂','🍩']
2675    };
2676
2677    function createEmojiPicker(containerEl, onSelect) {
2678      if (!containerEl) return;
2679      containerEl.innerHTML = '';
2680      var tabBar = document.createElement('div');
2681      tabBar.className = 'emoji-tabs';
2682      var grid = document.createElement('div');
2683      grid.className = 'emoji-grid';
2684
2685      var catKeys = Object.keys(_emojiCategories);
2686      catKeys.forEach(function(catName, idx) {
2687        var tab = document.createElement('button');
2688        tab.type = 'button';
2689        tab.className = 'emoji-tab' + (idx === 0 ? ' active' : '');
2690        tab.textContent = catName.split(' ')[0];
2691        tab.title = catName;
2692        tab.onclick = function() {
2693          tabBar.querySelectorAll('.emoji-tab').forEach(function(t) { t.classList.remove('active'); });
2694          tab.classList.add('active');
2695          renderEmojiGrid(grid, catName, onSelect);
2696        };
2697        tabBar.appendChild(tab);
2698      });
2699
2700      containerEl.appendChild(tabBar);
2701      containerEl.appendChild(grid);
2702      renderEmojiGrid(grid, catKeys[0], onSelect);
2703    }
2704
2705    function renderEmojiGrid(gridEl, catName, onSelect) {
2706      gridEl.innerHTML = '';
2707      var emojis = _emojiCategories[catName] || [];
2708      emojis.forEach(function(em) {
2709        var span = document.createElement('span');
2710        span.className = 'emoji-item';
2711        span.textContent = em;
2712        span.onclick = function() { if (onSelect) onSelect(em); };
2713        gridEl.appendChild(span);
2714      });
2715    }
2716
2717    // ✅ ===== 채팅 이모티콘 피커 초기화 =====
2718    (function initChatEmoji() {
2719      var emojiBtn = document.getElementById('chatEmojiBtn');
2720      var emojiPicker = document.getElementById('chatEmojiPicker');
2721      var msgInput = document.getElementById('chat-input-msg');
2722      if (!emojiBtn || !emojiPicker || !msgInput) return;
2723
2724      var pickerReady = false;
2725      emojiBtn.addEventListener('click', function(e) {
2726        e.preventDefault();
2727        e.stopPropagation();
2728        if (!pickerReady) {
2729          createEmojiPicker(emojiPicker, function(emoji) {
2730            msgInput.value += emoji;
2731            msgInput.focus();
2732          });
2733          pickerReady = true;
2734        }
2735        emojiPicker.style.display = emojiPicker.style.display === 'none' ? 'block' : 'none';
2736      });
2737      // 외부 클릭 시 닫기
2738      document.addEventListener('click', function(e) {
2739        if (emojiPicker.style.display !== 'none' && !emojiPicker.contains(e.target) && e.target !== emojiBtn) {
2740          emojiPicker.style.display = 'none';
2741        }
2742      });
2743    })();
2744
2745    // ✅ ===== 채팅 파일 첨부 =====
2746    window._chatPendingFile = null;
2747    (function initChatFileUpload() {
2748      var fileBtn = document.getElementById('chatFileBtn');
2749      var fileInput = document.getElementById('chatFileInput');
2750      var preview = document.getElementById('chatFilePreview');
2751      if (!fileBtn || !fileInput || !preview) return;
2752
2753      fileBtn.addEventListener('click', function(e) {
2754        e.preventDefault();
2755        fileInput.click();
2756      });
2757
2758      fileInput.addEventListener('change', async function() {
2759        var file = fileInput.files[0];
2760        if (!file) return;
2761        if (file.size > 10 * 1024 * 1024) {
2762          var _cl9 = localStorage.getItem('selectedLang') || 'ko';
2763          alert(_cl9 === 'en' ? 'File size exceeds 10MB.' : '파일 크기가 10MB를 초과합니다.');
2764          fileInput.value = '';
2765          return;
2766        }
2767
2768        preview.style.display = 'flex';
2769        var _cl10 = localStorage.getItem('selectedLang') || 'ko';
2770        preview.innerHTML = '<span style="color:#888;">
2770' + (_cl10 === 'en' ? 'Uploading...' : '업로드 중...') + '</span>';
2771
2772        try {
2773          var safeName = file.name.replace(/[^a-zA-Z0-9._\-\uAC00-\uD7AF]/g, '_');
2774          var storagePath = 'chat_files/' + Date.now() + '_' + safeName;
2775          var storageRef = window.ref(window.storage, storagePath);
2776          await window.uploadBytes(storageRef, file);
2777          var url = await window.getDownloadURL(storageRef);
2778
2779          window._chatPendingFile = { url: url, name: file.name, type: file.type, size: file.size };
2780
2781          var isImage = file.type.startsWith('image/');
2782          if (isImage) {
2783            preview.innerHTML = '<img src="' + url + '" style="max-height:60px;max-width:100px;border-radius:4px;object-fit:cover;">' +
2784              '<span style="font-size:11px;color:#666;margin-left:6px;">' + escapeHtml(file.name) + '</span>' +
2785              '<button type="button" onclick="cancelChatFile()" style="margin-left:auto;background:none;border:none;cursor:pointer;font-size:14px;">✕</button>';
2786          } else {
2787            preview.innerHTML = '<span style="font-size:11px;">📎 ' + escapeHtml(file.name) + ' (' + (file.size/1024).toFixed(1) + 'KB)</span>' +
2788              '<button type="button" onclick="cancelChatFile()" style="margin-left:auto;background:none;border:none;cursor:pointer;font-size:14px;">✕</button>';
2789          }
2790        } catch (err) {
2791          console.error('Chat file upload error:', err);
2792          var _cl11 = localStorage.getItem('selectedLang') || 'ko';
2793          preview.innerHTML = '<span style="color:red;font-size:11px;">' + (_cl11 === 'en' ? 'Upload failed' : '업로드 실패') + '</span>';
2794          setTimeout(function() { preview.style.display = 'none'; }, 2000);
2795        }
2796        fileInput.value = '';
2797      });
2798    })();
2799
2800    window.cancelChatFile = function() {
2801      window._chatPendingFile = null;
2802      var preview = document.getElementById('chatFilePreview');
2803      if (preview) { preview.style.display = 'none'; preview.innerHTML = ''; }
2804    };
2805
2806    // ✅ ===== 채팅창 드래그 리사이즈 (하단 핸들) =====
2807    (function initChatResize() {
2808      var handle = document.getElementById('chatResizeHandle');
2809      var msgBox = document.getElementById('chat-messages');
2810      if (!handle || !msgBox) return;
2811
2812      // localStorage에서 저장된 높이 복원
2813      var saved = localStorage.getItem('chatHeight');
2814      if (saved) {
2815        var h = parseInt(saved, 10);
2816        if (h >= 100 && h <= 500) msgBox.style.height = h + 'px';
2817      }
2818
2819      var dragging = false;
2820      var startY = 0;
2821      var startH = 0;
2822
2823      function onStart(clientY) {
2824        dragging = true;
2825        startY = clientY;
2826        startH = msgBox.offsetHeight;
2827        document.body.style.userSelect = 'none';
2828        handle.classList.add('active');
2829      }
2830      function onMove(clientY) {
2831        if (!dragging) return;
2832        var diff = clientY - startY; // 아래로 드래그 시 높이 증가
2833        var newH = Math.max(100, Math.min(500, startH + diff));
2834        msgBox.style.height = newH + 'px';
2835      }
2836      function onEnd() {
2837        if (!dragging) return;
2838        dragging = false;
2839        document.body.style.userSelect = '';
2840        handle.classList.remove('active');
2841        localStorage.setItem('chatHeight', msgBox.offsetHeight);
2842      }
2843
2844      handle.addEventListener('mousedown', function(e) { e.preventDefault(); onStart(e.clientY); });
2845      document.addEventListener('mousemove', function(e) { onMove(e.clientY); });
2846      document.addEventListener('mouseup', onEnd);
2847
2848      handle.addEventListener('touchstart', function(e) { e.preventDefault(); onStart(e.touches[0].clientY); }, { passive: false });
2849      document.addEventListener('touchmove', function(e) { if (dragging) onMove(e.touches[0].clientY); }, { passive: true });
2850      document.addEventListener('touchend', onEnd);
2851    })();
2852
2853
2854    // ✅ ===== 메인피드 iframe 드래그 리사이즈 =====
2855    (function initIframeResize() {
2856      var handle = document.getElementById('iframeResizeHandle');
2857      var iframe = document.getElementById('minihp-frame');
2858      if (!handle || !iframe) return;
2859
2860      // localStorage에서 저장된 높이 복원
2861      var saved = localStorage.getItem('iframeHeight');
2862      if (saved) {
2863        var h = parseInt(saved, 10);
2864        if (h >
2864= 300 && h <= 1500) iframe.style.height = h + 'px';
2865      }
2866
2867      var dragging = false;
2868      var startY = 0;
2869      var startH = 0;
2870      var savedPointerEvents = '';
2871
2872      function onStart(clientY) {
2873        dragging = true;
2874        startY = clientY;
2875        startH = iframe.offsetHeight;
2876        document.body.style.userSelect = 'none';
2877        document.body.style.webkitUserSelect = 'none';
2878        handle.classList.add('active');
2879        savedPointerEvents = iframe.style.pointerEvents;
2880        iframe.style.pointerEvents = 'none';
2881      }
2882      function onMove(clientY) {
2883        var diff = clientY - startY;
2884        var maxH = Math.floor(window.innerHeight * 0.9);
2885        var newH = Math.max(300, Math.min(maxH, startH + diff));
2886        iframe.style.height = newH + 'px';
2887      }
2888      function onEnd() {
2889        if (!dragging) return;
2890        dragging = false;
2891        document.body.style.userSelect = '';
2892        document.body.style.webkitUserSelect = '';
2893        handle.classList.remove('active');
2894        iframe.style.pointerEvents = savedPointerEvents;
2895        localStorage.setItem('iframeHeight', iframe.offsetHeight);
2896      }
2897
2898      // Mouse events (PC)
2899      handle.addEventListener('mousedown', function(e) { e.preventDefault(); onStart(e.clientY); });
2900      document.addEventListener('mousemove', function(e) { if (dragging) onMove(e.clientY); });
2901      document.addEventListener('mouseup', onEnd);
2902
2903      // Touch events (Mobile) - 동적 등록/해제로 스크롤 간섭 방지
2904      var touchMoveHandler = function(e) {
2905        e.preventDefault();
2906        onMove(e.touches[0].clientY);
2907      };
2908      var touchEndHandler = function() {
2909        onEnd();
2910        document.removeEventListener('touchmove', touchMoveHandler);
2911        document.removeEventListener('touchend', touchEndHandler);
2912      };
2913      handle.addEventListener('touchstart', function(e) {
2914        e.preventDefault();
2915        e.stopPropagation();
2916        onStart(e.touches[0].clientY);
2917        document.addEventListener('touchmove', touchMoveHandler, { passive: false });
2918        document.addEventListener('touchend', touchEndHandler);
2919      }, { passive: false });
2920    })();
2921
2922    // ✅ ===== 1:1 DM 대화 시스템 =====
2923    var _dmState = {
2924      roomId: null,
2925      targetPhone: null,
2926      targetName: null,
2927      unsubscribe: null,
2928      emojiReady: false
2929    };
2930
2931    function getDMRoomId(phone1, phone2) {
2932      var sorted = [String(phone1), String(phone2)].sort();
2933      return 'dm_' + sorted[0] + '_' + sorted[1];
2934    }
2935
2936    window.openDMChat = async function(targetPhone, targetName) {
2937      if (!window.myUser || !window.myUser.phone) {
2938        var _cl12 = localStorage.getItem('selectedLang') || 'ko';
2939        Swal.fire(_cl12 === 'en' ? 'Login Required' : '로그인 필요', _cl12 === 'en' ? 'Please log in to start a 1:1 chat.' : '1:1 대화를 하려면 로그인이 필요합니다.', 'warning');
2940        return;
2941      }
2942      if (String(targetPhone) === String(window.myUser.phone)) return;
2943
2944      var myUid = window.auth.currentUser ? window.auth.currentUser.uid : '';
2945      if (!myUid) {
2946        var _cl13 = localStorage.getItem('selectedLang') || 'ko';
2947        Swal.fire(_cl13 === 'en' ? 'Auth Error' : '인증 오류', _cl13 === 'en' ? 'Please check your login status.' : '로그인 상태를 확인해주세요.', 'error');
2948        return;
2949      }
2950
2951      var roomId = getDMRoomId(window.myUser.phone, targetPhone);
2952      _dmState.roomId = roomId;
2953      _dmState.targetPhone = targetPhone;
2954      _dmState.targetName = targetName;
2955
2956      var _cl14 = localStorage.getItem('selectedLang') || 'ko';
2957      document.getElementById('dmTargetName').textContent = targetName || (_cl14 === 'en' ? 'Chat Partner' : '대화 상대');
2958      document.getElementById('dmMessages').innerHTML = '<div style="text-align:center;padding:30px;color:#999;">' + (_cl14 === 'en' ? 'Loading...' : '불러오는 중...') + '</div>';
2959      document.getElementById('dmInput').value = '';
2960      document.getElementById('dmOverlay').style.display = 'flex';
2961
2962      // DM Room 생성 또는 참여자 등록 (메시지 구독 전에 반드시 완료해야 함)
2963      var roomReady = false;
2964      try {
2965        var roomRef = window.doc(window.db, 'dm_rooms', roomId);
2966        var roomSnap = await window.getDoc(roomRef);
2967        var roomExists = typeof roomSnap.exists === 'function' ? roomSnap.exists() : roomSnap.exists;
2968
2969        if (!roomExists) {
2970          // 새 방 생성 — 내 UID 포함
2971          var roomData = {
2972            participants: [String(window.myUser.phone), String(targetPhone)].sort(),
2973            participantNames: {},
2974            participantUids: [myUid],
2975            lastMessage: '',
2976            lastMessageAt: window.serverTimestamp(),
2977            createdAt: window.serverTimestamp()
2978          };
2979          roomData.participantNames[String(window.myUser.phone)] = window.myUser.name;
2980          roomData.participantNames[String(targetPhone)] = targetName;
2981          await window.setDoc(roomRef, roomData);
2982          roomReady = true;
2983        } else {
2984          // 기존 방 — 내 UID가 아직 없으면 추가
2985          var existing = roomSnap.data();
2986          if (!existing.participantUids || !existing.participantUids.includes(myUid)) {
2987            await window.updateDoc(roomRef, {
2988              participantUids: window.arrayUnion(myUid)
2989            });
2990          }
2991          roomReady = true;
2992        }
2993      } catch (err) {
2994        console.error('DM room create error:', err);
2995        document.getElementById('dmMessages').innerHTML =
2996          '<div style="text-align:center;padding:30px;color:#e53935;">' + ((localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Failed to connect to chat room.<br>Please try again later.' : '대화방 연결에 실패했습니다.<br>잠시 후 다시 시도해주세요.') + '</div>';
2997      }
2998
2999      // 메시지 실시간 구독 — 방 준비 완료 후에만 실행
3000      if (_dmState.unsubscribe) { _dmState.unsubscribe(); _dmState.unsubscribe = null; }
3001      if (roomReady) {
3002        var msgCol = window.collection(window.db, 'dm_rooms', roomId, 'messages');
3003        var msgQuery = window.query(msgCol, window.orderBy('createdAt', 'asc'), window.limit(100));
3004        _dmState.unsubscribe = window.onSnapshot(msgQuery, function(snapshot) {
3005          var msgs = [];
3006          snapshot.forEach(function(doc) { msgs.push(doc.data()); });
3007          renderDMMessages(msgs);
3008        }, function(err) {
3009          console.error('DM snapshot error:', err);
3010          document.getElementById('dmMessages').innerHTML =
3011            '<div style="text-align:center;padding:30px;color:#e53935;">' + ((localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Unable to load messages.' : '메시지를 불러올 수 없습니다.') + '</div>';
3012        });
3013      }
3014
3015      // DM 이모티콘 피커 초기화
3016      if (!_dmState.emojiReady) {
3017        var dmEmojiBtn = document.getElementById('dmEmojiBtn');
3018        var dmEmojiPicker = document.getElementById('dmEmojiPicker');
3019        var dmInput = document.getElementById('dmInput');
3020        if (dmEmojiBtn && dmEmojiPicker && dmInput) {
3021          createEmojiPicker(dmEmojiPicker, function(emoji) {
3022            dmInput.value += emoji;
3023            dmInput.focus();
3024          });
3025          dmEmojiBtn.onclick = function(e) {
3026            e.preventDefault();
3027            e.stopPropagation();
3028            dmEmojiPicker.style.display = dmEmojiPicker.style.display === 'none' ? 'block' : 'none';
3029          };
3030          _dmState.emojiReady = true;
3031        }
3032      }
3033
3034      // DM 이미지 업로드
3035      var dmImgBtn = document.getElementById('dmImageBtn');
3036      var dmImgInput = document.getElementById('dmImageInput');
3037      if (dmImgBtn && dmImgInput) {
3038        dmImgBtn.onclick = function(e) { e.preventDefault(); dmImgInput.click(); };
3039        dmImgInput.onchange = async function() {
3040          var file = dmImgInput.files[0];
3041          if (!file) return;
3042          if (file.size > 10 * 1024 * 1024) { alert((localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Image size exceeds 10MB.' : '이미지 크기가 10MB를 초과합니다.'); dmImgInput.value = ''; return; }
3043          try {
3044            var safeName = file.name.replace(/[^a-zA-Z0-9._\-]/g, '_');
3045            var storagePath = 'dm_images/' + _dmState.roomId + '/' + Date.now() + '_' + safeName;
3046            var storageRef = window.ref(window.storage, storagePath);
3047            await window.uploadBytes(storageRef, file);
3048            var url = await window.getDownloadURL(storageRef);
3049            await _sendDMMsg('', url);
3050          } catch (err) {
3051            console.error('DM image upload error:', err);
3052            alert((localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Image upload failed' : '이미지 업로드 실패');
3053          }
3054          dmImgInput.value = '';
3055        };
3056      }
3057
3058      // Enter 키 전송
3059      var dmInput = document.getElementById('dmInput');
3060      dmInput.onkeydown = function(e) {
3061        if (e.key === 'Enter') { e.preventDefault(); sendDMMessage(); }
3062      };
3063      dmInput.focus();
3064    };
3065
3066    function renderDMMessages(messages) {
3067      var container = document.getElementById('dmMessages');
3068      if (!container) return;
3069      container.innerHTML = '';
3070
3071      if (messages.length === 0) {
3072        var _cl15 = localStorage.getItem('selectedLang') || 'ko';
3073        container.innerHTML = '<div style="text-align:center;padding:30px;color:#999;">' + (_cl15 === 'en' ? 'Send the first message!' : '첫 메시지를 보내보세요!') + '</div>';
3074        return;
3075      }
3076
3077      var myPhone = window.myUser ? String(window.myUser.phone) : '';
3078      messages.forEach(function(msg) {
3079        var isMine = String(msg.senderPhone) === myPhone;
3080        var div = document.createElement('div');
3081        div.className = 'dm-msg ' + (isMine ? 'dm-msg-mine' : 'dm-msg-other');
3082
3083        var timeStr = '';
3084        if (msg.createdAt && msg.createdAt.seconds) {
3085          var d = new Date(msg.createdAt.seconds * 1000);
3086          timeStr = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
3087        }
3088
3089        var contentHtml = '';
3090        if (msg.imageUrl) {
3091          contentHtml = '<a href="' + escapeHtml(msg.imageUrl) + '" target="_blank">' +
3092            '<img src="' + escapeHtml(msg.imageUrl) + '" class="dm-msg-img"></a>';
3093        }
3094        if (msg.text) {
3095          contentHtml += '<div class="dm-msg-text">' + escapeHtml(msg.text) + '</div>';
3096        }
3097
3098        div.innerHTML =
3099          (isMine ? '' : '<div class="dm-msg-name">' + escapeHtml(msg.senderName || ((localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Other' : '상대방')) + '</div>') +
3100          '<div class="dm-msg-bubble ' + (isMine ? 'dm-bubble-mine' : 'dm-bubble-other') + '">' +
3101            contentHtml +
3102          '</div>' +
3103          '<div class="dm-msg-time">' + timeStr + '</div>';
3104
3105        container.appendChild(div);
3106      });
3107      container.scrollTop = container.scrollHeight;
3108    }
3109
3110    window.sendDMMessage = async function() {
3111      var input = document.getElementById('dmInput');
3112      var text = input ? input.value.trim() : '';
3113      if (!text) return;
3114      input.value = '';
3115      await _sendDMMsg(text, null);
3116    };
3117
3118    async function _sendDMMsg(text, imageUrl) {
3119      if (!_dmState.roomId || !window.myUser) return;
3120      try {
3121        var msgData = {
3122          senderPhone: String(window.myUser.phone),
3123          senderName: window.myUser.name || '나',
3124          uid: window.auth.currentUser ? window.auth.currentUser.uid : '',
3125          text: text || '',
3126          createdAt: window.serverTimestamp(),
3127          type: imageUrl ? 'image' : 'text'
3128        };
3129        if (imageUrl) msgData.imageUrl = imageUrl;
3130
3131        var msgCol = window.collection(window.db, 'dm_rooms', _dmState.roomId, 'messages');
3132        await window.addDoc(msgCol, msgData);
3133
3134        // lastMessage 업데이트
3135        var roomRef = window.doc(window.db, 'dm_rooms', _dmState.roomId);
3136        await window.updateDoc(roomRef, {
3137          lastMessage: imageUrl ? ((localStorage.getItem('selectedLang') || 'ko') === 'en' ? '📷 Photo' : '📷 사진') : (text.length > 30 ? text.substring(0, 30) + '...' : text),
3138          lastMessageAt: window.serverTimestamp(),
3139          lastSenderPhone: String(window.myUser.phone),
3140          lastSenderName: window.myUser.name || '나'
3141        });
3142      } catch (err) {
3143        console.error('DM send error:', err);
3144      }
3145    }
3146
3147    window.closeDMChat = function() {
3148      if (_dmState.unsubscribe) { _dmState.unsubscribe(); _dmState.unsubscribe = null; }
3149      _dmState.roomId = null;
3150      document.getElementById('dmOverlay').style.display = 'none';
3151      var dmEmojiPicker = document.getElementById('dmEmojiPicker');
3152      if (dmEmojiPicker) dmEmojiPicker.style.display = 'none';
3153    };
3154
3155    // ✅ ===== DM 수신 알림 리스너 =====
3156    var _dmNotifUnsub = null;
3157
3158    window.initDMNotificationListener = function() {
3159      // 기존 리스너 정리
3160      if (_dmNotifUnsub) { _dmNotifUnsub(); _dmNotifUnsub = null; }
3161
3162      var myPhone = window.myUser ? String(window.myUser.phone) : '';
3163      if (!myPhone) return;
3164
3165      var roomsQuery = window.query(
3166        window.collection(window.db, 'dm_rooms'),
3167        window.where('participants', 'array-contains', myPhone)
3168      );
3169
3170      var isFirstLoad = true;
3171
3172      _dmNotifUnsub = window.onSnapshot(roomsQuery, function(snapshot) {
3173        // 첫 로드 시에는 알림 안 띄움 (기존 데이터)
3174        if (isFirstLoad) { isFirstLoad = false; return; }
3175
3176        snapshot.docChanges().forEach(function(change) {
3177          if (change.type !== 'modified') return;
3178          var d = change.doc.data();
3179
3180          // 내가 보낸 메시지면 무시
3181          if (!d.lastSenderPhone || String(d.lastSenderPhone) === myPhone) return;
3182
3183          // DM 창이 이미 해당 방에 열려있으면 무시
3184          if (_dmState.roomId === change.doc.id &&
3185              document.getElementById('dmOverlay').style.display !== 'none') return;
3186
3187          var _cl16 = localStorage.getItem('selectedLang') || 'ko';
3188          var senderName = d.lastSenderName || (_cl16 === 'en' ? 'Someone' : '누군가');
3189          var preview = d.lastMessage || (_cl16 === 'en' ? 'New message' : '새 메시지');
3190
3191          Swal.fire({
3192            title: _cl16 === 'en' ? '💬 Message from ' + senderName : '💬 ' + senderName + '님의 메시지',
3193            text: preview,
3194            icon: 'info',
3195            confirmButtonText: _cl16 === 'en' ? 'Open Chat' : '대화 열기',
3196            showCancelButton: true,
3197            cancelButtonText: _cl16 === 'en' ? 'Close' : '닫기',
3198            confirmButtonColor: '#7c4dff',
3199            customClass: { popup: 'retro-swal' },
3200            didOpen: function() {
3201              var c = Swal.getContainer();
3202              if (c) c.style.zIndex = '500000';
3203            }
3204          }).then(function(result) {
3205            if (result.isConfirmed) {
3206              // 상대방 전화번호 추출 (participants에서 내 번호 제외)
3207              var targetPhone = '';
3208              if (d.participants) {
3209                for (var i = 0; i < d.participants.length; i++) {
3210                  if (String(d.participants[i]) !== myPhone) {
3211                    targetPhone = d.participants[i];
3212                    break;
3213                  }
3214                }
3215              }
3216              var targetName = d.lastSenderName || (_cl16 === 'en' ? 'Chat Partner' : '대화 상대');
3217              if (targetPhone && typeof window.openDMChat === 'function') {
3218                window.openDMChat(targetPhone, targetName);
3219              }
3220            }
3221          });
3222        });
3223      }, function(err) {
3224        console.warn('DM notification listener error:', err);
3225      });
3226
3227      console.log('✅ DM notification listener initialized');
3228    };
3229
3230    window.stopDMNotificationListener = function() {
3231      if (_dmNotifUnsub) { _dmNotifUnsub(); _dmNotifUnsub = null; }
3232    };
3233
3234    // ★ 미니홈피 간 이동 메시지 수신
3235    window.addEventListener('message', function (e) {
3236      if (!e.data || !e.data.type) return;
3237      // [Security] Only accept messages from same origin
3238      if (e.origin !== window.location.origin) return;
3239      var iframe = document.getElementById('minihp-frame');
3240      var currentUser = window.myUser;
3241
3242      if (e.data.type === 'NAVIGATE_MINIHP' || e.data.type === 'navigate-minihp') {
3243        var targetPhone = e.data.phone;
3244        console.log('[INDEX] 미니홈피 이동 요청 수신:', targetPhone);
3245        if (iframe && targetPhone) {
3246          try {
3247            sessionStorage.setItem('minihp_target_phone', targetPhone);
3248          } catch (err) { }
3249
3250          var navUrl = 'minihp.html?phone=' + encodeURIComponent(targetPhone);
3251          if (e.data.tab) navUrl += '&tab=' + encodeURIComponent(e.data.tab);
3252          if (currentUser && currentUser.phone) {
3253            navUrl += '&vname=' + encodeURIComponent(currentUser.name || '');
3254            navUrl += '&vphone=' + encodeURIComponent(currentUser.phone);
3255            if (currentUser.isAdmin) navUrl += '&vadmin=true';
3256          }
3257          iframe.src = navUrl;
3258          iframe.style.height = '560px'; // 미니홈피 높이로 복원
3259          // 미니홈피에서는 오버레이 복원
3260          if (window._setIframeInteractive) window._setIframeInteractive(false);
3261          console.log('[INDEX] iframe → minihp.html');
3262        }
3263      }
3264
3265      // ★ 통합 메인 피드로 돌아가기
3266      if (e.data.type === 'navigate-main-feed') {
3267        console.log('[INDEX] 메인 피드 이동 요청 수신');
3268        if (iframe) {
3269          var feedUrl = 'main-feed.html';
3270          if (currentUser && currentUser.phone) {
3271            feedUrl += '?vname=' + encodeURIComponent(currentUser.name || '');
3272            feedUrl += '&vphone=' + encodeURIComponent(currentUser.phone);
3273            if (currentUser.isAdmin) feedUrl += '&vadmin=true';
3274          }
3275          iframe.src = feedUrl;
3276          var _sh = localStorage.getItem('iframeHeight');
3277          iframe.style.height = (_sh && parseInt(_sh,10) >= 300) ? _sh + 'px' : '700px';
3278          // 메인피드에서는 오버레이 비활성화
3279          if (window._setIframeInteractive) window._setIframeInteractive(true);
3280          console.log('[INDEX] iframe → main-feed.html');
3281        }
3282      }
3283
3284      // ★ 메인 피드 로드 완료 → iframe 높이 조정 + 오버레이 비활성화
3285      if (e.data.type === 'main-feed-loaded') {
3286        if (iframe) {
3287          var _sh2 = localStorage.getItem('iframeHeight');
3288          iframe.style.height = (_sh2 && parseInt(_sh2,10) >= 300) ? _sh2 + 'px' : '700px';
3289        }
3290        // 메인피드에서는 오버레이 비활성화 (터치 차단 방지)
3291        if (window._setIframeInteractive) window._setIframeInteractive(true);
3292      }
3293
3294      // ★ 모바일 리사이즈: iframe 내부에서 드래그로 높이 조절
3295      if (e.data.type === 'iframe-resize-delta') {
3296        if (iframe) {
3297          var curH = iframe.offsetHeight;
3298          var newH = Math.max(300, curH + (e.data.delta || 0));
3299          iframe.style.height = newH + 'px';
3300        }
3301      }
3302      if (e.data.type === 'iframe-resize-end') {
3303        if (iframe) {
3304          localStorage.setItem('iframeHeight', iframe.offsetHeight);
3305        }
3306      }
3307    });
3308
3309  </script>
3309
3310
3311  <!-- ✅ 1:1 DM 대화 UI -->
3312  <div id="dmOverlay" class="dm-overlay" style="display:none;">
3313    <div class="dm-container">
3314      <div class="dm-header">
3315        <span class="dm-header-title">💬 <span id="dmTargetName"><span id="txt-dm-default-name">대화 상대</span></span></span>
3316        <button id="dmCloseBtn" class="dm-close-btn" onclick="closeDMChat()">✕</button>
3317      </div>
3318      <div id="dmMessages" class="dm-messages"></div>
3319      <div class="dm-input-area">
3320        <button type="button" id="dmEmojiBtn" class="chat-tool-btn dm-emoji-btn" title="이모티콘">😊</button>
3321        <button type="button" id="dmImageBtn" class="chat-tool-btn dm-img-btn" title="사진 보내기">📷</button>
3322        <input type="file" id="dmImageInput" accept="image/*" style="display:none;">
3323        <input type="text" id="dmInput" class="dm-input" placeholder="메시지 입력..." data-placeholder-ko="메시지 입력..." data-placeholder-en="Type a message..." maxlength="500">
3324        <button type="button" id="dmSendBtn" class="dm-send-btn" onclick="sendDMMessage()"><span id="txt-dm-send">전송</span></button>
3325      </div>
3326      <div id="dmEmojiPicker" class="emoji-picker" style="display:none;"></div>
3327    </div>
3328  </div>
3329
3330  <!-- Pet System HTML -->
3331  <div id="pet-container" class="pet-container" style="display:none;">
3332    <div id="pet-nameplate" class="pet-nameplate">
3333      <div id="pet-title-effect-container" class="pet-title-effect-container"></div>
3334      <div id="pet-bible-badge" class="pet-bible-badge" style="display:none;"></div>
3335      <div id="pet-title-display" class="pet-title-display" style="display:none;"></div>
3336      <div id="pet-name-display" class="pet-name-display"></div>
3337    </div>
3338    <div id="pet-character" class="pet-character" onclick="togglePetMenu()"></div>
3339    <div id="pet-menu" class="pet-menu">
3340      <div class="pet-menu-header">
3341        <div class="pet-menu-title">
3342          <span class="pet-menu-lv">Lv.<span id="pet-level">1</span></span>
3343          <span id="pet-name" class="pet-menu-name"></span>
3344          <span id="pet-settings-btn" class="pet-settings-icon" onclick="event.stopPropagation(); if(window.myPet) window.myPet.openSettings();" title="펫 설정">⚙️</span>
3345        </div>
3346        <div class="pet-menu-coins" title="게임 코인 (하루 9개 리필)">
3347          <span class="pet-coins-icon">&#x1FA99;</span>
3348          <span id="pet-coins">9</span>
3349        </div>
3350        <div class="pet-menu-likes" title="받은 좋아요 수">
3351          <span class="pet-likes-icon">&#10084;</span>
3352          <span id="pet-likes">0</span>
3353        </div>
3354      </div>
3355      <div class="pet-stats">
3356        <div class="stat-row">
3357          <span class="stat-label">EXP</span>
3358          <div class="stat-bar-mini">
3359            <div id="bar-exp" class="stat-fill exp" style="width:0%"></div>
3360          </div>
3361        </div>
3362        <div class="stat-row">
3363          <span class="stat-label" id="txt-pet-hunger">배고픔</span>
3364          <div class="stat-bar-mini">
3365            <div id="bar-hunger" class="stat-fill hunger" style="width:100%"></div>
3366          </div>
3367        </div>
3368        <div class="stat-row">
3369          <span class="stat-label" id="txt-pet-fun">즐거움</span>
3370          <div class="stat-bar-mini">
3371            <div id="bar-fun" class="stat-fill fun" style="width:100%"></div>
3372          </div>
3373        </div>
3374        <div class="stat-row">
3375          <span class="stat-label" id="txt-pet-wisdom">지혜</span>
3376          <div class="stat-bar-mini">
3377            <div id="bar-wisdom" class="stat-fill wisdom" style="width:0%"></div>
3378          </div>
3379        </div>
3380      </div>
3381      <div id="pet-wisdom-evolution-btn" style="display:none; text-align:center; margin:5px 0;">
3382        <div class="pixel-btn-mini" style="background:#FFD700; color:#5D4037; font-weight:bold; animation:wisdomGlow 1.5s infinite;" onclick="petAction('wisdomEvolve')" id="txt-pet-wisdom-evolve">
3383          ✨ 특별 진화 가능! ✨
3384        </div>
3385      </div>
3386      <div class="pet-menu-divider"></div>
3387      <div class="pet-actions">
3388        <div class="pixel-btn-mini pet-btn-feed" onclick="petAction('feed')"><span id="txt-pet-btn-feed">🍖 먹이</span></div>
3389        <div class="pixel-btn-mini pet-btn-play" onclick="petAction('play')"><span id="txt-pet-btn-play">🥎 놀이</span></div>
3390        <div class="pixel-btn-mini pet-btn-trait" onclick="if(typeof showPetTraitModal==='function') showPetTraitModal();"><span id="txt-pet-btn-trait">🎭 특성</span></div>
3391        <div class="pixel-btn-mini pet-btn-achieve" onclick="if(window.achievementSystem) achievementSystem.showList();"><span id="txt-pet-btn-achieve">🏆 업적</span></div>
3392      </div>
3393    </div>
3394  </div>
3395
3396  <div id="weather-overlay"></div>
3397
3398  <!-- ============================================================
3399       스크롤 애니메이션 스크립트
3400       ============================================================ -->
3401  
3401<script>
3402  // ========================
3403  // 스크롤 애니메이션 (Intersection Observer)
3404  // ========================
3405  (function() {
3406    function initScrollAnimations() {
3407      if (!('IntersectionObserver' in window)) {
3408        // IntersectionObserver 미지원 시 전부 보이게
3409        document.querySelectorAll('.scroll-animate, .scroll-animate-left, .scroll-animate-right, .scroll-animate-scale').forEach(function(el) {
3410          el.classList.add('visible');
3411        });
3412        return;
3413      }
3414
3415      var observer = new IntersectionObserver(function(entries) {
3416        entries.forEach(function(entry) {
3417          if (entry.isIntersecting) {
3418            entry.target.classList.add('visible');
3419            observer.unobserve(entry.target);
3420          }
3421        });
3422      }, {
3423        threshold: 0.08,
3424        rootMargin: '0px 0px -40px 0px'
3425      });
3426
3427      // 정적 요소에 애니메이션 클래스 추가
3428      var staticSelectors = [
3429        '#mainNoticeWrapper .main-notice-box',
3430        '#minihp-container',
3431        '#homeFeedWrapper .home-feed-box',
3432        '#chatWrapper .home-feed-box',
3433        '#calendar-container'
3434      ];
3435
3436      staticSelectors.forEach(function(sel) {
3437        var el = document.querySelector(sel);
3438        if (el) {
3439          el.classList.add('scroll-animate');
3440          observer.observe(el);
3441        }
3442      });
3443
3444      // 메뉴 버튼에 순차 애니메이션
3445      document.querySelectorAll('.menu-grid .pixel-btn').forEach(function(btn) {
3446        btn.classList.add('scroll-animate');
3447        observer.observe(btn);
3448      });
3449
3450      // 동적으로 추가되는 피드 아이템 감시
3451      var feedMO = new MutationObserver(function(mutations) {
3452        mutations.forEach(function(m) {
3453          m.addedNodes.forEach(function(node) {
3454            if (node.nodeType === 1) {
3455              // 홈피드 아이템
3456              if (node.classList && node.classList.contains('home-feed-item')) {
3457                node.classList.add('scroll-animate');
3458                observer.observe(node);
3459              }
3460              // 내부의 피드 아이템도 감시
3461              var feedItems = node.querySelectorAll ? node.querySelectorAll('.home-feed-item') : [];
3462              feedItems.forEach(function(item) {
3463                item.classList.add('scroll-animate');
3464                observer.observe(item);
3465              });
3466            }
3467          });
3468        });
3469      });
3470
3471      var feedList = document.getElementById('homeFeedList');
3472      if (feedList) {
3473        feedMO.observe(feedList, { childList: true, subtree: true });
3474      }
3475    }
3476
3477    if (document.readyState === 'loading') {
3478      document.addEventListener('DOMContentLoaded', function() {
3479        // 약간의 딜레이 후 실행 (다른 스크립트 로딩 후)
3480        setTimeout(initScrollAnimations, 300);
3481      });
3482    } else {
3483      setTimeout(initScrollAnimations, 300);
3484    }
3485  })();
3486  </script>
3486
3487
3488  <!-- Service Worker & PWA Install -->
3489  
3489<script>
3490  // Service Worker 등록
3491  if ('serviceWorker' in navigator) {
3492    window.addEventListener('load', function() {
3493      navigator.serviceWorker.register('./service-worker.js')
3494        .then(function(reg) {
3495          console.log('[PWA] Service Worker registered:', reg.scope);
3496          // FCM 토큰 요청을 위해 SW 등록 객체 저장
3497          window._swRegistration = reg;
3498        })
3499        .catch(function(err) {
3500          console.warn('[PWA] SW registration failed:', err);
3501        });
3502    });
3503  }
3504
3505  // PWA 설치 프롬프트 관리
3506  var _deferredInstallPrompt = null;
3507  window.addEventListener('beforeinstallprompt', function(e) {
3508    e.preventDefault();
3509    _deferredInstallPrompt = e;
3510    // 설치 버튼 표시
3511    var installBtn = document.getElementById('pwa-install-btn');
3512    if (installBtn) installBtn.style.display = 'inline-block';
3513  });
3514
3515  var _pwaInstalling = false;
3516
3517  function promptPWAInstall() {
3518    if (!_deferredInstallPrompt) {
3519      if (typeof Swal !== 'undefined') {
3520        var _cl17 = localStorage.getItem('selectedLang') || 'ko';
3521        Swal.fire({
3522          icon: 'info',
3523          title: _cl17 === 'en' ? 'Install App' : '앱 설치',
3524          html: _cl17 === 'en' ?
3525            '<div style="text-align:left; font-size:14px; line-height:1.8;">' +
3526            '<b>Already installed or you can install directly from your browser:</b><br><br>' +
3527            '<b>Android Chrome:</b><br>Menu(⋮) → "Add to Home screen"<br><br>' +
3528            '<b>iPhone Safari:</b><br>Share(↑) → "Add to Home Screen"<br><br>' +
3529            '<b>PC Chrome:</b><br>Click install(⊕) icon on address bar</div>' :
3530            '<div style="text-align:left; font-size:14px; line-height:1.8;">' +
3531            '<b>이미 설치되었거나 브라우저에서 직접 설치할 수 있습니다:</b><br><br>' +
3532            '<b>Android Chrome:</b><br>메뉴(⋮) → "홈 화면에 추가"<br><br>' +
3533            '<b>iPhone Safari:</b><br>공유(↑) → "홈 화면에 추가"<br><br>' +
3534            '<b>PC Chrome:</b><br>주소창 오른쪽 설치(⊕) 아이콘 클릭</div>',
3535          confirmButtonText: _cl17 === 'en' ? 'OK' : '확인',
3536          customClass: { popup: 'retro-swal' }
3537        });
3538      }
3539      return;
3540    }
3541    _deferredInstallPrompt.prompt();
3542    _deferredInstallPrompt.userChoice.then(function(result) {
3543      if (result.outcome === 'accepted') {
3544        console.log('[PWA] User accepted install');
3545        _pwaInstalling = true;
3546        showPWAProgress();
3547      } else {
3548        console.log('[PWA] User dismissed install');
3549        _deferredInstallPrompt = null;
3550      }
3551    });
3552  }
3553
3554  // PWA 설치 진행 오버레이
3555  function showPWAProgress() {
3556    // 이미 표시 중이면 무시
3557    if (document.getElementById('pwa-progress-overlay')) return;
3558
3559    var overlay = document.createElement('div');
3560    overlay.id = 'pwa-progress-overlay';
3561    overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:99999;display:flex;align-items:center;justify-content:center;';
3562    overlay.innerHTML =
3563      '<div style="background:#fff8e1;border:4px solid #5d4037;padding:30px 40px;text-align:center;max-width:340px;width:90%;box-shadow:8px 8px 0 rgba(0,0,0,0.4);font-family:DungGeunMo,monospace;">' +
3564        '<div style="font-size:40px;margin-bottom:12px;">📲</div>' +
3565        '<div style="font-size:18px;font-weight:bold;color:#3e2723;margin-bottom:16px;">' + ((localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Installing app...' : '앱 설치 중...') + '</div>' +
3566        '<div style="background:#e0e0e0;border:3px solid #5d4037;height:28px;overflow:hidden;margin-bottom:12px;">' +
3567          '<div id="pwa-progress-bar" style="height:100%;width:0%;background:linear-gradient(90deg,#ff9800,#ff5722);transition:width 0.4s ease;box-shadow:inset 0 -2px 0 rgba(0,0,0,0.2);"></div>' +
3568        '</div>' +
3569        '<div id="pwa-progress-text" style="font-size:13px;color:#5d4037;">' + ((localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Preparing download...' : '리소스 다운로드 준비 중...') + '</div>' +
3570        '<div style="font-size:11px;color:#8d6e63;margin-top:10px;">' + ((localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Please wait! Almost done.' : '잠시만 기다려주ì
3570„¸ìš”! ê³§ 완료됩니다.') + '</div>' +
3571      '</div>';
3572    document.body.appendChild(overlay);
3573
3574    // 진행 시뮬레이션 (실제 SW 캐시 진행과 병행)
3575    var bar = document.getElementById('pwa-progress-bar');
3576    var text = document.getElementById('pwa-progress-text');
3577    var steps = [
3578      { pct: 15, msg: (localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Registering service worker...' : '서비스 워커 등록 중...' },
3579      { pct: 30, msg: (localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Caching core files...' : '핵심 파일 캐싱 중...' },
3580      { pct: 50, msg: (localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Downloading styles & scripts...' : '스타일 & 스크립트 다운로드...' },
3581      { pct: 65, msg: (localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Downloading icons & fonts...' : '아이콘 & 폰트 다운로드...' },
3582      { pct: 80, msg: (localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Almost done! Finishing up...' : '거의 완료! 마무리 중...' },
3583      { pct: 92, msg: (localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Registering app...' : '앱 등록 중...' }
3584    ];
3585
3586    var stepIdx = 0;
3587    var progressTimer = setInterval(function() {
3588      if (stepIdx < steps.length) {
3589        bar.style.width = steps[stepIdx].pct + '%';
3590        text.textContent = steps[stepIdx].msg;
3591        stepIdx++;
3592      } else {
3593        // appinstalled 이벤트를 기다림 (최대 30초)
3594        clearInterval(progressTimer);
3595        // 30초 후에도 완료 안되면 자동 닫기
3596        setTimeout(function() {
3597          if (_pwaInstalling) completePWAProgress();
3598        }, 30000);
3599      }
3600    }, 1500);
3601
3602    // 타이머 ID 저장
3603    overlay._progressTimer = progressTimer;
3604  }
3605
3606  function completePWAProgress() {
3607    _pwaInstalling = false;
3608    var bar = document.getElementById('pwa-progress-bar');
3609    var text = document.getElementById('pwa-progress-text');
3610    var overlay = document.getElementById('pwa-progress-overlay');
3611
3612    if (bar) bar.style.width = '100%';
3613    if (text) text.textContent = (localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Installation complete!' : '설치 완료!';
3614
3615    setTimeout(function() {
3616      if (overlay) {
3617        overlay.innerHTML =
3618          '<div style="background:#fff8e1;border:4px solid #5d4037;padding:30px 40px;text-align:center;max-width:340px;width:90%;box-shadow:8px 8px 0 rgba(0,0,0,0.4);font-family:DungGeunMo,monospace;">' +
3619            '<div style="font-size:48px;margin-bottom:12px;">🎉</div>' +
3620            '<div style="font-size:20px;font-weight:bold;color:#3e2723;margin-bottom:10px;">' + ((localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'Installation complete!' : '설치 완료!') + '</div>' +
3621            '<div style="font-size:13px;color:#5d4037;margin-bottom:16px;">' + ((localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'You can launch the app from your home screen.' : '홈 화면에서 앱을 실행할 수 있습니다.') + '</div>' +
3622            '<button onclick="this.closest(\'#pwa-progress-overlay\').remove()" ' +
3623              'style="background:#ff9800;color:#fff;border:3px solid #000;padding:8px 24px;font-family:DungGeunMo,monospace;font-size:14px;cursor:pointer;box-shadow:3px 3px 0 rgba(0,0,0,0.3);">
3623' + ((localStorage.getItem('selectedLang') || 'ko') === 'en' ? 'OK' : '확인') + '</button>' +
3624          '</div>';
3625      }
3626    }, 800);
3627
3628    _deferredInstallPrompt = null;
3629    var installBtn = document.getElementById('pwa-install-btn');
3630    if (installBtn) installBtn.style.display = 'none';
3631  }
3632
3633  window.addEventListener('appinstalled', function() {
3634    console.log('[PWA] App installed successfully');
3635    completePWAProgress();
3636  });
3637  </script>
3637
3638
3639<!-- ====== Phase 5: 일일 미션 보드 모달 ====== -->
3640<div id="missionModal" class="modal">
3641  <div class="modal-content retro-box" style="max-width:450px;">
3642    <span class="close" onclick="closeModal('missionModal')">X</span>
3643    <h2 id="txt-mission-title">📋 오늘의 미션</h2>
3644    <div class="mission-board">
3645      <div id="missionList"></div>
3646      <div class="mission-progress-bar">
3647        <span id="missionProgressText" style="font-size:13px;">0/5 <span id="txt-mission-complete">완료</span></span>
3648        <div class="mission-progress-track">
3649          <div class="mission-progress-fill" id="missionProgressFill" style="width:0%"></div>
3650        </div>
3651      </div>
3652    </div>
3653  </div>
3654</div>
3655
3656<!-- ====== Phase 6: 실시간 기도실 모달 ====== -->
3657<div id="prayerRoomModal" class="modal">
3658  <div class="modal-content retro-box" style="max-width:500px;">
3659    <span class="close" onclick="closePrayerRoom()">X</span>
3660    <h2 id="txt-prayerroom-title">🕯️ 기도실</h2>
3661    <div class="prayer-room-container">
3662      <div class="prayer-room-status" id="prayerRoomStatus"><span id="txt-prayerroom-status">🕯️ 지금 0명이 함께 기도하고 있습니다</span></div>
3663      <div class="prayer-room-candles" id="prayerRoomCandles"></div>
3664      <div class="prayer-room-requests" id="prayerRoomRequests"></div>
3665      <div class="prayer-room-input">
3666        <input type="text" id="prayerRoomInput" placeholder="기도 제목을 나눠주세요..." data-placeholder-ko="기도 제목을 나눠주세요..." data-placeholder-en="Share your prayer request..." maxlength="100">
3667        <button class="pixel-btn" onclick="submitPrayerRoomRequest()" style="background:var(--primary-color);">🙏</button>
3668      </div>
3669      <button class="emergency-prayer-btn" onclick="sendEmergencyPrayer()"><span id="txt-emergency-prayer">🚨 긴급 기도 요청</span></button>
3670    </div>
3671  </div>
3672</div>
3673
3674<!-- ====== Phase 7: 펫 특성 모달 ====== -->
3675<div id="petTraitModal" class="modal">
3676  <div class="modal-content retro-box" style="max-width:420px;">
3677    <span class="close" onclick="closeModal('petTraitModal')">X</span>
3678    <h2 id="txt-pet-trait-title">🎭 펫 특성</h2>
3679    <div style="text-align:center; padding:16px;">
3680      <div id="petTraitIcon" style="font-size:64px;"></div>
3681      <div id="petTraitName" style="font-size:20px; font-weight:bold; margin:8px 0;"></div>
3682      <div id="petTraitDesc" style="font-size:13px; color:var(--secondary-color); margin-bottom:16px;"></div>
3683      <div id="petTraitBonus" style="font-size:14px; color:var(--primary-color); font-weight:bold;"></div>
3684    </div>
3685    <h3 style="font-size:15px;" id="txt-pet-memory-album">📸 추억 앨범</h3>
3686    <div class="pet-memory-album" id="petMemoryAlbum"></div>
3687  </div>
3688</div>
3689
3690<!-- ====== Phase 7: 도전장 모달 ====== -->
3691<div id="challengeModal" class="modal">
3692  <div class="modal-content retro-box" style="max-width:450px;">
3693    <span class="close" onclick="closeModal('challengeModal')">X</span>
3694    <h2 id="txt-challenge-title">⚔️ 도전장</h2>
3695    <div id="challengeList" style="padding:12px;"></div>
3696    <div style="text-align:center; padding:12px; font-size:13px; color:var(--secondary-color);" id="challengeEmpty">
3697      <span id="txt-challenge-empty">아직 도전장이 없습니다. 게임에서 좋은 점수를 내고 도전장을 올려보세요!</span>
3698    </div>
3699  </div>
3700</div>
3701
3702<!-- 주간 리캡 모달 -->
3703<div id="weeklyRecapModal" class="modal">
3704  <div class="modal-content retro-box" style="max-width:400px;">
3705    <span class="close" onclick="closeModal('weeklyRecapModal')">X</span>
3706    <h2 id="txt-recap-title">📊 주간 리캡</h2>
3707    <div class="weekly-recap" id="weeklyRecapContent">
3708      <div class="weekly-recap-title" id="txt-recap-subtitle">🔥 이번 주 활동 요약</div>
3709      <div class="weekly-recap-stats">
3710        <div class="weekly-recap-stat">
3711          <div class="weekly-recap-value" id="recapQT">0</div>
3712          <div class="weekly-recap-label" id="txt-recap-qt">QT 나눔</div>
3713        </div>
3714        <div class="weekly-recap-stat">
3715          <div class="weekly-recap-value" id="recapPrayer">0</div>
3716          <div class="weekly-recap-label" id="txt-recap-prayer">기도 참여</div>
3717        </div>
3718        <div class="weekly-recap-stat">
3719          <div class="weekly-recap-value" id="recapPoints">0</div>
3720          <div class="weekly-recap-label" id="txt-recap-points">획득 포인트</div>
3721        </div>
3722        <div class="weekly-recap-stat">
3723          <div class="weekly-recap-value" id="recapGames">0</div>
3724          <div class="weekly-recap-label" id="txt-recap-games">게임 플레이</div>
3725        </div>
3726      </div>
3727      <div style="font-size:12px; opacity:0.6; margin-top:12px;">⛪ HOLY SPARK</div>
3728    </div>
3729    <div style="text-align:center; margin-top:12px;">
3730      <button class="pixel-btn" onclick="shareWeeklyRecap()" style="background:var(--green-color);"><span id="txt-recap-share">📤 공유하기</span></button>
3731    </div>
3732  </div>
3733</div>
3734
3735<!-- 업적 모달 -->
3736<div id="achievementListModal" class="modal">
3737  <div class="modal-content retro-box" style="max-width:450px;">
3738    <span class="close" onclick="closeModal('achievementListModal')">X</span>
3739    <h2 id="txt-achievement-title">🏆 업적</h2>
3740    <div id="achievementList" style="padding:12px;"></div>
3741  </div>
3742</div>
3743
3744<!-- 오프라인 인디케이터 -->
3745<div class="offline-indicator" id="offlineIndicator"><span id="txt-offline">📡 오프라인 모드</span></div>
3746
3747</body>
3748
3749</html>

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.