PageSourceSearch

https://sinsinmnc.com/theme/basic/sinsin_utils.js?v=1789963769

js sinsinmnc.com collected 2026-09-24 14:18:21 UTC 10,278 bytes, 265 lines download raw bytes

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.