feat: 대시보드 바로가기 새 탭 열기 + 탭 새로고침 시각 피드백
- 업무 현황의 "자주 가는 화면"·"최근 방문 메뉴"·메뉴검색 결과 클릭 시 워크스페이스 새 탭으로 열기(부모 wsOpenTab 호출, 밖이면 화면 이동 폴백) - 탭 새로고침(↻): 아이콘 회전 + 화면 잠깐 페이드 후 복구로 새로고침 확인 가능하게 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -129,7 +129,7 @@ $donutCss = $donutStops !== [] ? implode(', ', $donutStops) : '#e5e7eb 0% 100%';
|
|||||||
}
|
}
|
||||||
var html = '<p class="text-[11px] text-white/70 mb-1.5"><i class="fa-regular fa-clock mr-1"></i>최근 방문 메뉴</p>';
|
var html = '<p class="text-[11px] text-white/70 mb-1.5"><i class="fa-regular fa-clock mr-1"></i>최근 방문 메뉴</p>';
|
||||||
r.forEach(function (m) {
|
r.forEach(function (m) {
|
||||||
html += '<a href="' + m.url + '" class="block text-[12px] px-2 py-1.5 rounded bg-white/12 hover:bg-white/25 mb-1 truncate" title="' + esc(m.name) + '">' + esc(m.name) +
|
html += '<a href="' + m.url + '" data-title="' + esc(m.name) + '" class="js-tab-link block text-[12px] px-2 py-1.5 rounded bg-white/12 hover:bg-white/25 mb-1 truncate" title="' + esc(m.name) + '">' + esc(m.name) +
|
||||||
(m.parent ? ' <span class="text-white/55">· ' + esc(m.parent) + '</span>' : '') + '</a>';
|
(m.parent ? ' <span class="text-white/55">· ' + esc(m.parent) + '</span>' : '') + '</a>';
|
||||||
});
|
});
|
||||||
box.innerHTML = html;
|
box.innerHTML = html;
|
||||||
@@ -160,7 +160,28 @@ $donutCss = $donutStops !== [] ? implode(', ', $donutStops) : '#e5e7eb 0% 100%';
|
|||||||
list.classList.remove('hidden');
|
list.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
function go(url) { if (url) window.location.href = url; }
|
// 워크스페이스(부모) 안이면 새 탭으로 열기, 아니면 현재 화면 이동
|
||||||
|
function openInTab(url, name) {
|
||||||
|
try {
|
||||||
|
if (window.parent && window.parent !== window && typeof window.parent.wsOpenTab === 'function') {
|
||||||
|
window.parent.wsOpenTab(url, name || '');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function go(url, name) { if (!openInTab(url, name) && url) window.location.href = url; }
|
||||||
|
|
||||||
|
// 자주 가는 화면·최근 방문 메뉴 링크 → 새 탭으로 열기
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var a = e.target.closest ? e.target.closest('a.js-tab-link') : null;
|
||||||
|
if (!a) return;
|
||||||
|
var url = a.getAttribute('href');
|
||||||
|
if (!url || url.charAt(0) === '#') return;
|
||||||
|
if (openInTab(url, a.getAttribute('data-title') || (a.textContent || '').trim())) {
|
||||||
|
e.preventDefault(); e.stopPropagation();
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
function highlight() {
|
function highlight() {
|
||||||
Array.prototype.forEach.call(list.children, function (li, i) {
|
Array.prototype.forEach.call(list.children, function (li, i) {
|
||||||
@@ -178,20 +199,20 @@ $donutCss = $donutStops !== [] ? implode(', ', $donutStops) : '#e5e7eb 0% 100%';
|
|||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var q = input.value.trim();
|
var q = input.value.trim();
|
||||||
if (active >= 0 && current[active]) { go(current[active].url); return; }
|
if (active >= 0 && current[active]) { go(current[active].url, current[active].name); return; }
|
||||||
// 전체 이름 정확히 일치 우선
|
// 전체 이름 정확히 일치 우선
|
||||||
var exact = FLAT.filter(function (m) { return norm(m.name) === norm(q); });
|
var exact = FLAT.filter(function (m) { return norm(m.name) === norm(q); });
|
||||||
if (exact.length) { go(exact[0].url); return; }
|
if (exact.length) { go(exact[0].url, exact[0].name); return; }
|
||||||
if (current.length) { go(current[0].url); return; }
|
if (current.length) { go(current[0].url, current[0].name); return; }
|
||||||
var any = matches(q);
|
var any = matches(q);
|
||||||
if (any.length) { go(any[0].url); return; }
|
if (any.length) { go(any[0].url, any[0].name); return; }
|
||||||
alert('일치하는 메뉴가 없습니다.');
|
alert('일치하는 메뉴가 없습니다.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
list.addEventListener('mousedown', function (e) {
|
list.addEventListener('mousedown', function (e) {
|
||||||
var li = e.target.closest('li[data-url]');
|
var li = e.target.closest('li[data-url]');
|
||||||
if (li) { e.preventDefault(); go(li.getAttribute('data-url')); }
|
if (li) { e.preventDefault(); var i = +li.getAttribute('data-i'); go(li.getAttribute('data-url'), current[i] && current[i].name); }
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('click', function (e) {
|
document.addEventListener('click', function (e) {
|
||||||
@@ -279,7 +300,7 @@ $donutCss = $donutStops !== [] ? implode(', ', $donutStops) : '#e5e7eb 0% 100%';
|
|||||||
];
|
];
|
||||||
foreach ($links as [$path, $label, $desc, $icon, $c]):
|
foreach ($links as [$path, $label, $desc, $icon, $c]):
|
||||||
?>
|
?>
|
||||||
<a href="<?= base_url($path) ?>" class="group flex items-center gap-3 px-3 py-2 rounded border border-gray-200 hover:border-blue-500 hover:bg-blue-50/40 transition">
|
<a href="<?= base_url($path) ?>" data-title="<?= esc($label, 'attr') ?>" class="js-tab-link group flex items-center gap-3 px-3 py-2 rounded border border-gray-200 hover:border-blue-500 hover:bg-blue-50/40 transition">
|
||||||
<div class="h-8 w-8 rounded-full bg-<?= $c ?>-50 text-<?= $c ?>-600 flex items-center justify-center shrink-0">
|
<div class="h-8 w-8 rounded-full bg-<?= $c ?>-50 text-<?= $c ?>-600 flex items-center justify-center shrink-0">
|
||||||
<i class="fa-solid <?= $icon ?>"></i>
|
<i class="fa-solid <?= $icon ?>"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ if ($effectiveLgIdx) {
|
|||||||
.ws-tab.focused-tab { box-shadow: 0 -2px 0 #243a5e inset; }
|
.ws-tab.focused-tab { box-shadow: 0 -2px 0 #243a5e inset; }
|
||||||
.ws-tab .t-refresh, .ws-tab .t-close { width: 16px; height: 16px; line-height: 14px; text-align: center; border-radius: 50%; color: #999; font-size: 12px; }
|
.ws-tab .t-refresh, .ws-tab .t-close { width: 16px; height: 16px; line-height: 14px; text-align: center; border-radius: 50%; color: #999; font-size: 12px; }
|
||||||
.ws-tab .t-refresh:hover { background: #dbeafe; color: #1d4ed8; }
|
.ws-tab .t-refresh:hover { background: #dbeafe; color: #1d4ed8; }
|
||||||
|
.ws-tab .t-refresh.spin { animation: ws-spin .6s linear; }
|
||||||
|
@keyframes ws-spin { to { transform: rotate(360deg); } }
|
||||||
.ws-tab .t-close:hover { background: #e2e8f0; color: #333; }
|
.ws-tab .t-close:hover { background: #e2e8f0; color: #333; }
|
||||||
/* 분할 레이아웃 컨트롤 */
|
/* 분할 레이아웃 컨트롤 */
|
||||||
.ws-layout { display: flex; align-items: center; gap: 3px; padding: 4px 8px; flex-shrink: 0; border-left: 1px solid var(--border); }
|
.ws-layout { display: flex; align-items: center; gap: 3px; padding: 4px 8px; flex-shrink: 0; border-left: 1px solid var(--border); }
|
||||||
@@ -58,7 +60,7 @@ if ($effectiveLgIdx) {
|
|||||||
.ws-layout button:hover { background: #e2e8f0; color: #334155; }
|
.ws-layout button:hover { background: #e2e8f0; color: #334155; }
|
||||||
.ws-layout button.active { background: #243a5e; color: #fff; border-color: #243a5e; }
|
.ws-layout button.active { background: #243a5e; color: #fff; border-color: #243a5e; }
|
||||||
.ws-panels { flex: 1; position: relative; min-height: 0; background: #cbd5e1; }
|
.ws-panels { flex: 1; position: relative; min-height: 0; background: #cbd5e1; }
|
||||||
.ws-frame { position: absolute; left: 0; top: 0; width: 100%; height: 100%; border: 0; display: none; background: #fff; box-sizing: border-box; }
|
.ws-frame { position: absolute; left: 0; top: 0; width: 100%; height: 100%; border: 0; display: none; background: #fff; box-sizing: border-box; transition: opacity .15s; }
|
||||||
/* 분할 칸 헤더 */
|
/* 분할 칸 헤더 */
|
||||||
.ws-slot-head { position: absolute; display: none; align-items: center; gap: .3rem; height: 28px; padding: 0 .3rem 0 .6rem; background: #eef2f7; border-bottom: 1px solid var(--border); font-size: .72rem; color: #475569; box-sizing: border-box; cursor: pointer; z-index: 3; }
|
.ws-slot-head { position: absolute; display: none; align-items: center; gap: .3rem; height: 28px; padding: 0 .3rem 0 .6rem; background: #eef2f7; border-bottom: 1px solid var(--border); font-size: .72rem; color: #475569; box-sizing: border-box; cursor: pointer; z-index: 3; }
|
||||||
.ws-slot-head.focused { background: #dbeafe; color: var(--navy); box-shadow: inset 0 2px 0 #007bff; font-weight: 700; }
|
.ws-slot-head.focused { background: #dbeafe; color: var(--navy); box-shadow: inset 0 2px 0 #007bff; font-weight: 700; }
|
||||||
@@ -385,6 +387,9 @@ if ($effectiveLgIdx) {
|
|||||||
function reloadTab(id) {
|
function reloadTab(id) {
|
||||||
var t = tabs[id];
|
var t = tabs[id];
|
||||||
if (!t) return;
|
if (!t) return;
|
||||||
|
t.frame.style.opacity = '0.35'; // 새로고침 시각 피드백(load 시 복구)
|
||||||
|
var ic = t.btn && t.btn.querySelector('.t-refresh');
|
||||||
|
if (ic) { ic.classList.remove('spin'); void ic.offsetWidth; ic.classList.add('spin'); setTimeout(function () { ic.classList.remove('spin'); }, 650); }
|
||||||
try { t.frame.contentWindow.location.reload(); }
|
try { t.frame.contentWindow.location.reload(); }
|
||||||
catch (e) { t.frame.src = t.frame.src; }
|
catch (e) { t.frame.src = t.frame.src; }
|
||||||
}
|
}
|
||||||
@@ -413,6 +418,7 @@ if ($effectiveLgIdx) {
|
|||||||
frame.setAttribute('title', title || '탭');
|
frame.setAttribute('title', title || '탭');
|
||||||
// iframe 내부 포커스에서도 단축키 동작 + 칸 클릭 시 그 칸 포커스
|
// iframe 내부 포커스에서도 단축키 동작 + 칸 클릭 시 그 칸 포커스
|
||||||
frame.addEventListener('load', function () {
|
frame.addEventListener('load', function () {
|
||||||
|
frame.style.opacity = ''; // 새로고침 페이드 복구
|
||||||
try {
|
try {
|
||||||
var d = frame.contentDocument;
|
var d = frame.contentDocument;
|
||||||
d.addEventListener('keydown', handleShortcut);
|
d.addEventListener('keydown', handleShortcut);
|
||||||
|
|||||||
Reference in New Issue
Block a user