태그: 편집 취소 |
편집 요약 없음 태그: 되돌려진 기여 |
||
| 148번째 줄: | 148번째 줄: | ||
$.getJSON( | $.getJSON( | ||
'/api.php?action=query&list=recentchanges&rclimit=' + encodeURIComponent(limit || | '/api.php?action=query&list=recentchanges&rclimit=' + encodeURIComponent(limit || 5) + '&rcprop=title|timestamp&format=json&rcnamespace=0&rctype=edit|new', | ||
function(data) { | function(data) { | ||
var items = data && data.query ? data.query.recentchanges : []; | var items = data && data.query ? data.query.recentchanges : []; | ||
| 160번째 줄: | 160번째 줄: | ||
$.each(items, function(i, item) { | $.each(items, function(i, item) { | ||
var label = timeAgo(item.timestamp); | var label = timeAgo(item.timestamp); | ||
html += | html += | ||
'< | '<div class="clbi-recent-item">' + | ||
'< | '<div class="clbi-recent-title-wrap">' + | ||
'<a href="/index.php/' + encodeURIComponent(item.title) + '" class="clbi-recent-title">' + item.title + '</a>' + | |||
'</div>' + | |||
'<span class="clbi-recent-time">' + label + '</span>' + | |||
'</div>'; | |||
'</ | |||
}); | }); | ||
$target.html(html); | $target.html(html); | ||
$target.find('.clbi-recent-item').each(function() { | |||
var wrap = $(this).find('.clbi-recent-title-wrap'); | |||
var title = $(this).find('.clbi-recent-title'); | |||
var wrapW = wrap.width(); | |||
var titleW = title[0].scrollWidth; | |||
if (titleW > wrapW + 20) { | |||
var duration = titleW / 40; | |||
title.css({ | |||
animation: 'clbi-scroll ' + duration + 's linear infinite', | |||
'--scroll-dist': '-' + (titleW - wrapW + 8) + 'px' | |||
}); | |||
} | |||
}); | |||
} | } | ||
).fail(function() { | ).fail(function() { | ||
| 204번째 줄: | 217번째 줄: | ||
$('#clbi-title-left-news').text(newsTitle); | $('#clbi-title-left-news').text(newsTitle); | ||
$('#clbi-left-news-changelog-main').text(changelogTitle); | $('#clbi-left-news-changelog-main').text(changelogTitle); | ||
$('#clbi-left-news-recent-main').text(recentTitle); | |||
$('#clbi-left-news-recent- | |||
$('#clbi-title-search a').text(t.search); | $('#clbi-title-search a').text(t.search); | ||
| 222번째 줄: | 234번째 줄: | ||
var pageName = normalizePageName(mw.config.get('wgPageName')); | var pageName = normalizePageName(mw.config.get('wgPageName')); | ||
var specialPage = String(mw.config.get('wgCanonicalSpecialPageName') || ''); | var specialPage = String(mw.config.get('wgCanonicalSpecialPageName') || ''); | ||
$('#clbi-left-news-changelog-main').text(changelogTitle); | |||
$('#clbi-left-news-recent-title').text('RECENT CHANGES'); | |||
$('.clbi-user-btn').removeClass('clbi-user-btn-active'); | $('.clbi-user-btn').removeClass('clbi-user-btn-active'); | ||
| 268번째 줄: | 283번째 줄: | ||
var isEditable = mw.config.get('wgIsProbablyEditable'); | var isEditable = mw.config.get('wgIsProbablyEditable'); | ||
if (isEditable === false) { | if (isEditable === false) { | ||
return false; | return false; | ||
} | } | ||
| 278번째 줄: | 288번째 줄: | ||
return true; | return true; | ||
} | } | ||
function moveCatlinksToBottom() { | function moveCatlinksToBottom() { | ||
var main = $('.liberty-content-main'); | var main = $('.liberty-content-main'); | ||
| 298번째 줄: | 307번째 줄: | ||
// 대문 스타일 | // 대문 스타일 | ||
function initCategoryNavIfAvailable(root) { | |||
/* | |||
CategoryNav.js는 대문 카테고리 네비를 SVG로 생성한다. | |||
Common.js가 SPA로 본문을 갈아끼운 뒤에는 MediaWiki 원래 페이지 로드와 달리 | |||
CategoryNav.js의 초기 DOMContentLoaded만으로는 새 mount를 다시 잡지 못할 수 있다. | |||
CategoryNav.js 자체도 mw.hook('wikipage.content')를 듣지만, 로드 순서와 SPA 타이밍이 | |||
엇갈릴 수 있으므로 Common.js 쪽에서도 존재 여부를 확인한 뒤 한 번 더 호출한다. | |||
이 함수는 CategoryNav.js가 아직 로드되지 않았으면 아무 것도 하지 않는다. | |||
*/ | |||
if ( | |||
window.CLBI && | |||
window.CLBI.categoryNav && | |||
typeof window.CLBI.categoryNav.init === 'function' | |||
) { | |||
window.CLBI.categoryNav.init(root || document); | |||
} | |||
} | |||
function removeLegacyMainPageHero() { | |||
/* | |||
기존 대문 전용 레거시 요소 정리 | |||
----------------------------------------- | |||
이전 대문 구조에서는 Common.js가 본문 바깥에 #clbi-main-logo를 직접 삽입하고, | |||
본문 안의 #clbi-main-crt-hero를 #clbi-main-crt-hero-wrap으로 감싸서 | |||
.liberty-content-main 위쪽으로 재배치했다. | |||
새 대문은 본문 내부의 .main-portal이 로고, 알림, 카테고리 네비, 이미지 피드, | |||
방명록, 상태 패널을 모두 담당한다. 따라서 Common.js가 별도 로고나 CRT 래퍼를 | |||
삽입하면 새 로고/콘텐츠와 중복된다. | |||
여기서는 JS가 만들던 바깥 로고와 CRT 래퍼를 제거하고, 예전 대문 원본이나 | |||
캐시된 렌더 결과에 남아 있을 수 있는 #clbi-main-crt-hero도 제거한다. | |||
*/ | |||
$('#clbi-main-logo').remove(); | |||
$('#clbi-main-crt-hero-wrap').remove(); | |||
$('#clbi-main-crt-hero').remove(); | |||
} | |||
function applyMainPageStyle() { | function applyMainPageStyle() { | ||
var specialPage = mw.config.get('wgCanonicalSpecialPageName'); | var specialPage = mw.config.get('wgCanonicalSpecialPageName'); | ||
| 303번째 줄: | 352번째 줄: | ||
var pageName = normalizePageName(mw.config.get('wgPageName')); | var pageName = normalizePageName(mw.config.get('wgPageName')); | ||
var | var isMainPage = (pageName === '대문'); | ||
var isScreenDoc = ($('.screen-header').length > 0); | |||
var hideTools = (isMainPage || !canShowContentTools()); | |||
// 모든 문서에서 분류 바를 본문 컨테이너 아래로 이동 | // 모든 문서에서 분류 바를 본문 컨테이너 아래로 이동 | ||
moveCatlinksToBottom(); | moveCatlinksToBottom(); | ||
if ( | if (isMainPage) { | ||
$('.liberty-content-header').css('display', 'none'); | $('.liberty-content-header').css('display', 'none'); | ||
$('.mw-page-title-main').addClass('clbi-hide'); | $('.mw-page-title-main').addClass('clbi-hide'); | ||
| 314번째 줄: | 365번째 줄: | ||
$('.liberty-content-main').css('border-radius', '5px'); | $('.liberty-content-main').css('border-radius', '5px'); | ||
// 새 대문은 .main-portal 본문 구조가 로고/히어로를 담당한다. | |||
// Common.js의 구식 바깥 로고/CRT 재배치 루틴은 사용하지 않는다. | |||
removeLegacyMainPageHero(); | |||
$('#clbi-tools-box').remove(); | |||
$('.content-tools').css('display', 'none'); | |||
$('.liberty-content').addClass('content-tools-hidden'); | |||
initCategoryNavIfAvailable(document); | |||
} else if (isScreenDoc) { | |||
} else if ( | |||
$('.liberty-content-header').css('display', 'none'); | $('.liberty-content-header').css('display', 'none'); | ||
$('.mw-page-title-main').addClass('clbi-hide'); | $('.mw-page-title-main').addClass('clbi-hide'); | ||
$('.catlinks').css('display', ''); | $('.catlinks').css('display', ''); | ||
$('.liberty-content-main').css('border-radius', '5px'); | $('.liberty-content-main').css('border-radius', '5px'); | ||
$('#clbi-main-logo').remove(); | $('#clbi-main-logo').remove(); | ||
$('#clbi-main-crt-hero-wrap').remove(); | $('#clbi-main-crt-hero-wrap').remove(); | ||
| 345번째 줄: | 388번째 줄: | ||
var $toolsTitle = $('<div class="clbi-left-title"><span class="clbi-icon" style="--icon:var(--ic-ui-003)"></span> 관리</div>'); | var $toolsTitle = $('<div class="clbi-left-title"><span class="clbi-icon" style="--icon:var(--ic-ui-003)"></span> 관리</div>'); | ||
var $toolsContent = $('<div class="clbi-left-content"></div>'); | var $toolsContent = $('<div class="clbi-left-content"></div>'); | ||
$toolsContent.append($('.content-tools .btn-group').clone(true)); | $toolsContent.append($('.content-tools .btn-group').clone(true)); | ||
$toolsBox.append($toolsTitle).append($toolsContent); | $toolsBox.append($toolsTitle).append($toolsContent); | ||
| 352번째 줄: | 396번째 줄: | ||
$('.content-tools').css('display', 'none'); | $('.content-tools').css('display', 'none'); | ||
$('.liberty-content').addClass('content-tools-hidden'); | $('.liberty-content').addClass('content-tools-hidden'); | ||
} else { | } else { | ||
$('.liberty-content-header').css('display', ''); | $('.liberty-content-header').css('display', ''); | ||
| 357번째 줄: | 402번째 줄: | ||
$('.catlinks').css('display', ''); | $('.catlinks').css('display', ''); | ||
$('.liberty-content-main').css('border-radius', '5px'); | $('.liberty-content-main').css('border-radius', '5px'); | ||
$('#clbi-main-logo').remove(); | $('#clbi-main-logo').remove(); | ||
$('#clbi-main-crt-hero-wrap').remove(); | $('#clbi-main-crt-hero-wrap').remove(); | ||
| 365번째 줄: | 411번째 줄: | ||
$('.content-tools').css('display', 'none'); | $('.content-tools').css('display', 'none'); | ||
$('.liberty-content').addClass('content-tools-hidden'); | $('.liberty-content').addClass('content-tools-hidden'); | ||
} else if (! | } else if (!isScreenDoc) { | ||
$('.content-tools').css('display', ''); | $('.content-tools').css('display', ''); | ||
$('.liberty-content').removeClass('content-tools-hidden'); | $('.liberty-content').removeClass('content-tools-hidden'); | ||
| 560번째 줄: | 606번째 줄: | ||
tocBox.appendChild(title); | tocBox.appendChild(title); | ||
tocBox.appendChild(body); | tocBox.appendChild(body); | ||
// 왼쪽 목차: | // 왼쪽 목차: 사용자 박스 바로 아래, 뉴스 박스 위에 배치한다. | ||
var userBox = document.getElementById('clbi-user-box'); | |||
var newsBox = document.getElementById('clbi-left-news-box'); | |||
if (userBox && userBox.parentNode === leftSidebar) { | |||
if (userBox.nextSibling) { | |||
leftSidebar.insertBefore(tocBox, userBox.nextSibling); | |||
} else { | |||
leftSidebar.appendChild(tocBox); | |||
} | |||
} else if (newsBox && newsBox.parentNode === leftSidebar) { | |||
leftSidebar.insertBefore(tocBox, newsBox); | |||
} else { | |||
leftSidebar.appendChild(tocBox); | |||
} | |||
// 왼쪽 목차: DOM에 붙은 뒤 실제 폭을 계산해야 하므로 비동기 재측정한다. | |||
requestAnimationFrame(function () { | requestAnimationFrame(function () { | ||
initTocTitleScroll(tocBox); | initTocTitleScroll(tocBox); | ||
}); | |||
setTimeout(function () { | |||
initTocTitleScroll(tocBox); | |||
}, 300); | |||
} | } | ||
// 초기화 함수 | // 초기화 함수 | ||
function initSidebars() { | function initSidebars() { | ||
var | var userName = mw.config.get('wgUserName'); | ||
var | var scriptPath = mw.config.get('wgScriptPath') || ''; | ||
$('#clbi-left-sidebar, #clbi-right-sidebar, #clbi-user-box, #clbi-left-news-box, #clbi-left-search-box, #clbi-left-links-box').remove(); | |||
var leftBoxes = | |||
'<div id="clbi-left-sidebar">' + | |||
'<div id="clbi-user-box" class="clbi-left-box">' + | |||
'<div class="clbi-left-title" id="clbi-title-user">' + | |||
'<span class="clbi-icon" style="--icon:var(--ic-ui-005)"></span> 사용자' + | |||
'</div>' + | |||
'<div class="clbi-left-content">' + | |||
(userName ? | |||
'<div id="clbi-user-logged-in">' + | |||
'<div id="clbi-user-avatar-wrap">' + | |||
'<img id="clbi-user-avatar" src="/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(userName) + '.png">' + | |||
'<div id="clbi-user-name">' + userName + '</div>' + | |||
'</div>' + | |||
'<div class="clbi-user-btn-grid">' + | |||
'<a class="clbi-user-btn" id="clbi-btn-contribution" href="/index.php/특수:기여/' + encodeURIComponent(userName) + '">기여</a>' + | |||
'<a class="clbi-user-btn" id="clbi-btn-watchlist" href="/index.php/특수:주시문서목록">주시문서</a>' + | |||
'< | '<a class="clbi-user-btn" id="clbi-btn-preferences" href="/index.php/특수:환경설정">설정</a>' + | ||
'< | '<a class="clbi-user-btn" id="clbi-btn-logout" href="/index.php/특수:로그아웃">로그아웃</a>' + | ||
'< | '</div>' + | ||
'</div>' | |||
: | |||
'<div id="clbi-user-anon">' + | |||
'<a class="clbi-user-btn" id="clbi-btn-login" href="/index.php/특수:로그인">로그인</a>' + | |||
'</div>' | |||
) + | |||
'</div>' + | '</div>' + | ||
'</div>' | '</div>' + | ||
'<div id="clbi-left-news-box" class="clbi-left-box">' + | |||
'<div class="clbi-left-title" id="clbi-title-left-news">' + | |||
'<div class="clbi- | '<span class="clbi-icon" style="--icon:var(--ic-ui-010)"></span> 뉴스' + | ||
'<div class="clbi- | |||
'</div>' + | '</div>' + | ||
'<div class="clbi-left-content">' + | |||
'<div class="clbi-left-news-section" id="clbi-left-news-changelog">' + | |||
'<a id="clbi-left-news-changelog-main" class="clbi-left-news-main" href="/index.php/프로젝트:체인지로그">체인지로그</a>' + | |||
'</div>' + | |||
'<div class="clbi-left-news-section" id="clbi-left-news-recent">' + | |||
'<div id="clbi-left-news-recent-title">RECENT CHANGES</div>' + | |||
'<div id="clbi-left-news-recent-list"></div>' + | |||
'</div>' + | |||
'</div>' + | '</div>' + | ||
'</div>' + | '</div>' + | ||
'<div id="clbi-left-search-box" class="clbi-left-box">' + | |||
'<div class="clbi-left-title" id="clbi-title-search">' + | |||
'<span class="clbi-icon" style="--icon:var(--ic-ui-002)"></span> <a href="/index.php/특수:검색">검색</a>' + | |||
'</div>' + | |||
'<div class="clbi-left-content">' + | |||
'<div id="clbi-search-form">' + | |||
'<input type="text" id="clbi-search-input" placeholder="검색...">' + | |||
'<button id="clbi-search-btn">›</button>' + | |||
'</div>' + | |||
'</div>' + | |||
'</div>' + | '</div>' + | ||
'<div id="clbi-left-links-box" class="clbi-left-box">' + | |||
'<div class="clbi-left-title" id="clbi-title-links">' + | |||
'<span class="clbi-icon" style="--icon:var(--ic-ui-001)"></span> <span id="clbi-title-links-label">링크</span>' + | |||
'</div>' + | '</div>' + | ||
'<div class="clbi-left-content">' + | |||
'<div class="clbi- | '<a class="clbi-link-btn" href="/index.php/특수:최근바뀜">최근 바뀜</a>' + | ||
'<a class="clbi-link-btn" href="/index.php/특수:임의문서">임의 문서</a>' + | |||
'<a class="clbi-link-btn" href="/index.php/특수:분류">분류</a>' + | |||
'< | |||
'< | |||
'</div>' + | '</div>' + | ||
'</div>' + | '</div>' + | ||
'</div>'; | |||
$(function() { | $(function() { | ||
| 823번째 줄: | 797번째 줄: | ||
mw.hook('wikipage.content').fire($('.liberty-content-main')); | mw.hook('wikipage.content').fire($('.liberty-content-main')); | ||
applyMainPageStyle(); | applyMainPageStyle(); | ||
initCategoryNavIfAvailable(document); | |||
$('#side-toc-box').remove(); | $('#side-toc-box').remove(); | ||
| 836번째 줄: | 811번째 줄: | ||
} | } | ||
// 목차 링크는 전용 처리 | // 목차 링크는 전용 처리 | ||
$(document).on('click', '#side-toc-box a, #toc a, .toc a', function(e) { | $(document).on('click', '#side-toc-box a, #toc a, .toc a', function(e) { | ||
var href = $(this).attr('href'); | |||
if (!href || href.charAt(0) !== '#') return; | |||
var rawId = href.slice(1); | |||
if (!rawId) return; | |||
var decodedId = rawId; | |||
try { | |||
decodedId = decodeURIComponent(rawId); | |||
} catch (err) { | |||
decodedId = rawId; | |||
} | |||
var target = document.getElementById(decodedId); | |||
if (!target && window.CSS && CSS.escape) { | |||
target = document.querySelector('#' + CSS.escape(decodedId)); | |||
} | |||
if (!target) return; | |||
e.preventDefault(); | |||
e.stopPropagation(); | |||
var scrollTarget = target.closest('h2, h3') || target; | |||
scrollTarget.scrollIntoView({ | |||
behavior: 'auto', | |||
block: 'start' | |||
}); | |||
history.replaceState(null, '', '#' + rawId); | |||
}); | }); | ||
$(document).on('click', 'a', function(e) { | $(document).on('click', 'a', function(e) { | ||
| 923번째 줄: | 898번째 줄: | ||
} | } | ||
// 펼접 토글 | // 펼접 토글 | ||
function getFoldTexts() { | function getFoldTexts() { | ||
| 1,052번째 줄: | 1,026번째 줄: | ||
} | } | ||
window.scrollTo( | requestAnimationFrame(function () { | ||
window.scrollTo(window.scrollX, scrollY); | |||
}); | |||
}); | }); | ||
}); | }); | ||
// ========== 프로필 | // ========== 사용자 설정 / 프로필 ========== | ||
function initProfile() { | function initProfile() { | ||
$('.profile-card').remove(); | var pageName = normalizePageName(mw.config.get('wgPageName')); | ||
var namespace = mw.config.get('wgNamespaceNumber'); | |||
if (namespace !== 2) { | |||
$('.profile-card').remove(); | |||
return; | |||
} | |||
var username = pageName.replace(/^사용자:/, '').replace(/^User:/, ''); | |||
if (!username) return; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var | var api = new mw.Api(); | ||
api.get({ | |||
action: 'query', | |||
list: 'users', | |||
ususers: username, | |||
usprop: 'editcount|registration|groups', | |||
format: 'json' | |||
}).then(function (data) { | |||
var users = data && data.query ? data.query.users : []; | |||
var user = users && users.length ? users[0] : null; | |||
var contentEl = document.getElementById('mw-content-text'); | |||
if (!contentEl) return; | |||
var pageContent = contentEl.querySelector('.mw-parser-output') || contentEl; | |||
injectProfileCard(username, user, pageContent); | |||
}); | |||
}); | }); | ||
} | } | ||
| 1,180번째 줄: | 1,158번째 줄: | ||
} | } | ||
} | } | ||
// ========== 알림 시스템 ========== | // ========== 알림 시스템 ========== | ||
| 1,350번째 줄: | 1,327번째 줄: | ||
var btn = document.getElementById('clbi-notification-toggle'); | var btn = document.getElementById('clbi-notification-toggle'); | ||
if (!btn) return; | if (!btn) return; | ||
2026년 5월 19일 (화) 10:16 판
mw.loader.load('https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js');
mw.loader.load('/index.php?title=MediaWiki:DevTools.js&action=raw&ctype=text/javascript');
mw.loader.load('/index.php?title=MediaWiki:LangDrawer.js&action=raw&ctype=text/javascript');
mw.loader.load('/index.php?title=MediaWiki:CategoryNav.js&action=raw&ctype=text/javascript');
function loadLangScript(done) {
$.getScript('/index.php?title=미디어위키:Lang.js&action=raw&ctype=text/javascript')
.done(function() {
if (typeof done === 'function') done();
})
.fail(function(a, b, c) {
console.error('Lang.js load failed:', b, c);
if (typeof done === 'function') done();
});
}
$(function() {
$('body').prepend(
'<div class="WW-bg" style="position:fixed;top:0;left:0;width:100%;height:100vh;"></div>'
);
// ── 상단 네비게이션 바 ──
var navHtml =
'<div id="clbi-top-nav-wrap">' +
'<div id="clbi-top-nav">' +
'<div id="clbi-top-nav-main">' +
'<div id="clbi-top-nav-tabs">' +
'<div class="clbi-top-nav-item clbi-tnav-lang" id="clbi-tnav-lang">' +
'<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-language-001.png" alt="">' +
'<span class="clbi-tnav-lang-bottom">' +
'<span class="clbi-tnav-lang-code" id="clbi-tnav-lang-code">KR</span>' +
'<span class="clbi-tnav-arrow">▾</span>' +
'</span>' +
'</div>' +
'<a class="clbi-top-nav-item" href="/index.php/대문">' +
'<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-main-menu-001.png" alt="">' +
'<span class="clbi-tnav-label">메인 메뉴</span>' +
'</a>' +
'<a class="clbi-top-nav-item" href="/index.php/프로젝트:소개">' +
'<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-project-001.png" alt="">' +
'<span class="clbi-tnav-label">프로젝트</span>' +
'</a>' +
'<div class="clbi-top-nav-item" id="clbi-tnav-worldbuilding">' +
'<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-worldbuilding-001.png" alt="">' +
'<span class="clbi-tnav-label">세계관</span>' +
'<span class="clbi-tnav-arrow">▾</span>' +
'</div>' +
'</div>' +
'<div id="clbi-top-nav-search">' +
'<input type="text" id="clbi-top-search-input" placeholder="검색...">' +
'</div>' +
'<div id="clbi-top-nav-right">' +
'<div class="clbi-top-nav-item" id="clbi-tnav-info">' +
'<span class="clbi-tnav-label">ℹ</span>' +
'</div>' +
'</div>' +
'</div>' +
'<div id="clbi-sub-worldbuilding">' +
'<div id="clbi-sub-worldbuilding-inner">' +
'<div class="clbi-tnav-sub-list">' +
'<a class="clbi-tnav-sub-item" href="/index.php/역사적_사건">역사적 사건</a>' +
'<a class="clbi-tnav-sub-item" href="/index.php/설정">설정</a>' +
'<a class="clbi-tnav-sub-item" href="/index.php/국가_및_조합">국가 및 조합</a>' +
'<a class="clbi-tnav-sub-item" href="/index.php/기업_및_공동체">기업 및 공동체</a>' +
'<a class="clbi-tnav-sub-item" href="/index.php/군_정치집단">군, 정치집단</a>' +
'<a class="clbi-tnav-sub-item" href="/index.php/인물">인물</a>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
$('.content-wrapper').before(navHtml);
$('#clbi-tnav-worldbuilding').on('click', function() {
var $menu = $('#clbi-sub-worldbuilding');
var $btn = $(this);
$menu.toggleClass('worldbuilding-open');
$btn.toggleClass('clbi-tnav-active', $menu.hasClass('worldbuilding-open'));
});
$('#clbi-top-search-input').on('keydown', function(e) {
if (e.key === 'Enter') {
var q = $(this).val().trim();
if (q) window.location.href = '/index.php?search=' + encodeURIComponent(q);
}
});
});
// 페이지 전환 사운드
var transitionSound = new Audio('/index.php?title=특수:Redirect/file/Sfx-ui-001.mp3');
(function() {
var master = parseFloat(localStorage.getItem('clbi-audio-master') || 80) / 100;
var sfx = parseFloat(localStorage.getItem('clbi-audio-sfx') || 60) / 100;
var sfxOn = localStorage.getItem('clbi-audio-sfxOn') !== 'false';
transitionSound.volume = sfxOn ? master * sfx : 0;
})();
function playStaticSound() {
var master = parseFloat(localStorage.getItem('clbi-audio-master') || 80) / 100;
var sfx = parseFloat(localStorage.getItem('clbi-audio-sfx') || 60) / 100;
var sfxOn = localStorage.getItem('clbi-audio-sfxOn') !== 'false';
if (!sfxOn) return;
transitionSound.volume = master * sfx;
transitionSound.currentTime = 0;
transitionSound.play();
}
// 현재 언어 감지
function getCurrentLang() {
var langData = document.getElementById('clbi-lang-data');
return langData ? (langData.getAttribute('data-lang') || 'ko') : 'ko';
}
function normalizePageName(value) {
return String(value || '')
.split('?')[0]
.replace(/^\/index\.php\//, '')
.replace(/_/g, ' ')
.trim();
}
function buildWikiPath(title) {
return '/index.php/' + encodeURI(String(title || '').replace(/ /g, '_'));
}
function loadRecentChangesList(targetSelector, limit) {
var $target = $(targetSelector);
if (!$target.length) return;
var lang = getCurrentLang();
var t = (window.LANG && window.LANG[lang]) ? window.LANG[lang] : (window.LANG ? window.LANG.ko : null);
$target.html((t && t.loading) ? t.loading : '불러오는 중...');
$.getJSON(
'/api.php?action=query&list=recentchanges&rclimit=' + encodeURIComponent(limit || 5) + '&rcprop=title|timestamp&format=json&rcnamespace=0&rctype=edit|new',
function(data) {
var items = data && data.query ? data.query.recentchanges : [];
var html = '';
if (!items || !items.length) {
$target.html('표시할 변경 사항이 없습니다.');
return;
}
$.each(items, function(i, item) {
var label = timeAgo(item.timestamp);
html +=
'<div class="clbi-recent-item">' +
'<div class="clbi-recent-title-wrap">' +
'<a href="/index.php/' + encodeURIComponent(item.title) + '" class="clbi-recent-title">' + item.title + '</a>' +
'</div>' +
'<span class="clbi-recent-time">' + label + '</span>' +
'</div>';
});
$target.html(html);
$target.find('.clbi-recent-item').each(function() {
var wrap = $(this).find('.clbi-recent-title-wrap');
var title = $(this).find('.clbi-recent-title');
var wrapW = wrap.width();
var titleW = title[0].scrollWidth;
if (titleW > wrapW + 20) {
var duration = titleW / 40;
title.css({
animation: 'clbi-scroll ' + duration + 's linear infinite',
'--scroll-dist': '-' + (titleW - wrapW + 8) + 'px'
});
}
});
}
).fail(function() {
var lang = getCurrentLang();
var t = (window.LANG && window.LANG[lang]) ? window.LANG[lang] : (window.LANG ? window.LANG.ko : null);
$target.html((t && t.loadFail) ? t.loadFail : '불러오기 실패');
});
}
// 국가_및_조합 전용 왼쪽 사이드바 이미지
function updateLeftSidebarNationsImage() {
$('#clbi-left-nations-image').remove();
}
// 사이드바 업데이트
function updateSidebar() {
if (!window.LANG) {
setTimeout(updateSidebar, 100);
return;
}
var currentLang = getCurrentLang();
var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
var newsTitle = t.news || '뉴스';
var changelogTitle = t.changelog || '체인지로그';
var recentTitle = t.recentChanges || '최근 변경';
$('#clbi-title-left-news').text(newsTitle);
$('#clbi-left-news-changelog-main').text(changelogTitle);
$('#clbi-left-news-recent-main').text(recentTitle);
$('#clbi-title-search a').text(t.search);
$('#clbi-search-input').attr('placeholder', t.search + '...');
$('#clbi-title-recent a').text(recentTitle);
$('#clbi-title-guide-label').text(t.guide);
$('#clbi-guide-link').text(t.getStarted);
$('#clbi-title-links-label').text(t.links);
$('#clbi-btn-contribution').text(t.contribution);
$('#clbi-btn-watchlist').text(t.watchlist);
$('#clbi-btn-preferences').text(t.preferences);
$('#clbi-btn-logout').text(t.logout);
$('#clbi-btn-login').text(t.login);
var pageName = normalizePageName(mw.config.get('wgPageName'));
var specialPage = String(mw.config.get('wgCanonicalSpecialPageName') || '');
$('#clbi-left-news-changelog-main').text(changelogTitle);
$('#clbi-left-news-recent-title').text('RECENT CHANGES');
$('.clbi-user-btn').removeClass('clbi-user-btn-active');
if (
specialPage === 'Contributions' ||
specialPage === '기여' ||
pageName.indexOf('특수:기여') === 0 ||
pageName.indexOf('Special:Contributions') === 0
) {
$('#clbi-btn-contribution').addClass('clbi-user-btn-active');
}
if (specialPage === 'Watchlist') {
$('#clbi-btn-watchlist').addClass('clbi-user-btn-active');
}
if (
specialPage === '설정' ||
pageName === '특수:설정' ||
pageName === 'Special:설정'
) {
$('#clbi-btn-preferences').addClass('clbi-user-btn-active');
}
$('.toggleBtn').each(function() {
var btn = $(this);
if (!$('#' + btn.data('target')).hasClass('folding-open')) {
btn.text(t.expand);
} else {
btn.text(t.collapse);
}
});
updateLeftSidebarNationsImage();
}
function canShowContentTools() {
// 비로그인 사용자는 편집/역사/공유 버튼을 숨김
if (!mw.config.get('wgUserName')) {
return false;
}
// MediaWiki가 현재 문서를 편집 가능하지 않다고 판단하면 숨김
var isEditable = mw.config.get('wgIsProbablyEditable');
if (isEditable === false) {
return false;
}
return true;
}
function moveCatlinksToBottom() {
var main = $('.liberty-content-main');
var parserOutput = $('.liberty-content-main .mw-parser-output').first();
var catlinks = $('.catlinks');
if (!main.length || !catlinks.length) return;
catlinks.each(function () {
var cat = $(this);
if (parserOutput.length) {
cat.appendTo(parserOutput);
} else {
cat.appendTo(main);
}
});
}
// 대문 스타일
function initCategoryNavIfAvailable(root) {
/*
CategoryNav.js는 대문 카테고리 네비를 SVG로 생성한다.
Common.js가 SPA로 본문을 갈아끼운 뒤에는 MediaWiki 원래 페이지 로드와 달리
CategoryNav.js의 초기 DOMContentLoaded만으로는 새 mount를 다시 잡지 못할 수 있다.
CategoryNav.js 자체도 mw.hook('wikipage.content')를 듣지만, 로드 순서와 SPA 타이밍이
엇갈릴 수 있으므로 Common.js 쪽에서도 존재 여부를 확인한 뒤 한 번 더 호출한다.
이 함수는 CategoryNav.js가 아직 로드되지 않았으면 아무 것도 하지 않는다.
*/
if (
window.CLBI &&
window.CLBI.categoryNav &&
typeof window.CLBI.categoryNav.init === 'function'
) {
window.CLBI.categoryNav.init(root || document);
}
}
function removeLegacyMainPageHero() {
/*
기존 대문 전용 레거시 요소 정리
-----------------------------------------
이전 대문 구조에서는 Common.js가 본문 바깥에 #clbi-main-logo를 직접 삽입하고,
본문 안의 #clbi-main-crt-hero를 #clbi-main-crt-hero-wrap으로 감싸서
.liberty-content-main 위쪽으로 재배치했다.
새 대문은 본문 내부의 .main-portal이 로고, 알림, 카테고리 네비, 이미지 피드,
방명록, 상태 패널을 모두 담당한다. 따라서 Common.js가 별도 로고나 CRT 래퍼를
삽입하면 새 로고/콘텐츠와 중복된다.
여기서는 JS가 만들던 바깥 로고와 CRT 래퍼를 제거하고, 예전 대문 원본이나
캐시된 렌더 결과에 남아 있을 수 있는 #clbi-main-crt-hero도 제거한다.
*/
$('#clbi-main-logo').remove();
$('#clbi-main-crt-hero-wrap').remove();
$('#clbi-main-crt-hero').remove();
}
function applyMainPageStyle() {
var specialPage = mw.config.get('wgCanonicalSpecialPageName');
if (specialPage === 'Preferences') return;
var pageName = normalizePageName(mw.config.get('wgPageName'));
var isMainPage = (pageName === '대문');
var isScreenDoc = ($('.screen-header').length > 0);
var hideTools = (isMainPage || !canShowContentTools());
// 모든 문서에서 분류 바를 본문 컨테이너 아래로 이동
moveCatlinksToBottom();
if (isMainPage) {
$('.liberty-content-header').css('display', 'none');
$('.mw-page-title-main').addClass('clbi-hide');
$('.catlinks').css('display', 'none');
$('.liberty-content-main').css('border-radius', '5px');
// 새 대문은 .main-portal 본문 구조가 로고/히어로를 담당한다.
// Common.js의 구식 바깥 로고/CRT 재배치 루틴은 사용하지 않는다.
removeLegacyMainPageHero();
$('#clbi-tools-box').remove();
$('.content-tools').css('display', 'none');
$('.liberty-content').addClass('content-tools-hidden');
initCategoryNavIfAvailable(document);
} else if (isScreenDoc) {
$('.liberty-content-header').css('display', 'none');
$('.mw-page-title-main').addClass('clbi-hide');
$('.catlinks').css('display', '');
$('.liberty-content-main').css('border-radius', '5px');
$('#clbi-main-logo').remove();
$('#clbi-main-crt-hero-wrap').remove();
if ($('#clbi-tools-box').length === 0 && canShowContentTools()) {
var $toolsBox = $('<div id="clbi-tools-box" class="clbi-left-box"></div>');
var $toolsTitle = $('<div class="clbi-left-title"><span class="clbi-icon" style="--icon:var(--ic-ui-003)"></span> 관리</div>');
var $toolsContent = $('<div class="clbi-left-content"></div>');
$toolsContent.append($('.content-tools .btn-group').clone(true));
$toolsBox.append($toolsTitle).append($toolsContent);
$('#clbi-left-sidebar').append($toolsBox);
}
$('.content-tools').css('display', 'none');
$('.liberty-content').addClass('content-tools-hidden');
} else {
$('.liberty-content-header').css('display', '');
$('.mw-page-title-main').removeClass('clbi-hide');
$('.catlinks').css('display', '');
$('.liberty-content-main').css('border-radius', '5px');
$('#clbi-main-logo').remove();
$('#clbi-main-crt-hero-wrap').remove();
$('#clbi-tools-box').remove();
}
if (hideTools) {
$('.content-tools').css('display', 'none');
$('.liberty-content').addClass('content-tools-hidden');
} else if (!isScreenDoc) {
$('.content-tools').css('display', '');
$('.liberty-content').removeClass('content-tools-hidden');
}
updateSidebar();
}
// 본문 기본 목차 제거
function removeNativeTocFromContent() {
$('.liberty-content-main #toc, .liberty-content-main .toc').remove();
}
// 왼쪽 목차: MediaWiki 문단 ID 가져오기
function getHeadingId(heading) {
if (heading.id) {
return heading.id;
}
var headline = heading.querySelector('.mw-headline[id]');
if (headline && headline.id) {
return headline.id;
}
return '';
}
// 왼쪽 목차: MediaWiki 문단 제목 텍스트 가져오기
function getHeadingText(heading) {
var headline = heading.querySelector('.mw-headline');
var source = headline || heading;
var clone = source.cloneNode(true);
$(clone).find('.mw-editsection, .mw-editsection-bracket, .mw-editsection-divider').remove();
return (clone.textContent || '')
.replace(/\s+/g, ' ')
.trim();
}
// 왼쪽 목차: 긴 제목에 자동 스크롤 적용
function initTocTitleScroll(root) {
var $items = root
? $(root).find('.toc-scroll-text')
: $('#side-toc-box .toc-scroll-text');
$items.each(function () {
var $text = $(this);
var $wrap = $text.closest('.toc-scroll-wrap');
if (!$wrap.length) return;
var wrapW = Math.floor($wrap.width());
var textW = Math.ceil(this.scrollWidth);
// 왼쪽 목차: 레이아웃 계산이 끝나지 않았으면 이번 실행에서는 건드리지 않는다.
if (!wrapW || !textW) return;
if (textW <= wrapW + 12) {
// 왼쪽 목차: 칸을 넘지 않는 제목은 전체 텍스트를 그대로 보여준다.
$wrap.removeClass('is-scrolling');
if ($text.data('toc-scroll-enabled')) {
$text.css({
animation: '',
'animation-delay': '',
'--scroll-dist': ''
});
$text.removeData('toc-scroll-enabled');
$text.removeData('toc-scroll-key');
}
return;
}
var scrollDist = '-' + (textW - wrapW + 10) + 'px';
var duration = Math.max(7, textW / 38) * 1.25;
var scrollKey = scrollDist + '|' + duration;
// 왼쪽 목차: 긴 제목에는 오른쪽 페이드와 스크롤을 적용한다.
$wrap.addClass('is-scrolling');
// 왼쪽 목차: 같은 값으로 이미 적용된 애니메이션은 다시 초기화하지 않는다.
if ($text.data('toc-scroll-key') === scrollKey) {
return;
}
$text.data('toc-scroll-enabled', true);
$text.data('toc-scroll-key', scrollKey);
$text.css({
// 왼쪽 목차: 페이지 진입 직후에는 잠시 읽을 시간을 준 뒤 흐르게 한다.
animation: 'toc-scroll-blink-reset ' + duration + 's linear infinite',
'animation-delay': '1s',
'--scroll-dist': scrollDist
});
});
}
// 목차를 왼쪽 사이드바에 새로 생성
function moveTocToLeftSidebar() {
// 왼쪽 목차: MediaWiki가 만든 원래 목차는 본문에서 제거한다.
removeNativeTocFromContent();
var leftSidebar = document.getElementById('clbi-left-sidebar');
if (!leftSidebar) return;
var content =
document.querySelector('.liberty-content-main .mw-parser-output') ||
document.querySelector('.liberty-content-main');
if (!content) return;
var headings = Array.prototype.slice.call(
content.querySelectorAll('h2, h3')
).filter(function (heading) {
if (heading.closest('#toc, .toc, #side-toc-box')) return false;
var id = getHeadingId(heading);
var text = getHeadingText(heading);
if (!id || !text) return false;
return true;
});
var tocKey = headings.map(function (heading) {
return getHeadingId(heading) + '|' + getHeadingText(heading);
}).join('||');
var existingBox = document.getElementById('side-toc-box');
// 왼쪽 목차: 같은 문서에서 같은 목차를 이미 만들었다면 다시 지우고 만들지 않는다.
if (existingBox && existingBox.getAttribute('data-toc-key') === tocKey) {
initTocTitleScroll(existingBox);
return;
}
if (existingBox) {
existingBox.remove();
}
if (!headings.length) return;
var tocBox = document.createElement('div');
tocBox.className = 'clbi-left-box';
tocBox.id = 'side-toc-box';
tocBox.setAttribute('data-toc-key', tocKey);
var title = document.createElement('div');
title.className = 'clbi-left-title';
// 왼쪽 목차: 박스 제목은 Lang.js의 현재 UI 언어를 따른다.
var currentLang = getCurrentLang();
var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
var tocTitleText = (t && t.toc) ? t.toc : '목차';
title.innerHTML =
'<span class="clbi-icon" style="--icon:var(--ic-ui-002)"></span> ' + tocTitleText;
var body = document.createElement('div');
body.className = 'clbi-left-content toc-sidebar-content';
var list = document.createElement('ul');
list.className = 'generated-toc';
headings.forEach(function (heading) {
var id = getHeadingId(heading);
var text = getHeadingText(heading);
var level = heading.tagName.toLowerCase() === 'h3' ? 3 : 2;
var item = document.createElement('li');
item.className = 'toc-level-' + level;
var link = document.createElement('a');
link.setAttribute('href', '#' + id);
// 왼쪽 목차: 긴 제목 스크롤을 위해 텍스트를 별도 span으로 감싼다.
var textWrap = document.createElement('span');
textWrap.className = 'toc-scroll-wrap';
var textSpan = document.createElement('span');
textSpan.className = 'toc-scroll-text';
textSpan.textContent = text;
textWrap.appendChild(textSpan);
link.appendChild(textWrap);
item.appendChild(link);
list.appendChild(item);
});
body.appendChild(list);
tocBox.appendChild(title);
tocBox.appendChild(body);
// 왼쪽 목차: 사용자 박스 바로 아래, 뉴스 박스 위에 배치한다.
var userBox = document.getElementById('clbi-user-box');
var newsBox = document.getElementById('clbi-left-news-box');
if (userBox && userBox.parentNode === leftSidebar) {
if (userBox.nextSibling) {
leftSidebar.insertBefore(tocBox, userBox.nextSibling);
} else {
leftSidebar.appendChild(tocBox);
}
} else if (newsBox && newsBox.parentNode === leftSidebar) {
leftSidebar.insertBefore(tocBox, newsBox);
} else {
leftSidebar.appendChild(tocBox);
}
// 왼쪽 목차: DOM에 붙은 뒤 실제 폭을 계산해야 하므로 비동기 재측정한다.
requestAnimationFrame(function () {
initTocTitleScroll(tocBox);
});
setTimeout(function () {
initTocTitleScroll(tocBox);
}, 300);
}
// 초기화 함수
function initSidebars() {
var userName = mw.config.get('wgUserName');
var scriptPath = mw.config.get('wgScriptPath') || '';
$('#clbi-left-sidebar, #clbi-right-sidebar, #clbi-user-box, #clbi-left-news-box, #clbi-left-search-box, #clbi-left-links-box').remove();
var leftBoxes =
'<div id="clbi-left-sidebar">' +
'<div id="clbi-user-box" class="clbi-left-box">' +
'<div class="clbi-left-title" id="clbi-title-user">' +
'<span class="clbi-icon" style="--icon:var(--ic-ui-005)"></span> 사용자' +
'</div>' +
'<div class="clbi-left-content">' +
(userName ?
'<div id="clbi-user-logged-in">' +
'<div id="clbi-user-avatar-wrap">' +
'<img id="clbi-user-avatar" src="/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(userName) + '.png">' +
'<div id="clbi-user-name">' + userName + '</div>' +
'</div>' +
'<div class="clbi-user-btn-grid">' +
'<a class="clbi-user-btn" id="clbi-btn-contribution" href="/index.php/특수:기여/' + encodeURIComponent(userName) + '">기여</a>' +
'<a class="clbi-user-btn" id="clbi-btn-watchlist" href="/index.php/특수:주시문서목록">주시문서</a>' +
'<a class="clbi-user-btn" id="clbi-btn-preferences" href="/index.php/특수:환경설정">설정</a>' +
'<a class="clbi-user-btn" id="clbi-btn-logout" href="/index.php/특수:로그아웃">로그아웃</a>' +
'</div>' +
'</div>'
:
'<div id="clbi-user-anon">' +
'<a class="clbi-user-btn" id="clbi-btn-login" href="/index.php/특수:로그인">로그인</a>' +
'</div>'
) +
'</div>' +
'</div>' +
'<div id="clbi-left-news-box" class="clbi-left-box">' +
'<div class="clbi-left-title" id="clbi-title-left-news">' +
'<span class="clbi-icon" style="--icon:var(--ic-ui-010)"></span> 뉴스' +
'</div>' +
'<div class="clbi-left-content">' +
'<div class="clbi-left-news-section" id="clbi-left-news-changelog">' +
'<a id="clbi-left-news-changelog-main" class="clbi-left-news-main" href="/index.php/프로젝트:체인지로그">체인지로그</a>' +
'</div>' +
'<div class="clbi-left-news-section" id="clbi-left-news-recent">' +
'<div id="clbi-left-news-recent-title">RECENT CHANGES</div>' +
'<div id="clbi-left-news-recent-list"></div>' +
'</div>' +
'</div>' +
'</div>' +
'<div id="clbi-left-search-box" class="clbi-left-box">' +
'<div class="clbi-left-title" id="clbi-title-search">' +
'<span class="clbi-icon" style="--icon:var(--ic-ui-002)"></span> <a href="/index.php/특수:검색">검색</a>' +
'</div>' +
'<div class="clbi-left-content">' +
'<div id="clbi-search-form">' +
'<input type="text" id="clbi-search-input" placeholder="검색...">' +
'<button id="clbi-search-btn">›</button>' +
'</div>' +
'</div>' +
'</div>' +
'<div id="clbi-left-links-box" class="clbi-left-box">' +
'<div class="clbi-left-title" id="clbi-title-links">' +
'<span class="clbi-icon" style="--icon:var(--ic-ui-001)"></span> <span id="clbi-title-links-label">링크</span>' +
'</div>' +
'<div class="clbi-left-content">' +
'<a class="clbi-link-btn" href="/index.php/특수:최근바뀜">최근 바뀜</a>' +
'<a class="clbi-link-btn" href="/index.php/특수:임의문서">임의 문서</a>' +
'<a class="clbi-link-btn" href="/index.php/특수:분류">분류</a>' +
'</div>' +
'</div>' +
'</div>';
$(function() {
loadLangScript(function() {
setTimeout(function() {
initSidebars();
}, 100);
});
});
// SPA 네비게이션
function shouldSkip(url) {
return url.match(/action=edit|action=submit|action=history|action=delete|action=protect|action=purge|특수:로그인|특수:로그아웃|Special:UserLogin|Special:UserLogout|특수:사용자정보|특수:비밀번호바꾸기|uselang=/);
}
$(function() {
if (window._spaInitialized) return;
window._spaInitialized = true;
function isInternal(url) {
var a = document.createElement('a');
a.href = url;
return a.hostname === window.location.hostname;
}
function loadPage(url) {
fetch(url)
.then(function(res) {
return res.text();
})
.then(function(html) {
var parser = new DOMParser();
var doc = parser.parseFromString(html, 'text/html');
var scripts = doc.querySelectorAll('script');
for (var i = 0; i < scripts.length; i++) {
var src = scripts[i].textContent;
if (src.indexOf('wgNamespaceNumber') !== -1) {
var match = src.match(/"wgNamespaceNumber":(-?\d+)/);
if (match) mw.config.set('wgNamespaceNumber', parseInt(match[1], 10));
var matchTitle = src.match(/"wgTitle":"([^"]+)"/);
if (matchTitle) mw.config.set('wgTitle', matchTitle[1]);
var matchPage = src.match(/"wgPageName":"([^"]+)"/);
if (matchPage) mw.config.set('wgPageName', matchPage[1]);
var matchSpecial = src.match(/"wgCanonicalSpecialPageName":"([^"]+)"/);
if (matchSpecial) {
mw.config.set('wgCanonicalSpecialPageName', matchSpecial[1]);
} else {
mw.config.set('wgCanonicalSpecialPageName', false);
}
break;
}
}
var newContent = doc.querySelector('.liberty-content-main');
var newTitle = doc.querySelector('.mw-page-title-main');
var newHead = doc.querySelector('title');
var newHeader = doc.querySelector('.liberty-content-header');
if (newContent) {
$('#side-toc-box').remove();
$('.liberty-content-main').html(newContent.innerHTML);
$('.profile-card').remove();
$('body').removeClass('page-loading');
}
if (newTitle) {
$('.mw-page-title-main').html(newTitle.innerHTML);
}
if (newHead) {
document.title = newHead.textContent;
}
if (newHeader) {
$('.liberty-content-header').html(newHeader.innerHTML);
}
window.scrollTo(0, 0);
$('#clbi-lang-list').hide();
$('#clbi-lang-current').css('margin-bottom', '0');
mw.hook('wikipage.content').fire($('.liberty-content-main'));
applyMainPageStyle();
initCategoryNavIfAvailable(document);
$('#side-toc-box').remove();
setTimeout(moveTocToLeftSidebar, 100);
setTimeout(moveTocToLeftSidebar, 500);
setTimeout(moveTocToLeftSidebar, 1200);
mw.loader.using(['mediawiki.api']).then(function() {
initProfile();
moveTocToLeftSidebar();
});
});
}
// 목차 링크는 전용 처리
$(document).on('click', '#side-toc-box a, #toc a, .toc a', function(e) {
var href = $(this).attr('href');
if (!href || href.charAt(0) !== '#') return;
var rawId = href.slice(1);
if (!rawId) return;
var decodedId = rawId;
try {
decodedId = decodeURIComponent(rawId);
} catch (err) {
decodedId = rawId;
}
var target = document.getElementById(decodedId);
if (!target && window.CSS && CSS.escape) {
target = document.querySelector('#' + CSS.escape(decodedId));
}
if (!target) return;
e.preventDefault();
e.stopPropagation();
var scrollTarget = target.closest('h2, h3') || target;
scrollTarget.scrollIntoView({
behavior: 'auto',
block: 'start'
});
history.replaceState(null, '', '#' + rawId);
});
$(document).on('click', 'a', function(e) {
var href = $(this).attr('href');
if (!href) return;
// 목차 링크는 별도 핸들러에서 처리
if ($(this).closest('#side-toc-box, #toc, .toc').length) return;
// 단순 해시 링크는 SPA 가로채기 제외
if (href.startsWith('#')) return;
var link = document.createElement('a');
link.href = href;
var samePath = decodeURIComponent(link.pathname) === decodeURIComponent(window.location.pathname);
var sameSearch = (link.search || '') === (window.location.search || '');
if (link.hash && samePath && sameSearch) return;
var currentBase = window.location.href.split('#')[0];
var targetBase = link.href.split('#')[0];
if (link.hash && currentBase === targetBase) return;
if (!isInternal(href)) return;
if (shouldSkip(href)) return;
e.preventDefault();
playStaticSound();
$('body').addClass('page-loading');
history.pushState(null, '', href);
loadPage(href);
});
window.addEventListener('popstate', function() {
loadPage(window.location.href);
});
});
// 시간 계산 함수
function timeAgo(timestamp) {
var now = new Date();
var date = new Date(timestamp);
var diff = Math.floor((now - date) / 1000);
if (diff < 60) return diff + '초 전';
if (diff < 3600) return Math.floor(diff / 60) + '분 전';
if (diff < 86400) return Math.floor(diff / 3600) + '시간 전';
return Math.floor(diff / 86400) + '일 전';
}
// 펼접 토글
function getFoldTexts() {
var lang = getCurrentLang();
return (window.LANG && window.LANG[lang])
? window.LANG[lang]
: (window.LANG ? window.LANG.ko : { expand: '펼치기', collapse: '접기' });
}
function refreshOpenAncestors($start) {
$start.parents('[id^="collapsible"]').each(function () {
var $parent = $(this);
if (!$parent.hasClass('folding-open')) return;
// 이미 fully open 상태면 굳이 다시 잠그지 않음
if ($parent.data('fold-state') === 'open') {
return;
}
$parent.css('max-height', this.scrollHeight + 'px');
});
}
function bindInnerResizeUpdates($target) {
// 이미지 늦게 로드될 때 높이 갱신
$target.find('img').off('.foldimg').on('load.foldimg', function () {
if ($target.hasClass('folding-open')) {
if ($target.data('fold-state') !== 'open') {
$target.css('max-height', $target[0].scrollHeight + 'px');
}
refreshOpenAncestors($target);
}
});
}
function openFold($target, $btn) {
var t = getFoldTexts();
$target.data('fold-state', 'opening');
$target.addClass('folding-open');
// 열린 뒤 자연 확장 가능하게 만들기 위해 먼저 px로 열기
$target.css('max-height', '0px');
$target[0].offsetHeight;
$target.css('max-height', $target[0].scrollHeight + 'px');
$btn.text(t.collapse);
bindInnerResizeUpdates($target);
// 바깥 펼접 즉시 갱신
refreshOpenAncestors($target);
// 전환 끝나면 none으로 풀어서 중첩 펼접/동적 내용 증가를 자연스럽게 허용
$target.off('transitionend.foldopen').on('transitionend.foldopen', function (e) {
if (e.target !== this) return;
if (!$target.hasClass('folding-open')) return;
$target.css('max-height', 'none');
$target.data('fold-state', 'open');
refreshOpenAncestors($target);
});
// 늦게 렌더되는 콘텐츠 대응
requestAnimationFrame(function () {
if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
$target.css('max-height', $target[0].scrollHeight + 'px');
refreshOpenAncestors($target);
}
});
setTimeout(function () {
if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
$target.css('max-height', $target[0].scrollHeight + 'px');
refreshOpenAncestors($target);
}
}, 80);
setTimeout(function () {
if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
$target.css('max-height', $target[0].scrollHeight + 'px');
refreshOpenAncestors($target);
}
}, 220);
}
function closeFold($target, $btn) {
var t = getFoldTexts();
// none 상태에서 닫으면 transition이 안 되므로 실제 높이로 고정
if ($target.css('max-height') === 'none' || $target.data('fold-state') === 'open') {
$target.css('max-height', $target[0].scrollHeight + 'px');
} else {
$target.css('max-height', $target[0].scrollHeight + 'px');
}
$target.data('fold-state', 'closing');
$target[0].offsetHeight;
$target.css('max-height', '0px');
$target.removeClass('folding-open');
$btn.text(t.expand);
refreshOpenAncestors($target);
setTimeout(function () {
refreshOpenAncestors($target);
$target.data('fold-state', 'closed');
}, 250);
}
$(function () {
$(document)
.off('click.clbiToggle')
.on('click.clbiToggle', '.toggleBtn', function () {
var $btn = $(this);
var targetId = $btn.data('target');
var $target = $('#' + targetId);
if (!$target.length) return;
var scrollY = window.scrollY;
if ($target.hasClass('folding-open')) {
closeFold($target, $btn);
} else {
openFold($target, $btn);
}
requestAnimationFrame(function () {
window.scrollTo(window.scrollX, scrollY);
});
});
});
// ========== 사용자 설정 / 프로필 ==========
function initProfile() {
var pageName = normalizePageName(mw.config.get('wgPageName'));
var namespace = mw.config.get('wgNamespaceNumber');
if (namespace !== 2) {
$('.profile-card').remove();
return;
}
var username = pageName.replace(/^사용자:/, '').replace(/^User:/, '');
if (!username) return;
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
api.get({
action: 'query',
list: 'users',
ususers: username,
usprop: 'editcount|registration|groups',
format: 'json'
}).then(function (data) {
var users = data && data.query ? data.query.users : [];
var user = users && users.length ? users[0] : null;
var contentEl = document.getElementById('mw-content-text');
if (!contentEl) return;
var pageContent = contentEl.querySelector('.mw-parser-output') || contentEl;
injectProfileCard(username, user, pageContent);
});
});
}
function injectProfileCard(username, userData, container) {
var isOwnPage = mw.config.get('wgUserName') === username;
var editCount = (userData && userData.editcount) ? userData.editcount : 0;
var editBtn = isOwnPage
? '<a href="/index.php/특수:사용자정보" class="profile-edit-btn">프로필 수정</a>'
: '';
var card = document.createElement('div');
card.className = 'profile-card';
card.innerHTML =
'<div class="profile-header">' +
'<div class="profile-avatar">' +
'<img src="/index.php?title=특수:Redirect/file/Pfp-' + username + '.png&width=120"' +
' onerror="this.src=\'/index.php?title=특수:Redirect/file/Pfp-default.png&width=120\'"' +
' alt="' + username + '">' +
'</div>' +
'<div class="profile-info">' +
'<h2 class="profile-username">' + username + '</h2>' +
'<div class="profile-name" data-field="name"></div>' +
'<div class="profile-role" data-field="role"></div>' +
'<div class="profile-discord" data-field="discord"></div>' +
'<div class="profile-bio" data-field="bio"></div>' +
'<div class="profile-badges" data-field="badges"></div>' +
'</div>' +
editBtn +
'</div>' +
'<div class="profile-stats">' +
'<div class="profile-stat">' +
'<span class="clbi-stat-value">' + editCount + '</span>' +
'<span class="clbi-stat-label">수정 횟수</span>' +
'</div>' +
'</div>';
$('.profile-card').remove();
container.insertBefore(card, container.firstChild);
loadProfileFields(username, card);
}
function loadProfileFields(username, card) {
var api = new mw.Api();
api.get({
action: 'userprofile',
user: username
}).then(function(data) {
var profile = data.userprofile;
updateProfileFields(card, {
name: profile.name || '',
discord: profile.discord || '',
role: profile.role || '',
bio: profile.bio || '',
badges: profile.badges || ''
});
}).fail(function() {
updateProfileFields(card, {
name: '',
discord: '',
role: '',
bio: '',
badges: ''
});
});
}
function updateProfileFields(card, data) {
var nameEl = card.querySelector('[data-field="name"]');
var roleEl = card.querySelector('[data-field="role"]');
var discordEl = card.querySelector('[data-field="discord"]');
var bioEl = card.querySelector('[data-field="bio"]');
var badgesEl = card.querySelector('[data-field="badges"]');
if (nameEl) nameEl.textContent = data.name || '';
if (roleEl) roleEl.textContent = data.role || '';
if (discordEl) discordEl.textContent = data.discord ? ('디스코드: ' + data.discord) : '';
if (bioEl) bioEl.textContent = data.bio || '';
if (badgesEl) {
if (data.badges) {
var badges = data.badges.split(',');
var html = '';
for (var i = 0; i < badges.length; i++) {
html += '<span class="clbi-badge">' + badges[i].trim() + '</span>';
}
badgesEl.innerHTML = html;
} else {
badgesEl.innerHTML = '';
}
}
}
// ========== 알림 시스템 ==========
function ensureNotificationPopup() {
if (document.getElementById('clbi-notification-popup')) return;
var popup = document.createElement('div');
popup.id = 'clbi-notification-popup';
popup.style.cssText =
'display:none;position:fixed;z-index:99999;width:320px;max-height:420px;' +
'background:#0a0909;border:2px solid #854369;border-radius:5px;' +
'box-shadow:0 0 0 1px #1a1a1a, 0 8px 24px rgba(0,0,0,0.55);overflow:hidden;';
popup.innerHTML =
'<div style="padding:10px 12px;border-bottom:2px solid #854369;background:linear-gradient(to bottom, #171114 0%, #0a0909 100%);color:#E2E2E2;font-size:13px;font-weight:700;display:flex;align-items:center;justify-content:space-between;gap:8px;">' +
'<span>알림</span>' +
'<button type="button" id="clbi-notification-readall" style="background:#171717;border:1px solid #854369;border-radius:6px;color:#E2E2E2;font-size:11px;font-weight:700;padding:4px 8px;cursor:pointer;">전체 읽음</button>' +
'</div>' +
'<div id="clbi-notification-list" style="max-height:320px;overflow-y:auto;padding:8px 0;color:#E2E2E2;font-size:12px;">불러오는 중...</div>' +
'<div style="padding:8px;border-top:1px solid #2a2a2a;background:#111;">' +
'<a href="/index.php?title=Special:Notifications" id="clbi-notification-more" style="display:block;width:100%;text-align:center;padding:8px 10px;border-radius:6px;background:#171717;border:1px solid #854369;color:#E2E2E2 !important;text-decoration:none !important;font-size:12px;font-weight:700;">더보기</a>' +
'</div>';
document.body.appendChild(popup);
}
function positionNotificationPopup() {
var btn = document.getElementById('clbi-notification-toggle');
var popup = document.getElementById('clbi-notification-popup');
if (!btn || !popup) return;
var rect = btn.getBoundingClientRect();
var top = rect.bottom + 2;
var left = rect.right - popup.offsetWidth;
if (left < 8) left = 8;
if (left + popup.offsetWidth > window.innerWidth - 8) {
left = window.innerWidth - popup.offsetWidth - 8;
}
if (top + popup.offsetHeight > window.innerHeight - 8) {
top = Math.max(8, window.innerHeight - popup.offsetHeight - 8);
}
popup.style.top = top + 'px';
popup.style.left = left + 'px';
}
function parseNotificationItemsFromHtml(html) {
var parser = new DOMParser();
var doc = parser.parseFromString(html, 'text/html');
var selectors = [
'.mw-echo-ui-notificationItemWidget',
'.mw-echo-ui-notificationsInboxWidgetRow',
'.echo-ui-notificationItemWidget',
'li[data-notification-id]',
'.mw-echo-notifications-list li'
];
var items = [];
for (var i = 0; i < selectors.length; i++) {
items = Array.prototype.slice.call(doc.querySelectorAll(selectors[i]));
if (items.length) break;
}
return items.slice(0, 5).map(function(item) {
var link = item.querySelector('a[href]');
var href = link ? link.getAttribute('href') : '/index.php?title=Special:Notifications';
var text = (item.textContent || '').replace(/\s+/g, ' ').trim();
var notificationId =
item.getAttribute('data-notification-id') ||
item.getAttribute('data-id') ||
item.getAttribute('data-notification') ||
'';
if (!notificationId) {
var anyWithId = item.querySelector('[data-notification-id], [data-id], [data-notification]');
if (anyWithId) {
notificationId =
anyWithId.getAttribute('data-notification-id') ||
anyWithId.getAttribute('data-id') ||
anyWithId.getAttribute('data-notification') ||
'';
}
}
if (href && href.indexOf('http') !== 0) {
href = href.charAt(0) === '/'
? href
: '/index.php' + (href.charAt(0) === '?' ? href : '/' + href);
}
return {
id: notificationId,
href: href,
text: text || '알림'
};
});
}
function renderNotificationPopup(items) {
var list = document.getElementById('clbi-notification-list');
var badge = document.getElementById('clbi-notification-badge');
if (!list) return;
if (!items || !items.length) {
list.innerHTML = '<div style="padding:14px 12px;color:#999;">표시할 알림이 없습니다.</div>';
if (badge) badge.style.display = 'none';
return;
}
var html = '';
for (var i = 0; i < items.length; i++) {
html +=
'<a href="' + items[i].href + '" class="clbi-notification-item" data-notification-id="' + (items[i].id || '') + '" style="display:block;padding:10px 12px;color:#E2E2E2 !important;text-decoration:none !important;border-bottom:1px solid #1f1f1f;line-height:1.5;">' +
items[i].text +
'</a>';
}
list.innerHTML = html;
if (badge) {
badge.textContent = items.length;
badge.style.display = 'block';
}
}
function loadNotificationsIntoPopup() {
var list = document.getElementById('clbi-notification-list');
if (list) {
list.innerHTML = '<div style="padding:14px 12px;color:#999;">불러오는 중...</div>';
}
fetch('/index.php?title=Special:Notifications', { credentials: 'same-origin' })
.then(function(res) {
return res.text();
})
.then(function(html) {
var items = parseNotificationItemsFromHtml(html);
renderNotificationPopup(items);
})
.catch(function(err) {
console.error(err);
if (list) {
list.innerHTML = '<div style="padding:14px 12px;color:#999;">알림을 불러오지 못했습니다.</div>';
}
});
}
function markAllNotificationsRead() {
return new mw.Api().postWithToken('csrf', {
action: 'echomarkread',
list: 'all'
});
}
function markNotificationReadById(notificationId) {
if (!notificationId) {
return $.Deferred().resolve().promise();
}
return new mw.Api().postWithToken('csrf', {
action: 'echomarkread',
list: notificationId
});
}
function initNotifications() {
var btn = document.getElementById('clbi-notification-toggle');
if (!btn) return;