태그: 편집 취소 |
편집 요약 없음 |
||
| 381번째 줄: | 381번째 줄: | ||
}); | }); | ||
}); | }); | ||
// ========== 프로필 시스템 ========== | |||
function initProfile() { | |||
const title = mw.config.get('wgTitle'); | |||
const ns = mw.config.get('wgNamespaceNumber'); | |||
const currentUser = mw.config.get('wgUserName'); | |||
const isUserPage = ns === 2; | |||
// 사용자 문서에서 프로필 렌더링 | |||
if (isUserPage) { | |||
const profileUser = title.split('/')[0]; | |||
renderProfile(profileUser); | |||
} | |||
// 환경설정 페이지에 프로필 탭 추가 | |||
if (mw.config.get('wgCanonicalSpecialPageName') === 'Preferences') { | |||
addProfileTab(); | |||
} | |||
} | |||
function renderProfile(username) { | |||
const api = new mw.Api(); | |||
api.get({ | |||
action: 'query', | |||
meta: 'userinfo', | |||
uiprop: 'options', | |||
format: 'json', | |||
// 다른 유저 옵션 읽기 | |||
}).then(function(data) { | |||
// 자기 자신일 때만 options로 읽기 가능, 다른 유저는 별도 처리 | |||
}); | |||
// users API로 기본 정보 + options | |||
api.get({ | |||
action: 'query', | |||
list: 'users', | |||
ususers: username, | |||
usprop: 'groups|editcount|registration', | |||
}).then(function(data) { | |||
const user = data.query.users[0]; | |||
fetchProfileData(username, user); | |||
}); | |||
} | |||
function fetchProfileData(username, userData) { | |||
const api = new mw.Api(); | |||
// 프로필 데이터는 사용자 문서의 숨김 카테고리 대신 options에서 읽음 | |||
// 단, options는 본인만 읽을 수 있으므로 다른 유저 프로필은 사용자 문서 파싱 방식 필요 | |||
// → 대신 프로필 데이터를 사용자 문서에 숨김 div로 저장하는 방식 사용 | |||
const contentEl = document.getElementById('mw-content-text'); | |||
if (!contentEl) return; | |||
// 사용자 문서가 비어있으면 프로필 카드 삽입 | |||
const pageContent = contentEl.querySelector('.mw-parser-output'); | |||
injectProfileCard(username, userData, pageContent || contentEl); | |||
} | |||
function injectProfileCard(username, userData, container) { | |||
const isOwnPage = mw.config.get('wgUserName') === username; | |||
const isAdmin = mw.config.get('wgUserGroups') && mw.config.get('wgUserGroups').includes('sysop'); | |||
const card = document.createElement('div'); | |||
card.className = 'clbi-profile-card'; | |||
card.innerHTML = ` | |||
<div class="clbi-profile-header"> | |||
<div class="clbi-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="clbi-profile-info"> | |||
<h1 class="clbi-profile-username">${username}</h1> | |||
<div class="clbi-profile-name" data-field="name">불러오는 중...</div> | |||
<div class="clbi-profile-role" data-field="role"></div> | |||
<div class="clbi-profile-discord" data-field="discord"></div> | |||
<div class="clbi-profile-bio" data-field="bio"></div> | |||
<div class="clbi-profile-badges" data-field="badges"></div> | |||
</div> | |||
${isOwnPage ? '<a href="/index.php?title=특수:Preferences#mw-prefsection-clbi-profile" class="clbi-profile-edit-btn">프로필 수정</a>' : ''} | |||
</div> | |||
<div class="clbi-profile-stats"> | |||
<div class="clbi-profile-stat"> | |||
<span class="clbi-stat-value">${userData.editcount || 0}</span> | |||
<span class="clbi-stat-label">수정 횟수</span> | |||
</div> | |||
</div> | |||
`; | |||
container.insertBefore(card, container.firstChild); | |||
// 프로필 데이터 로드 (사용자 문서 숨김 div에서) | |||
loadProfileFields(username, card); | |||
} | |||
function loadProfileFields(username, card) { | |||
const api = new mw.Api(); | |||
// 프로필 데이터를 사용자 토크 페이지의 숨김 섹션 대신 | |||
// MediaWiki API userinfo로 읽기 (본인) 또는 페이지 파싱 (타인) | |||
const currentUser = mw.config.get('wgUserName'); | |||
if (currentUser === username) { | |||
// 본인: options API로 읽기 | |||
api.get({ | |||
action: 'query', | |||
meta: 'userinfo', | |||
uiprop: 'options', | |||
}).then(function(data) { | |||
const opts = data.query.userinfo.options; | |||
updateProfileFields(card, { | |||
name: opts['clbi-profile-name'] || '', | |||
discord: opts['clbi-profile-discord'] || '', | |||
role: opts['clbi-profile-role'] || '', | |||
bio: opts['clbi-profile-bio'] || '', | |||
badges: opts['clbi-profile-badges'] || '', | |||
}); | |||
}); | |||
} else { | |||
// 타인: 사용자 문서에서 숨김 데이터 div 파싱 | |||
api.get({ | |||
action: 'parse', | |||
page: '사용자:' + username, | |||
prop: 'text', | |||
format: 'json', | |||
}).then(function(data) { | |||
if (!data.parse) return; | |||
const html = data.parse.text['*']; | |||
const temp = document.createElement('div'); | |||
temp.innerHTML = html; | |||
const dataEl = temp.querySelector('#clbi-profile-data'); | |||
if (dataEl) { | |||
updateProfileFields(card, { | |||
name: dataEl.dataset.name || '', | |||
discord: dataEl.dataset.discord || '', | |||
role: dataEl.dataset.role || '', | |||
bio: dataEl.dataset.bio || '', | |||
badges: dataEl.dataset.badges || '', | |||
}); | |||
} else { | |||
updateProfileFields(card, { name: username, discord: '', role: '', bio: '', badges: '' }); | |||
} | |||
}); | |||
} | |||
} | |||
function updateProfileFields(card, data) { | |||
const nameEl = card.querySelector('[data-field="name"]'); | |||
const roleEl = card.querySelector('[data-field="role"]'); | |||
const discordEl = card.querySelector('[data-field="discord"]'); | |||
const bioEl = card.querySelector('[data-field="bio"]'); | |||
const badgesEl = card.querySelector('[data-field="badges"]'); | |||
if (nameEl) nameEl.textContent = data.name || ''; | |||
if (roleEl) roleEl.textContent = data.role || ''; | |||
if (discordEl && data.discord) discordEl.textContent = '디스코드: ' + data.discord; | |||
if (bioEl) bioEl.textContent = data.bio || ''; | |||
if (badgesEl && data.badges) { | |||
badgesEl.innerHTML = data.badges.split(',').map(b => | |||
`<span class="clbi-badge">${b.trim()}</span>` | |||
).join(''); | |||
} | |||
} | |||
function addProfileTab() { | |||
mw.loader.using(['mediawiki.api']).then(function() { | |||
const api = new mw.Api(); | |||
api.get({ | |||
action: 'query', | |||
meta: 'userinfo', | |||
uiprop: 'options', | |||
}).then(function(data) { | |||
const opts = data.query.userinfo.options; | |||
const currentUser = mw.config.get('wgUserName'); | |||
const isAdmin = mw.config.get('wgUserGroups') && mw.config.get('wgUserGroups').includes('sysop'); | |||
// 탭 추가 | |||
const tabList = document.querySelector('#preftoc'); | |||
if (!tabList) return; | |||
const tab = document.createElement('li'); | |||
tab.id = 'preftab-clbi-profile'; | |||
tab.innerHTML = '<a href="#mw-prefsection-clbi-profile">프로필</a>'; | |||
tabList.appendChild(tab); | |||
// 탭 내용 | |||
const prefsForm = document.querySelector('#mw-prefs-form'); | |||
if (!prefsForm) return; | |||
const section = document.createElement('div'); | |||
section.id = 'mw-prefsection-clbi-profile'; | |||
section.className = 'prefsection'; | |||
section.style.display = 'none'; | |||
section.innerHTML = ` | |||
<h2>프로필 설정</h2> | |||
<div class="clbi-prefs-profile"> | |||
<div class="clbi-pref-row"> | |||
<label>이름</label> | |||
<input type="text" id="clbi-pref-name" value="${opts['clbi-profile-name'] || ''}" placeholder="표시될 이름"> | |||
</div> | |||
<div class="clbi-pref-row"> | |||
<label>디스코드 ID</label> | |||
<input type="text" id="clbi-pref-discord" value="${opts['clbi-profile-discord'] || ''}" placeholder="username#0000"> | |||
</div> | |||
<div class="clbi-pref-row"> | |||
<label>역할</label> | |||
<input type="text" id="clbi-pref-role" value="${opts['clbi-profile-role'] || ''}" placeholder="예: 시나리오 작가"> | |||
</div> | |||
<div class="clbi-pref-row"> | |||
<label>자기소개</label> | |||
<textarea id="clbi-pref-bio" placeholder="간단한 자기소개">${opts['clbi-profile-bio'] || ''}</textarea> | |||
</div> | |||
${isAdmin ? ` | |||
<div class="clbi-pref-row"> | |||
<label>훈장 (관리자 전용, 쉼표로 구분)</label> | |||
<input type="text" id="clbi-pref-badges" value="${opts['clbi-profile-badges'] || ''}" placeholder="훈장1, 훈장2"> | |||
</div> | |||
` : ''} | |||
<button type="button" id="clbi-pref-save" class="clbi-btn">저장</button> | |||
<span id="clbi-pref-status"></span> | |||
</div> | |||
`; | |||
prefsForm.appendChild(section); | |||
// 탭 클릭 이벤트 | |||
tab.querySelector('a').addEventListener('click', function(e) { | |||
e.preventDefault(); | |||
document.querySelectorAll('.prefsection').forEach(s => s.style.display = 'none'); | |||
document.querySelectorAll('#preftoc li').forEach(t => t.classList.remove('selected')); | |||
section.style.display = ''; | |||
tab.classList.add('selected'); | |||
}); | |||
// 저장 버튼 | |||
document.getElementById('clbi-pref-save').addEventListener('click', function() { | |||
const statusEl = document.getElementById('clbi-pref-status'); | |||
statusEl.textContent = '저장 중...'; | |||
const saveData = { | |||
action: 'options', | |||
format: 'json', | |||
optionname: 'clbi-profile-name', | |||
optionvalue: document.getElementById('clbi-pref-name').value, | |||
}; | |||
const fields = ['name', 'discord', 'role', 'bio']; | |||
if (isAdmin) fields.push('badges'); | |||
const promises = fields.map(field => { | |||
const el = document.getElementById('clbi-pref-' + field); | |||
if (!el) return Promise.resolve(); | |||
return api.postWithToken('csrf', { | |||
action: 'options', | |||
optionname: 'clbi-profile-' + field, | |||
optionvalue: el.value, | |||
}); | |||
}); | |||
Promise.all(promises).then(function() { | |||
statusEl.textContent = '✓ 저장됨'; | |||
setTimeout(() => statusEl.textContent = '', 2000); | |||
}).catch(function() { | |||
statusEl.textContent = '저장 실패'; | |||
}); | |||
}); | |||
}); | |||
}); | |||
} | |||
mw.loader.using(['mediawiki.api', 'mediawiki.user']).then(function() { | |||
$(document).ready(function() { | |||
initProfile(); | |||
}); | |||
}); | |||
// ========== 프로필 시스템 끝 ========== | |||
2026년 4월 19일 (일) 08:20 판
importScript('MediaWiki:Lang.js');
$(function() {
$('body').prepend('<div class="WW-bg" style="position:fixed;top:0px;left:0px;width:100%;height:100vh;"></div>');
});
// 페이지 전환 사운드
var transitionSound = new Audio('/index.php?title=특수:Redirect/file/Sfx-ui-001.mp3');
transitionSound.volume = 0.6;
function playStaticSound() {
transitionSound.currentTime = 0;
transitionSound.play();
}
// 현재 언어 감지
function getCurrentLang() {
var langData = document.getElementById('clbi-lang-data');
return langData ? (langData.getAttribute('data-lang') || 'ko') : 'ko';
}
// 사이드바 업데이트
function updateSidebar() {
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="/index.php/' + encodeURIComponent(target) + '">' + 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', '/index.php/' + cl.main);
$('#clbi-cat-main .clbi-cat-label').text(t.mainMenu);
$('#clbi-cat-nations a').attr('href', '/index.php/' + cl.nations);
$('#clbi-cat-nations .clbi-cat-label').text(t.nations);
$('#clbi-cat-corporations a').attr('href', '/index.php/' + cl.corporations);
$('#clbi-cat-corporations .clbi-cat-label').text(t.corporations);
$('#clbi-cat-military a').attr('href', '/index.php/' + cl.military);
$('#clbi-cat-military .clbi-cat-label').text(t.military);
$('#clbi-cat-history a').attr('href', '/index.php/' + cl.history);
$('#clbi-cat-history .clbi-cat-label').text(t.history);
$('#clbi-cat-personnel a').attr('href', '/index.php/' + cl.personnel);
$('#clbi-cat-personnel .clbi-cat-label').text(t.personnel);
$('#clbi-btn-contribution').text(t.contribution);
$('#clbi-btn-talk').text(t.talk);
$('#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 = mw.config.get('wgPageName');
$('.clbi-cat-btn').removeClass('clbi-cat-active');
$.each(['main', 'nations', 'corporations', 'military', 'history', 'personnel'], function(i, key) {
if (cl[key] && pageName === cl[key]) {
$('#clbi-cat-' + key).addClass('clbi-cat-active');
}
});
}
// 대문 스타일
function applyMainPageStyle() {
var pageName = mw.config.get('wgPageName');
if (pageName === '대문') {
$('.liberty-content-header').css('display', 'none');
$('.mw-page-title-main').addClass('clbi-hide');
$('.content-tools').css('display', 'none');
$('.liberty-content-main').css('border-radius', '5px');
if ($('#clbi-main-logo').length === 0) {
$('.liberty-content').prepend(
'<div id="clbi-main-logo" style="text-align:center;padding:20px 0;">' +
'<img src="/index.php?title=특수:Redirect/file/Img-clbi-001.png" style="width:800px;height:auto;">' +
'</div>'
);
}
} else {
$('.liberty-content-header').css('display', '');
$('.mw-page-title-main').removeClass('clbi-hide');
$('.content-tools').css('display', '');
$('.liberty-content-main').css('border-radius', '');
$('#clbi-main-logo').remove();
}
updateSidebar();
}
// 초기화 함수
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 = $('.profile-img').attr('src') || '/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 + '" style="width:64px;height:64px;border-radius:5px;object-fit:cover;border:2px solid #854369;margin-bottom:8px;">' +
'<a href="/index.php/사용자:' + username + '" style="font-size:13px;font-weight:700;color:#E2E2E2 !important;text-decoration:none !important;">' + username + '</a>' +
'</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/사용자_토론:' + username + '" class="clbi-user-btn" id="clbi-btn-talk">토론</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 spotifyBox =
'<div class="clbi-right-box" style="overflow:hidden;">' +
'<div class="clbi-right-title" id="clbi-title-playlist"><span class="clbi-icon" style="--icon:var(--ic-ui-003)"></span> 플레이리스트</div>' +
'<iframe style="border-radius:0;width:100%;height:380px;border:none;" 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>';
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>' +
spotifyBox;
var wrapper = '<div id="clbi-right-sidebar">' + sidebar + '</div>';
$('.content-wrapper').append(wrapper);
$('#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 class="clbi-left-box">' +
'<div class="clbi-left-title"><span class="clbi-icon" style="--icon:var(--ic-ui-003)"></span> <span id="clbi-title-links">링크</span></div>' +
'<div class="clbi-left-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>' +
'</div>';
$('.content-wrapper').prepend(leftSidebar);
$(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();
}
$(function() {
setTimeout(function() {
initSidebars();
}, 100);
});
// SPA 네비게이션
$(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 shouldSkip(url) {
return url.match(/action=edit|action=submit|action=history|action=delete|action=protect|action=purge|특수:로그인|특수:로그아웃|Special:UserLogin|Special:UserLogout/);
}
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 newContent = doc.querySelector('.liberty-content-main');
var newTitle = doc.querySelector('.mw-page-title-main');
var newHead = doc.querySelector('title');
if (newContent) {
$('.liberty-content-main').html(newContent.innerHTML);
$('body').removeClass('page-loading');
}
if (newTitle) {
$('.mw-page-title-main').html(newTitle.innerHTML);
}
if (newHead) {
document.title = newHead.textContent;
}
var newHeader = doc.querySelector('.liberty-content-header');
if (newHeader) {
$('.liberty-content-header').html(newHeader.innerHTML);
}
var newPageName = doc.querySelector('html').getAttribute('data-mw-requested-url') ||
url.replace(/.*\/index\.php\//, '').replace(/.*\/wiki\//, '').split('?')[0];
newPageName = decodeURIComponent(newPageName);
mw.config.set('wgPageName', newPageName);
window.scrollTo(0, 0);
$('#clbi-lang-list').hide();
$('#clbi-lang-current').css('margin-bottom', '0');
mw.hook('wikipage.content').fire($('.liberty-content-main'));
applyMainPageStyle();
});
}
$(document).on('click', 'a', function(e) {
var href = $(this).attr('href');
if (!href) return;
if (!isInternal(href)) return;
if (shouldSkip(href)) return;
if (href.startsWith('#')) 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() {
$(document).on('click', '.toggleBtn', function() {
var targetId = $(this).data('target');
var target = $('#' + targetId);
var scrollY = window.scrollY;
if (target.hasClass('folding-open')) {
target.css('max-height', target[0].scrollHeight + 'px');
target[0].offsetHeight;
target.css('max-height', '0px');
target.removeClass('folding-open');
} else {
target.css('max-height', '0px');
target[0].offsetHeight;
target.css('max-height', target[0].scrollHeight + 'px');
target.addClass('folding-open');
}
window.scrollTo(0, scrollY);
});
});
// ========== 프로필 시스템 ==========
function initProfile() {
const title = mw.config.get('wgTitle');
const ns = mw.config.get('wgNamespaceNumber');
const currentUser = mw.config.get('wgUserName');
const isUserPage = ns === 2;
// 사용자 문서에서 프로필 렌더링
if (isUserPage) {
const profileUser = title.split('/')[0];
renderProfile(profileUser);
}
// 환경설정 페이지에 프로필 탭 추가
if (mw.config.get('wgCanonicalSpecialPageName') === 'Preferences') {
addProfileTab();
}
}
function renderProfile(username) {
const api = new mw.Api();
api.get({
action: 'query',
meta: 'userinfo',
uiprop: 'options',
format: 'json',
// 다른 유저 옵션 읽기
}).then(function(data) {
// 자기 자신일 때만 options로 읽기 가능, 다른 유저는 별도 처리
});
// users API로 기본 정보 + options
api.get({
action: 'query',
list: 'users',
ususers: username,
usprop: 'groups|editcount|registration',
}).then(function(data) {
const user = data.query.users[0];
fetchProfileData(username, user);
});
}
function fetchProfileData(username, userData) {
const api = new mw.Api();
// 프로필 데이터는 사용자 문서의 숨김 카테고리 대신 options에서 읽음
// 단, options는 본인만 읽을 수 있으므로 다른 유저 프로필은 사용자 문서 파싱 방식 필요
// → 대신 프로필 데이터를 사용자 문서에 숨김 div로 저장하는 방식 사용
const contentEl = document.getElementById('mw-content-text');
if (!contentEl) return;
// 사용자 문서가 비어있으면 프로필 카드 삽입
const pageContent = contentEl.querySelector('.mw-parser-output');
injectProfileCard(username, userData, pageContent || contentEl);
}
function injectProfileCard(username, userData, container) {
const isOwnPage = mw.config.get('wgUserName') === username;
const isAdmin = mw.config.get('wgUserGroups') && mw.config.get('wgUserGroups').includes('sysop');
const card = document.createElement('div');
card.className = 'clbi-profile-card';
card.innerHTML = `
<div class="clbi-profile-header">
<div class="clbi-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="clbi-profile-info">
<h1 class="clbi-profile-username">${username}</h1>
<div class="clbi-profile-name" data-field="name">불러오는 중...</div>
<div class="clbi-profile-role" data-field="role"></div>
<div class="clbi-profile-discord" data-field="discord"></div>
<div class="clbi-profile-bio" data-field="bio"></div>
<div class="clbi-profile-badges" data-field="badges"></div>
</div>
${isOwnPage ? '<a href="/index.php?title=특수:Preferences#mw-prefsection-clbi-profile" class="clbi-profile-edit-btn">프로필 수정</a>' : ''}
</div>
<div class="clbi-profile-stats">
<div class="clbi-profile-stat">
<span class="clbi-stat-value">${userData.editcount || 0}</span>
<span class="clbi-stat-label">수정 횟수</span>
</div>
</div>
`;
container.insertBefore(card, container.firstChild);
// 프로필 데이터 로드 (사용자 문서 숨김 div에서)
loadProfileFields(username, card);
}
function loadProfileFields(username, card) {
const api = new mw.Api();
// 프로필 데이터를 사용자 토크 페이지의 숨김 섹션 대신
// MediaWiki API userinfo로 읽기 (본인) 또는 페이지 파싱 (타인)
const currentUser = mw.config.get('wgUserName');
if (currentUser === username) {
// 본인: options API로 읽기
api.get({
action: 'query',
meta: 'userinfo',
uiprop: 'options',
}).then(function(data) {
const opts = data.query.userinfo.options;
updateProfileFields(card, {
name: opts['clbi-profile-name'] || '',
discord: opts['clbi-profile-discord'] || '',
role: opts['clbi-profile-role'] || '',
bio: opts['clbi-profile-bio'] || '',
badges: opts['clbi-profile-badges'] || '',
});
});
} else {
// 타인: 사용자 문서에서 숨김 데이터 div 파싱
api.get({
action: 'parse',
page: '사용자:' + username,
prop: 'text',
format: 'json',
}).then(function(data) {
if (!data.parse) return;
const html = data.parse.text['*'];
const temp = document.createElement('div');
temp.innerHTML = html;
const dataEl = temp.querySelector('#clbi-profile-data');
if (dataEl) {
updateProfileFields(card, {
name: dataEl.dataset.name || '',
discord: dataEl.dataset.discord || '',
role: dataEl.dataset.role || '',
bio: dataEl.dataset.bio || '',
badges: dataEl.dataset.badges || '',
});
} else {
updateProfileFields(card, { name: username, discord: '', role: '', bio: '', badges: '' });
}
});
}
}
function updateProfileFields(card, data) {
const nameEl = card.querySelector('[data-field="name"]');
const roleEl = card.querySelector('[data-field="role"]');
const discordEl = card.querySelector('[data-field="discord"]');
const bioEl = card.querySelector('[data-field="bio"]');
const badgesEl = card.querySelector('[data-field="badges"]');
if (nameEl) nameEl.textContent = data.name || '';
if (roleEl) roleEl.textContent = data.role || '';
if (discordEl && data.discord) discordEl.textContent = '디스코드: ' + data.discord;
if (bioEl) bioEl.textContent = data.bio || '';
if (badgesEl && data.badges) {
badgesEl.innerHTML = data.badges.split(',').map(b =>
`<span class="clbi-badge">${b.trim()}</span>`
).join('');
}
}
function addProfileTab() {
mw.loader.using(['mediawiki.api']).then(function() {
const api = new mw.Api();
api.get({
action: 'query',
meta: 'userinfo',
uiprop: 'options',
}).then(function(data) {
const opts = data.query.userinfo.options;
const currentUser = mw.config.get('wgUserName');
const isAdmin = mw.config.get('wgUserGroups') && mw.config.get('wgUserGroups').includes('sysop');
// 탭 추가
const tabList = document.querySelector('#preftoc');
if (!tabList) return;
const tab = document.createElement('li');
tab.id = 'preftab-clbi-profile';
tab.innerHTML = '<a href="#mw-prefsection-clbi-profile">프로필</a>';
tabList.appendChild(tab);
// 탭 내용
const prefsForm = document.querySelector('#mw-prefs-form');
if (!prefsForm) return;
const section = document.createElement('div');
section.id = 'mw-prefsection-clbi-profile';
section.className = 'prefsection';
section.style.display = 'none';
section.innerHTML = `
<h2>프로필 설정</h2>
<div class="clbi-prefs-profile">
<div class="clbi-pref-row">
<label>이름</label>
<input type="text" id="clbi-pref-name" value="${opts['clbi-profile-name'] || ''}" placeholder="표시될 이름">
</div>
<div class="clbi-pref-row">
<label>디스코드 ID</label>
<input type="text" id="clbi-pref-discord" value="${opts['clbi-profile-discord'] || ''}" placeholder="username#0000">
</div>
<div class="clbi-pref-row">
<label>역할</label>
<input type="text" id="clbi-pref-role" value="${opts['clbi-profile-role'] || ''}" placeholder="예: 시나리오 작가">
</div>
<div class="clbi-pref-row">
<label>자기소개</label>
<textarea id="clbi-pref-bio" placeholder="간단한 자기소개">${opts['clbi-profile-bio'] || ''}</textarea>
</div>
${isAdmin ? `
<div class="clbi-pref-row">
<label>훈장 (관리자 전용, 쉼표로 구분)</label>
<input type="text" id="clbi-pref-badges" value="${opts['clbi-profile-badges'] || ''}" placeholder="훈장1, 훈장2">
</div>
` : ''}
<button type="button" id="clbi-pref-save" class="clbi-btn">저장</button>
<span id="clbi-pref-status"></span>
</div>
`;
prefsForm.appendChild(section);
// 탭 클릭 이벤트
tab.querySelector('a').addEventListener('click', function(e) {
e.preventDefault();
document.querySelectorAll('.prefsection').forEach(s => s.style.display = 'none');
document.querySelectorAll('#preftoc li').forEach(t => t.classList.remove('selected'));
section.style.display = '';
tab.classList.add('selected');
});
// 저장 버튼
document.getElementById('clbi-pref-save').addEventListener('click', function() {
const statusEl = document.getElementById('clbi-pref-status');
statusEl.textContent = '저장 중...';
const saveData = {
action: 'options',
format: 'json',
optionname: 'clbi-profile-name',
optionvalue: document.getElementById('clbi-pref-name').value,
};
const fields = ['name', 'discord', 'role', 'bio'];
if (isAdmin) fields.push('badges');
const promises = fields.map(field => {
const el = document.getElementById('clbi-pref-' + field);
if (!el) return Promise.resolve();
return api.postWithToken('csrf', {
action: 'options',
optionname: 'clbi-profile-' + field,
optionvalue: el.value,
});
});
Promise.all(promises).then(function() {
statusEl.textContent = '✓ 저장됨';
setTimeout(() => statusEl.textContent = '', 2000);
}).catch(function() {
statusEl.textContent = '저장 실패';
});
});
});
});
}
mw.loader.using(['mediawiki.api', 'mediawiki.user']).then(function() {
$(document).ready(function() {
initProfile();
});
});
// ========== 프로필 시스템 끝 ==========