From 287691328ecc34361be3bda8759889e2145e1e41 Mon Sep 17 00:00:00 2001 From: taekyoungc Date: Sat, 13 Jun 2026 23:53:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EB=B0=94=EB=A1=9C=EA=B0=80=EA=B8=B0=20=EC=83=88=20=ED=83=AD=20?= =?UTF-8?q?=EC=97=B4=EA=B8=B0=20+=20=ED=83=AD=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8=20=EC=8B=9C=EA=B0=81=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 업무 현황의 "자주 가는 화면"·"최근 방문 메뉴"·메뉴검색 결과 클릭 시 워크스페이스 새 탭으로 열기(부모 wsOpenTab 호출, 밖이면 화면 이동 폴백) - 탭 새로고침(↻): 아이콘 회전 + 화면 잠깐 페이드 후 복구로 새로고침 확인 가능하게 Co-Authored-By: Claude Opus 4.8 --- app/Views/bag/dashboard_portal.php | 37 +++++++++++++++++++++++------- app/Views/bag/layout/workspace.php | 8 ++++++- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/app/Views/bag/dashboard_portal.php b/app/Views/bag/dashboard_portal.php index 7da50e6..25ae5bc 100644 --- a/app/Views/bag/dashboard_portal.php +++ b/app/Views/bag/dashboard_portal.php @@ -129,7 +129,7 @@ $donutCss = $donutStops !== [] ? implode(', ', $donutStops) : '#e5e7eb 0% 100%'; } var html = '

최근 방문 메뉴

'; r.forEach(function (m) { - html += '' + esc(m.name) + + html += '' + esc(m.name) + (m.parent ? ' · ' + esc(m.parent) + '' : '') + ''; }); 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]): ?> - +
diff --git a/app/Views/bag/layout/workspace.php b/app/Views/bag/layout/workspace.php index 30ae5a1..70cc829 100644 --- a/app/Views/bag/layout/workspace.php +++ b/app/Views/bag/layout/workspace.php @@ -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);