미디어위키:Minerva.js: 두 판 사이의 차이

미네르바 스킨 메뉴 추가
 
구조 개선
 
1번째 줄: 1번째 줄:
/* All JavaScript here will be loaded for users of the MinervaNeue skin */
/* All JavaScript here will be loaded for users of the MinervaNeue skin */
/* 미네르바 노이에 전용 커스텀 메뉴 (PC/모바일 공통)
/* 가온 위키 Minerva Neue 전용 커스텀 메뉴
  * MediaWiki:Minerva.js 에 넣는 것을 기준으로 작성되었습니다.
* - PC/모바일 공통
  * 이 스크립트는 미네르바 스킨의 햄버거 메뉴 안에
  * - Minerva 기본 메뉴와 같은 구조/클래스 사용
  * "게시판 / 버그/기능 개선 / 도구 / 도움말" 메뉴와 하위 메뉴를 추가합니다.
*  (toggle-list__list / toggle-list-item / toggle-list-item__anchor /
  *   minerva-icon / toggle-list-item__label)
  * - 상위 그룹(게시판, 버그/기능 개선, 도구, 도움말)은 접고 펼칠 수 있음
* - "파일 올리기" 아래에 3단계 메뉴 포함
  */
  */


9번째 줄: 12번째 줄:
'use strict';
'use strict';


// 미네르바 스킨이 아닐 경우 아무 것도 하지 않는다.
// Minerva 스킨이 아닐 때는 아무 것도 하지 않는다.
if ( mw.config.get( 'skin' ) !== 'minerva' ) {
if ( mw.config.get( 'skin' ) !== 'minerva' ) {
return;
return;
}
}


/* -----------------------------
/* -------------------------------------------------
* 1. 메뉴 설정 (질문에서 주신 구조 그대로)
* 1. 메뉴 설정 (질문에서 주신 내용을 JS 객체로 변환)
* ----------------------------- */
* ------------------------------------------------- */
 
// 미네르바에서 사용할 아이콘 클래스는
// https://www.mediawiki.org/wiki/Topic:Vqy1kx6q4e0bzvyb 등에서 정리된 목록에 기반합니다. :contentReference[oaicite:5]{index=5}
// 필요하면 iconClass 값을 수정하여 원하는 아이콘으로 교체하면 됩니다.


// icon 필드에는 Minerva 메뉴에서 사용하는
// `minerva-icon minerva-icon--XXX` 의 XXX 부분만 넣는다.
// 예: icon: 'minerva-icon--speechBubbles'
var gaonMenuConfig = [
var gaonMenuConfig = [
{
{
// * icon=comments | display=게시판
// 게시판 그룹
label: '게시판',
label: '게시판',
iconClass: 'mw-ui-icon-minerva-speechBubbles',
icon: 'minerva-icon--speechBubbles',
children: [
children: [
// ** icon=pencil-alt | display=위키방 | link=https://bbs.gaonwiki.com | title=https://bbs.gaonwiki.com
{
{
label: '위키방',
label: '위키방',
href: 'https://bbs.gaonwiki.com',
href: 'https://bbs.gaonwiki.com',
title: 'https://bbs.gaonwiki.com',
title: 'https://bbs.gaonwiki.com',
iconClass: 'mw-ui-icon-minerva-speechBubbles'
icon: 'minerva-icon--speechBubbles'
},
},
// ** icon=copyright | display=저작권 처리 게시판 | link=https://bbs.gaonwiki.com/copyrights | title=https://bbs.gaonwiki.com/copyrights
{
{
label: '저작권 처리 게시판',
label: '저작권 처리 게시판',
href: 'https://bbs.gaonwiki.com/copyrights',
href: 'https://bbs.gaonwiki.com/copyrights',
title: 'https://bbs.gaonwiki.com/copyrights',
title: 'https://bbs.gaonwiki.com/copyrights',
iconClass: 'mw-ui-icon-minerva-speechBubbles'
icon: 'minerva-icon--speechBubbles'
},
},
// ** icon=comments | display=자유게시판 | link=https://bbs.gaonwiki.com/free | title=https://bbs.gaonwiki.com/free
{
{
label: '자유게시판',
label: '자유게시판',
href: 'https://bbs.gaonwiki.com/free',
href: 'https://bbs.gaonwiki.com/free',
title: 'https://bbs.gaonwiki.com/free',
title: 'https://bbs.gaonwiki.com/free',
iconClass: 'mw-ui-icon-minerva-speechBubbles'
icon: 'minerva-icon--speechBubbles'
}
}
]
]
},
},
{
{
// * icon=exclamation | display=버그/기능 개선
// 버그/기능 개선 그룹
label: '버그/기능 개선',
label: '버그/기능 개선',
iconClass: 'mw-ui-icon-minerva-die', // 경고/이슈 느낌의 주사위 아이콘 사용 예시
icon: 'minerva-icon--die',
children: [
children: [
// ** icon=bullhorn | display=이슈 등록 | link=https://bbs.gaonwiki.com/issue | title=https://bbs.gaonwiki.com/issue
{
{
label: '이슈 등록',
label: '이슈 등록',
href: 'https://bbs.gaonwiki.com/issue',
href: 'https://bbs.gaonwiki.com/issue',
title: 'https://bbs.gaonwiki.com/issue',
title: 'https://bbs.gaonwiki.com/issue',
iconClass: 'mw-ui-icon-minerva-die'
icon: 'minerva-icon--die'
},
},
// ** icon=hands-helping | display=기능 구현 도움 | link=... | title=...
{
{
label: '기능 구현 도움',
label: '기능 구현 도움',
href: 'https://www.gaonwiki.com/w/%EA%B0%80%EC%98%A8_%EC%9C%84%ED%82%A4:%EA%B8%B0%EB%8A%A5_%EA%B5%AC%ED%98%84_%EB%B6%88%EA%B0%80-%EB%88%84%EA%B0%80_%ED%95%B4%EA%B2%B0%EC%B1%85%EC%9D%84_%EC%95%8C%EB%A0%A4%EC%A3%BC%EC%84%B8%EC%9A%94!',
href: 'https://www.gaonwiki.com/w/%EA%B0%80%EC%98%A8_%EC%9C%84%ED%82%A4:%EA%B8%B0%EB%8A%A5_%EA%B5%AC%ED%98%84_%EB%B6%88%EA%B0%80-%EB%88%84%EA%B0%80_%ED%95%B4%EA%B2%B0%EC%B1%85%EC%9D%84_%EC%95%8C%EB%A0%A4%EC%A3%BC%EC%84%B8%EC%9A%94!',
title: 'https://www.gaonwiki.com/w/가온_위키:기능_구현_불가-누가_해결책을_알려주세요!',
title: 'https://www.gaonwiki.com/w/가온_위키:기능_구현_불가-누가_해결책을_알려주세요!',
iconClass: 'mw-ui-icon-wikimedia-edit-base20'
icon: 'minerva-icon--speechBubbles'
},
},
// ** icon=bug | display=알려진 문제들 | link=... | title=...
{
{
label: '알려진 문제들',
label: '알려진 문제들',
href: 'https://www.gaonwiki.com/w/%EA%B0%80%EC%98%A8_%EC%9C%84%ED%82%A4/%EC%95%8C%EB%A0%A4%EC%A7%84_%EB%AC%B8%EC%A0%9C%EB%93%A4',
href: 'https://www.gaonwiki.com/w/%EA%B0%80%EC%98%A8_%EC%9C%84%ED%82%A4/%EC%95%8C%EB%A0%A4%EC%A7%84_%EB%AC%B8%EC%A0%9C%EB%93%A4',
title: 'https://www.gaonwiki.com/w/가온_위키/알려진_문제들',
title: 'https://www.gaonwiki.com/w/가온_위키/알려진_문제들',
iconClass: 'mw-ui-icon-wikimedia-history-base20'
icon: 'minerva-icon--recentChanges'
}
}
]
]
},
},
{
{
// * icon=gear | display=도구
// 도구 그룹
label: '도구',
label: '도구',
iconClass: 'mw-ui-icon-minerva-settings',
icon: 'minerva-icon--specialPages',
children: [
children: [
// ** icon=wrench | display=특수 문서 목록 | link=특수:특수문서 | title=특수:특수문서
{
{
label: '특수 문서 목록',
label: '특수 문서 목록',
// mw.util.getUrl 을 사용하면 위키 내 링크를 환경에 맞게 자동으로 생성합니다.
href: mw.util.getUrl( '특수:특수문서' ),
href: mw.util.getUrl( '특수:특수문서' ),
title: '특수:특수문서',
title: '특수:특수문서',
iconClass: 'mw-ui-icon-minerva-settings'
icon: 'minerva-icon--specialPages'
},
},
// ** icon=upload | display=파일 올리기
{
{
// "파일 올리기" (3단계 메뉴를 가진 항목)
label: '파일 올리기',
label: '파일 올리기',
href: '#', // 상위 항목은 드롭다운용이므로 실제 링크는 서브 메뉴에 둡니다.
href: '#', // 드롭다운 전용이므로 실제 링크는 없음
title: '파일 올리기',
title: '파일 올리기',
iconClass: 'mw-ui-icon-minerva-download',
icon: 'minerva-icon--specialPages',
children: [
children: [
// *** icon=cloud-upload-alt | display=(기존) 업로드 | link=특수:올리기 | title=특수:올리기
{
{
label: '(기존) 업로드',
label: '(기존) 업로드',
href: mw.util.getUrl( '특수:올리기' ),
href: mw.util.getUrl( '특수:올리기' ),
title: '특수:올리기',
title: '특수:올리기',
iconClass: 'mw-ui-icon-minerva-download'
icon: 'minerva-icon--specialPages'
},
},
// *** icon=file-upload | display=(신규) 업로드 마법사 | link=특수:UploadWizard | title=특수:UploadWizard
{
{
label: '(신규) 업로드 마법사',
label: '(신규) 업로드 마법사',
href: mw.util.getUrl( '특수:UploadWizard' ),
href: mw.util.getUrl( '특수:UploadWizard' ),
title: '특수:UploadWizard',
title: '특수:UploadWizard',
iconClass: 'mw-ui-icon-minerva-download'
icon: 'minerva-icon--specialPages'
}
}
]
]
},
},
// ** icon=flag | display=이슈 트래커 | link=https://bbs.gaonwiki.com/issue/ | title=...
{
{
label: '이슈 트래커',
label: '이슈 트래커',
href: 'https://bbs.gaonwiki.com/issue/',
href: 'https://bbs.gaonwiki.com/issue/',
title: 'https://bbs.gaonwiki.com/issue/',
title: 'https://bbs.gaonwiki.com/issue/',
iconClass: 'mw-ui-icon-minerva-die'
icon: 'minerva-icon--die'
},
},
// ** icon=flask | display=가온 위키 베타 | link=https://beta.gaonwiki.com | title=...
{
{
label: '가온 위키 베타',
label: '가온 위키 베타',
href: 'https://beta.gaonwiki.com',
href: 'https://beta.gaonwiki.com',
title: 'https://beta.gaonwiki.com',
title: 'https://beta.gaonwiki.com',
iconClass: 'mw-ui-icon-minerva-mapPin'
icon: 'minerva-icon--specialPages'
}
}
]
]
},
},
{
{
// * icon=book | display=도움말
// 도움말 그룹
label: '도움말',
label: '도움말',
iconClass: 'mw-ui-icon-wikimedia-help-base20', // 도움말 느낌의 아이콘
icon: 'minerva-icon--specialPages',
children: [
children: [
// ** icon=spell-check | display=위키 문법 | link=도움말:위키_문법 | title=도움말:위키_문법
{
{
label: '위키 문법',
label: '위키 문법',
href: mw.util.getUrl( '도움말:위키_문법' ),
href: mw.util.getUrl( '도움말:위키_문법' ),
title: '도움말:위키_문법',
title: '도움말:위키_문법',
iconClass: 'mw-ui-icon-wikimedia-help-base20'
icon: 'minerva-icon--specialPages'
},
},
// ** icon=book | display=설명서 | link=도움말:설명서 | title=도움말:설명서
{
{
label: '설명서',
label: '설명서',
href: mw.util.getUrl( '도움말:설명서' ),
href: mw.util.getUrl( '도움말:설명서' ),
title: '도움말:설명서',
title: '도움말:설명서',
iconClass: 'mw-ui-icon-wikimedia-help-base20'
icon: 'minerva-icon--specialPages'
}
}
]
]
154번째 줄: 142번째 줄:
];
];


/* -----------------------------
/* -------------------------------------------------
* 2. DOM 유틸리티 함수
* 2. DOM 생성 유틸리티
* ----------------------------- */
* ------------------------------------------------- */
 
/**
* Minerva 스타일의 아이콘 <span> 요소를 생성한다.
*
* @param {string} iconClass  "minerva-icon--XXX" 형식의 클래스
* @return {jQuery}
*/
function buildIcon( iconClass ) {
var $icon = $( '<span>' ).addClass( 'minerva-icon' );
if ( iconClass ) {
$icon.addClass( iconClass );
}
return $icon;
}
 
/**
* 메뉴 라벨 <span> 요소를 생성한다.
*
* @param {string} text  화면에 보일 텍스트
* @return {jQuery}
*/
function buildLabel( text ) {
return $( '<span>' )
.addClass( 'toggle-list-item__label' )
.text( text || '' );
}
 
/**
* Minerva 스타일의 <a> 요소를 생성한다.
*
* @param {string} href  링크 URL (없으면 '#')
* @param {string} title  title 속성
* @return {jQuery}
*/
function buildAnchor( href, title ) {
return $( '<a>' )
.addClass( 'toggle-list-item__anchor gaon-menu-link' )
.attr( 'href', href || '#' )
.attr( 'title', title || '' );
}


/**
/**
* 하위 항목(2단계, 3단계)을 생성하고, 지정된 <ul> 안에 넣는 재귀 함수입니다.
* 하위 메뉴 항목(레벨 2, 3)을 재귀적으로 생성하여 <ul> 안에 추가한다.
*
*
* @param {jQuery} $ul   현재 레벨의 <ul> 요소(jQuery 객체)
* @param {jQuery} $ul     현재 레벨의 <ul>
* @param {Object} item   메뉴 항목 정의 (label, href, title, iconClass, children 등)
* @param {Object} item   메뉴 정의 객체
* @param {number} level 현재 레벨(2, 3 ...)
* @param {number} level   레벨 번호(2, 3, ...)
*/
*/
function appendGaonMenuItem( $ul, item, level ) {
function appendGaonMenuItem( $ul, item, level ) {
var hasChildren = Array.isArray( item.children ) && item.children.length > 0;
// <li> 생성
// <li> 생성
var $li = $( '<li>' )
var $li = $( '<li>' )
.addClass( 'gaon-menu-item' )
.addClass( 'toggle-list-item gaon-menu-item gaon-menu-item-level' + level );
.addClass( 'gaon-menu-item-level' + level );
 
// 실제 링크가 없고 드롭다운 전용인 항목이면 href 를 '#' 으로 둡니다.
var href = item.href || '#';
 
// <a> 생성 및 속성 설정
var $a = $( '<a>' )
.attr( 'href', href )
// title 이 지정되어 있으면 title 사용, 없으면 label 사용
.attr( 'title', item.title || item.label || '' )
// 미네르바 아이콘 스타일 적용
.addClass( 'mw-ui-icon mw-ui-icon-before' );


// 아이콘 클래스가 지정되어 있다면 추가
if ( hasChildren ) {
if ( item.iconClass ) {
$li.addClass( 'gaon-menu-item--has-children gaon-menu-item--collapsed' );
$a.addClass( item.iconClass );
}
}


// 화면에 표시될 텍스트는 <span> 안에 넣습니다.
// <a> 생성
var $span = $( '<span>' ).text( item.label || '' );
var $a = buildAnchor( item.href, item.title || item.label );
$a.append( $span );
$a.append( buildIcon( item.icon ) );
 
$a.append( buildLabel( item.label ) );
$li.append( $a );
$li.append( $a );
$ul.append( $li );
$ul.append( $li );


// children 이 있다면 하위 <ul> 을 하나 더 만들어 재귀적으로 추가합니다.
// 하위 레벨이 있으면 <ul> 을 만들어 재귀 호출
if ( Array.isArray( item.children ) && item.children.length > 0 ) {
if ( hasChildren ) {
var nextLevel = level + 1;
var nextLevel = level + 1;
var $subUl = $( '<ul>' )
var $subUl = $( '<ul>' )
.addClass( 'gaon-menu' )
.addClass( 'gaon-menu gaon-menu-level' + nextLevel );
.addClass( 'gaon-menu-level' + nextLevel );


item.children.forEach( function ( child ) {
item.children.forEach( function ( child ) {
205번째 줄: 221번째 줄:
} );
} );


// 여기서는 부모 <li> 아래에 중첩 <ul> 을 넣어 구조를 명확히 합니다.
// 이 부분이 중요:
// 하위 <ul> 은 반드시 <li> 안에 들어가야 한다.
$li.append( $subUl );
$li.append( $subUl );
}
}
211번째 줄: 228번째 줄:


/**
/**
* 상위 그룹(게시판, 버그/기능 개선 )을 생성합니다.
* 한 개의 상위 그룹(게시판 / 버그/기능 개선 / 도구 / 도움말)을
* 각 그룹은 level1 <ul> 안에 헤더 <li> 와 하위 level2 <ul> 을 가지게 됩니다.
* Minerva 스타일의 <ul> 블록으로 생성한다.
*
*
* @returns {jQuery} 햄버거 메뉴에 삽입할 상위 DOM 묶음(jQuery 객체)
* @param {Object} group  그룹 정의
* @param {number} index  그룹 index
* @return {jQuery}       완성된 <ul> 요소
*/
*/
function buildGaonMenu() {
function buildGroup( group, index ) {
// 루트 컨테이너 역할을 할 <div> 를 하나 만든 뒤,
// Minerva 기본 메뉴와 같은 class 사용
// 그 안에 여러 개의 <ul class="gaon-menu gaon-menu-level1"> 을 쌓습니다.
var $level1 = $( '<ul>' )
var $container = $( '<div>' ).addClass( 'gaon-menu-container' );
.addClass( 'toggle-list__list gaon-menu gaon-menu-level1' )
.attr( 'data-gaon-group-index', index );


gaonMenuConfig.forEach( function ( group, index ) {
var hasChildren = Array.isArray( group.children ) && group.children.length > 0;
// 각 그룹마다 level1 <ul> 하나씩 생성
var $level1 = $( '<ul>' )
.addClass( 'gaon-menu' )
.addClass( 'gaon-menu-level1' )
.attr( 'data-gaon-group-index', index );


// 헤더(클릭 시 하위 level2 열고 닫는 부분) <li>
// 헤더 <li>
var $headerLi = $( '<li>' )
var $headerLi = $( '<li>' )
.addClass( 'gaon-menu-header' );
.addClass( 'toggle-list-item gaon-menu-header gaon-menu-header--collapsed' );


var $headerA = $( '<a>' )
var $headerA = buildAnchor( '#', group.label );
// 상위 그룹은 토글 전용이므로 href="#" 로 두고 기본 동작을 막습니다.
$headerA
.attr( 'href', '#' )
.addClass( 'gaon-menu-header__anchor' )
.attr( 'title', group.label || '' )
.append( buildIcon( group.icon ) )
.addClass( 'mw-ui-icon mw-ui-icon-before' );
.append( buildLabel( group.label ) );


if ( group.iconClass ) {
$headerLi.append( $headerA );
$headerA.addClass( group.iconClass );
}


$headerA.append(
// 그룹에 하위 항목이 있으면 level2 <ul> 생성
$( '<span>' ).text( group.label || '' )
if ( hasChildren ) {
);
var $level2 = $( '<ul>' )
.addClass( 'gaon-menu gaon-menu-level2' );


$headerLi.append( $headerA );
group.children.forEach( function ( item ) {
$level1.append( $headerLi );
appendGaonMenuItem( $level2, item, 2 );
} );


// 하위 메뉴(level2) 생성
// 헤더 <li> 안에 하위 <ul> 을 넣는다.
if ( Array.isArray( group.children ) && group.children.length > 0 ) {
$headerLi.append( $level2 );
var $level2 = $( '<ul>' )
}
.addClass( 'gaon-menu' )
.addClass( 'gaon-menu-level2' );
 
group.children.forEach( function ( item ) {
appendGaonMenuItem( $level2, item, 2 );
} );
 
$level1.append( $level2 );
}
 
$container.append( $level1 );
} );


// 컨테이너 안의 자식 요소들만 반환합니다.
$level1.append( $headerLi );
return $container.children();
return $level1;
}
}


/* -----------------------------
/* -------------------------------------------------
* 3. 메뉴를 미네르바 햄버거 메뉴 안에 삽입
* 3. 메뉴를 실제 Minerva 햄버거 메뉴 안에 삽입
* ----------------------------- */
* ------------------------------------------------- */


/**
/**
* 실제로 DOM 을 찾아서, 기본 메뉴 아래에 가온 메뉴를 삽입하는 함수입니다.
* 가온 메뉴를 Minerva 왼쪽 메뉴에 삽입한다.
*/
*/
function insertGaonMenuIntoMinerva() {
function insertGaonMenuIntoMinerva() {
// 미네르바 햄버거 메뉴에서, 기존 최상위 <ul> 을 찾습니다.
// 기본 내비게이션 블록(대문/임의의 문서로)이 들어 있는 첫 번째 toggle-list__list 를 찾는다.
// Qiita 예제에서는 '.menu ul:first' 를 기준으로 사용합니다. :contentReference[oaicite:6]{index=6}
var $firstList = $( '#p-navigation.toggle-list__list' ).first();
var $firstUl = $( '.menu ul:first' );
 
// 혹시 id 가 다르다면 fallback: 메뉴 안의 첫 toggle-list__list
if ( !$firstList.length ) {
$firstList = $( '#mw-mf-page-left .toggle-list__list' ).first();
}


if ( !$firstUl.length ) {
if ( !$firstList.length ) {
return;
return;
}
}


// 중복 삽입 방지: 이미 가온 메뉴를 추가했으면 다시 하지 않습니다.
// 중복 삽입 방지
if ( $firstUl.data( 'gaonMenuInjected' ) ) {
if ( $firstList.data( 'gaonMenuInjected' ) ) {
return;
return;
}
}
$firstUl.data( 'gaonMenuInjected', true );
$firstList.data( 'gaonMenuInjected', true );


// 우리의 메뉴 DOM 을 생성합니다.
// 여러 그룹을 한 번에 생성하여 첫 번째 리스트 뒤에 삽입
var $gaonMenuNodes = buildGaonMenu();
var $allGroups = $( [] );
gaonMenuConfig.forEach( function ( group, index ) {
$allGroups = $allGroups.add( buildGroup( group, index ) );
} );


// 기존 첫 번째 <ul> 바로 뒤에 삽입합니다.
$firstList.after( $allGroups );
$firstUl.after( $gaonMenuNodes );


// 하위 레벨 메뉴(level2, level3 …)는 기본적으로 숨겨 둡니다.
// 모든 하위 레벨을 기본적으로 숨긴다.
$( '.gaon-menu-level2, .gaon-menu-level3' ).hide();
$( '.gaon-menu-level2, .gaon-menu-level3' ).hide();


// 상위 그룹(게시판 / 버그/기능 개선 / 도구 / 도움말) 클릭 시,
// 상위 그룹 헤더 클릭 시 level2 토글
// 해당 그룹 내부의 level2 <ul> 을 토글합니다.
$( '.gaon-menu-header__anchor' ).on( 'click', function ( e ) {
$( '.gaon-menu-header > a' ).on( 'click', function ( e ) {
e.preventDefault();
e.preventDefault();


var $header = $( this );
var $headerLi = $( this ).closest( '.gaon-menu-header' );
var $level1 = $header.closest( '.gaon-menu-level1' );
var $sub = $headerLi.children( '.gaon-menu-level2' );
var $level2 = $level1.children( '.gaon-menu-level2' );
 
if ( !$sub.length ) {
return;
}


$level2.slideToggle( 200 );
$headerLi.toggleClass( 'gaon-menu-header--collapsed' );
$sub.slideToggle( 150 );
} );
} );


// "파일 올리기" 같은 중간 단계에 children 이 있는 항목은
// "파일 올리기"처럼 children 이 있는 2단계 항목 클릭 시 level3 토글
// 자신의 하위 level3 <ul> 을 토글합니다.
$( '.gaon-menu-item--has-children > .gaon-menu-link' ).on( 'click', function ( e ) {
$( '.gaon-menu-item-level2 > a' ).each( function () {
e.preventDefault();
var $a = $( this );
var $li = $a.closest( 'li' );
var $subUl = $li.children( '.gaon-menu-level3' );


if ( $subUl.length ) {
var $li = $( this ).closest( '.gaon-menu-item--has-children' );
// children 이 있는 항목은 토글 버튼 역할을 하도록 클릭 핸들러를 붙입니다.
var $sub = $li.children( '.gaon-menu-level3' );
$a.on( 'click', function ( e ) {
 
e.preventDefault();
if ( !$sub.length ) {
$subUl.slideToggle( 200 );
return;
} );
}
}
$li.toggleClass( 'gaon-menu-item--collapsed' );
$sub.slideToggle( 150 );
} );
} );
}
}


/* -----------------------------
/* -------------------------------------------------
* 4. 메뉴 DOM 생성 시점 기다리기
* 4. 메뉴 HTML 로드 완료를 기다렸다가 삽입
* ----------------------------- */
* ------------------------------------------------- */
 
// 미네르바 메뉴 HTML 은 JS 로 동적으로 만들어지는 경우가 있기 때문에,
// DOM 이 준비될 때까지 주기적으로 검사하는 방식(Qiita 예제와 동일한 방식)을 사용합니다. :contentReference[oaicite:7]{index=7}


var maxAttempts = 50; // 최대 50번(약 5초)까지 시도
var maxAttempts = 50; // 최대 50번(약 5초 정도) 시도
var attemptCount = 0;
var attemptCount = 0;


function waitForMinervaMenu() {
function waitForMinervaMenu() {
var $firstUl = $( '.menu ul:first' );
var $firstList = $( '#mw-mf-page-left .toggle-list__list' ).first();


if ( $firstUl.length ) {
if ( $firstList.length ) {
// 메뉴를 찾았으면 바로 삽입 시도
insertGaonMenuIntoMinerva();
insertGaonMenuIntoMinerva();
} else if ( attemptCount < maxAttempts ) {
} else if ( attemptCount < maxAttempts ) {
351번째 줄: 358번째 줄:
}
}


// 문서 로드 이후에 검사 시작
$( waitForMinervaMenu );
$( waitForMinervaMenu );


}( mediaWiki, jQuery ) );
}( mediaWiki, jQuery ) );

2025년 11월 19일 (수) 20:12 기준 최신판

/* All JavaScript here will be loaded for users of the MinervaNeue skin */
/* 가온 위키 Minerva Neue 전용 커스텀 메뉴
 * - PC/모바일 공통
 * - Minerva 기본 메뉴와 같은 구조/클래스 사용
 *   (toggle-list__list / toggle-list-item / toggle-list-item__anchor /
 *    minerva-icon / toggle-list-item__label)
 * - 상위 그룹(게시판, 버그/기능 개선, 도구, 도움말)은 접고 펼칠 수 있음
 * - "파일 올리기" 아래에 3단계 메뉴 포함
 */

( function ( mw, $ ) {
	'use strict';

	// Minerva 스킨이 아닐 때는 아무 것도 하지 않는다.
	if ( mw.config.get( 'skin' ) !== 'minerva' ) {
		return;
	}

	/* -------------------------------------------------
	 * 1. 메뉴 설정 (질문에서 주신 내용을 JS 객체로 변환)
	 * ------------------------------------------------- */

	// icon 필드에는 Minerva 메뉴에서 사용하는
	// `minerva-icon minerva-icon--XXX` 의 XXX 부분만 넣는다.
	// 예: icon: 'minerva-icon--speechBubbles'
	var gaonMenuConfig = [
		{
			// 게시판 그룹
			label: '게시판',
			icon: 'minerva-icon--speechBubbles',
			children: [
				{
					label: '위키방',
					href: 'https://bbs.gaonwiki.com',
					title: 'https://bbs.gaonwiki.com',
					icon: 'minerva-icon--speechBubbles'
				},
				{
					label: '저작권 처리 게시판',
					href: 'https://bbs.gaonwiki.com/copyrights',
					title: 'https://bbs.gaonwiki.com/copyrights',
					icon: 'minerva-icon--speechBubbles'
				},
				{
					label: '자유게시판',
					href: 'https://bbs.gaonwiki.com/free',
					title: 'https://bbs.gaonwiki.com/free',
					icon: 'minerva-icon--speechBubbles'
				}
			]
		},
		{
			// 버그/기능 개선 그룹
			label: '버그/기능 개선',
			icon: 'minerva-icon--die',
			children: [
				{
					label: '이슈 등록',
					href: 'https://bbs.gaonwiki.com/issue',
					title: 'https://bbs.gaonwiki.com/issue',
					icon: 'minerva-icon--die'
				},
				{
					label: '기능 구현 도움',
					href: 'https://www.gaonwiki.com/w/%EA%B0%80%EC%98%A8_%EC%9C%84%ED%82%A4:%EA%B8%B0%EB%8A%A5_%EA%B5%AC%ED%98%84_%EB%B6%88%EA%B0%80-%EB%88%84%EA%B0%80_%ED%95%B4%EA%B2%B0%EC%B1%85%EC%9D%84_%EC%95%8C%EB%A0%A4%EC%A3%BC%EC%84%B8%EC%9A%94!',
					title: 'https://www.gaonwiki.com/w/가온_위키:기능_구현_불가-누가_해결책을_알려주세요!',
					icon: 'minerva-icon--speechBubbles'
				},
				{
					label: '알려진 문제들',
					href: 'https://www.gaonwiki.com/w/%EA%B0%80%EC%98%A8_%EC%9C%84%ED%82%A4/%EC%95%8C%EB%A0%A4%EC%A7%84_%EB%AC%B8%EC%A0%9C%EB%93%A4',
					title: 'https://www.gaonwiki.com/w/가온_위키/알려진_문제들',
					icon: 'minerva-icon--recentChanges'
				}
			]
		},
		{
			// 도구 그룹
			label: '도구',
			icon: 'minerva-icon--specialPages',
			children: [
				{
					label: '특수 문서 목록',
					href: mw.util.getUrl( '특수:특수문서' ),
					title: '특수:특수문서',
					icon: 'minerva-icon--specialPages'
				},
				{
					// "파일 올리기" (3단계 메뉴를 가진 항목)
					label: '파일 올리기',
					href: '#', // 드롭다운 전용이므로 실제 링크는 없음
					title: '파일 올리기',
					icon: 'minerva-icon--specialPages',
					children: [
						{
							label: '(기존) 업로드',
							href: mw.util.getUrl( '특수:올리기' ),
							title: '특수:올리기',
							icon: 'minerva-icon--specialPages'
						},
						{
							label: '(신규) 업로드 마법사',
							href: mw.util.getUrl( '특수:UploadWizard' ),
							title: '특수:UploadWizard',
							icon: 'minerva-icon--specialPages'
						}
					]
				},
				{
					label: '이슈 트래커',
					href: 'https://bbs.gaonwiki.com/issue/',
					title: 'https://bbs.gaonwiki.com/issue/',
					icon: 'minerva-icon--die'
				},
				{
					label: '가온 위키 베타',
					href: 'https://beta.gaonwiki.com',
					title: 'https://beta.gaonwiki.com',
					icon: 'minerva-icon--specialPages'
				}
			]
		},
		{
			// 도움말 그룹
			label: '도움말',
			icon: 'minerva-icon--specialPages',
			children: [
				{
					label: '위키 문법',
					href: mw.util.getUrl( '도움말:위키_문법' ),
					title: '도움말:위키_문법',
					icon: 'minerva-icon--specialPages'
				},
				{
					label: '설명서',
					href: mw.util.getUrl( '도움말:설명서' ),
					title: '도움말:설명서',
					icon: 'minerva-icon--specialPages'
				}
			]
		}
	];

	/* -------------------------------------------------
	 * 2. DOM 생성 유틸리티
	 * ------------------------------------------------- */

	/**
	 * Minerva 스타일의 아이콘 <span> 요소를 생성한다.
	 *
	 * @param {string} iconClass  "minerva-icon--XXX" 형식의 클래스
	 * @return {jQuery}
	 */
	function buildIcon( iconClass ) {
		var $icon = $( '<span>' ).addClass( 'minerva-icon' );
		if ( iconClass ) {
			$icon.addClass( iconClass );
		}
		return $icon;
	}

	/**
	 * 메뉴 라벨 <span> 요소를 생성한다.
	 *
	 * @param {string} text  화면에 보일 텍스트
	 * @return {jQuery}
	 */
	function buildLabel( text ) {
		return $( '<span>' )
			.addClass( 'toggle-list-item__label' )
			.text( text || '' );
	}

	/**
	 * Minerva 스타일의 <a> 요소를 생성한다.
	 *
	 * @param {string} href   링크 URL (없으면 '#')
	 * @param {string} title  title 속성
	 * @return {jQuery}
	 */
	function buildAnchor( href, title ) {
		return $( '<a>' )
			.addClass( 'toggle-list-item__anchor gaon-menu-link' )
			.attr( 'href', href || '#' )
			.attr( 'title', title || '' );
	}

	/**
	 * 하위 메뉴 항목(레벨 2, 3)을 재귀적으로 생성하여 <ul> 안에 추가한다.
	 *
	 * @param {jQuery} $ul     현재 레벨의 <ul>
	 * @param {Object} item    메뉴 정의 객체
	 * @param {number} level   레벨 번호(2, 3, ...)
	 */
	function appendGaonMenuItem( $ul, item, level ) {
		var hasChildren = Array.isArray( item.children ) && item.children.length > 0;

		// <li> 생성
		var $li = $( '<li>' )
			.addClass( 'toggle-list-item gaon-menu-item gaon-menu-item-level' + level );

		if ( hasChildren ) {
			$li.addClass( 'gaon-menu-item--has-children gaon-menu-item--collapsed' );
		}

		// <a> 생성
		var $a = buildAnchor( item.href, item.title || item.label );
		$a.append( buildIcon( item.icon ) );
		$a.append( buildLabel( item.label ) );
		$li.append( $a );
		$ul.append( $li );

		// 하위 레벨이 있으면 <ul> 을 만들어 재귀 호출
		if ( hasChildren ) {
			var nextLevel = level + 1;
			var $subUl = $( '<ul>' )
				.addClass( 'gaon-menu gaon-menu-level' + nextLevel );

			item.children.forEach( function ( child ) {
				appendGaonMenuItem( $subUl, child, nextLevel );
			} );

			// 이 부분이 중요:
			// 하위 <ul> 은 반드시 <li> 안에 들어가야 한다.
			$li.append( $subUl );
		}
	}

	/**
	 * 한 개의 상위 그룹(게시판 / 버그/기능 개선 / 도구 / 도움말)을
	 * Minerva 스타일의 <ul> 블록으로 생성한다.
	 *
	 * @param {Object} group   그룹 정의
	 * @param {number} index   그룹 index
	 * @return {jQuery}        완성된 <ul> 요소
	 */
	function buildGroup( group, index ) {
		// Minerva 기본 메뉴와 같은 class 사용
		var $level1 = $( '<ul>' )
			.addClass( 'toggle-list__list gaon-menu gaon-menu-level1' )
			.attr( 'data-gaon-group-index', index );

		var hasChildren = Array.isArray( group.children ) && group.children.length > 0;

		// 헤더 <li>
		var $headerLi = $( '<li>' )
			.addClass( 'toggle-list-item gaon-menu-header gaon-menu-header--collapsed' );

		var $headerA = buildAnchor( '#', group.label );
		$headerA
			.addClass( 'gaon-menu-header__anchor' )
			.append( buildIcon( group.icon ) )
			.append( buildLabel( group.label ) );

		$headerLi.append( $headerA );

		// 그룹에 하위 항목이 있으면 level2 <ul> 생성
		if ( hasChildren ) {
			var $level2 = $( '<ul>' )
				.addClass( 'gaon-menu gaon-menu-level2' );

			group.children.forEach( function ( item ) {
				appendGaonMenuItem( $level2, item, 2 );
			} );

			// 헤더 <li> 안에 하위 <ul> 을 넣는다.
			$headerLi.append( $level2 );
		}

		$level1.append( $headerLi );
		return $level1;
	}

	/* -------------------------------------------------
	 * 3. 메뉴를 실제 Minerva 햄버거 메뉴 안에 삽입
	 * ------------------------------------------------- */

	/**
	 * 가온 메뉴를 Minerva 왼쪽 메뉴에 삽입한다.
	 */
	function insertGaonMenuIntoMinerva() {
		// 기본 내비게이션 블록(대문/임의의 문서로)이 들어 있는 첫 번째 toggle-list__list 를 찾는다.
		var $firstList = $( '#p-navigation.toggle-list__list' ).first();

		// 혹시 id 가 다르다면 fallback: 메뉴 안의 첫 toggle-list__list
		if ( !$firstList.length ) {
			$firstList = $( '#mw-mf-page-left .toggle-list__list' ).first();
		}

		if ( !$firstList.length ) {
			return;
		}

		// 중복 삽입 방지
		if ( $firstList.data( 'gaonMenuInjected' ) ) {
			return;
		}
		$firstList.data( 'gaonMenuInjected', true );

		// 여러 그룹을 한 번에 생성하여 첫 번째 리스트 뒤에 삽입
		var $allGroups = $( [] );
		gaonMenuConfig.forEach( function ( group, index ) {
			$allGroups = $allGroups.add( buildGroup( group, index ) );
		} );

		$firstList.after( $allGroups );

		// 모든 하위 레벨을 기본적으로 숨긴다.
		$( '.gaon-menu-level2, .gaon-menu-level3' ).hide();

		// 상위 그룹 헤더 클릭 시 level2 토글
		$( '.gaon-menu-header__anchor' ).on( 'click', function ( e ) {
			e.preventDefault();

			var $headerLi = $( this ).closest( '.gaon-menu-header' );
			var $sub = $headerLi.children( '.gaon-menu-level2' );

			if ( !$sub.length ) {
				return;
			}

			$headerLi.toggleClass( 'gaon-menu-header--collapsed' );
			$sub.slideToggle( 150 );
		} );

		// "파일 올리기"처럼 children 이 있는 2단계 항목 클릭 시 level3 토글
		$( '.gaon-menu-item--has-children > .gaon-menu-link' ).on( 'click', function ( e ) {
			e.preventDefault();

			var $li = $( this ).closest( '.gaon-menu-item--has-children' );
			var $sub = $li.children( '.gaon-menu-level3' );

			if ( !$sub.length ) {
				return;
			}

			$li.toggleClass( 'gaon-menu-item--collapsed' );
			$sub.slideToggle( 150 );
		} );
	}

	/* -------------------------------------------------
	 * 4. 메뉴 HTML 로드 완료를 기다렸다가 삽입
	 * ------------------------------------------------- */

	var maxAttempts = 50; // 최대 50번(약 5초 정도) 시도
	var attemptCount = 0;

	function waitForMinervaMenu() {
		var $firstList = $( '#mw-mf-page-left .toggle-list__list' ).first();

		if ( $firstList.length ) {
			insertGaonMenuIntoMinerva();
		} else if ( attemptCount < maxAttempts ) {
			attemptCount += 1;
			setTimeout( waitForMinervaMenu, 100 );
		}
	}

	$( waitForMinervaMenu );

}( mediaWiki, jQuery ) );

최근 바뀜

더 보기