From 71edc1eb20a180789c232dca9e34e47b6fb02cf8 Mon Sep 17 00:00:00 2001 From: taekyoungc Date: Wed, 8 Apr 2026 15:22:24 +0900 Subject: [PATCH] feat: add designated shop detail and PII masking updates Rebase current admin changes on top of origin/main and exclude local artifacts from tracking to reduce push payload. Made-with: Cursor --- .gitignore | 3 + app/Config/Routes.php | 254 ++++++++++++---------- app/Controllers/Admin/Company.php | 89 ++++++-- app/Controllers/Admin/DesignatedShop.php | 39 +++- app/Controllers/Admin/FreeRecipient.php | 148 ++++++++++--- app/Controllers/Admin/Manager.php | 136 +++++++++--- app/Controllers/Admin/SalesAgency.php | 142 ++++++++---- app/Helpers/pii_mask_helper.php | 84 +++++++ app/Views/admin/company/index.php | 25 +++ app/Views/admin/designated_shop/index.php | 30 ++- app/Views/admin/designated_shop/map.php | 5 +- app/Views/admin/designated_shop/show.php | 94 ++++++++ app/Views/admin/free_recipient/index.php | 31 ++- app/Views/admin/manager/index.php | 29 ++- app/Views/admin/sales_agency/index.php | 45 ++-- e2e/new-features.spec.js | 20 ++ env | 78 ------- tests/unit/PiiMaskTest.php | 42 ++++ 18 files changed, 934 insertions(+), 360 deletions(-) create mode 100644 app/Helpers/pii_mask_helper.php create mode 100644 app/Views/admin/designated_shop/show.php delete mode 100644 env create mode 100644 tests/unit/PiiMaskTest.php diff --git a/.gitignore b/.gitignore index 7d9db00..030efdd 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ $RECYCLE.BIN/ # These should never be under version control, # as it poses a security risk. .env +env .env.local .env.*.local .env.production @@ -56,10 +57,12 @@ Vagrantfile # Local docs & MCP (저장소에 올리지 않음) #------------------------- docs/local/ +docs/ mcp-servers/ # Cursor MCP — API 키·로컬 경로 등 포함 가능 .cursor/mcp.json +.cursor/ #------------------------- # Secrets & credentials (보안) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index eb04c25..4787270 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -17,6 +17,14 @@ $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'); + +// 옛 주소 호환: 세부 목록만 사이트로 이동 +$routes->get('admin/code-details/(:num)', 'Admin\CodeDetail::index/$1'); $routes->get('bag/purchase-inbound', 'Bag::purchaseInbound'); $routes->get('bag/issue', 'Bag::issue'); $routes->get('bag/inventory', 'Bag::inventory'); @@ -35,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'); @@ -42,6 +51,112 @@ $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->post('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->post('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->post('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/show/(:num)', 'Admin\DesignatedShop::show/$1'); + $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'); @@ -63,6 +178,7 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo $routes->post('users/store', 'Admin\User::store'); $routes->get('users/edit/(:num)', 'Admin\User::edit/$1'); $routes->post('users/update/(:num)', 'Admin\User::update/$1'); + $routes->post('users/unlock-login/(:num)', 'Admin\User::unlockLogin/$1'); $routes->post('users/delete/(:num)', 'Admin\User::delete/$1'); $routes->get('access/login-history', 'Admin\Access::loginHistory'); $routes->get('access/approvals', 'Admin\Access::approvals'); @@ -84,12 +200,7 @@ $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', 'Admin\CodeKind::index'); + // 기본코드 종류 관리 (P2-01) — 등록·수정·삭제는 관리자 전용 $routes->get('code-kinds/create', 'Admin\CodeKind::create'); $routes->post('code-kinds/store', 'Admin\CodeKind::store'); $routes->get('code-kinds/edit/(:num)', 'Admin\CodeKind::edit/$1'); @@ -97,119 +208,32 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo $routes->post('code-kinds/delete/(:num)', 'Admin\CodeKind::delete/$1'); // 세부코드 관리 (P2-02) - $routes->get('code-details/(:num)', 'Admin\CodeDetail::index/$1'); $routes->get('code-details/(:num)/create', 'Admin\CodeDetail::create/$1'); $routes->post('code-details/store', 'Admin\CodeDetail::store'); $routes->get('code-details/edit/(:num)', 'Admin\CodeDetail::edit/$1'); $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'); + } }); diff --git a/app/Controllers/Admin/Company.php b/app/Controllers/Admin/Company.php index 36d22ee..f3cefcb 100644 --- a/app/Controllers/Admin/Company.php +++ b/app/Controllers/Admin/Company.php @@ -18,25 +18,68 @@ class Company extends BaseController { helper('admin'); $lgIdx = admin_effective_lg_idx(); - if (!$lgIdx) { - return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); + if (! $lgIdx) { + return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.'); } - $list = $this->model->where('cp_lg_idx', $lgIdx)->orderBy('cp_idx', 'DESC')->paginate(20); - $pager = $this->model->pager; + if ($this->request->is('post')) { + $searchField = trim((string) ($this->request->getPost('search_field') ?? '')); + $searchQuery = trim((string) ($this->request->getPost('search_query') ?? '')); + session()->setFlashdata('company_search', [ + 'search_field' => $searchField, + 'search_query' => $searchQuery, + ]); - return view('admin/layout', [ - 'title' => '업체 관리', - 'content' => view('admin/company/index', ['list' => $list, 'pager' => $pager]), + return redirect()->to(mgmt_url('companies')); + } + + $fromGetField = trim((string) ($this->request->getGet('search_field') ?? '')); + $fromGetQuery = trim((string) ($this->request->getGet('search_query') ?? '')); + $flash = session()->getFlashdata('company_search'); + if ($fromGetField !== '' || $fromGetQuery !== '') { + $searchField = $fromGetField; + $searchQuery = $fromGetQuery; + } elseif (is_array($flash)) { + $searchField = trim((string) ($flash['search_field'] ?? '')); + $searchQuery = trim((string) ($flash['search_query'] ?? '')); + } else { + $searchField = ''; + $searchQuery = ''; + } + + $allowedFields = ['cp_idx', 'cp_type', 'cp_name', 'cp_biz_no', 'cp_rep_name', 'cp_tel', 'cp_addr']; + if (! in_array($searchField, $allowedFields, true)) { + $searchField = 'cp_name'; + } + + $builder = $this->model->where('cp_lg_idx', $lgIdx); + if ($searchQuery !== '') { + if ($searchField === 'cp_idx') { + if (ctype_digit($searchQuery)) { + $builder->where('cp_idx', (int) $searchQuery); + } else { + $builder->where('cp_idx', 0); + } + } else { + $builder->like($searchField, $searchQuery); + } + } + + $list = $builder->orderBy('cp_idx', 'DESC')->paginate(20); + $pager = $this->model->pager; + $pager->setPath('bag/companies'); + + return $this->renderWorkPage('업체 관리', 'admin/company/index', [ + 'list' => $list, + 'pager' => $pager, + 'search_field' => $searchField, + 'search_query' => $searchQuery, ]); } public function create() { - return view('admin/layout', [ - 'title' => '업체 등록', - 'content' => view('admin/company/create'), - ]); + return $this->renderWorkPage('업체 등록', 'admin/company/create'); } public function store() @@ -66,29 +109,26 @@ class Company extends BaseController 'cp_regdate' => date('Y-m-d H:i:s'), ]); - return redirect()->to(site_url('admin/companies'))->with('success', '업체가 등록되었습니다.'); + return redirect()->to(mgmt_url('companies'))->with('success', '업체가 등록되었습니다.'); } public function edit(int $id) { helper('admin'); $item = $this->model->find($id); - if (!$item || (int) $item->cp_lg_idx !== admin_effective_lg_idx()) { - return redirect()->to(site_url('admin/companies'))->with('error', '업체를 찾을 수 없습니다.'); + if (! $item || (int) $item->cp_lg_idx !== admin_effective_lg_idx()) { + return redirect()->to(mgmt_url('companies'))->with('error', '업체를 찾을 수 없습니다.'); } - return view('admin/layout', [ - 'title' => '업체 수정', - 'content' => view('admin/company/edit', ['item' => $item]), - ]); + return $this->renderWorkPage('업체 수정', 'admin/company/edit', ['item' => $item]); } public function update(int $id) { helper('admin'); $item = $this->model->find($id); - if (!$item || (int) $item->cp_lg_idx !== admin_effective_lg_idx()) { - return redirect()->to(site_url('admin/companies'))->with('error', '업체를 찾을 수 없습니다.'); + if (! $item || (int) $item->cp_lg_idx !== admin_effective_lg_idx()) { + return redirect()->to(mgmt_url('companies'))->with('error', '업체를 찾을 수 없습니다.'); } $rules = [ @@ -110,18 +150,19 @@ class Company extends BaseController 'cp_state' => (int) $this->request->getPost('cp_state'), ]); - return redirect()->to(site_url('admin/companies'))->with('success', '업체가 수정되었습니다.'); + return redirect()->to(mgmt_url('companies'))->with('success', '업체가 수정되었습니다.'); } public function delete(int $id) { helper('admin'); $item = $this->model->find($id); - if (!$item || (int) $item->cp_lg_idx !== admin_effective_lg_idx()) { - return redirect()->to(site_url('admin/companies'))->with('error', '업체를 찾을 수 없습니다.'); + if (! $item || (int) $item->cp_lg_idx !== admin_effective_lg_idx()) { + return redirect()->to(mgmt_url('companies'))->with('error', '업체를 찾을 수 없습니다.'); } $this->model->delete($id); - return redirect()->to(site_url('admin/companies'))->with('success', '업체가 삭제되었습니다.'); + + return redirect()->to(mgmt_url('companies'))->with('success', '업체가 삭제되었습니다.'); } } diff --git a/app/Controllers/Admin/DesignatedShop.php b/app/Controllers/Admin/DesignatedShop.php index 32c3649..1d3fa48 100644 --- a/app/Controllers/Admin/DesignatedShop.php +++ b/app/Controllers/Admin/DesignatedShop.php @@ -89,7 +89,7 @@ class DesignatedShop extends BaseController public function export() { - helper(['admin', 'export']); + helper(['admin', 'export', 'pii_mask']); $lgIdx = admin_effective_lg_idx(); if (!$lgIdx) { return redirect()->to(site_url('admin/designated-shops'))->with('error', '지자체를 선택해 주세요.'); @@ -104,7 +104,7 @@ class DesignatedShop extends BaseController $row->ds_idx, $row->ds_shop_no, $row->ds_name, - $row->ds_rep_name, + mask_person_name($row->ds_rep_name ?? null), $row->ds_biz_no, $row->ds_va_number, $row->ds_tel ?? '', @@ -121,6 +121,41 @@ class DesignatedShop extends BaseController ); } + /** + * 지정판매소 상세 (읽기 전용, 목록에서 연결) + */ + public function show(int $id) + { + helper('admin'); + $lgIdx = admin_effective_lg_idx(); + if ($lgIdx === null || $lgIdx <= 0) { + return redirect()->to(work_area_home_url()) + ->with('error', '작업할 지자체가 선택되지 않았습니다. 지자체를 선택해 주세요.'); + } + + $shop = $this->shopModel->find($id); + if ($shop === null || (int) $shop->ds_lg_idx !== $lgIdx) { + return redirect()->to(mgmt_url('designated-shops')) + ->with('error', '해당 지정판매소를 찾을 수 없습니다.'); + } + + $currentLg = $this->lgModel->find($lgIdx); + + $stateLabel = match ((int) $shop->ds_state) { + 1 => '정상', + 2 => '폐업', + 3 => '직권해지', + default => (string) $shop->ds_state, + }; + + return $this->renderWorkPage('지정판매소 정보', 'admin/designated_shop/show', [ + 'shop' => $shop, + 'currentLg' => $currentLg, + 'stateLabel' => $stateLabel, + 'can_edit' => $this->isSuperAdmin() || $this->isLocalAdmin(), + ]); + } + /** * 지정판매소 등록 폼 (효과 지자체 기준) */ diff --git a/app/Controllers/Admin/FreeRecipient.php b/app/Controllers/Admin/FreeRecipient.php index 79448ce..6f4f8cc 100644 --- a/app/Controllers/Admin/FreeRecipient.php +++ b/app/Controllers/Admin/FreeRecipient.php @@ -18,35 +18,123 @@ class FreeRecipient extends BaseController private function getCodeOptions(string $ckCode): array { - $kind = model(CodeKindModel::class)->where('ck_code', $ckCode)->first(); - return $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true) : []; + helper('admin'); + $lgIdx = admin_effective_lg_idx(); + $kind = model(CodeKindModel::class)->where('ck_code', $ckCode)->first(); + + return $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : []; } public function index() { helper('admin'); $lgIdx = admin_effective_lg_idx(); - if (!$lgIdx) { - return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); + if (! $lgIdx) { + return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.'); } - $list = $this->model->where('fr_lg_idx', $lgIdx)->orderBy('fr_idx', 'DESC')->paginate(20); - $pager = $this->model->pager; + if ($this->request->is('post')) { + $searchField = trim((string) ($this->request->getPost('search_field') ?? '')); + $searchQuery = trim((string) ($this->request->getPost('search_query') ?? '')); + session()->setFlashdata('free_recipient_search', [ + 'search_field' => $searchField, + 'search_query' => $searchQuery, + ]); - return view('admin/layout', [ - 'title' => '무료용 대상자 관리', - 'content' => view('admin/free_recipient/index', ['list' => $list, 'pager' => $pager]), + return redirect()->to(mgmt_url('free-recipients')); + } + + $fromGetField = trim((string) ($this->request->getGet('search_field') ?? '')); + $fromGetQuery = trim((string) ($this->request->getGet('search_query') ?? '')); + $flash = session()->getFlashdata('free_recipient_search'); + if ($fromGetField !== '' || $fromGetQuery !== '') { + $searchField = $fromGetField; + $searchQuery = $fromGetQuery; + } elseif (is_array($flash)) { + $searchField = trim((string) ($flash['search_field'] ?? '')); + $searchQuery = trim((string) ($flash['search_query'] ?? '')); + } else { + $searchField = ''; + $searchQuery = ''; + } + + $allowedFields = ['fr_idx', 'fr_type_code', 'fr_name', 'fr_phone', 'fr_addr', 'fr_dong_code', 'fr_note']; + if (! in_array($searchField, $allowedFields, true)) { + $searchField = 'fr_name'; + } + + $typeCodes = $this->getCodeOptions('H'); + $typeCodeMap = []; + foreach ($typeCodes as $cd) { + $typeCodeMap[(string) $cd->cd_code] = (string) $cd->cd_name; + } + + $dongCodes = $this->getCodeOptions('D'); + $dongCodeMap = []; + foreach ($dongCodes as $cd) { + $dongCodeMap[(string) $cd->cd_code] = (string) $cd->cd_name; + } + + $builder = $this->model->where('fr_lg_idx', $lgIdx); + if ($searchQuery !== '') { + if ($searchField === 'fr_idx') { + if (ctype_digit($searchQuery)) { + $builder->where('fr_idx', (int) $searchQuery); + } else { + $builder->where('fr_idx', 0); + } + } elseif ($searchField === 'fr_type_code') { + $codes = []; + foreach ($typeCodes as $cd) { + $code = (string) ($cd->cd_code ?? ''); + $name = (string) ($cd->cd_name ?? ''); + if ($code !== '' && (stripos($code, $searchQuery) !== false || stripos($name, $searchQuery) !== false)) { + $codes[] = $code; + } + } + if ($codes === []) { + $builder->where('fr_idx', 0); + } else { + $builder->whereIn('fr_type_code', array_values(array_unique($codes))); + } + } elseif ($searchField === 'fr_dong_code') { + $codes = []; + foreach ($dongCodes as $cd) { + $code = (string) ($cd->cd_code ?? ''); + $name = (string) ($cd->cd_name ?? ''); + if ($code !== '' && (stripos($code, $searchQuery) !== false || stripos($name, $searchQuery) !== false)) { + $codes[] = $code; + } + } + if ($codes === []) { + $builder->where('fr_idx', 0); + } else { + $builder->whereIn('fr_dong_code', array_values(array_unique($codes))); + } + } else { + $builder->like($searchField, $searchQuery); + } + } + + $list = $builder->orderBy('fr_idx', 'DESC')->paginate(20); + $pager = $this->model->pager; + $pager->setPath('bag/free-recipients'); + + return $this->renderWorkPage('무료용 대상자 관리', 'admin/free_recipient/index', [ + 'list' => $list, + 'pager' => $pager, + 'search_field' => $searchField, + 'search_query' => $searchQuery, + 'type_code_map' => $typeCodeMap, + 'dong_code_map' => $dongCodeMap, ]); } public function create() { - return view('admin/layout', [ - 'title' => '무료용 대상자 등록', - 'content' => view('admin/free_recipient/create', [ - 'typeCodes' => $this->getCodeOptions('H'), - 'dongCodes' => $this->getCodeOptions('D'), - ]), + return $this->renderWorkPage('무료용 대상자 등록', 'admin/free_recipient/create', [ + 'typeCodes' => $this->getCodeOptions('H'), + 'dongCodes' => $this->getCodeOptions('D'), ]); } @@ -75,24 +163,21 @@ class FreeRecipient extends BaseController 'fr_regdate' => date('Y-m-d H:i:s'), ]); - return redirect()->to(site_url('admin/free-recipients'))->with('success', '무료용 대상자가 등록되었습니다.'); + return redirect()->to(mgmt_url('free-recipients'))->with('success', '무료용 대상자가 등록되었습니다.'); } public function edit(int $id) { helper('admin'); $item = $this->model->find($id); - if (!$item || (int) $item->fr_lg_idx !== admin_effective_lg_idx()) { - return redirect()->to(site_url('admin/free-recipients'))->with('error', '대상자를 찾을 수 없습니다.'); + if (! $item || (int) $item->fr_lg_idx !== admin_effective_lg_idx()) { + return redirect()->to(mgmt_url('free-recipients'))->with('error', '대상자를 찾을 수 없습니다.'); } - return view('admin/layout', [ - 'title' => '무료용 대상자 수정', - 'content' => view('admin/free_recipient/edit', [ - 'item' => $item, - 'typeCodes' => $this->getCodeOptions('H'), - 'dongCodes' => $this->getCodeOptions('D'), - ]), + return $this->renderWorkPage('무료용 대상자 수정', 'admin/free_recipient/edit', [ + 'item' => $item, + 'typeCodes' => $this->getCodeOptions('H'), + 'dongCodes' => $this->getCodeOptions('D'), ]); } @@ -100,8 +185,8 @@ class FreeRecipient extends BaseController { helper('admin'); $item = $this->model->find($id); - if (!$item || (int) $item->fr_lg_idx !== admin_effective_lg_idx()) { - return redirect()->to(site_url('admin/free-recipients'))->with('error', '대상자를 찾을 수 없습니다.'); + if (! $item || (int) $item->fr_lg_idx !== admin_effective_lg_idx()) { + return redirect()->to(mgmt_url('free-recipients'))->with('error', '대상자를 찾을 수 없습니다.'); } $rules = [ @@ -123,18 +208,19 @@ class FreeRecipient extends BaseController 'fr_state' => (int) $this->request->getPost('fr_state'), ]); - return redirect()->to(site_url('admin/free-recipients'))->with('success', '무료용 대상자가 수정되었습니다.'); + return redirect()->to(mgmt_url('free-recipients'))->with('success', '무료용 대상자가 수정되었습니다.'); } public function delete(int $id) { helper('admin'); $item = $this->model->find($id); - if (!$item || (int) $item->fr_lg_idx !== admin_effective_lg_idx()) { - return redirect()->to(site_url('admin/free-recipients'))->with('error', '대상자를 찾을 수 없습니다.'); + if (! $item || (int) $item->fr_lg_idx !== admin_effective_lg_idx()) { + return redirect()->to(mgmt_url('free-recipients'))->with('error', '대상자를 찾을 수 없습니다.'); } $this->model->delete($id); - return redirect()->to(site_url('admin/free-recipients'))->with('success', '무료용 대상자가 삭제되었습니다.'); + + return redirect()->to(mgmt_url('free-recipients'))->with('success', '무료용 대상자가 삭제되었습니다.'); } } diff --git a/app/Controllers/Admin/Manager.php b/app/Controllers/Admin/Manager.php index c3bce32..1008529 100644 --- a/app/Controllers/Admin/Manager.php +++ b/app/Controllers/Admin/Manager.php @@ -18,8 +18,11 @@ class Manager extends BaseController private function getCodeOptions(string $ckCode): array { - $kind = model(CodeKindModel::class)->where('ck_code', $ckCode)->first(); - return $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true) : []; + helper('admin'); + $lgIdx = admin_effective_lg_idx(); + $kind = model(CodeKindModel::class)->where('ck_code', $ckCode)->first(); + + return $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : []; } public function index() @@ -27,32 +30,105 @@ class Manager extends BaseController helper('admin'); $lgIdx = admin_effective_lg_idx(); if (!$lgIdx) { - return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); + helper('admin'); + + return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.'); } - $list = $this->model->where('mg_lg_idx', $lgIdx)->orderBy('mg_idx', 'DESC')->paginate(20); - $pager = $this->model->pager; + if ($this->request->is('post')) { + $searchField = trim((string) ($this->request->getPost('search_field') ?? '')); + $searchQuery = trim((string) ($this->request->getPost('search_query') ?? '')); + session()->setFlashdata('manager_search', [ + 'search_field' => $searchField, + 'search_query' => $searchQuery, + ]); - return view('admin/layout', [ - 'title' => '담당자 관리', - 'content' => view('admin/manager/index', ['list' => $list, 'pager' => $pager]), + return redirect()->to(mgmt_url('managers')); + } + + $fromGetField = trim((string) ($this->request->getGet('search_field') ?? '')); + $fromGetQuery = trim((string) ($this->request->getGet('search_query') ?? '')); + $flash = session()->getFlashdata('manager_search'); + if ($fromGetField !== '' || $fromGetQuery !== '') { + $searchField = $fromGetField; + $searchQuery = $fromGetQuery; + } elseif (is_array($flash)) { + $searchField = trim((string) ($flash['search_field'] ?? '')); + $searchQuery = trim((string) ($flash['search_query'] ?? '')); + } else { + $searchField = ''; + $searchQuery = ''; + } + + $allowedFields = ['mg_idx', 'mg_name', 'mg_dept_code', 'mg_position_code', 'mg_tel', 'mg_phone', 'mg_email']; + if (! in_array($searchField, $allowedFields, true)) { + $searchField = 'mg_name'; + } + + $deptCodes = $this->getCodeOptions('S'); + $posCodes = $this->getCodeOptions('T'); + $deptCodeMap = []; + foreach ($deptCodes as $cd) { + $deptCodeMap[(string) $cd->cd_code] = (string) $cd->cd_name; + } + $posCodeMap = []; + foreach ($posCodes as $cd) { + $posCodeMap[(string) $cd->cd_code] = (string) $cd->cd_name; + } + + $builder = $this->model->where('mg_lg_idx', $lgIdx); + if ($searchQuery !== '') { + if ($searchField === 'mg_idx') { + if (ctype_digit($searchQuery)) { + $builder->where('mg_idx', (int) $searchQuery); + } else { + $builder->where('mg_idx', 0); + } + } elseif ($searchField === 'mg_dept_code' || $searchField === 'mg_position_code') { + $sourceList = $searchField === 'mg_dept_code' ? $deptCodes : $posCodes; + $codes = []; + foreach ($sourceList as $cd) { + $code = (string) ($cd->cd_code ?? ''); + $name = (string) ($cd->cd_name ?? ''); + if ($code !== '' && (stripos($code, $searchQuery) !== false || stripos($name, $searchQuery) !== false)) { + $codes[] = $code; + } + } + if ($codes === []) { + $builder->where('mg_idx', 0); + } else { + $builder->whereIn($searchField, array_values(array_unique($codes))); + } + } else { + $builder->like($searchField, $searchQuery); + } + } + + $list = $builder->orderBy('mg_idx', 'DESC')->paginate(20); + $pager = $this->model->pager; + $pager->setPath('bag/managers'); + + return $this->renderWorkPage('담당자 관리', 'admin/manager/index', [ + 'list' => $list, + 'pager' => $pager, + 'search_field' => $searchField, + 'search_query' => $searchQuery, + 'dept_code_map' => $deptCodeMap, + 'pos_code_map' => $posCodeMap, ]); } public function create() { - return view('admin/layout', [ - 'title' => '담당자 등록', - 'content' => view('admin/manager/create', [ - 'deptCodes' => $this->getCodeOptions('S'), - 'positionCodes' => $this->getCodeOptions('T'), - ]), + return $this->renderWorkPage('담당자 등록', 'admin/manager/create', [ + 'deptCodes' => $this->getCodeOptions('S'), + 'positionCodes' => $this->getCodeOptions('T'), ]); } public function store() { - helper('admin'); + helper(['admin', 'url']); $rules = [ 'mg_name' => 'required|max_length[50]', 'mg_tel' => 'permit_empty|max_length[20]', @@ -75,33 +151,30 @@ class Manager extends BaseController 'mg_regdate' => date('Y-m-d H:i:s'), ]); - return redirect()->to(site_url('admin/managers'))->with('success', '담당자가 등록되었습니다.'); + return redirect()->to(mgmt_url('managers'))->with('success', '담당자가 등록되었습니다.'); } public function edit(int $id) { - helper('admin'); + helper(['admin', 'url']); $item = $this->model->find($id); if (!$item || (int) $item->mg_lg_idx !== admin_effective_lg_idx()) { - return redirect()->to(site_url('admin/managers'))->with('error', '담당자를 찾을 수 없습니다.'); + return redirect()->to(mgmt_url('managers'))->with('error', '담당자를 찾을 수 없습니다.'); } - return view('admin/layout', [ - 'title' => '담당자 수정', - 'content' => view('admin/manager/edit', [ - 'item' => $item, - 'deptCodes' => $this->getCodeOptions('S'), - 'positionCodes' => $this->getCodeOptions('T'), - ]), + return $this->renderWorkPage('담당자 수정', 'admin/manager/edit', [ + 'item' => $item, + 'deptCodes' => $this->getCodeOptions('S'), + 'positionCodes' => $this->getCodeOptions('T'), ]); } public function update(int $id) { - helper('admin'); + helper(['admin', 'url']); $item = $this->model->find($id); if (!$item || (int) $item->mg_lg_idx !== admin_effective_lg_idx()) { - return redirect()->to(site_url('admin/managers'))->with('error', '담당자를 찾을 수 없습니다.'); + return redirect()->to(mgmt_url('managers'))->with('error', '담당자를 찾을 수 없습니다.'); } $rules = [ @@ -122,18 +195,19 @@ class Manager extends BaseController 'mg_state' => (int) $this->request->getPost('mg_state'), ]); - return redirect()->to(site_url('admin/managers'))->with('success', '담당자가 수정되었습니다.'); + return redirect()->to(mgmt_url('managers'))->with('success', '담당자가 수정되었습니다.'); } public function delete(int $id) { - helper('admin'); + helper(['admin', 'url']); $item = $this->model->find($id); if (!$item || (int) $item->mg_lg_idx !== admin_effective_lg_idx()) { - return redirect()->to(site_url('admin/managers'))->with('error', '담당자를 찾을 수 없습니다.'); + return redirect()->to(mgmt_url('managers'))->with('error', '담당자를 찾을 수 없습니다.'); } $this->model->delete($id); - return redirect()->to(site_url('admin/managers'))->with('success', '담당자가 삭제되었습니다.'); + + return redirect()->to(mgmt_url('managers'))->with('success', '담당자가 삭제되었습니다.'); } } diff --git a/app/Controllers/Admin/SalesAgency.php b/app/Controllers/Admin/SalesAgency.php index 08befa7..ee396e0 100644 --- a/app/Controllers/Admin/SalesAgency.php +++ b/app/Controllers/Admin/SalesAgency.php @@ -1,5 +1,7 @@ to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); + if (! $lgIdx) { + return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.'); } - $list = $this->model->where('sa_lg_idx', $lgIdx)->orderBy('sa_idx', 'DESC')->paginate(20); - $pager = $this->model->pager; + $searchField = trim((string) ($this->request->getGet('search_field') ?? '')); + $searchQuery = trim((string) ($this->request->getGet('search_query') ?? '')); + $allowedFields = ['sa_idx', 'sa_kind', 'sa_code', 'sa_name']; + if (! in_array($searchField, $allowedFields, true)) { + $searchField = 'sa_name'; + } - return view('admin/layout', [ - 'title' => '판매 대행소 관리', - 'content' => view('admin/sales_agency/index', ['list' => $list, 'pager' => $pager]), + $builder = $this->model->where('sa_lg_idx', $lgIdx); + if ($searchQuery !== '') { + if ($searchField === 'sa_idx') { + if (ctype_digit($searchQuery)) { + $builder->where('sa_idx', (int) $searchQuery); + } else { + // 번호 검색은 숫자만 허용한다. + $builder->where('sa_idx', 0); + } + } else { + $builder->like($searchField, $searchQuery); + } + } + + $list = $builder->orderBy('sa_idx', 'DESC')->paginate(20); + $pager = $this->model->pager; + // 전체 URL·쿼리를 setPath에 넣으면 Pager URI 경로가 깨져 404가 난다. 경로만 지정한다(필터는 현재 GET이 병합됨). + $pager->setPath('bag/sales-agencies'); + + return $this->renderWorkPage('판매 대행소 관리', 'admin/sales_agency/index', [ + 'list' => $list, + 'pager' => $pager, + 'search_field' => $searchField, + 'search_query' => $searchQuery, ]); } public function create() { - return view('admin/layout', [ - 'title' => '판매 대행소 등록', - 'content' => view('admin/sales_agency/create'), - ]); + helper('admin'); + if (! admin_effective_lg_idx()) { + return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.'); + } + + return $this->renderWorkPage('판매 대행소 등록', 'admin/sales_agency/create'); } public function store() { helper('admin'); + $lgIdx = admin_effective_lg_idx(); + if (! $lgIdx) { + return redirect()->to(mgmt_url('sales-agencies'))->with('error', '지자체를 선택해 주세요.'); + } + + if (! $this->model->hasKindCodeColumns()) { + return redirect()->back()->withInput()->with('error', self::SCHEMA_ERROR); + } + $rules = [ - 'sa_name' => 'required|max_length[100]', - 'sa_biz_no' => 'permit_empty|max_length[20]', - 'sa_rep_name' => 'permit_empty|max_length[50]', - 'sa_tel' => 'permit_empty|max_length[20]', - 'sa_addr' => 'permit_empty|max_length[255]', + 'sa_kind' => 'required|max_length[50]', + 'sa_code' => 'required|max_length[50]', + 'sa_name' => 'required|max_length[100]', ]; if (! $this->validate($rules)) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } + $code = trim((string) $this->request->getPost('sa_code')); + if ($this->model->where('sa_lg_idx', $lgIdx)->where('sa_code', $code)->first() !== null) { + return redirect()->back()->withInput()->with('error', '동일 지자체에 같은 대행소 코드가 이미 있습니다.'); + } + $this->model->insert([ - 'sa_lg_idx' => admin_effective_lg_idx(), - 'sa_name' => $this->request->getPost('sa_name'), - 'sa_biz_no' => $this->request->getPost('sa_biz_no') ?? '', - 'sa_rep_name' => $this->request->getPost('sa_rep_name') ?? '', - 'sa_tel' => $this->request->getPost('sa_tel') ?? '', - 'sa_addr' => $this->request->getPost('sa_addr') ?? '', - 'sa_state' => 1, - 'sa_regdate' => date('Y-m-d H:i:s'), + 'sa_lg_idx' => $lgIdx, + 'sa_kind' => trim((string) $this->request->getPost('sa_kind')), + 'sa_code' => $code, + 'sa_name' => trim((string) $this->request->getPost('sa_name')), + 'sa_regdate' => date('Y-m-d H:i:s'), ]); - return redirect()->to(site_url('admin/sales-agencies'))->with('success', '판매 대행소가 등록되었습니다.'); + return redirect()->to(mgmt_url('sales-agencies'))->with('success', '판매 대행소가 등록되었습니다.'); } public function edit(int $id) { helper('admin'); $item = $this->model->find($id); - if (!$item || (int) $item->sa_lg_idx !== admin_effective_lg_idx()) { - return redirect()->to(site_url('admin/sales-agencies'))->with('error', '대행소를 찾을 수 없습니다.'); + if (! $item || (int) $item->sa_lg_idx !== admin_effective_lg_idx()) { + return redirect()->to(mgmt_url('sales-agencies'))->with('error', '대행소를 찾을 수 없습니다.'); } - return view('admin/layout', [ - 'title' => '판매 대행소 수정', - 'content' => view('admin/sales_agency/edit', ['item' => $item]), - ]); + return $this->renderWorkPage('판매 대행소 수정', 'admin/sales_agency/edit', ['item' => $item]); } public function update(int $id) { helper('admin'); - $item = $this->model->find($id); - if (!$item || (int) $item->sa_lg_idx !== admin_effective_lg_idx()) { - return redirect()->to(site_url('admin/sales-agencies'))->with('error', '대행소를 찾을 수 없습니다.'); + $lgIdx = admin_effective_lg_idx(); + $item = $this->model->find($id); + if (! $item || ! $lgIdx || (int) $item->sa_lg_idx !== $lgIdx) { + return redirect()->to(mgmt_url('sales-agencies'))->with('error', '대행소를 찾을 수 없습니다.'); + } + + if (! $this->model->hasKindCodeColumns()) { + return redirect()->back()->withInput()->with('error', self::SCHEMA_ERROR); } $rules = [ - 'sa_name' => 'required|max_length[100]', - 'sa_state' => 'required|in_list[0,1]', + 'sa_kind' => 'required|max_length[50]', + 'sa_code' => 'required|max_length[50]', + 'sa_name' => 'required|max_length[100]', ]; if (! $this->validate($rules)) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } + $code = trim((string) $this->request->getPost('sa_code')); + $dup = $this->model->where('sa_lg_idx', $lgIdx)->where('sa_code', $code)->where('sa_idx !=', $id)->first(); + if ($dup !== null) { + return redirect()->back()->withInput()->with('error', '동일 지자체에 같은 대행소 코드가 이미 있습니다.'); + } + $this->model->update($id, [ - 'sa_name' => $this->request->getPost('sa_name'), - 'sa_biz_no' => $this->request->getPost('sa_biz_no') ?? '', - 'sa_rep_name' => $this->request->getPost('sa_rep_name') ?? '', - 'sa_tel' => $this->request->getPost('sa_tel') ?? '', - 'sa_addr' => $this->request->getPost('sa_addr') ?? '', - 'sa_state' => (int) $this->request->getPost('sa_state'), + 'sa_kind' => trim((string) $this->request->getPost('sa_kind')), + 'sa_code' => $code, + 'sa_name' => trim((string) $this->request->getPost('sa_name')), ]); - return redirect()->to(site_url('admin/sales-agencies'))->with('success', '판매 대행소가 수정되었습니다.'); + return redirect()->to(mgmt_url('sales-agencies'))->with('success', '판매 대행소가 수정되었습니다.'); } public function delete(int $id) { helper('admin'); - $item = $this->model->find($id); - if (!$item || (int) $item->sa_lg_idx !== admin_effective_lg_idx()) { - return redirect()->to(site_url('admin/sales-agencies'))->with('error', '대행소를 찾을 수 없습니다.'); + $lgIdx = admin_effective_lg_idx(); + $item = $this->model->find($id); + if (! $item || ! $lgIdx || (int) $item->sa_lg_idx !== $lgIdx) { + return redirect()->to(mgmt_url('sales-agencies'))->with('error', '대행소를 찾을 수 없습니다.'); } $this->model->delete($id); - return redirect()->to(site_url('admin/sales-agencies'))->with('success', '판매 대행소가 삭제되었습니다.'); + + return redirect()->to(mgmt_url('sales-agencies'))->with('success', '삭제되었습니다.'); } } diff --git a/app/Helpers/pii_mask_helper.php b/app/Helpers/pii_mask_helper.php new file mode 100644 index 0000000..f0ff058 --- /dev/null +++ b/app/Helpers/pii_mask_helper.php @@ -0,0 +1,84 @@ += 12) { + $digits = '0' . substr($digits, 2); + } + $len = strlen($digits); + if ($len < 7) { + return str_repeat('*', min(11, max(4, $len))); + } + // 7~9자리도 실제 앞자리(통상 010 등)를 노출하고 가운데만 **** (***-****-xxxx 방지) + if ($len < 10) { + return substr($digits, 0, 3) . '-****-' . substr($digits, -4); + } + if ($len === 10) { + if (str_starts_with($digits, '02')) { + return '02-****-' . substr($digits, -4); + } + + return substr($digits, 0, 3) . '-****-' . substr($digits, -4); + } + if ($len === 11) { + return substr($digits, 0, 3) . '-****-' . substr($digits, -4); + } + + return substr($digits, 0, 3) . '-****-' . substr($digits, -4); + } +} diff --git a/app/Views/admin/company/index.php b/app/Views/admin/company/index.php index 3c3b1b3..8e18d03 100644 --- a/app/Views/admin/company/index.php +++ b/app/Views/admin/company/index.php @@ -8,6 +8,31 @@ +
+
+ +
+ + +
+
+ + +
+
+ + 초기화 +
+
+
diff --git a/app/Views/admin/designated_shop/index.php b/app/Views/admin/designated_shop/index.php index ca5e08f..02a814e 100644 --- a/app/Views/admin/designated_shop/index.php +++ b/app/Views/admin/designated_shop/index.php @@ -1,17 +1,18 @@ + '지정판매소 목록']) ?>
-
+ @@ -29,7 +30,7 @@ - 초기화 + 초기화
@@ -50,19 +51,26 @@
- - + + - - - + + +
ds_idx) ?>
+ ds_idx) ?> + ds_lg_idx] ?? '') ?>ds_shop_no) ?>ds_name) ?>ds_rep_name) ?> + ds_shop_no) ?> + + ds_name) ?> + ds_rep_name ?? null)) ?> ds_biz_no) ?> ds_va_number) ?> ds_state === 1 ? '정상' : ((int) $row->ds_state === 2 ? '폐업' : '직권해지') ?> ds_regdate ?? '') ?> - 수정 -
+ 상세 + 수정 +
diff --git a/app/Views/admin/designated_shop/map.php b/app/Views/admin/designated_shop/map.php index 50906da..340dab1 100644 --- a/app/Views/admin/designated_shop/map.php +++ b/app/Views/admin/designated_shop/map.php @@ -1,3 +1,6 @@ + '지정판매소 지도']) ?>
@@ -25,7 +28,7 @@ document.addEventListener('DOMContentLoaded', function() { var geocoder = new kakao.maps.services.Geocoder(); var shops = $s->ds_name, 'addr' => $s->ds_addr ?? '', 'rep' => $s->ds_rep_name ?? '', 'tel' => $s->ds_tel ?? '']; + return ['name' => $s->ds_name, 'addr' => $s->ds_addr ?? '', 'rep' => mask_person_name($s->ds_rep_name ?? null), 'tel' => $s->ds_tel ?? '']; }, $shops), JSON_UNESCAPED_UNICODE) ?>; var bounds = new kakao.maps.LatLngBounds(); diff --git a/app/Views/admin/designated_shop/show.php b/app/Views/admin/designated_shop/show.php new file mode 100644 index 0000000..a176aad --- /dev/null +++ b/app/Views/admin/designated_shop/show.php @@ -0,0 +1,94 @@ + $s !== null && $s !== '' ? $s : '—'; +$dispMask = static function (string $masked): string { + return $masked !== '' ? $masked : '—'; +}; +?> +
+
+ 지정판매소 정보 +
+ 목록 + + 수정 + +
+
+
+
+ +
+ 지자체 + lg_name) ?> (lg_code) ?>) +
+ + +
+ 판매소번호 + ds_shop_no) ?> +
+
+ 구코드 + ds_gugun_code ?? null)) ?> +
+
+ 상호명 + ds_name) ?> +
+
+ 사업자번호 + ds_biz_no ?? null)) ?> +
+
+ 대표자명 + ds_rep_name ?? null))) ?> +
+
+ 가상계좌 + ds_va_number ?? null)) ?> +
+
+ 우편번호 + ds_zip ?? null)) ?> +
+
+ 도로명주소 + ds_addr ?? null)) ?> +
+
+ 지번주소 + ds_addr_jibun ?? null)) ?> +
+
+ 일반전화 + ds_tel ?? null)) ?> +
+
+ 대표 휴대전화 + ds_rep_phone ?? null))) ?> +
+
+ 이메일 + ds_email ?? null)) ?> +
+
+ 지정일자 + ds_designated_at ?? null)) ?> +
+
+ 영업상태 + +
+
+ 등록일시 + ds_regdate ?? null)) ?> +
+
diff --git a/app/Views/admin/free_recipient/index.php b/app/Views/admin/free_recipient/index.php index 1a2bf8f..4bc6470 100644 --- a/app/Views/admin/free_recipient/index.php +++ b/app/Views/admin/free_recipient/index.php @@ -8,6 +8,31 @@
+
+
+ +
+ + +
+
+ + +
+
+ + 초기화 +
+
+
@@ -17,7 +42,7 @@ - + @@ -28,11 +53,11 @@ - + - + diff --git a/app/Views/admin/manager/index.php b/app/Views/admin/manager/index.php index 3c5fe73..9b6d248 100644 --- a/app/Views/admin/manager/index.php +++ b/app/Views/admin/manager/index.php @@ -8,6 +8,31 @@ +
+
+ +
+ + +
+
+ + +
+
+ + 초기화 +
+ +
대상자명 연락처 주소동코드 비고 종료일 상태
fr_idx) ?>fr_type_code) ?>fr_type_code ?? '')] ?? ($row->fr_type_code ?? '')) ?> fr_name) ?> fr_phone) ?> fr_addr) ?>fr_dong_code) ?>fr_dong_code ?? '')] ?? ($row->fr_dong_code ?? '')) ?> fr_note) ?> fr_end_date) ?> fr_state === 1 ? '사용' : '미사용' ?>
@@ -28,8 +53,8 @@ - - + + diff --git a/app/Views/admin/sales_agency/index.php b/app/Views/admin/sales_agency/index.php index 54cab40..860dfa0 100644 --- a/app/Views/admin/sales_agency/index.php +++ b/app/Views/admin/sales_agency/index.php @@ -4,21 +4,39 @@ 판매 대행소 관리 +
+
+
+ + +
+
+ + +
+
+ + 초기화 +
+ +
mg_idx) ?> mg_name) ?>mg_dept_code) ?>mg_position_code) ?>mg_dept_code ?? '')] ?? ($row->mg_dept_code ?? '')) ?>mg_position_code ?? '')] ?? ($row->mg_position_code ?? '')) ?> mg_tel) ?> mg_phone) ?> mg_email) ?>
- - - - - - + + + @@ -26,15 +44,12 @@ + + - - - - - - +
번호대행소명사업자번호대표자전화주소상태대행소 구분대행소 코드대행소 명 작업
sa_idx) ?>sa_kind ?? '') ?>sa_code ?? '') ?> sa_name) ?>sa_biz_no) ?>sa_rep_name) ?>sa_tel) ?>sa_addr) ?>sa_state === 1 ? '정상' : '미사용' ?> - 수정 -
+ 수정 +
@@ -42,7 +57,7 @@
등록된 데이터가 없습니다.
등록된 데이터가 없습니다.
diff --git a/e2e/new-features.spec.js b/e2e/new-features.spec.js index 37a4fae..1c29c72 100644 --- a/e2e/new-features.spec.js +++ b/e2e/new-features.spec.js @@ -106,6 +106,26 @@ test.describe('P2-15: 지정판매소 다조건 조회', () => { }); }); +// 지정판매소 목록 → 상세(읽기 전용) +test.describe('지정판매소 상세', () => { + test('목록에서 상호명 클릭 시 상세 정보 표시', async ({ page }) => { + await loginAsLocal(page); + await page.goto('/bag/designated-shops'); + const rowCount = await page.locator('table.data-table tbody tr').count(); + if (rowCount === 0) { + test.skip(); + return; + } + const shopLink = page.locator('table.data-table tbody td a.font-medium').first(); + await expect(shopLink).toBeVisible(); + await shopLink.click(); + await expect(page).toHaveURL(/\/bag\/designated-shops\/show\/\d+/); + await expect(page.getByText('지정판매소 정보').first()).toBeVisible(); + await expect(page.getByText('판매소번호').first()).toBeVisible(); + await expect(page.getByRole('link', { name: '목록' })).toBeVisible(); + }); +}); + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // P2-17: 지정판매소 지도 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/env b/env deleted file mode 100644 index 864b455..0000000 --- a/env +++ /dev/null @@ -1,78 +0,0 @@ -#-------------------------------------------------------------------- -# Example Environment Configuration file -# -# This file can be used as a starting point for your own -# custom .env files, and contains most of the possible settings -# available in a default install. -# -# By default, all of the settings are commented out. If you want -# to override the setting, you must un-comment it by removing the '#' -# at the beginning of the line. -#-------------------------------------------------------------------- - -#-------------------------------------------------------------------- -# ENVIRONMENT -#-------------------------------------------------------------------- - -# CI_ENVIRONMENT = production - -#-------------------------------------------------------------------- -# APP -#-------------------------------------------------------------------- - -# app.baseURL = '' -# If you have trouble with `.`, you could also use `_`. -# app_baseURL = '' -# app.forceGlobalSecureRequests = false -# app.CSPEnabled = false - -#-------------------------------------------------------------------- -# DATABASE -#-------------------------------------------------------------------- - -# database.default.hostname = localhost -# database.default.database = ci4 -# database.default.username = root -# database.default.password = root -# database.default.DBDriver = MySQLi -# database.default.DBPrefix = -# database.default.port = 3306 - -# If you use MySQLi as tests, first update the values of Config\Database::$tests. -# database.tests.hostname = localhost -# database.tests.database = ci4_test -# database.tests.username = root -# database.tests.password = root -# database.tests.DBDriver = MySQLi -# database.tests.DBPrefix = -# database.tests.charset = utf8mb4 -# database.tests.DBCollat = utf8mb4_general_ci -# database.tests.port = 3306 - -#-------------------------------------------------------------------- -# ENCRYPTION -#-------------------------------------------------------------------- - -# encryption.key = - -#-------------------------------------------------------------------- -# AUTH (TOTP 2차 인증) — 관리자(mb_level 3·4·5)만 적용, 로컬은 false 권장 -#-------------------------------------------------------------------- - -# auth.requireTotp = true -# auth.totpIssuer = "쓰레기봉투 물류시스템" -# auth.totpMaxAttempts = 5 -# auth.pending2faTtlSeconds = 600 - -#-------------------------------------------------------------------- -# SESSION -#-------------------------------------------------------------------- - -# session.driver = 'CodeIgniter\Session\Handlers\FileHandler' -# session.savePath = null - -#-------------------------------------------------------------------- -# LOGGER -#-------------------------------------------------------------------- - -# logger.threshold = 4 diff --git a/tests/unit/PiiMaskTest.php b/tests/unit/PiiMaskTest.php new file mode 100644 index 0000000..5df60a8 --- /dev/null +++ b/tests/unit/PiiMaskTest.php @@ -0,0 +1,42 @@ +assertSame('', mask_person_name(null)); + $this->assertSame('', mask_person_name('')); + } + + public function testMaskPersonNameKorean(): void + { + $this->assertSame('*', mask_person_name('홍')); + $this->assertSame('김*', mask_person_name('김철')); + $this->assertSame('홍*동', mask_person_name('홍길동')); + $this->assertSame('남**수', mask_person_name('남궁민수')); + } + + public function testMaskMobilePhone(): void + { + $this->assertSame('', mask_mobile_phone(null)); + $this->assertSame('', mask_mobile_phone('')); + $this->assertSame('010-****-5678', mask_mobile_phone('010-1234-5678')); + $this->assertSame('010-****-5678', mask_mobile_phone('01012345678')); + $this->assertSame('010-****-5678', mask_mobile_phone('821012345678')); + $this->assertSame('02-****-7890', mask_mobile_phone('0212347890')); + $this->assertSame('010-****-2312', mask_mobile_phone('0102312')); + } +}