- robthree/twofactorauth, Auth 설정·TotpService·2FA 뷰·라우트 - member TOTP 컬럼 DDL(login_tables, member_add_totp.sql) - 관리자 메뉴·레이아웃·필터·대시보드 등 연관 변경 - env 샘플에 auth.requireTotp 주석 Made-with: Cursor
214 lines
8.3 KiB
PHP
214 lines
8.3 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);
|
|
}
|
|
|
|
/**
|
|
* 메뉴 관리 화면 (목록 + 등록/수정 폼). 지자체별 메뉴만 조회·관리.
|
|
*/
|
|
public function index()
|
|
{
|
|
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();
|
|
$mtIdx = (int) ($this->request->getGet('mt_idx') ?? 0);
|
|
if ($mtIdx <= 0 && ! empty($types)) {
|
|
// 기본 선택: 사이트 메뉴(mt_code=site), 없으면 첫 번째 타입
|
|
$siteType = $this->typeModel->where('mt_code', 'site')->first();
|
|
$mtIdx = $siteType ? (int) $siteType->mt_idx : (int) $types[0]->mt_idx;
|
|
}
|
|
$list = $mtIdx > 0 ? $this->menuModel->getAllByType($mtIdx, $lgIdx) : [];
|
|
|
|
// 현재 지자체에 메뉴가 없으면, mt_idx별로 기본 지자체(lg_idx=1)의 메뉴를 한 번 복사한다.
|
|
if ($mtIdx > 0 && empty($list)) {
|
|
$this->menuModel->copyDefaultsFromLg($mtIdx, 1, $lgIdx);
|
|
$list = $this->menuModel->getAllByType($mtIdx, $lgIdx);
|
|
}
|
|
|
|
// 트리 순서대로 상위 메뉴 바로 아래에 하위 메뉴가 오도록 평탄화
|
|
if (! empty($list)) {
|
|
$tree = build_menu_tree($list);
|
|
$list = flatten_menu_tree($tree);
|
|
}
|
|
|
|
$currentType = $mtIdx > 0 ? $this->typeModel->find($mtIdx) : null;
|
|
|
|
return view('admin/layout', [
|
|
'title' => '메뉴 관리',
|
|
'content' => view('admin/menu/index', [
|
|
'types' => $types,
|
|
'mtIdx' => $mtIdx,
|
|
'mtCode' => $currentType->mt_code ?? '',
|
|
'list' => $list,
|
|
'levelNames' => config('Roles')->levelNames,
|
|
]),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 메뉴 목록 JSON (트리 정렬된 평면 배열). 현재 지자체만.
|
|
*/
|
|
public function list()
|
|
{
|
|
$lgIdx = admin_effective_lg_idx();
|
|
if ($lgIdx === null) {
|
|
return $this->response->setJSON(['status' => 0, 'msg' => '지자체를 선택하세요.']);
|
|
}
|
|
$mtIdx = (int) $this->request->getGet('mt_idx');
|
|
if ($mtIdx <= 0) {
|
|
return $this->response->setJSON(['status' => 0, 'msg' => 'mt_idx required']);
|
|
}
|
|
$list = $this->menuModel->getAllByType($mtIdx, $lgIdx);
|
|
return $this->response->setJSON(['status' => 1, 'data' => $list]);
|
|
}
|
|
|
|
/**
|
|
* 메뉴 등록 (현재 지자체 소속으로 저장)
|
|
*/
|
|
public function store()
|
|
{
|
|
$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 redirect()->back()->with('error', '메뉴 종류를 선택하세요.');
|
|
}
|
|
if ($mmName === '') {
|
|
return redirect()->back()->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);
|
|
}
|
|
return redirect()->back()->with('success', '메뉴가 등록되었습니다.');
|
|
}
|
|
|
|
/**
|
|
* 메뉴 수정 (현재 지자체 소속 메뉴만 허용)
|
|
*/
|
|
public function update(int $id)
|
|
{
|
|
$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 redirect()->back()->with('error', '메뉴를 찾을 수 없습니다.');
|
|
}
|
|
if ((int) $row->lg_idx !== $lgIdx) {
|
|
return redirect()->back()->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);
|
|
return redirect()->back()->with('success', '메뉴가 수정되었습니다.');
|
|
}
|
|
|
|
/**
|
|
* 메뉴 삭제 (현재 지자체 소속만 허용, 하위 있으면 불가)
|
|
*/
|
|
public function delete(int $id)
|
|
{
|
|
$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 redirect()->back()->with('error', '해당 지자체의 메뉴만 삭제할 수 있습니다.');
|
|
}
|
|
$result = $this->menuModel->deleteSafe($id);
|
|
if ($result['ok']) {
|
|
return redirect()->back()->with('success', '메뉴가 삭제되었습니다.');
|
|
}
|
|
return redirect()->back()->with('error', $result['msg']);
|
|
}
|
|
|
|
/**
|
|
* 순서 변경 (mm_idx[] 순서대로 mm_num 부여). 현재 지자체 메뉴만.
|
|
*/
|
|
public function move()
|
|
{
|
|
$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');
|
|
if (! is_array($ids) || empty($ids)) {
|
|
return redirect()->back()->with('error', '순서를 적용할 메뉴가 없습니다.');
|
|
}
|
|
$this->menuModel->setOrder($ids, $lgIdx);
|
|
return redirect()->back()->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));
|
|
}
|
|
}
|