편집 요약 없음 |
편집 요약 없음 태그: 되돌려진 기여 |
||
| 1번째 줄: | 1번째 줄: | ||
mw.loader.load('https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js'); | |||
/ | 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() { | |||
position: | $('body').prepend( | ||
'<div class="WW-bg" style="position:fixed;top:0;left:0;width:100%;height:100vh;"></div>' | |||
); | ); | ||
}); | |||
// 페이지 전환 사운드 | |||
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 updateLeftSidebarNationsImage() { | |||
var imageId = 'clbi-left-nations-image'; | |||
// SPA 이동 시 이전 문서에서 삽입했던 이미지를 먼저 제거한다. | |||
// 이 처리가 없으면 국가_및_조합에서 다른 문서로 이동했을 때 이미지가 남을 수 있다. | |||
$('#' + imageId).remove(); | |||
// 현재 문서명이 국가 및 조합일 때만 삽입한다. | |||
// wgPageName은 보통 국가_및_조합처럼 언더스코어를 포함하므로 normalizePageName으로 통일해서 비교한다. | |||
var pageName = normalizePageName(mw.config.get('wgPageName')); | |||
var targetPage = normalizePageName('국가_및_조합'); | |||
if (pageName !== targetPage) { | |||
return; | |||
} | |||
} | |||
var $leftSidebar = $('#clbi-left-sidebar'); | |||
. | if (!$leftSidebar.length) { | ||
return; | |||
} | } | ||
// 왼쪽 사이드바 기본 자식 순서: | |||
// 0 = 언어 섹션, 1 = 카테고리 섹션. | |||
. | // 이미지는 카테고리 섹션 바로 아래에 삽입한다. | ||
. | var $categoryBox = $leftSidebar.children('.clbi-left-box').eq(1); | ||
if (!$categoryBox.length) { | |||
return; | |||
} | |||
. | // 컨테이너나 테두리를 추가하지 않고 img 요소만 넣는다. | ||
// 실제 파일을 바꾸려면 Redirect/file 뒤 파일명만 교체하면 된다. | |||
$categoryBox.after( | |||
'<img id="' + imageId + '" ' + | |||
'class="clbi-left-page-image" ' + | |||
'src="/index.php?title=특수:Redirect/file/img-nations-sidebar-001.png" ' + | |||
'alt="" loading="lazy">' | |||
); | |||
} | } | ||
// 사이드바 업데이트 | |||
function updateSidebar() { | |||
if (!window.LANG || !window.LANG_NAMES || !window.CAT_LINKS) { | |||
setTimeout(updateSidebar, 100); | |||
return; | |||
} | |||
} | |||
. | var langData = document.getElementById('clbi-lang-data'); | ||
var currentLang = getCurrentLang(); | |||
var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko; | |||
var names = (window.LANG_NAMES && window.LANG_NAMES[currentLang]) ? window.LANG_NAMES[currentLang] : window.LANG_NAMES.ko; | |||
var cl = (window.CAT_LINKS && window.CAT_LINKS[currentLang]) ? window.CAT_LINKS[currentLang] : window.CAT_LINKS.ko; | |||
var allLangs = langData ? { | |||
ko: langData.getAttribute('data-ko') || '', | |||
en: langData.getAttribute('data-en') || '', | |||
zh: langData.getAttribute('data-zh') || '', | |||
ja: langData.getAttribute('data-ja') || '' | |||
} : { | |||
ko: '', | |||
en: '', | |||
zh: '', | |||
ja: '' | |||
}; | |||
} | |||
$('#clbi-lang-current').text(names[currentLang]); | |||
var langHtml = ''; | |||
$.each(['ko', 'en', 'zh', 'ja'], function(i, lang) { | |||
if (lang === currentLang) return; | |||
var target = allLangs[lang]; | |||
if (target) { | |||
langHtml += '<div class="clbi-lang-link"><a href="' + buildWikiPath(target) + '?uselang=' + lang + '">' + names[lang] + '</a></div>'; | |||
} else { | |||
langHtml += '<div class="clbi-lang-wip">' + names[lang] + '</div>'; | |||
} | |||
}); | |||
$('#clbi-lang-list').html(langHtml); | |||
$('#clbi-title-language').text(t.language); | |||
$('#clbi-title-categories').text(t.categories); | |||
$('#clbi-title-links').text(t.links); | |||
$('#clbi-title-search a').text(t.search); | |||
$('#clbi-search-input').attr('placeholder', t.search + '...'); | |||
$('#clbi-title-recent a').text(t.recentChanges); | |||
$('#clbi-title-guide').text(t.guide); | |||
$('#clbi-guide-link').text(t.getStarted); | |||
$('#clbi-title-playlist').text(t.playlist); | |||
$('#clbi-cat-main a').attr('href', buildWikiPath(cl.main)); | |||
$('#clbi-cat-main .clbi-cat-label').text(t.mainMenu); | |||
$('#clbi-cat-nations a').attr('href', buildWikiPath(cl.nations)); | |||
$('#clbi-cat-nations .clbi-cat-label').text(t.nations); | |||
$('#clbi-cat-corporations a').attr('href', buildWikiPath(cl.corporations)); | |||
$('#clbi-cat-corporations .clbi-cat-label').text(t.corporations); | |||
$('#clbi-cat-military a').attr('href', buildWikiPath(cl.military)); | |||
$('#clbi-cat-military .clbi-cat-label').text(t.military); | |||
$('#clbi-cat-history a').attr('href', buildWikiPath(cl.history)); | |||
$('#clbi-cat-history .clbi-cat-label').text(t.history); | |||
$('#clbi-cat-personnel a').attr('href', buildWikiPath(cl.personnel)); | |||
$('#clbi-cat-personnel .clbi-cat-label').text(t.personnel); | |||
$('#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-cat-btn').removeClass('clbi-cat-active'); | |||
$.each(['main', 'nations', 'corporations', 'military', 'history', 'personnel'], function(i, key) { | |||
if (cl[key] && pageName === normalizePageName(cl[key])) { | |||
$('#clbi-cat-' + key).addClass('clbi-cat-active'); | |||
} | } | ||
}); | |||
$('.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; | |||
} | } | ||
var relevantEditable = mw.config.get('wgRelevantPageIsProbablyEditable'); | |||
if (relevantEditable === 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 applyMainPageStyle() { | |||
var specialPage = mw.config.get('wgCanonicalSpecialPageName'); | |||
if (specialPage === 'Preferences') return; | |||
var pageName = normalizePageName(mw.config.get('wgPageName')); | |||
var hideTools = (pageName === '대문' || !canShowContentTools()); | |||
// 모든 문서에서 분류 바를 본문 컨테이너 아래로 이동 | |||
moveCatlinksToBottom(); | |||
if (pageName === '대문') { | |||
$('.liberty-content-header').css('display', 'none'); | |||
$('.mw-page-title-main').addClass('clbi-hide'); | |||
$('.catlinks').css('display', 'none'); | |||
$('.liberty-content-main').css('border-radius', '5px 5px 0 0'); | |||
if ($('#clbi-main-logo').length === 0) { | |||
$('.liberty-content').prepend( | |||
'<div id="clbi-main-logo" style="text-align:center;padding:10px 0;">' + | |||
'<img src="/index.php?title=특수:Redirect/file/Img-clbi-001.png" style="width:900px;height:auto;">' + | |||
'</div>' | |||
); | |||
} | |||
if ($('#clbi-main-crt-hero').length && $('#clbi-main-crt-hero-wrap').length === 0) { | |||
var heroWrap = $('<div id="clbi-main-crt-hero-wrap"></div>'); | |||
if ($('#clbi-main-logo').length) { | |||
heroWrap.insertAfter('#clbi-main-logo'); | |||
} else { | |||
heroWrap.insertBefore('.liberty-content-main'); | |||
} | |||
$('#clbi-main-crt-hero').appendTo('#clbi-main-crt-hero-wrap'); | |||
} | |||
} else if ($('.screen-header').length > 0) { | |||
$('.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', ''); | |||
$('#clbi-main-logo').remove(); | |||
$('#clbi-main-crt-hero-wrap').remove(); | |||
$('#clbi-tools-box').remove(); | |||
} | } | ||
$('.liberty-content-header').css('display', ''); | |||
$('.mw-page-title-main').removeClass('clbi-hide'); | |||
$('.catlinks').css('display', ''); | |||
$('.liberty-content-main').css('border-radius', ''); | |||
$('#clbi-main-logo').remove(); | |||
$('#clbi-main-crt-hero-wrap').remove(); | |||
} | } | ||
if (hideTools) { | |||
$('.content-tools').css('display', 'none'); | |||
$('.liberty-content').addClass('content-tools-hidden'); | |||
} else { | |||
$('.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가 만든 원래 목차는 본문에서 제거한다. | |||
left | 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); | |||
leftSidebar.appendChild(tocBox); | |||
// 왼쪽 목차: DOM 배치가 끝난 뒤 긴 제목 스크롤 여부를 계산한다. | |||
requestAnimationFrame(function () { | |||
initTocTitleScroll(tocBox); | |||
setTimeout(function () { | |||
initTocTitleScroll(tocBox); | |||
}, 120); | |||
}); | |||
} | } | ||
// 초기화 함수 | |||
. | function initSidebars() { | ||
var header = $('.liberty-content-header'); | |||
var content = $('.liberty-content'); | |||
. | if (header.length && content.length) { | ||
header.prependTo(content); | |||
} | } | ||
#clbi- | if ($('#clbi-right-sidebar').length === 0) { | ||
var username = mw.config.get('wgUserName'); | |||
var isLoggedIn = username !== null; | |||
var avatarSrc = isLoggedIn | |||
? '/index.php?title=특수:Redirect/file/Pfp-' + username + '.png' | |||
: '/index.php?title=특수:Redirect/file/Pfp-default.png'; | |||
#clbi- | var userBox; | ||
if (isLoggedIn) { | |||
} | userBox = | ||
'<div class="clbi-right-box">' + | |||
'<div style="display:flex;flex-direction:column;align-items:center;padding:14px 14px 10px;background:linear-gradient(to bottom, #171114 0%, #0a0909 100%);">' + | |||
'<img src="' + avatarSrc + '" onerror="this.src=\'/index.php?title=특수:Redirect/file/Pfp-default.png\'" style="width:64px;height:64px;border-radius:5px;object-fit:cover;border:2px solid #854369;margin-bottom:8px;">' + | |||
'<div style="position:relative;width:100%;margin-top:2px;height:18px;line-height:18px;text-align:center;">' + | |||
'<button type="button" id="clbi-playlist-toggle" aria-label="플레이리스트" style="position:absolute;top:0;left:10px;background:none;border:none;padding:0;width:18px;height:18px;color:#E2E2E2;cursor:pointer;display:flex;align-items:center;justify-content:center;">' + | |||
'<span class="clbi-icon" style="--icon:var(--ic-ui-009);width:16px;height:16px;"></span>' + | |||
'</button>' + | |||
'<a href="/index.php/사용자:' + username + '" style="font-size:13px;font-weight:700;color:#E2E2E2 !important;text-decoration:none !important;line-height:18px;display:inline-block;vertical-align:middle;max-width:calc(100% - 58px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + username + '</a>' + | |||
'<button type="button" id="clbi-notification-toggle" aria-label="알림" style="position:absolute;top:0;right:10px;background:none;border:none;padding:0;width:18px;height:18px;color:#E2E2E2;cursor:pointer;display:flex;align-items:center;justify-content:center;">' + | |||
'<span class="clbi-icon" style="--icon:var(--ic-ui-009);width:16px;height:16px;"></span>' + | |||
'<span id="clbi-notification-badge" style="display:none;position:absolute;top:-6px;right:-8px;min-width:14px;height:14px;padding:0 3px;border-radius:999px;background:#854369;color:#fff;font-size:9px;line-height:14px;font-weight:700;text-align:center;"></span>' + | |||
'</button>' + | |||
'</div>' + | |||
'</div>' + | |||
'<div class="clbi-right-content" style="border-top:1px solid #2a2a2a;padding:8px;">' + | |||
'<a href="/index.php/특수:기여/' + username + '" class="clbi-user-btn" id="clbi-btn-contribution">기여</a>' + | |||
'<a href="/index.php/특수:주시문서목록" class="clbi-user-btn" id="clbi-btn-watchlist">주시문서 목록</a>' + | |||
'<a href="/index.php/특수:설정" class="clbi-user-btn" id="clbi-btn-preferences">설정</a>' + | |||
'<a href="/index.php?title=특수:로그아웃&returnto=대문" class="clbi-user-btn clbi-user-btn-logout" id="clbi-btn-logout">로그아웃</a>' + | |||
'</div>' + | |||
'</div>'; | |||
} else { | |||
userBox = | |||
'<div class="clbi-right-box">' + | |||
'<div style="display:flex;flex-direction:column;align-items:center;padding:14px 14px 10px;background:linear-gradient(to bottom, #171114 0%, #0a0909 100%);">' + | |||
'<img src="/index.php?title=특수:Redirect/file/Pfp-default.png" style="width:64px;height:64px;border-radius:5px;object-fit:cover;border:2px solid #854369;margin-bottom:8px;">' + | |||
'<span style="font-size:13px;font-weight:700;color:#E2E2E2;">Guest</span>' + | |||
'</div>' + | |||
'<div class="clbi-right-content" style="border-top:1px solid #2a2a2a;padding:8px;">' + | |||
'<a href="/index.php?title=특수:로그인&returnto=대문" class="clbi-user-btn" id="clbi-btn-login">로그인</a>' + | |||
'</div>' + | |||
'</div>'; | |||
} | |||
var recentBox = | |||
'<div class="clbi-right-box">' + | |||
'<div class="clbi-right-title" id="clbi-title-recent"><span class="clbi-icon" style="--icon:var(--ic-ui-006)"></span> <a href="/index.php/특수:최근바뀜" style="color:#E2E2E2 !important;text-decoration:none !important;">최근 변경</a></div>' + | |||
'<div class="clbi-right-content" id="clbi-recent-list">불러오는 중...</div>' + | |||
'</div>'; | |||
var linkBox = | |||
'<div class="clbi-right-box">' + | |||
'<div class="clbi-right-title" id="clbi-title-links"><span class="clbi-icon" style="--icon:var(--ic-ui-003)"></span> 링크</div>' + | |||
'<div class="clbi-right-content clbi-link-box">' + | |||
'<ul>' + | |||
'<li><a href="https://discord.gg/ctaeJ9d3Q5" target="_blank">Discord</a></li>' + | |||
'<li><a href="https://www.youtube.com/@nxdsxn" target="_blank">YouTube</a></li>' + | |||
'<li><a href="https://x.com/nxd_sxn" target="_blank">X</a></li>' + | |||
'</ul>' + | |||
'</div>' + | |||
'</div>'; | |||
var sidebar = | |||
userBox + | |||
'<div class="clbi-right-box">' + | |||
'<div class="clbi-right-title" id="clbi-title-search"><span class="clbi-icon" style="--icon:var(--ic-ui-005)"></span> <a href="/index.php/특수:검색" style="color:#E2E2E2 !important;text-decoration:none !important;">검색</a></div>' + | |||
'<div class="clbi-right-content">' + | |||
'<input id="clbi-search-input" type="text" placeholder="검색...">' + | |||
'<button id="clbi-search-btn">GO</button>' + | |||
'</div>' + | |||
'</div>' + | |||
recentBox + | |||
'<div class="clbi-right-box">' + | |||
'<div class="clbi-right-title" id="clbi-title-guide"><span class="clbi-icon" style="--icon:var(--ic-ui-007)"></span> 가이드</div>' + | |||
'<div class="clbi-right-content">' + | |||
'<a href="/index.php/CLBI_Wiki/KR_시작하기_(CLBI)" id="clbi-guide-link">시작하기</a>' + | |||
'</div>' + | |||
'</div>' + | |||
linkBox; | |||
$('.content-wrapper').append('<div id="clbi-right-sidebar">' + sidebar + '</div>'); | |||
$('#clbi-search-btn').click(function() { | |||
var query = $('#clbi-search-input').val(); | |||
if (query) { | |||
window.location.href = '/index.php?search=' + encodeURIComponent(query); | |||
} | |||
}); | |||
$('#clbi-search-input').keypress(function(e) { | |||
if (e.which === 13) $('#clbi-search-btn').click(); | |||
}); | |||
); | |||
. | $.getJSON( | ||
'/api.php?action=query&list=recentchanges&rclimit=5&rcprop=title|timestamp&format=json&rcnamespace=0&rctype=edit|new', | |||
function(data) { | |||
var items = data.query.recentchanges; | |||
var html = ''; | |||
. | $.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>'; | |||
}); | |||
$('#clbi-recent-list').html(html); | |||
. | $('#clbi-recent-list .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() { | |||
$('#clbi-recent-list').html('불러오기 실패'); | |||
}); | |||
} | } | ||
if ($('#clbi-left-sidebar').length === 0) { | |||
var leftSidebar = | |||
'<div id="clbi-left-sidebar">' + | |||
0 | '<div class="clbi-left-box">' + | ||
'<div class="clbi-left-title"><span class="clbi-icon" style="--icon:var(--ic-ui-001)"></span> <span id="clbi-title-language">언어</span></div>' + | |||
'<div class="clbi-left-content clbi-lang-box" id="clbi-lang-box">' + | |||
'<div class="clbi-lang-current" id="clbi-lang-current">한국어</div>' + | |||
'<div id="clbi-lang-list" style="display:none;"></div>' + | |||
'</div>' + | |||
'</div>' + | |||
'<div class="clbi-left-box">' + | |||
'<div class="clbi-left-title"><span class="clbi-icon" style="--icon:var(--ic-ui-002)"></span> <span id="clbi-title-categories">카테고리</span></div>' + | |||
'<div class="clbi-left-content clbi-cat-box">' + | |||
'<div class="clbi-cat-btn" id="clbi-cat-main"><a href="/index.php/대문"><div class="clbi-cat-text"><span class="clbi-cat-label">메인 메뉴</span><span class="clbi-cat-sub">MAIN MENU</span></div></a><div class="clbi-cat-arrow">▶</div></div>' + | |||
'<div class="clbi-cat-btn" id="clbi-cat-nations"><a href="/index.php/국가_및_조합"><div class="clbi-cat-text"><span class="clbi-cat-label">국가 및 조합</span><span class="clbi-cat-sub">NATIONS & FACTIONS</span></div></a><div class="clbi-cat-arrow">▶</div></div>' + | |||
'<div class="clbi-cat-btn" id="clbi-cat-corporations"><a href="/index.php/기업_및_공동체"><div class="clbi-cat-text"><span class="clbi-cat-label">기업 및 공동체</span><span class="clbi-cat-sub">CORPORATIONS & COMMUNITIES</span></div></a><div class="clbi-cat-arrow">▶</div></div>' + | |||
'<div class="clbi-cat-btn" id="clbi-cat-military"><a href="/index.php/군_정치집단"><div class="clbi-cat-text"><span class="clbi-cat-label">군, 정치집단</span><span class="clbi-cat-sub">MILITARY & POLITICS</span></div></a><div class="clbi-cat-arrow">▶</div></div>' + | |||
'<div class="clbi-cat-btn" id="clbi-cat-history"><a href="/index.php/역사적_사건"><div class="clbi-cat-text"><span class="clbi-cat-label">역사적 사건</span><span class="clbi-cat-sub">HISTORICAL EVENTS</span></div></a><div class="clbi-cat-arrow">▶</div></div>' + | |||
'<div class="clbi-cat-btn" id="clbi-cat-personnel"><a href="/index.php/인물"><div class="clbi-cat-text"><span class="clbi-cat-label">인물</span><span class="clbi-cat-sub">KEY PERSONNEL</span></div></a><div class="clbi-cat-arrow">▶</div></div>' + | |||
'</div>' + | |||
'</div>' + | |||
'</div>'; | |||
$('.content-wrapper').prepend(leftSidebar); | |||
// 왼쪽 사이드바가 처음 생성된 직후에도 한 번 실행한다. | |||
// 이후 언어 갱신과 SPA 이동 시에는 updateSidebar()에서 다시 실행된다. | |||
updateLeftSidebarNationsImage(); | |||
$(document).on('click', '#clbi-lang-current', function() { | |||
if ($('#clbi-lang-list').is(':hidden')) { | |||
$('#clbi-lang-current').css('margin-bottom', '8px'); | |||
$('#clbi-lang-list').slideDown(200); | |||
} else { | |||
$('#clbi-lang-list').slideUp(200, function() { | |||
$('#clbi-lang-current').css('margin-bottom', '0'); | |||
}); | |||
} | |||
}); | |||
} | } | ||
applyMainPageStyle(); | |||
$('#side-toc-box').remove(); | |||
mw.loader.using(['mediawiki.api']).then(function() { | |||
setTimeout(function() { | |||
initNotifications(); | |||
initPlaylistPopup(); | |||
initProfile(); | |||
moveTocToLeftSidebar(); | |||
}, 300); | |||
setTimeout(moveTocToLeftSidebar, 800); | |||
setTimeout(moveTocToLeftSidebar, 1500); | |||
}); | |||
} | |||
$(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(); | |||
$('#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); | |||
} | |||
window.scrollTo(0, scrollY); | |||
}); | |||
}); | |||
// ========== 프로필 시스템 ========== | |||
function initProfile() { | |||
$('.profile-card').remove(); | |||
var ns = mw.config.get('wgNamespaceNumber'); | |||
var title = mw.config.get('wgTitle'); | |||
if (ns === 2) { | |||
var profileUser = title.split('/')[0]; | |||
renderProfile(profileUser); | |||
} | } | ||
if (mw.config.get('wgCanonicalSpecialPageName') === '사용자정보') { | |||
initUserProfilePage(); | |||
} | } | ||
} | } | ||
function renderProfile(username) { | |||
var api = new mw.Api(); | |||
api.get({ | |||
action: 'query', | |||
list: 'users', | |||
ususers: username, | |||
usprop: 'editcount' | |||
}).then(function(data) { | |||
var user = data.query.users[0]; | |||
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:10px;' + | |||
'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; | |||
ensureNotificationPopup(); | |||
loadNotificationsIntoPopup(); | |||
$(document) | |||
.off('click.clbiNotificationToggle') | |||
.on('click.clbiNotificationToggle', '#clbi-notification-toggle', function(e) { | |||
e.preventDefault(); | |||
e.stopPropagation(); | |||
. | var popup = document.getElementById('clbi-notification-popup'); | ||
var playlistPopup = document.getElementById('clbi-playlist-popup'); | |||
if (!popup) return; | |||
if (playlistPopup) { | |||
playlistPopup.style.display = 'none'; | |||
} | |||
} | |||
. | if (popup.style.display === 'none' || popup.style.display === '') { | ||
popup.style.display = 'block'; | |||
positionNotificationPopup(); | |||
loadNotificationsIntoPopup(); | |||
} else { | |||
popup.style.display = 'none'; | |||
} | |||
}); | |||
. | $(document) | ||
. | .off('click.clbiNotificationOutside') | ||
.on('click.clbiNotificationOutside', function(e) { | |||
var popup = document.getElementById('clbi-notification-popup'); | |||
var toggle = document.getElementById('clbi-notification-toggle'); | |||
if (!popup || !toggle) return; | |||
. | if (!popup.contains(e.target) && !toggle.contains(e.target)) { | ||
popup.style.display = 'none'; | |||
} | |||
}); | |||
. | $(document) | ||
.off('click.clbiNotificationReadAll') | |||
.on('click.clbiNotificationReadAll', '#clbi-notification-readall', function(e) { | |||
e.preventDefault(); | |||
e.stopPropagation(); | |||
var button = this; | |||
button.disabled = true; | |||
button.textContent = '처리 중...'; | |||
. | markAllNotificationsRead() | ||
.then(function() { | |||
loadNotificationsIntoPopup(); | |||
. | }) | ||
.always(function() { | |||
button.disabled = false; | |||
button.textContent = '전체 읽음'; | |||
}); | |||
}); | |||
. | $(document) | ||
.off('click.clbiNotificationItem') | |||
.on('click.clbiNotificationItem', '.clbi-notification-item', function(e) { | |||
e.preventDefault(); | |||
e.stopPropagation(); | |||
. | var href = this.getAttribute('href'); | ||
var notificationId = this.getAttribute('data-notification-id') || ''; | |||
. | markNotificationReadById(notificationId).always(function() { | ||
loadNotificationsIntoPopup(); | |||
if (href) { | |||
window.location.href = href; | |||
} | |||
}); | |||
}); | |||
$(window) | |||
.off('resize.clbiNotification') | |||
.on('resize.clbiNotification', function() { | |||
var popup = document.getElementById('clbi-notification-popup'); | |||
if (popup && popup.style.display === 'block') { | |||
positionNotificationPopup(); | |||
} | |||
}); | |||
) | |||
} | } | ||
// ========== 알림 시스템 끝 ========== | |||
// ========== 플레이리스트 팝업 시스템 ========== | |||
function ensurePlaylistPopup() { | |||
if (document.getElementById('clbi-playlist-popup')) return; | |||
. | var popup = document.createElement('div'); | ||
popup.id = 'clbi-playlist-popup'; | |||
popup.style.cssText = | |||
display: | 'display:none;position:fixed;z-index:99999;width:690px;height:540px;' + | ||
'background:#0a0909;border:2px solid #854369;border-radius:10px;' + | |||
'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;">' + | |||
'<span>플레이리스트</span>' + | |||
'</div>' + | |||
'<div style="padding:0;background:#111;height:calc(100% - 42px);">' + | |||
'<iframe style="border:none;width:100%;height:100%;" src="https://open.spotify.com/embed/playlist/32l4ke6djdQn8LoBp1ipR9?utm_source=generator&theme=0" allowfullscreen allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>' + | |||
'</div>'; | |||
. | document.body.appendChild(popup); | ||
} | } | ||
function positionPlaylistPopup() { | |||
var btn = document.getElementById('clbi-playlist-toggle'); | |||
var popup = document.getElementById('clbi-playlist-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 initPlaylistPopup() { | |||
var btn = document.getElementById('clbi-playlist-toggle'); | |||
if (!btn) return; | |||
ensurePlaylistPopup(); | |||
. | $(document) | ||
.off('click.clbiPlaylistToggle') | |||
.on('click.clbiPlaylistToggle', '#clbi-playlist-toggle', function(e) { | |||
e.preventDefault(); | |||
e.stopPropagation(); | |||
. | var popup = document.getElementById('clbi-playlist-popup'); | ||
var notificationPopup = document.getElementById('clbi-notification-popup'); | |||
if (!popup) return; | |||
if (notificationPopup) { | |||
notificationPopup.style.display = 'none'; | |||
} | |||
} | |||
. | if (popup.style.display === 'none' || popup.style.display === '') { | ||
popup.style.display = 'block'; | |||
positionPlaylistPopup(); | |||
} else { | |||
popup.style.display = 'none'; | |||
} | |||
}); | |||
. | $(document) | ||
.off('click.clbiPlaylistOutside') | |||
.on('click.clbiPlaylistOutside', function(e) { | |||
var popup = document.getElementById('clbi-playlist-popup'); | |||
var toggle = document.getElementById('clbi-playlist-toggle'); | |||
if (!popup || !toggle) return; | |||
. | if (!popup.contains(e.target) && !toggle.contains(e.target)) { | ||
popup.style.display = 'none'; | |||
} | |||
} | }); | ||
. | $(window) | ||
.off('resize.clbiPlaylist') | |||
.on('resize.clbiPlaylist', function() { | |||
var popup = document.getElementById('clbi-playlist-popup'); | |||
if (popup && popup.style.display === 'block') { | |||
positionPlaylistPopup(); | |||
} | |||
}); | |||
} | } | ||
// ========== 플레이리스트 팝업 시스템 끝 ========== | |||
function initUserProfilePage() { | |||
var saveBtn = document.getElementById('pref-save'); | |||
if (!saveBtn) return; | |||
. | var adminEl = document.getElementById('pref-is-admin'); | ||
var isAdmin = adminEl && adminEl.getAttribute('data-admin') === '1'; | |||
var api = new mw.Api(); | |||
var selectedFile = null; | |||
var cropper = null; | |||
. | if (!document.getElementById('clbi-gallery-modal')) { | ||
var gModal = document.createElement('div'); | |||
gModal.id = 'clbi-gallery-modal'; | |||
gModal.style.cssText = | |||
'display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:99999;align-items:center;justify-content:center;'; | |||
. | gModal.innerHTML = | ||
'<div style="background:#1e1e1e;border:2px solid #854369;border-radius:12px;padding:24px;max-width:480px;width:90%;display:flex;flex-direction:column;gap:16px;">' + | |||
'<div style="display:flex;justify-content:space-between;align-items:center;">' + | |||
'<span style="font-size:14px;font-weight:700;color:#e2e2e2;">프로필 사진 선택</span>' + | |||
'<button type="button" id="clbi-gallery-close" style="background:none;border:none;color:#aaa;font-size:18px;cursor:pointer;">✕</button>' + | |||
'</div>' + | |||
'<button type="button" id="clbi-gallery-upload-btn" style="background:#2a2a2a;border:2px dashed #854369;border-radius:8px;padding:32px;color:#e2e2e2;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:8px;font-size:13px;width:100%;">' + | |||
'<span style="font-size:32px;">🖼️</span>새 사진 업로드' + | |||
'</button>' + | |||
'<div id="clbi-gallery-history-section" style="display:none;">' + | |||
'<div style="font-size:11px;color:#888;margin-bottom:8px;">이전 사진 — 클릭하면 바로 적용</div>' + | |||
'<div id="clbi-gallery-history" style="display:flex;gap:8px;flex-wrap:wrap;"></div>' + | |||
'</div>' + | |||
'</div>'; | |||
. | document.body.appendChild(gModal); | ||
} | |||
} | |||
. | if (!document.getElementById('clbi-crop-modal')) { | ||
var cModal = document.createElement('div'); | |||
cModal.id = 'clbi-crop-modal'; | |||
cModal.style.cssText = | |||
'display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:99999;align-items:center;justify-content:center;'; | |||
. | cModal.innerHTML = | ||
'<div style="background:#1e1e1e;border:2px solid #854369;border-radius:12px;padding:24px;max-width:500px;width:90%;display:flex;flex-direction:column;gap:16px;">' + | |||
'<div style="font-size:14px;font-weight:700;color:#e2e2e2;">사진 조정</div>' + | |||
'<div style="width:100%;max-height:380px;overflow:hidden;border-radius:8px;">' + | |||
'<img id="clbi-crop-image" style="max-width:100%;">' + | |||
'</div>' + | |||
'<div style="display:flex;gap:8px;justify-content:flex-end;">' + | |||
'<button type="button" id="clbi-crop-cancel" style="background:#2a2a2a;color:#e2e2e2;border:1px solid #444;padding:8px 16px;border-radius:6px;cursor:pointer;">취소</button>' + | |||
'<button type="button" id="clbi-crop-confirm" style="background:#854369;color:#fff;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;">확정</button>' + | |||
'</div>' + | |||
'</div>'; | |||
. | document.body.appendChild(cModal); | ||
} | |||
} | |||
. | var gModal = document.getElementById('clbi-gallery-modal'); | ||
var cModal = document.getElementById('clbi-crop-modal'); | |||
var cropImage = document.getElementById('clbi-crop-image'); | |||
var pfpInput = document.getElementById('pref-pfp-input'); | |||
function openGallery() { | |||
gModal.style.display = 'flex'; | |||
. | var username = mw.config.get('wgUserName'); | ||
api.get({ | |||
action: 'query', | |||
titles: '파일:Pfp-' + username + '.png', | |||
prop: 'imageinfo', | |||
iiprop: 'url|timestamp', | |||
iilimit: 6 | |||
}).then(function(data) { | |||
var pages = data.query.pages; | |||
var page = pages[Object.keys(pages)[0]]; | |||
if (!page.imageinfo || page.imageinfo.length === 0) return; | |||
. | var historyEl = document.getElementById('clbi-gallery-history'); | ||
var sectionEl = document.getElementById('clbi-gallery-history-section'); | |||
historyEl.innerHTML = ''; | |||
. | page.imageinfo.forEach(function(info, idx) { | ||
. | var wrap = document.createElement('div'); | ||
wrap.style.cssText = 'position:relative;cursor:pointer;'; | |||
. | |||
. | |||
. | |||
. | var img = document.createElement('img'); | ||
img.src = info.url; | |||
img.style.cssText = | |||
'width:72px;height:72px;object-fit:cover;border-radius:8px;border:2px solid #444;flex-shrink:0;'; | |||
if (idx === 0) { | |||
img.style.borderColor = '#854369'; | |||
var badge = document.createElement('div'); | |||
badge.textContent = '현재'; | |||
badge.style.cssText = | |||
'position:absolute;bottom:4px;left:50%;transform:translateX(-50%);background:#854369;color:#fff;font-size:9px;padding:1px 6px;border-radius:10px;'; | |||
wrap.appendChild(badge); | |||
} | |||
} | |||
. | img.addEventListener('mouseenter', function() { | ||
if (idx !== 0) img.style.borderColor = '#854369'; | |||
}); | |||
. | img.addEventListener('mouseleave', function() { | ||
if (idx !== 0) img.style.borderColor = '#444'; | |||
}); | |||
} | |||
. | img.addEventListener('click', function() { | ||
fetch(info.url) | |||
.then(function(r) { | |||
return r.blob(); | |||
}) | |||
.then(function(blob) { | |||
selectedFile = new File([blob], 'profile.png', { type: 'image/png' }); | |||
document.getElementById('pref-pfp-preview').src = URL.createObjectURL(blob); | |||
gModal.style.display = 'none'; | |||
document.getElementById('pref-pfp-btn').textContent = '✓ 사진 선택됨'; | |||
}); | |||
}); | |||
} | |||
. | wrap.appendChild(img); | ||
. | historyEl.appendChild(wrap); | ||
}); | |||
} | |||
. | sectionEl.style.display = 'block'; | ||
}); | |||
} | |||
) | |||
} | |||
function openCrop(src) { | |||
cropImage.src = src; | |||
cModal.style.display = 'flex'; | |||
if (cropper) { | |||
cropper.destroy(); | |||
cropper = null; | |||
. | } | ||
} | |||
setTimeout(function() { | |||
cropper = new Cropper(cropImage, { | |||
aspectRatio: 1, | |||
viewMode: 1, | |||
dragMode: 'move', | |||
. | autoCropArea: 0.8, | ||
cropBoxResizable: true, | |||
cropBoxMovable: true | |||
}); | |||
}, 150); | |||
} | |||
} | |||
. | document.getElementById('pref-pfp-btn').addEventListener('click', function() { | ||
. | openGallery(); | ||
}); | |||
. | document.getElementById('clbi-gallery-upload-btn').addEventListener('click', function() { | ||
pfpInput.click(); | |||
}); | |||
. | document.getElementById('clbi-gallery-close').addEventListener('click', function() { | ||
gModal.style.display = 'none'; | |||
}); | |||
. | pfpInput.addEventListener('change', function() { | ||
. | var file = this.files[0]; | ||
if (!file) return; | |||
. | gModal.style.display = 'none'; | ||
. | var reader = new FileReader(); | ||
reader.onload = function(e) { | |||
} | openCrop(e.target.result); | ||
}; | |||
reader.readAsDataURL(file); | |||
}); | |||
. | document.getElementById('clbi-crop-cancel').addEventListener('click', function() { | ||
cModal.style.display = 'none'; | |||
if (cropper) { | |||
cropper.destroy(); | |||
cropper = null; | |||
} | |||
pfpInput.value = ''; | |||
}); | |||
. | document.getElementById('clbi-crop-confirm').addEventListener('click', function() { | ||
if (!cropper) return; | |||
. | var canvas = cropper.getCroppedCanvas({ width: 256, height: 256 }); | ||
if (!canvas) return; | |||
. | canvas.toBlob(function(blob) { | ||
selectedFile = new File([blob], 'profile.png', { type: 'image/png' }); | |||
document.getElementById('pref-pfp-preview').src = URL.createObjectURL(blob); | |||
} | cModal.style.display = 'none'; | ||
cropper.destroy(); | |||
cropper = null; | |||
document.getElementById('pref-pfp-btn').textContent = '✓ 사진 선택됨'; | |||
}, 'image/png'); | |||
}); | |||
var emailSaveBtn = document.getElementById('pref-email-save'); | |||
if (emailSaveBtn) { | |||
emailSaveBtn.addEventListener('click', function() { | |||
var statusEl = document.getElementById('pref-email-status'); | |||
var newEmail = document.getElementById('pref-new-email').value; | |||
var password = document.getElementById('pref-email-password').value; | |||
if (!newEmail || !password) { | |||
statusEl.textContent = '이메일과 비밀번호를 입력해주세요.'; | |||
return; | |||
} | |||
} | |||
. | statusEl.textContent = '변경 중...'; | ||
api.postWithToken('csrf', { | |||
action: 'changeemail', | |||
email: newEmail, | |||
password: password | |||
}).then(function() { | |||
statusEl.textContent = '✓ 이메일 변경됨'; | |||
document.getElementById('pref-new-email').value = ''; | |||
document.getElementById('pref-email-password').value = ''; | |||
setTimeout(function() { | |||
statusEl.textContent = ''; | |||
}, 3000); | |||
}).fail(function(code, data) { | |||
var msg = data && data.error && data.error.info ? data.error.info : '변경 실패'; | |||
statusEl.textContent = msg; | |||
}); | |||
}); | |||
} | |||
} | |||
saveBtn.addEventListener('click', function() { | |||
var statusEl = document.getElementById('pref-status'); | |||
statusEl.textContent = '저장 중...'; | |||
var promises = []; | |||
if (selectedFile) { | |||
var username = mw.config.get('wgUserName'); | |||
promises.push( | |||
api.postWithToken('csrf', { | |||
action: 'upload', | |||
filename: 'Pfp-' + username + '.png', | |||
ignorewarnings: true, | |||
file: selectedFile, | |||
format: 'json' | |||
}, { | |||
contentType: 'multipart/form-data' | |||
}) | |||
); | |||
} | |||
} | |||
var fields = ['name', 'discord', 'role', 'bio']; | |||
if (isAdmin) fields.push('badges'); | |||
. | for (var i = 0; i < fields.length; i++) { | ||
var el = document.getElementById('pref-' + fields[i]); | |||
if (!el) continue; | |||
. | promises.push( | ||
api.postWithToken('csrf', { | |||
action: 'options', | |||
optionname: 'profile-' + fields[i], | |||
optionvalue: el.value | |||
}) | |||
); | |||
} | |||
} | |||
. | $.when.apply($, promises) | ||
.then(function() { | |||
statusEl.textContent = '✓ 저장됨'; | |||
selectedFile = null; | |||
document.getElementById('pref-pfp-btn').textContent = '사진 선택'; | |||
. | setTimeout(function() { | ||
statusEl.textContent = ''; | |||
}, 2000); | |||
}) | |||
.fail(function() { | |||
statusEl.textContent = '저장 실패'; | |||
}); | |||
}); | |||
} | } | ||
/* ========================================= | /* ========================================= | ||
Banner / CRT Page Monitor thumbnail slices | |||
- base 이미지는 틀에 들어간 파일 문법 그대로 사용 | |||
- slice 레이어에는 300px MediaWiki 썸네일만 삽입 | |||
========================================= */ | ========================================= */ | ||
(function ($, mw) { | |||
var thumbCache = {}; | |||
function parseSliceWidth(value) { | |||
var parsed = parseInt(value, 10); | |||
} | if (!isFinite(parsed) || parsed < 120) { | ||
return 300; | |||
} | |||
return parsed; | |||
} | |||
function getImageSrc(img) { | |||
return img ? (img.currentSrc || img.getAttribute('src') || img.src || '') : ''; | |||
} | |||
function getFileNameFromSrc(src) { | |||
var a; | |||
var parts; | |||
var fileName; | |||
if (!src) return ''; | |||
. | a = document.createElement('a'); | ||
a.href = src; | |||
/ | parts = (a.pathname || '').split('/').filter(function (part) { | ||
return !!part; | |||
}); | |||
. | if (!parts.length) return ''; | ||
fileName = parts.pop(); | |||
/* | |||
* MediaWiki thumb URL 예시: | |||
* /images/thumb/a/ab/File.png/1000px-File.png | |||
* /images/thumb/a/ab/File.svg/1000px-File.svg.png | |||
* | |||
* 이 경우 실제 파일명은 마지막 조각이 아니라 그 앞 조각이다. | |||
*/ | |||
if (/^\d+px-/.test(fileName) && parts.length) { | |||
fileName = parts.pop(); | |||
} | |||
} | |||
. | fileName = fileName.replace(/^\d+px-/, ''); | ||
try { | |||
fileName = decodeURIComponent(fileName); | |||
} catch (e) {} | |||
} | |||
return fileName; | |||
} | |||
} | |||
function resolveThumbUrl(img, width, callback) { | |||
var src = getImageSrc(img); | |||
var fileName = getFileNameFromSrc(src); | |||
var cacheKey; | |||
var entry; | |||
if (!src) return; | |||
if (!fileName || !mw || !mw.loader) { | |||
callback(src); | |||
return; | |||
} | |||
cacheKey = fileName + '|' + width; | |||
entry = thumbCache[cacheKey]; | |||
if (entry) { | |||
if (entry.resolved) { | |||
callback(entry.url || src); | |||
} else { | |||
entry.callbacks.push(callback); | |||
} | |||
return; | |||
} | |||
entry = { | |||
resolved: false, | |||
url: '', | |||
callbacks: [callback] | |||
}; | |||
thumbCache[cacheKey] = entry; | |||
function finish(url) { | |||
var callbacks = entry.callbacks.slice(); | |||
var i; | |||
. | entry.resolved = true; | ||
entry.url = url || src; | |||
entry.callbacks = []; | |||
. | for (i = 0; i < callbacks.length; i++) { | ||
callbacks[i](entry.url); | |||
} | |||
} | } | ||
. | mw.loader.using('mediawiki.api').done(function () { | ||
var api = new mw.Api(); | |||
api.get({ | |||
action: 'query', | |||
titles: 'File:' + fileName, | |||
prop: 'imageinfo', | |||
iiprop: 'url', | |||
iiurlwidth: width, | |||
formatversion: 2 | |||
}).done(function (data) { | |||
var page; | |||
var info; | |||
. | if ( | ||
data && | |||
data.query && | |||
data.query.pages && | |||
data.query.pages.length | |||
) { | |||
page = data.query.pages[0]; | |||
if ( | |||
page && | |||
page.imageinfo && | |||
page.imageinfo.length | |||
) { | |||
info = page.imageinfo[0]; | |||
} | |||
} | |||
} | |||
. | finish((info && (info.thumburl || info.url)) || src); | ||
}).fail(function () { | |||
finish(src); | |||
}); | |||
}).fail(function () { | |||
finish(src); | |||
}); | |||
} | |||
} | |||
function applySliceImages(frame, thumbUrl) { | |||
var slices; | |||
var i; | |||
var img; | |||
if (!frame || !thumbUrl) return; | |||
slices = frame.querySelectorAll('.crt-page-monitor-slice'); | |||
for (i = 0; i < slices.length; i++) { | |||
slices[i].innerHTML = ''; | |||
img = document.createElement('img'); | |||
img.className = 'crt-page-monitor-slice-img'; | |||
img.src = thumbUrl; | |||
img.alt = ''; | |||
img.decoding = 'async'; | |||
img.loading = 'eager'; | |||
img.setAttribute('aria-hidden', 'true'); | |||
. | slices[i].appendChild(img); | ||
} | |||
} | |||
. | frame.setAttribute('data-crt-slices-ready', '1'); | ||
} | |||
} | |||
function initBannerFrame(frame) { | |||
var baseImg; | |||
var width; | |||
. | if (!frame) return; | ||
if (frame.getAttribute('data-crt-slices-ready') === '1') return; | |||
baseImg = frame.querySelector('.crt-page-monitor-image-base img'); | |||
if (!baseImg) return; | |||
. | width = parseSliceWidth(frame.getAttribute('data-crt-slice-width')); | ||
resolveThumbUrl(baseImg, width, function (thumbUrl) { | |||
if (!frame || !frame.parentNode) return; | |||
applySliceImages(frame, thumbUrl); | |||
}); | |||
} | |||
} | |||
. | function initBannerFrames(root) { | ||
var scope = root && root.querySelectorAll ? root : document; | |||
var frames = scope.querySelectorAll('.crt-page-monitor-frame'); | |||
var i; | |||
for (i = 0; i < frames.length; i++) { | |||
initBannerFrame(frames[i]); | |||
} | |||
} | |||
} | |||
$(function () { | |||
initBannerFrames(document); | |||
}); | |||
. | if (mw && mw.hook) { | ||
mw.hook('wikipage.content').add(function ($content) { | |||
initBannerFrames($content && $content[0] ? $content[0] : document); | |||
}); | |||
} | |||
})(jQuery, window.mw); | |||
/* ========================================= | /* ========================================= | ||
Doc Tab System — Q/E 단축키 탭 전환 | |||
글리치 플리커 + RGB split + 방향 슬라이드 | |||
========================================= */ | ========================================= */ | ||
(function () { | |||
'use strict'; | |||
var keydownBound = false; | |||
function initDocTabs() { | |||
var tabBars = document.querySelectorAll('.doc-tab-bar'); | |||
if (!tabBars.length) return; | |||
tabBars.forEach(function (bar) { | |||
if (bar.getAttribute('data-tabs-init')) return; | |||
bar.setAttribute('data-tabs-init', '1'); | |||
var tabs = Array.from(bar.querySelectorAll('.doc-tab')); | |||
if (!tabs.length) return; | |||
.doc- | var panel = bar.closest('.doc-panel'); | ||
var display = panel ? panel.querySelector('.doc-display') : null; | |||
if (!display) display = document.getElementById('doc-main-display'); | |||
if (!display) return; | |||
. | tabs.forEach(function (tab, i) { | ||
tab.addEventListener('click', function () { | |||
var currentIdx = tabs.findIndex(function (t) { | |||
return t.classList.contains('active'); | |||
} | }); | ||
if (currentIdx === i) return; | |||
switchTab(tabs, display, i, i > currentIdx ? 1 : -1); | |||
}); | |||
}); | |||
. | var initIdx = tabs.findIndex(function (t) { return t.classList.contains('active'); }); | ||
if (initIdx !== -1) { | |||
var initRef = tabs[initIdx].dataset.ref; | |||
var initEl = initRef ? document.getElementById(initRef) : null; | |||
display.innerHTML = initEl ? initEl.innerHTML : (tabs[initIdx].dataset.content || ''); | |||
} | |||
}); | |||
} | |||
. | if (!keydownBound) { | ||
keydownBound = true; | |||
document.addEventListener('keydown', function (e) { | |||
var tag = document.activeElement.tagName; | |||
if (tag === 'INPUT' || tag === 'TEXTAREA') return; | |||
.doc- | var bar = document.querySelector('.doc-tab-bar'); | ||
if (!bar) return; | |||
var tabs = Array.from(bar.querySelectorAll('.doc-tab')); | |||
.doc- | var panel = bar.closest('.doc-panel'); | ||
var display = panel ? panel.querySelector('.doc-display') : null; | |||
if (!display) display = document.getElementById('doc-main-display'); | |||
if (!display) return; | |||
. | var activeIdx = tabs.findIndex(function (t) { | ||
return t.classList.contains('active'); | |||
}); | |||
if (activeIdx === -1) return; | |||
. | if (e.key === 'q' || e.key === 'Q') { | ||
e.preventDefault(); | |||
var prev = (activeIdx - 1 + tabs.length) % tabs.length; | |||
if (prev !== activeIdx) switchTab(tabs, display, prev, -1); | |||
} | } else if (e.key === 'e' || e.key === 'E') { | ||
e.preventDefault(); | |||
var next = (activeIdx + 1) % tabs.length; | |||
if (next !== activeIdx) switchTab(tabs, display, next, 1); | |||
} | |||
}); | |||
} | |||
} | |||
var isAnimating = false; | |||
function switchTab(tabs, display, nextIdx, dir) { | |||
if (isAnimating) return; | |||
isAnimating = true; | |||
. | tabs.forEach(function (t) { t.classList.remove('active'); }); | ||
tabs[nextIdx].classList.add('active'); | |||
var ref = tabs[nextIdx].dataset.ref; | |||
var nextContent; | |||
if (ref) { | |||
var refEl = document.getElementById(ref); | |||
nextContent = refEl ? refEl.innerHTML : ''; | |||
} else { | |||
nextContent = tabs[nextIdx].dataset.content || ''; | |||
} | |||
glitchOut(display, dir, function () { | |||
display.innerHTML = nextContent; | |||
glitchIn(display, dir, function () { | |||
isAnimating = false; | |||
}); | |||
}); | |||
} | } | ||
function glitchOut(el, dir, cb) { | |||
var duration = 160; | |||
var start = null; | |||
var slideX = dir * 16; | |||
function step(ts) { | |||
if (!start) start = ts; | |||
var p = Math.min((ts - start) / duration, 1); | |||
var ease = p * p; | |||
var tx = slideX * ease; | |||
var skew = dir * ease * 1.0; | |||
var opacity = 1 - ease; | |||
var rgb = ease * 5; | |||
el.style.transform = 'translateX(' + tx + 'px) skewX(' + skew + 'deg)'; | |||
el.style.opacity = opacity; | |||
el.style.filter = | |||
'drop-shadow(' + (-rgb) + 'px 0 0 rgba(80,160,255,0.75)) ' + | |||
'drop-shadow(' + rgb + 'px 0 0 rgba(255,55,90,0.65)) ' + | |||
'brightness(' + (1 + ease * 0.25) + ')'; | |||
if (p < 1) { | |||
requestAnimationFrame(step); | |||
} else { | |||
el.style.opacity = '0'; | |||
cb(); | |||
} | |||
} | |||
requestAnimationFrame(step); | |||
} | } | ||
. | function glitchIn(el, dir, cb) { | ||
var duration = 200; | |||
var start = null; | |||
var startX = -dir * 16; | |||
el.style.transform = 'translateX(' + startX + 'px) skewX(' + (-dir * 1.0) + 'deg)'; | |||
el.style.opacity = '0'; | |||
function step(ts) { | |||
if (!start) start = ts; | |||
var p = Math.min((ts - start) / duration, 1); | |||
var ease = 1 - Math.pow(1 - p, 3); | |||
var tx = startX * (1 - ease); | |||
var skew = -dir * 1.0 * (1 - ease); | |||
var opacity = ease; | |||
var rgb = (1 - ease) * 3; | |||
var brightness = 1 + (1 - ease) * 0.35; | |||
el.style.transform = 'translateX(' + tx + 'px) skewX(' + skew + 'deg)'; | |||
el.style.opacity = opacity; | |||
el.style.filter = | |||
'drop-shadow(' + (-rgb) + 'px 0 0 rgba(80,160,255,0.65)) ' + | |||
'drop-shadow(' + rgb + 'px 0 0 rgba(255,55,90,0.55)) ' + | |||
'brightness(' + brightness + ')'; | |||
if (p < 1) { | |||
requestAnimationFrame(step); | |||
} else { | |||
el.style.transform = ''; | |||
el.style.opacity = ''; | |||
el.style.filter = ''; | |||
cb(); | |||
} | |||
} | |||
requestAnimationFrame(step); | |||
} | } | ||
. | if (document.readyState === 'loading') { | ||
document.addEventListener('DOMContentLoaded', initDocTabs); | |||
} | } else { | ||
initDocTabs(); | |||
} | |||
if (typeof mw !== 'undefined' && mw.hook) { | |||
mw.hook('wikipage.content').add(function () { | |||
initDocTabs(); | |||
}); | |||
} | } | ||
})(); | |||
. | |||
.doc- | /* ========================================= | ||
.doc- | Doc Section Switch — 좌측 섹션 전환 | ||
========================================= */ | |||
$(document).on('click', '.doc-nav-item[data-section]', function () { | |||
var name = $(this).attr('data-section'); | |||
var display = document.getElementById('doc-main-display'); | |||
var titleEl = document.getElementById('doc-center-title'); | |||
var tabBar = document.getElementById('doc-tab-bar-text'); | |||
if (!display) return; | |||
.doc- | $('.doc-nav-item[data-section]').removeClass('active'); | ||
$('.doc-nav-item[data-section="' + name + '"]').addClass('active'); | |||
.doc- | if (name === 'text') { | ||
if (titleEl) titleEl.textContent = '개요'; | |||
if (tabBar) $(tabBar).show(); | |||
var activeTab = tabBar ? tabBar.querySelector('.doc-tab.active') : null; | |||
if (!activeTab && tabBar) activeTab = tabBar.querySelector('.doc-tab'); | |||
if (activeTab) { | |||
var ref = activeTab.dataset.ref; | |||
var refEl = ref ? document.getElementById(ref) : null; | |||
display.innerHTML = refEl ? refEl.innerHTML : (activeTab.dataset.content || ''); | |||
} | |||
} else { | |||
if (titleEl) titleEl.textContent = name === 'factions' ? '세력' : name === 'people' ? '인물' : name; | |||
if (tabBar) $(tabBar).hide(); | |||
var refEl = document.getElementById('doc-content-' + name); | |||
display.innerHTML = refEl ? refEl.innerHTML : ''; | |||
} | } | ||
} | }); | ||
2026년 5월 10일 (일) 09:21 판
mw.loader.load('https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js');
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 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 updateLeftSidebarNationsImage() {
var imageId = 'clbi-left-nations-image';
// SPA 이동 시 이전 문서에서 삽입했던 이미지를 먼저 제거한다.
// 이 처리가 없으면 국가_및_조합에서 다른 문서로 이동했을 때 이미지가 남을 수 있다.
$('#' + imageId).remove();
// 현재 문서명이 국가 및 조합일 때만 삽입한다.
// wgPageName은 보통 국가_및_조합처럼 언더스코어를 포함하므로 normalizePageName으로 통일해서 비교한다.
var pageName = normalizePageName(mw.config.get('wgPageName'));
var targetPage = normalizePageName('국가_및_조합');
if (pageName !== targetPage) {
return;
}
var $leftSidebar = $('#clbi-left-sidebar');
if (!$leftSidebar.length) {
return;
}
// 왼쪽 사이드바 기본 자식 순서:
// 0 = 언어 섹션, 1 = 카테고리 섹션.
// 이미지는 카테고리 섹션 바로 아래에 삽입한다.
var $categoryBox = $leftSidebar.children('.clbi-left-box').eq(1);
if (!$categoryBox.length) {
return;
}
// 컨테이너나 테두리를 추가하지 않고 img 요소만 넣는다.
// 실제 파일을 바꾸려면 Redirect/file 뒤 파일명만 교체하면 된다.
$categoryBox.after(
'<img id="' + imageId + '" ' +
'class="clbi-left-page-image" ' +
'src="/index.php?title=특수:Redirect/file/img-nations-sidebar-001.png" ' +
'alt="" loading="lazy">'
);
}
// 사이드바 업데이트
function updateSidebar() {
if (!window.LANG || !window.LANG_NAMES || !window.CAT_LINKS) {
setTimeout(updateSidebar, 100);
return;
}
var langData = document.getElementById('clbi-lang-data');
var currentLang = getCurrentLang();
var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
var names = (window.LANG_NAMES && window.LANG_NAMES[currentLang]) ? window.LANG_NAMES[currentLang] : window.LANG_NAMES.ko;
var cl = (window.CAT_LINKS && window.CAT_LINKS[currentLang]) ? window.CAT_LINKS[currentLang] : window.CAT_LINKS.ko;
var allLangs = langData ? {
ko: langData.getAttribute('data-ko') || '',
en: langData.getAttribute('data-en') || '',
zh: langData.getAttribute('data-zh') || '',
ja: langData.getAttribute('data-ja') || ''
} : {
ko: '',
en: '',
zh: '',
ja: ''
};
$('#clbi-lang-current').text(names[currentLang]);
var langHtml = '';
$.each(['ko', 'en', 'zh', 'ja'], function(i, lang) {
if (lang === currentLang) return;
var target = allLangs[lang];
if (target) {
langHtml += '<div class="clbi-lang-link"><a href="' + buildWikiPath(target) + '?uselang=' + lang + '">' + names[lang] + '</a></div>';
} else {
langHtml += '<div class="clbi-lang-wip">' + names[lang] + '</div>';
}
});
$('#clbi-lang-list').html(langHtml);
$('#clbi-title-language').text(t.language);
$('#clbi-title-categories').text(t.categories);
$('#clbi-title-links').text(t.links);
$('#clbi-title-search a').text(t.search);
$('#clbi-search-input').attr('placeholder', t.search + '...');
$('#clbi-title-recent a').text(t.recentChanges);
$('#clbi-title-guide').text(t.guide);
$('#clbi-guide-link').text(t.getStarted);
$('#clbi-title-playlist').text(t.playlist);
$('#clbi-cat-main a').attr('href', buildWikiPath(cl.main));
$('#clbi-cat-main .clbi-cat-label').text(t.mainMenu);
$('#clbi-cat-nations a').attr('href', buildWikiPath(cl.nations));
$('#clbi-cat-nations .clbi-cat-label').text(t.nations);
$('#clbi-cat-corporations a').attr('href', buildWikiPath(cl.corporations));
$('#clbi-cat-corporations .clbi-cat-label').text(t.corporations);
$('#clbi-cat-military a').attr('href', buildWikiPath(cl.military));
$('#clbi-cat-military .clbi-cat-label').text(t.military);
$('#clbi-cat-history a').attr('href', buildWikiPath(cl.history));
$('#clbi-cat-history .clbi-cat-label').text(t.history);
$('#clbi-cat-personnel a').attr('href', buildWikiPath(cl.personnel));
$('#clbi-cat-personnel .clbi-cat-label').text(t.personnel);
$('#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-cat-btn').removeClass('clbi-cat-active');
$.each(['main', 'nations', 'corporations', 'military', 'history', 'personnel'], function(i, key) {
if (cl[key] && pageName === normalizePageName(cl[key])) {
$('#clbi-cat-' + key).addClass('clbi-cat-active');
}
});
$('.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;
}
var relevantEditable = mw.config.get('wgRelevantPageIsProbablyEditable');
if (relevantEditable === 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 applyMainPageStyle() {
var specialPage = mw.config.get('wgCanonicalSpecialPageName');
if (specialPage === 'Preferences') return;
var pageName = normalizePageName(mw.config.get('wgPageName'));
var hideTools = (pageName === '대문' || !canShowContentTools());
// 모든 문서에서 분류 바를 본문 컨테이너 아래로 이동
moveCatlinksToBottom();
if (pageName === '대문') {
$('.liberty-content-header').css('display', 'none');
$('.mw-page-title-main').addClass('clbi-hide');
$('.catlinks').css('display', 'none');
$('.liberty-content-main').css('border-radius', '5px 5px 0 0');
if ($('#clbi-main-logo').length === 0) {
$('.liberty-content').prepend(
'<div id="clbi-main-logo" style="text-align:center;padding:10px 0;">' +
'<img src="/index.php?title=특수:Redirect/file/Img-clbi-001.png" style="width:900px;height:auto;">' +
'</div>'
);
}
if ($('#clbi-main-crt-hero').length && $('#clbi-main-crt-hero-wrap').length === 0) {
var heroWrap = $('<div id="clbi-main-crt-hero-wrap"></div>');
if ($('#clbi-main-logo').length) {
heroWrap.insertAfter('#clbi-main-logo');
} else {
heroWrap.insertBefore('.liberty-content-main');
}
$('#clbi-main-crt-hero').appendTo('#clbi-main-crt-hero-wrap');
}
} else if ($('.screen-header').length > 0) {
$('.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', '');
$('#clbi-main-logo').remove();
$('#clbi-main-crt-hero-wrap').remove();
$('#clbi-tools-box').remove();
}
$('.liberty-content-header').css('display', '');
$('.mw-page-title-main').removeClass('clbi-hide');
$('.catlinks').css('display', '');
$('.liberty-content-main').css('border-radius', '');
$('#clbi-main-logo').remove();
$('#clbi-main-crt-hero-wrap').remove();
}
if (hideTools) {
$('.content-tools').css('display', 'none');
$('.liberty-content').addClass('content-tools-hidden');
} else {
$('.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);
leftSidebar.appendChild(tocBox);
// 왼쪽 목차: DOM 배치가 끝난 뒤 긴 제목 스크롤 여부를 계산한다.
requestAnimationFrame(function () {
initTocTitleScroll(tocBox);
setTimeout(function () {
initTocTitleScroll(tocBox);
}, 120);
});
}
// 초기화 함수
function initSidebars() {
var header = $('.liberty-content-header');
var content = $('.liberty-content');
if (header.length && content.length) {
header.prependTo(content);
}
if ($('#clbi-right-sidebar').length === 0) {
var username = mw.config.get('wgUserName');
var isLoggedIn = username !== null;
var avatarSrc = isLoggedIn
? '/index.php?title=특수:Redirect/file/Pfp-' + username + '.png'
: '/index.php?title=특수:Redirect/file/Pfp-default.png';
var userBox;
if (isLoggedIn) {
userBox =
'<div class="clbi-right-box">' +
'<div style="display:flex;flex-direction:column;align-items:center;padding:14px 14px 10px;background:linear-gradient(to bottom, #171114 0%, #0a0909 100%);">' +
'<img src="' + avatarSrc + '" onerror="this.src=\'/index.php?title=특수:Redirect/file/Pfp-default.png\'" style="width:64px;height:64px;border-radius:5px;object-fit:cover;border:2px solid #854369;margin-bottom:8px;">' +
'<div style="position:relative;width:100%;margin-top:2px;height:18px;line-height:18px;text-align:center;">' +
'<button type="button" id="clbi-playlist-toggle" aria-label="플레이리스트" style="position:absolute;top:0;left:10px;background:none;border:none;padding:0;width:18px;height:18px;color:#E2E2E2;cursor:pointer;display:flex;align-items:center;justify-content:center;">' +
'<span class="clbi-icon" style="--icon:var(--ic-ui-009);width:16px;height:16px;"></span>' +
'</button>' +
'<a href="/index.php/사용자:' + username + '" style="font-size:13px;font-weight:700;color:#E2E2E2 !important;text-decoration:none !important;line-height:18px;display:inline-block;vertical-align:middle;max-width:calc(100% - 58px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + username + '</a>' +
'<button type="button" id="clbi-notification-toggle" aria-label="알림" style="position:absolute;top:0;right:10px;background:none;border:none;padding:0;width:18px;height:18px;color:#E2E2E2;cursor:pointer;display:flex;align-items:center;justify-content:center;">' +
'<span class="clbi-icon" style="--icon:var(--ic-ui-009);width:16px;height:16px;"></span>' +
'<span id="clbi-notification-badge" style="display:none;position:absolute;top:-6px;right:-8px;min-width:14px;height:14px;padding:0 3px;border-radius:999px;background:#854369;color:#fff;font-size:9px;line-height:14px;font-weight:700;text-align:center;"></span>' +
'</button>' +
'</div>' +
'</div>' +
'<div class="clbi-right-content" style="border-top:1px solid #2a2a2a;padding:8px;">' +
'<a href="/index.php/특수:기여/' + username + '" class="clbi-user-btn" id="clbi-btn-contribution">기여</a>' +
'<a href="/index.php/특수:주시문서목록" class="clbi-user-btn" id="clbi-btn-watchlist">주시문서 목록</a>' +
'<a href="/index.php/특수:설정" class="clbi-user-btn" id="clbi-btn-preferences">설정</a>' +
'<a href="/index.php?title=특수:로그아웃&returnto=대문" class="clbi-user-btn clbi-user-btn-logout" id="clbi-btn-logout">로그아웃</a>' +
'</div>' +
'</div>';
} else {
userBox =
'<div class="clbi-right-box">' +
'<div style="display:flex;flex-direction:column;align-items:center;padding:14px 14px 10px;background:linear-gradient(to bottom, #171114 0%, #0a0909 100%);">' +
'<img src="/index.php?title=특수:Redirect/file/Pfp-default.png" style="width:64px;height:64px;border-radius:5px;object-fit:cover;border:2px solid #854369;margin-bottom:8px;">' +
'<span style="font-size:13px;font-weight:700;color:#E2E2E2;">Guest</span>' +
'</div>' +
'<div class="clbi-right-content" style="border-top:1px solid #2a2a2a;padding:8px;">' +
'<a href="/index.php?title=특수:로그인&returnto=대문" class="clbi-user-btn" id="clbi-btn-login">로그인</a>' +
'</div>' +
'</div>';
}
var recentBox =
'<div class="clbi-right-box">' +
'<div class="clbi-right-title" id="clbi-title-recent"><span class="clbi-icon" style="--icon:var(--ic-ui-006)"></span> <a href="/index.php/특수:최근바뀜" style="color:#E2E2E2 !important;text-decoration:none !important;">최근 변경</a></div>' +
'<div class="clbi-right-content" id="clbi-recent-list">불러오는 중...</div>' +
'</div>';
var linkBox =
'<div class="clbi-right-box">' +
'<div class="clbi-right-title" id="clbi-title-links"><span class="clbi-icon" style="--icon:var(--ic-ui-003)"></span> 링크</div>' +
'<div class="clbi-right-content clbi-link-box">' +
'<ul>' +
'<li><a href="https://discord.gg/ctaeJ9d3Q5" target="_blank">Discord</a></li>' +
'<li><a href="https://www.youtube.com/@nxdsxn" target="_blank">YouTube</a></li>' +
'<li><a href="https://x.com/nxd_sxn" target="_blank">X</a></li>' +
'</ul>' +
'</div>' +
'</div>';
var sidebar =
userBox +
'<div class="clbi-right-box">' +
'<div class="clbi-right-title" id="clbi-title-search"><span class="clbi-icon" style="--icon:var(--ic-ui-005)"></span> <a href="/index.php/특수:검색" style="color:#E2E2E2 !important;text-decoration:none !important;">검색</a></div>' +
'<div class="clbi-right-content">' +
'<input id="clbi-search-input" type="text" placeholder="검색...">' +
'<button id="clbi-search-btn">GO</button>' +
'</div>' +
'</div>' +
recentBox +
'<div class="clbi-right-box">' +
'<div class="clbi-right-title" id="clbi-title-guide"><span class="clbi-icon" style="--icon:var(--ic-ui-007)"></span> 가이드</div>' +
'<div class="clbi-right-content">' +
'<a href="/index.php/CLBI_Wiki/KR_시작하기_(CLBI)" id="clbi-guide-link">시작하기</a>' +
'</div>' +
'</div>' +
linkBox;
$('.content-wrapper').append('<div id="clbi-right-sidebar">' + sidebar + '</div>');
$('#clbi-search-btn').click(function() {
var query = $('#clbi-search-input').val();
if (query) {
window.location.href = '/index.php?search=' + encodeURIComponent(query);
}
});
$('#clbi-search-input').keypress(function(e) {
if (e.which === 13) $('#clbi-search-btn').click();
});
$.getJSON(
'/api.php?action=query&list=recentchanges&rclimit=5&rcprop=title|timestamp&format=json&rcnamespace=0&rctype=edit|new',
function(data) {
var items = data.query.recentchanges;
var html = '';
$.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>';
});
$('#clbi-recent-list').html(html);
$('#clbi-recent-list .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() {
$('#clbi-recent-list').html('불러오기 실패');
});
}
if ($('#clbi-left-sidebar').length === 0) {
var leftSidebar =
'<div id="clbi-left-sidebar">' +
'<div class="clbi-left-box">' +
'<div class="clbi-left-title"><span class="clbi-icon" style="--icon:var(--ic-ui-001)"></span> <span id="clbi-title-language">언어</span></div>' +
'<div class="clbi-left-content clbi-lang-box" id="clbi-lang-box">' +
'<div class="clbi-lang-current" id="clbi-lang-current">한국어</div>' +
'<div id="clbi-lang-list" style="display:none;"></div>' +
'</div>' +
'</div>' +
'<div class="clbi-left-box">' +
'<div class="clbi-left-title"><span class="clbi-icon" style="--icon:var(--ic-ui-002)"></span> <span id="clbi-title-categories">카테고리</span></div>' +
'<div class="clbi-left-content clbi-cat-box">' +
'<div class="clbi-cat-btn" id="clbi-cat-main"><a href="/index.php/대문"><div class="clbi-cat-text"><span class="clbi-cat-label">메인 메뉴</span><span class="clbi-cat-sub">MAIN MENU</span></div></a><div class="clbi-cat-arrow">▶</div></div>' +
'<div class="clbi-cat-btn" id="clbi-cat-nations"><a href="/index.php/국가_및_조합"><div class="clbi-cat-text"><span class="clbi-cat-label">국가 및 조합</span><span class="clbi-cat-sub">NATIONS & FACTIONS</span></div></a><div class="clbi-cat-arrow">▶</div></div>' +
'<div class="clbi-cat-btn" id="clbi-cat-corporations"><a href="/index.php/기업_및_공동체"><div class="clbi-cat-text"><span class="clbi-cat-label">기업 및 공동체</span><span class="clbi-cat-sub">CORPORATIONS & COMMUNITIES</span></div></a><div class="clbi-cat-arrow">▶</div></div>' +
'<div class="clbi-cat-btn" id="clbi-cat-military"><a href="/index.php/군_정치집단"><div class="clbi-cat-text"><span class="clbi-cat-label">군, 정치집단</span><span class="clbi-cat-sub">MILITARY & POLITICS</span></div></a><div class="clbi-cat-arrow">▶</div></div>' +
'<div class="clbi-cat-btn" id="clbi-cat-history"><a href="/index.php/역사적_사건"><div class="clbi-cat-text"><span class="clbi-cat-label">역사적 사건</span><span class="clbi-cat-sub">HISTORICAL EVENTS</span></div></a><div class="clbi-cat-arrow">▶</div></div>' +
'<div class="clbi-cat-btn" id="clbi-cat-personnel"><a href="/index.php/인물"><div class="clbi-cat-text"><span class="clbi-cat-label">인물</span><span class="clbi-cat-sub">KEY PERSONNEL</span></div></a><div class="clbi-cat-arrow">▶</div></div>' +
'</div>' +
'</div>' +
'</div>';
$('.content-wrapper').prepend(leftSidebar);
// 왼쪽 사이드바가 처음 생성된 직후에도 한 번 실행한다.
// 이후 언어 갱신과 SPA 이동 시에는 updateSidebar()에서 다시 실행된다.
updateLeftSidebarNationsImage();
$(document).on('click', '#clbi-lang-current', function() {
if ($('#clbi-lang-list').is(':hidden')) {
$('#clbi-lang-current').css('margin-bottom', '8px');
$('#clbi-lang-list').slideDown(200);
} else {
$('#clbi-lang-list').slideUp(200, function() {
$('#clbi-lang-current').css('margin-bottom', '0');
});
}
});
}
applyMainPageStyle();
$('#side-toc-box').remove();
mw.loader.using(['mediawiki.api']).then(function() {
setTimeout(function() {
initNotifications();
initPlaylistPopup();
initProfile();
moveTocToLeftSidebar();
}, 300);
setTimeout(moveTocToLeftSidebar, 800);
setTimeout(moveTocToLeftSidebar, 1500);
});
}
$(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();
$('#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);
}
window.scrollTo(0, scrollY);
});
});
// ========== 프로필 시스템 ==========
function initProfile() {
$('.profile-card').remove();
var ns = mw.config.get('wgNamespaceNumber');
var title = mw.config.get('wgTitle');
if (ns === 2) {
var profileUser = title.split('/')[0];
renderProfile(profileUser);
}
if (mw.config.get('wgCanonicalSpecialPageName') === '사용자정보') {
initUserProfilePage();
}
}
function renderProfile(username) {
var api = new mw.Api();
api.get({
action: 'query',
list: 'users',
ususers: username,
usprop: 'editcount'
}).then(function(data) {
var user = data.query.users[0];
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:10px;' +
'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;
ensureNotificationPopup();
loadNotificationsIntoPopup();
$(document)
.off('click.clbiNotificationToggle')
.on('click.clbiNotificationToggle', '#clbi-notification-toggle', function(e) {
e.preventDefault();
e.stopPropagation();
var popup = document.getElementById('clbi-notification-popup');
var playlistPopup = document.getElementById('clbi-playlist-popup');
if (!popup) return;
if (playlistPopup) {
playlistPopup.style.display = 'none';
}
if (popup.style.display === 'none' || popup.style.display === '') {
popup.style.display = 'block';
positionNotificationPopup();
loadNotificationsIntoPopup();
} else {
popup.style.display = 'none';
}
});
$(document)
.off('click.clbiNotificationOutside')
.on('click.clbiNotificationOutside', function(e) {
var popup = document.getElementById('clbi-notification-popup');
var toggle = document.getElementById('clbi-notification-toggle');
if (!popup || !toggle) return;
if (!popup.contains(e.target) && !toggle.contains(e.target)) {
popup.style.display = 'none';
}
});
$(document)
.off('click.clbiNotificationReadAll')
.on('click.clbiNotificationReadAll', '#clbi-notification-readall', function(e) {
e.preventDefault();
e.stopPropagation();
var button = this;
button.disabled = true;
button.textContent = '처리 중...';
markAllNotificationsRead()
.then(function() {
loadNotificationsIntoPopup();
})
.always(function() {
button.disabled = false;
button.textContent = '전체 읽음';
});
});
$(document)
.off('click.clbiNotificationItem')
.on('click.clbiNotificationItem', '.clbi-notification-item', function(e) {
e.preventDefault();
e.stopPropagation();
var href = this.getAttribute('href');
var notificationId = this.getAttribute('data-notification-id') || '';
markNotificationReadById(notificationId).always(function() {
loadNotificationsIntoPopup();
if (href) {
window.location.href = href;
}
});
});
$(window)
.off('resize.clbiNotification')
.on('resize.clbiNotification', function() {
var popup = document.getElementById('clbi-notification-popup');
if (popup && popup.style.display === 'block') {
positionNotificationPopup();
}
});
}
// ========== 알림 시스템 끝 ==========
// ========== 플레이리스트 팝업 시스템 ==========
function ensurePlaylistPopup() {
if (document.getElementById('clbi-playlist-popup')) return;
var popup = document.createElement('div');
popup.id = 'clbi-playlist-popup';
popup.style.cssText =
'display:none;position:fixed;z-index:99999;width:690px;height:540px;' +
'background:#0a0909;border:2px solid #854369;border-radius:10px;' +
'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;">' +
'<span>플레이리스트</span>' +
'</div>' +
'<div style="padding:0;background:#111;height:calc(100% - 42px);">' +
'<iframe style="border:none;width:100%;height:100%;" src="https://open.spotify.com/embed/playlist/32l4ke6djdQn8LoBp1ipR9?utm_source=generator&theme=0" allowfullscreen allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>' +
'</div>';
document.body.appendChild(popup);
}
function positionPlaylistPopup() {
var btn = document.getElementById('clbi-playlist-toggle');
var popup = document.getElementById('clbi-playlist-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 initPlaylistPopup() {
var btn = document.getElementById('clbi-playlist-toggle');
if (!btn) return;
ensurePlaylistPopup();
$(document)
.off('click.clbiPlaylistToggle')
.on('click.clbiPlaylistToggle', '#clbi-playlist-toggle', function(e) {
e.preventDefault();
e.stopPropagation();
var popup = document.getElementById('clbi-playlist-popup');
var notificationPopup = document.getElementById('clbi-notification-popup');
if (!popup) return;
if (notificationPopup) {
notificationPopup.style.display = 'none';
}
if (popup.style.display === 'none' || popup.style.display === '') {
popup.style.display = 'block';
positionPlaylistPopup();
} else {
popup.style.display = 'none';
}
});
$(document)
.off('click.clbiPlaylistOutside')
.on('click.clbiPlaylistOutside', function(e) {
var popup = document.getElementById('clbi-playlist-popup');
var toggle = document.getElementById('clbi-playlist-toggle');
if (!popup || !toggle) return;
if (!popup.contains(e.target) && !toggle.contains(e.target)) {
popup.style.display = 'none';
}
});
$(window)
.off('resize.clbiPlaylist')
.on('resize.clbiPlaylist', function() {
var popup = document.getElementById('clbi-playlist-popup');
if (popup && popup.style.display === 'block') {
positionPlaylistPopup();
}
});
}
// ========== 플레이리스트 팝업 시스템 끝 ==========
function initUserProfilePage() {
var saveBtn = document.getElementById('pref-save');
if (!saveBtn) return;
var adminEl = document.getElementById('pref-is-admin');
var isAdmin = adminEl && adminEl.getAttribute('data-admin') === '1';
var api = new mw.Api();
var selectedFile = null;
var cropper = null;
if (!document.getElementById('clbi-gallery-modal')) {
var gModal = document.createElement('div');
gModal.id = 'clbi-gallery-modal';
gModal.style.cssText =
'display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:99999;align-items:center;justify-content:center;';
gModal.innerHTML =
'<div style="background:#1e1e1e;border:2px solid #854369;border-radius:12px;padding:24px;max-width:480px;width:90%;display:flex;flex-direction:column;gap:16px;">' +
'<div style="display:flex;justify-content:space-between;align-items:center;">' +
'<span style="font-size:14px;font-weight:700;color:#e2e2e2;">프로필 사진 선택</span>' +
'<button type="button" id="clbi-gallery-close" style="background:none;border:none;color:#aaa;font-size:18px;cursor:pointer;">✕</button>' +
'</div>' +
'<button type="button" id="clbi-gallery-upload-btn" style="background:#2a2a2a;border:2px dashed #854369;border-radius:8px;padding:32px;color:#e2e2e2;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:8px;font-size:13px;width:100%;">' +
'<span style="font-size:32px;">🖼️</span>새 사진 업로드' +
'</button>' +
'<div id="clbi-gallery-history-section" style="display:none;">' +
'<div style="font-size:11px;color:#888;margin-bottom:8px;">이전 사진 — 클릭하면 바로 적용</div>' +
'<div id="clbi-gallery-history" style="display:flex;gap:8px;flex-wrap:wrap;"></div>' +
'</div>' +
'</div>';
document.body.appendChild(gModal);
}
if (!document.getElementById('clbi-crop-modal')) {
var cModal = document.createElement('div');
cModal.id = 'clbi-crop-modal';
cModal.style.cssText =
'display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:99999;align-items:center;justify-content:center;';
cModal.innerHTML =
'<div style="background:#1e1e1e;border:2px solid #854369;border-radius:12px;padding:24px;max-width:500px;width:90%;display:flex;flex-direction:column;gap:16px;">' +
'<div style="font-size:14px;font-weight:700;color:#e2e2e2;">사진 조정</div>' +
'<div style="width:100%;max-height:380px;overflow:hidden;border-radius:8px;">' +
'<img id="clbi-crop-image" style="max-width:100%;">' +
'</div>' +
'<div style="display:flex;gap:8px;justify-content:flex-end;">' +
'<button type="button" id="clbi-crop-cancel" style="background:#2a2a2a;color:#e2e2e2;border:1px solid #444;padding:8px 16px;border-radius:6px;cursor:pointer;">취소</button>' +
'<button type="button" id="clbi-crop-confirm" style="background:#854369;color:#fff;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;">확정</button>' +
'</div>' +
'</div>';
document.body.appendChild(cModal);
}
var gModal = document.getElementById('clbi-gallery-modal');
var cModal = document.getElementById('clbi-crop-modal');
var cropImage = document.getElementById('clbi-crop-image');
var pfpInput = document.getElementById('pref-pfp-input');
function openGallery() {
gModal.style.display = 'flex';
var username = mw.config.get('wgUserName');
api.get({
action: 'query',
titles: '파일:Pfp-' + username + '.png',
prop: 'imageinfo',
iiprop: 'url|timestamp',
iilimit: 6
}).then(function(data) {
var pages = data.query.pages;
var page = pages[Object.keys(pages)[0]];
if (!page.imageinfo || page.imageinfo.length === 0) return;
var historyEl = document.getElementById('clbi-gallery-history');
var sectionEl = document.getElementById('clbi-gallery-history-section');
historyEl.innerHTML = '';
page.imageinfo.forEach(function(info, idx) {
var wrap = document.createElement('div');
wrap.style.cssText = 'position:relative;cursor:pointer;';
var img = document.createElement('img');
img.src = info.url;
img.style.cssText =
'width:72px;height:72px;object-fit:cover;border-radius:8px;border:2px solid #444;flex-shrink:0;';
if (idx === 0) {
img.style.borderColor = '#854369';
var badge = document.createElement('div');
badge.textContent = '현재';
badge.style.cssText =
'position:absolute;bottom:4px;left:50%;transform:translateX(-50%);background:#854369;color:#fff;font-size:9px;padding:1px 6px;border-radius:10px;';
wrap.appendChild(badge);
}
img.addEventListener('mouseenter', function() {
if (idx !== 0) img.style.borderColor = '#854369';
});
img.addEventListener('mouseleave', function() {
if (idx !== 0) img.style.borderColor = '#444';
});
img.addEventListener('click', function() {
fetch(info.url)
.then(function(r) {
return r.blob();
})
.then(function(blob) {
selectedFile = new File([blob], 'profile.png', { type: 'image/png' });
document.getElementById('pref-pfp-preview').src = URL.createObjectURL(blob);
gModal.style.display = 'none';
document.getElementById('pref-pfp-btn').textContent = '✓ 사진 선택됨';
});
});
wrap.appendChild(img);
historyEl.appendChild(wrap);
});
sectionEl.style.display = 'block';
});
}
function openCrop(src) {
cropImage.src = src;
cModal.style.display = 'flex';
if (cropper) {
cropper.destroy();
cropper = null;
}
setTimeout(function() {
cropper = new Cropper(cropImage, {
aspectRatio: 1,
viewMode: 1,
dragMode: 'move',
autoCropArea: 0.8,
cropBoxResizable: true,
cropBoxMovable: true
});
}, 150);
}
document.getElementById('pref-pfp-btn').addEventListener('click', function() {
openGallery();
});
document.getElementById('clbi-gallery-upload-btn').addEventListener('click', function() {
pfpInput.click();
});
document.getElementById('clbi-gallery-close').addEventListener('click', function() {
gModal.style.display = 'none';
});
pfpInput.addEventListener('change', function() {
var file = this.files[0];
if (!file) return;
gModal.style.display = 'none';
var reader = new FileReader();
reader.onload = function(e) {
openCrop(e.target.result);
};
reader.readAsDataURL(file);
});
document.getElementById('clbi-crop-cancel').addEventListener('click', function() {
cModal.style.display = 'none';
if (cropper) {
cropper.destroy();
cropper = null;
}
pfpInput.value = '';
});
document.getElementById('clbi-crop-confirm').addEventListener('click', function() {
if (!cropper) return;
var canvas = cropper.getCroppedCanvas({ width: 256, height: 256 });
if (!canvas) return;
canvas.toBlob(function(blob) {
selectedFile = new File([blob], 'profile.png', { type: 'image/png' });
document.getElementById('pref-pfp-preview').src = URL.createObjectURL(blob);
cModal.style.display = 'none';
cropper.destroy();
cropper = null;
document.getElementById('pref-pfp-btn').textContent = '✓ 사진 선택됨';
}, 'image/png');
});
var emailSaveBtn = document.getElementById('pref-email-save');
if (emailSaveBtn) {
emailSaveBtn.addEventListener('click', function() {
var statusEl = document.getElementById('pref-email-status');
var newEmail = document.getElementById('pref-new-email').value;
var password = document.getElementById('pref-email-password').value;
if (!newEmail || !password) {
statusEl.textContent = '이메일과 비밀번호를 입력해주세요.';
return;
}
statusEl.textContent = '변경 중...';
api.postWithToken('csrf', {
action: 'changeemail',
email: newEmail,
password: password
}).then(function() {
statusEl.textContent = '✓ 이메일 변경됨';
document.getElementById('pref-new-email').value = '';
document.getElementById('pref-email-password').value = '';
setTimeout(function() {
statusEl.textContent = '';
}, 3000);
}).fail(function(code, data) {
var msg = data && data.error && data.error.info ? data.error.info : '변경 실패';
statusEl.textContent = msg;
});
});
}
saveBtn.addEventListener('click', function() {
var statusEl = document.getElementById('pref-status');
statusEl.textContent = '저장 중...';
var promises = [];
if (selectedFile) {
var username = mw.config.get('wgUserName');
promises.push(
api.postWithToken('csrf', {
action: 'upload',
filename: 'Pfp-' + username + '.png',
ignorewarnings: true,
file: selectedFile,
format: 'json'
}, {
contentType: 'multipart/form-data'
})
);
}
var fields = ['name', 'discord', 'role', 'bio'];
if (isAdmin) fields.push('badges');
for (var i = 0; i < fields.length; i++) {
var el = document.getElementById('pref-' + fields[i]);
if (!el) continue;
promises.push(
api.postWithToken('csrf', {
action: 'options',
optionname: 'profile-' + fields[i],
optionvalue: el.value
})
);
}
$.when.apply($, promises)
.then(function() {
statusEl.textContent = '✓ 저장됨';
selectedFile = null;
document.getElementById('pref-pfp-btn').textContent = '사진 선택';
setTimeout(function() {
statusEl.textContent = '';
}, 2000);
})
.fail(function() {
statusEl.textContent = '저장 실패';
});
});
}
/* =========================================
Banner / CRT Page Monitor thumbnail slices
- base 이미지는 틀에 들어간 파일 문법 그대로 사용
- slice 레이어에는 300px MediaWiki 썸네일만 삽입
========================================= */
(function ($, mw) {
var thumbCache = {};
function parseSliceWidth(value) {
var parsed = parseInt(value, 10);
if (!isFinite(parsed) || parsed < 120) {
return 300;
}
return parsed;
}
function getImageSrc(img) {
return img ? (img.currentSrc || img.getAttribute('src') || img.src || '') : '';
}
function getFileNameFromSrc(src) {
var a;
var parts;
var fileName;
if (!src) return '';
a = document.createElement('a');
a.href = src;
parts = (a.pathname || '').split('/').filter(function (part) {
return !!part;
});
if (!parts.length) return '';
fileName = parts.pop();
/*
* MediaWiki thumb URL 예시:
* /images/thumb/a/ab/File.png/1000px-File.png
* /images/thumb/a/ab/File.svg/1000px-File.svg.png
*
* 이 경우 실제 파일명은 마지막 조각이 아니라 그 앞 조각이다.
*/
if (/^\d+px-/.test(fileName) && parts.length) {
fileName = parts.pop();
}
fileName = fileName.replace(/^\d+px-/, '');
try {
fileName = decodeURIComponent(fileName);
} catch (e) {}
return fileName;
}
function resolveThumbUrl(img, width, callback) {
var src = getImageSrc(img);
var fileName = getFileNameFromSrc(src);
var cacheKey;
var entry;
if (!src) return;
if (!fileName || !mw || !mw.loader) {
callback(src);
return;
}
cacheKey = fileName + '|' + width;
entry = thumbCache[cacheKey];
if (entry) {
if (entry.resolved) {
callback(entry.url || src);
} else {
entry.callbacks.push(callback);
}
return;
}
entry = {
resolved: false,
url: '',
callbacks: [callback]
};
thumbCache[cacheKey] = entry;
function finish(url) {
var callbacks = entry.callbacks.slice();
var i;
entry.resolved = true;
entry.url = url || src;
entry.callbacks = [];
for (i = 0; i < callbacks.length; i++) {
callbacks[i](entry.url);
}
}
mw.loader.using('mediawiki.api').done(function () {
var api = new mw.Api();
api.get({
action: 'query',
titles: 'File:' + fileName,
prop: 'imageinfo',
iiprop: 'url',
iiurlwidth: width,
formatversion: 2
}).done(function (data) {
var page;
var info;
if (
data &&
data.query &&
data.query.pages &&
data.query.pages.length
) {
page = data.query.pages[0];
if (
page &&
page.imageinfo &&
page.imageinfo.length
) {
info = page.imageinfo[0];
}
}
finish((info && (info.thumburl || info.url)) || src);
}).fail(function () {
finish(src);
});
}).fail(function () {
finish(src);
});
}
function applySliceImages(frame, thumbUrl) {
var slices;
var i;
var img;
if (!frame || !thumbUrl) return;
slices = frame.querySelectorAll('.crt-page-monitor-slice');
for (i = 0; i < slices.length; i++) {
slices[i].innerHTML = '';
img = document.createElement('img');
img.className = 'crt-page-monitor-slice-img';
img.src = thumbUrl;
img.alt = '';
img.decoding = 'async';
img.loading = 'eager';
img.setAttribute('aria-hidden', 'true');
slices[i].appendChild(img);
}
frame.setAttribute('data-crt-slices-ready', '1');
}
function initBannerFrame(frame) {
var baseImg;
var width;
if (!frame) return;
if (frame.getAttribute('data-crt-slices-ready') === '1') return;
baseImg = frame.querySelector('.crt-page-monitor-image-base img');
if (!baseImg) return;
width = parseSliceWidth(frame.getAttribute('data-crt-slice-width'));
resolveThumbUrl(baseImg, width, function (thumbUrl) {
if (!frame || !frame.parentNode) return;
applySliceImages(frame, thumbUrl);
});
}
function initBannerFrames(root) {
var scope = root && root.querySelectorAll ? root : document;
var frames = scope.querySelectorAll('.crt-page-monitor-frame');
var i;
for (i = 0; i < frames.length; i++) {
initBannerFrame(frames[i]);
}
}
$(function () {
initBannerFrames(document);
});
if (mw && mw.hook) {
mw.hook('wikipage.content').add(function ($content) {
initBannerFrames($content && $content[0] ? $content[0] : document);
});
}
})(jQuery, window.mw);
/* =========================================
Doc Tab System — Q/E 단축키 탭 전환
글리치 플리커 + RGB split + 방향 슬라이드
========================================= */
(function () {
'use strict';
var keydownBound = false;
function initDocTabs() {
var tabBars = document.querySelectorAll('.doc-tab-bar');
if (!tabBars.length) return;
tabBars.forEach(function (bar) {
if (bar.getAttribute('data-tabs-init')) return;
bar.setAttribute('data-tabs-init', '1');
var tabs = Array.from(bar.querySelectorAll('.doc-tab'));
if (!tabs.length) return;
var panel = bar.closest('.doc-panel');
var display = panel ? panel.querySelector('.doc-display') : null;
if (!display) display = document.getElementById('doc-main-display');
if (!display) return;
tabs.forEach(function (tab, i) {
tab.addEventListener('click', function () {
var currentIdx = tabs.findIndex(function (t) {
return t.classList.contains('active');
});
if (currentIdx === i) return;
switchTab(tabs, display, i, i > currentIdx ? 1 : -1);
});
});
var initIdx = tabs.findIndex(function (t) { return t.classList.contains('active'); });
if (initIdx !== -1) {
var initRef = tabs[initIdx].dataset.ref;
var initEl = initRef ? document.getElementById(initRef) : null;
display.innerHTML = initEl ? initEl.innerHTML : (tabs[initIdx].dataset.content || '');
}
});
if (!keydownBound) {
keydownBound = true;
document.addEventListener('keydown', function (e) {
var tag = document.activeElement.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
var bar = document.querySelector('.doc-tab-bar');
if (!bar) return;
var tabs = Array.from(bar.querySelectorAll('.doc-tab'));
var panel = bar.closest('.doc-panel');
var display = panel ? panel.querySelector('.doc-display') : null;
if (!display) display = document.getElementById('doc-main-display');
if (!display) return;
var activeIdx = tabs.findIndex(function (t) {
return t.classList.contains('active');
});
if (activeIdx === -1) return;
if (e.key === 'q' || e.key === 'Q') {
e.preventDefault();
var prev = (activeIdx - 1 + tabs.length) % tabs.length;
if (prev !== activeIdx) switchTab(tabs, display, prev, -1);
} else if (e.key === 'e' || e.key === 'E') {
e.preventDefault();
var next = (activeIdx + 1) % tabs.length;
if (next !== activeIdx) switchTab(tabs, display, next, 1);
}
});
}
}
var isAnimating = false;
function switchTab(tabs, display, nextIdx, dir) {
if (isAnimating) return;
isAnimating = true;
tabs.forEach(function (t) { t.classList.remove('active'); });
tabs[nextIdx].classList.add('active');
var ref = tabs[nextIdx].dataset.ref;
var nextContent;
if (ref) {
var refEl = document.getElementById(ref);
nextContent = refEl ? refEl.innerHTML : '';
} else {
nextContent = tabs[nextIdx].dataset.content || '';
}
glitchOut(display, dir, function () {
display.innerHTML = nextContent;
glitchIn(display, dir, function () {
isAnimating = false;
});
});
}
function glitchOut(el, dir, cb) {
var duration = 160;
var start = null;
var slideX = dir * 16;
function step(ts) {
if (!start) start = ts;
var p = Math.min((ts - start) / duration, 1);
var ease = p * p;
var tx = slideX * ease;
var skew = dir * ease * 1.0;
var opacity = 1 - ease;
var rgb = ease * 5;
el.style.transform = 'translateX(' + tx + 'px) skewX(' + skew + 'deg)';
el.style.opacity = opacity;
el.style.filter =
'drop-shadow(' + (-rgb) + 'px 0 0 rgba(80,160,255,0.75)) ' +
'drop-shadow(' + rgb + 'px 0 0 rgba(255,55,90,0.65)) ' +
'brightness(' + (1 + ease * 0.25) + ')';
if (p < 1) {
requestAnimationFrame(step);
} else {
el.style.opacity = '0';
cb();
}
}
requestAnimationFrame(step);
}
function glitchIn(el, dir, cb) {
var duration = 200;
var start = null;
var startX = -dir * 16;
el.style.transform = 'translateX(' + startX + 'px) skewX(' + (-dir * 1.0) + 'deg)';
el.style.opacity = '0';
function step(ts) {
if (!start) start = ts;
var p = Math.min((ts - start) / duration, 1);
var ease = 1 - Math.pow(1 - p, 3);
var tx = startX * (1 - ease);
var skew = -dir * 1.0 * (1 - ease);
var opacity = ease;
var rgb = (1 - ease) * 3;
var brightness = 1 + (1 - ease) * 0.35;
el.style.transform = 'translateX(' + tx + 'px) skewX(' + skew + 'deg)';
el.style.opacity = opacity;
el.style.filter =
'drop-shadow(' + (-rgb) + 'px 0 0 rgba(80,160,255,0.65)) ' +
'drop-shadow(' + rgb + 'px 0 0 rgba(255,55,90,0.55)) ' +
'brightness(' + brightness + ')';
if (p < 1) {
requestAnimationFrame(step);
} else {
el.style.transform = '';
el.style.opacity = '';
el.style.filter = '';
cb();
}
}
requestAnimationFrame(step);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDocTabs);
} else {
initDocTabs();
}
if (typeof mw !== 'undefined' && mw.hook) {
mw.hook('wikipage.content').add(function () {
initDocTabs();
});
}
})();
/* =========================================
Doc Section Switch — 좌측 섹션 전환
========================================= */
$(document).on('click', '.doc-nav-item[data-section]', function () {
var name = $(this).attr('data-section');
var display = document.getElementById('doc-main-display');
var titleEl = document.getElementById('doc-center-title');
var tabBar = document.getElementById('doc-tab-bar-text');
if (!display) return;
$('.doc-nav-item[data-section]').removeClass('active');
$('.doc-nav-item[data-section="' + name + '"]').addClass('active');
if (name === 'text') {
if (titleEl) titleEl.textContent = '개요';
if (tabBar) $(tabBar).show();
var activeTab = tabBar ? tabBar.querySelector('.doc-tab.active') : null;
if (!activeTab && tabBar) activeTab = tabBar.querySelector('.doc-tab');
if (activeTab) {
var ref = activeTab.dataset.ref;
var refEl = ref ? document.getElementById(ref) : null;
display.innerHTML = refEl ? refEl.innerHTML : (activeTab.dataset.content || '');
}
} else {
if (titleEl) titleEl.textContent = name === 'factions' ? '세력' : name === 'people' ? '인물' : name;
if (tabBar) $(tabBar).hide();
var refEl = document.getElementById('doc-content-' + name);
display.innerHTML = refEl ? refEl.innerHTML : '';
}
});