Files
jongryangje/app/Helpers/admin_helper.php

939 lines
34 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use Config\Roles;
if (! function_exists('admin_effective_lg_idx')) {
/**
* 관리자 화면·사이트 메뉴·Bag 등에서 쓰는 작업 지자체 PK.
* Super/본부 admin_selected_lg_idx(미선택 null).
* 지자체관리자·지정판매소·일반 사용자 mb_lg_idx(없으면 null).
*/
function admin_effective_lg_idx(): ?int
{
$level = (int) session()->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<int,object> $items
* @return array<int,object> 루트 노드 배열
*/
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<int,object> $tree
* @return array<int,object>
*/
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<string>
*/
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<string> $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<string> $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<string> $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<int, object>} $parentNavItem
* @param list<string> $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<int, object>} $parentNavItem
* @param list<string> $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<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;
}
}