사용자 매뉴얼·번호알기·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>
This commit is contained in:
@@ -18,6 +18,21 @@ class Menu extends BaseController
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 관리 화면 (목록 + 등록/수정 폼). 지자체별 메뉴만 조회·관리.
|
||||
*/
|
||||
@@ -140,10 +155,10 @@ class Menu extends BaseController
|
||||
$mmDep = (int) $this->request->getPost('mm_dep');
|
||||
$mmName = trim((string) $this->request->getPost('mm_name'));
|
||||
if ($mtIdx <= 0) {
|
||||
return redirect()->back()->with('error', '메뉴 종류를 선택하세요.');
|
||||
return $this->menusRedirect($mtIdx)->with('error', '메뉴 종류를 선택하세요.');
|
||||
}
|
||||
if ($mmName === '') {
|
||||
return redirect()->back()->with('error', '메뉴명을 입력하세요.');
|
||||
return $this->menusRedirect($mtIdx)->with('error', '메뉴명을 입력하세요.');
|
||||
}
|
||||
$mmNum = $this->menuModel->getNextNum($mtIdx, $lgIdx, $mmPidx, $mmDep);
|
||||
$data = [
|
||||
@@ -164,7 +179,7 @@ class Menu extends BaseController
|
||||
}
|
||||
$this->menuModel->pruneInventoryManagementMenus($mtIdx, $lgIdx);
|
||||
$this->menuModel->syncTypeToAllLgs($mtIdx, $lgIdx);
|
||||
return redirect()->back()->with('success', '메뉴가 등록되었습니다.');
|
||||
return $this->menusRedirect($mtIdx)->with('success', '메뉴가 등록되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,10 +197,12 @@ class Menu extends BaseController
|
||||
}
|
||||
$row = $this->menuModel->find($id);
|
||||
if (! $row) {
|
||||
return redirect()->back()->with('error', '메뉴를 찾을 수 없습니다.');
|
||||
return $this->menusRedirect((int) $this->request->getPost('mt_idx'))
|
||||
->with('error', '메뉴를 찾을 수 없습니다.');
|
||||
}
|
||||
if ((int) $row->lg_idx !== $lgIdx) {
|
||||
return redirect()->back()->with('error', '해당 지자체의 메뉴만 수정할 수 있습니다.');
|
||||
return $this->menusRedirect((int) $row->mt_idx)
|
||||
->with('error', '해당 지자체의 메뉴만 수정할 수 있습니다.');
|
||||
}
|
||||
$data = [
|
||||
'mm_name' => (string) $this->request->getPost('mm_name'),
|
||||
@@ -196,7 +213,7 @@ class Menu extends BaseController
|
||||
$this->menuModel->update($id, $data);
|
||||
$this->menuModel->pruneInventoryManagementMenus((int) $row->mt_idx, $lgIdx);
|
||||
$this->menuModel->syncTypeToAllLgs((int) $row->mt_idx, $lgIdx);
|
||||
return redirect()->back()->with('success', '메뉴가 수정되었습니다.');
|
||||
return $this->menusRedirect((int) $row->mt_idx)->with('success', '메뉴가 수정되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,15 +231,16 @@ class Menu extends BaseController
|
||||
}
|
||||
$row = $this->menuModel->find($id);
|
||||
if (! $row || (int) $row->lg_idx !== $lgIdx) {
|
||||
return redirect()->back()->with('error', '해당 지자체의 메뉴만 삭제할 수 있습니다.');
|
||||
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 redirect()->back()->with('success', '메뉴가 삭제되었습니다.');
|
||||
return $this->menusRedirect((int) $row->mt_idx)->with('success', '메뉴가 삭제되었습니다.');
|
||||
}
|
||||
return redirect()->back()->with('error', $result['msg']);
|
||||
return $this->menusRedirect((int) $row->mt_idx)->with('error', $result['msg']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -239,8 +257,9 @@ class Menu extends BaseController
|
||||
->with('error', '지자체를 선택하세요.');
|
||||
}
|
||||
$ids = $this->request->getPost('mm_idx');
|
||||
$postMtIdx = (int) $this->request->getPost('mt_idx');
|
||||
if (! is_array($ids) || empty($ids)) {
|
||||
return redirect()->back()->with('error', '순서를 적용할 메뉴가 없습니다.');
|
||||
return $this->menusRedirect($postMtIdx)->with('error', '순서를 적용할 메뉴가 없습니다.');
|
||||
}
|
||||
$firstId = (int) ($ids[0] ?? 0);
|
||||
$firstRow = $firstId > 0 ? $this->menuModel->find($firstId) : null;
|
||||
@@ -249,7 +268,8 @@ class Menu extends BaseController
|
||||
$this->menuModel->pruneInventoryManagementMenus((int) $firstRow->mt_idx, $lgIdx);
|
||||
$this->menuModel->syncTypeToAllLgs((int) $firstRow->mt_idx, $lgIdx);
|
||||
}
|
||||
return redirect()->back()->with('success', '순서가 적용되었습니다.');
|
||||
$mtIdx = $firstRow ? (int) $firstRow->mt_idx : $postMtIdx;
|
||||
return $this->menusRedirect($mtIdx)->with('success', '순서가 적용되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2582,7 +2582,6 @@ class SalesReport extends BaseController
|
||||
'endDate' => $endDate,
|
||||
'ioType' => $ioType,
|
||||
'queried' => $queried,
|
||||
'exportQuery' => $this->returnsExportQueryString(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -2594,12 +2593,24 @@ class SalesReport extends BaseController
|
||||
return redirect()->to(mgmt_url('reports/returns'))->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
if ($this->request->getGet('search') !== '1') {
|
||||
$startDate = trim((string) ($this->request->getGet('start_date') ?? ''));
|
||||
$endDate = trim((string) ($this->request->getGet('end_date') ?? ''));
|
||||
$hasQuery = $this->request->getGet('search') === '1'
|
||||
|| ($startDate !== '' && $endDate !== '');
|
||||
|
||||
if (! $hasQuery) {
|
||||
return redirect()->to(mgmt_url('reports/returns'))->with('error', '조회 후 엑셀 저장을 이용해 주세요.');
|
||||
}
|
||||
|
||||
$startDate = (string) ($this->request->getGet('start_date') ?? date('Y-m-01'));
|
||||
$endDate = (string) ($this->request->getGet('end_date') ?? date('Y-m-d'));
|
||||
if ($startDate === '') {
|
||||
$startDate = date('Y-m-01');
|
||||
}
|
||||
if ($endDate === '') {
|
||||
$endDate = date('Y-m-d');
|
||||
}
|
||||
if ($startDate > $endDate) {
|
||||
[$startDate, $endDate] = [$endDate, $startDate];
|
||||
}
|
||||
$ioType = (string) ($this->request->getGet('io_type') ?? 'out');
|
||||
if (! in_array($ioType, ['in', 'out'], true)) {
|
||||
$ioType = 'out';
|
||||
@@ -2628,22 +2639,77 @@ class SalesReport extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 출고 = 지정판매소 반품(designated-return · bag_return_scan_code)
|
||||
* 입고 = 물류 입고분 파기(bag_dispose)
|
||||
*
|
||||
* @return list<object>
|
||||
*/
|
||||
private function fetchReturnDisposeRows(int $lgIdx, string $startDate, string $endDate, string $ioType): array
|
||||
{
|
||||
$bsTypes = $ioType === 'in' ? ['return'] : ['cancel'];
|
||||
$typePlaceholders = implode(',', array_fill(0, count($bsTypes), '?'));
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
if ($ioType === 'out') {
|
||||
return $this->fetchDesignatedReturnRows($db, $lgIdx, $startDate, $endDate);
|
||||
}
|
||||
|
||||
return $this->fetchBagDisposeRows($db, $lgIdx, $startDate, $endDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<object>
|
||||
*/
|
||||
private function fetchDesignatedReturnRows($db, int $lgIdx, string $startDate, string $endDate): array
|
||||
{
|
||||
if ($db->tableExists('bag_return_scan_code')) {
|
||||
return $db->query("
|
||||
SELECT r.brsc_return_date AS bs_sale_date,
|
||||
COALESCE(ds.ds_name, '') AS bs_ds_name,
|
||||
r.brsc_bag_code AS bs_bag_code,
|
||||
r.brsc_bag_name AS bs_bag_name,
|
||||
'return' AS bs_type,
|
||||
SUM(r.brsc_qty) AS qty
|
||||
FROM bag_return_scan_code r
|
||||
LEFT JOIN designated_shop ds
|
||||
ON ds.ds_idx = r.brsc_ds_idx AND ds.ds_lg_idx = r.brsc_lg_idx
|
||||
WHERE r.brsc_lg_idx = ?
|
||||
AND r.brsc_return_date BETWEEN ? AND ?
|
||||
AND r.brsc_state = 'returned'
|
||||
GROUP BY r.brsc_return_date, r.brsc_ds_idx, ds.ds_name,
|
||||
r.brsc_bag_code, r.brsc_bag_name
|
||||
ORDER BY r.brsc_return_date ASC, bs_ds_name ASC, r.brsc_bag_code ASC
|
||||
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||
}
|
||||
|
||||
return $db->query("
|
||||
SELECT bs_sale_date, bs_ds_name, bs_bag_code, bs_bag_name, bs_type,
|
||||
ABS(bs_qty) AS qty
|
||||
FROM bag_sale
|
||||
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
|
||||
AND bs_type IN ({$typePlaceholders})
|
||||
AND bs_type = 'return'
|
||||
ORDER BY bs_sale_date ASC, bs_ds_name ASC, bs_bag_code ASC
|
||||
", array_merge([$lgIdx, $startDate, $endDate], $bsTypes))->getResult();
|
||||
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<object>
|
||||
*/
|
||||
private function fetchBagDisposeRows($db, int $lgIdx, string $startDate, string $endDate): array
|
||||
{
|
||||
if (! $db->tableExists('bag_dispose')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $db->query("
|
||||
SELECT bd_dispose_date AS bs_sale_date,
|
||||
bd_location AS bs_ds_name,
|
||||
bd_bag_code AS bs_bag_code,
|
||||
bd_bag_name AS bs_bag_name,
|
||||
'dispose' AS bs_type,
|
||||
bd_qty AS qty
|
||||
FROM bag_dispose
|
||||
WHERE bd_lg_idx = ? AND bd_dispose_date BETWEEN ? AND ?
|
||||
ORDER BY bd_dispose_date ASC, bd_location ASC, bd_bag_code ASC
|
||||
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||
}
|
||||
|
||||
private function returnDisposeKindLabel(object $row): string
|
||||
@@ -2660,24 +2726,13 @@ class SalesReport extends BaseController
|
||||
private function returnDisposeTypeLabel(string $bsType): string
|
||||
{
|
||||
return match ($bsType) {
|
||||
'return' => '반품',
|
||||
'cancel' => '파기',
|
||||
default => $bsType,
|
||||
'return' => '반품',
|
||||
'dispose' => '파기',
|
||||
'cancel' => '파기',
|
||||
default => $bsType,
|
||||
};
|
||||
}
|
||||
|
||||
private function returnsExportQueryString(): string
|
||||
{
|
||||
$params = array_filter([
|
||||
'search' => '1',
|
||||
'start_date' => $this->request->getGet('start_date'),
|
||||
'end_date' => $this->request->getGet('end_date'),
|
||||
'io_type' => $this->request->getGet('io_type'),
|
||||
], static fn ($v) => $v !== null && $v !== '');
|
||||
|
||||
return http_build_query($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* P5-10: LOT 수불 조회 (레거시 w_gd033a — 바코드/봉투번호)
|
||||
*/
|
||||
|
||||
@@ -3571,6 +3571,71 @@ SQL);
|
||||
return $this->render('도움말', 'bag/help', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 매뉴얼(설명서) — 목차 첫 페이지로 이동.
|
||||
*/
|
||||
public function manual(): \CodeIgniter\HTTP\RedirectResponse
|
||||
{
|
||||
$first = (new \App\Libraries\ManualRenderer())->firstSlug();
|
||||
|
||||
return redirect()->to(site_url('bag/manual/' . $first));
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 매뉴얼 개별 페이지 (slug = 화이트리스트). 미등록 slug 는 404.
|
||||
*/
|
||||
public function manualPage(string $slug): string
|
||||
{
|
||||
$renderer = new \App\Libraries\ManualRenderer();
|
||||
$page = $renderer->find($slug);
|
||||
$body = $page !== null ? $renderer->render($slug) : null;
|
||||
if ($page === null || $body === null) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('매뉴얼 문서를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
return $this->render('사용자 매뉴얼 · ' . $page['title'], 'bag/manual', [
|
||||
'pages' => $renderer->pages(),
|
||||
'current' => $slug,
|
||||
'title' => $page['title'],
|
||||
'body' => $body,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 도움말 — 번호알기(봉투번호확인). 코드 → 바코드·인쇄숫자·인식번호.
|
||||
*/
|
||||
public function numberLookup(): string
|
||||
{
|
||||
$code = trim((string) ($this->request->getGet('code') ?? ''));
|
||||
$result = null;
|
||||
$error = '';
|
||||
|
||||
if ($code !== '') {
|
||||
$resolved = (new \App\Libraries\BagNumberLookup())->resolve($code, $this->lgIdx());
|
||||
$result = $resolved;
|
||||
if (! $resolved['ok']) {
|
||||
$error = (string) $resolved['message'];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('번호알기', 'bag/number_lookup', [
|
||||
'code' => $code,
|
||||
'result' => $result,
|
||||
'error' => $error,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 번호알기 AJAX 조회.
|
||||
*/
|
||||
public function numberLookupResolve()
|
||||
{
|
||||
$code = trim((string) ($this->request->getPost('code') ?? $this->request->getGet('code') ?? ''));
|
||||
$data = (new \App\Libraries\BagNumberLookup())->resolve($code, $this->lgIdx());
|
||||
|
||||
return $this->response->setJSON($data);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// CRUD — 사이트 레이아웃으로 등록/처리 폼 제공
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Libraries\GovPortalCodeKindsPage;
|
||||
use App\Models\LocalGovernmentModel;
|
||||
|
||||
class Home extends BaseController
|
||||
@@ -118,6 +119,107 @@ class Home extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공공 포털형(국가재난관리정보시스템 스타일) 메인 시안.
|
||||
* URL: /dashboard/gov-portal — 기능은 요약 대시보드와 동일, UI만 별도 레이아웃.
|
||||
*/
|
||||
public function dashboardGovPortal()
|
||||
{
|
||||
if (! session()->get('logged_in')) {
|
||||
return redirect()->to(base_url('login'));
|
||||
}
|
||||
|
||||
helper('admin');
|
||||
|
||||
return view('home/dashboard_gov_portal', gov_portal_dashboard_view_data($this->resolveLgLabel(), 'base'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 공공 포털형 변형 — 가로 MY MENU·와이드 맵·KPI 띠. URL: /dashboard/gov-portal-strip
|
||||
*/
|
||||
public function dashboardGovPortalStrip()
|
||||
{
|
||||
if (! session()->get('logged_in')) {
|
||||
return redirect()->to(base_url('login'));
|
||||
}
|
||||
|
||||
helper('admin');
|
||||
|
||||
return view('home/_dashboard_gov_portal_strip_layout', array_merge(
|
||||
gov_portal_dashboard_view_data($this->resolveLgLabel(), 'strip'),
|
||||
['stripInnerView' => 'home/_dashboard_gov_portal_strip_home_inner']
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 공공 포털형(기본) — 기본 코드관리 UI 시안. URL: /dashboard/gov-portal/code-kinds
|
||||
*/
|
||||
public function dashboardGovPortalCodeKinds()
|
||||
{
|
||||
return $this->renderGovPortalCodeKinds('base');
|
||||
}
|
||||
|
||||
/**
|
||||
* 공공 포털형(변형 strip) — 기본 코드관리 UI 시안. URL: /dashboard/gov-portal-strip/code-kinds
|
||||
*/
|
||||
public function dashboardGovPortalStripCodeKinds()
|
||||
{
|
||||
return $this->renderGovPortalCodeKinds('strip');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \CodeIgniter\HTTP\RedirectResponse|string
|
||||
*/
|
||||
private function renderGovPortalCodeKinds(string $variant)
|
||||
{
|
||||
if (! session()->get('logged_in')) {
|
||||
return redirect()->to(base_url('login'));
|
||||
}
|
||||
|
||||
helper('admin');
|
||||
|
||||
$portalPath = gov_portal_code_kinds_portal_path($variant);
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
$level = (int) session()->get('mb_level');
|
||||
$filters = [
|
||||
'q_code' => $this->request->getGet('q_code'),
|
||||
'q_name' => $this->request->getGet('q_name'),
|
||||
'q_state' => $this->request->getGet('q_state'),
|
||||
];
|
||||
$builder = new GovPortalCodeKindsPage();
|
||||
$ckIdx = (int) ($this->request->getGet('ck_idx') ?? 0);
|
||||
$pageData = $builder->buildPageData($lgIdx, $level, $lgIdx, $ckIdx, $filters);
|
||||
|
||||
if ($ckIdx === 0 && $pageData['codeKinds'] !== []) {
|
||||
$pageData = $builder->buildPageData(
|
||||
$lgIdx,
|
||||
$level,
|
||||
$lgIdx,
|
||||
(int) $pageData['codeKinds'][0]->ck_idx,
|
||||
$filters
|
||||
);
|
||||
}
|
||||
|
||||
$viewData = array_merge(
|
||||
gov_portal_dashboard_view_data($this->resolveLgLabel(), $variant),
|
||||
$pageData,
|
||||
[
|
||||
'govActiveChildHref' => $portalPath,
|
||||
'pageBaseUrl' => site_url($portalPath),
|
||||
]
|
||||
);
|
||||
|
||||
if ($variant === 'strip') {
|
||||
return view('home/_dashboard_gov_portal_strip_layout', array_merge($viewData, [
|
||||
'stripInnerView' => 'home/_gov_portal_code_kinds_body',
|
||||
'stripIncludeWorkCss' => true,
|
||||
'stripShowProfileLinks' => true,
|
||||
]));
|
||||
}
|
||||
|
||||
return view('home/dashboard_gov_portal_code_kinds', $viewData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 재고 조회(수불) 화면 (목업)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user