refactor: unify bag and admin layout routing

This commit is contained in:
taekyoungc
2026-04-08 00:18:01 +09:00
parent 5b0c3fac97
commit 89f80edc5d
15 changed files with 832 additions and 354 deletions

View File

@@ -40,7 +40,7 @@ class App extends BaseConfig
* something else. If you have configured your web server to remove this file
* from your site URIs, set this variable to an empty string.
*/
public string $indexPage = 'index.php';
public string $indexPage = '';
/**
* --------------------------------------------------------------------------

View File

@@ -43,11 +43,49 @@ class Roles extends BaseConfig
}
/**
* 기본코드(종류·세부) 등록·수정·삭제 가능 (지자체·super·본부 관리자)
* 기본코드(종류·세부) 등록·수정·삭제 가능 (super admin(4) · 본부 관리자(5)만)
*/
public static function canManageCodeMaster(int $level): bool
{
return $level === self::LEVEL_LOCAL_ADMIN || self::isSuperAdminEquivalent($level);
return self::isSuperAdminEquivalent($level);
}
/**
* 기본코드 종류(code_kind) CRUD — super·본부만
*/
public static function canManageCodeKindMaster(int $level): bool
{
return self::isSuperAdminEquivalent($level);
}
/**
* 플랫폼 공통 세부코드(CSV·시드) 수정·삭제 — super·본부만
*/
public static function canEditPlatformCodeDetail(int $level): bool
{
return self::isSuperAdminEquivalent($level);
}
/**
* 세부코드 행 단위 수정/삭제 가능 여부
*
* @param object $row code_detail (cd_source, cd_lg_idx)
*/
public static function canEditCodeDetailRow(int $level, object $row, ?int $adminEffectiveLgIdx): bool
{
if (! self::canManageCodeMaster($level)) {
return false;
}
$src = $row->cd_source ?? 'platform';
$lg = (int) ($row->cd_lg_idx ?? 0);
if ($src === 'platform' && $lg === 0) {
return self::canEditPlatformCodeDetail($level);
}
if (self::isSuperAdminEquivalent($level)) {
return true;
}
return $adminEffectiveLgIdx !== null && $adminEffectiveLgIdx > 0 && $lg === $adminEffectiveLgIdx;
}
/**

View File

@@ -17,6 +17,9 @@ $routes->get('bag/waste-suibal-enterprise', 'Home::wasteSuibalEnterprise');
// 사이트 메뉴 (/bag/*)
$routes->get('bag/basic-info', 'Bag::basicInfo');
$routes->get('bag/prices', 'Bag::prices');
$routes->post('bag/prices', 'Bag::prices');
$routes->get('bag/packaging-units', 'Bag::packagingUnits');
$routes->get('bag/code-kinds', 'Bag::codeKinds');
$routes->get('bag/code-details/(:num)', 'Bag::codeDetails/$1');
@@ -40,6 +43,7 @@ $routes->post('bag/issue/store', 'Bag::issueStore');
$routes->post('bag/issue/cancel/(:num)', 'Bag::issueCancel/$1');
$routes->get('bag/order/create', 'Bag::orderCreate');
$routes->post('bag/order/store', 'Bag::orderStore');
$routes->post('bag/order/cancel/(:num)', 'Bag::orderCancel/$1');
$routes->get('bag/receiving/create', 'Bag::receivingCreate');
$routes->post('bag/receiving/store', 'Bag::receivingStore');
$routes->get('bag/sale/create', 'Bag::saleCreate');
@@ -47,6 +51,108 @@ $routes->post('bag/sale/store', 'Bag::saleStore');
$routes->get('bag/shop-order/create', 'Bag::shopOrderCreate');
$routes->post('bag/shop-order/store', 'Bag::shopOrderStore');
// 메인 사이트 메뉴용 업무 URL (관리자 권한). 동일 컨트롤러가 URI 가 bag 이면 메인 사이트 레이아웃으로 렌더.
$routes->group('bag', ['filter' => 'adminAuth'], static function ($routes): void {
$routes->get('managers', 'Admin\Manager::index');
$routes->get('managers/create', 'Admin\Manager::create');
$routes->post('managers/store', 'Admin\Manager::store');
$routes->get('managers/edit/(:num)', 'Admin\Manager::edit/$1');
$routes->post('managers/update/(:num)', 'Admin\Manager::update/$1');
$routes->post('managers/delete/(:num)', 'Admin\Manager::delete/$1');
$routes->get('sales-agencies', 'Admin\SalesAgency::index');
$routes->get('sales-agencies/create', 'Admin\SalesAgency::create');
$routes->post('sales-agencies/store', 'Admin\SalesAgency::store');
$routes->get('sales-agencies/edit/(:num)', 'Admin\SalesAgency::edit/$1');
$routes->post('sales-agencies/update/(:num)', 'Admin\SalesAgency::update/$1');
$routes->post('sales-agencies/delete/(:num)', 'Admin\SalesAgency::delete/$1');
$routes->get('companies', 'Admin\Company::index');
$routes->get('companies/create', 'Admin\Company::create');
$routes->post('companies/store', 'Admin\Company::store');
$routes->get('companies/edit/(:num)', 'Admin\Company::edit/$1');
$routes->post('companies/update/(:num)', 'Admin\Company::update/$1');
$routes->post('companies/delete/(:num)', 'Admin\Company::delete/$1');
$routes->get('free-recipients', 'Admin\FreeRecipient::index');
$routes->get('free-recipients/create', 'Admin\FreeRecipient::create');
$routes->post('free-recipients/store', 'Admin\FreeRecipient::store');
$routes->get('free-recipients/edit/(:num)', 'Admin\FreeRecipient::edit/$1');
$routes->post('free-recipients/update/(:num)', 'Admin\FreeRecipient::update/$1');
$routes->post('free-recipients/delete/(:num)', 'Admin\FreeRecipient::delete/$1');
$routes->get('designated-shops/export', 'Admin\DesignatedShop::export');
$routes->get('designated-shops/map', 'Admin\DesignatedShop::map');
$routes->get('designated-shops/status', 'Admin\DesignatedShop::status');
$routes->get('designated-shops', 'Admin\DesignatedShop::index');
$routes->get('designated-shops/create', 'Admin\DesignatedShop::create');
$routes->post('designated-shops/store', 'Admin\DesignatedShop::store');
$routes->get('designated-shops/edit/(:num)', 'Admin\DesignatedShop::edit/$1');
$routes->post('designated-shops/update/(:num)', 'Admin\DesignatedShop::update/$1');
$routes->post('designated-shops/delete/(:num)', 'Admin\DesignatedShop::delete/$1');
$routes->get('bag-prices', 'Admin\BagPrice::index');
$routes->get('bag-prices/create', 'Admin\BagPrice::create');
$routes->post('bag-prices/store', 'Admin\BagPrice::store');
$routes->get('bag-prices/edit/(:num)', 'Admin\BagPrice::edit/$1');
$routes->post('bag-prices/update/(:num)', 'Admin\BagPrice::update/$1');
$routes->post('bag-prices/delete/(:num)', 'Admin\BagPrice::delete/$1');
$routes->get('bag-prices/history/(:num)', 'Admin\BagPrice::history/$1');
$routes->get('bag-orders/export', 'Admin\BagOrder::export');
$routes->get('bag-orders', 'Admin\BagOrder::index');
$routes->get('bag-orders/create', 'Admin\BagOrder::create');
$routes->post('bag-orders/store', 'Admin\BagOrder::store');
$routes->get('bag-orders/detail/(:num)', 'Admin\BagOrder::detail/$1');
$routes->post('bag-orders/cancel/(:num)', 'Admin\BagOrder::cancel/$1');
$routes->post('bag-orders/delete/(:num)', 'Admin\BagOrder::delete/$1');
$routes->get('bag-receivings', 'Admin\BagReceiving::index');
$routes->get('bag-receivings/create', 'Admin\BagReceiving::create');
$routes->post('bag-receivings/store', 'Admin\BagReceiving::store');
$routes->get('bag-inventory/export', 'Admin\BagInventory::export');
$routes->get('bag-inventory', 'Admin\BagInventory::index');
$routes->get('shop-orders', 'Admin\ShopOrder::index');
$routes->get('shop-orders/create', 'Admin\ShopOrder::create');
$routes->post('shop-orders/store', 'Admin\ShopOrder::store');
$routes->post('shop-orders/cancel/(:num)', 'Admin\ShopOrder::cancel/$1');
$routes->get('bag-sales/export', 'Admin\BagSale::export');
$routes->get('bag-sales', 'Admin\BagSale::index');
$routes->get('bag-sales/create', 'Admin\BagSale::create');
$routes->post('bag-sales/store', 'Admin\BagSale::store');
$routes->get('bag-issues', 'Admin\BagIssue::index');
$routes->get('bag-issues/create', 'Admin\BagIssue::create');
$routes->post('bag-issues/store', 'Admin\BagIssue::store');
$routes->post('bag-issues/cancel/(:num)', 'Admin\BagIssue::cancel/$1');
$routes->get('packaging-units/manage', 'Admin\PackagingUnit::index');
$routes->get('packaging-units/manage/create', 'Admin\PackagingUnit::create');
$routes->post('packaging-units/manage/store', 'Admin\PackagingUnit::store');
$routes->get('packaging-units/manage/edit/(:num)', 'Admin\PackagingUnit::edit/$1');
$routes->post('packaging-units/manage/update/(:num)', 'Admin\PackagingUnit::update/$1');
$routes->post('packaging-units/manage/delete/(:num)', 'Admin\PackagingUnit::delete/$1');
$routes->get('packaging-units/manage/history/(:num)', 'Admin\PackagingUnit::history/$1');
$routes->get('reports/sales-ledger', 'Admin\SalesReport::salesLedger');
$routes->get('reports/daily-summary', 'Admin\SalesReport::dailySummary');
$routes->get('reports/period-sales', 'Admin\SalesReport::periodSales');
$routes->get('reports/supply-demand', 'Admin\SalesReport::supplyDemand');
$routes->get('reports/yearly-sales', 'Admin\SalesReport::yearlySales');
$routes->get('reports/shop-sales', 'Admin\SalesReport::shopSales');
$routes->get('reports/hometax-export', 'Admin\SalesReport::hometaxExport');
$routes->get('reports/returns', 'Admin\SalesReport::returns');
$routes->get('reports/lot-flow', 'Admin\SalesReport::lotFlow');
$routes->get('reports/misc-flow', 'Admin\SalesReport::miscFlow');
$routes->post('reports/misc-flow', 'Admin\SalesReport::miscFlowStore');
$routes->get('password-change', 'Admin\PasswordChange::index');
$routes->post('password-change', 'Admin\PasswordChange::update');
});
// Auth
$routes->get('login', 'Auth::showLoginForm');
$routes->post('login', 'Auth::login');
@@ -90,10 +196,6 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo
$routes->post('local-governments/update/(:num)', 'Admin\LocalGovernment::update/$1');
$routes->post('local-governments/delete/(:num)', 'Admin\LocalGovernment::delete/$1');
// 비밀번호 변경 (P2-20)
$routes->get('password-change', 'Admin\PasswordChange::index');
$routes->post('password-change', 'Admin\PasswordChange::update');
// 기본코드 종류 관리 (P2-01) — 등록·수정·삭제는 관리자 전용
$routes->get('code-kinds/create', 'Admin\CodeKind::create');
$routes->post('code-kinds/store', 'Admin\CodeKind::store');
@@ -108,112 +210,26 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo
$routes->post('code-details/update/(:num)', 'Admin\CodeDetail::update/$1');
$routes->post('code-details/delete/(:num)', 'Admin\CodeDetail::delete/$1');
// 봉투 단가 관리 (P2-03/04)
$routes->get('bag-prices', 'Admin\BagPrice::index');
$routes->get('bag-prices/create', 'Admin\BagPrice::create');
$routes->post('bag-prices/store', 'Admin\BagPrice::store');
$routes->get('bag-prices/edit/(:num)', 'Admin\BagPrice::edit/$1');
$routes->post('bag-prices/update/(:num)', 'Admin\BagPrice::update/$1');
$routes->post('bag-prices/delete/(:num)', 'Admin\BagPrice::delete/$1');
$routes->get('bag-prices/history/(:num)', 'Admin\BagPrice::history/$1');
// 발주 관리 (P3-01~05)
$routes->get('bag-orders/export', 'Admin\BagOrder::export');
$routes->get('bag-orders', 'Admin\BagOrder::index');
$routes->get('bag-orders/create', 'Admin\BagOrder::create');
$routes->post('bag-orders/store', 'Admin\BagOrder::store');
$routes->get('bag-orders/detail/(:num)', 'Admin\BagOrder::detail/$1');
$routes->post('bag-orders/cancel/(:num)', 'Admin\BagOrder::cancel/$1');
$routes->post('bag-orders/delete/(:num)', 'Admin\BagOrder::delete/$1');
// 입고 관리 (P3-06~09)
$routes->get('bag-receivings', 'Admin\BagReceiving::index');
$routes->get('bag-receivings/create', 'Admin\BagReceiving::create');
$routes->post('bag-receivings/store', 'Admin\BagReceiving::store');
// 재고 현황 (P3-10)
$routes->get('bag-inventory/export', 'Admin\BagInventory::export');
$routes->get('bag-inventory', 'Admin\BagInventory::index');
// 주문 접수 관리 (P4-01~03)
$routes->get('shop-orders', 'Admin\ShopOrder::index');
$routes->get('shop-orders/create', 'Admin\ShopOrder::create');
$routes->post('shop-orders/store', 'Admin\ShopOrder::store');
$routes->post('shop-orders/cancel/(:num)', 'Admin\ShopOrder::cancel/$1');
// 판매/반품 관리 (P4-04~07)
$routes->get('bag-sales/export', 'Admin\BagSale::export');
$routes->get('bag-sales', 'Admin\BagSale::index');
$routes->get('bag-sales/create', 'Admin\BagSale::create');
$routes->post('bag-sales/store', 'Admin\BagSale::store');
// 무료용 불출 관리 (P4-08~10)
$routes->get('bag-issues', 'Admin\BagIssue::index');
$routes->get('bag-issues/create', 'Admin\BagIssue::create');
$routes->post('bag-issues/store', 'Admin\BagIssue::store');
$routes->post('bag-issues/cancel/(:num)', 'Admin\BagIssue::cancel/$1');
// 포장 단위 관리 (P2-05/06)
$routes->get('packaging-units', 'Admin\PackagingUnit::index');
$routes->get('packaging-units/create', 'Admin\PackagingUnit::create');
$routes->post('packaging-units/store', 'Admin\PackagingUnit::store');
$routes->get('packaging-units/edit/(:num)', 'Admin\PackagingUnit::edit/$1');
$routes->post('packaging-units/update/(:num)', 'Admin\PackagingUnit::update/$1');
$routes->post('packaging-units/delete/(:num)', 'Admin\PackagingUnit::delete/$1');
$routes->get('packaging-units/history/(:num)', 'Admin\PackagingUnit::history/$1');
// 현황/리포트 (Phase 5)
$routes->get('reports/sales-ledger', 'Admin\SalesReport::salesLedger');
$routes->get('reports/daily-summary', 'Admin\SalesReport::dailySummary');
$routes->get('reports/period-sales', 'Admin\SalesReport::periodSales');
$routes->get('reports/supply-demand', 'Admin\SalesReport::supplyDemand');
$routes->get('reports/yearly-sales', 'Admin\SalesReport::yearlySales');
$routes->get('reports/shop-sales', 'Admin\SalesReport::shopSales');
$routes->get('reports/hometax-export', 'Admin\SalesReport::hometaxExport');
$routes->get('reports/returns', 'Admin\SalesReport::returns');
$routes->get('reports/lot-flow', 'Admin\SalesReport::lotFlow');
$routes->get('reports/misc-flow', 'Admin\SalesReport::miscFlow');
$routes->post('reports/misc-flow', 'Admin\SalesReport::miscFlowStore');
// 판매 대행소 관리 (P2-07/08)
$routes->get('sales-agencies', 'Admin\SalesAgency::index');
$routes->get('sales-agencies/create', 'Admin\SalesAgency::create');
$routes->post('sales-agencies/store', 'Admin\SalesAgency::store');
$routes->get('sales-agencies/edit/(:num)', 'Admin\SalesAgency::edit/$1');
$routes->post('sales-agencies/update/(:num)', 'Admin\SalesAgency::update/$1');
$routes->post('sales-agencies/delete/(:num)', 'Admin\SalesAgency::delete/$1');
// 담당자 관리 (P2-09/10)
$routes->get('managers', 'Admin\Manager::index');
$routes->get('managers/create', 'Admin\Manager::create');
$routes->post('managers/store', 'Admin\Manager::store');
$routes->get('managers/edit/(:num)', 'Admin\Manager::edit/$1');
$routes->post('managers/update/(:num)', 'Admin\Manager::update/$1');
$routes->post('managers/delete/(:num)', 'Admin\Manager::delete/$1');
// 업체 관리 (P2-11/12)
$routes->get('companies', 'Admin\Company::index');
$routes->get('companies/create', 'Admin\Company::create');
$routes->post('companies/store', 'Admin\Company::store');
$routes->get('companies/edit/(:num)', 'Admin\Company::edit/$1');
$routes->post('companies/update/(:num)', 'Admin\Company::update/$1');
$routes->post('companies/delete/(:num)', 'Admin\Company::delete/$1');
// 무료용 대상자 관리 (P2-13/14)
$routes->get('free-recipients', 'Admin\FreeRecipient::index');
$routes->get('free-recipients/create', 'Admin\FreeRecipient::create');
$routes->post('free-recipients/store', 'Admin\FreeRecipient::store');
$routes->get('free-recipients/edit/(:num)', 'Admin\FreeRecipient::edit/$1');
$routes->post('free-recipients/update/(:num)', 'Admin\FreeRecipient::update/$1');
$routes->post('free-recipients/delete/(:num)', 'Admin\FreeRecipient::delete/$1');
$routes->get('designated-shops/export', 'Admin\DesignatedShop::export');
$routes->get('designated-shops/map', 'Admin\DesignatedShop::map');
$routes->get('designated-shops/status', 'Admin\DesignatedShop::status');
$routes->get('designated-shops', 'Admin\DesignatedShop::index');
$routes->get('designated-shops/create', 'Admin\DesignatedShop::create');
$routes->post('designated-shops/store', 'Admin\DesignatedShop::store');
$routes->get('designated-shops/edit/(:num)', 'Admin\DesignatedShop::edit/$1');
$routes->post('designated-shops/update/(:num)', 'Admin\DesignatedShop::update/$1');
$routes->post('designated-shops/delete/(:num)', 'Admin\DesignatedShop::delete/$1');
// 구 업무 URL → /bag/* (실제 처리는 bag 그룹). GET 301, POST 307.
$adminToBagPrefixes = [
'managers',
'sales-agencies',
'companies',
'free-recipients',
'designated-shops',
'bag-prices',
'bag-orders',
'bag-receivings',
'bag-inventory',
'shop-orders',
'bag-sales',
'bag-issues',
'packaging-units',
'reports',
'password-change',
];
foreach ($adminToBagPrefixes as $p) {
$routes->match(['get', 'post'], $p, 'Admin\WorkMovedToBag::toBag/' . $p);
$routes->match(['get', 'post'], $p . '/(:any)', 'Admin\WorkMovedToBag::toBag/' . $p . '/$1');
}
});

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Controllers\Admin;
use App\Controllers\BaseController;
/**
* 구 관리자 업무 URL(admin/…) → 메인 사이트 업무 URL(bag/…) 영구 이전.
* POST 폼은 307로 메서드·본문 유지를 시도하고, GET 은 301.
*/
class WorkMovedToBag extends BaseController
{
public function toBag(string $prefix, string $rest = ''): \CodeIgniter\HTTP\RedirectResponse
{
$rest = trim($rest, '/');
if ($prefix === 'packaging-units') {
$path = 'packaging-units/manage';
if ($rest !== '') {
$path .= '/' . $rest;
}
} else {
$path = $prefix;
if ($rest !== '') {
$path .= '/' . $rest;
}
}
$target = site_url('bag/' . $path);
$query = $this->request->getUri()->getQuery();
if ($query !== '') {
$target .= '?' . $query;
}
$code = $this->request->getMethod() === 'post' ? 307 : 301;
return redirect()->to($target)->setStatusCode($code);
}
}

View File

@@ -42,4 +42,31 @@ abstract class BaseController extends Controller
// Preload any models, libraries, etc, here.
// $this->session = service('session');
}
/**
* /admin/* 또는 /bag/* 업무 화면 공통: 요청이 bag 이면 메인 사이트 레이아웃, 아니면 관리자 레이아웃.
*
* @param array<string, mixed> $contentData
*/
protected function renderWorkPage(string $title, string $contentView, array $contentData = []): string
{
$content = view($contentView, $contentData);
$uri = service('request')->getUri();
$seg1 = $uri->getSegment(1);
$seg2 = $uri->getSegment(2);
// 지정판매소 관리는 관리자 전용 기능으로, /bag 경로여도 관리자 레이아웃을 유지한다.
$forceAdminLayoutOnBag = ($seg1 === 'bag' && $seg2 === 'designated-shops');
if ($seg1 === 'bag' && ! $forceAdminLayoutOnBag) {
return view('bag/layout/main', [
'title' => $title,
'content' => $content,
]);
}
return view('admin/layout', [
'title' => $title,
'content' => $content,
]);
}
}

View File

@@ -16,11 +16,16 @@ class Home extends BaseController
}
/**
* 로그인 후 원래 메인 화면 (admin 유사 레이아웃 + site 메뉴 호버 드롭다운)
* 로그인 후 메인 — site 메뉴 레이아웃 + 종합·그래프(blend) 본문
*/
public function dashboard()
{
return view('bag/daily_inventory');
return view('bag/layout/main', [
'title' => '업무 현황 · 종합·그래프',
'content' => view('bag/dashboard_blend_inner', [
'lgLabel' => $this->resolveLgLabel(),
]),
]);
}
/**
@@ -62,13 +67,11 @@ class Home extends BaseController
}
/**
* dense(표·KPI) + charts(Chart.js) 혼합. URL: /dashboard/blend
* /dashboard 와 동일 본문(호환 URL)
*/
public function dashboardBlend()
{
return view('bag/lg_dashboard_blend', [
'lgLabel' => $this->resolveLgLabel(),
]);
return $this->dashboard();
}
/**
@@ -114,4 +117,5 @@ class Home extends BaseController
return '북구 (데모)';
}
}

View File

@@ -6,8 +6,9 @@ use Config\Roles;
if (! function_exists('admin_effective_lg_idx')) {
/**
* 현재 로그인한 관리자가 작업 대상으로 사용하는 지자체 PK.
* Super/본부 관리자 → admin_selected_lg_idx, 지자체 관리자 → mb_lg_idx, 그 외 null.
* 관리자 화면·사이트 메뉴·Bag 등에서 쓰는 작업 지자체 PK.
* Super/본부 → admin_selected_lg_idx(미선택 시 null).
* 지자체관리자·지정판매소·일반 사용자 → mb_lg_idx(없으면 null).
*/
function admin_effective_lg_idx(): ?int
{
@@ -16,7 +17,9 @@ if (! function_exists('admin_effective_lg_idx')) {
$idx = session()->get('admin_selected_lg_idx');
return $idx !== null && $idx !== '' ? (int) $idx : null;
}
if ($level === Roles::LEVEL_LOCAL_ADMIN) {
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;
}
@@ -24,6 +27,23 @@ if (! function_exists('admin_effective_lg_idx')) {
}
}
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 기준, 평면 배열).
@@ -130,11 +150,7 @@ if (! function_exists('get_site_nav_tree')) {
function get_site_nav_tree(): array
{
try {
$lgIdx = session()->get('mb_lg_idx');
// 시민 등 지자체 정보가 세션에 없으면 기본 지자체(1) 기준으로 메뉴를 보여 준다.
if ($lgIdx === null || $lgIdx === '') {
$lgIdx = 1;
}
$lgIdx = resolve_site_menu_lg_idx();
$typeRow = model(\App\Models\MenuTypeModel::class)->getByCode('site');
if (! $typeRow) {
return [];
@@ -156,3 +172,319 @@ if (! function_exists('get_site_nav_tree')) {
}
}
}
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_url')) {
/**
* 업무 화면 링크: 정식 URL 은 /bag/* (adminAuth). 포장 단위 CRUD 는 packaging-units/manage 로 치환.
*/
function mgmt_url(string $path): string
{
helper('url');
$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 site_url('bag/' . $path);
}
}
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 (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('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,
];
}
}

View File

@@ -12,15 +12,17 @@ if ($effectiveLgIdx) {
$lgRow = model(\App\Models\LocalGovernmentModel::class)->find($effectiveLgIdx);
$effectiveLgName = $lgRow ? $lgRow->lg_name : null;
}
$currentPath = trim((string) $uriObj->getPath(), '/');
if (str_starts_with($currentPath, 'index.php/')) {
$currentPath = substr($currentPath, strlen('index.php/'));
}
$userNav = session_user_nav_display();
$currentPath = current_nav_request_path();
$adminNavTree = get_admin_nav_tree();
$isActive = static function (string $path) use ($uri, $seg3, $currentPath, $adminNavTree) {
if (! empty($adminNavTree)) {
return $currentPath === trim($path, '/');
}
/** DB 링크(mm_link)만 사용. 짧게 적은 항목(menus 등)은 실제 URI(admin/menus)와 맞춰 후보 비교 */
$adminNavItemIsCurrent = static function (?string $mmLink) use ($currentPath): bool {
return menu_link_matches_request($mmLink, $currentPath, []);
};
/** 메뉴가 DB에서 안 쓰일 때만(폴백 상단바) 세그먼트 기반 활성 */
$isActive = static function (string $path) use ($uri, $seg3) {
if ($path === 'admin' || $path === '') return $uri === '';
if ($path === 'users') return $uri === 'users';
if ($path === 'login-history') return $uri === 'access' && $seg3 === 'login-history';
@@ -38,7 +40,7 @@ $isActive = static function (string $path) use ($uri, $seg3, $currentPath, $admi
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title><?= esc($title ?? '관리자') ?> - 쓰레기봉투 물류시스템</title>
<title><?= esc($title ?? '관리자') ?> - 종량제 시스템</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&amp;display=swap" rel="stylesheet"/>
<script>
@@ -81,34 +83,53 @@ body { overflow: hidden; }
</style>
</head>
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased select-none">
<header class="bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0 z-20">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<div class="w-6 h-6 flex items-center justify-center shrink-0">
<svg class="h-5 w-5" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" fill="#2563eb"/><rect x="2" y="2" width="7" height="7" fill="white"/><rect x="5" y="5" width="9" height="9" fill="white"/>
</svg>
</div>
<a href="<?= base_url('admin') ?>" class="text-base font-semibold text-gray-800 tracking-tight hover:text-blue-600">쓰레기봉투 물류시스템</a>
<header class="relative bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0 z-50">
<div class="flex items-center gap-2 shrink-0">
<?= view('components/header_brand', ['href' => base_url('admin')]) ?>
</div>
<nav class="hidden md:flex gap-5 text-sm font-medium text-gray-600">
<?php if (! empty($adminNavTree)): ?>
<?php foreach ($adminNavTree as $navItem): ?>
<?php $hasChildren = ! empty($navItem->children); ?>
<?php
$hasChildren = ! empty($navItem->children);
$parentLink = menu_link_preferred_href_path($navItem->mm_link ?? null, $currentPath);
$parentIsCurrent = $adminNavItemIsCurrent($navItem->mm_link ?? null);
if (! $parentIsCurrent && $hasChildren) {
foreach ($navItem->children as $ch) {
if ($adminNavItemIsCurrent($ch->mm_link ?? null)) {
$parentIsCurrent = true;
break;
}
}
}
?>
<div class="relative group">
<a class="<?= $isActive($navItem->mm_link) ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>"
href="<?= base_url($navItem->mm_link) ?>">
<a class="<?= $parentIsCurrent ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>"
href="<?= $parentLink !== '' ? base_url($parentLink) : '#' ?>">
<?= esc($navItem->mm_name) ?>
</a>
<?php if ($hasChildren): ?>
<div class="absolute left-0 top-full hidden group-hover:block bg-white border border-gray-200 rounded shadow-lg min-w-[10rem] z-30">
<?php /* 사이트 메뉴와 동일: 호버 끊김 방지 pt-1, 키보드 포커스, z-index */ ?>
<div class="absolute left-0 top-full z-50 hidden pt-1 min-w-[12rem] group-hover:block group-focus-within:block">
<div class="bg-white border border-gray-200 rounded shadow-lg py-1">
<?php foreach ($navItem->children as $child): ?>
<a href="<?= base_url($child->mm_link) ?>"
class="block px-3 py-1.5 text-sm text-gray-700 hover:bg-blue-50 whitespace-nowrap">
<?php
$childLink = menu_link_preferred_href_path($child->mm_link ?? null, $currentPath);
$childIsCurrent = $adminNavItemIsCurrent($child->mm_link ?? null);
?>
<?php if ($childLink !== ''): ?>
<a href="<?= base_url($childLink) ?>"
class="block px-3 py-1.5 text-sm hover:bg-blue-50 whitespace-nowrap <?= $childIsCurrent ? 'text-blue-700 font-semibold bg-blue-50' : 'text-gray-700' ?>">
<?= esc($child->mm_name) ?>
</a>
<?php else: ?>
<span class="block px-3 py-1.5 text-sm text-gray-400 cursor-default whitespace-nowrap" title="메뉴 관리에서 링크를 설정해 주세요">
<?= esc($child->mm_name) ?>
</span>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
@@ -123,19 +144,15 @@ body { overflow: hidden; }
<a class="<?= $isActive('select-local-government') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/select-local-government') ?>">지자체 전환</a>
<a class="<?= $isActive('local-governments') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/local-governments') ?>">지자체</a>
<?php endif; ?>
<a class="<?= $isActive('designated-shops') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/designated-shops') ?>">지정판매소</a>
<a class="<?= $isActive('designated-shops') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('bag/designated-shops') ?>">지정판매소</a>
<?php endif; ?>
</nav>
</div>
<div class="flex items-center gap-3">
<?php if ($effectiveLgName !== null): ?>
<span class="text-sm text-gray-600" title="현재 작업 지자체"><?= esc($effectiveLgName) ?></span>
<?php endif; ?>
<a href="<?= base_url('/') ?>" class="text-gray-500 hover:text-blue-600 text-sm">사이트</a>
<a href="<?= base_url('logout') ?>" class="text-gray-500 hover:text-red-600 transition-colors inline-block p-1 rounded hover:bg-red-50" title="로그아웃">
<svg class="h-5 w-5 inline" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/></svg> 종료
</a>
</div>
<?= view('components/header_user_tools', [
'userNav' => $userNav,
'effectiveLgName' => $effectiveLgName,
'showSiteLink' => true,
'showAdminLink' => false,
]) ?>
</header>
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
<?= esc($title ?? '관리자') ?>
@@ -155,7 +172,7 @@ body { overflow: hidden; }
<?= $content ?>
</main>
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 flex items-center justify-between shrink-0">
<span>쓰레기봉투 물류시스템 관리자</span>
<span>종량제 시스템 관리자</span>
<span><?= date('Y.m.d (D) g:i:sA') ?></span>
</footer>
</body>

View File

@@ -1,16 +1,17 @@
<?php
/**
* dense(정보 집약 표·KPI) + charts(Chart.js) 혼합 대시보드
* dense(표·KPI) + Chart.js 혼합 본문 `bag/layout/main` 삽입
*
* @var string $lgLabel
*/
$lgLabel = $lgLabel ?? '북구';
$mbName = session()->get('mb_name') ?? '담당자';
$dashHome = base_url('dashboard');
$dashBlend = base_url('dashboard/blend');
$dashClassic = base_url('dashboard/classic-mock');
$dashModern = base_url('dashboard/modern');
$dashDense = base_url('dashboard/dense');
$dashCharts = base_url('dashboard/charts');
$dashBlend = base_url('dashboard/blend');
$kpiTop = [
['icon' => 'fa-triangle-exclamation', 'c' => 'text-amber-700', 'bg' => 'bg-amber-50', 'v' => '3', 'l' => '재고부족', 'sub' => '품목'],
@@ -67,40 +68,26 @@ $notices = [
'봉투 단가 조정 예고 — 3/1 적용 예정 (안내문 배포 완료)',
];
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>종량제 시스템 종합·그래프 혼합</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
body {
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
.blend-dash-inner {
font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif;
font-size: 12px;
color: #333;
}
.nav-top a.nav-active {
color: #2b4c8c;
font-weight: 600;
border-bottom: 2px solid #2b4c8c;
padding-bottom: 2px;
margin-bottom: -2px;
}
.dense-table th, .dense-table td { padding: 0.25rem 0.4rem; line-height: 1.25; }
.dense-table thead th { font-size: 11px; font-weight: 600; color: #555; background: #f3f4f6; border-bottom: 1px solid #d1d5db; }
.dense-table tbody td { border-bottom: 1px solid #eee; font-size: 11px; }
.spark { display: flex; align-items: flex-end; gap: 2px; height: 36px; }
.spark span { flex: 1; background: linear-gradient(180deg, #3b82f6, #93c5fd); border-radius: 1px; min-width: 4px; }
.chart-card {
.blend-dash-inner .dense-table th, .blend-dash-inner .dense-table td { padding: 0.25rem 0.4rem; line-height: 1.25; }
.blend-dash-inner .dense-table thead th { font-size: 11px; font-weight: 600; color: #555; background: #f3f4f6; border-bottom: 1px solid #d1d5db; }
.blend-dash-inner .dense-table tbody td { border-bottom: 1px solid #eee; font-size: 11px; }
.blend-dash-inner .spark { display: flex; align-items: flex-end; gap: 2px; height: 36px; }
.blend-dash-inner .spark span { flex: 1; background: linear-gradient(180deg, #3b82f6, #93c5fd); border-radius: 1px; min-width: 4px; }
.blend-dash-inner .chart-card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 0.25rem;
box-shadow: 0 1px 2px rgba(0,0,0,.04);
}
.chart-card h2 {
.blend-dash-inner .chart-card h2 {
font-size: 11px;
font-weight: 700;
color: #1f2937;
@@ -108,70 +95,26 @@ $notices = [
border-bottom: 1px solid #f3f4f6;
background: #fafafa;
}
.chart-wrap { position: relative; height: 180px; padding: 0.4rem 0.5rem 0.5rem; }
.chart-wrap.tall { height: 260px; }
.chart-wrap.wide { height: 220px; }
</style>
</head>
<body class="bg-[#f0f2f5] flex flex-col min-h-screen">
<header class="border-b border-gray-300 bg-white shadow-sm shrink-0" data-purpose="top-navigation">
<div class="flex items-center justify-between px-3 py-1.5 gap-3 flex-wrap">
<div class="flex items-center gap-2 shrink-0">
<div class="flex items-center gap-2 text-green-700 font-bold text-base">
<i class="fa-solid fa-recycle text-lg"></i>
<span>종량제 시스템</span>
</div>
<span class="hidden sm:inline text-[11px] text-gray-500 border-l border-gray-300 pl-2">
<?= esc($lgLabel) ?> · <strong class="text-gray-700"><?= esc($mbName) ?></strong>님
</span>
</div>
<nav class="nav-top hidden lg:flex flex-wrap items-center gap-3 xl:gap-4 text-[13px] font-medium text-gray-700">
<a class="nav-active flex items-center gap-1 whitespace-nowrap" href="<?= esc($dashBlend) ?>">
<i class="fa-solid fa-gauge-high"></i> 업무 현황
</a>
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="#"><i class="fa-regular fa-file-lines"></i> 문서 관리</a>
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="#"><i class="fa-solid fa-box-open"></i> 규격</a>
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="#"><i class="fa-solid fa-bag-shopping"></i> 봉투 양식</a>
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="#"><i class="fa-solid fa-table"></i> 데이터 양식</a>
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="#"><i class="fa-solid fa-clock-rotate-left"></i> 사용 내역</a>
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="<?= base_url('bag/inventory-inquiry') ?>"><i class="fa-solid fa-boxes-stacked"></i> 재고 현황</a>
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="<?= base_url('bag/waste-suibal-enterprise') ?>"><i class="fa-solid fa-table-list"></i> 수불 현황</a>
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="#"><i class="fa-solid fa-chart-line"></i> 통계 분석</a>
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="#"><i class="fa-solid fa-gear"></i> 설정</a>
</nav>
<div class="flex items-center gap-1.5 shrink-0 text-[11px]">
<a href="<?= esc($dashClassic) ?>" class="text-[#2b4c8c] hover:underline whitespace-nowrap hidden md:inline">클래식</a>
<span class="text-gray-300 hidden md:inline">|</span>
<a href="<?= esc($dashModern) ?>" class="text-[#2b4c8c] hover:underline whitespace-nowrap hidden md:inline">모던</a>
<span class="text-gray-300 hidden md:inline">|</span>
<a href="<?= esc($dashDense) ?>" class="text-[#2b4c8c] hover:underline whitespace-nowrap hidden md:inline" title="정보 집약">종합</a>
<span class="text-gray-300 hidden md:inline">|</span>
<a href="<?= esc($dashCharts) ?>" class="text-[#2b4c8c] hover:underline whitespace-nowrap hidden md:inline" title="그래프만">차트</a>
<a href="<?= base_url('logout') ?>" class="text-gray-500 hover:text-gray-800 p-1 ml-1" title="로그아웃">
<i class="fa-solid fa-arrow-right-from-bracket"></i>
</a>
</div>
</div>
</header>
.blend-dash-inner .chart-wrap { position: relative; height: 180px; padding: 0.4rem 0.5rem 0.5rem; }
.blend-dash-inner .chart-wrap.tall { height: 260px; }
.blend-dash-inner .chart-wrap.wide { height: 220px; }
</style>
<div class="bg-gradient-to-r from-[#eff5fb] to-[#e8eef8] border-b border-gray-300 px-3 py-1 flex flex-wrap items-center justify-between gap-2 text-[11px] shrink-0">
<div class="blend-dash-inner bg-[#f0f2f5] -mx-4 -my-4 p-2 sm:p-3 min-h-full">
<div class="bg-gradient-to-r from-[#eff5fb] to-[#e8eef8] border border-gray-300 rounded-sm px-3 py-1 flex flex-wrap items-center justify-between gap-2 text-[11px] mb-2">
<span class="font-semibold text-gray-800">
<i class="fa-solid fa-layer-group text-[#2b4c8c] mr-1"></i>종합·그래프 혼합 현황
<span class="font-normal text-gray-500 ml-1">· dense /KPI + Chart.js</span>
<span class="font-normal text-gray-500 ml-1">· dense /KPI + Chart.js (목업)</span>
</span>
<div class="flex flex-wrap items-center gap-2 text-gray-600">
<span><i class="fa-regular fa-calendar mr-0.5"></i><?= date('Y-m-d (D) H:i') ?></span>
<span class="text-gray-300">|</span>
<span>기준지자체 <strong class="text-gray-800"><?= esc($lgLabel) ?></strong></span>
<button type="button" class="bg-[#2b4c8c] text-white px-2 py-0.5 rounded text-[11px]"><i class="fa-solid fa-rotate mr-0.5"></i>새로고침</button>
<span><?= esc($lgLabel) ?> · <strong class="text-gray-800"><?= esc($mbName) ?></strong></span>
<button type="button" class="bg-[#2b4c8c] text-white px-2 py-0.5 rounded text-[11px]" onclick="location.reload()"><i class="fa-solid fa-rotate mr-0.5"></i>새로고침</button>
</div>
</div>
<main class="flex-1 overflow-y-auto p-2 sm:p-3">
<?php if (session()->getFlashdata('success')): ?>
<div class="mb-2 p-2 rounded border border-green-200 bg-green-50 text-green-800 text-[11px]"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<div class="space-y-2">
<div class="mb-2 flex flex-wrap gap-2">
<?php foreach ($notices as $n): ?>
<div class="flex-1 min-w-[200px] flex items-center gap-2 bg-amber-50 border border-amber-200 text-amber-900 px-2 py-1 rounded text-[11px]">
@@ -196,7 +139,6 @@ $notices = [
<?php endforeach; ?>
</div>
<!-- dense: 재고 / 발주·신청 / 로그+스파크 -->
<div class="grid grid-cols-1 xl:grid-cols-12 gap-2 mb-2">
<section class="xl:col-span-4 bg-white border border-gray-200 rounded shadow-sm overflow-hidden">
<div class="px-2 py-1 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
@@ -295,7 +237,6 @@ $notices = [
</section>
</div>
<!-- charts: 요약 차트 4 (dense 아래) -->
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-2 mb-2">
<section class="chart-card">
<h2><i class="fa-solid fa-chart-pie text-[#2b4c8c] mr-1"></i>규격 출고 비중</h2>
@@ -432,13 +373,14 @@ $notices = [
</div>
</div>
<p class="text-center text-[10px] text-gray-400">
<strong class="text-gray-600">/dashboard/blend</strong> ( + 차트 혼합)
<p class="text-center text-[10px] text-gray-400 pb-1">
메인 <a href="<?= esc($dashHome) ?>" class="text-[#2b4c8c] hover:underline">/dashboard</a>
· 동일 본문 <a href="<?= esc($dashBlend) ?>" class="text-[#2b4c8c] hover:underline">/dashboard/blend</a>
· <a href="<?= esc($dashDense) ?>" class="text-[#2b4c8c] hover:underline">/dashboard/dense</a>
· <a href="<?= esc($dashCharts) ?>" class="text-[#2b4c8c] hover:underline">/dashboard/charts</a>
· <a href="<?= esc($dashClassic) ?>" class="text-[#2b4c8c] hover:underline">클래식</a>
</p>
</main>
</div>
<script>
(function () {
@@ -696,5 +638,4 @@ $notices = [
});
})();
</script>
</body>
</html>
</div>

View File

@@ -2,26 +2,24 @@
declare(strict_types=1);
helper('admin');
$siteNavTree = get_site_nav_tree();
$uriObj = service('request')->getUri();
$currentPath = trim((string) $uriObj->getPath(), '/');
if (str_starts_with($currentPath, 'index.php/')) {
$currentPath = substr($currentPath, strlen('index.php/'));
}
$currentPath = current_nav_request_path();
$mbLevel = (int) session()->get('mb_level');
$isAdmin = ($mbLevel === \Config\Roles::LEVEL_SUPER_ADMIN || $mbLevel === \Config\Roles::LEVEL_LOCAL_ADMIN);
$dashboardPathAliases = ['dashboard', 'dashboard/blend'];
$effectiveLgIdx = admin_effective_lg_idx();
$effectiveLgName = null;
if ($effectiveLgIdx) {
$lgRow = model(\App\Models\LocalGovernmentModel::class)->find($effectiveLgIdx);
$effectiveLgName = $lgRow ? $lgRow->lg_name : null;
}
$userNav = session_user_nav_display();
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title><?= esc($title ?? '쓰레기봉투 물류시스템') ?></title>
<title><?= esc($title ?? '종량제 시스템') ?></title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&amp;display=swap" rel="stylesheet"/>
<script>
@@ -64,32 +62,19 @@ body { overflow: hidden; }
</head>
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased select-none">
<!-- BEGIN: Top Navigation -->
<header class="relative bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0 z-50">
<header class="relative bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0 z-[100]">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<div class="w-6 h-6 flex items-center justify-center shrink-0">
<svg class="h-5 w-5" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" fill="#2563eb"/><rect x="2" y="2" width="7" height="7" fill="white"/><rect x="5" y="5" width="9" height="9" fill="white"/>
</svg>
</div>
<a href="<?= base_url() ?>" class="text-base font-semibold text-gray-800 tracking-tight hover:text-blue-600">쓰레기봉투 물류시스템</a>
</div>
<?= view('components/header_brand') ?>
</div>
<nav class="hidden md:flex gap-5 text-sm font-medium text-gray-600">
<?php if (! empty($siteNavTree)): ?>
<?php foreach ($siteNavTree as $navItem): ?>
<?php
$navLink = trim((string) $navItem->mm_link, '/');
$isActive = ($currentPath === $navLink);
$navLink = menu_link_preferred_href_path($navItem->mm_link ?? null, $currentPath);
$isActive = site_nav_link_matches_current($navItem->mm_link ?? null, $currentPath, $dashboardPathAliases);
if (! $isActive && ! empty($navItem->children)) {
foreach ($navItem->children as $ch) {
$childPath = trim((string) $ch->mm_link, '/');
if ($currentPath === $childPath) {
$isActive = true;
break;
}
// 기본코드 세부는 메뉴에 직접 링크 없음 → 기본코드관리(bag/code-kinds)와 동일 메뉴군으로 표시
if ($childPath === 'bag/code-kinds' && str_starts_with($currentPath, 'bag/code-details')) {
if (site_nav_link_matches_current($ch->mm_link ?? null, $currentPath, $dashboardPathAliases)) {
$isActive = true;
break;
}
@@ -98,20 +83,21 @@ body { overflow: hidden; }
?>
<div class="relative group">
<a class="<?= $isActive ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>"
href="<?= base_url($navItem->mm_link) ?>">
href="<?= $navLink !== '' ? base_url($navLink) : '#' ?>">
<?= esc($navItem->mm_name) ?>
</a>
<?php if (! empty($navItem->children)): ?>
<?php /* pt-1: 부모와 패널이 호버 끊김 방지. z-50: 제목 바보다 위 */ ?>
<div class="absolute left-0 top-full z-50 hidden pt-1 min-w-[12rem] group-hover:block group-focus-within:block">
<?php /* -mt-1 + pt-2: 부모 링크와 패널이 살짝 겹쳐 호버기지 않게 함. z-index: 드롭다운 클릭 우선 */ ?>
<div class="absolute left-0 top-full z-[200] -mt-1 pt-2 min-w-[12rem] hidden group-hover:block group-focus-within:block">
<div class="bg-white border border-gray-200 rounded shadow-lg py-1">
<?php foreach ($navItem->children as $child): ?>
<?php
$childLink = trim((string) ($child->mm_link ?? ''));
$childLink = menu_link_preferred_href_path($child->mm_link ?? null, $currentPath);
$childCurrent = menu_link_matches_request($child->mm_link ?? null, $currentPath, $dashboardPathAliases);
?>
<?php if ($childLink !== ''): ?>
<a href="<?= base_url($childLink) ?>"
class="block px-3 py-1.5 text-sm text-gray-700 hover:bg-blue-50 whitespace-nowrap">
class="block px-3 py-1.5 text-sm hover:bg-blue-50 whitespace-nowrap <?= $childCurrent ? 'text-blue-700 font-semibold bg-blue-50' : 'text-gray-700' ?>">
<?= esc($child->mm_name) ?>
</a>
<?php else: ?>
@@ -127,17 +113,12 @@ body { overflow: hidden; }
<?php endforeach; ?>
<?php endif; ?>
</nav>
<div class="flex items-center gap-2">
<?php if ($effectiveLgName !== null): ?>
<span class="text-sm text-gray-600" title="현재 작업 지자체"><?= esc($effectiveLgName) ?></span>
<?php endif; ?>
<?php if ($isAdmin): ?>
<a href="<?= base_url('admin') ?>" class="text-gray-500 hover:text-blue-600 text-sm">관리자</a>
<?php endif; ?>
<a href="<?= base_url('logout') ?>" class="text-gray-500 hover:text-red-600 transition-colors inline-block p-1 rounded hover:bg-red-50" title="로그아웃">
<svg class="h-5 w-5 inline" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/></svg> 종료
</a>
</div>
<?= view('components/header_user_tools', [
'userNav' => $userNav,
'effectiveLgName' => $effectiveLgName,
'showSiteLink' => false,
'showAdminLink' => $isAdmin,
]) ?>
</header>
<!-- END: Top Navigation -->
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
@@ -153,7 +134,7 @@ body { overflow: hidden; }
<?= $content ?>
</main>
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 flex items-center justify-between shrink-0">
<span>쓰레기봉투 물류시스템</span>
<span>종량제 시스템</span>
<span><?= date('Y.m.d (D) g:i:sA') ?></span>
</footer>
</body>

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
/** @var string $href Brand link target */
$href = $href ?? base_url();
/** @var string $linkClass Anchor + inner flex typography */
$linkClass = $linkClass ?? 'flex items-center gap-2 shrink-0 text-base font-semibold text-gray-800 tracking-tight hover:text-blue-600';
?>
<a href="<?= esc($href) ?>" class="<?= esc($linkClass, 'attr') ?>" title="종량제 시스템">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-6 w-6 text-blue-900 translate-y-[1px] shrink-0" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M9 3a1 1 0 00-1 1v1H5.75a.75.75 0 000 1.5h12.5a.75.75 0 000-1.5H16V4a1 1 0 00-1-1H9zm9 4H6v11a2 2 0 002 2h8a2 2 0 002-2V7zM10 9a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0110 9zm4 0a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0114 9z"/>
</svg>
<span class="whitespace-nowrap">종량제 시스템</span>
</a>

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/**
* 상단 헤더 오른쪽: 사용자·역할 · 작업 지자체 · (사이트|관리자) · 로그아웃
*
* @var array{name:string,role_label:string}|null $userNav
* @var string|null $effectiveLgName
* @var bool $showSiteLink 관리자 → 일반 사이트
* @var bool $showAdminLink 사이트 → 관리자 패널
*/
helper('admin');
if (! isset($userNav)) {
$userNav = session_user_nav_display();
}
$showSiteLink = ! empty($showSiteLink);
$showAdminLink = ! empty($showAdminLink);
?>
<div class="flex items-center gap-2 sm:gap-3 shrink-0">
<?php if ($userNav !== null): ?>
<div class="hidden sm:flex flex-col items-end leading-tight max-w-[11rem] border-r border-gray-200 pr-3 mr-1"
title="<?= esc($userNav['name'] . ' · ' . $userNav['role_label']) ?>">
<span class="text-sm font-medium text-gray-800 truncate"><?= esc($userNav['name']) ?></span>
<span class="text-xxs text-gray-500 truncate"><?= esc($userNav['role_label']) ?></span>
</div>
<div class="sm:hidden text-xs text-gray-600 max-w-[6rem] truncate" title="<?= esc($userNav['name'] . ' · ' . $userNav['role_label']) ?>">
<?= esc($userNav['name']) ?>
</div>
<?php endif; ?>
<?php if (! empty($effectiveLgName)): ?>
<span class="text-sm text-gray-600 max-w-[10rem] truncate" title="현재 작업 지자체"><?= esc((string) $effectiveLgName) ?></span>
<?php endif; ?>
<?php if ($showSiteLink): ?>
<a href="<?= base_url('/') ?>" class="text-sm text-gray-600 hover:text-blue-600 whitespace-nowrap px-0.5">사이트</a>
<?php endif; ?>
<?php if ($showAdminLink): ?>
<a href="<?= base_url('admin') ?>" class="text-sm text-gray-600 hover:text-blue-600 whitespace-nowrap px-0.5">관리자</a>
<?php endif; ?>
<a href="<?= base_url('logout') ?>"
class="flex items-center gap-1 text-sm text-gray-500 hover:text-red-600 transition-colors px-1.5 py-1 rounded-md hover:bg-red-50 whitespace-nowrap"
title="로그아웃">
<svg class="h-5 w-5 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>종료</span>
</a>
</div>

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* 종량제 시스템 — 미니멀 에코 마크 (링 + 잎)
*
* @var string $svgClass Tailwind classes for the SVG root
*/
$svgClass = $svgClass ?? 'h-6 w-6 shrink-0';
$gid = 'jrMark_' . bin2hex(random_bytes(4));
?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="<?= esc($svgClass, 'attr') ?>" aria-hidden="true" focusable="false">
<defs>
<linearGradient id="<?= esc($gid, 'attr') ?>_ring" x1="5" y1="5" x2="19" y2="19" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#10b981"/>
<stop offset="1" stop-color="#047857"/>
</linearGradient>
<linearGradient id="<?= esc($gid, 'attr') ?>_leaf" x1="12" y1="6" x2="12" y2="18.5" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#6ee7b7"/>
<stop offset="1" stop-color="#059669"/>
</linearGradient>
</defs>
<circle cx="12" cy="12" r="10.5" fill="#f7fefb"/>
<!-- 외곽 링: 우상단을 비우고 화살표로 순환감을 최소 요소로 표현 -->
<path fill="none"
stroke="url(#<?= esc($gid, 'attr') ?>_ring)"
stroke-width="1.8"
stroke-linecap="round"
d="M8.1 5.5a7.8 7.8 0 107.9 1.7"/>
<path d="M18.5 4.95l1.7 1.45-2.22.62z" fill="#059669"/>
<!-- 중앙 잎 -->
<path fill="url(#<?= esc($gid, 'attr') ?>_leaf)"
d="M12 5.7C9.55 7.35 8.75 10.25 10.95 14.95C11.35 15.8 11.7 16.45 12 16.95C12.3 16.45 12.65 15.8 13.05 14.95C15.25 10.25 14.45 7.35 12 5.7z"/>
<path stroke="#ecfdf5" stroke-width="0.65" stroke-linecap="round" fill="none" d="M11.95 7.55C11.85 10.2 11.95 12.45 12.05 15.35"/>
</svg>

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title> - 쓰레기봉투 물류시스템</title>
<title> - 종량제 시스템</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&amp;display=swap" rel="stylesheet"/>
<script>
@@ -25,14 +25,7 @@ tailwind.config = {
</head>
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased">
<header class="bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0">
<div class="flex items-center gap-2">
<div class="w-6 h-6 flex items-center justify-center shrink-0">
<svg class="h-5 w-5" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" fill="#2563eb"/><rect x="2" y="2" width="7" height="7" fill="white"/><rect x="5" y="5" width="9" height="9" fill="white"/>
</svg>
</div>
<a href="<?= base_url() ?>" class="text-base font-semibold text-gray-800 tracking-tight hover:text-blue-600">쓰레기봉투 물류시스템</a>
</div>
<?= view('components/header_brand') ?>
<a href="<?= base_url('logout') ?>" class="text-gray-500 hover:text-red-600 transition-colors inline-block p-1 rounded hover:bg-red-50" title="로그아웃">
<svg class="h-5 w-5 inline" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/></svg> 종료
</a>
@@ -51,6 +44,6 @@ tailwind.config = {
<a href="<?= base_url('logout') ?>" class="inline-block bg-btn-exit text-white px-4 py-2 rounded-sm text-sm shadow hover:bg-red-700 transition">로그아웃</a>
</section>
</main>
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 shrink-0">쓰레기봉투 물류시스템</footer>
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 shrink-0">종량제 시스템</footer>
</body>
</html>

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>쓰레기봉투 물류시스템</title>
<title>종량제 시스템</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&amp;display=swap" rel="stylesheet"/>
<script>
@@ -26,21 +26,14 @@ tailwind.config = {
</head>
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased">
<header class="bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0">
<div class="flex items-center gap-2">
<div class="w-6 h-6 flex items-center justify-center shrink-0">
<svg class="h-5 w-5" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" fill="#2563eb"/><rect x="2" y="2" width="7" height="7" fill="white"/><rect x="5" y="5" width="9" height="9" fill="white"/>
</svg>
</div>
<a href="<?= base_url() ?>" class="text-base font-semibold text-gray-800 tracking-tight hover:text-blue-600">쓰레기봉투 물류시스템</a>
</div>
<?= view('components/header_brand') ?>
<nav class="flex gap-4 text-sm font-medium text-gray-600">
<a class="hover:text-blue-600" href="<?= base_url('login') ?>">로그인</a>
<a class="hover:text-blue-600" href="<?= base_url('register') ?>">회원가입</a>
</nav>
</header>
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
쓰레기봉투 물류시스템
종량제 시스템
</div>
<main class="flex-grow bg-control-panel border-b border-gray-300 p-6 flex items-center justify-center">
<section class="text-center max-w-lg">
@@ -51,6 +44,6 @@ tailwind.config = {
</div>
</section>
</main>
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 shrink-0">쓰레기봉투 물류시스템</footer>
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 shrink-0">종량제 시스템</footer>
</body>
</html>