- /workspace: 헤더+사이드바+탭바+iframe 패널. 메뉴 클릭=탭 열기, 전환해도 폼·스크롤·조회결과 등 작업 상태 유지(세션 동안) - 로그인 후 / = 워크스페이스(첫 탭=대시보드). iframe 내부는 임베드 렌더 - 임베드 레이아웃(bag/layout/embed): 헤더·사이드바 없이 본문만 - 임베드 판정: ?embed=1 또는 Sec-Fetch-Dest=iframe (iframe 내 링크·폼· 리다이렉트까지 중첩 크롬 없이 처리) - iframe 안 세션만료 시 상위 창을 로그인으로 전환(auth/_shell) - 포털 헤더에 워크스페이스 진입 링크, E2E(workspace.spec) 추가 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
186 lines
8.8 KiB
PHP
186 lines
8.8 KiB
PHP
<?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; }
|
||
.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-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';
|
||
}
|
||
|
||
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 || '탭';
|
||
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); } });
|
||
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') || '';
|
||
// 외부/특수 링크(로그아웃, 지자체선택 등 사이드바 하단 블록)는 그대로 이동
|
||
if (a.closest('.sidebar-blocks')) 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());
|
||
});
|
||
});
|
||
|
||
// 첫 화면: 대시보드 탭 자동 열기
|
||
openTab('<?= base_url('/') ?>', '업무 현황');
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|