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"/>
|
2026-06-09 14:43:24 +09:00
|
|
|
|
<title>워크스페이스 · GBLS</title>
|
2026-06-08 13:32:53 +09:00
|
|
|
|
<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; }
|
2026-06-10 10:19:37 +09:00
|
|
|
|
.ws-topbar { display: flex; align-items: stretch; background: #e9eef5; border-bottom: 1px solid var(--border); }
|
|
|
|
|
|
.ws-tabbar { display: flex; align-items: stretch; gap: 2px; padding: 4px 6px 0; overflow-x: auto; min-height: 36px; flex: 1; min-width: 0; }
|
2026-06-08 13:32:53 +09:00
|
|
|
|
.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-10 10:19:37 +09:00
|
|
|
|
.ws-tab.focused-tab { box-shadow: 0 -2px 0 #243a5e 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; }
|
2026-06-10 10:19:37 +09:00
|
|
|
|
/* 분할 레이아웃 컨트롤 */
|
|
|
|
|
|
.ws-layout { display: flex; align-items: center; gap: 3px; padding: 4px 8px; flex-shrink: 0; border-left: 1px solid var(--border); }
|
|
|
|
|
|
.ws-layout button { width: 30px; height: 26px; border: 1px solid var(--border); background: #f5f7fa; border-radius: 6px; color: #64748b; cursor: pointer; font-size: 12px; display: inline-flex; align-items: center; justify-content: center; }
|
|
|
|
|
|
.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-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; }
|
|
|
|
|
|
.ws-slot-head .sh-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
|
|
.ws-slot-head .sh-btn { width: 18px; height: 18px; line-height: 16px; text-align: center; border-radius: 50%; color: #94a3b8; flex-shrink: 0; font-size: 12px; }
|
|
|
|
|
|
.ws-slot-head .sh-btn:hover { background: #fff; color: #334155; }
|
|
|
|
|
|
.ws-slot-empty { position: absolute; display: none; align-items: center; justify-content: center; background: #f8fafc; color: #94a3b8; font-size: .8rem; box-sizing: border-box; cursor: pointer; z-index: 1; text-align: center; padding: 1rem; }
|
|
|
|
|
|
/* 분할 구분선 드래그 핸들 */
|
|
|
|
|
|
.ws-gutter { position: absolute; display: none; z-index: 6; background: transparent; }
|
|
|
|
|
|
.ws-gutter.v { width: 9px; height: 100%; top: 0; cursor: col-resize; }
|
|
|
|
|
|
.ws-gutter.h { width: 100%; height: 9px; left: 0; cursor: row-resize; }
|
|
|
|
|
|
.ws-gutter:hover { background: rgba(37, 99, 235, .25); }
|
|
|
|
|
|
/* 드래그 중 iframe 위에서도 마우스 이벤트를 받기 위한 오버레이 */
|
|
|
|
|
|
.ws-drag-overlay { position: absolute; inset: 0; z-index: 50; display: none; }
|
|
|
|
|
|
.ws-drag-overlay.v { cursor: col-resize; }
|
|
|
|
|
|
.ws-drag-overlay.h { cursor: row-resize; }
|
|
|
|
|
|
.ws-empty { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #94a3b8; font-size: .9rem; gap: .5rem; z-index: 2; background: #f0f4f8; }
|
2026-06-08 13:32:53 +09:00
|
|
|
|
</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">
|
2026-06-10 10:19:37 +09:00
|
|
|
|
<div class="ws-topbar">
|
|
|
|
|
|
<div class="ws-tabbar" id="wsTabBar" role="tablist"></div>
|
|
|
|
|
|
<div class="ws-layout" id="wsLayout">
|
|
|
|
|
|
<button type="button" data-mode="single" title="1분할 (한 화면)"><i class="fa-regular fa-square"></i></button>
|
|
|
|
|
|
<button type="button" data-mode="lr" title="2분할 (좌우)"><i class="fa-solid fa-table-columns"></i></button>
|
|
|
|
|
|
<button type="button" data-mode="tb" title="2분할 (상하)"><i class="fa-solid fa-table-columns fa-rotate-90"></i></button>
|
|
|
|
|
|
<button type="button" data-mode="quad" title="4분할"><i class="fa-solid fa-table-cells-large"></i></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-06-08 13:32:53 +09:00
|
|
|
|
<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 MAX = 12;
|
|
|
|
|
|
|
2026-06-10 10:19:37 +09:00
|
|
|
|
// 분할 레이아웃 상태
|
|
|
|
|
|
var layout = 'single'; // single | lr | tb | quad
|
|
|
|
|
|
var slots = [null]; // 칸별로 배치된 tab id (길이 = 칸 수)
|
|
|
|
|
|
var focused = 0; // 포커스된 칸 인덱스
|
|
|
|
|
|
|
2026-06-08 13:32:53 +09:00
|
|
|
|
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'; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-10 10:19:37 +09:00
|
|
|
|
// 분할 비율 (좌 컬럼 폭 / 위 행 높이), 드래그로 조절
|
|
|
|
|
|
var vRatio = 0.5, hRatio = 0.5;
|
|
|
|
|
|
var RATIO_MIN = 0.12, RATIO_MAX = 0.88;
|
|
|
|
|
|
function pct(r) { return (r * 100).toFixed(3) + '%'; }
|
|
|
|
|
|
|
|
|
|
|
|
// 레이아웃별 칸 사각형 (2px 간격, 비율 반영) — left/top/width/height CSS 문자열
|
|
|
|
|
|
function rectsFor(mode) {
|
|
|
|
|
|
var Lw = 'calc(' + pct(vRatio) + ' - 1px)'; // 좌 컬럼 폭
|
|
|
|
|
|
var Rl = 'calc(' + pct(vRatio) + ' + 1px)'; // 우 컬럼 시작
|
|
|
|
|
|
var Rw = 'calc(' + pct(1 - vRatio) + ' - 1px)'; // 우 컬럼 폭
|
|
|
|
|
|
var Th = 'calc(' + pct(hRatio) + ' - 1px)'; // 위 행 높이
|
|
|
|
|
|
var Bt = 'calc(' + pct(hRatio) + ' + 1px)'; // 아래 행 시작
|
|
|
|
|
|
var Bh = 'calc(' + pct(1 - hRatio) + ' - 1px)'; // 아래 행 높이
|
|
|
|
|
|
if (mode === 'lr') return [
|
|
|
|
|
|
{ l: '0', t: '0', w: Lw, h: '100%' },
|
|
|
|
|
|
{ l: Rl, t: '0', w: Rw, h: '100%' }
|
|
|
|
|
|
];
|
|
|
|
|
|
if (mode === 'tb') return [
|
|
|
|
|
|
{ l: '0', t: '0', w: '100%', h: Th },
|
|
|
|
|
|
{ l: '0', t: Bt, w: '100%', h: Bh }
|
|
|
|
|
|
];
|
|
|
|
|
|
if (mode === 'quad') return [
|
|
|
|
|
|
{ l: '0', t: '0', w: Lw, h: Th },
|
|
|
|
|
|
{ l: Rl, t: '0', w: Rw, h: Th },
|
|
|
|
|
|
{ l: '0', t: Bt, w: Lw, h: Bh },
|
|
|
|
|
|
{ l: Rl, t: Bt, w: Rw, h: Bh }
|
|
|
|
|
|
];
|
|
|
|
|
|
return [{ l: '0', t: '0', w: '100%', h: '100%' }]; // single
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 19:52:53 +09:00
|
|
|
|
var STORE_KEY = 'jrj_ws_tabs';
|
2026-06-09 14:43:24 +09:00
|
|
|
|
var WS_OWNER = '<?= (string) (session()->get('mb_idx') ?? '') ?>'; // 탭 저장 소유자(로그인 사용자) 식별
|
2026-06-08 19:52:53 +09:00
|
|
|
|
function persist() {
|
|
|
|
|
|
try {
|
2026-06-10 10:19:37 +09:00
|
|
|
|
sessionStorage.setItem(STORE_KEY, JSON.stringify({
|
2026-06-09 14:43:24 +09:00
|
|
|
|
owner: WS_OWNER,
|
2026-06-08 19:52:53 +09:00
|
|
|
|
tabs: order.map(function (id) { return { url: tabs[id].url, title: tabs[id].title }; }),
|
2026-06-10 10:19:37 +09:00
|
|
|
|
layout: layout,
|
|
|
|
|
|
focused: focused,
|
|
|
|
|
|
vRatio: vRatio,
|
|
|
|
|
|
hRatio: hRatio,
|
|
|
|
|
|
slots: slots.map(function (id) { return (id && tabs[id]) ? tabs[id].url : null; })
|
|
|
|
|
|
}));
|
2026-06-08 19:52:53 +09:00
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-10 10:19:37 +09:00
|
|
|
|
// 분할 칸 헤더·빈칸 안내 요소 4개 미리 생성
|
|
|
|
|
|
var slotHeads = [], slotEmpties = [];
|
|
|
|
|
|
for (var si = 0; si < 4; si++) {
|
|
|
|
|
|
(function (idx) {
|
|
|
|
|
|
var h = document.createElement('div');
|
|
|
|
|
|
h.className = 'ws-slot-head';
|
|
|
|
|
|
var nm = document.createElement('span'); nm.className = 'sh-name';
|
|
|
|
|
|
var rl = document.createElement('span'); rl.className = 'sh-btn sh-reload'; rl.textContent = '↻'; rl.title = '이 칸 새로고침';
|
|
|
|
|
|
var cl = document.createElement('span'); cl.className = 'sh-btn sh-clear'; cl.textContent = '×'; cl.title = '이 칸 비우기';
|
|
|
|
|
|
h.appendChild(nm); h.appendChild(rl); h.appendChild(cl);
|
|
|
|
|
|
h.addEventListener('click', function (e) {
|
|
|
|
|
|
if (e.target === rl) { var sid = slots[idx]; if (sid) reloadTab(sid); return; }
|
|
|
|
|
|
if (e.target === cl) { slots[idx] = null; render(); return; }
|
|
|
|
|
|
focusSlot(idx);
|
|
|
|
|
|
});
|
|
|
|
|
|
panels.appendChild(h);
|
|
|
|
|
|
var emp = document.createElement('div');
|
|
|
|
|
|
emp.className = 'ws-slot-empty';
|
|
|
|
|
|
emp.textContent = '+ 위 탭에서 이 칸에 표시할 화면을 선택하세요';
|
|
|
|
|
|
emp.addEventListener('click', function () { focusSlot(idx); });
|
|
|
|
|
|
panels.appendChild(emp);
|
|
|
|
|
|
slotHeads.push(h); slotEmpties.push(emp);
|
|
|
|
|
|
})(si);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 분할 구분선(드래그 핸들) + 드래그용 투명 오버레이
|
|
|
|
|
|
var vGutter = document.createElement('div'); vGutter.className = 'ws-gutter v';
|
|
|
|
|
|
var hGutter = document.createElement('div'); hGutter.className = 'ws-gutter h';
|
|
|
|
|
|
var dragOverlay = document.createElement('div'); dragOverlay.className = 'ws-drag-overlay';
|
|
|
|
|
|
panels.appendChild(vGutter); panels.appendChild(hGutter); panels.appendChild(dragOverlay);
|
|
|
|
|
|
|
|
|
|
|
|
function clampRatio(r) { return Math.min(RATIO_MAX, Math.max(RATIO_MIN, r)); }
|
|
|
|
|
|
function startDrag(axis) {
|
|
|
|
|
|
dragOverlay.className = 'ws-drag-overlay ' + axis;
|
|
|
|
|
|
dragOverlay.style.display = 'block';
|
|
|
|
|
|
function move(ev) {
|
|
|
|
|
|
var rect = panels.getBoundingClientRect();
|
|
|
|
|
|
if (axis === 'v') vRatio = clampRatio((ev.clientX - rect.left) / rect.width);
|
|
|
|
|
|
else hRatio = clampRatio((ev.clientY - rect.top) / rect.height);
|
|
|
|
|
|
render();
|
|
|
|
|
|
}
|
|
|
|
|
|
function up() {
|
|
|
|
|
|
document.removeEventListener('mousemove', move);
|
|
|
|
|
|
document.removeEventListener('mouseup', up);
|
|
|
|
|
|
dragOverlay.style.display = 'none';
|
|
|
|
|
|
persist();
|
|
|
|
|
|
}
|
|
|
|
|
|
document.addEventListener('mousemove', move);
|
|
|
|
|
|
document.addEventListener('mouseup', up);
|
|
|
|
|
|
}
|
|
|
|
|
|
vGutter.addEventListener('mousedown', function (e) { e.preventDefault(); startDrag('v'); });
|
|
|
|
|
|
hGutter.addEventListener('mousedown', function (e) { e.preventDefault(); startDrag('h'); });
|
|
|
|
|
|
// 더블클릭 시 50%로 초기화
|
|
|
|
|
|
vGutter.addEventListener('dblclick', function () { vRatio = 0.5; render(); persist(); });
|
|
|
|
|
|
hGutter.addEventListener('dblclick', function () { hRatio = 0.5; render(); persist(); });
|
|
|
|
|
|
|
|
|
|
|
|
var layoutBtns = Array.prototype.slice.call(document.querySelectorAll('#wsLayout button'));
|
|
|
|
|
|
|
|
|
|
|
|
// CSS 길이 가감 (calc(0 + 28px) 같은 무효 표현 방지)
|
|
|
|
|
|
function addPx(val, px) { return val === '0' ? (px + 'px') : ('calc(' + val + ' + ' + px + 'px)'); }
|
|
|
|
|
|
function subPx(val, px) { return 'calc(' + val + ' - ' + px + 'px)'; }
|
|
|
|
|
|
|
|
|
|
|
|
function render() {
|
|
|
|
|
|
var rects = rectsFor(layout);
|
|
|
|
|
|
var n = rects.length;
|
|
|
|
|
|
var split = layout !== 'single';
|
|
|
|
|
|
// single 모드에서 포커스 칸이 비면 가장 최근 탭으로 자동 채움
|
|
|
|
|
|
if (!split && !(slots[0] && tabs[slots[0]]) && order.length) {
|
|
|
|
|
|
slots[0] = order[order.length - 1];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (focused >= n) focused = n - 1;
|
|
|
|
|
|
var shown = {};
|
|
|
|
|
|
for (var i = 0; i < 4; i++) {
|
|
|
|
|
|
var head = slotHeads[i], emp = slotEmpties[i];
|
|
|
|
|
|
if (i >= n) { head.style.display = 'none'; emp.style.display = 'none'; continue; }
|
|
|
|
|
|
var r = rects[i];
|
|
|
|
|
|
var bodyTop = split ? addPx(r.t, 28) : r.t;
|
|
|
|
|
|
var bodyH = split ? subPx(r.h, 28) : r.h;
|
|
|
|
|
|
if (split) {
|
|
|
|
|
|
head.style.display = 'flex';
|
|
|
|
|
|
head.style.left = r.l; head.style.top = r.t; head.style.width = r.w;
|
|
|
|
|
|
head.classList.toggle('focused', i === focused);
|
|
|
|
|
|
var sid = slots[i];
|
|
|
|
|
|
head.querySelector('.sh-name').textContent = (sid && tabs[sid]) ? tabs[sid].title : '비어 있음';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
head.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
var id = slots[i];
|
|
|
|
|
|
if (id && tabs[id]) {
|
|
|
|
|
|
shown[id] = true;
|
|
|
|
|
|
var f = tabs[id].frame;
|
|
|
|
|
|
f.style.left = r.l; f.style.top = bodyTop; f.style.width = r.w; f.style.height = bodyH;
|
|
|
|
|
|
f.style.display = 'block';
|
|
|
|
|
|
emp.style.display = 'none';
|
|
|
|
|
|
} else if (split) {
|
|
|
|
|
|
emp.style.display = 'flex';
|
|
|
|
|
|
emp.style.left = r.l; emp.style.top = bodyTop; emp.style.width = r.w; emp.style.height = bodyH;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
emp.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 어느 칸에도 없는 iframe 은 숨김 / 포커스 칸 프레임에 active 표시
|
|
|
|
|
|
Object.keys(tabs).forEach(function (k) {
|
|
|
|
|
|
if (!shown[k]) tabs[k].frame.style.display = 'none';
|
|
|
|
|
|
tabs[k].frame.classList.toggle('active', k === slots[focused]);
|
|
|
|
|
|
});
|
|
|
|
|
|
// 탭 버튼 강조 (표시 중 = active, 포커스 칸 = focused-tab)
|
2026-06-08 13:32:53 +09:00
|
|
|
|
Object.keys(tabs).forEach(function (k) {
|
2026-06-10 10:19:37 +09:00
|
|
|
|
var pos = slots.indexOf(k);
|
|
|
|
|
|
tabs[k].btn.classList.toggle('active', pos >= 0 && pos < n);
|
|
|
|
|
|
tabs[k].btn.classList.toggle('focused-tab', split && slots[focused] === k);
|
2026-06-08 13:32:53 +09:00
|
|
|
|
});
|
2026-06-10 10:19:37 +09:00
|
|
|
|
// 분할 구분선 위치/표시
|
|
|
|
|
|
if (layout === 'lr' || layout === 'quad') { vGutter.style.display = 'block'; vGutter.style.left = 'calc(' + pct(vRatio) + ' - 4px)'; }
|
|
|
|
|
|
else { vGutter.style.display = 'none'; }
|
|
|
|
|
|
if (layout === 'tb' || layout === 'quad') { hGutter.style.display = 'block'; hGutter.style.top = 'calc(' + pct(hRatio) + ' - 4px)'; }
|
|
|
|
|
|
else { hGutter.style.display = 'none'; }
|
|
|
|
|
|
// 레이아웃 버튼 강조
|
|
|
|
|
|
layoutBtns.forEach(function (b) { b.classList.toggle('active', b.getAttribute('data-mode') === layout); });
|
|
|
|
|
|
// 전체 빈 상태
|
|
|
|
|
|
if (empty) empty.style.display = order.length === 0 ? 'flex' : 'none';
|
|
|
|
|
|
// 포커스 칸 화면에 맞춰 좌측 사이드바 동기화
|
|
|
|
|
|
var fid = slots[focused];
|
|
|
|
|
|
try { if (window.govPortalNav) window.govPortalNav.syncByUrl(fid && tabs[fid] ? tabs[fid].url : ' |