feat: 워크스페이스 편의 개선 + 매뉴얼에 화면구성·단축키 페이지 추가

워크스페이스(탭)
- 탭 전환 시 좌측 사이드바(대메뉴/소메뉴) 강조 자동 동기화
  - nav 스크립트에 window.govPortalNav.syncByUrl() 공개, renderSidebar(overrideHref) 확장
- 키보드 단축키(Alt 기반): Alt+1~9 탭 이동, Alt+W 닫기, Alt+[ / Alt+] 이전·다음
  - iframe 내부 포커스에서도 동작하도록 같은 출처 문서에 핸들러 부착
- 탭 가운데(휠) 클릭으로 닫기, 잘린 탭 제목 전체 툴팁

매뉴얼
- 신규 페이지 [화면 구성·워크스페이스·단축키] (05_workspace.md, 목차 2번째)
  - 화면 구성, 탭 사용법·유지 범위, 단축키 표, 이동/도움말 안내
- 개요 페이지에서 새 페이지로 안내

e2e: 워크스페이스(사이드바 동기화·가운데클릭) + 매뉴얼(새 페이지·단축키·검색) 케이스 추가

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
taekyoungc
2026-06-08 23:09:25 +09:00
parent 1a443de02e
commit 7e32f579e8
7 changed files with 174 additions and 2 deletions

View File

@@ -124,6 +124,8 @@ if ($effectiveLgIdx) {
tabs[k].btn.classList.toggle('active', k === id);
});
if (empty) empty.style.display = 'none';
// 현재 탭에 맞춰 좌측 사이드바(대메뉴/소메뉴) 강조 동기화
try { if (window.govPortalNav && tabs[id]) window.govPortalNav.syncByUrl(tabs[id].url); } catch (e) {}
persist();
}
@@ -155,11 +157,16 @@ if ($effectiveLgIdx) {
frame.className = 'ws-frame';
frame.src = withEmbed(url);
frame.setAttribute('title', title || '탭');
// iframe 내부에 포커스가 있어도 단축키가 동작하도록 같은 출처 문서에 핸들러 부착
frame.addEventListener('load', function () {
try { frame.contentDocument.addEventListener('keydown', handleShortcut); } catch (e) {}
});
panels.appendChild(frame);
var btn = document.createElement('div');
btn.className = 'ws-tab';
btn.setAttribute('role', 'tab');
btn.title = title || '탭'; // 잘린 이름 전체를 툴팁으로
var nameSpan = document.createElement('span');
nameSpan.className = 't-name';
nameSpan.textContent = title || '탭';
@@ -177,6 +184,9 @@ if ($effectiveLgIdx) {
else if (e.target === refresh) { activate(id); reloadTab(id); }
else { activate(id); }
});
// 가운데(휠) 클릭으로 탭 닫기 — 브라우저 탭과 동일한 동작
btn.addEventListener('mousedown', function (e) { if (e.button === 1) e.preventDefault(); });
btn.addEventListener('auxclick', function (e) { if (e.button === 1) { e.preventDefault(); closeTab(id); } });
bar.appendChild(btn);
tabs[id] = { url: url, title: title, frame: frame, btn: btn };
@@ -185,6 +195,26 @@ if ($effectiveLgIdx) {
}
window.wsOpenTab = openTab;
// 키보드 단축키 (브라우저 기본 단축키와 충돌하지 않도록 Alt 기반)
// Alt+1~9: 해당 번호 탭으로 · Alt+W: 현재 탭 닫기 · Alt+[ / Alt+]: 이전/다음 탭
function handleShortcut(e) {
if (!e.altKey || e.ctrlKey || e.metaKey) return;
var k = e.key;
if (k >= '1' && k <= '9') {
var i = parseInt(k, 10) - 1;
if (order[i]) { e.preventDefault(); activate(order[i]); }
} else if (k === 'w' || k === 'W' || k === 'ㅈ') {
if (activeId) { e.preventDefault(); closeTab(activeId); }
} else if (k === '[' || k === ']') {
if (!activeId || order.length < 2) return;
e.preventDefault();
var cur = order.indexOf(activeId);
var nx = k === ']' ? (cur + 1) % order.length : (cur - 1 + order.length) % order.length;
activate(order[nx]);
}
}
document.addEventListener('keydown', handleShortcut);
// 좌측 사이드바 소메뉴 클릭 → 탭으로 열기 (전체 페이지 이동 대신)
document.querySelector('.sidebar').addEventListener('click', function (e) {
var a = e.target.closest('a[href]');

View File

@@ -7,7 +7,7 @@
var titleEl = document.getElementById('portalSidebarTitle');
if (listEl && navData.length) {
function renderSidebar(idx) {
function renderSidebar(idx, overrideHref) {
var parent = navData[idx];
if (!parent) return;
if (titleEl) titleEl.textContent = parent.name || 'MY MENU';
@@ -22,10 +22,13 @@
listEl.appendChild(empty);
return;
}
// overrideHref 가 빈 문자열이면 "이 그룹에 일치 항목 없음" → 첫 항목 강조하지 않음
var hasOverride = (typeof overrideHref === 'string');
var activeHref = hasOverride ? overrideHref : activeChildHref;
items.forEach(function (child, ci) {
var li = document.createElement('li');
var chHref = (child.href || '').toLowerCase().replace(/^\//, '');
var on = activeChildHref ? (chHref === activeChildHref) : (ci === 0);
var on = activeHref ? (chHref === activeHref) : (hasOverride ? false : ci === 0);
if (child.href) {
li.innerHTML = '<a href="' + child.url + '" class="' + (on ? 'active' : '') + '">' +
'<span class="menu-ico">' + (on ? '▸' : '·') + '</span>' + child.name + '</a>';
@@ -58,6 +61,30 @@
setActiveTrigger(activeIdx);
renderSidebar(activeIdx);
// 워크스페이스 등 외부에서 "현재 보는 화면(URL)"에 맞춰 사이드바를 동기화하기 위한 공개 API
function pathOf(u) {
try { var a = new URL(u, location.origin); return (a.pathname + a.search).toLowerCase(); }
catch (e) { return (u || '').toLowerCase(); }
}
window.govPortalNav = {
// URL 로 소속 대메뉴/소메뉴를 찾아 사이드바 강조를 갱신. 일치 없으면 false.
syncByUrl: function (url) {
var target = pathOf(url);
for (var p = 0; p < navData.length; p++) {
var par = navData[p];
var kids = (par.children && par.children.length) ? par.children : (par.href ? [par] : []);
for (var i = 0; i < kids.length; i++) {
if (kids[i].url && pathOf(kids[i].url) === target) {
setActiveTrigger(p);
renderSidebar(p, (kids[i].href || '').toLowerCase().replace(/^\//, ''));
return true;
}
}
}
return false;
}
};
}
var searchInput = document.getElementById('menuSearch') || document.getElementById('menuSearchStrip');