사용자 매뉴얼·번호알기·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>
This commit is contained in:
@@ -594,3 +594,345 @@ if (! function_exists('session_user_nav_display')) {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('gov_portal_active_variant')) {
|
||||
/**
|
||||
* 공공 포털 시안 변형: base(좌측 MY MENU) | strip(호버 상단 메뉴).
|
||||
*/
|
||||
function gov_portal_active_variant(?string $fromPath = null): string
|
||||
{
|
||||
$path = strtolower(ltrim($fromPath ?? current_nav_request_path(), '/'));
|
||||
|
||||
return str_starts_with($path, 'dashboard/gov-portal-strip') ? 'strip' : 'base';
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('gov_portal_code_kinds_portal_path')) {
|
||||
/**
|
||||
* 포털 UI 기본 코드관리 시안 경로 (변형별).
|
||||
*/
|
||||
function gov_portal_code_kinds_portal_path(?string $variant = null): string
|
||||
{
|
||||
return gov_portal_active_variant($variant) === 'strip'
|
||||
? 'dashboard/gov-portal-strip/code-kinds'
|
||||
: 'dashboard/gov-portal/code-kinds';
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('gov_portal_menu_href_remap')) {
|
||||
/**
|
||||
* gov-portal 상·좌측·드롭다운 메뉴 전용: 기본 코드관리 → 변형별 포털 시안 URL.
|
||||
*/
|
||||
function gov_portal_menu_href_remap(string $href, ?string $variant = null): string
|
||||
{
|
||||
if (strtolower(ltrim($href, '/')) !== 'bag/code-kinds') {
|
||||
return $href;
|
||||
}
|
||||
|
||||
return gov_portal_code_kinds_portal_path($variant);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('gov_portal_nav_match_path')) {
|
||||
/**
|
||||
* 포털 시안 URL 접속 시 사이트 메뉴(mm_link) 활성 판별용.
|
||||
*/
|
||||
function gov_portal_nav_match_path(string $currentPath): string
|
||||
{
|
||||
$key = strtolower(ltrim($currentPath, '/'));
|
||||
|
||||
return match ($key) {
|
||||
'dashboard/gov-portal/code-kinds',
|
||||
'dashboard/gov-portal-strip/code-kinds' => 'bag/code-kinds',
|
||||
default => $currentPath,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('gov_portal_dashboard_aliases')) {
|
||||
/**
|
||||
* 포털 대시보드 현재 경로·메뉴 활성 판별용 별칭.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
function gov_portal_dashboard_aliases(): array
|
||||
{
|
||||
return [
|
||||
'dashboard',
|
||||
'dashboard/blend',
|
||||
'dashboard/simple',
|
||||
'dashboard/lite',
|
||||
'dashboard/compact',
|
||||
'dashboard/dense',
|
||||
'dashboard/charts',
|
||||
'dashboard/modern',
|
||||
'dashboard/gov-portal',
|
||||
'dashboard/gov-portal-strip',
|
||||
'dashboard/gov-portal/code-kinds',
|
||||
'dashboard/gov-portal-strip/code-kinds',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('gov_portal_nav_context')) {
|
||||
/**
|
||||
* 공공 포털형 대시보드(gov-portal)용 사이트 메뉴 트리·JSON·활성 대메뉴 인덱스.
|
||||
*
|
||||
* @return array{
|
||||
* siteNavTree: array<int, object>,
|
||||
* navItems: list<array{idx:int,name:string,href:string,url:string,children:list<array>,hasChildren:bool}>,
|
||||
* navJson: string,
|
||||
* activeParentIdx: int,
|
||||
* currentPath: string,
|
||||
* dashboardAliases: list<string>
|
||||
* }
|
||||
*/
|
||||
function gov_portal_nav_context(): array
|
||||
{
|
||||
helper('url');
|
||||
|
||||
$tree = get_site_nav_tree();
|
||||
$rawPath = current_nav_request_path();
|
||||
$variant = gov_portal_active_variant($rawPath);
|
||||
$currentPath = gov_portal_nav_match_path($rawPath);
|
||||
$aliases = gov_portal_dashboard_aliases();
|
||||
$items = [];
|
||||
$activeParentIdx = 0;
|
||||
|
||||
foreach ($tree as $pIdx => $parent) {
|
||||
$children = [];
|
||||
foreach ($parent->children ?? [] as $child) {
|
||||
$href = gov_portal_menu_href_remap(
|
||||
menu_link_preferred_href_path($child->mm_link ?? null, $currentPath),
|
||||
$variant
|
||||
);
|
||||
$children[] = [
|
||||
'idx' => (int) ($child->mm_idx ?? 0),
|
||||
'name' => (string) ($child->mm_name ?? ''),
|
||||
'href' => $href,
|
||||
'url' => $href !== '' ? base_url($href) : '',
|
||||
];
|
||||
}
|
||||
|
||||
$parentHref = menu_link_preferred_href_path($parent->mm_link ?? null, $currentPath);
|
||||
$isParentActive = site_nav_link_matches_current($parent->mm_link ?? null, $currentPath, $aliases);
|
||||
$activeChild = menu_active_child_for_parent($parent, $currentPath, $aliases);
|
||||
if ($isParentActive || $activeChild !== null) {
|
||||
$activeParentIdx = (int) $pIdx;
|
||||
}
|
||||
|
||||
$items[] = [
|
||||
'idx' => (int) ($parent->mm_idx ?? 0),
|
||||
'name' => (string) ($parent->mm_name ?? ''),
|
||||
'href' => $parentHref,
|
||||
'url' => $parentHref !== '' ? base_url($parentHref) : '',
|
||||
'children' => $children,
|
||||
'hasChildren' => $children !== [],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'siteNavTree' => $tree,
|
||||
'navItems' => $items,
|
||||
'navJson' => json_encode($items, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS),
|
||||
'activeParentIdx' => $activeParentIdx,
|
||||
'currentPath' => $rawPath,
|
||||
'dashboardAliases'=> $aliases,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('gov_portal_menu_search_options')) {
|
||||
/**
|
||||
* 메뉴검색 입력 아래 표시할 바로가기(사이트 메뉴에서 추출, 최대 N개).
|
||||
*
|
||||
* @param list<array> $navItems gov_portal_nav_context()['navItems']
|
||||
* @return list<array{label:string,url:string,keyword:string}>
|
||||
*/
|
||||
function gov_portal_menu_search_options(array $navItems, int $max = 8): array
|
||||
{
|
||||
$picked = [];
|
||||
$seen = [];
|
||||
$prefer = ['재고', '발주', '수불', '판매', '통계', '실사', '발급', '주문', '코드', '지정판매', '구매'];
|
||||
|
||||
$push = static function (array $child) use (&$picked, &$seen, $max): bool {
|
||||
$href = (string) ($child['href'] ?? '');
|
||||
if ($href === '' || isset($seen[$href])) {
|
||||
return false;
|
||||
}
|
||||
$seen[$href] = true;
|
||||
$picked[] = [
|
||||
'label' => (string) ($child['name'] ?? ''),
|
||||
'url' => (string) ($child['url'] ?? ''),
|
||||
'keyword' => (string) ($child['name'] ?? ''),
|
||||
];
|
||||
|
||||
return count($picked) >= $max;
|
||||
};
|
||||
|
||||
foreach ($prefer as $needle) {
|
||||
if (count($picked) >= $max) {
|
||||
break;
|
||||
}
|
||||
foreach ($navItems as $parent) {
|
||||
foreach ($parent['children'] ?? [] as $child) {
|
||||
$name = (string) ($child['name'] ?? '');
|
||||
if ($name === '' || ! str_contains($name, $needle)) {
|
||||
continue;
|
||||
}
|
||||
if ($push($child)) {
|
||||
break 3;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (count($picked) >= $max) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($navItems as $parent) {
|
||||
if (count($picked) >= $max) {
|
||||
break;
|
||||
}
|
||||
foreach ($parent['children'] ?? [] as $child) {
|
||||
if ($push($child)) {
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $picked;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('gov_portal_dashboard_view_data')) {
|
||||
/**
|
||||
* 공공 포털형 대시보드(gov-portal / gov-portal-strip) 뷰 데이터.
|
||||
* 컨트롤러에서 view() 두 번째 인자로 전달한다.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function gov_portal_dashboard_view_data(string $lgLabel, string $activeVariant): array
|
||||
{
|
||||
helper('url');
|
||||
$govNav = gov_portal_nav_context();
|
||||
|
||||
return [
|
||||
'lgLabel' => $lgLabel,
|
||||
'activeVariant' => $activeVariant,
|
||||
'mbName' => (string) (session()->get('mb_name') ?? '담당자'),
|
||||
'mbId' => (string) (session()->get('mb_id') ?? ''),
|
||||
'levelName' => config('Roles')->getLevelName((int) session()->get('mb_level')),
|
||||
'weeklyRequests' => [7, 12, 9, 14, 8, 11, 10],
|
||||
'stockMix' => [
|
||||
['name' => '일반용', 'value' => 52, 'color' => '#3b82f6'],
|
||||
['name' => '음식물', 'value' => 28, 'color' => '#10b981'],
|
||||
['name' => '특수', 'value' => 20, 'color' => '#f59e0b'],
|
||||
],
|
||||
'lowStock' => [
|
||||
['name' => '일반 20L', 'percent' => 34],
|
||||
['name' => '특수규격 A', 'percent' => 22],
|
||||
['name' => '재사용봉투', 'percent' => 58],
|
||||
],
|
||||
'stockAlerts' => [
|
||||
[
|
||||
'count' => 18,
|
||||
'label' => '정상',
|
||||
'class' => 'al-blue',
|
||||
'bags' => ['일반 10L', '일반 20L', '음식물 2L', '음식물 5L'],
|
||||
],
|
||||
[
|
||||
'count' => 3,
|
||||
'label' => '주의',
|
||||
'class' => 'al-yellow',
|
||||
'bags' => ['일반 50L', '특수 소형', '음식물 10L'],
|
||||
],
|
||||
[
|
||||
'count' => 2,
|
||||
'label' => '경계',
|
||||
'class' => 'al-orange',
|
||||
'bags' => ['특수규격 A', '재사용봉투'],
|
||||
],
|
||||
[
|
||||
'count' => 1,
|
||||
'label' => '부족',
|
||||
'class' => 'al-red',
|
||||
'bags' => ['일반 20L'],
|
||||
],
|
||||
],
|
||||
'quickLinks' => [
|
||||
['label' => '기본 코드관리', 'desc' => '포털 UI · 코드 종류·세부코드', 'url' => base_url(gov_portal_code_kinds_portal_path($activeVariant)), 'icon' => 'fa-code'],
|
||||
['label' => '창고 재고 조회', 'desc' => '품목별 현재 재고', 'url' => base_url('bag/inventory'), 'icon' => 'fa-boxes-stacked'],
|
||||
['label' => '발주(구매신청) 등록', 'desc' => '지정판매소 발주 입력', 'url' => base_url('bag/order/create'), 'icon' => 'fa-cart-shopping'],
|
||||
['label' => '수불 흐름 보기', 'desc' => '입고·출고 내역', 'url' => base_url('bag/flow'), 'icon' => 'fa-arrow-right-arrow-left'],
|
||||
['label' => '판매 내역 조회', 'desc' => '기간별 판매 현황', 'url' => base_url('bag/sales'), 'icon' => 'fa-receipt'],
|
||||
['label' => '전년 대비 통계', 'desc' => '통계분석 · YoY', 'url' => base_url('bag/analytics/year-over-year'), 'icon' => 'fa-chart-line'],
|
||||
['label' => '도움말 / 매뉴얼', 'desc' => '업무별 사용 안내', 'url' => base_url('bag/help'), 'icon' => 'fa-circle-question'],
|
||||
],
|
||||
'notices' => [
|
||||
['title' => '봉투 단가 조정 예고 — 3/1 적용 예정', 'date' => '2026.05.12'],
|
||||
['title' => '실사·재고 점검 일정 안내', 'date' => '2026.05.08'],
|
||||
['title' => '지정판매소 바코드 연동 점검 완료', 'date' => '2026.04.29'],
|
||||
],
|
||||
'timeline' => [
|
||||
['time' => '14:32', 'text' => 'GS25 검단점 — 일반 20L 판매 3건'],
|
||||
['time' => '13:10', 'text' => '북구 창고 — 입고 확인 완료'],
|
||||
['time' => '11:45', 'text' => '구매신청 #1042 승인 대기'],
|
||||
['time' => '09:20', 'text' => '회원 가입 승인 1건 처리'],
|
||||
],
|
||||
'govMapPanel' => [
|
||||
'centerLat' => 35.8714,
|
||||
'centerLng' => 128.6014,
|
||||
'zoom' => 11,
|
||||
'markers' => [
|
||||
['lat' => 35.8852, 'lng' => 128.5821, 'kind' => 'warehouse', 'title' => '북구 종량제 창고'],
|
||||
['lat' => 35.8684, 'lng' => 128.6243, 'kind' => 'shop', 'title' => 'GS25 검단점'],
|
||||
['lat' => 35.8621, 'lng' => 128.5948, 'kind' => 'shop', 'title' => 'CU 칠곡중앙점'],
|
||||
['lat' => 35.8776, 'lng' => 128.6479, 'kind' => 'flow', 'title' => '동구 입고·출고 거점'],
|
||||
['lat' => 35.8512, 'lng' => 128.6112, 'kind' => 'shop', 'title' => '이마트 월배점'],
|
||||
],
|
||||
],
|
||||
'portalVariants' => [
|
||||
['label' => '기본', 'url' => base_url('dashboard/gov-portal')],
|
||||
['label' => '변형', 'url' => base_url('dashboard/gov-portal-strip')],
|
||||
],
|
||||
'siteNavTree' => $govNav['siteNavTree'],
|
||||
'govNavItems' => $govNav['navItems'],
|
||||
'menuSearchOptions' => gov_portal_menu_search_options($govNav['navItems']),
|
||||
'govNavJson' => $govNav['navJson'],
|
||||
'govActiveParentIdx' => $govNav['activeParentIdx'],
|
||||
'govCurrentPath' => gov_portal_nav_match_path($govNav['currentPath']),
|
||||
'govDashboardAliases' => $govNav['dashboardAliases'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('gov_portal_nav_partial_vars')) {
|
||||
/**
|
||||
* CI4 view() partial에 넘길 사이트 메뉴·네비 변수만 추출.
|
||||
*
|
||||
* @param array<string, mixed> $viewData
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function gov_portal_nav_partial_vars(array $viewData): array
|
||||
{
|
||||
$keys = [
|
||||
'siteNavTree',
|
||||
'govNavItems',
|
||||
'govNavJson',
|
||||
'govActiveParentIdx',
|
||||
'govCurrentPath',
|
||||
'govDashboardAliases',
|
||||
'govActiveChildHref',
|
||||
];
|
||||
$out = [];
|
||||
foreach ($keys as $key) {
|
||||
if (array_key_exists($key, $viewData)) {
|
||||
$out[$key] = $viewData[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user