1// 26.08.07 í´ë¹ js ê°ì 2// ë¤ë¥¸ê±´ íì´ì§ê¹ë´ 무ììì ì 못ëê² ì;;; 3 4const SSUtils = { 5 // ììì ìë¨ ì ë Y ìì¹ êµ¬í기 6 getAbsTop(targetEl) { 7 if (!targetEl) return 0; 8 const scrollTop = window.scrollY || document.documentElement.scrollTop; 9 return scrollTop + targetEl.getBoundingClientRect().top; 10 }, 11 // ììì íë¨ ì ë Y ìì¹ êµ¬í기 12 getAbsBottom(targetEl) { 13 if (!targetEl) return 0; 14 const scrollTop = window.scrollY || document.documentElement.scrollTop; 15 return scrollTop + targetEl.getBoundingClientRect().bottom; 16 }, 17 // í´ëí° ë²í¸ ê²ì¦ (ëì ìì) 18 verifyPhoneDash(phone) { 19 // const regex = /^01[016789]-\d{3,4}-\d{4}$/; 20 const regex = /^010-\d{4}-\d{4}$/; 21 return regex.test(phone); 22 }, 23 24 // í´ëí° ìë - ìì± (ìë íì´í íì© ë° ì¤ìê° íì´í ëì) 25 initPhoneDash(phone) { 26 let cleaned = phone.replace(/\D/g, ""); 27 if (cleaned.length === 0) return ""; 28 // 1ë²ì§¸ ì리ë 무조건 '0' ì´ì´ì¼ í¨ (ìë ê²½ì° ë¹ê°) 29 if (cleaned[0] !== "0") return ""; 30 // 2ë²ì§¸ ì리ë 무조건 '1' ì´ì´ì¼ í¨ (ìë ê²½ì° '0'ë§ ì ì§) 31 if (cleaned.length >= 2 && cleaned[1] !== "1") { 32 cleaned = "0"; 33 } 34 // 3ë²ì§¸ ì리ë 무조건 '0' ì´ì´ì¼ í¨ (ìë ê²½ì° '01'ë§ ì ì§) 35 if (cleaned.length >= 3 && cleaned[2] !== "0") { 36 cleaned = "01"; 37 } 38 // ìµë 11ì리(010 + 8ì리)ê¹ì§ë§ ìë¼ë 39 cleaned = cleaned.slice(0, 11); 40 const hasTrailingDash = phone.endsWith("-"); 41 // ì릿ìë³ íì´í í¬ë§·í 42 if (cleaned.length < 3) return cleaned; 43 if (cleaned.length === 3) { 44 return hasTrailingDash ? "010-" : "010"; 45 } 46 if (cleaned.length < 7) { 47 return `${cleaned.slice(0, 3)}-${cleaned.slice(3)}`; 48 } 49 if (cleaned.length === 7) { 50 return hasTrailingDash 51 ? `${cleaned.slice(0, 3)}-${cleaned.slice(3)}-` 52 : `${cleaned.slice(0, 3)}-${cleaned.slice(3)}`; 53 } 54 return `${cleaned.slice(0, 3)}-${cleaned.slice(3, 7)}-${cleaned.slice(7, 11)}`; 55 }, 56 57 // triggerììë elementToHide 를 ì¨ê¸°ë ë°©ìì ì´ì© 58 // ì¬ì©ë°©ë² 59 // SSUtils.hideOnIntersect(".some-section-class, .any-section-class", "#the-cta-id"); 60 // SSUtils.hideOnIntersect(".new-section-class", ".the-cta-class"); 61 // SSUtils.hideOnIntersect([El1, El2], ".the-cta-class"); 62 63 hideOnIntersect(trigger, elementToHide, threshold = 0.1) { 64 const targets = 65 typeof trigger === "string" 66 ? Array.from(document.querySelectorAll(trigger)) 67 : (Array.isArray(trigger) ? trigger : [trigger]); 68 const hideEl = 69 typeof elementToHide === "string" 70 ? document.querySelector(elementToHide) 71 : elementToHide; 72 if (!targets.length || !hideEl) return; 73 74 // íê² ììì ê°ë³ ê°ì ëª©ë¡ Set í ë¹ (ì¬ë¬ ë² ë°ë¡ í¸ì¶í´ë ì¶©ë ë°©ì§) 75 if (!hideEl._activeIntersectTriggers) { 76 hideEl._activeIntersectTriggers = new Set(); 77 } 78 79 const observer = new IntersectionObserver( 80 (entries) => { 81 entries.forEach((entry) => { 82 if (entry.isIntersecting) { 83 hideEl._activeIntersectTriggers.add(entry.target); 84 } else { 85 hideEl._activeIntersectTriggers.delete(entry.target); 86 } 87 }); 88 89 const shouldHide = hideEl._activeIntersectTriggers.size > 0; 90 hideEl.classList.toggle("ss-hidden", shouldHide); 91 hideEl.style.display = shouldHide ? "none" : ""; 92 }, 93 { threshold: threshold }, 94 ); 95 96 // ì´ê¸° ë기 ê²ì¬: ë¹ë기 ì½ë°± ë¶í ì¤íì¼ë¡ ì¸í ê¹ë¹¡ì(FOUC) ë°©ì§ 97 const viewportHeight = window.innerHeight || document.documentElement.clientHeight; 98 targets.forEach((target) => { 99 if (target) { 100 const rect = target.getBoundingClientRect(); 101 // íì¬ íë©´(viewport)ê³¼ ê²¹ì¹ëì§ ì¦ì íì¸íì¬ Setì ì¬ì ë±ë¡ 102 const isVisibleInitially = rect.top < viewportHeight && rect.bottom > 0; 103 if (isVisibleInitially) { 104 hideEl._activeIntersectTriggers.add(target); 105 } 106 observer.observe(target); 107 } 108 }); 109 110 // 첫 ë ëë§ ì ì ì¦ì ë기 ì ì©
111 const initialShouldHide = hideEl._activeIntersectTriggers.size > 0; 112 hideEl.classList.toggle("ss-hidden", initialShouldHide); 113 hideEl.style.display = initialShouldHide ? "none" : ""; 114 115 return observer; 116 }, 117 118 // 26.09.13 ì¶ê° // 260921 localStorage -> sessionStorage ë°©ìì¼ë¡¤ ë³ê²½ 119 // head.php ìì localStorage ì ì ì¥í´ë UTM ê°ì í¼ ì ì¡ì© ê°ì²´ë¡ ë°í 120 // ì¬ì©ë²) Object.assign(postData, SSUtils.getUTM()); 121 // ì¬í리 íë¼ì´ë¹ 모ë ë± localStorage ì ê·¼ì´ ë§íë íê²½ììë ë¹ ê°ì¼ë¡ ìì íê² ëì 122 // 26.09.14 ì¶ê° â ì í(tel:) í´ë¦ ìì§ 123 // head.php ì localStorage UTM + tail.php ê° ì£¼ì í SS_UTM_MAP ì í¨ê» ì¨ì 124 // ì¹í¼Â·ì±í ê³¼ ëì¼í ì ì ê²½ë¡ ê°ì ë§ë¤ì´ ìë²ì 기ë¡íë¤. 125 // 126 // ëì 127 // 1) tel: ë§í¬ í´ë¦ ì¦ì sendBeacon ì¼ë¡ 1ì°¨ ê¸°ë¡ (íì´ì§ë¥¼ ë ëë ì ì¡ ë³´ì¥) 128 // 2) 15ì´ ë¤ ë¤ì´ì¼ë¬ ì²´ë¥ ê²°ê³¼ë¥¼ 2ì°¨ë¡ ë³´ë (ë³´ì¡° ì ë³´. ì¤í¨í´ë 무방) 129 // 130 // e.preventDefault() ë ì°ì§ ìëë¤. 기ë¡ì´ ì¤í¨í´ë ì íë ì ìì ì¼ë¡ ê±¸ë ¤ì¼ íë¤. 131 PHONE_CLICK_ENDPOINT: "/log_phone_click.php", 132 PHONE_DIAL_WAIT_MS: 15000, 133 134 ssBeacon(payload) { 135 try { 136 const body = new Blob([JSON.stringify(payload)], { type: "application/json" }); 137 if (navigator.sendBeacon && navigator.sendBeacon(this.PHONE_CLICK_ENDPOINT, body)) return; 138 // sendBeacon 미ì§ì/ì¤í¨ ì keepalive fetch ë¡ í´ë°± 139 fetch(this.PHONE_CLICK_ENDPOINT, { 140 method: "POST", 141 headers: { "Content-Type": "application/json" }, 142 body: JSON.stringify(payload), 143 keepalive: true, 144 }).catch(() => {}); 145 } catch (error) { 146 console.log(`Phone Beacon Error : ${error.message}`); 147 } 148 }, 149 150 // UTM â ì ì ê²½ë¡ (kakao_chat_btn.js ì resolveInflowFromUTM ê³¼ ëì¼ ê·ì¹) 151 ssResolveInflow() { 152 const map = window.SS_UTM_MAP; 153 if (!map || !map.full) return ""; 154 155 const v = this.getUTM(); 156 const norm = (x) => String(x || "").toLowerCase().replace(/[^a-z0-9_.\-]/g, ""); 157 const s = norm(v.ss_utm_source), m = norm(v.ss_utm_medium), c = norm(v.ss_utm_campaign); 158 if (!s || !m) return ""; 159 160 if (map.full[`${s}|${m}|${c}`]) return map.full[`${s}|${m}|${c}`]; 161 if (map.sm && map.sm[`${s}|${m}`]) return map.sm[`${s}|${m}`]; 162 if (m === "organic") return "ííì´ì§ì ê·ìë´"; 163 return "기í"; 164 }, 165 166 initPhoneClickLog() { 167 if (window.__ssPhoneLogBound) return; // ì¤ë³µ ë°ì¸ë© ë°©ì§ 168 window.__ssPhoneLogBound = true; 169 170 const self = this; 171 172 document.addEventListener("click", function (e) { 173 const link = e.target.closest && e.target.closest('a[href^="tel:"]'); 174 if (!link) return; 175 176 const phone = (link.getAttribute("href") || "").replace(/^tel:/i, "").replace(/\D+/g, ""); 177 if (!phone) return; 178 179 // ê°ì ë²í¸ë¥¼ 30ì´ ìì ë¤ì ëë ì¼ë©´ ì¤í´ë¦ì¼ë¡ ë³´ê³ ê±´ëë´ë¤ 180 const dedupeKey = `ss_tel_${phone}`; 181 try { 182 const last = Number(sessionStorage.getItem(dedupeKey) || 0); 183 if (Date.now() - last < 30000) return; 184 sessionStorage.setItem(dedupeKey, String(Date.now())); 185 } catch (error) { /* sessionStorage ì°¨ë¨ íê²½ì ê·¸ë¥ ì§í */ } 186 187 const clickKey = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; 188 const utm = self.getUTM(); 189 190 // ââ 1ì°¨ : í´ë¦ ì¦ì ê¸°ë¡ 191 self.ssBeacon({ 192 action: "click", 193 click_key: clickKey, 194 phone: phone, 195 inflow: self.ssResolveInflow(), 196 ss_utm_source: utm.ss_utm_source, 197 ss_utm_medium: utm.ss_utm_medium, 198 ss_utm_campaign: utm.ss_utm_campaign, 199 ss_utm_content: utm.ss_utm_content, 200 ss_utm_term: utm.ss_utm_term, 201 ss_utm_link: utm.ss_utm_link, //260921 ì¶ê° 202 url: location.href, 203 referer: document.referrer || "", 204 is_mobile: /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent) ? 1 : 0, 205 }); 206 207 // ââ 2ì°¨ : ë¤ì´ì¼ë¬ë¡ ëì´ê°ëì§ 15ì´ê° ê´ì°° 208 const startedAt = Date.now(); 209 let wentHidden = false;
210 let hiddenAt = 0; 211 let visibleSec = 0; 212 213 const onVisibility = () => { 214 if (document.hidden) { 215 if (!wentHidden) { wentHidden = true; hiddenAt = Date.now(); } 216 } else if (wentHidden && !visibleSec) { 217 visibleSec = Math.round((Date.now() - hiddenAt) / 1000); 218 } 219 }; 220 document.addEventListener("visibilitychange", onVisibility); 221 222 setTimeout(() => { 223 document.removeEventListener("visibilitychange", onVisibility); 224 225 let state; 226 if (!wentHidden) { 227 state = "no_dialer"; // ë¤ì´ì¼ë¬ ìì²´ê° ì ë¸ (PC ì´ê±°ë ê·¸ë¥ ëë¬ë³¸ ê²) 228 } else if (document.hidden) { 229 state = "called"; // 15ì´ì§¸ ìì§ ì ëìì´ = íµí ê°ë¥ì± 230 } else { 231 state = "cancelled"; // ë´ë¤ê° ëìì´ = ì·¨ì ê°ë¥ì± 232 } 233 234 self.ssBeacon({ 235 action: "dial", 236 click_key: clickKey, 237 dial_state: state, 238 dial_sec: state === "called" 239 ? Math.round((Date.now() - startedAt) / 1000) 240 : visibleSec, 241 }); 242 }, self.PHONE_DIAL_WAIT_MS); 243 }, true); 244 }, 245 246 getUTM() { 247 // 260921 utm_link ì¶ê° 248 const keys = ["ss_utm_source", "ss_utm_medium", "ss_utm_campaign", "ss_utm_content", "ss_utm_term", "ss_utm_link"]; 249 const out = {}; 250 keys.forEach((k) => { out[k] = ""; }); 251 try { 252 keys.forEach((k) => { out[k] = sessionStorage.getItem(k) ?? ""; }); 253 } catch (error) { 254 console.log(`UTM Get Error : ${error.message}`); 255 } 256 return out; 257 }, 258}; 259 260// 26.09.14 ì¶ê° â ì í í´ë¦ ìì§ ìë ë°ì¸ë© 261if (document.readyState === "loading") { 262 document.addEventListener("DOMContentLoaded", () => SSUtils.initPhoneClickLog()); 263} else { 264 SSUtils.initPhoneClickLog(); 265}
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.