2026-06-08 13:32:53 +09:00
|
|
|
|
<?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>워크스페이스 · 종량제 시스템</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; }
|
2026-06-08 19:04:41 +09:00
|
|
|
|
.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; }
|
2026-06-08 13:32:53 +09:00
|
|
|
|
.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'; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 19:04:41 +09:00
|
|
|
|
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 재설정
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 13:32:53 +09:00
|
|
|
|
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'; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 || '탭');
|
|
|
|
|
|
panels.appendChild(frame);
|
|
|
|
|
|
|
|
|
|
|
|
var btn = document.createElement('div');
|
|
|
|
|
|
btn.className = 'ws-tab';
|
|
|
|
|
|
btn.setAttribute('role', 'tab');
|
|
|
|
|
|
var nameSpan = document.createElement('span');
|
|
|
|
|
|
nameSpan.className = 't-name';
|
|
|
|
|
|
nameSpan.textContent = title || '탭';
|
2026-06-08 19:04:41 +09:00
|
|
|
|
var refresh = document.createElement('span');
|
|
|
|
|
|
refresh.className = 't-refresh';
|
|
|
|
|
|
refresh.textContent = '↻';
|
|
|
|
|
|
refresh.title = '이 탭 새로고침';
|
2026-06-08 13:32:53 +09:00
|
|
|
|
var close = document.createElement('span');
|
|
|
|
|
|
close.className = 't-close';
|
|
|
|
|
|
close.textContent = '×';
|
2026-06-08 19:04:41 +09:00
|
|
|
|
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); }
|
|
|
|
|
|
});
|
2026-06-08 13:32:53 +09:00
|
|
|
|
bar.appendChild(btn);
|
|
|
|
|
|
|
|
|
|
|
|
tabs[id] = { url: url, title: title, frame: frame, btn: btn };
|
|
|
|
|
|
order.push(id);
|
|
|
|
|
|
activate(id);
|
|
|
|
|
|
}
|
|
|
|
|
|
window.wsOpenTab = openTab;
|
|
|
|
|
|
|
|
|
|
|
|
// 좌측 사이드바 소메뉴 클릭 → 탭으로 열기 (전체 페이지 이동 대신)
|
|
|
|
|
|
document.querySelector('.sidebar').addEventListener('click', function (e) {
|
|
|
|
|
|
var a = e.target.closest('a[href]');
|
|
|
|
|
|
if (!a) return;
|
|
|
|
|
|
var href = a.getAttribute('href') || '';
|
2026-06-08 19:04:41 +09:00
|
|
|
|
// 지자체 선택(sb-gray)·모바일앱(sb-teal)은 그대로 이동, 나머지(소메뉴·하단 링크)는 탭으로
|
|
|
|
|
|
if (a.closest('.sb-gray') || a.closest('.sb-teal')) return;
|
2026-06-08 13:32:53 +09:00
|
|
|
|
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());
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-08 19:04:41 +09:00
|
|
|
|
// 브라우저 전체 새로고침/이탈 시 경고 (기본 대시보드 외 탭이 열려 있을 때)
|
|
|
|
|
|
window.addEventListener('beforeunload', function (e) {
|
|
|
|
|
|
if (order.length > 1) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.returnValue = '열어 둔 탭이 모두 닫힙니다. 계속할까요?';
|
|
|
|
|
|
return e.returnValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-08 13:32:53 +09:00
|
|
|
|
// 첫 화면: 대시보드 탭 자동 열기
|
|
|
|
|
|
openTab('<?= base_url('/') ?>', '업무 현황');
|
|
|
|
|
|
})();
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|