참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.
- 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
- 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
- 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
- 오페라: Ctrl-F5를 입력.
/* =========================================
CategoryPillar
대문 상단 카테고리의 중앙 「프로젝트」 기둥
========================================= */
/*
CategoryNav.js가 만든 네 개의 실제 버튼은 그대로 둔다.
중앙 project 항목의 SVG 좌표만 읽어서 클릭 가능한 버튼을 제거하고,
상하 우물을 관통해 #1d1d1d 외곽 프레임에 이어지는 비조작 기둥으로 바꾼다.
*/
(function (window, document, mw) {
'use strict';
var SVG_NS = 'http://www.w3.org/2000/svg';
var VERSION = '20260716-category-project-pillar-open-sides-021';
var resizeTimer = 0;
var renderTimer = 0;
var observer = null;
function createSvgElement(name, className) {
var node = document.createElementNS(SVG_NS, name);
if (className) node.setAttribute('class', className);
return node;
}
function parsePoints(value) {
var points = [];
String(value || '').trim().split(/\s+/).forEach(function (pair) {
var xy = pair.split(',');
var x = Number(xy[0]);
var y = Number(xy[1]);
if (xy.length === 2 && Number.isFinite(x) && Number.isFinite(y)) {
points.push([x, y]);
}
});
return points;
}
function getViewBox(svg) {
var base = svg && svg.viewBox && svg.viewBox.baseVal;
var raw;
var values;
if (base && base.width > 0 && base.height > 0) {
return {
x:base.x,
y:base.y,
width:base.width,
height:base.height
};
}
raw = String(svg.getAttribute('viewBox') || '').trim();
values = raw.split(/[\s,]+/).map(Number);
if (
values.length === 4 &&
values.every(Number.isFinite) &&
values[2] > 0 &&
values[3] > 0
) {
return { x:values[0], y:values[1], width:values[2], height:values[3] };
}
return null;
}
function getVerticalExtension(svg, viewBox) {
var nav = svg.closest ? svg.closest('.portal-category-nav') : svg.parentNode;
var style;
var rect;
var renderedHeight;
var paddingTop;
var paddingBottom;
var scale;
if (!nav || !window.getComputedStyle) return { top:3, bottom:3 };
style = window.getComputedStyle(nav);
rect = svg.getBoundingClientRect ? svg.getBoundingClientRect() : null;
renderedHeight = rect && rect.height ? rect.height : Number(svg.clientHeight || 0);
paddingTop = parseFloat(style.paddingTop) || 0;
paddingBottom = parseFloat(style.paddingBottom) || 0;
scale = renderedHeight > 0 ? viewBox.height / renderedHeight : 1;
return {
top:paddingTop > 0 ? paddingTop * scale : 3,
bottom:paddingBottom > 0 ? paddingBottom * scale : 3
};
}
function setLine(line, x1, y1, x2, y2) {
line.setAttribute('x1', String(x1));
line.setAttribute('y1', String(y1));
line.setAttribute('x2', String(x2));
line.setAttribute('y2', String(y2));
}
function lineXAtY(topX, bottomX, y, viewBox) {
return topX + (bottomX - topX) * ((y - viewBox.y) / viewBox.height);
}
function pointString(points) {
return points.map(function (point) {
return point[0] + ',' + point[1];
}).join(' ');
}
function findProjectSource(svg) {
var direct = svg.querySelector(
'.portal-cat-anchor[data-category-key="project"]'
);
var match = null;
if (direct) return direct;
Array.prototype.some.call(svg.querySelectorAll('.portal-cat-anchor'), function (anchor) {
var label = anchor.querySelector('.portal-cat-label');
if (label && String(label.textContent || '').trim() === '프로젝트') {
match = anchor;
return true;
}
return false;
});
return match;
}
function renderSvg(svg) {
var source = findProjectSource(svg);
var edge = source && source.querySelector('.portal-cat-edge');
var sourceLabel = source && source.querySelector('.portal-cat-label');
var points = edge ? parsePoints(edge.getAttribute('points')) : [];
var viewBox = getViewBox(svg);
var previous = svg.querySelector('.portal-category-pillar');
var extension;
var leftTop;
var leftBottom;
var rightTop;
var rightBottom;
var pillarLeftTop;
var pillarLeftBottom;
var pillarRightTop;
var pillarRightBottom;
var top;
var bottom;
var group;
var surface;
var leftInset;
var rightInset;
var label;
if (!source || points.length < 4 || !viewBox) return false;
if (previous && previous.parentNode) previous.parentNode.removeChild(previous);
extension = getVerticalExtension(svg, viewBox);
top = viewBox.y - extension.top;
bottom = viewBox.y + viewBox.height + extension.bottom;
/*
CategoryNav의 점 순서는 좌상→우상→우하→좌하다.
원래 project 버튼의 좌우 사선을 그대로 사용하면 양옆 버튼이 이미 확보한
2px 간격도 전 높이에서 그대로 유지된다. 기둥 구간은 같은 기울기로
위·아래 프레임까지 직선 연장한다.
*/
leftTop = points[0][0];
rightTop = points[1][0];
rightBottom = points[2][0];
leftBottom = points[3][0];
pillarLeftTop = lineXAtY(leftTop, leftBottom, top, viewBox);
pillarLeftBottom = lineXAtY(leftTop, leftBottom, bottom, viewBox);
pillarRightTop = lineXAtY(rightTop, rightBottom, top, viewBox);
pillarRightBottom = lineXAtY(rightTop, rightBottom, bottom, viewBox);
if (
!(pillarRightTop > pillarLeftTop) ||
!(pillarRightBottom > pillarLeftBottom)
) return false;
group = createSvgElement('g', 'portal-category-pillar');
group.setAttribute('data-category-key', 'project');
group.setAttribute('aria-label', sourceLabel ? sourceLabel.textContent : '프로젝트');
group.setAttribute('pointer-events', 'none');
surface = createSvgElement('polygon', 'portal-category-pillar-surface');
surface.setAttribute('points', pointString([
[pillarLeftTop, top],
[pillarRightTop, top],
[pillarRightBottom, bottom],
[pillarLeftBottom, bottom]
]));
group.appendChild(surface);
/*
상하선은 만들지 않는다. 그래야 같은 #1d1d1d인 외곽 프레임과 기둥이
끊기지 않는다. 버튼과 접하는 좌우에도 #050505 외곽선을 두지 않고,
기둥 안쪽의 방향성 inset만 유지한다.
*/
leftInset = createSvgElement('line', 'portal-category-pillar-line portal-category-pillar-shadow');
setLine(leftInset, pillarLeftTop + 1.5, top, pillarLeftBottom + 1.5, bottom);
group.appendChild(leftInset);
rightInset = createSvgElement('line', 'portal-category-pillar-line portal-category-pillar-highlight');
setLine(rightInset, pillarRightTop - 1.5, top, pillarRightBottom - 1.5, bottom);
group.appendChild(rightInset);
label = createSvgElement('text', 'portal-cat-label portal-category-pillar-label');
label.setAttribute('x', String((leftTop + rightTop + rightBottom + leftBottom) / 4));
label.setAttribute('y', String(viewBox.y + viewBox.height / 2));
label.textContent = sourceLabel ? sourceLabel.textContent : '프로젝트';
group.appendChild(label);
source.setAttribute('aria-hidden', 'true');
source.setAttribute('tabindex', '-1');
source.classList.add('portal-category-pillar-source');
svg.appendChild(group);
svg.classList.add('has-category-pillar');
return true;
}
function collectSvgs(root) {
var scope = root && root.querySelectorAll ? root : document;
var output = [];
if (scope.matches && scope.matches('.portal-category-svg')) output.push(scope);
Array.prototype.forEach.call(
scope.querySelectorAll('.main-portal [data-component="category-nav"] .portal-category-svg'),
function (svg) { output.push(svg); }
);
return output;
}
function renderAll(root) {
var rendered = 0;
collectSvgs(root).forEach(function (svg) {
if (renderSvg(svg)) rendered += 1;
});
return rendered;
}
function scheduleRender(root) {
window.clearTimeout(renderTimer);
renderTimer = window.setTimeout(function () {
renderTimer = 0;
renderAll(root || document);
}, 0);
}
function observe() {
if (observer || !window.MutationObserver || !document.documentElement) return;
observer = new window.MutationObserver(function (records) {
var relevant = records.some(function (record) {
return Array.prototype.some.call(record.addedNodes || [], function (node) {
return node && node.nodeType === 1 && (
(node.matches && node.matches('.portal-category-svg')) ||
(node.querySelector && node.querySelector('.portal-category-svg'))
);
});
});
if (relevant) scheduleRender(document);
});
observer.observe(document.documentElement, { childList:true, subtree:true });
}
function init(root) {
renderAll(root || document);
observe();
}
if (mw && mw.hook) {
mw.hook('wikipage.content').add(function (content) {
scheduleRender(content && content[0] ? content[0] : document);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { init(document); });
} else {
init(document);
}
window.addEventListener('resize', function () {
window.clearTimeout(resizeTimer);
resizeTimer = window.setTimeout(function () {
resizeTimer = 0;
renderAll(document);
}, 80);
});
window.CategoryPillar = {
version:VERSION,
init:init,
render:renderSvg,
renderAll:renderAll
};
}(window, document, window.mw));