<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ko">
	<id>https://wiki.conaondaapps.com/index.php?action=history&amp;feed=atom&amp;title=%EB%AF%B8%EB%94%94%EC%96%B4%EC%9C%84%ED%82%A4%3ACommon.js</id>
	<title>미디어위키:Common.js - 편집 역사</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.conaondaapps.com/index.php?action=history&amp;feed=atom&amp;title=%EB%AF%B8%EB%94%94%EC%96%B4%EC%9C%84%ED%82%A4%3ACommon.js"/>
	<link rel="alternate" type="text/html" href="https://wiki.conaondaapps.com/index.php?title=%EB%AF%B8%EB%94%94%EC%96%B4%EC%9C%84%ED%82%A4:Common.js&amp;action=history"/>
	<updated>2026-08-20T19:55:19Z</updated>
	<subtitle>이 문서의 편집 역사</subtitle>
	<generator>MediaWiki 1.46.0</generator>
	<entry>
		<id>https://wiki.conaondaapps.com/index.php?title=%EB%AF%B8%EB%94%94%EC%96%B4%EC%9C%84%ED%82%A4:Common.js&amp;diff=85&amp;oldid=prev</id>
		<title>Admina7ec671ac513: migrate: import legacy Wiki content to Vultr</title>
		<link rel="alternate" type="text/html" href="https://wiki.conaondaapps.com/index.php?title=%EB%AF%B8%EB%94%94%EC%96%B4%EC%9C%84%ED%82%A4:Common.js&amp;diff=85&amp;oldid=prev"/>
		<updated>2026-08-20T04:19:38Z</updated>

		<summary type="html">&lt;p&gt;migrate: import legacy Wiki content to Vultr&lt;/p&gt;
&lt;p&gt;&lt;b&gt;새 문서&lt;/b&gt;&lt;/p&gt;&lt;div&gt;/* Conaonda Wiki - Main Page Dynamic Content */&lt;br /&gt;
/*&lt;br /&gt;
 * 게시 위치: MediaWiki:Common.js (스킨 비의존).&lt;br /&gt;
 * 링크는 mw.util.getUrl()로 생성한다. $wgArticlePath의 &amp;#039;$1&amp;#039; 치환을 JS에서&lt;br /&gt;
 * 직접 하면 shell/ResourceLoader를 거치며 &amp;#039;$1&amp;#039;이 소실되는 문제가 있었으므로&lt;br /&gt;
 * 문자열 치환을 쓰지 않는다. MediaWiki:Vector-2022.js의 구버전은 깨진 링크를&lt;br /&gt;
 * 만들고 이 스크립트와 같은 DOM을 경쟁하므로 게시하지 않는다.&lt;br /&gt;
 */&lt;br /&gt;
mw.loader.using( &amp;#039;mediawiki.util&amp;#039; ).then( function () {&lt;br /&gt;
  function init() {&lt;br /&gt;
    var title = mw.config.get( &amp;#039;wgPageName&amp;#039; );&lt;br /&gt;
    if ( title !== &amp;#039;대문&amp;#039; ) return;&lt;br /&gt;
&lt;br /&gt;
    var API = mw.config.get( &amp;#039;wgScriptPath&amp;#039; ) + &amp;#039;/api.php&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
    function timeAgo( d ) {&lt;br /&gt;
      var s = Math.floor( ( Date.now() - d.getTime() ) / 1000 );&lt;br /&gt;
      if ( s &amp;lt; 60 )   return &amp;#039;방금 전&amp;#039;;&lt;br /&gt;
      if ( s &amp;lt; 3600 )  return Math.floor( s / 60 ) + &amp;#039;분 전&amp;#039;;&lt;br /&gt;
      if ( s &amp;lt; 86400 ) return Math.floor( s / 3600 ) + &amp;#039;시간 전&amp;#039;;&lt;br /&gt;
      if ( s &amp;lt; 604800 ) return Math.floor( s / 86400 ) + &amp;#039;일 전&amp;#039;;&lt;br /&gt;
      return d.toLocaleDateString( &amp;#039;ko-KR&amp;#039; );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function esc( s ) { var e = document.createElement( &amp;#039;span&amp;#039; ); e.textContent = s; return e.innerHTML; }&lt;br /&gt;
&lt;br /&gt;
    /* Feed 렌더러: 최근 활동/새 문서 공용. 대문 자기 자신은 제외한다. */&lt;br /&gt;
    function renderFeed( rootId, entries, limit ) {&lt;br /&gt;
      var root = document.getElementById( rootId );&lt;br /&gt;
      if ( !root ) return;&lt;br /&gt;
      var cs = entries.filter( function ( c ) { return c.title !== &amp;#039;대문&amp;#039;; } ).slice( 0, limit );&lt;br /&gt;
      if ( !cs.length ) { root.innerHTML = &amp;#039;&amp;lt;div class=&amp;quot;cx-empty&amp;quot;&amp;gt;아직 기록이 없습니다.&amp;lt;/div&amp;gt;&amp;#039;; return; }&lt;br /&gt;
      var h = &amp;#039;&amp;lt;ul class=&amp;quot;cx-feed&amp;quot;&amp;gt;&amp;#039;;&lt;br /&gt;
      cs.forEach( function ( c ) {&lt;br /&gt;
        var cls = c.type === &amp;#039;new&amp;#039; ? &amp;#039;cx-dot-new&amp;#039; : &amp;#039;cx-dot-edit&amp;#039;;&lt;br /&gt;
        var t = new Date( c.timestamp );&lt;br /&gt;
        var ago = timeAgo( t );&lt;br /&gt;
        var href = mw.util.getUrl( c.title );&lt;br /&gt;
        var diff = mw.util.getUrl( &amp;#039;Special:Diff/&amp;#039; + c.revid );&lt;br /&gt;
        h += &amp;#039;&amp;lt;li&amp;gt;&amp;lt;span class=&amp;quot;cx-dot &amp;#039; + cls + &amp;#039;&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&amp;#039;&lt;br /&gt;
          + &amp;#039;&amp;lt;span class=&amp;quot;cx-feed-main&amp;quot;&amp;gt;&amp;#039;&lt;br /&gt;
          + &amp;#039;&amp;lt;span class=&amp;quot;cx-feed-title&amp;quot;&amp;gt;&amp;lt;a href=&amp;quot;&amp;#039; + href + &amp;#039;&amp;quot;&amp;gt;&amp;#039; + esc( c.title ) + &amp;#039;&amp;lt;/a&amp;gt;&amp;lt;/span&amp;gt;&amp;#039;&lt;br /&gt;
          + ( c.comment ? &amp;#039;&amp;lt;span class=&amp;quot;cx-feed-comment&amp;quot;&amp;gt;&amp;#039; + esc( c.comment ) + &amp;#039;&amp;lt;/span&amp;gt;&amp;#039; : &amp;#039;&amp;#039; )&lt;br /&gt;
          + &amp;#039;&amp;lt;/span&amp;gt;&amp;#039;&lt;br /&gt;
          + &amp;#039;&amp;lt;span class=&amp;quot;cx-feed-meta&amp;quot;&amp;gt;&amp;#039; + ago + &amp;#039; — &amp;#039; + esc( c.user )&lt;br /&gt;
          + &amp;#039; · &amp;lt;a href=&amp;quot;&amp;#039; + diff + &amp;#039;&amp;quot;&amp;gt;차이&amp;lt;/a&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;#039;;&lt;br /&gt;
      } );&lt;br /&gt;
      root.innerHTML = h + &amp;#039;&amp;lt;/ul&amp;gt;&amp;#039;;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var rcRoot = document.getElementById( &amp;#039;cx-recent-root&amp;#039; );&lt;br /&gt;
    if ( rcRoot ) {&lt;br /&gt;
      var p = [&amp;#039;action=query&amp;#039;,&amp;#039;list=recentchanges&amp;#039;,&amp;#039;rclimit=12&amp;#039;,&amp;#039;rcprop=title|timestamp|user|type|comment|ids&amp;#039;,&amp;#039;rcshow=!bot&amp;#039;,&amp;#039;rctype=edit|new&amp;#039;,&amp;#039;rcnamespace=0&amp;#039;,&amp;#039;format=json&amp;#039;,&amp;#039;formatversion=2&amp;#039;].join( &amp;#039;&amp;amp;&amp;#039; );&lt;br /&gt;
      fetch( API + &amp;#039;?&amp;#039; + p )&lt;br /&gt;
        .then( function ( r ) { return r.json(); } )&lt;br /&gt;
        .then( function ( d ) { renderFeed( &amp;#039;cx-recent-root&amp;#039;, ( d.query || {} ).recentchanges || [], 8 ); } )&lt;br /&gt;
        .catch( function () { rcRoot.innerHTML = &amp;#039;&amp;lt;div class=&amp;quot;cx-empty&amp;quot;&amp;gt;활동 정보를 불러올 수 없습니다.&amp;lt;/div&amp;gt;&amp;#039;; } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var newRoot = document.getElementById( &amp;#039;cx-new-root&amp;#039; );&lt;br /&gt;
    if ( newRoot ) {&lt;br /&gt;
      var np = [&amp;#039;action=query&amp;#039;,&amp;#039;list=recentchanges&amp;#039;,&amp;#039;rclimit=8&amp;#039;,&amp;#039;rcprop=title|timestamp|user|type|comment|ids&amp;#039;,&amp;#039;rcshow=!bot&amp;#039;,&amp;#039;rctype=new&amp;#039;,&amp;#039;rcnamespace=0&amp;#039;,&amp;#039;format=json&amp;#039;,&amp;#039;formatversion=2&amp;#039;].join( &amp;#039;&amp;amp;&amp;#039; );&lt;br /&gt;
      fetch( API + &amp;#039;?&amp;#039; + np )&lt;br /&gt;
        .then( function ( r ) { return r.json(); } )&lt;br /&gt;
        .then( function ( d ) { renderFeed( &amp;#039;cx-new-root&amp;#039;, ( d.query || {} ).recentchanges || [], 5 ); } )&lt;br /&gt;
        .catch( function () { newRoot.innerHTML = &amp;#039;&amp;lt;div class=&amp;quot;cx-empty&amp;quot;&amp;gt;새 문서 정보를 불러올 수 없습니다.&amp;lt;/div&amp;gt;&amp;#039;; } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var stRoot = document.getElementById( &amp;#039;cx-stats-root&amp;#039; );&lt;br /&gt;
    if ( stRoot ) {&lt;br /&gt;
      Promise.all( [&lt;br /&gt;
        fetch( API + &amp;#039;?action=query&amp;amp;meta=siteinfo&amp;amp;siprop=statistics&amp;amp;format=json&amp;amp;formatversion=2&amp;#039; ).then( function ( r ) { return r.json(); } ),&lt;br /&gt;
        fetch( API + &amp;#039;?action=query&amp;amp;list=allcategories&amp;amp;aclimit=500&amp;amp;format=json&amp;amp;formatversion=2&amp;#039; ).then( function ( r ) { return r.json(); } )&lt;br /&gt;
      ] )&lt;br /&gt;
        .then( function ( results ) {&lt;br /&gt;
          var s = ( results[0].query || {} ).statistics || {};&lt;br /&gt;
          var cats = ( ( results[1].query || {} ).allcategories || [] ).length;&lt;br /&gt;
          stRoot.innerHTML = &amp;#039;&amp;lt;div class=&amp;quot;cx-stats&amp;quot;&amp;gt;&amp;#039;&lt;br /&gt;
            + &amp;#039;&amp;lt;div class=&amp;quot;cx-stat&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;cx-stat-num&amp;quot;&amp;gt;&amp;#039; + ( s.articles || 0 ) + &amp;#039;&amp;lt;/div&amp;gt;&amp;lt;div class=&amp;quot;cx-stat-label&amp;quot;&amp;gt;문서&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&amp;#039;&lt;br /&gt;
            + &amp;#039;&amp;lt;div class=&amp;quot;cx-stat&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;cx-stat-num&amp;quot;&amp;gt;&amp;#039; + ( s.edits || 0 ) + &amp;#039;&amp;lt;/div&amp;gt;&amp;lt;div class=&amp;quot;cx-stat-label&amp;quot;&amp;gt;편집&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&amp;#039;&lt;br /&gt;
            + &amp;#039;&amp;lt;div class=&amp;quot;cx-stat&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;cx-stat-num&amp;quot;&amp;gt;&amp;#039; + cats + &amp;#039;&amp;lt;/div&amp;gt;&amp;lt;div class=&amp;quot;cx-stat-label&amp;quot;&amp;gt;카테고리&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&amp;#039;&lt;br /&gt;
            + &amp;#039;&amp;lt;div class=&amp;quot;cx-stat&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;cx-stat-num&amp;quot;&amp;gt;&amp;#039; + ( s.files || 0 ) + &amp;#039;&amp;lt;/div&amp;gt;&amp;lt;div class=&amp;quot;cx-stat-label&amp;quot;&amp;gt;파일&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&amp;#039;&lt;br /&gt;
            + &amp;#039;&amp;lt;/div&amp;gt;&amp;#039;;&lt;br /&gt;
        } )&lt;br /&gt;
        .catch( function () { stRoot.innerHTML = &amp;#039;&amp;#039;; } );&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  if ( document.readyState === &amp;#039;loading&amp;#039; ) {&lt;br /&gt;
    document.addEventListener( &amp;#039;DOMContentLoaded&amp;#039;, init );&lt;br /&gt;
  } else {&lt;br /&gt;
    init();&lt;br /&gt;
  }&lt;br /&gt;
} );&lt;br /&gt;
&lt;br /&gt;
/* 자동 문서 생성 (AutoCreate.js) 로드: 텍스트 선택 → 초안 생성/승인 */&lt;br /&gt;
mw.loader.load( mw.config.get( &amp;#039;wgScriptPath&amp;#039; ) + &amp;#039;/index.php?title=MediaWiki:AutoCreate.js&amp;amp;action=raw&amp;amp;ctype=text/javascript&amp;#039; );&lt;br /&gt;
&lt;br /&gt;
/* ───────── Network Graph (interactive, vanilla JS) ───────── */&lt;br /&gt;
( function () {&lt;br /&gt;
  if ( !window.mw || !mw.config ) return;&lt;br /&gt;
  if ( mw.config.get( &amp;#039;wgPageName&amp;#039; ) !== &amp;#039;대문&amp;#039; ) return;&lt;br /&gt;
&lt;br /&gt;
  var GRAPH_ROOT = document.getElementById( &amp;#039;cx-graph-root&amp;#039; );&lt;br /&gt;
  if ( !GRAPH_ROOT ) return;&lt;br /&gt;
  if ( GRAPH_ROOT.getAttribute( &amp;#039;data-cx-graph-inited&amp;#039; ) ) return;&lt;br /&gt;
  GRAPH_ROOT.setAttribute( &amp;#039;data-cx-graph-inited&amp;#039;, &amp;#039;1&amp;#039; );&lt;br /&gt;
&lt;br /&gt;
  var API = mw.config.get( &amp;#039;wgScriptPath&amp;#039; ) + &amp;#039;/api.php&amp;#039;;&lt;br /&gt;
  var pageLinks = {};   // title -&amp;gt; array of linked titles&lt;br /&gt;
  var allTitles = new Set();&lt;br /&gt;
&lt;br /&gt;
  var prevContinueKeys = [];&lt;br /&gt;
  function fetchAll( url, collect ) {&lt;br /&gt;
    return fetch( url ).then( function ( r ) { return r.json(); } ).then( function ( data ) {&lt;br /&gt;
      if ( data &amp;amp;&amp;amp; data.query ) collect( data.query );&lt;br /&gt;
      if ( data &amp;amp;&amp;amp; data.continue ) {&lt;br /&gt;
        var params = new URLSearchParams( data.continue );&lt;br /&gt;
        var base = url.split( &amp;#039;?&amp;#039; )[0];&lt;br /&gt;
        var qs = new URLSearchParams( url.split( &amp;#039;?&amp;#039; )[1] || &amp;#039;&amp;#039; );&lt;br /&gt;
        qs.delete( &amp;#039;continue&amp;#039; );&lt;br /&gt;
        prevContinueKeys.forEach( function ( k ) { qs.delete( k ); } );&lt;br /&gt;
        params.forEach( function ( v, k ) { qs.append( k, v ); } );&lt;br /&gt;
        prevContinueKeys = Array.from( params.keys() );&lt;br /&gt;
        return fetchAll( base + &amp;#039;?&amp;#039; + qs.toString(), collect );&lt;br /&gt;
      }&lt;br /&gt;
    } );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function collectQuery( query ) {&lt;br /&gt;
    ( query.pages || [] ).forEach( function ( page ) {&lt;br /&gt;
      if ( !page.title || page.title === &amp;#039;대문&amp;#039; ) return;&lt;br /&gt;
      allTitles.add( page.title );&lt;br /&gt;
      var links = [];&lt;br /&gt;
      ( page.links || [] ).forEach( function ( l ) {&lt;br /&gt;
        if ( l.title &amp;amp;&amp;amp; l.ns === 0 &amp;amp;&amp;amp; l.title !== &amp;#039;대문&amp;#039; ) links.push( l.title );&lt;br /&gt;
      } );&lt;br /&gt;
      pageLinks[ page.title ] = links;&lt;br /&gt;
    } );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildData() {&lt;br /&gt;
    var titles = Array.from( allTitles );&lt;br /&gt;
    var index = {};&lt;br /&gt;
    titles.forEach( function ( t, i ) { index[ t ] = i; } );&lt;br /&gt;
&lt;br /&gt;
    var edges = [];&lt;br /&gt;
    var inCount = {};&lt;br /&gt;
    titles.forEach( function ( t ) { inCount[ t ] = 0; } );&lt;br /&gt;
    Object.keys( pageLinks ).forEach( function ( src ) {&lt;br /&gt;
      pageLinks[ src ].forEach( function ( dst ) {&lt;br /&gt;
        if ( index[ dst ] !== undefined ) {&lt;br /&gt;
          inCount[ dst ]++;&lt;br /&gt;
          edges.push( [ index[ src ], index[ dst ] ] );&lt;br /&gt;
        }&lt;br /&gt;
      } );&lt;br /&gt;
    } );&lt;br /&gt;
&lt;br /&gt;
    var connected = new Set();&lt;br /&gt;
    edges.forEach( function ( e ) { connected.add( e[0] ); connected.add( e[1] ); } );&lt;br /&gt;
    var keptTitles = titles.filter( function ( t, i ) { return connected.has( i ); } );&lt;br /&gt;
    if ( keptTitles.length &amp;lt; 3 ) {&lt;br /&gt;
      GRAPH_ROOT.innerHTML = &amp;#039;&amp;lt;div class=&amp;quot;cx-empty&amp;quot; style=&amp;quot;padding:2em;text-align:center&amp;quot;&amp;gt;아직 연결된 페이지가 충분하지 않습니다.&amp;lt;/div&amp;gt;&amp;#039;;&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var keptIndex = {};&lt;br /&gt;
    keptTitles.forEach( function ( t, i ) { keptIndex[ t ] = i; } );&lt;br /&gt;
    var keptEdges = edges&lt;br /&gt;
      .filter( function ( e ) { return keptIndex[ titles[ e[0] ] ] !== undefined &amp;amp;&amp;amp; keptIndex[ titles[ e[1] ] ] !== undefined; } )&lt;br /&gt;
      .map( function ( e ) { return [ keptIndex[ titles[ e[0] ] ], keptIndex[ titles[ e[1] ] ] ]; } );&lt;br /&gt;
&lt;br /&gt;
    var maxIn = 1;&lt;br /&gt;
    keptTitles.forEach( function ( t ) { maxIn = Math.max( maxIn, inCount[ t ] ); } );&lt;br /&gt;
    initInteractiveGraph( keptTitles, keptEdges, inCount, maxIn );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function initInteractiveGraph( titles, edges, inCount, maxIn ) {&lt;br /&gt;
    var W = GRAPH_ROOT.clientWidth || 860;&lt;br /&gt;
    var H = 480;&lt;br /&gt;
    var N = titles.length;&lt;br /&gt;
&lt;br /&gt;
    var pos = new Array( N );&lt;br /&gt;
    var vel = new Array( N );&lt;br /&gt;
    var rng = 1;&lt;br /&gt;
    function rand() { rng = ( rng * 16807 ) % 2147483647; return ( rng - 1 ) / 2147483646; }&lt;br /&gt;
    for ( var i = 0; i &amp;lt; N; i++ ) {&lt;br /&gt;
      pos[ i ] = { x: W / 2 + ( rand() - 0.5 ) * W * 0.6, y: H / 2 + ( rand() - 0.5 ) * H * 0.6 };&lt;br /&gt;
      vel[ i ] = { x: 0, y: 0 };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var REPULSION = 900, SPRING = 0.05, CENTER = 0.02, DAMPING = 0.85, REST_LEN = 90;&lt;br /&gt;
    function stepSim( iters ) {&lt;br /&gt;
      for ( var iter = 0; iter &amp;lt; iters; iter++ ) {&lt;br /&gt;
        for ( var a = 0; a &amp;lt; N; a++ ) {&lt;br /&gt;
          for ( var b = a + 1; b &amp;lt; N; b++ ) {&lt;br /&gt;
            var dx = pos[ a ].x - pos[ b ].x, dy = pos[ a ].y - pos[ b ].y;&lt;br /&gt;
            var dist = Math.sqrt( dx * dx + dy * dy ) || 1;&lt;br /&gt;
            var force = REPULSION / ( dist * dist );&lt;br /&gt;
            vel[ a ].x += ( dx / dist ) * force; vel[ a ].y += ( dy / dist ) * force;&lt;br /&gt;
            vel[ b ].x -= ( dx / dist ) * force; vel[ b ].y -= ( dy / dist ) * force;&lt;br /&gt;
          }&lt;br /&gt;
        }&lt;br /&gt;
        edges.forEach( function ( e ) {&lt;br /&gt;
          var sa = pos[ e[0] ], sb = pos[ e[1] ];&lt;br /&gt;
          var dx = sb.x - sa.x, dy = sb.y - sa.y;&lt;br /&gt;
          var dist = Math.sqrt( dx * dx + dy * dy ) || 1;&lt;br /&gt;
          var force = ( dist - REST_LEN ) * SPRING;&lt;br /&gt;
          vel[ e[0] ].x += ( dx / dist ) * force; vel[ e[0] ].y += ( dy / dist ) * force;&lt;br /&gt;
          vel[ e[1] ].x -= ( dx / dist ) * force; vel[ e[1] ].y -= ( dy / dist ) * force;&lt;br /&gt;
        } );&lt;br /&gt;
        for ( var c = 0; c &amp;lt; N; c++ ) {&lt;br /&gt;
          vel[ c ].x += ( W / 2 - pos[ c ].x ) * CENTER;&lt;br /&gt;
          vel[ c ].y += ( H / 2 - pos[ c ].y ) * CENTER;&lt;br /&gt;
        }&lt;br /&gt;
        for ( var m = 0; m &amp;lt; N; m++ ) {&lt;br /&gt;
          vel[ m ].x *= DAMPING; vel[ m ].y *= DAMPING;&lt;br /&gt;
          pos[ m ].x += vel[ m ].x; pos[ m ].y += vel[ m ].y;&lt;br /&gt;
          pos[ m ].x = Math.max( 15, Math.min( W - 15, pos[ m ].x ) );&lt;br /&gt;
          pos[ m ].y = Math.max( 15, Math.min( H - 15, pos[ m ].y ) );&lt;br /&gt;
        }&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
    stepSim( 220 );&lt;br /&gt;
&lt;br /&gt;
    // Adjacency for highlighting&lt;br /&gt;
    var adj = [];&lt;br /&gt;
    for ( var i = 0; i &amp;lt; N; i++ ) adj.push( {} );&lt;br /&gt;
    var edgeNeighbors = new Array( edges.length );&lt;br /&gt;
    edges.forEach( function ( e, ei ) {&lt;br /&gt;
      adj[ e[0] ][ e[1] ] = true;&lt;br /&gt;
      adj[ e[1] ][ e[0] ] = true;&lt;br /&gt;
      edgeNeighbors[ ei ] = e;&lt;br /&gt;
    } );&lt;br /&gt;
&lt;br /&gt;
    var nodeR = [], nodeColor = [];&lt;br /&gt;
    titles.forEach( function ( t, i ) {&lt;br /&gt;
      var ratio = ( inCount[ t ] || 0 ) / maxIn;&lt;br /&gt;
      nodeR[ i ] = 6 + 10 * ratio;&lt;br /&gt;
      nodeColor[ i ] = &amp;#039;rgb(&amp;#039; + Math.round( 37 + ( 1 - ratio ) * 160 ) + &amp;#039;,&amp;#039; + Math.round( 99 + ( 1 - ratio ) * 80 ) + &amp;#039;,&amp;#039; + Math.round( 235 - ratio * 80 ) + &amp;#039;)&amp;#039;;&lt;br /&gt;
    } );&lt;br /&gt;
&lt;br /&gt;
    // Build DOM&lt;br /&gt;
    GRAPH_ROOT.innerHTML = &amp;#039;&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
    var controls = document.createElement( &amp;#039;div&amp;#039; );&lt;br /&gt;
    controls.className = &amp;#039;cx-graph-controls&amp;#039;;&lt;br /&gt;
    var search = document.createElement( &amp;#039;input&amp;#039; );&lt;br /&gt;
    search.type = &amp;#039;text&amp;#039;;&lt;br /&gt;
    search.placeholder = &amp;#039;페이지 검색...&amp;#039;;&lt;br /&gt;
    search.className = &amp;#039;cx-graph-search&amp;#039;;&lt;br /&gt;
    var legend = document.createElement( &amp;#039;div&amp;#039; );&lt;br /&gt;
    legend.className = &amp;#039;cx-graph-legend&amp;#039;;&lt;br /&gt;
    legend.textContent = &amp;#039;크기/색 = 연결 수 · 드래그로 이동 · 휠 줌 · 배경 드래그로 이동&amp;#039;;&lt;br /&gt;
    controls.appendChild( search );&lt;br /&gt;
    controls.appendChild( legend );&lt;br /&gt;
    GRAPH_ROOT.appendChild( controls );&lt;br /&gt;
&lt;br /&gt;
    var svg = document.createElementNS( &amp;#039;http://www.w3.org/2000/svg&amp;#039;, &amp;#039;svg&amp;#039; );&lt;br /&gt;
    svg.setAttribute( &amp;#039;viewBox&amp;#039;, &amp;#039;0 0 &amp;#039; + W + &amp;#039; &amp;#039; + H );&lt;br /&gt;
    svg.setAttribute( &amp;#039;preserveAspectRatio&amp;#039;, &amp;#039;xMidYMid meet&amp;#039; );&lt;br /&gt;
    svg.setAttribute( &amp;#039;class&amp;#039;, &amp;#039;cx-graph-svg&amp;#039; );&lt;br /&gt;
    GRAPH_ROOT.appendChild( svg );&lt;br /&gt;
&lt;br /&gt;
    var content = document.createElementNS( &amp;#039;http://www.w3.org/2000/svg&amp;#039;, &amp;#039;g&amp;#039; );&lt;br /&gt;
    svg.appendChild( content );&lt;br /&gt;
    var edgeLayer = document.createElementNS( &amp;#039;http://www.w3.org/2000/svg&amp;#039;, &amp;#039;g&amp;#039; );&lt;br /&gt;
    var nodeLayer = document.createElementNS( &amp;#039;http://www.w3.org/2000/svg&amp;#039;, &amp;#039;g&amp;#039; );&lt;br /&gt;
    content.appendChild( edgeLayer );&lt;br /&gt;
    content.appendChild( nodeLayer );&lt;br /&gt;
&lt;br /&gt;
    var tooltip = document.createElement( &amp;#039;div&amp;#039; );&lt;br /&gt;
    tooltip.className = &amp;#039;cx-graph-tooltip&amp;#039;;&lt;br /&gt;
    tooltip.style.opacity = &amp;#039;0&amp;#039;;&lt;br /&gt;
    GRAPH_ROOT.appendChild( tooltip );&lt;br /&gt;
&lt;br /&gt;
    // View transform (pan/zoom)&lt;br /&gt;
    var view = { k: 1, tx: 0, ty: 0 };&lt;br /&gt;
    function applyView() {&lt;br /&gt;
      content.setAttribute( &amp;#039;transform&amp;#039;, &amp;#039;translate(&amp;#039; + view.tx + &amp;#039;,&amp;#039; + view.ty + &amp;#039;) scale(&amp;#039; + view.k + &amp;#039;)&amp;#039; );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Edge elements&lt;br /&gt;
    var lineEls = edges.map( function ( e ) {&lt;br /&gt;
      var line = document.createElementNS( &amp;#039;http://www.w3.org/2000/svg&amp;#039;, &amp;#039;line&amp;#039; );&lt;br /&gt;
      line.setAttribute( &amp;#039;class&amp;#039;, &amp;#039;cx-graph-link&amp;#039; );&lt;br /&gt;
      edgeLayer.appendChild( line );&lt;br /&gt;
      return { line: line, e: e };&lt;br /&gt;
    } );&lt;br /&gt;
&lt;br /&gt;
    // Node + label elements&lt;br /&gt;
    var nodeEls = titles.map( function ( t, i ) {&lt;br /&gt;
      var g = document.createElementNS( &amp;#039;http://www.w3.org/2000/svg&amp;#039;, &amp;#039;g&amp;#039; );&lt;br /&gt;
      g.setAttribute( &amp;#039;class&amp;#039;, &amp;#039;cx-graph-node-group&amp;#039; );&lt;br /&gt;
      var circle = document.createElementNS( &amp;#039;http://www.w3.org/2000/svg&amp;#039;, &amp;#039;circle&amp;#039; );&lt;br /&gt;
      circle.setAttribute( &amp;#039;r&amp;#039;, nodeR[ i ] );&lt;br /&gt;
      circle.setAttribute( &amp;#039;fill&amp;#039;, nodeColor[ i ] );&lt;br /&gt;
      circle.setAttribute( &amp;#039;class&amp;#039;, &amp;#039;cx-graph-node&amp;#039; );&lt;br /&gt;
      circle.style.cursor = &amp;#039;pointer&amp;#039;;&lt;br /&gt;
      g.appendChild( circle );&lt;br /&gt;
      var text = document.createElementNS( &amp;#039;http://www.w3.org/2000/svg&amp;#039;, &amp;#039;text&amp;#039; );&lt;br /&gt;
      text.setAttribute( &amp;#039;text-anchor&amp;#039;, &amp;#039;middle&amp;#039; );&lt;br /&gt;
      text.setAttribute( &amp;#039;class&amp;#039;, &amp;#039;cx-graph-label&amp;#039; );&lt;br /&gt;
      text.textContent = t.length &amp;gt; 18 ? t.slice( 0, 16 ) + &amp;#039;...&amp;#039; : t;&lt;br /&gt;
      g.appendChild( text );&lt;br /&gt;
      nodeLayer.appendChild( g );&lt;br /&gt;
      return { g: g, circle: circle, text: text, i: i, title: t };&lt;br /&gt;
    } );&lt;br /&gt;
&lt;br /&gt;
    function draw() {&lt;br /&gt;
      lineEls.forEach( function ( o ) {&lt;br /&gt;
        var e = o.e;&lt;br /&gt;
        o.line.setAttribute( &amp;#039;x1&amp;#039;, pos[ e[0] ].x );&lt;br /&gt;
        o.line.setAttribute( &amp;#039;y1&amp;#039;, pos[ e[0] ].y );&lt;br /&gt;
        o.line.setAttribute( &amp;#039;x2&amp;#039;, pos[ e[1] ].x );&lt;br /&gt;
        o.line.setAttribute( &amp;#039;y2&amp;#039;, pos[ e[1] ].y );&lt;br /&gt;
      } );&lt;br /&gt;
      nodeEls.forEach( function ( o ) {&lt;br /&gt;
        o.g.setAttribute( &amp;#039;transform&amp;#039;, &amp;#039;translate(&amp;#039; + pos[ o.i ].x + &amp;#039;,&amp;#039; + pos[ o.i ].y + &amp;#039;)&amp;#039; );&lt;br /&gt;
        o.text.setAttribute( &amp;#039;dy&amp;#039;, -( nodeR[ o.i ] + 5 ) );&lt;br /&gt;
      } );&lt;br /&gt;
    }&lt;br /&gt;
    draw();&lt;br /&gt;
    applyView();&lt;br /&gt;
&lt;br /&gt;
    // Hover highlight&lt;br /&gt;
    function setHighlight( i ) {&lt;br /&gt;
      var active = i !== null &amp;amp;&amp;amp; i &amp;gt;= 0;&lt;br /&gt;
      lineEls.forEach( function ( o ) {&lt;br /&gt;
        var e = o.e;&lt;br /&gt;
        var conn = active &amp;amp;&amp;amp; ( e[0] === i || e[1] === i );&lt;br /&gt;
        o.line.setAttribute( &amp;#039;class&amp;#039;, &amp;#039;cx-graph-link&amp;#039; + ( conn ? &amp;#039; cx-graph-link-active&amp;#039; : &amp;#039;&amp;#039; ) );&lt;br /&gt;
      } );&lt;br /&gt;
      nodeEls.forEach( function ( o ) {&lt;br /&gt;
        if ( i === null ) {&lt;br /&gt;
          o.circle.setAttribute( &amp;#039;class&amp;#039;, &amp;#039;cx-graph-node&amp;#039; );&lt;br /&gt;
          o.text.style.opacity = &amp;#039;1&amp;#039;;&lt;br /&gt;
          return;&lt;br /&gt;
        }&lt;br /&gt;
        var conn = o.i === i || ( active &amp;amp;&amp;amp; adj[ i ][ o.i ] );&lt;br /&gt;
        o.circle.setAttribute( &amp;#039;class&amp;#039;, &amp;#039;cx-graph-node&amp;#039; + ( conn ? &amp;#039; cx-graph-node-active&amp;#039; : &amp;#039; cx-graph-node-dim&amp;#039; ) );&lt;br /&gt;
        o.text.style.opacity = conn ? &amp;#039;1&amp;#039; : &amp;#039;0.25&amp;#039;;&lt;br /&gt;
      } );&lt;br /&gt;
    }&lt;br /&gt;
    function showTooltip( title, i, ev ) {&lt;br /&gt;
      tooltip.textContent = title + &amp;#039; · 연결 &amp;#039; + ( inCount[ title ] || 0 ) + &amp;#039;개&amp;#039;;&lt;br /&gt;
      tooltip.style.opacity = &amp;#039;1&amp;#039;;&lt;br /&gt;
      var rect = GRAPH_ROOT.getBoundingClientRect();&lt;br /&gt;
      tooltip.style.left = ( ev.clientX - rect.left + 12 ) + &amp;#039;px&amp;#039;;&lt;br /&gt;
      tooltip.style.top = ( ev.clientY - rect.top - 34 ) + &amp;#039;px&amp;#039;;&lt;br /&gt;
    }&lt;br /&gt;
    function hideTooltip() { tooltip.style.opacity = &amp;#039;0&amp;#039;; }&lt;br /&gt;
&lt;br /&gt;
    // Node drag + click. Pointer capture retargets pointermove/pointerup&lt;br /&gt;
    // and the derived click event to the capture target, which would break&lt;br /&gt;
    // node dragging and node click navigation. Use window-level listeners.&lt;br /&gt;
    var dragging = null, dragMoved = false;&lt;br /&gt;
    function clientToSvg( clientX, clientY ) {&lt;br /&gt;
      var pt = svg.createSVGPoint ? svg.createSVGPoint() : null;&lt;br /&gt;
      if ( pt &amp;amp;&amp;amp; svg.getScreenCTM ) {&lt;br /&gt;
        pt.x = clientX; pt.y = clientY;&lt;br /&gt;
        var ctm = svg.getScreenCTM();&lt;br /&gt;
        if ( ctm ) { var p = pt.matrixTransform( ctm.inverse() ); return { x: p.x, y: p.y }; }&lt;br /&gt;
      }&lt;br /&gt;
      var rect = svg.getBoundingClientRect();&lt;br /&gt;
      return { x: clientX - rect.left, y: clientY - rect.top };&lt;br /&gt;
    }&lt;br /&gt;
    function onDragMove( ev ) {&lt;br /&gt;
      if ( dragging === null ) return;&lt;br /&gt;
      var w = clientToSvg( ev.clientX, ev.clientY );&lt;br /&gt;
      var wx = ( w.x - view.tx ) / view.k;&lt;br /&gt;
      var wy = ( w.y - view.ty ) / view.k;&lt;br /&gt;
      var dx = wx - pos[ dragging ].x, dy = wy - pos[ dragging ].y;&lt;br /&gt;
      if ( Math.abs( dx ) + Math.abs( dy ) &amp;gt; 3 ) dragMoved = true;&lt;br /&gt;
      pos[ dragging ].x = Math.max( 15, Math.min( W - 15, wx ) );&lt;br /&gt;
      pos[ dragging ].y = Math.max( 15, Math.min( H - 15, wy ) );&lt;br /&gt;
      vel[ dragging ].x = 0; vel[ dragging ].y = 0;&lt;br /&gt;
      stepSim( 4 );&lt;br /&gt;
      draw();&lt;br /&gt;
    }&lt;br /&gt;
    function onDragEnd() {&lt;br /&gt;
      if ( dragging === null ) return;&lt;br /&gt;
      dragging = null;&lt;br /&gt;
      window.removeEventListener( &amp;#039;pointermove&amp;#039;, onDragMove );&lt;br /&gt;
      window.removeEventListener( &amp;#039;pointerup&amp;#039;, onDragEnd );&lt;br /&gt;
      window.removeEventListener( &amp;#039;pointercancel&amp;#039;, onDragEnd );&lt;br /&gt;
    }&lt;br /&gt;
    function onNodePointerDown( ev, i ) {&lt;br /&gt;
      if ( ev.button !== undefined &amp;amp;&amp;amp; ev.button !== 0 ) return;&lt;br /&gt;
      ev.preventDefault();&lt;br /&gt;
      dragging = i;&lt;br /&gt;
      dragMoved = false;&lt;br /&gt;
      window.addEventListener( &amp;#039;pointermove&amp;#039;, onDragMove );&lt;br /&gt;
      window.addEventListener( &amp;#039;pointerup&amp;#039;, onDragEnd );&lt;br /&gt;
      window.addEventListener( &amp;#039;pointercancel&amp;#039;, onDragEnd );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    nodeEls.forEach( function ( o ) {&lt;br /&gt;
      o.g.addEventListener( &amp;#039;pointerdown&amp;#039;, function ( ev ) { onNodePointerDown( ev, o.i ); } );&lt;br /&gt;
      o.g.addEventListener( &amp;#039;pointermove&amp;#039;, function ( ev ) {&lt;br /&gt;
        if ( dragging !== null ) return;&lt;br /&gt;
        showTooltip( o.title, o.i, ev );&lt;br /&gt;
        setHighlight( o.i );&lt;br /&gt;
      } );&lt;br /&gt;
      o.g.addEventListener( &amp;#039;pointerleave&amp;#039;, function () {&lt;br /&gt;
        if ( dragging === null ) { setHighlight( null ); hideTooltip(); }&lt;br /&gt;
      } );&lt;br /&gt;
      o.g.addEventListener( &amp;#039;click&amp;#039;, function () {&lt;br /&gt;
        if ( !dragMoved ) window.location.href = mw.util.getUrl( o.title );&lt;br /&gt;
      } );&lt;br /&gt;
    } );&lt;br /&gt;
    // Background pan&lt;br /&gt;
    var panning = false, panStart = null;&lt;br /&gt;
    svg.addEventListener( &amp;#039;pointerdown&amp;#039;, function ( ev ) {&lt;br /&gt;
      if ( dragging !== null ) return;&lt;br /&gt;
      if ( ev.button !== undefined &amp;amp;&amp;amp; ev.button !== 0 ) return;&lt;br /&gt;
      panning = true;&lt;br /&gt;
      panStart = { x: ev.clientX, y: ev.clientY, tx: view.tx, ty: view.ty };&lt;br /&gt;
    } );&lt;br /&gt;
    svg.addEventListener( &amp;#039;pointermove&amp;#039;, function ( ev ) {&lt;br /&gt;
      if ( dragging !== null ) return; // node drag handled on window&lt;br /&gt;
      if ( panning &amp;amp;&amp;amp; panStart ) {&lt;br /&gt;
        view.tx = panStart.tx + ( ev.clientX - panStart.x );&lt;br /&gt;
        view.ty = panStart.ty + ( ev.clientY - panStart.y );&lt;br /&gt;
        applyView();&lt;br /&gt;
      }&lt;br /&gt;
    } );&lt;br /&gt;
    svg.addEventListener( &amp;#039;pointerup&amp;#039;, function () { panning = false; panStart = null; } );&lt;br /&gt;
    svg.addEventListener( &amp;#039;pointerleave&amp;#039;, function () { panning = false; panStart = null; } );&lt;br /&gt;
    // Zoom (focal point precise even with viewBox letterboxing)&lt;br /&gt;
    svg.addEventListener( &amp;#039;wheel&amp;#039;, function ( ev ) {&lt;br /&gt;
      ev.preventDefault();&lt;br /&gt;
      var w = clientToSvg( ev.clientX, ev.clientY );&lt;br /&gt;
      var px = w.x, py = w.y;&lt;br /&gt;
      var factor = ev.deltaY &amp;lt; 0 ? 1.15 : 0.87;&lt;br /&gt;
      var newK = Math.max( 0.3, Math.min( 4, view.k * factor ) );&lt;br /&gt;
      var ratio = newK / view.k;&lt;br /&gt;
      view.tx = px - ( px - view.tx ) * ratio;&lt;br /&gt;
      view.ty = py - ( py - view.ty ) * ratio;&lt;br /&gt;
      view.k = newK;&lt;br /&gt;
      applyView();&lt;br /&gt;
    }, { passive: false } );&lt;br /&gt;
    // Search&lt;br /&gt;
    search.addEventListener( &amp;#039;input&amp;#039;, function () {&lt;br /&gt;
      var q = search.value.trim().toLowerCase();&lt;br /&gt;
      if ( !q ) { setHighlight( null ); return; }&lt;br /&gt;
      var found = -1;&lt;br /&gt;
      titles.forEach( function ( t, i ) {&lt;br /&gt;
        if ( t.toLowerCase().indexOf( q ) === 0 &amp;amp;&amp;amp; found === -1 ) found = i;&lt;br /&gt;
      } );&lt;br /&gt;
      if ( found === -1 ) {&lt;br /&gt;
        titles.forEach( function ( t, i ) {&lt;br /&gt;
          if ( t.toLowerCase().indexOf( q ) &amp;gt; -1 &amp;amp;&amp;amp; found === -1 ) found = i;&lt;br /&gt;
        } );&lt;br /&gt;
      }&lt;br /&gt;
      setHighlight( found );&lt;br /&gt;
      if ( found &amp;gt; -1 ) {&lt;br /&gt;
        view.k = Math.max( view.k, 1.4 );&lt;br /&gt;
        view.tx = W / 2 - pos[ found ].x * view.k;&lt;br /&gt;
        view.ty = H / 2 - pos[ found ].y * view.k;&lt;br /&gt;
        applyView();&lt;br /&gt;
      }&lt;br /&gt;
    } );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  var base = API + &amp;#039;?action=query&amp;amp;generator=allpages&amp;amp;gaplimit=50&amp;amp;gapnamespace=0&amp;amp;prop=links&amp;amp;pllimit=100&amp;amp;format=json&amp;amp;formatversion=2&amp;#039;;&lt;br /&gt;
  fetchAll( base, collectQuery )&lt;br /&gt;
    .then( buildData )&lt;br /&gt;
    .catch( function () {&lt;br /&gt;
      GRAPH_ROOT.innerHTML = &amp;#039;&amp;lt;div class=&amp;quot;cx-empty&amp;quot; style=&amp;quot;padding:2em;text-align:center&amp;quot;&amp;gt;그래프를 불러오지 못했습니다.&amp;lt;/div&amp;gt;&amp;#039;;&lt;br /&gt;
    } );&lt;br /&gt;
} )();&lt;/div&gt;</summary>
		<author><name>Admina7ec671ac513</name></author>
	</entry>
</feed>