P2-15: 지정판매소 다조건 조회 (이름/구군/상태 필터) P2-17: 지정판매소 지도 표시 (Kakao Maps) P2-18: 지정판매소 현황 (연도별 신규/취소 통계) P5-04: 년 판매 현황 (월별 피벗 테이블) P5-05: 지정판매소별 판매현황 (판매소별 수량/금액) P5-06: 홈택스 세금계산서 엑셀 내보내기 P5-08: 반품/파기 현황 (기간별 조회) P5-10: LOT 수불 조회 (LOT 번호 검색) P5-11: 기타 입출고 (등록 + 재고 연동) CT-05: CRUD 로깅 (activity_log 테이블 + audit_helper) CT-06: 대시보드 실 데이터 (발주/판매/재고/불출 통계) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
438 lines
16 KiB
PHP
438 lines
16 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']);
|
|
$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,
|
|
$row->ds_rep_name,
|
|
$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 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);
|
|
}
|
|
}
|
|
|