참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.

  • 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
  • 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
  • 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
  • 오페라: Ctrl-F5를 입력.
mw.loader.load('https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js');
mw.loader.load('/index.php?title=MediaWiki:DevTools.js&action=raw&ctype=text/javascript');
mw.loader.load('/index.php?title=MediaWiki:LangDrawer.js&action=raw&ctype=text/javascript');
mw.loader.load('/index.php?title=MediaWiki:CategoryNav.js&action=raw&ctype=text/javascript');

function loadLangScript(done) {
    $.getScript('/index.php?title=미디어위키:Lang.js&action=raw&ctype=text/javascript')
        .done(function() {
            if (typeof done === 'function') done();
        })
        .fail(function(a, b, c) {
            console.error('Lang.js load failed:', b, c);
            if (typeof done === 'function') done();
        });
}

$(function() {
    $('body').prepend(
        '<div class="WW-bg" style="position:fixed;top:0;left:0;width:100%;height:100vh;"></div>'
    );

// ── 상단 네비게이션 바 ──
var navHtml =
    '<div id="clbi-top-nav-wrap">' +
        '<div id="clbi-top-nav">' +

            '<div id="clbi-top-nav-main">' +

                '<div id="clbi-top-nav-tabs">' +
'<div class="clbi-top-nav-item clbi-tnav-lang" id="clbi-tnav-lang">' +
    '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-language-001.png" alt="">' +
    '<span class="clbi-tnav-lang-bottom">' +
        '<span class="clbi-tnav-lang-code" id="clbi-tnav-lang-code">KR</span>' +
        '<span class="clbi-tnav-arrow">▾</span>' +
    '</span>' +
'</div>' +
'<a class="clbi-top-nav-item" href="/index.php/대문">' +
    '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-main-menu-001.png" alt="">' +
    '<span class="clbi-tnav-label">메인 메뉴</span>' +
'</a>' +
'<a class="clbi-top-nav-item" href="/index.php/프로젝트:소개">' +
    '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-project-001.png" alt="">' +
    '<span class="clbi-tnav-label">프로젝트</span>' +
'</a>' +
'<div class="clbi-top-nav-item" id="clbi-tnav-worldbuilding">' +
    '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-worldbuilding-001.png" alt="">' +
    '<span class="clbi-tnav-label">세계관</span>' +
    '<span class="clbi-tnav-arrow">▾</span>' +
'</div>' +
'</div>' +

                '<div id="clbi-top-nav-search">' +
                    '<input type="text" id="clbi-top-search-input" placeholder="검색...">' +
                '</div>' +

                '<div id="clbi-top-nav-right">' +
                    '<div class="clbi-top-nav-item" id="clbi-tnav-info">' +
                        '<span class="clbi-tnav-label">ℹ</span>' +
                    '</div>' +
                '</div>' +

            '</div>' +

'<div id="clbi-sub-worldbuilding">' +
    '<div id="clbi-sub-worldbuilding-inner">' +
        '<div class="clbi-tnav-sub-list">' +
'<a class="clbi-tnav-sub-item" href="/index.php/역사적_사건">역사적 사건</a>' +
'<a class="clbi-tnav-sub-item" href="/index.php/설정">설정</a>' +
'<a class="clbi-tnav-sub-item" href="/index.php/국가_및_조합">국가 및 조합</a>' +
'<a class="clbi-tnav-sub-item" href="/index.php/기업_및_공동체">기업 및 공동체</a>' +
'<a class="clbi-tnav-sub-item" href="/index.php/군_정치집단">군, 정치집단</a>' +
'<a class="clbi-tnav-sub-item" href="/index.php/인물">인물</a>' +
        '</div>' +
    '</div>' +
'</div>' +

        '</div>' +
    '</div>';

$('.content-wrapper').before(navHtml);

$('#clbi-tnav-worldbuilding').on('click', function() {
    var $menu = $('#clbi-sub-worldbuilding');
    var $btn = $(this);

    $menu.toggleClass('worldbuilding-open');
    $btn.toggleClass('clbi-tnav-active', $menu.hasClass('worldbuilding-open'));
});

$('#clbi-top-search-input').on('keydown', function(e) {
    if (e.key === 'Enter') {
        var q = $(this).val().trim();
        if (q) window.location.href = '/index.php?search=' + encodeURIComponent(q);
    }
});

});

// 페이지 전환 사운드
var transitionSound = new Audio('/index.php?title=특수:Redirect/file/Sfx-ui-001.mp3');

(function() {
    var master = parseFloat(localStorage.getItem('clbi-audio-master') || 80) / 100;
    var sfx = parseFloat(localStorage.getItem('clbi-audio-sfx') || 60) / 100;
    var sfxOn = localStorage.getItem('clbi-audio-sfxOn') !== 'false';
    transitionSound.volume = sfxOn ? master * sfx : 0;
})();

function playStaticSound() {
    var master = parseFloat(localStorage.getItem('clbi-audio-master') || 80) / 100;
    var sfx = parseFloat(localStorage.getItem('clbi-audio-sfx') || 60) / 100;
    var sfxOn = localStorage.getItem('clbi-audio-sfxOn') !== 'false';

    if (!sfxOn) return;

    transitionSound.volume = master * sfx;
    transitionSound.currentTime = 0;
    transitionSound.play();
}

// 현재 언어 감지
function getCurrentLang() {
    var langData = document.getElementById('clbi-lang-data');
    return langData ? (langData.getAttribute('data-lang') || 'ko') : 'ko';
}

function normalizePageName(value) {
    return String(value || '')
        .split('?')[0]
        .replace(/^\/index\.php\//, '')
        .replace(/_/g, ' ')
        .trim();
}

function buildWikiPath(title) {
    return '/index.php/' + encodeURI(String(title || '').replace(/ /g, '_'));
}

function loadRecentChangesList(targetSelector, limit) {
    var $target = $(targetSelector);

    if (!$target.length) return;

    var lang = getCurrentLang();
    var t = (window.LANG && window.LANG[lang]) ? window.LANG[lang] : (window.LANG ? window.LANG.ko : null);

    $target.html((t && t.loading) ? t.loading : '불러오는 중...');

    $.getJSON(
        '/api.php?action=query&list=recentchanges&rclimit=' + encodeURIComponent(limit || 5) + '&rcprop=title|timestamp&format=json&rcnamespace=0&rctype=edit|new',
        function(data) {
            var items = data && data.query ? data.query.recentchanges : [];
            var html = '';

            if (!items || !items.length) {
                $target.html('표시할 변경 사항이 없습니다.');
                return;
            }

            $.each(items, function(i, item) {
                var label = timeAgo(item.timestamp);

                html +=
                    '<div class="clbi-recent-item">' +
                        '<div class="clbi-recent-title-wrap">' +
                            '<a href="/index.php/' + encodeURIComponent(item.title) + '" class="clbi-recent-title">' + item.title + '</a>' +
                        '</div>' +
                        '<span class="clbi-recent-time">' + label + '</span>' +
                    '</div>';
            });

            $target.html(html);

            $target.find('.clbi-recent-item').each(function() {
                var wrap = $(this).find('.clbi-recent-title-wrap');
                var title = $(this).find('.clbi-recent-title');
                var wrapW = wrap.width();
                var titleW = title[0].scrollWidth;

                if (titleW > wrapW + 20) {
                    var duration = titleW / 40;

                    title.css({
                        animation: 'clbi-scroll ' + duration + 's linear infinite',
                        '--scroll-dist': '-' + (titleW - wrapW + 8) + 'px'
                    });
                }
            });
        }
    ).fail(function() {
        var lang = getCurrentLang();
        var t = (window.LANG && window.LANG[lang]) ? window.LANG[lang] : (window.LANG ? window.LANG.ko : null);

        $target.html((t && t.loadFail) ? t.loadFail : '불러오기 실패');
    });
}

// 국가_및_조합 전용 왼쪽 사이드바 이미지
function updateLeftSidebarNationsImage() {
    $('#clbi-left-nations-image').remove();
}

// 사이드바 업데이트
function updateSidebar() {
    if (!window.LANG) {
        setTimeout(updateSidebar, 100);
        return;
    }

    var currentLang = getCurrentLang();
    var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;

    var newsTitle = t.news || '뉴스';
    var changelogTitle = t.changelog || '체인지로그';
    var recentTitle = t.recentChanges || '최근 변경';

    $('#clbi-title-left-news').text(newsTitle);
    $('#clbi-left-news-changelog-main').text(changelogTitle);
    $('#clbi-left-news-recent-main').text(recentTitle);

    $('#clbi-title-search a').text(t.search);
    $('#clbi-search-input').attr('placeholder', t.search + '...');
    $('#clbi-title-recent a').text(recentTitle);
    $('#clbi-title-guide-label').text(t.guide);
    $('#clbi-guide-link').text(t.getStarted);
    $('#clbi-title-links-label').text(t.links);

    $('#clbi-btn-contribution').text(t.contribution);
    $('#clbi-btn-watchlist').text(t.watchlist);
    $('#clbi-btn-preferences').text(t.preferences);
    $('#clbi-btn-logout').text(t.logout);
    $('#clbi-btn-login').text(t.login);

    var pageName = normalizePageName(mw.config.get('wgPageName'));
    var specialPage = String(mw.config.get('wgCanonicalSpecialPageName') || '');

    $('#clbi-left-news-changelog-main').text(changelogTitle);
    $('#clbi-left-news-recent-title').text('RECENT CHANGES');

    $('.clbi-user-btn').removeClass('clbi-user-btn-active');

    if (
        specialPage === 'Contributions' ||
        specialPage === '기여' ||
        pageName.indexOf('특수:기여') === 0 ||
        pageName.indexOf('Special:Contributions') === 0
    ) {
        $('#clbi-btn-contribution').addClass('clbi-user-btn-active');
    }

    if (specialPage === 'Watchlist') {
        $('#clbi-btn-watchlist').addClass('clbi-user-btn-active');
    }

    if (
        specialPage === '설정' ||
        pageName === '특수:설정' ||
        pageName === 'Special:설정'
    ) {
        $('#clbi-btn-preferences').addClass('clbi-user-btn-active');
    }

    $('.toggleBtn').each(function() {
        var btn = $(this);

        if (!$('#' + btn.data('target')).hasClass('folding-open')) {
            btn.text(t.expand);
        } else {
            btn.text(t.collapse);
        }
    });

    updateLeftSidebarNationsImage();
}

function canShowContentTools() {
    // 비로그인 사용자는 편집/역사/공유 버튼을 숨김
    if (!mw.config.get('wgUserName')) {
        return false;
    }

    // MediaWiki가 현재 문서를 편집 가능하지 않다고 판단하면 숨김
    var isEditable = mw.config.get('wgIsProbablyEditable');
    if (isEditable === false) {
        return false;
    }

    return true;
}
function moveCatlinksToBottom() {
    var main = $('.liberty-content-main');
    var parserOutput = $('.liberty-content-main .mw-parser-output').first();
    var catlinks = $('.catlinks');

    if (!main.length || !catlinks.length) return;

    catlinks.each(function () {
        var cat = $(this);

        if (parserOutput.length) {
            cat.appendTo(parserOutput);
        } else {
            cat.appendTo(main);
        }
    });
}

// 대문 스타일
function initCategoryNavIfAvailable(root) {
    /*
    CategoryNav.js는 대문 카테고리 네비를 SVG로 생성한다.

    Common.js가 SPA로 본문을 갈아끼운 뒤에는 MediaWiki 원래 페이지 로드와 달리
    CategoryNav.js의 초기 DOMContentLoaded만으로는 새 mount를 다시 잡지 못할 수 있다.
    CategoryNav.js 자체도 mw.hook('wikipage.content')를 듣지만, 로드 순서와 SPA 타이밍이
    엇갈릴 수 있으므로 Common.js 쪽에서도 존재 여부를 확인한 뒤 한 번 더 호출한다.

    이 함수는 CategoryNav.js가 아직 로드되지 않았으면 아무 것도 하지 않는다.
    */
    if (
        window.CLBI &&
        window.CLBI.categoryNav &&
        typeof window.CLBI.categoryNav.init === 'function'
    ) {
        window.CLBI.categoryNav.init(root || document);
    }
}

function removeLegacyMainPageHero() {
    /*
    기존 대문 전용 레거시 요소 정리
    -----------------------------------------
    이전 대문 구조에서는 Common.js가 본문 바깥에 #clbi-main-logo를 직접 삽입하고,
    본문 안의 #clbi-main-crt-hero를 #clbi-main-crt-hero-wrap으로 감싸서
    .liberty-content-main 위쪽으로 재배치했다.

    새 대문은 본문 내부의 .main-portal이 로고, 알림, 카테고리 네비, 이미지 피드,
    방명록, 상태 패널을 모두 담당한다. 따라서 Common.js가 별도 로고나 CRT 래퍼를
    삽입하면 새 로고/콘텐츠와 중복된다.

    여기서는 JS가 만들던 바깥 로고와 CRT 래퍼를 제거하고, 예전 대문 원본이나
    캐시된 렌더 결과에 남아 있을 수 있는 #clbi-main-crt-hero도 제거한다.
    */
    $('#clbi-main-logo').remove();
    $('#clbi-main-crt-hero-wrap').remove();
    $('#clbi-main-crt-hero').remove();
}

function applyMainPageStyle() {
    var specialPage = mw.config.get('wgCanonicalSpecialPageName');
    if (specialPage === 'Preferences') return;

    var pageName = normalizePageName(mw.config.get('wgPageName'));
    var isMainPage = (pageName === '대문');
    var isScreenDoc = ($('.screen-header').length > 0);
    var hideTools = (isMainPage || !canShowContentTools());

    // 모든 문서에서 분류 바를 본문 컨테이너 아래로 이동
    moveCatlinksToBottom();

    if (isMainPage) {
        $('.liberty-content-header').css('display', 'none');
        $('.mw-page-title-main').addClass('clbi-hide');
        $('.catlinks').css('display', 'none');
        $('.liberty-content-main').css('border-radius', '5px');

        // 새 대문은 .main-portal 본문 구조가 로고/히어로를 담당한다.
        // Common.js의 구식 바깥 로고/CRT 재배치 루틴은 사용하지 않는다.
        removeLegacyMainPageHero();
        $('#clbi-tools-box').remove();

        $('.content-tools').css('display', 'none');
        $('.liberty-content').addClass('content-tools-hidden');

        initCategoryNavIfAvailable(document);

    } else if (isScreenDoc) {
        $('.liberty-content-header').css('display', 'none');
        $('.mw-page-title-main').addClass('clbi-hide');
        $('.catlinks').css('display', '');
        $('.liberty-content-main').css('border-radius', '5px');

        $('#clbi-main-logo').remove();
        $('#clbi-main-crt-hero-wrap').remove();

        if ($('#clbi-tools-box').length === 0 && canShowContentTools()) {
            var $toolsBox = $('<div id="clbi-tools-box" class="clbi-left-box"></div>');
            var $toolsTitle = $('<div class="clbi-left-title"><span class="clbi-icon" style="--icon:var(--ic-ui-003)"></span> 관리</div>');
            var $toolsContent = $('<div class="clbi-left-content"></div>');

            $toolsContent.append($('.content-tools .btn-group').clone(true));
            $toolsBox.append($toolsTitle).append($toolsContent);
            $('#clbi-left-sidebar').append($toolsBox);
        }

        $('.content-tools').css('display', 'none');
        $('.liberty-content').addClass('content-tools-hidden');

    } else {
        $('.liberty-content-header').css('display', '');
        $('.mw-page-title-main').removeClass('clbi-hide');
        $('.catlinks').css('display', '');
        $('.liberty-content-main').css('border-radius', '5px');

        $('#clbi-main-logo').remove();
        $('#clbi-main-crt-hero-wrap').remove();
        $('#clbi-tools-box').remove();
    }

    if (hideTools) {
        $('.content-tools').css('display', 'none');
        $('.liberty-content').addClass('content-tools-hidden');
    } else if (!isScreenDoc) {
        $('.content-tools').css('display', '');
        $('.liberty-content').removeClass('content-tools-hidden');
    }

    updateSidebar();
}

// 본문 기본 목차 제거
function removeNativeTocFromContent() {
    $('.liberty-content-main #toc, .liberty-content-main .toc').remove();
}

// 왼쪽 목차: MediaWiki 문단 ID 가져오기
function getHeadingId(heading) {
    if (heading.id) {
        return heading.id;
    }

    var headline = heading.querySelector('.mw-headline[id]');
    if (headline && headline.id) {
        return headline.id;
    }

    return '';
}

// 왼쪽 목차: MediaWiki 문단 제목 텍스트 가져오기
function getHeadingText(heading) {
    var headline = heading.querySelector('.mw-headline');
    var source = headline || heading;
    var clone = source.cloneNode(true);

    $(clone).find('.mw-editsection, .mw-editsection-bracket, .mw-editsection-divider').remove();

    return (clone.textContent || '')
        .replace(/\s+/g, ' ')
        .trim();
}

// 왼쪽 목차: 긴 제목에 자동 스크롤 적용
function initTocTitleScroll(root) {
    var $items = root
        ? $(root).find('.toc-scroll-text')
        : $('#side-toc-box .toc-scroll-text');

    $items.each(function () {
        var $text = $(this);
        var $wrap = $text.closest('.toc-scroll-wrap');

        if (!$wrap.length) return;

        var wrapW = Math.floor($wrap.width());
        var textW = Math.ceil(this.scrollWidth);

        // 왼쪽 목차: 레이아웃 계산이 끝나지 않았으면 이번 실행에서는 건드리지 않는다.
        if (!wrapW || !textW) return;

        if (textW <= wrapW + 12) {
            // 왼쪽 목차: 칸을 넘지 않는 제목은 전체 텍스트를 그대로 보여준다.
            $wrap.removeClass('is-scrolling');

            if ($text.data('toc-scroll-enabled')) {
                $text.css({
                    animation: '',
                    'animation-delay': '',
                    '--scroll-dist': ''
                });
                $text.removeData('toc-scroll-enabled');
                $text.removeData('toc-scroll-key');
            }

            return;
        }

        var scrollDist = '-' + (textW - wrapW + 10) + 'px';
        var duration = Math.max(7, textW / 38) * 1.25;
        var scrollKey = scrollDist + '|' + duration;

        // 왼쪽 목차: 긴 제목에는 오른쪽 페이드와 스크롤을 적용한다.
        $wrap.addClass('is-scrolling');

        // 왼쪽 목차: 같은 값으로 이미 적용된 애니메이션은 다시 초기화하지 않는다.
        if ($text.data('toc-scroll-key') === scrollKey) {
            return;
        }

        $text.data('toc-scroll-enabled', true);
        $text.data('toc-scroll-key', scrollKey);

        $text.css({
            // 왼쪽 목차: 페이지 진입 직후에는 잠시 읽을 시간을 준 뒤 흐르게 한다.
            animation: 'toc-scroll-blink-reset ' + duration + 's linear infinite',
            'animation-delay': '1s',
            '--scroll-dist': scrollDist
        });
    });
}

// 목차를 왼쪽 사이드바에 새로 생성
function moveTocToLeftSidebar() {
    // 왼쪽 목차: MediaWiki가 만든 원래 목차는 본문에서 제거한다.
    removeNativeTocFromContent();

    var leftSidebar = document.getElementById('clbi-left-sidebar');
    if (!leftSidebar) return;

    var content =
        document.querySelector('.liberty-content-main .mw-parser-output') ||
        document.querySelector('.liberty-content-main');

    if (!content) return;

    var headings = Array.prototype.slice.call(
        content.querySelectorAll('h2, h3')
    ).filter(function (heading) {
        if (heading.closest('#toc, .toc, #side-toc-box')) return false;

        var id = getHeadingId(heading);
        var text = getHeadingText(heading);

        if (!id || !text) return false;

        return true;
    });

    var tocKey = headings.map(function (heading) {
        return getHeadingId(heading) + '|' + getHeadingText(heading);
    }).join('||');

    var existingBox = document.getElementById('side-toc-box');

    // 왼쪽 목차: 같은 문서에서 같은 목차를 이미 만들었다면 다시 지우고 만들지 않는다.
    if (existingBox && existingBox.getAttribute('data-toc-key') === tocKey) {
        initTocTitleScroll(existingBox);
        return;
    }

    if (existingBox) {
        existingBox.remove();
    }

    if (!headings.length) return;

    var tocBox = document.createElement('div');
    tocBox.className = 'clbi-left-box';
    tocBox.id = 'side-toc-box';
    tocBox.setAttribute('data-toc-key', tocKey);

    var title = document.createElement('div');
    title.className = 'clbi-left-title';

    // 왼쪽 목차: 박스 제목은 Lang.js의 현재 UI 언어를 따른다.
    var currentLang = getCurrentLang();
    var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
    var tocTitleText = (t && t.toc) ? t.toc : '목차';

    title.innerHTML =
        '<span class="clbi-icon" style="--icon:var(--ic-ui-002)"></span> ' + tocTitleText;

    var body = document.createElement('div');
    body.className = 'clbi-left-content toc-sidebar-content';

    var list = document.createElement('ul');
    list.className = 'generated-toc';

    headings.forEach(function (heading) {
        var id = getHeadingId(heading);
        var text = getHeadingText(heading);
        var level = heading.tagName.toLowerCase() === 'h3' ? 3 : 2;

        var item = document.createElement('li');
        item.className = 'toc-level-' + level;

        var link = document.createElement('a');
        link.setAttribute('href', '#' + id);

        // 왼쪽 목차: 긴 제목 스크롤을 위해 텍스트를 별도 span으로 감싼다.
        var textWrap = document.createElement('span');
        textWrap.className = 'toc-scroll-wrap';

        var textSpan = document.createElement('span');
        textSpan.className = 'toc-scroll-text';
        textSpan.textContent = text;

        textWrap.appendChild(textSpan);
        link.appendChild(textWrap);

        item.appendChild(link);
        list.appendChild(item);
    });

    body.appendChild(list);
    tocBox.appendChild(title);
    tocBox.appendChild(body);

    // 왼쪽 목차: 사용자 박스 바로 아래, 뉴스 박스 위에 배치한다.
    var userBox = document.getElementById('clbi-user-box');
    var newsBox = document.getElementById('clbi-left-news-box');

    if (userBox && userBox.parentNode === leftSidebar) {
        if (userBox.nextSibling) {
            leftSidebar.insertBefore(tocBox, userBox.nextSibling);
        } else {
            leftSidebar.appendChild(tocBox);
        }
    } else if (newsBox && newsBox.parentNode === leftSidebar) {
        leftSidebar.insertBefore(tocBox, newsBox);
    } else {
        leftSidebar.appendChild(tocBox);
    }

    // 왼쪽 목차: DOM에 붙은 뒤 실제 폭을 계산해야 하므로 비동기 재측정한다.
    requestAnimationFrame(function () {
        initTocTitleScroll(tocBox);
    });

    setTimeout(function () {
        initTocTitleScroll(tocBox);
    }, 300);
}

// 초기화 함수
function initSidebars() {
    var userName = mw.config.get('wgUserName');
    var scriptPath = mw.config.get('wgScriptPath') || '';

    $('#clbi-left-sidebar, #clbi-right-sidebar, #clbi-user-box, #clbi-left-news-box, #clbi-left-search-box, #clbi-left-links-box').remove();

    var leftBoxes =
        '<div id="clbi-left-sidebar">' +

            '<div id="clbi-user-box" class="clbi-left-box">' +
                '<div class="clbi-left-title" id="clbi-title-user">' +
                    '<span class="clbi-icon" style="--icon:var(--ic-ui-005)"></span> 사용자' +
                '</div>' +
                '<div class="clbi-left-content">' +

                    (userName ?
                        '<div id="clbi-user-logged-in">' +
                            '<div id="clbi-user-avatar-wrap">' +
                                '<img id="clbi-user-avatar" src="/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(userName) + '.png">' +
                                '<div id="clbi-user-name">' + userName + '</div>' +
                            '</div>' +
                            '<div class="clbi-user-btn-grid">' +
                                '<a class="clbi-user-btn" id="clbi-btn-contribution" href="/index.php/특수:기여/' + encodeURIComponent(userName) + '">기여</a>' +
                                '<a class="clbi-user-btn" id="clbi-btn-watchlist" href="/index.php/특수:주시문서목록">주시문서</a>' +
                                '<a class="clbi-user-btn" id="clbi-btn-preferences" href="/index.php/특수:환경설정">설정</a>' +
                                '<a class="clbi-user-btn" id="clbi-btn-logout" href="/index.php/특수:로그아웃">로그아웃</a>' +
                            '</div>' +
                        '</div>'
                    :
                        '<div id="clbi-user-anon">' +
                            '<a class="clbi-user-btn" id="clbi-btn-login" href="/index.php/특수:로그인">로그인</a>' +
                        '</div>'
                    ) +

                '</div>' +
            '</div>' +

            '<div id="clbi-left-news-box" class="clbi-left-box">' +
                '<div class="clbi-left-title" id="clbi-title-left-news">' +
                    '<span class="clbi-icon" style="--icon:var(--ic-ui-010)"></span> 뉴스' +
                '</div>' +
                '<div class="clbi-left-content">' +
                    '<div class="clbi-left-news-section" id="clbi-left-news-changelog">' +
                        '<a id="clbi-left-news-changelog-main" class="clbi-left-news-main" href="/index.php/프로젝트:체인지로그">체인지로그</a>' +
                    '</div>' +
                    '<div class="clbi-left-news-section" id="clbi-left-news-recent">' +
                        '<div id="clbi-left-news-recent-title">RECENT CHANGES</div>' +
                        '<div id="clbi-left-news-recent-list"></div>' +
                    '</div>' +
                '</div>' +
            '</div>' +

            '<div id="clbi-left-search-box" class="clbi-left-box">' +
                '<div class="clbi-left-title" id="clbi-title-search">' +
                    '<span class="clbi-icon" style="--icon:var(--ic-ui-002)"></span> <a href="/index.php/특수:검색">검색</a>' +
                '</div>' +
                '<div class="clbi-left-content">' +
                    '<div id="clbi-search-form">' +
                        '<input type="text" id="clbi-search-input" placeholder="검색...">' +
                        '<button id="clbi-search-btn">›</button>' +
                    '</div>' +
                '</div>' +
            '</div>' +

            '<div id="clbi-left-links-box" class="clbi-left-box">' +
                '<div class="clbi-left-title" id="clbi-title-links">' +
                    '<span class="clbi-icon" style="--icon:var(--ic-ui-001)"></span> <span id="clbi-title-links-label">링크</span>' +
                '</div>' +
                '<div class="clbi-left-content">' +
                    '<a class="clbi-link-btn" href="/index.php/특수:최근바뀜">최근 바뀜</a>' +
                    '<a class="clbi-link-btn" href="/index.php/특수:임의문서">임의 문서</a>' +
                    '<a class="clbi-link-btn" href="/index.php/특수:분류">분류</a>' +
                '</div>' +
            '</div>' +

        '</div>';

$(function() {
    loadLangScript(function() {
        setTimeout(function() {
            initSidebars();
        }, 100);
    });
});

// SPA 네비게이션
function shouldSkip(url) {
    return url.match(/action=edit|action=submit|action=history|action=delete|action=protect|action=purge|특수:로그인|특수:로그아웃|Special:UserLogin|Special:UserLogout|특수:사용자정보|특수:비밀번호바꾸기|uselang=/);
}

$(function() {
    if (window._spaInitialized) return;
    window._spaInitialized = true;

    function isInternal(url) {
        var a = document.createElement('a');
        a.href = url;
        return a.hostname === window.location.hostname;
    }

    function loadPage(url) {
        fetch(url)
            .then(function(res) {
                return res.text();
            })
            .then(function(html) {
                var parser = new DOMParser();
                var doc = parser.parseFromString(html, 'text/html');

                var scripts = doc.querySelectorAll('script');
                for (var i = 0; i < scripts.length; i++) {
                    var src = scripts[i].textContent;

                    if (src.indexOf('wgNamespaceNumber') !== -1) {
                        var match = src.match(/"wgNamespaceNumber":(-?\d+)/);
                        if (match) mw.config.set('wgNamespaceNumber', parseInt(match[1], 10));

                        var matchTitle = src.match(/"wgTitle":"([^"]+)"/);
                        if (matchTitle) mw.config.set('wgTitle', matchTitle[1]);

                        var matchPage = src.match(/"wgPageName":"([^"]+)"/);
                        if (matchPage) mw.config.set('wgPageName', matchPage[1]);

                        var matchSpecial = src.match(/"wgCanonicalSpecialPageName":"([^"]+)"/);
                        if (matchSpecial) {
                            mw.config.set('wgCanonicalSpecialPageName', matchSpecial[1]);
                        } else {
                            mw.config.set('wgCanonicalSpecialPageName', false);
                        }
                        break;
                    }
                }

                var newContent = doc.querySelector('.liberty-content-main');
                var newTitle = doc.querySelector('.mw-page-title-main');
                var newHead = doc.querySelector('title');
                var newHeader = doc.querySelector('.liberty-content-header');

                if (newContent) {
                    $('#side-toc-box').remove();
                    $('.liberty-content-main').html(newContent.innerHTML);
                    $('.profile-card').remove();
                    $('body').removeClass('page-loading');
                }

                if (newTitle) {
                    $('.mw-page-title-main').html(newTitle.innerHTML);
                }

                if (newHead) {
                    document.title = newHead.textContent;
                }

                if (newHeader) {
                    $('.liberty-content-header').html(newHeader.innerHTML);
                }

                window.scrollTo(0, 0);
                $('#clbi-lang-list').hide();
                $('#clbi-lang-current').css('margin-bottom', '0');

                mw.hook('wikipage.content').fire($('.liberty-content-main'));
                applyMainPageStyle();
                initCategoryNavIfAvailable(document);

                $('#side-toc-box').remove();
                setTimeout(moveTocToLeftSidebar, 100);
                setTimeout(moveTocToLeftSidebar, 500);
                setTimeout(moveTocToLeftSidebar, 1200);

                mw.loader.using(['mediawiki.api']).then(function() {
                    initProfile();
                    moveTocToLeftSidebar();
                });
            });
    }

    // 목차 링크는 전용 처리
    $(document).on('click', '#side-toc-box a, #toc a, .toc a', function(e) {
        var href = $(this).attr('href');
        if (!href || href.charAt(0) !== '#') return;

        var rawId = href.slice(1);
        if (!rawId) return;

        var decodedId = rawId;

        try {
            decodedId = decodeURIComponent(rawId);
        } catch (err) {
            decodedId = rawId;
        }

        var target = document.getElementById(decodedId);

        if (!target && window.CSS && CSS.escape) {
            target = document.querySelector('#' + CSS.escape(decodedId));
        }

        if (!target) return;

        e.preventDefault();
        e.stopPropagation();

        var scrollTarget = target.closest('h2, h3') || target;

        scrollTarget.scrollIntoView({
            behavior: 'auto',
            block: 'start'
        });

        history.replaceState(null, '', '#' + rawId);
    });

    $(document).on('click', 'a', function(e) {
        var href = $(this).attr('href');
        if (!href) return;

        // 목차 링크는 별도 핸들러에서 처리
        if ($(this).closest('#side-toc-box, #toc, .toc').length) return;

        // 단순 해시 링크는 SPA 가로채기 제외
        if (href.startsWith('#')) return;

        var link = document.createElement('a');
        link.href = href;

        var samePath = decodeURIComponent(link.pathname) === decodeURIComponent(window.location.pathname);
        var sameSearch = (link.search || '') === (window.location.search || '');

        if (link.hash && samePath && sameSearch) return;

        var currentBase = window.location.href.split('#')[0];
        var targetBase = link.href.split('#')[0];

        if (link.hash && currentBase === targetBase) return;

        if (!isInternal(href)) return;
        if (shouldSkip(href)) return;

        e.preventDefault();
        playStaticSound();
        $('body').addClass('page-loading');
        history.pushState(null, '', href);
        loadPage(href);
    });

    window.addEventListener('popstate', function() {
        loadPage(window.location.href);
    });
});

// 시간 계산 함수
function timeAgo(timestamp) {
    var now = new Date();
    var date = new Date(timestamp);
    var diff = Math.floor((now - date) / 1000);

    if (diff < 60) return diff + '초 전';
    if (diff < 3600) return Math.floor(diff / 60) + '분 전';
    if (diff < 86400) return Math.floor(diff / 3600) + '시간 전';
    return Math.floor(diff / 86400) + '일 전';
}

// 펼접 토글
function getFoldTexts() {
    var lang = getCurrentLang();
    return (window.LANG && window.LANG[lang])
        ? window.LANG[lang]
        : (window.LANG ? window.LANG.ko : { expand: '펼치기', collapse: '접기' });
}

function refreshOpenAncestors($start) {
    $start.parents('[id^="collapsible"]').each(function () {
        var $parent = $(this);
        if (!$parent.hasClass('folding-open')) return;

        // 이미 fully open 상태면 굳이 다시 잠그지 않음
        if ($parent.data('fold-state') === 'open') {
            return;
        }

        $parent.css('max-height', this.scrollHeight + 'px');
    });
}

function bindInnerResizeUpdates($target) {
    // 이미지 늦게 로드될 때 높이 갱신
    $target.find('img').off('.foldimg').on('load.foldimg', function () {
        if ($target.hasClass('folding-open')) {
            if ($target.data('fold-state') !== 'open') {
                $target.css('max-height', $target[0].scrollHeight + 'px');
            }
            refreshOpenAncestors($target);
        }
    });
}

function openFold($target, $btn) {
    var t = getFoldTexts();

    $target.data('fold-state', 'opening');
    $target.addClass('folding-open');

    // 열린 뒤 자연 확장 가능하게 만들기 위해 먼저 px로 열기
    $target.css('max-height', '0px');
    $target[0].offsetHeight;
    $target.css('max-height', $target[0].scrollHeight + 'px');

    $btn.text(t.collapse);

    bindInnerResizeUpdates($target);

    // 바깥 펼접 즉시 갱신
    refreshOpenAncestors($target);

    // 전환 끝나면 none으로 풀어서 중첩 펼접/동적 내용 증가를 자연스럽게 허용
    $target.off('transitionend.foldopen').on('transitionend.foldopen', function (e) {
        if (e.target !== this) return;
        if (!$target.hasClass('folding-open')) return;

        $target.css('max-height', 'none');
        $target.data('fold-state', 'open');

        refreshOpenAncestors($target);
    });

    // 늦게 렌더되는 콘텐츠 대응
    requestAnimationFrame(function () {
        if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
            $target.css('max-height', $target[0].scrollHeight + 'px');
            refreshOpenAncestors($target);
        }
    });

    setTimeout(function () {
        if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
            $target.css('max-height', $target[0].scrollHeight + 'px');
            refreshOpenAncestors($target);
        }
    }, 80);

    setTimeout(function () {
        if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
            $target.css('max-height', $target[0].scrollHeight + 'px');
            refreshOpenAncestors($target);
        }
    }, 220);
}

function closeFold($target, $btn) {
    var t = getFoldTexts();

    // none 상태에서 닫으면 transition이 안 되므로 실제 높이로 고정
    if ($target.css('max-height') === 'none' || $target.data('fold-state') === 'open') {
        $target.css('max-height', $target[0].scrollHeight + 'px');
    } else {
        $target.css('max-height', $target[0].scrollHeight + 'px');
    }

    $target.data('fold-state', 'closing');
    $target[0].offsetHeight;
    $target.css('max-height', '0px');
    $target.removeClass('folding-open');

    $btn.text(t.expand);

    refreshOpenAncestors($target);

    setTimeout(function () {
        refreshOpenAncestors($target);
        $target.data('fold-state', 'closed');
    }, 250);
}

$(function () {
    $(document)
        .off('click.clbiToggle')
        .on('click.clbiToggle', '.toggleBtn', function () {
            var $btn = $(this);
            var targetId = $btn.data('target');
            var $target = $('#' + targetId);
            if (!$target.length) return;

            var scrollY = window.scrollY;

            if ($target.hasClass('folding-open')) {
                closeFold($target, $btn);
            } else {
                openFold($target, $btn);
            }

            requestAnimationFrame(function () {
                window.scrollTo(window.scrollX, scrollY);
            });
        });
});

// ========== 사용자 설정 / 프로필 ==========
function initProfile() {
    var pageName = normalizePageName(mw.config.get('wgPageName'));
    var namespace = mw.config.get('wgNamespaceNumber');

    if (namespace !== 2) {
        $('.profile-card').remove();
        return;
    }

    var username = pageName.replace(/^사용자:/, '').replace(/^User:/, '');

    if (!username) return;

    mw.loader.using(['mediawiki.api']).then(function () {
        var api = new mw.Api();

        api.get({
            action: 'query',
            list: 'users',
            ususers: username,
            usprop: 'editcount|registration|groups',
            format: 'json'
        }).then(function (data) {
            var users = data && data.query ? data.query.users : [];
            var user = users && users.length ? users[0] : null;
            var contentEl = document.getElementById('mw-content-text');

            if (!contentEl) return;

            var pageContent = contentEl.querySelector('.mw-parser-output') || contentEl;
            injectProfileCard(username, user, pageContent);
        });
    });
}

function injectProfileCard(username, userData, container) {
    var isOwnPage = mw.config.get('wgUserName') === username;
    var editCount = (userData && userData.editcount) ? userData.editcount : 0;
    var editBtn = isOwnPage
        ? '<a href="/index.php/특수:사용자정보" class="profile-edit-btn">프로필 수정</a>'
        : '';

    var card = document.createElement('div');
    card.className = 'profile-card';
    card.innerHTML =
        '<div class="profile-header">' +
            '<div class="profile-avatar">' +
                '<img src="/index.php?title=특수:Redirect/file/Pfp-' + username + '.png&width=120"' +
                ' onerror="this.src=\'/index.php?title=특수:Redirect/file/Pfp-default.png&width=120\'"' +
                ' alt="' + username + '">' +
            '</div>' +
            '<div class="profile-info">' +
                '<h2 class="profile-username">' + username + '</h2>' +
                '<div class="profile-name" data-field="name"></div>' +
                '<div class="profile-role" data-field="role"></div>' +
                '<div class="profile-discord" data-field="discord"></div>' +
                '<div class="profile-bio" data-field="bio"></div>' +
                '<div class="profile-badges" data-field="badges"></div>' +
            '</div>' +
            editBtn +
        '</div>' +
        '<div class="profile-stats">' +
            '<div class="profile-stat">' +
                '<span class="clbi-stat-value">' + editCount + '</span>' +
                '<span class="clbi-stat-label">수정 횟수</span>' +
            '</div>' +
        '</div>';

    $('.profile-card').remove();
    container.insertBefore(card, container.firstChild);
    loadProfileFields(username, card);
}

function loadProfileFields(username, card) {
    var api = new mw.Api();
    api.get({
        action: 'userprofile',
        user: username
    }).then(function(data) {
        var profile = data.userprofile;
        updateProfileFields(card, {
            name: profile.name || '',
            discord: profile.discord || '',
            role: profile.role || '',
            bio: profile.bio || '',
            badges: profile.badges || ''
        });
    }).fail(function() {
        updateProfileFields(card, {
            name: '',
            discord: '',
            role: '',
            bio: '',
            badges: ''
        });
    });
}

function updateProfileFields(card, data) {
    var nameEl = card.querySelector('[data-field="name"]');
    var roleEl = card.querySelector('[data-field="role"]');
    var discordEl = card.querySelector('[data-field="discord"]');
    var bioEl = card.querySelector('[data-field="bio"]');
    var badgesEl = card.querySelector('[data-field="badges"]');

    if (nameEl) nameEl.textContent = data.name || '';
    if (roleEl) roleEl.textContent = data.role || '';
    if (discordEl) discordEl.textContent = data.discord ? ('디스코드: ' + data.discord) : '';
    if (bioEl) bioEl.textContent = data.bio || '';

    if (badgesEl) {
        if (data.badges) {
            var badges = data.badges.split(',');
            var html = '';

            for (var i = 0; i < badges.length; i++) {
                html += '<span class="clbi-badge">' + badges[i].trim() + '</span>';
            }

            badgesEl.innerHTML = html;
        } else {
            badgesEl.innerHTML = '';
        }
    }
}

// ========== 알림 시스템 ==========
function ensureNotificationPopup() {
    if (document.getElementById('clbi-notification-popup')) return;

    var popup = document.createElement('div');
    popup.id = 'clbi-notification-popup';
    popup.style.cssText =
        'display:none;position:fixed;z-index:99999;width:320px;max-height:420px;' +
        'background:#0a0909;border:2px solid #854369;border-radius:5px;' +
        'box-shadow:0 0 0 1px #1a1a1a, 0 8px 24px rgba(0,0,0,0.55);overflow:hidden;';

    popup.innerHTML =
        '<div style="padding:10px 12px;border-bottom:2px solid #854369;background:linear-gradient(to bottom, #171114 0%, #0a0909 100%);color:#E2E2E2;font-size:13px;font-weight:700;display:flex;align-items:center;justify-content:space-between;gap:8px;">' +
            '<span>알림</span>' +
            '<button type="button" id="clbi-notification-readall" style="background:#171717;border:1px solid #854369;border-radius:6px;color:#E2E2E2;font-size:11px;font-weight:700;padding:4px 8px;cursor:pointer;">전체 읽음</button>' +
        '</div>' +
        '<div id="clbi-notification-list" style="max-height:320px;overflow-y:auto;padding:8px 0;color:#E2E2E2;font-size:12px;">불러오는 중...</div>' +
        '<div style="padding:8px;border-top:1px solid #2a2a2a;background:#111;">' +
            '<a href="/index.php?title=Special:Notifications" id="clbi-notification-more" style="display:block;width:100%;text-align:center;padding:8px 10px;border-radius:6px;background:#171717;border:1px solid #854369;color:#E2E2E2 !important;text-decoration:none !important;font-size:12px;font-weight:700;">더보기</a>' +
        '</div>';

    document.body.appendChild(popup);
}

function positionNotificationPopup() {
    var btn = document.getElementById('clbi-notification-toggle');
    var popup = document.getElementById('clbi-notification-popup');
    if (!btn || !popup) return;

    var rect = btn.getBoundingClientRect();
    var top = rect.bottom + 2;
    var left = rect.right - popup.offsetWidth;

    if (left < 8) left = 8;
    if (left + popup.offsetWidth > window.innerWidth - 8) {
        left = window.innerWidth - popup.offsetWidth - 8;
    }
    if (top + popup.offsetHeight > window.innerHeight - 8) {
        top = Math.max(8, window.innerHeight - popup.offsetHeight - 8);
    }

    popup.style.top = top + 'px';
    popup.style.left = left + 'px';
}

function parseNotificationItemsFromHtml(html) {
    var parser = new DOMParser();
    var doc = parser.parseFromString(html, 'text/html');

    var selectors = [
        '.mw-echo-ui-notificationItemWidget',
        '.mw-echo-ui-notificationsInboxWidgetRow',
        '.echo-ui-notificationItemWidget',
        'li[data-notification-id]',
        '.mw-echo-notifications-list li'
    ];

    var items = [];
    for (var i = 0; i < selectors.length; i++) {
        items = Array.prototype.slice.call(doc.querySelectorAll(selectors[i]));
        if (items.length) break;
    }

    return items.slice(0, 5).map(function(item) {
        var link = item.querySelector('a[href]');
        var href = link ? link.getAttribute('href') : '/index.php?title=Special:Notifications';
        var text = (item.textContent || '').replace(/\s+/g, ' ').trim();

        var notificationId =
            item.getAttribute('data-notification-id') ||
            item.getAttribute('data-id') ||
            item.getAttribute('data-notification') ||
            '';

        if (!notificationId) {
            var anyWithId = item.querySelector('[data-notification-id], [data-id], [data-notification]');
            if (anyWithId) {
                notificationId =
                    anyWithId.getAttribute('data-notification-id') ||
                    anyWithId.getAttribute('data-id') ||
                    anyWithId.getAttribute('data-notification') ||
                    '';
            }
        }

        if (href && href.indexOf('http') !== 0) {
            href = href.charAt(0) === '/'
                ? href
                : '/index.php' + (href.charAt(0) === '?' ? href : '/' + href);
        }

        return {
            id: notificationId,
            href: href,
            text: text || '알림'
        };
    });
}

function renderNotificationPopup(items) {
    var list = document.getElementById('clbi-notification-list');
    var badge = document.getElementById('clbi-notification-badge');
    if (!list) return;

    if (!items || !items.length) {
        list.innerHTML = '<div style="padding:14px 12px;color:#999;">표시할 알림이 없습니다.</div>';
        if (badge) badge.style.display = 'none';
        return;
    }

    var html = '';
    for (var i = 0; i < items.length; i++) {
        html +=
            '<a href="' + items[i].href + '" class="clbi-notification-item" data-notification-id="' + (items[i].id || '') + '" style="display:block;padding:10px 12px;color:#E2E2E2 !important;text-decoration:none !important;border-bottom:1px solid #1f1f1f;line-height:1.5;">' +
                items[i].text +
            '</a>';
    }
    list.innerHTML = html;

    if (badge) {
        badge.textContent = items.length;
        badge.style.display = 'block';
    }
}

function loadNotificationsIntoPopup() {
    var list = document.getElementById('clbi-notification-list');
    if (list) {
        list.innerHTML = '<div style="padding:14px 12px;color:#999;">불러오는 중...</div>';
    }

    fetch('/index.php?title=Special:Notifications', { credentials: 'same-origin' })
        .then(function(res) {
            return res.text();
        })
        .then(function(html) {
            var items = parseNotificationItemsFromHtml(html);
            renderNotificationPopup(items);
        })
        .catch(function(err) {
            console.error(err);
            if (list) {
                list.innerHTML = '<div style="padding:14px 12px;color:#999;">알림을 불러오지 못했습니다.</div>';
            }
        });
}

function markAllNotificationsRead() {
    return new mw.Api().postWithToken('csrf', {
        action: 'echomarkread',
        list: 'all'
    });
}

function markNotificationReadById(notificationId) {
    if (!notificationId) {
        return $.Deferred().resolve().promise();
    }

    return new mw.Api().postWithToken('csrf', {
        action: 'echomarkread',
        list: notificationId
    });
}

function initNotifications() {
    var btn = document.getElementById('clbi-notification-toggle');
    if (!btn) return;