Files
jongryangje/app/Views/bag/layout/workspace.php

263 lines
12 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
/**
* 워크스페이스() 크롬처럼 메뉴를 (iframe)으로 열어두고 작업 상태 유지.
* 헤더 + 대메뉴(클릭) + 좌측 사이드바(소메뉴) + 탭바 + iframe 패널.
* 탭은 세션( 셸이 열려 있는 동안) 유지된다.
*/
helper('admin');
$gov = gov_portal_nav_context(false);
$navPartial = [
'govNavItems' => $gov['navItems'],
'govNavJson' => $gov['navJson'],
'govActiveParentIdx' => $gov['activeParentIdx'],
'govCurrentPath' => gov_portal_nav_match_path($gov['currentPath']),
'govDashboardAliases' => $gov['dashboardAliases'],
'govActiveChildHref' => '',
];
$mbLevel = (int) session()->get('mb_level');
$mbName = (string) (session()->get('mb_name') ?? '담당자');
$levelName = config(\Config\Roles::class)->getLevelName($mbLevel);
$isAdmin = ($mbLevel === \Config\Roles::LEVEL_SUPER_ADMIN || $mbLevel === \Config\Roles::LEVEL_LOCAL_ADMIN);
$effectiveLgIdx = admin_effective_lg_idx();
$lgLabel = '';
if ($effectiveLgIdx) {
$lgRow = model(\App\Models\LocalGovernmentModel::class)->find($effectiveLgIdx);
$lgLabel = $lgRow ? (string) $lgRow->lg_name : '';
}
?>
<!DOCTYPE html>
<html lang="ko" class="gov-portal-html">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>워크스페이스 · GBLS</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css"/>
<style>
<?php include __DIR__ . '/../../home/_dashboard_gov_portal_brand_css.php'; ?>
<?php include __DIR__ . '/../../home/_dashboard_gov_portal_topnav_css.php'; ?>
<?php include __DIR__ . '/../../home/_dashboard_gov_portal_chrome_css.php'; ?>
html, body { height: 100%; }
body.gov-portal-shell { height: 100vh; overflow: hidden; }
.ws-main { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.ws-tabbar { display: flex; align-items: stretch; gap: 2px; background: #e9eef5; border-bottom: 1px solid var(--border); padding: 4px 6px 0; overflow-x: auto; min-height: 36px; }
.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-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; }
.ws-frame.active { display: block; }
.ws-empty { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #94a3b8; font-size: .9rem; gap: .5rem; }
</style>
</head>
<body class="gov-portal-shell">
<header class="portal-header">
<div class="portal-header-inner">
<?= view('home/_dashboard_gov_portal_brand', ['brandHref' => base_url('workspace')]) ?>
<?= view('home/_dashboard_gov_portal_topnav_click', $navPartial) ?>
<div class="portal-header-utils" style="display:flex;align-items:center;gap:.5rem;">
<span class="user-line">
<?php if ($lgLabel !== ''): ?><strong><?= esc($lgLabel) ?></strong> · <?php endif; ?>
<?= esc($levelName) ?> · <?= esc($mbName) ?>님
</span>
<?php if ($isAdmin): ?>
<a href="<?= base_url('admin') ?>" title="관리자 페이지" style="display:inline-flex;align-items:center;gap:.3rem;padding:.25rem .6rem;border-radius:6px;background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.3);color:#fff;text-decoration:none;font-size:.75rem;font-weight:600;"><i class="fa-solid fa-gear"></i> 관리자</a>
<?php endif; ?>
<a href="<?= base_url('logout') ?>" title="로그아웃" style="display:inline-flex;align-items:center;gap:.3rem;padding:.25rem .6rem;border-radius:6px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.22);color:#fff;text-decoration:none;font-size:.75rem;font-weight:600;"><i class="fa-solid fa-right-from-bracket"></i> 로그아웃</a>
</div>
</div>
</header>
<div class="layout">
<?= view('home/_dashboard_gov_portal_sidebar', $navPartial) ?>
<main class="ws-main">
<div class="ws-tabbar" id="wsTabBar" role="tablist"></div>
<div class="ws-panels" id="wsPanels">
<div class="ws-empty" id="wsEmpty">
<i class="fa-regular fa-window-restore" style="font-size:1.6rem;opacity:.5;"></i>
<div>왼쪽 메뉴를 클릭하면 탭으로 열립니다. 여러 화면을 열어두고 전환해도 작업 내용이 유지됩니다.</div>
</div>
</div>
</main>
</div>
<?= view('home/_dashboard_gov_portal_nav_script_base', $navPartial) ?>
<script>
(function () {
var bar = document.getElementById('wsTabBar');
var panels = document.getElementById('wsPanels');
var empty = document.getElementById('wsEmpty');
var tabs = {}; // id -> {url,title,frame,btn}
var order = [];
var activeId = null;
var MAX = 12;
function norm(u) { try { var a = new URL(u, location.origin); return a.pathname + a.search; } catch (e) { return u; } }
function withEmbed(u) {
try { var a = new URL(u, location.origin); a.searchParams.set('embed', '1'); return a.pathname + a.search; }
catch (e) { return u + (u.indexOf('?') >= 0 ? '&' : '?') + 'embed=1'; }
}
var STORE_KEY = 'jrj_ws_tabs';
var WS_OWNER = '<?= (string) (session()->get('mb_idx') ?? '') ?>'; // 탭 저장 소유자(로그인 사용자) 식별
function persist() {
try {
var data = {
owner: WS_OWNER,
tabs: order.map(function (id) { return { url: tabs[id].url, title: tabs[id].title }; }),
active: activeId
};
sessionStorage.setItem(STORE_KEY, JSON.stringify(data));
} catch (e) {}
}
function activate(id) {
if (!tabs[id]) return;
activeId = id;
Object.keys(tabs).forEach(function (k) {
tabs[k].frame.classList.toggle('active', k === id);
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();
}
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();
tabs[id].btn.remove();
delete tabs[id];
order = order.filter(function (x) { return x !== id; });
if (activeId === id) {
var next = order[order.length - 1];
if (next) activate(next); else { activeId = null; if (empty) empty.style.display = 'flex'; }
}
persist();
}
function openTab(url, title) {
var id = norm(url);
if (tabs[id]) { activate(id); return; }
if (order.length >= MAX) { closeTab(order[0]); } // 오래된 탭 정리
var frame = document.createElement('iframe');
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 || '탭';
var refresh = document.createElement('span');
refresh.className = 't-refresh';
refresh.textContent = '↻';
refresh.title = '이 탭 새로고침';
var close = document.createElement('span');
close.className = 't-close';
close.textContent = '×';
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); }
});
// 가운데(휠) 클릭으로 탭 닫기 — 브라우저 탭과 동일한 동작
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 };
order.push(id);
activate(id);
}
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]');
if (!a) return;
var href = a.getAttribute('href') || '';
// 지자체 선택(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());
});
// 상단 대메뉴의 직접 링크(자식 없는 대메뉴)도 탭으로
document.querySelectorAll('.portal-nav-link[href]').forEach(function (lnk) {
lnk.addEventListener('click', function (e) {
var href = lnk.getAttribute('href') || '';
if (!href || href.charAt(0) === '#') return;
e.preventDefault();
openTab(href, (lnk.textContent || '').trim());
});
});
// 첫 화면: 세션에 저장된 탭이 있으면 복원(관리자 페이지 등 전체 이동 후 복귀·새로고침 대응),
// 없으면 대시보드 탭 자동 열기
(function restore() {
var saved = null;
try { saved = JSON.parse(sessionStorage.getItem(STORE_KEY) || 'null'); } catch (e) {}
// 저장된 탭의 소유자가 현재 로그인 사용자와 다르면(같은 브라우저 탭에서 계정 전환 등) 복원하지 않고 초기화
if (saved && saved.owner !== WS_OWNER) {
try { sessionStorage.removeItem(STORE_KEY); } catch (e) {}
saved = null;
}
if (saved && saved.tabs && saved.tabs.length) {
saved.tabs.forEach(function (t) { if (t && t.url) openTab(t.url, t.title); });
if (saved.active && tabs[saved.active]) activate(saved.active);
return;
}
openTab('<?= base_url('/') ?>', '업무 현황');
})();
})();
</script>
</body>
</html>