feat: 대시보드 바로가기 새 탭 열기 + 탭 새로고침 시각 피드백

- 업무 현황의 "자주 가는 화면"·"최근 방문 메뉴"·메뉴검색 결과 클릭 시
  워크스페이스 새 탭으로 열기(부모 wsOpenTab 호출, 밖이면 화면 이동 폴백)
- 탭 새로고침(↻): 아이콘 회전 + 화면 잠깐 페이드 후 복구로 새로고침 확인 가능하게

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
taekyoungc
2026-06-13 23:53:22 +09:00
parent fd3da428ab
commit 287691328e
2 changed files with 36 additions and 9 deletions

View File

@@ -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>';
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>';
});
box.innerHTML = html;
@@ -160,7 +160,28 @@ $donutCss = $donutStops !== [] ? implode(', ', $donutStops) : '#e5e7eb 0% 100%';
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() {
Array.prototype.forEach.call(list.children, function (li, i) {
@@ -178,20 +199,20 @@ $donutCss = $donutStops !== [] ? implode(', ', $donutStops) : '#e5e7eb 0% 100%';
if (e.key === 'Enter') {
e.preventDefault();
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); });
if (exact.length) { go(exact[0].url); return; }
if (current.length) { go(current[0].url); return; }
if (exact.length) { go(exact[0].url, exact[0].name); return; }
if (current.length) { go(current[0].url, current[0].name); return; }
var any = matches(q);
if (any.length) { go(any[0].url); return; }
if (any.length) { go(any[0].url, any[0].name); return; }
alert('일치하는 메뉴가 없습니다.');
}
});
list.addEventListener('mousedown', function (e) {
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) {
@@ -279,7 +300,7 @@ $donutCss = $donutStops !== [] ? implode(', ', $donutStops) : '#e5e7eb 0% 100%';
];
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">
<i class="fa-solid <?= $icon ?>"></i>
</div>

View File

@@ -51,6 +51,8 @@ if ($effectiveLgIdx) {
.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: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-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.active { background: #243a5e; color: #fff; border-color: #243a5e; }
.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.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) {
var t = tabs[id];
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(); }
catch (e) { t.frame.src = t.frame.src; }
}
@@ -413,6 +418,7 @@ if ($effectiveLgIdx) {
frame.setAttribute('title', title || '탭');
// iframe 내부 포커스에서도 단축키 동작 + 칸 클릭 시 그 칸 포커스
frame.addEventListener('load', function () {
frame.style.opacity = ''; // 새로고침 페이드 복구
try {
var d = frame.contentDocument;
d.addEventListener('keydown', handleShortcut);