PageSourceSearch

https://yeestudio.co.kr/assets/renewal/site.js?v=20260907-r1

js yeestudio.co.kr collected 2026-10-02 01:04:27 UTC 8,752 bytes, 176 lines download raw bytes

1/* Shared enhancement. Core content and links remain available without JavaScript. */
2(() => {
3  'use strict';
4  document.documentElement.classList.add('js');
5  const supported = new Set(['ko', 'en']);
6  const storage = {
7    get() { try { return localStorage.getItem('updatesLanguagePreference'); } catch { return null; } },
8    set(value) {
9      try {
10        localStorage.setItem('updatesLanguagePreference', value);
11        localStorage.setItem('language', value);
12      } catch { /* Storage is optional. */ }
13    }
14  };
15  const requested = new URLSearchParams(location.search).get('lang');
16  let deviceLanguage = '';
17  try {
18    deviceLanguage = navigator.languages?.[0] || navigator.language || navigator.userLanguage || '';
19  } catch { /* Device language is optional. */ }
20  const detected = /^ko(?:-|$)/i.test(String(deviceLanguage)) ? 'ko' : 'en';
21  let language = supported.has(requested) ? requested : (supported.has(storage.get()) ? storage.get() : detected);
22  let toastTimer;
23  const labels = (ko, en) => language === 'en' ? en : ko;
24
25  function notify(ko, en, duration = 4000) {
26    const node = document.getElementById('toast');
27    if (!node) return;
28    clearTimeout(toastTimer);
29    node.textContent = labels(ko, en);
30    node.hidden = false;
31    toastTimer = setTimeout(() => { node.hidden = true; }, duration);
32  }
33
34  // Only trusted, supplied privacy strings use rich text. URLs and tags are restricted.
35  function safePolicyFragment(value) {
36    const template = document.createElement('template');
37    template.innerHTML = String(value);
38    const allowed = new Set(['BR', 'A', 'STRONG', 'EM', 'SPAN']);
39    [...template.content.querySelectorAll('*')].forEach(node => {
40      if (!allowed.has(node.tagName)) { node.replaceWith(...node.childNodes); return; }
41      const href = node.getAttribute('href');
42      [...node.attributes].forEach(attr => node.removeAttribute(attr.name));
43      if (node.tagName === 'A' && href) {
44        try {
45          const url = new URL(href);
46          if (url.protocol === 'https:' || url.protocol === 'mailto:') {
47            node.setAttribute('href', url.href);
48            node.className = 'policy-link';
49            if (url.protocol === 'https:') {
50              node.target = '_blank'; node.rel = 'noopener noreferrer';
51            }
52          }
53        } catch { /* Leave malformed links as inert text. */ }
54      }
55    });
56    return template.content;
57  }
58
59  function applyLanguage(next, updateUrl = false, persistChoice = false) {
60    language = supported.has(next) ? next : 'en';
61    document.documentElement.lang = language;
62    if (persistChoice) storage.set(language);
63    document.querySelectorAll('[data-ko][data-en]').forEach(el => {
64      el.textContent = el.getAttribute(`data-${language}`);
65    });
66    document.querySelectorAll('[data-ko-placeholder]').forEach(el => {
67      el.setAttribute('placeholder', el.getAttribute(`data-${language}-placeholder`) || '');
68    });
69    document.querySelectorAll('[data-ko-label]').forEach(el => {
70      el.setAttribute('aria-label', el.getAttribute(`data-${language}-label`) || '');
71    });
72    if (window.YEE_POLICY) {
73      document.querySelectorAll('[data-policy-key]').forEach(el => {
74        const value = window.YEE_POLICY[language]?.[el.dataset.policyKey];
75        if (typeof value === 'string') el.replaceChildren(safePolicyFragment(value));
76      });
77    }
78    document.querySelectorAll('[data-language-toggle]').forEach(button => {
79      button.setAttribute('aria-label', labels('Switch to English', '한국어로 전환'));
80      button.querySelector('[data-language-label]').textContent = labels('EN', 'KO');
81    });
82    document.querySelectorAll('a[data-local-link]').forEach(link => {
83      try {
84        const url = new URL(link.getAttribute('href'), location.href);
85        if (url.origin === location.origin) {
86          url.searchParams.set('lang', language);
87          link.setAttribute('href', url.pathname + url.search + url.hash);
88        }
89      } catch { /* Relative navigation is still usable. */ }
90    });
91    const page = document.body.dataset.page;
92    const titles = {
93      home: labels('YeeStudio — 작은 아이디어, 더 편리한 일상', 'YeeStudio — Small ideas. Better everyday.'),
94      privacy: labels('YeeStudio — 개인정보처리방침', 'YeeStudio — Privacy Policy'),
95      error: `${document.body.dataset.error || '404'} — YeeStudio`
96    };
97    document.title = titles[page] || 'YeeStudio';
98    const description = page === 'privacy'
99      ? labels('YeeStudio 앱과 웹 서비스의 개인정보 처리방침을 확인하세요.', 'Read the privacy policy for YeeStudio apps and web services.')
100      : page === 'home'
101        ? labels('일상의 작은 불편함을 해결하는 YeeStudio의 앱과 웹 도구를 만나보세요.', 'Thoughtful apps and web tools that make everyday life a little easier.')
102        : labels('요청한 페이지를 열 수 없습니다. 홈으로 돌아가거나 문의해 주세요.', 'This page is unavailable. Head home or contact us for help.');
103    document.querySelector('meta[name="description"]')?.setAttribute('content', description);
104    document.querySelector('meta[property="og:description"]')?.setAttribute('content', description);
105    document.querySelector('meta[property="og:title"]')?.setAttribute('content', document.title);
106    if (updateUrl) {
107      try {
108        const url = new URL(location.href); url.searchParams.set('lang', language);
109        history.replaceState(null, '', url.pathname + url.search + url.hash);
110      } catch { /* Local file previews may restrict history changes. */ }
111    }
112    document.dispatchEvent(new CustomEvent('yee:languagechange', { detail: { language } }));
113  }
114
115  document.querySelectorAll('[data-language-toggle]').forEach(button => {
116    button.addEventListener('click', () => applyLanguage(language === 'ko' ? 'en' : 'ko', true, true));
117  });
118  const menuButton = document.querySelector('.menu-toggle');
119  const menu = document.getElementById('mobile-menu');
120  function closeMenu(returnFocus = false) {
121    if (!menu || !menuButton) return;
122    menu.hidden = true; menuButton.setAttribute('aria-expanded', 'false');
123    if (returnFocus) menuButton.focus();
124  }
125  menuButton?.addEventListener('click', () => {
126    const opening = menu.hidden;
127    menu.hidden = !opening;
128    menuButton.setAttribute('aria-expanded', String(opening));
129  });
130  menu?.querySelectorAll('a').forEach(link => link.addEventListener('click', () => closeMenu()));
131  document.addEventListener('keydown', event => {
132    if (event.key === 'Escape' && menu && !menu.hidden) closeMenu(true);
133  });
134  document.addEventListener('click', event => {
135    if (menu && !menu.hidden && !event.target.closest('.site-header')) closeMenu();
136  });
137  const desktop = matchMedia('(min-width: 901px)');
138  desktop.addEventListener?.('change', event => { if (event.matches) closeMenu(); });
139  const header = document.querySelector('.site-header');
140  const top = document.querySelector('.back-to-top');
141  let scrollFrame;
142  function updateScroll() {
143    scrollFrame = null;
144    header?.classList.toggle('is-scrolled', scrollY > 12);
145    top?.classList.toggle('is-visible', scrollY > 650);
146  }
147  window.addEventListener('scroll', () => {
148    if (!scrollFrame) scrollFrame = requestAnimationFrame(updateScroll);
149  }, { passive: true });
150  updateScroll();
151
152  document.querySelectorAll('[data-copy]').forEach(button => {
153    button.addEventListener('click', async () => {
154      const value = button.dataset.copy || '';
155      try {
156        if (!navigator.clipboard?.writeText) throw new Error('Clipboard unavailable');
157        await navigator.clipboard.writeText(value);
158        notify('이메일 주소를 복사했어요.', 'Email address copied.');
159      } catch {
160        // Do not pretend success or read the clipboard. Offer selectable source text instead.
161        const address = document.querySelector('.contact-email');
162        if (address) {
163          const range = document.createRange(); range.selectNodeContents(address);
164          const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range);
165        }
166        notify('자동 복사가 제한되어 있어요. 표시된 이메일 주소를 직접 복사해 주세요.', 'Automatic copying is unavailable. Please copy the displayed email address.', 6500);
167      }
168    });
169  });
170  document.querySelectorAll('[data-print]').forEach(button => button.addEventListener('click', () => window.print()));
171  window.YeeRenewal = Object.freeze({
172    getLanguage: () => language, t: labels, notify, applyLanguage,
173    motion: () => matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth'
174  });
175  applyLanguage(language, false, false);
176})();

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.