기존 전역 localStorage 방식(모든 메뉴 일괄 적용)을 화면 경로(메뉴키) 기준 계정별 DB 저장으로 변경. A−/A+ 조정 시 해당 화면만 조정·저장되고, 다음 방문·재로그인 시 각 화면의 저장값이 서버 주입으로 복원된다. - member_menu_font_scale 테이블 + 저장 엔드포인트(bag/pref/font-scale) - member_font_scale_for() 헬퍼로 렌더 시 현재 화면 저장값 주입 - admin/portal/embed 레이아웃 + 워크스페이스(탭 iframe)에 적용 · 워크스페이스는 포커스된 탭(메뉴)만 조정, 디바운스 저장으로 CSRF 경합 방지 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
233 lines
12 KiB
PHP
233 lines
12 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
/**
|
||
* 관리자 공통 레이아웃 — gov-portal 디자인 적용판.
|
||
* 헤더 + 관리자 대메뉴(클릭) + 좌측 사이드바(소메뉴) + 본문($content).
|
||
*
|
||
* @var string $title
|
||
* @var string $content
|
||
*/
|
||
helper('admin');
|
||
|
||
$mbLevel = (int) session()->get('mb_level');
|
||
$isSuperAdmin = \Config\Roles::isSuperAdminEquivalent($mbLevel);
|
||
$mbName = (string) (session()->get('mb_name') ?? '담당자');
|
||
$levelName = config(\Config\Roles::class)->getLevelName($mbLevel);
|
||
|
||
$effectiveLgIdx = admin_effective_lg_idx();
|
||
$effectiveLgName = '';
|
||
if ($effectiveLgIdx) {
|
||
$lgRow = model(\App\Models\LocalGovernmentModel::class)->find($effectiveLgIdx);
|
||
$effectiveLgName = $lgRow ? (string) $lgRow->lg_name : '';
|
||
}
|
||
|
||
$adminTree = function_exists('get_admin_nav_tree') ? get_admin_nav_tree() : [];
|
||
$gov = gov_portal_nav_context(false, $adminTree);
|
||
|
||
// 관리자 메뉴가 비어 있으면(지자체 미선택 등) 핵심 항목 폴백 노출
|
||
$navItems = $gov['navItems'];
|
||
if ($navItems === []) {
|
||
$mk = static fn (string $name, string $path): array => [
|
||
'idx' => 0, 'name' => $name, 'href' => $path, 'url' => base_url($path),
|
||
];
|
||
$navItems = [
|
||
['idx' => 0, 'name' => '대시보드', 'href' => 'admin', 'url' => base_url('admin'), 'children' => [], 'hasChildren' => false],
|
||
['idx' => 0, 'name' => '회원·접근', 'href' => '', 'url' => '', 'hasChildren' => true, 'children' => [
|
||
$mk('회원 관리', 'admin/users'), $mk('로그인 이력', 'admin/access/login-history'), $mk('활동 로그', 'admin/access/activity-logs'), $mk('승인 대기', 'admin/access/approvals'),
|
||
]],
|
||
['idx' => 0, 'name' => '시스템', 'href' => '', 'url' => '', 'hasChildren' => true, 'children' => [
|
||
$mk('역할', 'admin/roles'), $mk('메뉴', 'admin/menus'),
|
||
]],
|
||
];
|
||
if ($isSuperAdmin) {
|
||
$navItems[] = ['idx' => 0, 'name' => '지자체', 'href' => '', 'url' => '', 'hasChildren' => true, 'children' => [
|
||
$mk('지자체 전환', 'admin/select-local-government'), $mk('지자체 관리', 'admin/local-governments'),
|
||
]];
|
||
}
|
||
}
|
||
$navJson = json_encode($navItems, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS);
|
||
|
||
$navPartial = [
|
||
'govNavItems' => $navItems,
|
||
'govNavJson' => $navJson,
|
||
'govActiveParentIdx' => $gov['activeParentIdx'],
|
||
'govCurrentPath' => $gov['currentPath'],
|
||
'govDashboardAliases' => $gov['dashboardAliases'],
|
||
'govActiveChildHref' => $gov['currentPath'],
|
||
];
|
||
?>
|
||
<!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><?= esc($title ?? '관리자') ?> - GBLS</title>
|
||
<link rel="icon" type="image/svg+xml" href="<?= base_url('favicon.svg') ?>"/>
|
||
<link rel="alternate icon" href="<?= base_url('favicon.ico') ?>"/>
|
||
<link rel="apple-touch-icon" href="<?= base_url('favicon.svg') ?>"/>
|
||
<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"/>
|
||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||
<script>
|
||
tailwind.config = {
|
||
theme: {
|
||
extend: {
|
||
fontFamily: { sans: ['Pretendard', '"Malgun Gothic"', '"Noto Sans KR"', 'sans-serif'] },
|
||
colors: {
|
||
'system-header': '#ffffff', 'title-bar': '#1a2b4b', 'control-panel': '#f8f9fa',
|
||
'btn-search': '#243a5e', 'btn-excel-border': '#28a745', 'btn-excel-text': '#28a745',
|
||
'btn-print-border': '#ced4da', 'btn-exit': '#d9534f',
|
||
},
|
||
fontSize: { 'xxs': '0.65rem' }
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
<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'; ?>
|
||
.data-table { width: 100%; border-collapse: collapse; font-family: 'Pretendard', 'Malgun Gothic', 'Noto Sans KR', sans-serif; }
|
||
.data-table { font-size: 13px; }
|
||
.data-table th, .data-table td { text-align: left; padding: 0.55rem 0.5rem; white-space: nowrap; border: 0; border-bottom: 1px solid #e5e7eb; }
|
||
.data-table thead th { font-size: 0.6875rem; font-weight: 600; color: #6b7280; background: transparent; vertical-align: middle; }
|
||
.data-table tbody td { color: #374151; }
|
||
.data-table tbody tr:last-child td { border-bottom: 0; }
|
||
.data-table tbody tr:hover td { background-color: #f9fafb; }
|
||
/* body의 select-none 상속을 무효화해 본문 텍스트를 드래그·복사할 수 있게 하는 opt-in 클래스 */
|
||
.fb-selectable, .fb-selectable * {
|
||
-webkit-user-select: text;
|
||
-moz-user-select: text;
|
||
-ms-user-select: text;
|
||
user-select: text;
|
||
}
|
||
@media print {
|
||
.portal-header, .sidebar, .portal-footer, .no-print, nav.portal-top-nav { display: none !important; }
|
||
body.gov-portal-shell { background: #fff; display: block; }
|
||
.gov-portal-shell .main.work-main { overflow: visible !important; padding: 0 !important; }
|
||
.print-header { display: block !important; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="gov-portal-shell select-none">
|
||
<header class="portal-header">
|
||
<div class="portal-header-inner">
|
||
<?= view('home/_dashboard_gov_portal_brand', ['brandHref' => base_url('/')]) ?>
|
||
<?= view('home/_dashboard_gov_portal_topnav_click', $navPartial) ?>
|
||
<div class="portal-header-utils" style="display:flex;align-items:center;gap:.5rem;">
|
||
<div class="ws-fontctl" title="글씨 크기 조절" style="display:inline-flex;align-items:center;gap:2px;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.25);border-radius:6px;padding:1px;">
|
||
<button type="button" id="wsFontMinus" title="글씨 작게" style="width:24px;height:22px;border:0;background:transparent;color:#fff;cursor:pointer;font-size:11px;line-height:1;border-radius:5px;">A−</button>
|
||
<span id="wsFontPct" style="min-width:34px;text-align:center;color:#fff;font-size:.68rem;font-weight:600;">100%</span>
|
||
<button type="button" id="wsFontPlus" title="글씨 크게" style="width:24px;height:22px;border:0;background:transparent;color:#fff;cursor:pointer;font-size:14px;line-height:1;border-radius:5px;">A+</button>
|
||
</div>
|
||
<span class="user-line">
|
||
<?php if ($effectiveLgName !== ''): ?><strong><?= esc($effectiveLgName) ?></strong> · <?php endif; ?>
|
||
<?= esc($levelName) ?> · <?= esc($mbName) ?>님
|
||
</span>
|
||
<a href="<?= base_url('/') ?>" 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;white-space:nowrap;">
|
||
<i class="fa-solid fa-house"></i> 사이트
|
||
</a>
|
||
<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;white-space:nowrap;">
|
||
<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="main work-main main-content-area">
|
||
<?php if (! empty($title)): ?>
|
||
<h1 class="work-titlebar"><i class="fa-solid fa-gear tb-ico"></i><?= esc($title) ?></h1>
|
||
<?php endif; ?>
|
||
<?php if (session()->getFlashdata('success')): ?>
|
||
<div class="work-flash ok"><?= esc(session()->getFlashdata('success')) ?></div>
|
||
<?php endif; ?>
|
||
<?php if (session()->getFlashdata('error')): ?>
|
||
<div class="work-flash err"><?= esc(session()->getFlashdata('error')) ?></div>
|
||
<?php endif; ?>
|
||
<?php if (session()->getFlashdata('errors')): ?>
|
||
<div class="work-flash err"><?php foreach (session()->getFlashdata('errors') as $err): ?><div><?= esc($err) ?></div><?php endforeach; ?></div>
|
||
<?php endif; ?>
|
||
<div class="work-surface">
|
||
<?= $content ?>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<footer class="portal-footer">
|
||
<span>GBLS 관리자</span>
|
||
<span><?= date('Y.m.d (D) H:i') ?></span>
|
||
</footer>
|
||
|
||
<?= view('home/_dashboard_gov_portal_nav_script_base', $navPartial) ?>
|
||
<script>
|
||
(function () {
|
||
// bfcache 복원 시 열린 채 남은 모달/팝업으로 회색 레이어가 클릭을 막는 문제 방지
|
||
function closeStuckOverlays() {
|
||
document.querySelectorAll('.fixed.inset-0[id$="-modal"], .fixed.inset-0[id$="-popup"]').forEach(function (el) {
|
||
el.classList.add('hidden'); el.setAttribute('aria-hidden', 'true');
|
||
});
|
||
document.body.style.overflow = '';
|
||
}
|
||
window.addEventListener('pageshow', function (e) { if (e.persisted) closeStuckOverlays(); });
|
||
window.addEventListener('pagehide', closeStuckOverlays);
|
||
})();
|
||
|
||
// 글씨 크기 조절(A−/A+) — 계정별·메뉴별 저장(서버). 현재 화면 경로 기준으로 개별 적용.
|
||
<?php $__fmk = function_exists('current_nav_request_path') ? current_nav_request_path() : ''; $__fsc = function_exists('member_font_scale_for') ? member_font_scale_for($__fmk) : 100; ?>
|
||
(function () {
|
||
var MENU_KEY = <?= json_encode($__fmk) ?>;
|
||
var scale = <?= (int) $__fsc ?>;
|
||
var SAVE_URL = <?= json_encode(site_url('bag/pref/font-scale')) ?>;
|
||
var CSRF_NAME = <?= json_encode(csrf_token()) ?>;
|
||
var csrfHash = <?= json_encode(csrf_hash()) ?>;
|
||
var scaleSelectors = ['.sidebar', '.work-main'];
|
||
var saveTimer = null;
|
||
function persist(s) {
|
||
try { localStorage.setItem('jrj_font_scale::' + MENU_KEY, String(s)); } catch (e) {}
|
||
if (!MENU_KEY) return;
|
||
clearTimeout(saveTimer);
|
||
saveTimer = setTimeout(function () {
|
||
var b = new URLSearchParams(); b.set(CSRF_NAME, csrfHash); b.set('menu_key', MENU_KEY); b.set('scale', String(s));
|
||
fetch(SAVE_URL, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: b.toString() })
|
||
.then(function (r) { return r.json(); }).then(function (d) { if (d && d.csrf) csrfHash = d.csrf; }).catch(function () {});
|
||
}, 400);
|
||
}
|
||
function applyScale(s, save) {
|
||
s = Math.min(150, Math.max(70, s));
|
||
scale = s;
|
||
var z = s / 100;
|
||
scaleSelectors.forEach(function (sel) { var el = document.querySelector(sel); if (el) el.style.zoom = z; });
|
||
var topnav = document.querySelector('.portal-top-nav');
|
||
if (topnav) topnav.style.fontSize = (0.875 * z).toFixed(4) + 'rem';
|
||
var pct = document.getElementById('wsFontPct'); if (pct) pct.textContent = s + '%';
|
||
if (save) persist(s);
|
||
}
|
||
applyScale(scale, false);
|
||
var plus = document.getElementById('wsFontPlus'), minus = document.getElementById('wsFontMinus');
|
||
if (plus) plus.addEventListener('click', function () { applyScale(scale + 10, true); });
|
||
if (minus) minus.addEventListener('click', function () { applyScale(scale - 10, true); });
|
||
})();
|
||
</script>
|
||
<script>
|
||
// 네이티브 날짜 입력의 월 표기를 한글(예: 6월)로 — 요소 lang 지정(브라우저 영어 로케일 대응)
|
||
(function () {
|
||
function fixLang() {
|
||
document.querySelectorAll('input[type=date],input[type=month],input[type=datetime-local],input[type=week],input[type=time]')
|
||
.forEach(function (el) { el.setAttribute('lang', 'ko'); });
|
||
}
|
||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fixLang); else fixLang();
|
||
})();
|
||
</script>
|
||
<?= view('components/feedback_widget') ?>
|
||
<script>
|
||
/* 인쇄 시 활동 로그 기록(beacon) */
|
||
(function(){var last=0;window.addEventListener('beforeprint',function(){var now=Date.now();if(now-last<4000)return;last=now;try{var fd=new FormData();fd.append('title',document.title||'');fd.append('path',location.pathname||'');navigator.sendBeacon('<?= site_url('bag/activity/print-log') ?>',fd);}catch(e){}});})();
|
||
</script>
|
||
</body>
|
||
</html>
|