Files
jongryangje/app/Views/bag/layout/portal.php
taekyoungc e8d58b5837 화면별 매뉴얼·이 화면 설명 버튼·탭 새로고침/경고·최근방문 기록 보강.
- 매뉴얼: 화면(소메뉴)별 용어·버튼·필드 설명으로 확장 + 기본정보 페이지 신규,
  개요에 용어 사전 추가 (종량제 지식 없는 사용자 대상)
- "이 화면 설명" 버튼: 화면 경로→매뉴얼 매핑(Config\Manual::screenHelp,
  manual_help_url_for_path). 워크스페이스 탭은 새 탭으로, 직접 페이지는 새 창으로
- 워크스페이스: 개별 탭 새로고침(↻) 버튼, 탭 2개 이상일 때만 새로고침 경고,
  사이드바 하단 링크(매뉴얼 등)도 탭으로 열기
- 임베드: 탭 내 링크/폼 embed 유지(중첩 헤더 방지), 매뉴얼 리다이렉트 embed 유지
- 사이드바 하단: 종합그래프 → 사용자 매뉴얼 링크
- 최근 방문 메뉴: embed 페이지에도 방문 기록, 대시보드는 storage 이벤트로 실시간 갱신
- E2E qa_sweep 추가(주요 화면 콘솔/오버레이/매뉴얼/도움말 매핑 점검)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 19:04:41 +09:00

212 lines
9.9 KiB
PHP

<?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 ?? '종량제 시스템') ?></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 th, .data-table td { border: 1px solid #ccc; padding: 4px 8px; white-space: nowrap; font-size: 13px; }
.data-table th { background-color: #e9ecef; text-align: center; vertical-align: middle; font-weight: bold; color: #333; }
.data-table tbody tr:nth-child(even) td { background-color: #f9f9f9; }
.data-table tbody tr:hover td { background-color: #e6f7ff !important; }
@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;">
<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') ?>" target="_blank" rel="noopener" class="no-print" 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>종량제 시스템</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>
</body>
</html>