Files
jongryangje/app/Controllers/Admin/Menu.php
taekyoungc 8763876f19 사용자 매뉴얼·번호알기·gov-portal 대시보드와 메뉴 동선·수불 리포트를 보강한다.
- 사용자 매뉴얼: league/commonmark 기반 bag/manual(로그인 전용),
  ManualRenderer + Config\Manual manifest, 콘텐츠 8종, E2E
- 번호알기(봉투번호확인): bag/number-lookup, BagNumberLookup, E2E
- gov-portal 대시보드 시안(기본/strip)·기본코드관리 화면
- 메뉴 관리: 등록·수정 후 메뉴 화면 유지, 수정 버튼 클릭 시 상단 스크롤
- 수불/분석 리포트(LOT 수불·반품/파기·수급계획·추이) 표시 보강
- .gitignore: docs/ → /docs/ 앵커링(최상위 개발문서만 제외, app/Docs는 추적)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 00:46:51 +09:00

352 lines
14 KiB
PHP

<?php
namespace App\Controllers\Admin;
use App\Controllers\BaseController;
use App\Models\MenuModel;
use App\Models\MenuTypeModel;
use Config\Roles;
class Menu extends BaseController
{
private MenuModel $menuModel;
private MenuTypeModel $typeModel;
public function __construct()
{
$this->menuModel = model(MenuModel::class);
$this->typeModel = model(MenuTypeModel::class);
}
/**
* 메뉴 등록·수정·삭제·순서변경 후 항상 같은 메뉴 관리 화면(mt_idx 유지)으로 돌아간다.
* redirect()->back() 은 목록의 새 탭(target="_blank") 링크 클릭으로 세션 직전 URL(_ci_previous_url)이
* 메뉴 대상 페이지로 덮어써지면 그 페이지로 이탈하므로, 명시적으로 메뉴 화면 URL 을 사용한다.
*/
private function menusRedirect(int $mtIdx): \CodeIgniter\HTTP\RedirectResponse
{
$url = base_url('admin/menus');
if ($mtIdx > 0) {
$url .= '?mt_idx=' . $mtIdx;
}
return redirect()->to($url);
}
/**
* 메뉴 관리 화면 (목록 + 등록/수정 폼). 지자체별 메뉴만 조회·관리.
*/
public function index()
{
if ($deny = $this->denyUnlessLevel4Plus()) {
return $deny;
}
helper('admin');
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) {
return redirect()->to(base_url('admin/select-local-government'))
->with('error', '메뉴를 관리하려면 먼저 지자체를 선택하세요.');
}
$types = $this->typeModel->orderBy('mt_sort', 'ASC')->findAll();
$requestedMtIdx = (int) ($this->request->getGet('mt_idx') ?? 0);
$mtIdx = $this->resolveMtIdx($requestedMtIdx, $types);
$effectiveMtIdx = $mtIdx;
$debugMode = $this->request->getGet('debug') === '1';
$fallbackApplied = false;
$list = $effectiveMtIdx > 0 ? $this->menuModel->getAllByType($effectiveMtIdx, $lgIdx) : [];
$currentType = $mtIdx > 0 ? $this->typeModel->find($mtIdx) : null;
$currentTypeCode = (string) ($currentType->mt_code ?? '');
// 현재 지자체에 메뉴가 없으면, mt_idx별로 기본 지자체(lg_idx=1)의 메뉴를 한 번 복사한다.
if ($effectiveMtIdx > 0 && empty($list)) {
$this->menuModel->copyDefaultsFromLg($effectiveMtIdx, 1, $lgIdx);
$list = $this->menuModel->getAllByType($effectiveMtIdx, $lgIdx);
}
// 운영 DB 불일치 대응: site 타입인데 mt_idx 매핑이 어긋난 경우(예: menu_type=2, menu는 4 사용)
if (empty($list) && $currentTypeCode === 'site' && $effectiveMtIdx !== 4) {
$fallbackMtIdx = 4;
$fallbackList = $this->menuModel->getAllByType($fallbackMtIdx, $lgIdx);
if (empty($fallbackList)) {
$this->menuModel->copyDefaultsFromLg($fallbackMtIdx, 1, $lgIdx);
$fallbackList = $this->menuModel->getAllByType($fallbackMtIdx, $lgIdx);
}
if (! empty($fallbackList)) {
$effectiveMtIdx = $fallbackMtIdx;
$list = $fallbackList;
$fallbackApplied = true;
}
}
if ($effectiveMtIdx > 0 && $currentTypeCode === 'site') {
$this->menuModel->pruneInventoryManagementMenus($effectiveMtIdx, $lgIdx);
$list = $this->menuModel->getAllByType($effectiveMtIdx, $lgIdx);
}
// 트리 순서대로 상위 메뉴 바로 아래에 하위 메뉴가 오도록 평탄화
if (! empty($list)) {
$tree = build_menu_tree($list);
$list = flatten_menu_tree($tree);
}
return view('admin/layout', [
'title' => '메뉴 관리',
'content' => view('admin/menu/index', [
'types' => $types,
'mtIdx' => $mtIdx,
'mtCode' => $currentTypeCode,
'list' => $list,
'levelNames' => config('Roles')->levelNames,
'debug_mode' => $debugMode,
'debug_info' => [
'lg_idx' => $lgIdx,
'requested_mt_idx' => $requestedMtIdx,
'resolved_mt_idx' => $mtIdx,
'effective_mt_idx' => $effectiveMtIdx,
'resolved_mt_code' => $currentTypeCode,
'list_count' => count($list),
'fallback_applied' => $fallbackApplied ? 'Y' : 'N',
],
]),
]);
}
/**
* 메뉴 목록 JSON (트리 정렬된 평면 배열). 현재 지자체만.
*/
public function list()
{
if ($deny = $this->denyUnlessLevel4Plus(true)) {
return $deny;
}
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) {
return $this->response->setJSON(['status' => 0, 'msg' => '지자체를 선택하세요.']);
}
$types = $this->typeModel->orderBy('mt_sort', 'ASC')->findAll();
$requestedMtIdx = (int) $this->request->getGet('mt_idx');
$mtIdx = $this->resolveMtIdx($requestedMtIdx, $types);
if ($mtIdx <= 0) {
return $this->response->setJSON(['status' => 0, 'msg' => 'mt_idx required']);
}
$type = $this->typeModel->find($mtIdx);
if ($type && (string) ($type->mt_code ?? '') === 'site') {
$this->menuModel->pruneInventoryManagementMenus($mtIdx, $lgIdx);
}
$list = $this->menuModel->getAllByType($mtIdx, $lgIdx);
return $this->response->setJSON(['status' => 1, 'data' => $list]);
}
/**
* 메뉴 등록 (현재 지자체 소속으로 저장)
*/
public function store()
{
if ($deny = $this->denyUnlessLevel4Plus()) {
return $deny;
}
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) {
return redirect()->to(base_url('admin/select-local-government'))
->with('error', '메뉴를 등록하려면 먼저 지자체를 선택하세요.');
}
$mtIdx = (int) $this->request->getPost('mt_idx');
$mmPidx = (int) $this->request->getPost('mm_pidx');
$mmDep = (int) $this->request->getPost('mm_dep');
$mmName = trim((string) $this->request->getPost('mm_name'));
if ($mtIdx <= 0) {
return $this->menusRedirect($mtIdx)->with('error', '메뉴 종류를 선택하세요.');
}
if ($mmName === '') {
return $this->menusRedirect($mtIdx)->with('error', '메뉴명을 입력하세요.');
}
$mmNum = $this->menuModel->getNextNum($mtIdx, $lgIdx, $mmPidx, $mmDep);
$data = [
'mt_idx' => $mtIdx,
'lg_idx' => $lgIdx,
'mm_name' => $mmName,
'mm_link' => (string) $this->request->getPost('mm_link'),
'mm_pidx' => $mmPidx,
'mm_dep' => $mmDep,
'mm_num' => $mmNum,
'mm_cnode' => 0,
'mm_level' => $this->normalizeMmLevel($mtIdx),
'mm_is_view' => $this->request->getPost('mm_is_view') ? 'Y' : 'N',
];
$this->menuModel->insert($data);
if ($mmPidx > 0) {
$this->menuModel->updateCnode($mmPidx, 1);
}
$this->menuModel->pruneInventoryManagementMenus($mtIdx, $lgIdx);
$this->menuModel->syncTypeToAllLgs($mtIdx, $lgIdx);
return $this->menusRedirect($mtIdx)->with('success', '메뉴가 등록되었습니다.');
}
/**
* 메뉴 수정 (현재 지자체 소속 메뉴만 허용)
*/
public function update(int $id)
{
if ($deny = $this->denyUnlessLevel4Plus()) {
return $deny;
}
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) {
return redirect()->to(base_url('admin/select-local-government'))
->with('error', '지자체를 선택하세요.');
}
$row = $this->menuModel->find($id);
if (! $row) {
return $this->menusRedirect((int) $this->request->getPost('mt_idx'))
->with('error', '메뉴를 찾을 수 없습니다.');
}
if ((int) $row->lg_idx !== $lgIdx) {
return $this->menusRedirect((int) $row->mt_idx)
->with('error', '해당 지자체의 메뉴만 수정할 수 있습니다.');
}
$data = [
'mm_name' => (string) $this->request->getPost('mm_name'),
'mm_link' => (string) $this->request->getPost('mm_link'),
'mm_level' => $this->normalizeMmLevel((int) $row->mt_idx),
'mm_is_view' => $this->request->getPost('mm_is_view') ? 'Y' : 'N',
];
$this->menuModel->update($id, $data);
$this->menuModel->pruneInventoryManagementMenus((int) $row->mt_idx, $lgIdx);
$this->menuModel->syncTypeToAllLgs((int) $row->mt_idx, $lgIdx);
return $this->menusRedirect((int) $row->mt_idx)->with('success', '메뉴가 수정되었습니다.');
}
/**
* 메뉴 삭제 (현재 지자체 소속만 허용, 하위 있으면 불가)
*/
public function delete(int $id)
{
if ($deny = $this->denyUnlessLevel4Plus()) {
return $deny;
}
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) {
return redirect()->to(base_url('admin/select-local-government'))
->with('error', '지자체를 선택하세요.');
}
$row = $this->menuModel->find($id);
if (! $row || (int) $row->lg_idx !== $lgIdx) {
return $this->menusRedirect((int) $this->request->getPost('mt_idx'))
->with('error', '해당 지자체의 메뉴만 삭제할 수 있습니다.');
}
$result = $this->menuModel->deleteSafe($id);
if ($result['ok']) {
$this->menuModel->pruneInventoryManagementMenus((int) $row->mt_idx, $lgIdx);
$this->menuModel->syncTypeToAllLgs((int) $row->mt_idx, $lgIdx);
return $this->menusRedirect((int) $row->mt_idx)->with('success', '메뉴가 삭제되었습니다.');
}
return $this->menusRedirect((int) $row->mt_idx)->with('error', $result['msg']);
}
/**
* 순서 변경 (mm_idx[] 순서대로 mm_num 부여). 현재 지자체 메뉴만.
*/
public function move()
{
if ($deny = $this->denyUnlessLevel4Plus()) {
return $deny;
}
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) {
return redirect()->to(base_url('admin/select-local-government'))
->with('error', '지자체를 선택하세요.');
}
$ids = $this->request->getPost('mm_idx');
$postMtIdx = (int) $this->request->getPost('mt_idx');
if (! is_array($ids) || empty($ids)) {
return $this->menusRedirect($postMtIdx)->with('error', '순서를 적용할 메뉴가 없습니다.');
}
$firstId = (int) ($ids[0] ?? 0);
$firstRow = $firstId > 0 ? $this->menuModel->find($firstId) : null;
$this->menuModel->setOrder($ids, $lgIdx);
if ($firstRow && (int) $firstRow->lg_idx === $lgIdx) {
$this->menuModel->pruneInventoryManagementMenus((int) $firstRow->mt_idx, $lgIdx);
$this->menuModel->syncTypeToAllLgs((int) $firstRow->mt_idx, $lgIdx);
}
$mtIdx = $firstRow ? (int) $firstRow->mt_idx : $postMtIdx;
return $this->menusRedirect($mtIdx)->with('success', '순서가 적용되었습니다.');
}
/**
* 노출 대상: 전체(mm_level_all)이면 빈 문자열, 아니면 선택한 레벨을 쉼표 구분 문자열로
*/
private function normalizeMmLevel(int $mtIdx): string
{
// 관리자 메뉴(admin)는 시민/판매소 노출을 허용하지 않음 → 지자체관리자(3)로 고정
$type = $this->typeModel->find($mtIdx);
if ($type && (string) $type->mt_code === 'admin') {
return (string) Roles::LEVEL_LOCAL_ADMIN;
}
if ($this->request->getPost('mm_level_all')) {
return '';
}
$levels = $this->request->getPost('mm_level');
if (! is_array($levels) || empty($levels)) {
return '';
}
$levels = array_map('intval', $levels);
// super/본부(4·5)는 mm_level 저장 대상 아님. 1,2,3은 그대로 저장
$levels = array_filter($levels, static fn ($v) => $v > 0 && ! \Config\Roles::isSuperAdminEquivalent($v));
return implode(',', array_values($levels));
}
/**
* 요청된 mt_idx를 현재 DB 상태에 맞게 보정.
* - 유효한 mt_idx면 그대로 사용
* - 레거시 site 값(2) 요청 시 site 타입의 실제 mt_idx로 치환
* - 그 외 미지정/잘못된 값은 site 우선, 없으면 첫 타입으로 보정
*
* @param array<int,object> $types
*/
private function resolveMtIdx(int $requestedMtIdx, array $types): int
{
if (empty($types)) {
return 0;
}
$validTypeIds = array_map(static fn ($t): int => (int) ($t->mt_idx ?? 0), $types);
if ($requestedMtIdx > 0 && in_array($requestedMtIdx, $validTypeIds, true)) {
return $requestedMtIdx;
}
$siteType = $this->typeModel->where('mt_code', 'site')->first();
if ($siteType !== null) {
// 과거 링크(/admin/menus?mt_idx=2) 호환
if ($requestedMtIdx === 2 || $requestedMtIdx <= 0 || ! in_array($requestedMtIdx, $validTypeIds, true)) {
return (int) $siteType->mt_idx;
}
}
return (int) $types[0]->mt_idx;
}
/**
* 메뉴 관리는 레벨4 이상(슈퍼/본부 관리자)만 허용.
*
* @return \CodeIgniter\HTTP\RedirectResponse|\CodeIgniter\HTTP\ResponseInterface|null
*/
private function denyUnlessLevel4Plus(bool $json = false)
{
$level = (int) session()->get('mb_level');
if (Roles::isSuperAdminEquivalent($level)) {
return null;
}
if ($json) {
return $this->response->setJSON([
'status' => 0,
'msg' => '메뉴 관리는 레벨4 이상만 접근할 수 있습니다.',
]);
}
return redirect()->to(base_url('admin/dashboard'))
->with('error', '메뉴 관리는 레벨4 이상만 접근할 수 있습니다.');
}
}