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