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">🪙</span> 3348 <span id="pet-coins">9</span> 3349 </div> 3350 <div class="pet-menu-likes" title="ë°ì ì¢ìì ì"> 3351 <span class="pet-likes-icon">❤</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.