Files
jongryangje/app/Views/home/dashboard_gov_portal.php
taekyoungc 8763876f19 사용자 매뉴얼·번호알기·gov-portal 대시보드와 메뉴 동선·수불 리포트를 보강한다.
- 사용자 매뉴얼: league/commonmark 기반 bag/manual(로그인 전용),
  ManualRenderer + Config\Manual manifest, 콘텐츠 8종, E2E
- 번호알기(봉투번호확인): bag/number-lookup, BagNumberLookup, E2E
- gov-portal 대시보드 시안(기본/strip)·기본코드관리 화면
- 메뉴 관리: 등록·수정 후 메뉴 화면 유지, 수정 버튼 클릭 시 상단 스크롤
- 수불/분석 리포트(LOT 수불·반품/파기·수급계획·추이) 표시 보강
- .gitignore: docs/ → /docs/ 앵커링(최상위 개발문서만 제외, app/Docs는 추적)

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

639 lines
20 KiB
PHP

<?php
declare(strict_types=1);
/**
* 공공 포털형 UI — 기본 레이아웃 (좌측 MY MENU)
*
* @var string $lgLabel
* @var string $activeVariant
*/
helper('admin');
$govPortalNavPartial = gov_portal_nav_partial_vars(get_defined_vars());
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<?= view('home/_dashboard_gov_portal_head') ?>
<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"/>
<?= view('home/_dashboard_gov_portal_map_leaflet_assets') ?>
<style>
:root {
--navy: #1a2b4b;
--navy-deep: #002b4e;
--blue: #0056b3;
--blue-ui: #007bff;
--blue-menu: #4a69bd;
--blue-light: #eef6ff;
--teal: #009688;
--bg: #f0f4f8;
--card: #fff;
--text: #444;
--text-dark: #222;
--muted: #888;
--border: #dde4ec;
--font-scale: 1;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html {
font-size: calc(14px * var(--font-scale));
-webkit-text-size-adjust: 100%;
}
body {
font-family: 'Pretendard', 'Malgun Gothic', 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif;
font-size: 0.8125rem; /* ~13px @14px root — 본문 */
font-weight: 400;
line-height: 1.45;
letter-spacing: -0.01em;
color: var(--text);
background: var(--bg);
min-height: 100vh;
display: flex;
flex-direction: column;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
<?php include __DIR__ . '/_dashboard_gov_portal_brand_css.php'; ?>
<?php include __DIR__ . '/_dashboard_gov_portal_topnav_css.php'; ?>
.layout {
display: flex;
flex: 1;
min-height: 0;
}
/* 좌측 사이드 */
.sidebar {
width: 168px;
flex-shrink: 0;
background: #fff;
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
font-size: 0.8125rem;
}
.my-menu-hd {
background: var(--navy);
color: #fff;
padding: 0.5rem 0.625rem;
font-weight: 700;
font-size: 0.6875rem; /* 11px — MY MENU 라벨 */
letter-spacing: 0.04em;
}
.my-menu-list { list-style: none; padding: 0.375rem 0.25rem; flex: 1; }
.my-menu-list li { margin: 0.1875rem 0.375rem; }
.my-menu-list a,
.my-menu-list .menu-sub {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.4375rem 0.625rem;
margin: 0;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.22);
text-decoration: none;
font-size: 0.8125rem; /* 13px */
font-weight: 600;
line-height: 1.35;
letter-spacing: -0.02em;
box-sizing: border-box;
transition: filter 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.my-menu-list a {
color: #fff;
background: var(--blue-menu);
}
.my-menu-list a .menu-ico,
.my-menu-list .menu-sub .menu-ico {
font-size: 0.625rem;
opacity: .9;
width: 0.75rem;
text-align: center;
flex-shrink: 0;
}
.my-menu-list a:hover { filter: brightness(1.06); border-color: rgba(255, 255, 255, 0.35); }
.my-menu-list a.active {
background: #3d5a9e;
font-weight: 700;
border-color: rgba(255, 255, 255, 0.4);
box-shadow: 0 1px 3px rgba(26, 43, 75, 0.12);
}
.my-menu-list a.menu-sub,
.my-menu-list .menu-sub {
background: var(--blue-light);
color: var(--blue);
font-size: 0.75rem;
font-weight: 600;
border-color: rgba(0, 86, 179, 0.18);
}
.sidebar-blocks { margin-top: auto; }
.sb-teal {
background: var(--teal);
color: #fff;
padding: 0.75rem 0.625rem;
font-size: 0.6875rem;
line-height: 1.5;
letter-spacing: -0.02em;
}
.sb-teal i { font-size: 1.125rem; margin-bottom: 0.25rem; display: block; }
.sb-gray {
background: #4a5568;
color: #fff;
padding: 0.625rem;
font-size: 0.6875rem;
line-height: 1.45;
}
.sb-links {
padding: 0.625rem;
background: #f5f7fa;
font-size: 0.6875rem;
}
.sb-links a {
display: block;
color: #555;
text-decoration: none;
padding: 0.1875rem 0;
letter-spacing: -0.02em;
font-weight: 600;
}
.sb-links a:hover { color: var(--blue-ui); }
/* 메인 그리드 */
.main {
flex: 1;
padding: 0.875rem 1rem 1rem;
overflow-y: auto;
}
.grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 0.875rem; /* ~14px */
align-items: stretch;
}
.grid .card-low-stock.stock-tall {
grid-row: span 2;
}
.card {
background: var(--card);
border-radius: 12px;
box-shadow: 0 1px 3px rgba(26,43,75,.06), 0 2px 8px rgba(26,43,75,.04);
border: 1px solid var(--border);
overflow: hidden;
}
.card-hd {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.625rem 0.875rem;
border-bottom: 1px solid var(--border);
font-weight: 700;
font-size: 1rem; /* ~16px 카드 제목 */
color: var(--text-dark);
letter-spacing: -0.03em;
line-height: 1.3;
}
.card-hd i {
color: var(--blue-ui);
margin-right: 0.3rem;
font-size: 0.875rem;
}
.card-bd { padding: 0.875rem 1rem; }
.span-3 { grid-column: span 3; }
.span-4 { grid-column: span 4; }
.span-5 { grid-column: span 5; }
.span-6 { grid-column: span 6; }
.span-8 { grid-column: span 8; }
.span-12 { grid-column: span 12; }
@media (max-width: 1200px) {
.span-3, .span-4, .span-5, .span-6, .span-8 { grid-column: span 12; }
.grid .card-low-stock.stock-tall { grid-row: span 1; }
.sidebar { width: 100%; border-right: none; border-bottom: 1px solid var(--border); }
.layout { flex-direction: column; }
.my-menu-list { display: flex; flex-wrap: wrap; }
}
.card-welcome {
background: #4a5568;
color: #fff;
border: none;
box-shadow: 0 2px 6px rgba(74,85,104,.25);
}
.card-welcome .card-bd { padding: 1rem 0.875rem; }
.welcome-hi {
font-size: 0.75rem;
color: rgba(255,255,255,.75);
letter-spacing: -0.02em;
}
.welcome-name {
font-size: 1rem;
font-weight: 700;
margin: 0.25rem 0 0.375rem;
letter-spacing: -0.03em;
}
.welcome-meta {
font-size: 0.6875rem;
color: rgba(255,255,255,.7);
line-height: 1.5;
letter-spacing: -0.02em;
}
.welcome-btns { display: flex; gap: 0.375rem; margin-top: 0.625rem; }
.welcome-btns a {
padding: 0.3125rem 0.625rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
text-decoration: none;
border: 1px solid rgba(255,255,255,.35);
background: rgba(255,255,255,.08);
color: #fff;
letter-spacing: -0.02em;
}
.welcome-btns a:hover { background: rgba(255,255,255,.18); }
.notice-item {
padding: 0.4375rem 0;
border-bottom: 1px dashed #e8edf2;
}
.notice-item:last-child { border-bottom: none; }
.notice-item .notice-title {
display: block;
font-size: 0.8125rem;
font-weight: 600;
color: var(--text-dark);
margin-bottom: 0.25rem;
line-height: 1.4;
letter-spacing: -0.02em;
}
.notice-item .notice-date {
display: inline-block;
font-size: 0.6875rem;
font-weight: 500;
color: var(--blue-ui);
background: var(--blue-light);
padding: 0.125rem 0.375rem;
border-radius: 2px;
letter-spacing: 0;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.375rem;
text-align: center;
}
.stat-grid .num {
font-size: 1.5rem; /* ~22px */
font-weight: 700;
color: #2563eb;
line-height: 1.15;
letter-spacing: -0.03em;
}
.stat-grid .num.num-text { font-size: 1.125rem; color: #10b981; }
.stat-grid .lbl {
font-size: 0.6875rem;
color: var(--muted);
margin-top: 0.1875rem;
font-weight: 400;
letter-spacing: -0.02em;
}
.stat-foot {
margin-top: 0.625rem;
padding-top: 0.4375rem;
border-top: 1px solid var(--border);
font-size: 0.6875rem;
color: var(--muted);
letter-spacing: -0.02em;
}
<?php include __DIR__ . '/_dashboard_gov_portal_stock_cards_css.php'; ?>
.search-teal {
background: var(--teal);
color: #fff;
padding: 0.875rem;
border-radius: 12px;
height: 100%;
min-height: 5.5rem;
}
.search-teal strong {
font-size: 0.875rem;
font-weight: 600;
letter-spacing: -0.02em;
}
.search-teal input {
width: 100%;
margin-top: 0.4375rem;
padding: 0.4375rem 1.75rem 0.4375rem 0.625rem;
border: none;
border-radius: 4px;
font-size: 0.8125rem;
font-family: inherit;
letter-spacing: -0.02em;
}
.search-wrap { position: relative; }
.search-wrap i {
position: absolute;
right: 0.65rem;
top: 50%;
transform: translateY(-50%);
color: var(--muted);
}
<?php include __DIR__ . '/_dashboard_gov_portal_menu_search_css.php'; ?>
.desk-blue {
background: linear-gradient(160deg, #1a4a8a 0%, #007bff 100%);
color: #fff;
padding: 0.875rem;
border-radius: 12px;
font-size: 0.75rem;
line-height: 1.65;
letter-spacing: -0.02em;
height: 100%;
min-height: 5.5rem;
}
.desk-blue strong {
font-size: 0.875rem;
font-weight: 700;
display: block;
margin-bottom: 0.3rem;
letter-spacing: -0.03em;
}
<?php include __DIR__ . '/_dashboard_gov_portal_map_css.php'; ?>
.timeline-side {
background: var(--navy-deep);
color: #fff;
padding: 0.375rem 0.25rem;
max-height: 200px;
overflow-y: auto;
font-size: 0.75rem;
}
.timeline-side .item {
padding: 0.375rem 0.3125rem;
border-bottom: 1px solid rgba(255,255,255,.12);
}
.timeline-side .time {
display: block;
font-size: 0.8125rem;
font-weight: 700;
color: #fff;
letter-spacing: -0.02em;
margin-bottom: 0.125rem;
}
.timeline-side .ev-text {
font-size: 0.75rem;
font-weight: 600;
color: #4fc3f7;
line-height: 1.35;
letter-spacing: -0.02em;
}
.donut-wrap {
display: flex;
align-items: center;
gap: 1rem;
}
.donut {
width: 82px;
height: 82px;
border-radius: 50%;
background: conic-gradient(#3b82f6 0% 52%, #10b981 52% 80%, #f59e0b 80% 100%);
position: relative;
flex-shrink: 0;
}
.donut::after {
content: '52%';
position: absolute;
inset: 24%;
background: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1.125rem;
color: var(--navy);
letter-spacing: -0.03em;
}
.donut-legend { list-style: none; font-size: 0.6875rem; color: var(--muted); line-height: 1.5; }
.mini-bars {
display: flex;
align-items: flex-end;
gap: 3px;
height: 64px;
}
.mini-bars .col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
gap: 2px;
font-size: 0.625rem;
color: var(--muted);
font-weight: 500;
}
.mini-bars .bar {
width: 100%;
background: linear-gradient(180deg, #2563eb, #60a5fa);
border-radius: 3px 3px 0 0;
min-height: 4px;
}
.gis-btn {
background: var(--blue-ui);
color: #fff;
font-size: 0.6875rem;
font-weight: 500;
padding: 0.25rem 0.5rem;
border-radius: 3px;
text-decoration: none;
letter-spacing: -0.02em;
}
.flash-ok {
grid-column: 1 / -1;
background: #ecfdf5;
border: 1px solid #a7f3d0;
color: #065f46;
padding: 0.65rem 1rem;
border-radius: 8px;
font-size: 12px;
}
</style>
</head>
<body>
<header class="portal-header">
<div class="portal-header-inner">
<?= view('home/_dashboard_gov_portal_brand', ['brandHref' => base_url('dashboard/gov-portal')]) ?>
<?= view('home/_dashboard_gov_portal_topnav_click', $govPortalNavPartial) ?>
<?= view('home/_dashboard_gov_portal_header_utils', ['activeVariant' => $activeVariant, 'portalVariants' => $portalVariants, 'mbName' => $mbName, 'levelName' => $levelName, 'lgLabel' => $lgLabel]) ?>
</div>
</header>
<div class="layout">
<?= view('home/_dashboard_gov_portal_sidebar', $govPortalNavPartial) ?>
<main class="main">
<div class="grid">
<?php if (session()->getFlashdata('success')): ?>
<div class="flash-ok"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<!-- 사용자 정보 (스크린샷: 다크 그레이 프로필 카드) -->
<div class="card card-welcome span-3">
<div class="card-bd">
<p class="welcome-hi">안녕하세요.</p>
<p class="welcome-name"><?= esc($mbName) ?>님</p>
<p class="welcome-meta">아이디 <?= esc($mbId) ?><br/>최근접속 <?= date('Y.m.d H:i') ?></p>
<div class="welcome-btns">
<a href="<?= base_url(gov_portal_code_kinds_portal_path('base')) ?>">기본 코드관리</a>
<a href="<?= base_url('dashboard/simple') ?>">마이페이지</a>
<a href="<?= base_url('logout') ?>">로그아웃</a>
</div>
</div>
</div>
<!-- 메시지 -->
<div class="card span-4">
<div class="card-hd"><span><i class="fa-regular fa-envelope"></i> 메시지</span></div>
<div class="card-bd">
<?php foreach ($notices as $n): ?>
<div class="notice-item">
<span class="notice-title"><?= esc($n['title']) ?></span>
<span class="notice-date"><?= esc($n['date']) ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- 핵심 지표 (메인 요약과 동일) -->
<div class="card span-5">
<div class="card-hd"><span><i class="fa-solid fa-warehouse"></i> 업무 현황 요약</span></div>
<div class="card-bd">
<div class="stat-grid">
<div>
<div class="num num-text">양호</div>
<div class="lbl">봉투 재고</div>
</div>
<div>
<div class="num">12</div>
<div class="lbl">미처리 구매신청</div>
</div>
<div>
<div class="num">4</div>
<div class="lbl">승인 대기</div>
</div>
</div>
<div class="stat-foot">
<i class="fa-solid fa-location-dot"></i> <?= esc($lgLabel) ?> · 기준일 <?= date('Y.m.d') ?>
</div>
</div>
</div>
<!-- 지도/타임라인 영역 -->
<div class="card span-8" style="padding:0;">
<div class="card-hd">
<span><i class="fa-solid fa-map"></i> 판매·수불 최근 동향</span>
<a href="<?= base_url('bag/flow') ?>" class="gis-btn">수불 통합 조회 &gt;</a>
</div>
<div style="display:grid;grid-template-columns:1fr 140px;">
<?= view('home/_dashboard_gov_portal_map_panel', [
'mapId' => 'govPortalMainMap',
'mapHeight' => '200px',
'lgLabel' => $lgLabel,
'govMapPanel' => $govMapPanel,
]) ?>
<div class="timeline-side">
<?php foreach ($timeline as $ev): ?>
<div class="item">
<span class="time"><?= esc($ev['time']) ?></span>
<span class="ev-text"><?= esc($ev['text']) ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- 메뉴 검색 -->
<div class="span-4">
<?= view('home/_dashboard_gov_portal_menu_search', [
'variant' => 'teal',
'inputId' => 'menuSearch',
'menuSearchOptions' => $menuSearchOptions,
]) ?>
</div>
<!-- 서비스 데스크 -->
<div class="span-4">
<div class="desk-blue">
<strong><i class="fa-solid fa-headset"></i> 서비스 데스크</strong>
담당: 시스템 운영팀<br/>
문의: help@wxn.local (목업)<br/>
평일 09:00 ~ 18:00
</div>
</div>
<!-- 재고 경보 단계 -->
<div class="card span-4">
<div class="card-hd"><span><i class="fa-solid fa-triangle-exclamation"></i> 재고 경보</span></div>
<div class="card-bd">
<?= view('home/_dashboard_gov_portal_stock_alert_levels', ['stockAlerts' => $stockAlerts]) ?>
</div>
</div>
<!-- 부족 재고 — 2행 span으로 재고 구성 오른쪽 빈칸(4열)까지 세로 채움 -->
<div class="card card-low-stock stock-tall span-4">
<div class="card-hd"><span><i class="fa-solid fa-box-open"></i> 부족 재고</span></div>
<div class="card-bd">
<div class="low-stock-grid">
<?php foreach ($lowStock as $item): ?>
<div class="bar-row">
<div class="meta"><span><?= esc($item['name']) ?></span><span><?= esc((string) $item['percent']) ?>%</span></div>
<div class="bar-track"><div class="bar-fill" style="width:<?= (int) $item['percent'] ?>%"></div></div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- 7일 추이 -->
<div class="card span-4">
<div class="card-hd"><span><i class="fa-solid fa-chart-column"></i> 최근 7일 신청</span></div>
<div class="card-bd">
<?php $maxReq = max($weeklyRequests); ?>
<div class="mini-bars">
<?php foreach ($weeklyRequests as $idx => $v): ?>
<?php $h = (int) round(($v / $maxReq) * 100); ?>
<div class="col">
<span><?= esc((string) $v) ?></span>
<div class="bar" style="height:<?= $h ?>%"></div>
<span>D<?= 6 - $idx ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- 재고 구성 -->
<div class="card span-4">
<div class="card-hd"><span><i class="fa-solid fa-chart-pie"></i> 재고 구성</span></div>
<div class="card-bd">
<div class="donut-wrap">
<div class="donut" aria-hidden="true"></div>
<ul class="donut-legend">
<?php foreach ($stockMix as $item): ?>
<li>
<span style="display:inline-block;width:7px;height:7px;border-radius:50%;background:<?= esc($item['color'], 'attr') ?>;vertical-align:middle;margin-right:3px;"></span>
<?= esc($item['name']) ?> <?= esc((string) $item['value']) ?>%
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
</div>
</div>
</main>
</div>
<?= view('home/_dashboard_gov_portal_nav_script_base', $govPortalNavPartial) ?>
<?= view('home/_dashboard_gov_portal_font_zoom_script') ?>
</body>
</html>