화면별 매뉴얼·이 화면 설명 버튼·탭 새로고침/경고·최근방문 기록 보강.
- 매뉴얼: 화면(소메뉴)별 용어·버튼·필드 설명으로 확장 + 기본정보 페이지 신규, 개요에 용어 사전 추가 (종량제 지식 없는 사용자 대상) - "이 화면 설명" 버튼: 화면 경로→매뉴얼 매핑(Config\Manual::screenHelp, manual_help_url_for_path). 워크스페이스 탭은 새 탭으로, 직접 페이지는 새 창으로 - 워크스페이스: 개별 탭 새로고침(↻) 버튼, 탭 2개 이상일 때만 새로고침 경고, 사이드바 하단 링크(매뉴얼 등)도 탭으로 열기 - 임베드: 탭 내 링크/폼 embed 유지(중첩 헤더 방지), 매뉴얼 리다이렉트 embed 유지 - 사이드바 하단: 종합그래프 → 사용자 매뉴얼 링크 - 최근 방문 메뉴: embed 페이지에도 방문 기록, 대시보드는 storage 이벤트로 실시간 갱신 - E2E qa_sweep 추가(주요 화면 콘솔/오버레이/매뉴얼/도움말 매핑 점검) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -202,6 +202,8 @@ $donutCss = $donutStops !== [] ? implode(', ', $donutStops) : '#e5e7eb 0% 100%';
|
||||
// 뒤로가기(bfcache 복원)·탭 복귀 시에도 최근 목록 갱신
|
||||
window.addEventListener('pageshow', renderRecent);
|
||||
document.addEventListener('visibilitychange', function () { if (!document.hidden) renderRecent(); });
|
||||
// 다른 탭(iframe)에서 메뉴를 방문해 기록이 바뀌면 즉시 반영
|
||||
window.addEventListener('storage', function (e) { if (e.key === 'jrj_recent_menus') renderRecent(); });
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ declare(strict_types=1);
|
||||
* @var bool $bare true면 본문을 카드 래퍼 없이 그대로 출력(대시보드용)
|
||||
*/
|
||||
$bare = ! empty($bare);
|
||||
helper('admin');
|
||||
$helpUrl = function_exists('manual_help_url_for_path') ? manual_help_url_for_path() : '';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
@@ -50,8 +52,15 @@ tailwind.config = {
|
||||
</head>
|
||||
<body>
|
||||
<div style="padding: 0.875rem 1rem 1.25rem;">
|
||||
<?php if (! empty($title)): ?>
|
||||
<h1 class="embed-titlebar"><i class="fa-solid fa-folder-open" style="color:#007bff;"></i><?= esc($title) ?></h1>
|
||||
<?php if (! empty($title) || $helpUrl !== ''): ?>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:.5rem;margin:0 0 .75rem;">
|
||||
<h1 class="embed-titlebar" style="margin:0;"><?php if (! empty($title)): ?><i class="fa-solid fa-folder-open" style="color:#007bff;"></i><?= esc($title) ?><?php endif; ?></h1>
|
||||
<?php if ($helpUrl !== ''): ?>
|
||||
<a href="<?= esc($helpUrl, 'attr') ?>" class="embed-help no-print" style="display:inline-flex;align-items:center;gap:.3rem;padding:.3rem .6rem;border-radius:6px;background:#eef2f7;border:1px solid #dde4ec;color:#1a2b4b;text-decoration:none;font-size:.75rem;font-weight:600;white-space:nowrap;">
|
||||
<i class="fa-regular fa-circle-question"></i> 이 화면 설명
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="embed-flash ok"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||
@@ -73,6 +82,61 @@ tailwind.config = {
|
||||
if (window.top !== window.self && /\/login(\/|$)/.test(location.pathname)) {
|
||||
try { window.top.location.href = location.href; } catch (e) {}
|
||||
}
|
||||
|
||||
// 방문한 업무 메뉴 경로 기록 (대시보드 "최근 방문 메뉴"용). localStorage 는 동일 출처 탭과 공유된다.
|
||||
try {
|
||||
var rp = (location.pathname || '').replace(/\/+$/, '') || '/';
|
||||
if (rp !== '/' && !/\/login|\/logout|\/register|\/manual/.test(rp)) {
|
||||
var RK = 'jrj_recent_menus';
|
||||
var ra = JSON.parse(localStorage.getItem(RK) || '[]');
|
||||
if (!Array.isArray(ra)) ra = [];
|
||||
ra = ra.filter(function (x) { return x && x.p && x.p !== rp; });
|
||||
ra.unshift({ p: rp, t: Date.now() });
|
||||
localStorage.setItem(RK, JSON.stringify(ra.slice(0, 12)));
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 탭(iframe) 안에서의 링크·폼 이동은 항상 embed 유지 → 중첩 헤더/사이드바 방지
|
||||
function withEmbed(href) {
|
||||
try {
|
||||
var u = new URL(href, location.href);
|
||||
if (u.origin !== location.origin) return null;
|
||||
if (u.searchParams.get('embed') === '1') return null;
|
||||
u.searchParams.set('embed', '1');
|
||||
return u.href;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
// "이 화면 설명" → 워크스페이스 새 탭으로 매뉴얼 열기(없으면 새 창)
|
||||
document.addEventListener('click', function (e) {
|
||||
var h = e.target.closest ? e.target.closest('a.embed-help') : null;
|
||||
if (!h) return;
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
var url = h.getAttribute('href');
|
||||
try {
|
||||
if (window.parent && window.parent !== window && typeof window.parent.wsOpenTab === 'function') {
|
||||
window.parent.wsOpenTab(url, '도움말'); return;
|
||||
}
|
||||
} catch (err) {}
|
||||
window.open(url, '_blank');
|
||||
}, true);
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var a = e.target.closest ? e.target.closest('a[href]') : null;
|
||||
if (!a || a.classList.contains('embed-help')) return;
|
||||
var t = (a.getAttribute('target') || '').toLowerCase();
|
||||
if (t && t !== '_self') return;
|
||||
if (a.hasAttribute('download')) return;
|
||||
var href = a.getAttribute('href') || '';
|
||||
if (!href || href.charAt(0) === '#' || /^(javascript:|mailto:|tel:|data:)/i.test(href)) return;
|
||||
var ne = withEmbed(href);
|
||||
if (ne) a.setAttribute('href', ne);
|
||||
}, true);
|
||||
document.addEventListener('submit', function (e) {
|
||||
var f = e.target;
|
||||
if (!f || f.tagName !== 'FORM') return;
|
||||
var ne = withEmbed(f.getAttribute('action') || location.href);
|
||||
if (ne) f.setAttribute('action', ne);
|
||||
}, true);
|
||||
// 표 '번호' 컬럼 역순 채번 (사이트 레이아웃과 동일)
|
||||
var run = function () {
|
||||
document.querySelectorAll('table').forEach(function (table) {
|
||||
|
||||
@@ -35,6 +35,7 @@ if ($effectiveLgIdx) {
|
||||
$lgRow = model(\App\Models\LocalGovernmentModel::class)->find($effectiveLgIdx);
|
||||
$lgLabel = $lgRow ? (string) $lgRow->lg_name : '';
|
||||
}
|
||||
$helpUrl = function_exists('manual_help_url_for_path') ? manual_help_url_for_path() : '';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko" class="gov-portal-html">
|
||||
@@ -116,7 +117,14 @@ tailwind.config = {
|
||||
|
||||
<main class="main work-main main-content-area">
|
||||
<?php if (! $bare && ! empty($title)): ?>
|
||||
<h1 class="work-titlebar"><i class="fa-solid fa-folder-open tb-ico"></i><?= esc($title) ?></h1>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:.5rem;">
|
||||
<h1 class="work-titlebar" style="margin-bottom:0;"><i class="fa-solid fa-folder-open tb-ico"></i><?= esc($title) ?></h1>
|
||||
<?php if ($helpUrl !== ''): ?>
|
||||
<a href="<?= esc($helpUrl, 'attr') ?>" target="_blank" rel="noopener" class="no-print" style="display:inline-flex;align-items:center;gap:.3rem;padding:.3rem .6rem;border-radius:6px;background:#fff;border:1px solid var(--border);color:#1a2b4b;text-decoration:none;font-size:.75rem;font-weight:600;white-space:nowrap;">
|
||||
<i class="fa-regular fa-circle-question"></i> 이 화면 설명
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="work-flash ok"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||
|
||||
@@ -47,7 +47,8 @@ if ($effectiveLgIdx) {
|
||||
.ws-tab { display: inline-flex; align-items: center; gap: .4rem; max-width: 200px; padding: .35rem .6rem; background: #f5f7fa; border: 1px solid var(--border); border-bottom: none; border-radius: 7px 7px 0 0; font-size: .78rem; color: #555; cursor: pointer; white-space: nowrap; }
|
||||
.ws-tab .t-name { overflow: hidden; text-overflow: ellipsis; max-width: 150px; }
|
||||
.ws-tab.active { background: #fff; color: var(--navy); font-weight: 700; box-shadow: 0 -2px 0 #007bff inset; }
|
||||
.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-close:hover { background: #e2e8f0; color: #333; }
|
||||
.ws-panels { flex: 1; position: relative; min-height: 0; background: #f0f4f8; }
|
||||
.ws-frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: none; background: #f0f4f8; }
|
||||
@@ -114,6 +115,13 @@ if ($effectiveLgIdx) {
|
||||
if (empty) empty.style.display = 'none';
|
||||
}
|
||||
|
||||
function reloadTab(id) {
|
||||
var t = tabs[id];
|
||||
if (!t) return;
|
||||
try { t.frame.contentWindow.location.reload(); }
|
||||
catch (e) { t.frame.src = t.frame.src; } // 교차출처 등 예외 시 src 재설정
|
||||
}
|
||||
|
||||
function closeTab(id) {
|
||||
if (!tabs[id]) return;
|
||||
tabs[id].frame.remove();
|
||||
@@ -142,11 +150,20 @@ if ($effectiveLgIdx) {
|
||||
var nameSpan = document.createElement('span');
|
||||
nameSpan.className = 't-name';
|
||||
nameSpan.textContent = title || '탭';
|
||||
var refresh = document.createElement('span');
|
||||
refresh.className = 't-refresh';
|
||||
refresh.textContent = '↻';
|
||||
refresh.title = '이 탭 새로고침';
|
||||
var close = document.createElement('span');
|
||||
close.className = 't-close';
|
||||
close.textContent = '×';
|
||||
btn.appendChild(nameSpan); btn.appendChild(close);
|
||||
btn.addEventListener('click', function (e) { if (e.target === close) { closeTab(id); } else { activate(id); } });
|
||||
close.title = '탭 닫기';
|
||||
btn.appendChild(nameSpan); btn.appendChild(refresh); btn.appendChild(close);
|
||||
btn.addEventListener('click', function (e) {
|
||||
if (e.target === close) { closeTab(id); }
|
||||
else if (e.target === refresh) { activate(id); reloadTab(id); }
|
||||
else { activate(id); }
|
||||
});
|
||||
bar.appendChild(btn);
|
||||
|
||||
tabs[id] = { url: url, title: title, frame: frame, btn: btn };
|
||||
@@ -160,8 +177,8 @@ if ($effectiveLgIdx) {
|
||||
var a = e.target.closest('a[href]');
|
||||
if (!a) return;
|
||||
var href = a.getAttribute('href') || '';
|
||||
// 외부/특수 링크(로그아웃, 지자체선택 등 사이드바 하단 블록)는 그대로 이동
|
||||
if (a.closest('.sidebar-blocks')) return;
|
||||
// 지자체 선택(sb-gray)·모바일앱(sb-teal)은 그대로 이동, 나머지(소메뉴·하단 링크)는 탭으로
|
||||
if (a.closest('.sb-gray') || a.closest('.sb-teal')) return;
|
||||
if (/\/logout|\/login/.test(href) || href.charAt(0) === '#' || href === '') return;
|
||||
e.preventDefault();
|
||||
openTab(href, (a.textContent || '').trim());
|
||||
@@ -177,6 +194,15 @@ if ($effectiveLgIdx) {
|
||||
});
|
||||
});
|
||||
|
||||
// 브라우저 전체 새로고침/이탈 시 경고 (기본 대시보드 외 탭이 열려 있을 때)
|
||||
window.addEventListener('beforeunload', function (e) {
|
||||
if (order.length > 1) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '열어 둔 탭이 모두 닫힙니다. 계속할까요?';
|
||||
return e.returnValue;
|
||||
}
|
||||
});
|
||||
|
||||
// 첫 화면: 대시보드 탭 자동 열기
|
||||
openTab('<?= base_url('/') ?>', '업무 현황');
|
||||
})();
|
||||
|
||||
@@ -52,7 +52,7 @@ $activeChildHref = strtolower(ltrim((string) ($govActiveChildHref ?? ''), '/'));
|
||||
</div>
|
||||
<div class="sb-links">
|
||||
<a href="<?= base_url('bag/help') ?>">나의 할일</a>
|
||||
<a href="<?= base_url('dashboard') ?>">종합·그래프</a>
|
||||
<a href="<?= base_url('bag/manual') ?>">사용자 매뉴얼</a>
|
||||
<a href="<?= base_url('bag/help') ?>">FAQ</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user