1// âââââââââââââââââââââââââââââââââââââââââââââââ 2// ThinkingMap · ê³ ì URL ë·°ì´ 3// âââââââââââââââââââââââââââââââââââââââââââââââ 4 5const state = { 6 weeks: [], 7 currentWeek: null, 8 dataCache: new Map(), 9 dots: [], 10 curFilter: 'all', 11 isTouch: false, 12 activeDotIdx: -1, 13 hlDot: null 14}; 15 16// ââ ì¹´í ê³ ë¦¬ ì ì (ê´ë¦¬ì í´ê³¼ ëì¼) ââ 17const CATEGORIES = { 18 ai: { label:'AI·í í¬', emoji:'ð¤', color:'#8259FF', keywords:['ai','gpt','llm','ìì±í','ì±ë´','ìëí','ìì´ì í¸','머ì ë¬ë','ë¥ë¬ë','geo','aeo','rag','í롬íí¸','ì½íì¼ë¿','copilot','chatgpt','claude','gemini','openai','ìë¹ëì','nvidia'] }, 19 brand: { label:'ë¸ëëÂ·ìº íì¸', emoji:'â¨', color:'#fb923c', keywords:['ë¸ëë','ìº íì¸','리ë¸ëë©','ìì´ë´í°í°','ë¤ì´ë°','ì¤í 리í ë§','í¬ë¦¬ìì´í°ë¸ ê´ê³ ','imc','ì¬ë¡ê±´','ë¸ëë©','ë¸ëëê²½í','bx'] }, 20 performance: { label:'í¼í¬ë¨¼ì¤Â·ê´ê³ ', emoji:'ð', color:'#34d399', keywords:['í¼í¬ë¨¼ì¤','roas','cpa','cpc','ctr','cvr','ì íì¨','리íê²í ','ë©íê´ê³ ','ë©í ê´ê³ ','구ê¸ì ì¦','êµ¬ê¸ ì ì¦','google ads','meta ads','ë¤ì´ë²gfa','gfa','ì¹´ì¹´ì¤ëª¨ë¨¼í¸','ì¹´ì¹´ì¤ ëª¨ë¨¼í¸','ì´í¸ë¦¬ë·°ì ','ê´ê³ ë¹','미ëì´ë¯¹ì¤','bidding'] }, 21 channel: { label:'ì±ë·SNS', emoji:'ð±', color:'#f472b6', keywords:['ì¸ì¤íê·¸ë¨','ì¸ì¤í','í±í¡','tiktok','ì íë¸','youtube','sns','ìí¼','shorts','릴ì¤','reels','ì¤ë ë','threads','ë§í¬ëì¸','linkedin','íì´ì¤ë¶','facebook','twitter','í¸ìí°','ë¤ì´ë²','ì¹´ì¹´ì¤í¡','ì¹´í¡','ì±ë'] }, 22 content: { label:'ì½í ì¸ Â·í¬ë¦¬ìì´í°ë¸', emoji:'ð¨', color:'#a78bfa', keywords:['ì½í ì¸ ','ì¹´í¼','ì¹´í¼ë¼ì´í ','í¬ë¦¬ìì´í°ë¸','ëìì¸','ìì','ì¸ë¤ì¼','í¸ì§','ì ì','ì¤í 리보ë','ë¹ì£¼ì¼','íì´í¬','í¬ì¤í°','ì¼ë¬ì¤í¸'] }, 23 data: { label:'ë°ì´í°Â·ë¶ì', emoji:'ð', color:'#60a5fa', keywords:['ë°ì´í°','ë¶ì','ga4','êµ¬ê¸ ì ë리í±ì¤','analytics','ëìë³´ë','ì§í','kpi','íµê³','ì½í¸í¸','í¼ë','리í ì ','retention','cohort','funnel'] }, 24 career: { label:'ì 무·커리ì´', emoji:'ð¼', color:'#0891b2', keywords:['ì 무','ì 무í¨ì¨','ì 무ì¤í¬','ìì°ì±','productivity','íì¥','í','ì¬ì','주ëì´','ì ì ','ìëì´','리ë','리ëì','매ëì ','ìì¬','ì§ì¥ì¸','ì§ì¥','ì§ì','íì¬','ì¬ë´','ì¡°ì§ë¬¸í','ì¡°ì§','커리ì´','career','ì´ì§','ì°ë´','í´ì¬','ì ì¬','ìë¼ë°¸','ì¬í근무','ì¬í','근무','ì¶ê·¼','í´ê·¼','íì','미í ','ë³´ê³ ì','ë³´ê³ ','기íì','ì ìì','íì ','ìíµ','커뮤ëì¼ì´ì ','ì기ê³ë°','ë©´ì ','ì
24ìì','ì기ìê°ì'] }, 25 trend: { label:'í¸ë ë', emoji:'ð¥', color:'#f87171', keywords:['í¸ë ë','ì´ì','ìë','ìë¹','ì¸ë','mz','ìíì¸ë','ì´ê´','ë°ì´ë´','ì ë롬','ì í','2025','2026','ì ë ','í«í'] }, 26 other: { label:'기í', emoji:'ð', color:'#94a3b8', keywords:[] } 27}; 28const CAT_ORDER = ['ai','performance','data','channel','career','content','brand','trend']; 29const CAT_CODES = ['ai','brand','performance','channel','content','data','career','trend','other']; 30 31function classifyCategory(title) { 32 const t = (title || '').toLowerCase(); 33 for (const code of CAT_ORDER) { 34 for (const k of CATEGORIES[code].keywords) { 35 if (t.includes(k.toLowerCase())) return code; 36 } 37 } 38 return 'other'; 39} 40 41// ââ ì í¸ ââ 42function getQ(x, y) { 43 // x: í¸ë ë(-) â ì¤ë¬´(+), y: ë¼ì´í¸(+) â ë¥(-) 44 if (x < 0 && y >= 0) return 'q1'; // í¸ë ë+ë¼ì´í¸ 45 if (x >= 0 && y >= 0) return 'q2'; // ì¤ë¬´+ë¼ì´í¸ 46 if (x < 0 && y < 0) return 'q3'; // í¸ë ë+ë¥ 47 return 'q4'; // ì¤ë¬´+ë¥ 48} 49 50async function fetchJSON(url) { 51 const r = await fetch(url, { cache: 'no-cache' }); 52 if (!r.ok) throw new Error(`HTTP ${r.status}: ${url}`); 53 return r.json(); 54} 55 56// ââ ì´ê¸°í ââ 57async function init() { 58 try { 59 const index = await fetchJSON('data/index.json'); 60 state.weeks = index.weeks || []; 61 if (!state.weeks.length) { 62 showError('ë±ë¡ë 주차 ë°ì´í°ê° ììµëë¤.'); 63 return; 64 } 65 66 const urlParams = new URLSearchParams(window.location.search); 67 const weekParam = urlParams.get('week') || index.latest || state.weeks[0].date; 68 69 renderWeekSelector(weekParam); 70 await loadAndRenderWeek(weekParam); 71 attachEvents(); 72 } catch (e) { 73 console.error(e); 74 showError('ì´ê¸°í ì¤í¨: ' + e.message); 75 } 76} 77 78// ââ 주차 ëë¡ë¤ì´ ââ 79function renderWeekSelector(selected) { 80 const sel = document.getElementById('weekSelector'); 81 // ìµì ì´ ìë¡ ì¤ëë¡ ì ë ¬ 82 const sorted = [...state.weeks].sort((a, b) => b.date.localeCompare(a.date)); 83 sel.innerHTML = sorted.map(w => { 84 const isSel = w.date === selected ? 'selected' : ''; 85 const vol = w.volume || ''; 86 const title = w.title || w.date; 87 return `<option value="${w.date}" ${isSel}>${vol} · ${title} (${w.contentCount || 0}ê°)</option>`; 88 }).join(''); 89} 90 91// ââ 주차 ë°ì´í° ë¡ë + ë ë ââ 92async function loadAndRenderWeek(date) { 93 try { 94 showLoading(true); 95 let data = state.dataCache.get(date); 96 if (!data) { 97 data = await fetchJSON(`data/${date}.json`); 98 state.dataCache.set(date, data); 99 } 100 state.currentWeek = data; 101 renderAll(data); 102 updateURL(date); 103 showLoading(false); 104 } catch (e) { 105 console.error(e); 106 showError(`${date} ë°ì´í° ë¡ë© ì¤í¨: ${e.message}`); 107 } 108} 109 110// ââ ì ì²´ ë ë ââ 111function renderAll(data) { 112 // í¤ë 113 const meta = data.meta || {}; 114 const wlbl = meta.weekLabel || meta.week || ''; 115 document.getElementById('centerNumber').textContent = meta.totalContents || data.contents.length; 116 const clTitleEl = document.getElementById('clTitle'); 117 if (clTitleEl) clTitleEl.textContent = wlbl ? `${wlbl} ì½í ì¸ ëª©ë¡` : 'ì½í ì¸ ëª©ë¡'; 118 119 // ì´ë² 주ì í¹ì§ ì½ë©í¸ 120 const commentEl = document.getElementById('weeklyComment'); 121 if (meta.weeklyComment && meta.weeklyComment.trim()) { 122 commentEl.textContent = meta.weeklyComment.trim(); 123 commentEl.classList.remove('hidden'); 124 } else { 125 commentEl.textContent = ''; 126 commentEl.classList.add('hidden'); 127 } 128 129 // Dots ë³í (겹침 íí¼) 130 state.dots = computeDots(data.contents); 131 renderDots(); 132 renderTabs(); 133 renderContentList(); 134} 135 136// ââ Dots ì¢í ê³ì° (HTML downloaderì ëì¼ ë¡ì§) ââ 137function computeDots(contents) { 138 const gridMap = {}; 139 return contents.map((c, idx) => { 140 const q = c.q || getQ(c.x, c.y); 141 let rawX = 5 + ((c.x + 11) / 22) * 90; 142 let rawY = 5 + ((11 - (-c.y)) / 22) * 90; 143 const gk = Math.round(rawX / 3) + '_' + Math.round(rawY / 3); 144 if (!gridMap[gk]) gridMap[gk] = 0; 145 const offset = gridMap[gk]++; 146 const angle = (offset * 137.5) * Math.PI / 180; 147 const dist = offset * 1.8;
148 let pctX = rawX + Math.cos(angle) * dist; 149 let pctY = rawY + Math.sin(angle) * dist; 150 pctX = Math.max(4, Math.min(96, pctX)); 151 pctY = Math.max(4, Math.min(96, pctY)); 152 // ì¤ì ë°°ì§ íí¼ 153 const dxc = pctX - 50, dyc = pctY - 50; 154 const distC = Math.sqrt(dxc * dxc + dyc * dyc); 155 if (distC < 9) { 156 if (distC > 0) { pctX = 50 + (dxc / distC) * 9; pctY = 50 + (dyc / distC) * 9; } 157 else pctX = 59; 158 } 159 return { 160 title: c.title, 161 url: c.url || '', 162 image: c.image || '', 163 q: q, 164 cat: c.category || classifyCategory(c.title), // 기존 JSON í¸í 165 x: pctX, 166 y: pctY, 167 size: 17, 168 idx: idx 169 }; 170 }); 171} 172 173// ââ Dot DOM ë ë ââ 174function renderDots() { 175 const inner = document.getElementById('scatterInner'); 176 // 기존 dots ì ê±° 177 inner.querySelectorAll('.dot').forEach(d => d.remove()); 178 const frag = document.createDocumentFragment(); 179 state.dots.forEach((dot, i) => { 180 const el = document.createElement('div'); 181 el.className = 'dot dot-' + dot.q; 182 el.dataset.idx = i; 183 el.style.width = dot.size + 'px'; 184 el.style.height = dot.size + 'px'; 185 el.style.left = dot.x.toFixed(1) + '%'; 186 el.style.top = dot.y.toFixed(1) + '%'; 187 frag.appendChild(el); 188 }); 189 inner.appendChild(frag); 190 applyDotFade(); 191} 192 193// ââ ë¹ì í ì¹´í ê³ ë¦¬ ì fade ââ 194function applyDotFade() { 195 const dots = document.querySelectorAll('.dot'); 196 dots.forEach(el => { 197 const idx = parseInt(el.dataset.idx); 198 const d = state.dots[idx]; 199 if (!d) return; 200 if (state.curFilter === 'all' || d.cat === state.curFilter) { 201 el.classList.remove('cat-dim'); 202 } else { 203 el.classList.add('cat-dim'); 204 } 205 }); 206} 207 208// ââ í ì¹´ì´í¸ ì ë°ì´í¸ (ì¹´í ê³ ë¦¬ 기ë°, 0ê°ë ì¨ê¹) ââ 209function renderTabs() { 210 const cc = {}; 211 CAT_CODES.forEach(c => cc[c] = 0); 212 state.dots.forEach(d => { cc[d.cat] = (cc[d.cat] || 0) + 1; }); 213 const tabs = document.querySelectorAll('#clTabs .cl-tab');
214 tabs.forEach(t => { 215 const f = t.dataset.f; 216 if (f === 'all') { 217 t.textContent = `ì ì²´ ${state.dots.length}`; 218 t.style.display = ''; 219 } else if (CATEGORIES[f]) { 220 const n = cc[f] || 0; 221 t.textContent = `${CATEGORIES[f].emoji} ${CATEGORIES[f].label} ${n}`; 222 t.style.display = n > 0 ? '' : 'none'; 223 } 224 t.classList.toggle('active', f === state.curFilter); 225 }); 226 // íì¬ íí°ê° 0ê° ì¹´í ê³ ë¦¬ë©´ 'ì ì²´'ë¡ ë³µê· 227 if (state.curFilter !== 'all' && (cc[state.curFilter] || 0) === 0) { 228 state.curFilter = 'all'; 229 tabs.forEach(t => t.classList.toggle('active', t.dataset.f === 'all')); 230 } 231} 232 233// ââ ì½í ì¸ ë¦¬ì¤í¸ (ì¹´í ê³ ë¦¬ íí°) ââ 234function renderContentList() { 235 const list = document.getElementById('clList'); 236 const cnt = document.getElementById('clCount'); 237 const filtered = state.dots 238 .map((d, i) => ({ d, i })) 239 .filter(o => state.curFilter === 'all' || o.d.cat === state.curFilter); 240 cnt.textContent = `${filtered.length}ê° ì½í ì¸ `; 241 if (!filtered.length) { 242 list.innerHTML = '<div class="cl-empty">íëª©ì´ ììµëë¤</div>'; 243 return; 244 } 245 list.innerHTML = filtered.map(o => { 246 const d = o.d; 247 const t = d.title.replace(/&/g, '&').replace(/</g, '<'); 248 const href = d.url ? buildUTM(d.url) : ''; 249 return `<a class="cl-item ${d.q}" data-idx="${o.i}"${href ? ` href="${href}" target="_blank"` : ''}><span class="cl-item-title">${t}</span>${d.url ? '<span class="cl-item-arrow">â</span>' : ''}</a>`; 250 }).join(''); 251} 252 253// ââ UTM ë¶ì°© ââ 254function buildUTM(url) { 255 const week = state.currentWeek?.meta?.week || ''; 256 const sep = url.indexOf('?') >= 0 ? '&' : '?'; 257 return url + sep + 'utm_source=newsletter&utm_medium=thinking_map&utm_campaign=weekly_' + week; 258} 259 260// ââ í´í ââ 261function showTooltip(el, dot) { 262 const tt = document.getElementById('tooltip'); 263 const ttText = document.getElementById('ttText'); 264 const ttThumb = document.getElementById('ttThumb'); 265 const ttArrow = document.getElementById('ttArrow'); 266 ttText.textContent = dot.title; 267 if (dot.image) { ttThumb.src = dot.image; ttThumb.classList.remove('hide'); } 268 else ttThumb.classList.add('hide'); 269 tt.style.left = '-9999px'; tt.style.top = '-9999px'; 270 tt.classList.add('show'); 271 requestAnimationFrame(() => { 272 const r = el.getBoundingClientRect(); 273 const dotCX = r.left + r.width / 2; 274 const dotCY = r.top; 275 const ttH = tt.offsetHeight || 50; 276 const ttW = tt.offsetWidth || 240; 277 let ttLeft = Math.max(8, Math.min(window.innerWidth - ttW - 8, dotCX - ttW / 2)); 278 let ttTop = dotCY - ttH - 14; 279 ttArrow.style.borderTop = '9px solid #fff'; 280 ttArrow.style.borderBottom = 'none'; 281 let arrowTop = dotCY - 9; 282 if (ttTop < 8) { 283 ttTop = r.bottom + 14; 284 ttArrow.style.borderTop = 'none'; 285 ttArrow.style.borderBottom = '9px solid #fff'; 286 arrowTop = r.bottom + 5; 287 } 288 tt.style.left = ttLeft + 'px'; 289 tt.style.top = ttTop + 'px'; 290 ttArrow.style.left = (dotCX - 8) + 'px'; 291 ttArrow.style.top = arrowTop + 'px'; 292 ttArrow.classList.add('show'); 293 }); 294} 295 296function hideTooltip() { 297 document.getElementById('tooltip').classList.remove('show'); 298 document.getElementById('ttArrow').classList.remove('show'); 299 state.activeDotIdx = -1; 300} 301 302// ââ ì´ë ââ 303function navigateDot(dot) { 304 if (!dot || !dot.url) return; 305 const link = buildUTM(dot.url); 306 if (typeof gtag === 'function') { 307 const qMap = { q1: 'A_í¸ë ëë¼ì´í¸', q2: 'B_ì¤ë¬´ë¼ì´í¸', q3: 'C_í¸ë ëë¥', q4: 'D_ì¤ë¬´ë¥' }; 308 gtag('event', 'dot_click', { 309 content_title: dot.title, 310 content_url: dot.url, 311 quadrant: qMap[dot.q], 312 category: dot.cat || 'other', 313 week_label: state.currentWeek?.meta?.weekLabel || '' 314 }); 315 } 316 window.open(link, '_blank'); 317} 318 319// ââ ì´ë²¤í¸ ââ 320function attachEvents() { 321 document.addEventListener('touchstart', () => { 322 state.isTouch = true; 323 document.body.classList.add('touch-device'); 324 }, { once: true, passive: true }); 325 326 // 주차 ë³ê²½ 327 document.getElementById('weekSelector').addEventListener('change', (e) => { 328 loadAndRenderWeek(e.target.value); 329 }); 330 331 // ê³µì í기 ë²í¼ 332 const shareBtn = document.getElementById('shareBtn'); 333 if (shareBtn) shareBtn.addEventListener('click', () => copyShareUrl(shareBtn)); 334 335 // í (ì¹´í ê³ ë¦¬ íí°) 336 document.getElementById('clTabs').addEventListener('click', (e) => { 337 const tab = e.target.closest('.cl-tab'); 338 if (!tab) return; 339 state.curFilter = tab.dataset.f; 340 if (typeof gtag === 'function') { 341 gtag('event', 'category_filter', { 342 category: state.curFilter, 343 week_label: state.currentWeek?.meta?.weekLabel || '' 344 }); 345 } 346 renderTabs(); 347 renderContentList(); 348 applyDotFade(); 349 }); 350 351 // Scatter ì´ë²¤í¸ 352 const scatter = document.getElementById('scatter'); 353 scatter.addEventListener('click', (e) => { 354 const el = e.target.closest('.dot'); 355 if (state.isTouch) { 356 if (!el) { hideTooltip(); return; } 357 const idx = parseInt(el.dataset.idx); 358 const dot = state.dots[idx]; 359 if (!dot) return; 360 if (state.activeDotIdx === idx) { navigateDot(dot); hideTooltip(); } 361 else { state.activeDotIdx = idx; showTooltip(el, dot); } 362 } else { 363 if (!el) return; 364 navigateDot(state.dots[parseInt(el.dataset.idx)]); 365 } 366 }); 367 scatter.addEventListener('mouseover', (e) => { 368 if (state.isTouch) return; 369 const el = e.target.closest('.dot'); 370 if (!el) { hideTooltip(); return; } 371 const dot = state.dots[parseInt(el.dataset.idx)]; 372 if (!dot) return; 373 showTooltip(el, dot); 374 el.style.cursor = dot.url ? 'pointer' : 'default'; 375 }); 376 scatter.addEventListener('mouseout', (e) => { 377 if (state.isTouch) return; 378 if (e.target.closest('.dot')) hideTooltip(); 379 }); 380 381 // 모ë°ì¼ Go ë²í¼ 382 document.getElementById('ttGo').addEventListener('click', (e) => { 383 e.stopPropagation(); 384 if (state.activeDotIdx >= 0) navigateDot(state.dots[state.activeDotIdx]); 385 hideTooltip(); 386 }); 387 document.addEventListener('click', (e) => { 388 if (state.isTouch && !e.target.closest('#scatter') && !e.target.closest('#tooltip')) hideTooltip(); 389 }); 390 391 // ì½í ì¸ ë¦¬ì¤í¸ hover â dot íë + í´í 392 const clList = document.getElementById('clList'); 393 clList.addEventListener('mouseover', (e) => { 394 if (state.isTouch) return; 395 const item = e.target.closest('.cl-item'); 396 if (!item) return; 397 const idx = parseInt(item.dataset.idx); 398 const dot = state.dots[idx]; 399 if (!dot) return;
400 const dotEl = document.querySelector(`.dot[data-idx="${idx}"]`); 401 if (!dotEl) return; 402 if (state.hlDot && state.hlDot !== dotEl) state.hlDot.classList.remove('cl-hover'); 403 dotEl.classList.add('cl-hover'); 404 state.hlDot = dotEl; 405 showTooltip(dotEl, dot); 406 }); 407 clList.addEventListener('mouseout', (e) => { 408 if (state.isTouch) return; 409 const item = e.target.closest('.cl-item'); 410 if (!item) return; 411 const to = e.relatedTarget; 412 if (to && to.closest && to.closest('.cl-item')) return; 413 if (state.hlDot) { state.hlDot.classList.remove('cl-hover'); state.hlDot = null; } 414 hideTooltip(); 415 }); 416} 417 418// ââ ê³µì URL ë³µì¬ ââ 419function copyShareUrl(btn) { 420 const url = window.location.href; 421 const done = (ok) => { 422 if (ok) { 423 showShareToast(); 424 if (btn) { 425 const original = btn.textContent; 426 btn.textContent = 'â ë³µì¬ë¨!'; 427 btn.classList.add('copied'); 428 setTimeout(() => { 429 btn.textContent = original; 430 btn.classList.remove('copied'); 431 }, 2000); 432 } 433 if (typeof gtag === 'function') { 434 gtag('event', 'share_click', { 435 share_method: 'copy_link', 436 week_label: state.currentWeek?.meta?.weekLabel || '', 437 shared_url: url 438 }); 439 } 440 } else { 441 window.prompt('ìë URLì ë³µì¬í´ 주ì¸ì (Ctrl+C):', url); 442 } 443 }; 444 if (navigator.clipboard && window.isSecureContext) { 445 navigator.clipboard.writeText(url).then(() => done(true)).catch(() => fallbackCopy(url, done)); 446 } else { 447 fallbackCopy(url, done); 448 } 449} 450 451function fallbackCopy(text, done) { 452 try { 453 const ta = document.createElement('textarea'); 454 ta.value = text; 455 ta.style.position = 'fixed'; 456 ta.style.top = '-1000px'; 457 ta.style.opacity = '0'; 458 document.body.appendChild(ta); 459 ta.focus(); ta.select(); 460 const ok = document.execCommand('copy'); 461 document.body.removeChild(ta); 462 done(!!ok); 463 } catch (e) { 464 done(false); 465 } 466} 467 468function showShareToast() { 469 const t = document.getElementById('shareToast'); 470 if (!t) return; 471 t.classList.add('show'); 472 clearTimeout(showShareToast._tid); 473 showShareToast._tid = setTimeout(() => t.classList.remove('show'), 2000); 474} 475 476// ââ URL ì ë°ì´í¸ ââ 477function updateURL(date) { 478 const url = new URL(window.location.href); 479 url.searchParams.set('week', date); 480 window.history.replaceState({}, '', url); 481} 482 483// ââ ìí UI ââ 484function showLoading(show) { 485 document.getElementById('loadingMsg').classList.toggle('hidden', !show); 486 document.getElementById('mainGrid').style.display = show ? 'none' : 'flex'; 487} 488 489function showError(msg) { 490 const err = document.getElementById('errorMsg'); 491 err.textContent = msg; 492 err.classList.remove('hidden'); 493 document.getElementById('loadingMsg').classList.add('hidden'); 494 document.getElementById('mainGrid').style.display = 'none'; 495} 496 497// ââ ìì ââ 498document.addEventListener('DOMContentLoaded', init);
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.