Files
jongryangje/app/Controllers/Admin/DesignatedShop.php
taekyoungc 71edc1eb20 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
2026-04-08 15:22:24 +09:00

473 lines
17 KiB
PHP

<?php
namespace App\Controllers\Admin;
use App\Controllers\BaseController;
use App\Models\DesignatedShopModel;
use App\Models\LocalGovernmentModel;
use Config\Roles;
class DesignatedShop extends BaseController
{
private DesignatedShopModel $shopModel;
private LocalGovernmentModel $lgModel;
private Roles $roles;
public function __construct()
{
$this->shopModel = model(DesignatedShopModel::class);
$this->lgModel = model(LocalGovernmentModel::class);
$this->roles = config('Roles');
}
private function isSuperAdmin(): bool
{
return Roles::isSuperAdminEquivalent((int) session()->get('mb_level'));
}
private function isLocalAdmin(): bool
{
return (int) session()->get('mb_level') === Roles::LEVEL_LOCAL_ADMIN;
}
/**
* 지정판매소 목록 (효과 지자체 기준: super admin = 선택 지자체, 지자체관리자 = mb_lg_idx)
*/
public function index()
{
helper('admin');
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null || $lgIdx <= 0) {
return redirect()->to(site_url('admin'))
->with('error', '작업할 지자체가 선택되지 않았습니다. 지자체를 선택해 주세요.');
}
$builder = $this->shopModel->where('ds_lg_idx', $lgIdx);
// 다조건 검색 (P2-15)
$dsName = $this->request->getGet('ds_name');
$dsGugunCode = $this->request->getGet('ds_gugun_code');
$dsState = $this->request->getGet('ds_state');
if ($dsName !== null && $dsName !== '') {
$builder->like('ds_name', $dsName);
}
if ($dsGugunCode !== null && $dsGugunCode !== '') {
$builder->where('ds_gugun_code', $dsGugunCode);
}
if ($dsState !== null && $dsState !== '') {
$builder->where('ds_state', (int) $dsState);
}
$list = $builder->orderBy('ds_idx', 'DESC')->paginate(20);
$pager = $this->shopModel->pager;
// 지자체 이름 매핑용
$lgMap = [];
foreach ($this->lgModel->findAll() as $lg) {
$lgMap[$lg->lg_idx] = $lg->lg_name;
}
// 구군코드 목록 (검색 필터용)
$db = \Config\Database::connect();
$gugunCodes = $db->query("SELECT DISTINCT ds_gugun_code FROM designated_shop WHERE ds_lg_idx = ? AND ds_gugun_code != '' ORDER BY ds_gugun_code", [$lgIdx])->getResult();
return view('admin/layout', [
'title' => '지정판매소 관리',
'content' => view('admin/designated_shop/index', [
'list' => $list,
'lgMap' => $lgMap,
'pager' => $pager,
'dsName' => $dsName ?? '',
'dsGugunCode' => $dsGugunCode ?? '',
'dsState' => $dsState ?? '',
'gugunCodes' => $gugunCodes,
]),
]);
}
public function export()
{
helper(['admin', 'export', 'pii_mask']);
$lgIdx = admin_effective_lg_idx();
if (!$lgIdx) {
return redirect()->to(site_url('admin/designated-shops'))->with('error', '지자체를 선택해 주세요.');
}
$list = $this->shopModel->where('ds_lg_idx', $lgIdx)->orderBy('ds_idx', 'DESC')->findAll();
$rows = [];
foreach ($list as $row) {
$stateMap = [1 => '정상', 2 => '폐업', 3 => '직권해지'];
$rows[] = [
$row->ds_idx,
$row->ds_shop_no,
$row->ds_name,
mask_person_name($row->ds_rep_name ?? null),
$row->ds_biz_no,
$row->ds_va_number,
$row->ds_tel ?? '',
$row->ds_addr ?? '',
$stateMap[(int) $row->ds_state] ?? '',
$row->ds_regdate ?? '',
];
}
export_csv(
'지정판매소_' . date('Ymd') . '.csv',
['번호', '판매소번호', '상호명', '대표자', '사업자번호', '가상계좌', '전화번호', '주소', '상태', '등록일'],
$rows
);
}
/**
* 지정판매소 상세 (읽기 전용, 목록에서 연결)
*/
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(),
]);
}
/**
* 지정판매소 등록 폼 (효과 지자체 기준)
*/
public function create()
{
helper('admin');
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null || $lgIdx <= 0) {
return redirect()->to(site_url('admin/designated-shops'))
->with('error', '작업할 지자체가 선택되지 않았습니다. 지자체를 선택해 주세요.');
}
$currentLg = $this->lgModel->find($lgIdx);
if ($currentLg === null) {
return redirect()->to(site_url('admin/designated-shops'))
->with('error', '선택한 지자체 정보를 찾을 수 없습니다.');
}
return view('admin/layout', [
'title' => '지정판매소 등록',
'content' => view('admin/designated_shop/create', [
'localGovs' => [],
'currentLg' => $currentLg,
]),
]);
}
/**
* 지정판매소 등록 처리
*/
public function store()
{
if (! $this->isSuperAdmin() && ! $this->isLocalAdmin()) {
return redirect()->to(site_url('admin/designated-shops'))
->with('error', '지정판매소 등록은 관리자만 가능합니다.');
}
$rules = [
'ds_name' => 'required|max_length[100]',
'ds_biz_no' => 'required|max_length[20]',
'ds_rep_name' => 'required|max_length[50]',
'ds_va_number' => 'permit_empty|max_length[50]',
'ds_email' => 'permit_empty|valid_email|max_length[100]',
];
if (! $this->validate($rules)) {
return redirect()->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
helper('admin');
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null || $lgIdx <= 0) {
return redirect()->back()
->withInput()
->with('error', '소속 지자체가 올바르지 않습니다.');
}
$lg = $this->lgModel->find($lgIdx);
if ($lg === null || (string) $lg->lg_code === '') {
return redirect()->back()
->withInput()
->with('error', '지자체 코드 정보를 찾을 수 없습니다.');
}
$dsShopNo = $this->generateNextShopNo($lgIdx, (string) $lg->lg_code);
$data = [
'ds_lg_idx' => $lgIdx,
'ds_mb_idx' => null,
'ds_shop_no' => $dsShopNo,
'ds_name' => (string) $this->request->getPost('ds_name'),
'ds_biz_no' => (string) $this->request->getPost('ds_biz_no'),
'ds_rep_name' => (string) $this->request->getPost('ds_rep_name'),
'ds_va_number' => (string) $this->request->getPost('ds_va_number'),
'ds_zip' => (string) $this->request->getPost('ds_zip'),
'ds_addr' => (string) $this->request->getPost('ds_addr'),
'ds_addr_jibun' => (string) $this->request->getPost('ds_addr_jibun'),
'ds_tel' => (string) $this->request->getPost('ds_tel'),
'ds_rep_phone' => (string) $this->request->getPost('ds_rep_phone'),
'ds_email' => (string) $this->request->getPost('ds_email'),
'ds_gugun_code' => (string) $lg->lg_code,
'ds_designated_at' => $this->request->getPost('ds_designated_at') ?: null,
'ds_state' => 1,
'ds_regdate' => date('Y-m-d H:i:s'),
];
$this->shopModel->insert($data);
return redirect()->to(site_url('admin/designated-shops'))
->with('success', '지정판매소가 등록되었습니다.');
}
/**
* 지정판매소 수정 폼 (효과 지자체 소속만 허용)
* 문서: docs/기본 개발계획/23-지정판매소_수정_삭제_기능.md
*/
public function edit(int $id)
{
if (! $this->isSuperAdmin() && ! $this->isLocalAdmin()) {
return redirect()->to(site_url('admin/designated-shops'))
->with('error', '권한이 없습니다.');
}
helper('admin');
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null || $lgIdx <= 0) {
return redirect()->to(site_url('admin/designated-shops'))
->with('error', '작업할 지자체가 선택되지 않았습니다.');
}
$shop = $this->shopModel->find($id);
if ($shop === null || (int) $shop->ds_lg_idx !== $lgIdx) {
return redirect()->to(site_url('admin/designated-shops'))
->with('error', '해당 지정판매소를 찾을 수 없거나 수정할 수 없습니다.');
}
$currentLg = $this->lgModel->find($lgIdx);
return view('admin/layout', [
'title' => '지정판매소 수정',
'content' => view('admin/designated_shop/edit', [
'shop' => $shop,
'currentLg' => $currentLg,
]),
]);
}
/**
* 지정판매소 수정 처리
*/
public function update(int $id)
{
if (! $this->isSuperAdmin() && ! $this->isLocalAdmin()) {
return redirect()->to(site_url('admin/designated-shops'))
->with('error', '권한이 없습니다.');
}
helper('admin');
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null || $lgIdx <= 0) {
return redirect()->to(site_url('admin/designated-shops'))
->with('error', '작업할 지자체가 선택되지 않았습니다.');
}
$shop = $this->shopModel->find($id);
if ($shop === null || (int) $shop->ds_lg_idx !== $lgIdx) {
return redirect()->to(site_url('admin/designated-shops'))
->with('error', '해당 지정판매소를 찾을 수 없거나 수정할 수 없습니다.');
}
$rules = [
'ds_name' => 'required|max_length[100]',
'ds_biz_no' => 'required|max_length[20]',
'ds_rep_name' => 'required|max_length[50]',
'ds_va_number' => 'permit_empty|max_length[50]',
'ds_email' => 'permit_empty|valid_email|max_length[100]',
'ds_state' => 'permit_empty|in_list[1,2,3]',
];
if (! $this->validate($rules)) {
return redirect()->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$data = [
'ds_name' => (string) $this->request->getPost('ds_name'),
'ds_biz_no' => (string) $this->request->getPost('ds_biz_no'),
'ds_rep_name' => (string) $this->request->getPost('ds_rep_name'),
'ds_va_number' => (string) $this->request->getPost('ds_va_number'),
'ds_zip' => (string) $this->request->getPost('ds_zip'),
'ds_addr' => (string) $this->request->getPost('ds_addr'),
'ds_addr_jibun' => (string) $this->request->getPost('ds_addr_jibun'),
'ds_tel' => (string) $this->request->getPost('ds_tel'),
'ds_rep_phone' => (string) $this->request->getPost('ds_rep_phone'),
'ds_email' => (string) $this->request->getPost('ds_email'),
'ds_designated_at' => $this->request->getPost('ds_designated_at') ?: null,
'ds_state' => (int) ($this->request->getPost('ds_state') ?: 1),
];
$this->shopModel->update($id, $data);
return redirect()->to(site_url('admin/designated-shops'))
->with('success', '지정판매소 정보가 수정되었습니다.');
}
/**
* 지정판매소 삭제 (물리 삭제, 효과 지자체 소속만 허용)
* 문서: docs/기본 개발계획/23-지정판매소_수정_삭제_기능.md
*/
public function delete(int $id)
{
if (! $this->isSuperAdmin() && ! $this->isLocalAdmin()) {
return redirect()->to(site_url('admin/designated-shops'))
->with('error', '권한이 없습니다.');
}
helper('admin');
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null || $lgIdx <= 0) {
return redirect()->to(site_url('admin/designated-shops'))
->with('error', '작업할 지자체가 선택되지 않았습니다.');
}
$shop = $this->shopModel->find($id);
if ($shop === null || (int) $shop->ds_lg_idx !== $lgIdx) {
return redirect()->to(site_url('admin/designated-shops'))
->with('error', '해당 지정판매소를 찾을 수 없거나 삭제할 수 없습니다.');
}
$this->shopModel->delete($id);
return redirect()->to(site_url('admin/designated-shops'))
->with('success', '지정판매소가 삭제되었습니다.');
}
/**
* P2-17: 지정판매소 지도 표시
*/
public function map()
{
helper('admin');
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null || $lgIdx <= 0) {
return redirect()->to(site_url('admin'))
->with('error', '작업할 지자체가 선택되지 않았습니다.');
}
$shops = $this->shopModel
->where('ds_lg_idx', $lgIdx)
->where('ds_state', 1)
->findAll();
return view('admin/layout', [
'title' => '지정판매소 지도',
'content' => view('admin/designated_shop/map', [
'shops' => $shops,
]),
]);
}
/**
* P2-18: 지정판매소 현황 (연도별 신규/취소)
*/
public function status()
{
helper('admin');
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null || $lgIdx <= 0) {
return redirect()->to(site_url('admin'))
->with('error', '작업할 지자체가 선택되지 않았습니다.');
}
$db = \Config\Database::connect();
// 연도별 신규등록 건수 (ds_designated_at 기준)
$newByYear = $db->query("
SELECT YEAR(ds_designated_at) as yr, COUNT(*) as cnt
FROM designated_shop
WHERE ds_lg_idx = ? AND ds_designated_at IS NOT NULL
GROUP BY YEAR(ds_designated_at)
ORDER BY yr DESC
", [$lgIdx])->getResult();
// 연도별 취소/비활성 건수 (ds_state != 1, ds_regdate 기준)
$cancelByYear = $db->query("
SELECT YEAR(ds_regdate) as yr, COUNT(*) as cnt
FROM designated_shop
WHERE ds_lg_idx = ? AND ds_state != 1
GROUP BY YEAR(ds_regdate)
ORDER BY yr DESC
", [$lgIdx])->getResult();
// 전체 현황 합계
$totalActive = $this->shopModel->where('ds_lg_idx', $lgIdx)->where('ds_state', 1)->countAllResults(false);
$totalInactive = $this->shopModel->where('ds_lg_idx', $lgIdx)->where('ds_state !=', 1)->countAllResults(false);
return view('admin/layout', [
'title' => '지정판매소 현황',
'content' => view('admin/designated_shop/status', [
'newByYear' => $newByYear,
'cancelByYear' => $cancelByYear,
'totalActive' => $totalActive,
'totalInactive' => $totalInactive,
]),
]);
}
/**
* 지자체별 다음 판매소번호 생성 (lg_code + 3자리 일련번호)
* 문서: docs/기본 개발계획/22-판매소번호_일련번호_결정.md §3
*/
private function generateNextShopNo(int $lgIdx, string $lgCode): string
{
$prefixLen = strlen($lgCode);
$existing = $this->shopModel->where('ds_lg_idx', $lgIdx)->findAll();
$maxSerial = 0;
foreach ($existing as $row) {
$no = $row->ds_shop_no;
if (strlen($no) === $prefixLen + 3 && str_starts_with($no, $lgCode)) {
$n = (int) substr($no, -3);
if ($n > $maxSerial) {
$maxSerial = $n;
}
}
}
return $lgCode . sprintf('%03d', $maxSerial + 1);
}
}