get('mb_level'); if (Roles::isSuperAdminEquivalent($level)) { $idx = session()->get('admin_selected_lg_idx'); return $idx !== null && $idx !== '' ? (int) $idx : null; } if ($level === Roles::LEVEL_LOCAL_ADMIN || $level === Roles::LEVEL_SHOP || $level === Roles::LEVEL_CITIZEN) { $idx = session()->get('mb_lg_idx'); return $idx !== null && $idx !== '' ? (int) $idx : null; } return null; } } if (! function_exists('resolve_site_menu_lg_idx')) { /** * site 상단 메뉴(menu 테이블) 조회용 지자체 PK. * admin_effective_lg_idx() 우선(메뉴 관리·Bag과 동일), 없으면 mb_lg_idx, 그다음 기본 1. */ function resolve_site_menu_lg_idx(): int { $lgIdx = admin_effective_lg_idx(); if ($lgIdx !== null) { return $lgIdx; } $raw = session()->get('mb_lg_idx'); return ($raw !== null && $raw !== '') ? (int) $raw : 1; } } if (! function_exists('get_admin_nav_items')) { /** * 관리자 상단 메뉴 항목 (DB menu 테이블, admin 타입, 현재 지자체·mb_level 기준, 평면 배열). * 지자체 미선택(super/본부)이면 빈 배열. 테이블/조회 실패 시에도 빈 배열. * * 하위 메뉴 포함 트리 구조가 필요하면 get_admin_nav_tree() 사용. */ function get_admin_nav_items(): array { try { $lgIdx = admin_effective_lg_idx(); if ($lgIdx === null) { return []; } $typeRow = model(\App\Models\MenuTypeModel::class)->getByCode('admin'); if (! $typeRow) { return []; } $mbLevel = (int) session()->get('mb_level'); return model(\App\Models\MenuModel::class)->getVisibleByLevel((int) $typeRow->mt_idx, $mbLevel, $lgIdx); } catch (\Throwable $e) { return []; } } } if (! function_exists('build_menu_tree')) { /** * menu 평면 배열을 mm_pidx/mm_idx 기준 트리로 변환. * * @param array $items * @return array 루트 노드 배열 */ function build_menu_tree(array $items): array { $map = []; foreach ($items as $item) { $item->children = []; $map[(int) $item->mm_idx] = $item; } $roots = []; foreach ($map as $id => $item) { $pidx = (int) $item->mm_pidx; if ($pidx === 0 || ! isset($map[$pidx])) { $roots[] = $item; } else { $map[$pidx]->children[] = $item; } } return $roots; } } if (! function_exists('flatten_menu_tree')) { /** * 트리 구조의 메뉴를 상위 → 하위 순으로 평면 배열로 풀어낸다. * 관리자 메뉴 목록 화면에서 "부모 바로 아래에 자식"이 나오도록 하기 위한 용도. * * @param array $tree * @return array */ function flatten_menu_tree(array $tree): array { $result = []; $walk = function ($nodes) use (&$result, &$walk) { foreach ($nodes as $node) { $children = $node->children ?? []; // children 속성은 목록에서 사용하지 않으므로 제거 unset($node->children); $result[] = $node; if (! empty($children)) { $walk($children); } } }; $walk($tree); return $result; } } if (! function_exists('get_admin_nav_tree')) { /** * 관리자 상단 메뉴 트리 (admin 타입, 현재 지자체·mb_level 기준). * 1차 메뉴는 mm_pidx=0, 하위 메뉴는 children 속성으로 접근. */ function get_admin_nav_tree(): array { $flat = get_admin_nav_items(); if (empty($flat)) { return []; } return build_menu_tree($flat); } } if (! function_exists('get_site_nav_tree')) { /** * 일반 사이트 상단 메뉴 트리 (site 타입, 현재 회원의 지자체·mb_level 기준). * 1차 메뉴는 mm_pidx=0, 하위 메뉴는 children 속성으로 접근. */ function get_site_nav_tree(): array { try { $lgIdx = resolve_site_menu_lg_idx(); $mbLevel = (int) session()->get('mb_level'); $menuModel = model(\App\Models\MenuModel::class); $typeRow = model(\App\Models\MenuTypeModel::class)->getByCode('site'); $siteMtIdx = $typeRow ? (int) $typeRow->mt_idx : 0; if ($siteMtIdx <= 0) { // 운영 DB 불일치 대비: menu_type 누락 시 legacy site mt_idx(4)로 시도 $siteMtIdx = 4; } $flat = $menuModel->getVisibleByLevel($siteMtIdx, $mbLevel, (int) $lgIdx); // 현재 지자체에 site 메뉴가 없으면, 기본 지자체(1)의 메뉴를 한 번 복사한 뒤 다시 시도 if (empty($flat)) { $menuModel->copyDefaultsFromLg($siteMtIdx, 1, (int) $lgIdx); $flat = $menuModel->getVisibleByLevel($siteMtIdx, $mbLevel, (int) $lgIdx); } // site 타입 매핑 불일치(예: menu_type=2, menu 데이터=4) 보정 if (empty($flat) && $siteMtIdx !== 4) { $legacyMtIdx = 4; $flat = $menuModel->getVisibleByLevel($legacyMtIdx, $mbLevel, (int) $lgIdx); if (empty($flat)) { $menuModel->copyDefaultsFromLg($legacyMtIdx, 1, (int) $lgIdx); $flat = $menuModel->getVisibleByLevel($legacyMtIdx, $mbLevel, (int) $lgIdx); } } if (empty($flat)) { return []; } return build_menu_tree($flat); } catch (\Throwable $e) { return []; } } } if (! function_exists('current_nav_request_path')) { /** * 메뉴 활성·mm_link 비교용 현재 경로 (라우트 기준, base_url 뒤 세그먼트). * request->getPath() · uri_string() · SiteURI::getRoutePath() 중 비어 있지 않은 값을 사용. */ function current_nav_request_path(): string { helper('url'); $request = service('request'); // 프레임워크 권장: uri_string() = baseURL 기준 경로 (우선) $candidates = [trim(uri_string(), '/')]; if ($request instanceof \CodeIgniter\HTTP\IncomingRequest) { $candidates[] = trim((string) $request->getPath(), '/'); } $uri = $request->getUri(); if ($uri instanceof \CodeIgniter\HTTP\SiteURI) { $candidates[] = trim($uri->getRoutePath(), '/'); } $path = ''; foreach ($candidates as $c) { if ($c !== '') { $path = $c; break; } } while (str_starts_with($path, 'index.php/')) { $path = substr($path, strlen('index.php/')); } // baseURL 에 경로가 있으면(서브폴더 설치) URI 앞에 붙은 동일 접두 제거 $basePath = parse_url(config(\Config\App::class)->baseURL, PHP_URL_PATH); $basePath = is_string($basePath) ? trim($basePath, '/') : ''; if ($basePath !== '' && $path !== '' && ($path === $basePath || str_starts_with($path, $basePath . '/'))) { $path = $path === $basePath ? '' : substr($path, strlen($basePath) + 1); } return $path; } } if (! function_exists('normalize_menu_link_for_url')) { /** * menu.mm_link 를 base_url() 인자로 쓸 수 있는 상대 경로로 정규화합니다. * http(s)://... 전체 URL이면 path 만 사용하고, 앞뒤 공백·슬래시를 정리합니다. */ function normalize_menu_link_for_url(?string $mmLink): string { $s = trim((string) $mmLink); if ($s === '') { return ''; } if (str_contains($s, '://')) { $path = parse_url($s, PHP_URL_PATH); $s = is_string($path) ? trim($path, '/') : ''; } else { $s = trim($s, '/'); } while (str_starts_with($s, 'index.php/')) { $s = substr($s, strlen('index.php/')); } if (str_starts_with($s, 'public/')) { $s = substr($s, strlen('public/')); } $basePath = parse_url(config(\Config\App::class)->baseURL, PHP_URL_PATH); $basePath = is_string($basePath) ? trim($basePath, '/') : ''; if ($basePath !== '' && $s !== '' && ($s === $basePath || str_starts_with($s, $basePath . '/'))) { $s = $s === $basePath ? '' : substr($s, strlen($basePath) + 1); } return $s; } } if (! function_exists('mgmt_path')) { /** * 업무 화면 상대 경로 (페이저 setPath 등). 선행 슬래시 없음. */ function mgmt_path(string $path): string { $path = trim($path, '/'); // bag/packaging-units 는 조회 전용(Bag) — CRUD 는 /bag/packaging-units/manage/* 로 분리 if ($path === 'packaging-units') { $path = 'packaging-units/manage'; } elseif (str_starts_with($path, 'packaging-units/')) { $path = 'packaging-units/manage/' . substr($path, strlen('packaging-units/')); } return 'bag/' . $path; } } if (! function_exists('mgmt_url')) { /** * 업무 화면 링크: 정식 URL 은 /bag/* (adminAuth). 포장 단위 CRUD 는 packaging-units/manage 로 치환. */ function mgmt_url(string $path): string { helper('url'); return site_url(mgmt_path($path)); } } if (! function_exists('apply_pager_path')) { /** * CI4 페이저: setPath 는 상대 경로만 허용(전체 URL 시 baseURL 이중 결합). * 검색 조건은 only() 로 유지합니다. * * @param \CodeIgniter\Pager\Pager $pager */ function apply_pager_path($pager, string $path, array $queryForPager = []): void { $pager->setPath($path); if ($queryForPager !== []) { $pager->only(array_keys($queryForPager)); } } } if (! function_exists('work_area_home_url')) { /** * 지자체 미선택 등으로 돌아갈 때: bag 업무 중이면 대시보드, 관리자면 admin 홈. */ function work_area_home_url(): string { helper('url'); $seg1 = service('request')->getUri()->getSegment(1); return ($seg1 === 'bag') ? site_url('dashboard') : site_url('admin'); } } if (! function_exists('format_ymd_korean')) { /** * Y-m-d 날짜를 '2026년 1월 5일' 형식으로 (월·일은 숫자, 월명은 한글 '월'). */ function format_ymd_korean(?string $ymd): string { if ($ymd === null || trim($ymd) === '') { return '—'; } $t = \DateTimeImmutable::createFromFormat('Y-m-d', trim($ymd)); if ($t === false) { return $ymd; } return $t->format('Y') . '년 ' . (int) $t->format('n') . '월 ' . (int) $t->format('j') . '일'; } } if (! function_exists('parse_ymd_from_triple')) { /** * 연·월·일 GET 값으로 Y-m-d 생성 (유효하지 않은 날짜는 null). */ function parse_ymd_from_triple(?string $y, ?string $m, ?string $d): ?string { if ($y === null || $y === '' || $m === null || $m === '' || $d === null || $d === '') { return null; } $yi = (int) $y; $mi = (int) $m; $di = (int) $d; if ($yi < 1000 || $yi > 9999 || ! checkdate($mi, $di, $yi)) { return null; } return sprintf('%04d-%02d-%02d', $yi, $mi, $di); } } if (! function_exists('site_nav_resolved_link_path')) { /** * 사이트 상단 메뉴 URL 세그먼트. mm_link(DB)만 사용 (비어 있으면 빈 문자열). * * @param string|null $mmName 호환용(미사용). * * @return string base_url() 인자 세그먼트(앞뒤 슬래시 없음) */ function site_nav_resolved_link_path(?string $mmLink, ?string $mmName = null): string { return normalize_menu_link_for_url($mmLink); } } if (! function_exists('menu_link_candidate_paths')) { /** * 활성 비교용 경로 후보. DB에 "menus" 처럼 짧게 넣은 경우 실제 URI가 admin/menus·bag/… 일 수 있어, * 현재 요청 경로에 맞게 admin/·bag/ 접두를 붙인 후보도 만든다. (슬래시 포함·admin 단독은 그대로 1개만) * * @return list */ function menu_link_candidate_paths(?string $mmLink, string $currentPath): array { $p = normalize_menu_link_for_url($mmLink); if ($p === '') { return []; } if (str_contains($p, '/') || $p === 'admin') { $cands = [$p]; if (preg_match('#^bag/packaging-units/manage(/.*)?$#', $p, $m)) { $cands[] = 'admin/packaging-units' . ($m[1] ?? ''); } elseif (preg_match('#^admin/packaging-units(/.*)?$#', $p, $m)) { $cands[] = 'bag/packaging-units/manage' . ($m[1] ?? ''); } elseif ($p === 'bag/inventory/inspection-select') { // 실사 선별 조회 메뉴는 작업 화면(inspection-work)도 동일 메뉴로 활성 처리 $cands[] = 'bag/inventory/inspection-work'; $cands[] = 'bag/inventory/inspection'; } elseif (str_starts_with($p, 'admin/')) { $cands[] = 'bag/' . substr($p, strlen('admin/')); } elseif (str_starts_with($p, 'bag/')) { $cands[] = 'admin/' . substr($p, strlen('bag/')); } return array_values(array_unique($cands)); } $out = [$p]; if (str_starts_with($currentPath, 'admin/') || $currentPath === 'admin') { $out[] = 'admin/' . $p; } if (str_starts_with($currentPath, 'bag/') || $currentPath === 'bag') { $out[] = 'bag/' . $p; } return array_values(array_unique($out)); } } if (! function_exists('menu_link_preferred_href_path')) { /** * base_url() 용 경로: 짧게 저장된 mm_link 는 현재 요청 기준 admin/·bag/ 후보 중 가장 알맞은 것 사용. */ function menu_link_preferred_href_path(?string $mmLink, string $currentPath): string { $cands = menu_link_candidate_paths($mmLink, $currentPath); if ($cands === []) { return ''; } foreach ($cands as $c) { $cl = strtolower($currentPath); $cc = strtolower($c); if ($cl === $cc || str_starts_with($cl, $cc . '/')) { return $c; } } foreach ($cands as $c) { if (str_contains($c, '/')) { return $c; } } return $cands[0]; } } if (! function_exists('menu_single_path_matches_request')) { /** * 단일 정규 경로가 현재 요청 path 와 일치하는지. * * @param list $dashboardPathAliases */ function menu_single_path_matches_request(string $path, string $currentPath, array $dashboardPathAliases = []): bool { if ($path === '') { return false; } $pathLower = strtolower($path); $currentLower = strtolower($currentPath); $aliasesLower = array_map(strtolower(...), $dashboardPathAliases); if ($dashboardPathAliases !== [] && in_array($pathLower, $aliasesLower, true) && in_array($currentLower, $aliasesLower, true)) { return true; } if ($currentLower === $pathLower) { return true; } if ($pathLower === 'admin') { return false; } return str_starts_with($currentLower, $pathLower . '/'); } } if (! function_exists('menu_link_matches_request')) { /** * 메뉴 mm_link(DB)가 현재 요청과 같은 메뉴인지. 비어 있으면 false. * * @param list $dashboardPathAliases */ function menu_link_matches_request(?string $mmLink, string $currentPath, array $dashboardPathAliases = []): bool { foreach (menu_link_candidate_paths($mmLink, $currentPath) as $cand) { if (menu_single_path_matches_request($cand, $currentPath, $dashboardPathAliases)) { return true; } } return false; } } if (! function_exists('site_nav_link_matches_current')) { /** * 사이트 상단 메뉴 활성 여부 (경로 후보·대시보드 별칭은 menu_link_matches_request 와 동일). * * @param list $dashboardPathAliases */ function site_nav_link_matches_current(?string $mmLink, string $currentPath, array $dashboardPathAliases = []): bool { return menu_link_matches_request($mmLink, $currentPath, $dashboardPathAliases); } } if (! function_exists('menu_active_child_for_parent')) { /** * 같은 부모 아래 형제 소메뉴 중, 현재 요청에 해당하는 항목을 하나만 고른다. * * 짧은 mm_link(예: bag/designated-shops)가 긴 경로(bag/designated-shops/browse)와 * 동시에 prefix 규칙으로 매칭될 때, 가장 구체적인 경로(일치한 후보 문자열 길이 최대)만 활성으로 본다. * 길이가 같으면 mm_num이 작은 항목을 선택(동일 URL이 여러 메뉴에 매핑된 경우 등). * * @param object{children?: array} $parentNavItem * @param list $dashboardPathAliases * * @return object|null 활성으로 표시할 자식 노드(mm_idx 등 포함), 없으면 null */ function menu_active_child_for_parent(object $parentNavItem, string $currentPath, array $dashboardPathAliases = []): ?object { $children = $parentNavItem->children ?? []; if ($children === []) { return null; } $best = null; $bestLen = -1; $bestMmNum = PHP_INT_MAX; foreach ($children as $child) { $mmLink = $child->mm_link ?? null; $maxLen = -1; foreach (menu_link_candidate_paths($mmLink, $currentPath) as $cand) { if (menu_single_path_matches_request($cand, $currentPath, $dashboardPathAliases)) { $maxLen = max($maxLen, strlen($cand)); } } if ($maxLen < 0) { continue; } $mmNum = (int) ($child->mm_num ?? 0); if ($maxLen > $bestLen || ($maxLen === $bestLen && $mmNum < $bestMmNum)) { $bestLen = $maxLen; $bestMmNum = $mmNum; $best = $child; } } return $best; } } if (! function_exists('site_nav_active_child_for_parent')) { /** * 사이트 상단 메뉴 전용 호환 래퍼. * * @param object{children?: array} $parentNavItem * @param list $dashboardPathAliases * * @return object|null */ function site_nav_active_child_for_parent(object $parentNavItem, string $currentPath, array $dashboardPathAliases = []): ?object { return menu_active_child_for_parent($parentNavItem, $currentPath, $dashboardPathAliases); } } if (! function_exists('session_user_nav_display')) { /** * 상단 메뉴바용: 로그인 사용자 이름·역할 표시 * * @return array{name: string, role_label: string}|null */ function session_user_nav_display(): ?array { if (! session()->get('logged_in')) { return null; } $name = trim((string) session()->get('mb_name')); if ($name === '') { $name = (string) session()->get('mb_id'); } $level = (int) session()->get('mb_level'); $roleLabel = config('Roles')->getLevelName($level); return [ 'name' => $name, 'role_label' => $roleLabel, ]; } } 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 */ 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, * navItems: list,hasChildren:bool}>, * navJson: string, * activeParentIdx: int, * currentPath: string, * dashboardAliases: list * } */ function gov_portal_nav_context(bool $remapLinks = true, ?array $tree = null): array { helper('url'); $tree = $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 = menu_link_preferred_href_path($child->mm_link ?? null, $currentPath); if ($remapLinks) { $href = gov_portal_menu_href_remap($href, $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 $navItems gov_portal_nav_context()['navItems'] * @return list */ 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 */ 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 $viewData * @return array */ 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; } }