Files
jongryangje/app/Views/bag/layout/portal.php
taekyoungc b9dd24082c feat: 화면설명 소제목 스크롤·강조 + 글씨크기 메뉴 확대 + 드로어 개선
- screenHelp 앵커(?hl=)로 '이 화면 설명' 클릭 시 해당 소제목으로 스크롤·강조, 재오픈 시 재강조(postMessage)
- 글씨 크기(A−/A+)가 상단 대메뉴·좌측 사이드바까지 확대, 관리자 페이지에도 조절 기능 추가
- 화면 설명 드로어 양방향 리사이즈(좁히기 가능) + 기본 너비 2배

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 13:31:31 +09:00

286 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/**
* 사이트 업무 페이지 공통 셸 — gov-portal 디자인 적용판.
* 헤더 + 대메뉴(클릭) + 좌측 사이드바(소메뉴) + 본문($content).
* 본문은 기존 Tailwind 마크업을 그대로 쓰므로 Tailwind CDN·config·data-table 스타일을 함께 로드한다.
*
* @var string $title
* @var string $content (이스케이프 없이 출력되는 본문 HTML)
* @var bool $bare true면 work-surface 카드/제목바 없이 본문을 그대로 출력(대시보드용)
*/
helper('admin');
$bare = ! empty($bare);
$gov = gov_portal_nav_context(false); // 업무 셸: 실제 bag/* 링크 유지(코드관리 포털 remap 안 함)
$govActiveChildHref = gov_portal_nav_match_path($gov['currentPath']);
$navPartial = [
'govNavItems' => $gov['navItems'],
'govNavJson' => $gov['navJson'],
'govActiveParentIdx' => $gov['activeParentIdx'],
'govCurrentPath' => gov_portal_nav_match_path($gov['currentPath']),
'govDashboardAliases' => $gov['dashboardAliases'],
'govActiveChildHref' => $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 : '';
}
$helpUrl = function_exists('manual_help_url_for_path') ? manual_help_url_for_path() : '';
?>
<!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 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; }
@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">
<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 ($lgLabel !== ''): ?><strong><?= esc($lgLabel) ?></strong> · <?php endif; ?>
<?= esc($levelName) ?> · <?= esc($mbName) ?>님
</span>
<a href="<?= base_url('workspace') ?>" 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-regular fa-window-restore"></i> 워크스페이스
</a>
<?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;white-space:nowrap;">
<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;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 (! $bare && ! empty($title)): ?>
<div style="display:flex;align-items:center;justify-content:space-between;gap:.5rem;">
<h1 class="work-titlebar" style="margin-bottom:0;"><i class="fa-solid fa-folder-open tb-ico"></i><?= esc($title) ?></h1>
<?php if ($helpUrl !== ''): ?>
<a href="<?= esc($helpUrl, 'attr') ?>" rel="noopener" class="no-print portal-help" style="display:inline-flex;align-items:center;gap:.3rem;padding:.3rem .6rem;border-radius:6px;background:#fff;border:1px solid var(--border);color:#1a2b4b;text-decoration:none;font-size:.75rem;font-weight:600;white-space:nowrap;">
<i class="fa-regular fa-circle-question"></i> 이 화면 설명
</a>
<?php endif; ?>
</div>
<?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 ($bare): ?>
<?= $content ?>
<?php else: ?>
<div class="work-surface">
<?= $content ?>
</div>
<?php endif; ?>
</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 () {
// 방문한 업무 메뉴 경로 기록 (메인 메뉴검색의 "최근 방문 메뉴"용)
try {
var p = (location.pathname || '').replace(/\/+$/, '') || '/';
if (p === '/' || /\/login|\/logout|\/register/.test(p)) return;
var KEY = 'jrj_recent_menus';
var arr = JSON.parse(localStorage.getItem(KEY) || '[]');
if (!Array.isArray(arr)) arr = [];
arr = arr.filter(function (x) { return x && x.p && x.p !== p; });
arr.unshift({ p: p, t: Date.now() });
localStorage.setItem(KEY, JSON.stringify(arr.slice(0, 12)));
} catch (e) {}
})();
</script>
<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);
})();
</script>
<script>
(() => {
// 표의 '번호' 컬럼 역순 자동 채번 (기존 사이트 레이아웃 계승)
const normalize = (s) => String(s || '').replace(/\s+/g, '').trim();
const renumberTable = (table) => {
const headRow = table.querySelector('thead tr');
if (!headRow) return;
const headers = Array.from(headRow.querySelectorAll('th'));
const numberCol = headers.findIndex((th) => normalize(th.textContent) === '번호');
if (numberCol < 0) return;
const body = table.querySelector('tbody');
if (!body) return;
const rows = Array.from(body.querySelectorAll(':scope > tr')).filter((tr) => {
const cells = tr.querySelectorAll('td');
if (cells.length === 0) return false;
if (cells.length === 1 && Number(cells[0].getAttribute('colspan') || '1') > 1) return false;
return true;
});
let no = rows.length;
rows.forEach((tr) => {
const cells = tr.querySelectorAll('td');
if (cells[numberCol]) cells[numberCol].textContent = String(no--);
});
};
const run = () => document.querySelectorAll('table').forEach(renumberTable);
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run, { once: true });
else run();
})();
</script>
<!-- 화면 설명 드로어(팝업) -->
<style>
.help-drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(920px, 92vw); background: #fff; box-shadow: -8px 0 26px rgba(0,0,0,.18); z-index: 9999; display: none; flex-direction: column; }
.help-drawer.open { display: flex; }
.help-drawer-head { display: flex; align-items: center; justify-content: space-between; padding: .5rem .75rem; background: #1a2b4b; color: #fff; font-size: .8rem; font-weight: 700; flex-shrink: 0; }
.help-drawer-head .hd-btn { color: #fff; background: rgba(255,255,255,.14); border: 0; width: 26px; height: 26px; border-radius: 6px; cursor: pointer; font-size: 13px; display: inline-flex; align-items: center; justify-content: center; text-decoration: none; margin-left: 4px; }
.help-drawer-head .hd-btn:hover { background: rgba(255,255,255,.28); }
.help-drawer iframe { flex: 1; width: 100%; border: 0; }
.help-drawer-grip { position: absolute; left: -4px; top: 0; bottom: 0; width: 8px; cursor: col-resize; }
</style>
<div id="helpDrawer" class="help-drawer no-print" aria-hidden="true">
<div class="help-drawer-grip" id="helpDrawerGrip"></div>
<div class="help-drawer-head">
<span><i class="fa-regular fa-circle-question"></i> 화면 설명</span>
<div><button type="button" id="helpDrawerClose" class="hd-btn" title="닫기">&times;</button></div>
</div>
<iframe id="helpDrawerFrame" title="화면 설명"></iframe>
</div>
<script>
(function () {
var drawer = document.getElementById('helpDrawer'), dFrame = document.getElementById('helpDrawerFrame');
function openHelp(url) {
var u = url; try { var x = new URL(url, location.href); x.searchParams.set('embed', '1'); u = x.href; } catch (e) {}
if (dFrame.getAttribute('data-src') !== u) {
dFrame.src = u; dFrame.setAttribute('data-src', u);
} else {
// 같은 URL → 재로드 안 함. 매뉴얼에 다시 강조하라고 알림(껐다 켜도 강조되도록)
try { dFrame.contentWindow.postMessage({ type: 'manual-hl' }, location.origin); } catch (e) {}
}
drawer.classList.add('open');
}
function closeHelp() { drawer.classList.remove('open'); }
document.addEventListener('click', function (e) {
var h = e.target.closest ? e.target.closest('a.portal-help') : null;
if (!h) return;
e.preventDefault();
openHelp(h.getAttribute('href'));
});
document.getElementById('helpDrawerClose').addEventListener('click', closeHelp);
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeHelp(); });
var grip = document.getElementById('helpDrawerGrip'), dragging = false;
// 드래그 중 iframe 이 마우스 이벤트를 가로채면(특히 좁힐 때) 멈추므로, 화면 전체 투명 오버레이로 이벤트를 가로챈다.
var ov = document.createElement('div');
ov.style.cssText = 'position:fixed;inset:0;z-index:10000;cursor:col-resize;display:none;';
document.body.appendChild(ov);
grip.addEventListener('mousedown', function (e) { e.preventDefault(); dragging = true; ov.style.display = 'block'; document.body.style.userSelect = 'none'; });
document.addEventListener('mousemove', function (e) { if (!dragging) return; var w = window.innerWidth - e.clientX; drawer.style.width = Math.min(window.innerWidth * 0.92, Math.max(300, w)) + 'px'; });
document.addEventListener('mouseup', function () { if (!dragging) return; dragging = false; ov.style.display = 'none'; document.body.style.userSelect = ''; });
// 글씨 크기 조절 — 본문 + 상단 대메뉴 + 좌측 사이드바(메뉴)에 zoom 적용.
var FONT_KEY = 'jrj_font_scale';
var scaleSelectors = ['.portal-header', '.sidebar', '.work-main'];
function curScale() { var s = parseInt(localStorage.getItem(FONT_KEY) || '100', 10); return (s >= 70 && s <= 150) ? s : 100; }
function applyScale(s) {
s = Math.min(150, Math.max(70, s));
try { localStorage.setItem(FONT_KEY, String(s)); } catch (e) {}
var z = s / 100;
scaleSelectors.forEach(function (sel) { var el = document.querySelector(sel); if (el) el.style.zoom = z; });
var pct = document.getElementById('wsFontPct'); if (pct) pct.textContent = s + '%';
}
applyScale(curScale());
var plus = document.getElementById('wsFontPlus'), minus = document.getElementById('wsFontMinus');
if (plus) plus.addEventListener('click', function () { applyScale(curScale() + 10); });
if (minus) minus.addEventListener('click', function () { applyScale(curScale() - 10); });
})();
</script>
</body>
</html>