Compare commits
3 Commits
main
...
7af4f082c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7af4f082c9 | ||
|
|
f55a6f0146 | ||
|
|
db1afb6e88 |
10
README.md
10
README.md
@@ -6,15 +6,7 @@
|
||||
백엔드는 **[CodeIgniter 4](https://codeigniter.com/)** 기반입니다.
|
||||
|
||||
**저장소:** [wixon-associates/jongryangje](https://github.com/wixon-associates/jongryangje)
|
||||
| **[구현 화면 스크린샷](./docs/SCREENSHOTS.md)** | **[Notion 진행상황](https://www.notion.so/31b42b987c3780baba32ded04a1d41bb)** | **[서버/배포 가이드](./docs/server.md)** |
|
||||
|
||||
### 운영 환경
|
||||
|
||||
| 서비스 | URL |
|
||||
|--------|-----|
|
||||
| 웹 서비스 | https://trash.wxn.co.kr |
|
||||
| Gitea (Git) | https://gitea.wxn.co.kr |
|
||||
| GitHub | https://github.com/wixon-associates/jongryangje |
|
||||
| **[구현 화면 스크린샷](./docs/SCREENSHOTS.md)** | **[Notion 진행상황](https://www.notion.so/31b42b987c3780baba32ded04a1d41bb)** |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
|
||||
/**
|
||||
* 로그인·2차 인증(TOTP) 관련 설정
|
||||
*
|
||||
* .env 예:
|
||||
* auth.requireTotp = true
|
||||
* auth.totpIssuer = "쓰레기봉투 물류시스템"
|
||||
*/
|
||||
class Auth extends BaseConfig
|
||||
{
|
||||
/** 운영·스테이징 true 권장. 로컬 개발 시 false 로 1단계만 로그인 가능 */
|
||||
public bool $requireTotp = true;
|
||||
|
||||
/** 인증 앱에 표시되는 발급자(issuer) */
|
||||
public string $totpIssuer = '쓰레기봉투 물류시스템';
|
||||
|
||||
/** TOTP 연속 실패 시 세션 종료 전 허용 횟수 */
|
||||
public int $totpMaxAttempts = 5;
|
||||
|
||||
/** 비밀번호 통과 후 2단계 완료까지 허용 시간(초) */
|
||||
public int $pending2faTtlSeconds = 600;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$require = env('auth.requireTotp');
|
||||
if ($require !== null && $require !== '') {
|
||||
$this->requireTotp = filter_var($require, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
$issuer = env('auth.totpIssuer');
|
||||
if (is_string($issuer) && $issuer !== '') {
|
||||
$this->totpIssuer = $issuer;
|
||||
}
|
||||
|
||||
$max = env('auth.totpMaxAttempts');
|
||||
if ($max !== null && $max !== '' && is_numeric($max)) {
|
||||
$this->totpMaxAttempts = max(1, (int) $max);
|
||||
}
|
||||
|
||||
$ttl = env('auth.pending2faTtlSeconds');
|
||||
if ($ttl !== null && $ttl !== '' && is_numeric($ttl)) {
|
||||
$this->pending2faTtlSeconds = max(60, (int) $ttl);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@ class Pager extends BaseConfig
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public array $templates = [
|
||||
'default_full' => 'App\Views\components\pager',
|
||||
'default_simple' => 'App\Views\components\pager',
|
||||
'default_full' => 'CodeIgniter\Pager\Views\default_full',
|
||||
'default_simple' => 'CodeIgniter\Pager\Views\default_simple',
|
||||
'default_head' => 'CodeIgniter\Pager\Views\default_head',
|
||||
];
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ class Roles extends BaseConfig
|
||||
* mb_level 상수 (member.mb_level)
|
||||
*/
|
||||
public const LEVEL_SUPER_ADMIN = 4;
|
||||
/** 본부 관리자 — 현재는 super admin과 동일한 관리자 권한(지자체 선택 후 작업). 추후 super 전용 기능 분리 시 여기만 조정 */
|
||||
public const LEVEL_HEADQUARTERS_ADMIN = 5;
|
||||
public const LEVEL_LOCAL_ADMIN = 3; // 지자체관리자
|
||||
public const LEVEL_SHOP = 2; // 지정판매소
|
||||
public const LEVEL_CITIZEN = 1; // 일반 사용자(시민)
|
||||
@@ -31,27 +29,8 @@ class Roles extends BaseConfig
|
||||
self::LEVEL_SHOP => '지정판매소',
|
||||
self::LEVEL_LOCAL_ADMIN => '지자체관리자',
|
||||
self::LEVEL_SUPER_ADMIN => 'super admin',
|
||||
self::LEVEL_HEADQUARTERS_ADMIN => '본부 관리자',
|
||||
];
|
||||
|
||||
/**
|
||||
* super admin(4) 또는 본부 관리자(5) — 동일 관리자 UX(지자체 선택 등)에 사용
|
||||
*/
|
||||
public static function isSuperAdminEquivalent(int $level): bool
|
||||
{
|
||||
return $level === self::LEVEL_SUPER_ADMIN || $level === self::LEVEL_HEADQUARTERS_ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* TOTP 2차 인증 적용 대상 (지자체·super·본부 관리자)
|
||||
*/
|
||||
public static function requiresTotp(int $level): bool
|
||||
{
|
||||
return $level === self::LEVEL_LOCAL_ADMIN
|
||||
|| $level === self::LEVEL_SUPER_ADMIN
|
||||
|| $level === self::LEVEL_HEADQUARTERS_ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 자체 회원가입 시 기본 역할 (mb_level)
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,6 @@ $routes->get('dashboard/classic-mock', 'Home::dashboardClassicMock');
|
||||
$routes->get('dashboard/modern', 'Home::dashboardModern');
|
||||
$routes->get('dashboard/dense', 'Home::dashboardDense');
|
||||
$routes->get('dashboard/charts', 'Home::dashboardCharts');
|
||||
$routes->get('dashboard/blend', 'Home::dashboardBlend');
|
||||
$routes->get('bag/inventory-inquiry', 'Home::inventoryInquiry');
|
||||
$routes->get('bag/waste-suibal-enterprise', 'Home::wasteSuibalEnterprise');
|
||||
|
||||
@@ -27,28 +26,9 @@ $routes->get('bag/analytics', 'Bag::analytics');
|
||||
$routes->get('bag/window', 'Bag::window');
|
||||
$routes->get('bag/help', 'Bag::help');
|
||||
|
||||
// 사이트 메뉴 CRUD (사이트 레이아웃)
|
||||
$routes->get('bag/inventory/adjust', 'Bag::inventoryAdjust');
|
||||
$routes->post('bag/inventory/adjust', 'Bag::inventoryAdjustStore');
|
||||
$routes->get('bag/issue/create', 'Bag::issueCreate');
|
||||
$routes->post('bag/issue/store', 'Bag::issueStore');
|
||||
$routes->post('bag/issue/cancel/(:num)', 'Bag::issueCancel/$1');
|
||||
$routes->get('bag/order/create', 'Bag::orderCreate');
|
||||
$routes->post('bag/order/store', 'Bag::orderStore');
|
||||
$routes->get('bag/receiving/create', 'Bag::receivingCreate');
|
||||
$routes->post('bag/receiving/store', 'Bag::receivingStore');
|
||||
$routes->get('bag/sale/create', 'Bag::saleCreate');
|
||||
$routes->post('bag/sale/store', 'Bag::saleStore');
|
||||
$routes->get('bag/shop-order/create', 'Bag::shopOrderCreate');
|
||||
$routes->post('bag/shop-order/store', 'Bag::shopOrderStore');
|
||||
|
||||
// Auth
|
||||
$routes->get('login', 'Auth::showLoginForm');
|
||||
$routes->post('login', 'Auth::login');
|
||||
$routes->get('login/two-factor', 'Auth::showTwoFactor');
|
||||
$routes->post('login/two-factor', 'Auth::verifyTwoFactor');
|
||||
$routes->get('login/totp-setup', 'Auth::showTotpSetup');
|
||||
$routes->post('login/totp-setup', 'Auth::completeTotpSetup');
|
||||
$routes->get('logout', 'Auth::logout');
|
||||
$routes->get('register', 'Auth::showRegisterForm');
|
||||
$routes->post('register', 'Auth::register');
|
||||
@@ -114,7 +94,6 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo
|
||||
$routes->get('bag-prices/history/(:num)', 'Admin\BagPrice::history/$1');
|
||||
|
||||
// 발주 관리 (P3-01~05)
|
||||
$routes->get('bag-orders/export', 'Admin\BagOrder::export');
|
||||
$routes->get('bag-orders', 'Admin\BagOrder::index');
|
||||
$routes->get('bag-orders/create', 'Admin\BagOrder::create');
|
||||
$routes->post('bag-orders/store', 'Admin\BagOrder::store');
|
||||
@@ -128,7 +107,6 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo
|
||||
$routes->post('bag-receivings/store', 'Admin\BagReceiving::store');
|
||||
|
||||
// 재고 현황 (P3-10)
|
||||
$routes->get('bag-inventory/export', 'Admin\BagInventory::export');
|
||||
$routes->get('bag-inventory', 'Admin\BagInventory::index');
|
||||
|
||||
// 주문 접수 관리 (P4-01~03)
|
||||
@@ -138,7 +116,6 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo
|
||||
$routes->post('shop-orders/cancel/(:num)', 'Admin\ShopOrder::cancel/$1');
|
||||
|
||||
// 판매/반품 관리 (P4-04~07)
|
||||
$routes->get('bag-sales/export', 'Admin\BagSale::export');
|
||||
$routes->get('bag-sales', 'Admin\BagSale::index');
|
||||
$routes->get('bag-sales/create', 'Admin\BagSale::create');
|
||||
$routes->post('bag-sales/store', 'Admin\BagSale::store');
|
||||
@@ -163,13 +140,6 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo
|
||||
$routes->get('reports/daily-summary', 'Admin\SalesReport::dailySummary');
|
||||
$routes->get('reports/period-sales', 'Admin\SalesReport::periodSales');
|
||||
$routes->get('reports/supply-demand', 'Admin\SalesReport::supplyDemand');
|
||||
$routes->get('reports/yearly-sales', 'Admin\SalesReport::yearlySales');
|
||||
$routes->get('reports/shop-sales', 'Admin\SalesReport::shopSales');
|
||||
$routes->get('reports/hometax-export', 'Admin\SalesReport::hometaxExport');
|
||||
$routes->get('reports/returns', 'Admin\SalesReport::returns');
|
||||
$routes->get('reports/lot-flow', 'Admin\SalesReport::lotFlow');
|
||||
$routes->get('reports/misc-flow', 'Admin\SalesReport::miscFlow');
|
||||
$routes->post('reports/misc-flow', 'Admin\SalesReport::miscFlowStore');
|
||||
|
||||
// 판매 대행소 관리 (P2-07/08)
|
||||
$routes->get('sales-agencies', 'Admin\SalesAgency::index');
|
||||
@@ -203,9 +173,6 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo
|
||||
$routes->post('free-recipients/update/(:num)', 'Admin\FreeRecipient::update/$1');
|
||||
$routes->post('free-recipients/delete/(:num)', 'Admin\FreeRecipient::delete/$1');
|
||||
|
||||
$routes->get('designated-shops/export', 'Admin\DesignatedShop::export');
|
||||
$routes->get('designated-shops/map', 'Admin\DesignatedShop::map');
|
||||
$routes->get('designated-shops/status', 'Admin\DesignatedShop::status');
|
||||
$routes->get('designated-shops', 'Admin\DesignatedShop::index');
|
||||
$routes->get('designated-shops/create', 'Admin\DesignatedShop::create');
|
||||
$routes->post('designated-shops/store', 'Admin\DesignatedShop::store');
|
||||
|
||||
@@ -31,7 +31,8 @@ class Access extends BaseController
|
||||
{
|
||||
$start = $this->request->getGet('start');
|
||||
$end = $this->request->getGet('end');
|
||||
$builder = $this->memberLogModel;
|
||||
$builder = $this->memberLogModel->builder();
|
||||
$builder->select('member_log.*');
|
||||
$builder->orderBy('mll_regdate', 'DESC');
|
||||
if ($start !== null && $start !== '') {
|
||||
$builder->where('mll_regdate >=', $start . ' 00:00:00');
|
||||
@@ -39,11 +40,10 @@ class Access extends BaseController
|
||||
if ($end !== null && $end !== '') {
|
||||
$builder->where('mll_regdate <=', $end . ' 23:59:59');
|
||||
}
|
||||
$list = $builder->paginate(20);
|
||||
$pager = $this->memberLogModel->pager;
|
||||
$list = $builder->get()->getResult();
|
||||
return view('admin/layout', [
|
||||
'title' => '로그인 이력',
|
||||
'content' => view('admin/access/login_history', ['list' => $list, 'start' => $start, 'end' => $end, 'pager' => $pager]),
|
||||
'content' => view('admin/access/login_history', ['list' => $list, 'start' => $start, 'end' => $end]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -59,14 +59,15 @@ class Access extends BaseController
|
||||
$status = MemberApprovalRequestModel::STATUS_PENDING;
|
||||
}
|
||||
|
||||
$list = $this->approvalModel
|
||||
->select('member_approval_request.*, member.mb_id, member.mb_name, member.mb_lg_idx, local_government.lg_name')
|
||||
->join('member', 'member.mb_idx = member_approval_request.mb_idx', 'left')
|
||||
->join('local_government', 'local_government.lg_idx = member.mb_lg_idx', 'left')
|
||||
->where('member_approval_request.mar_status', $status)
|
||||
->orderBy('member_approval_request.mar_requested_at', 'DESC')
|
||||
->paginate(20);
|
||||
$pager = $this->approvalModel->pager;
|
||||
$builder = $this->approvalModel->builder();
|
||||
$builder->select(
|
||||
'member_approval_request.*, member.mb_id, member.mb_name, member.mb_lg_idx, local_government.lg_name'
|
||||
);
|
||||
$builder->join('member', 'member.mb_idx = member_approval_request.mb_idx', 'left');
|
||||
$builder->join('local_government', 'local_government.lg_idx = member.mb_lg_idx', 'left');
|
||||
$builder->where('member_approval_request.mar_status', $status);
|
||||
$builder->orderBy('member_approval_request.mar_requested_at', 'DESC');
|
||||
$list = $builder->get()->getResult();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '승인 대기',
|
||||
@@ -74,7 +75,6 @@ class Access extends BaseController
|
||||
'list' => $list,
|
||||
'status' => $status,
|
||||
'roles' => $this->roles,
|
||||
'pager' => $pager,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -90,8 +90,8 @@ class Access extends BaseController
|
||||
}
|
||||
|
||||
$requestedLevel = (int) $requestRow->mar_requested_level;
|
||||
if ($requestedLevel === Roles::LEVEL_SUPER_ADMIN || $requestedLevel === Roles::LEVEL_HEADQUARTERS_ADMIN) {
|
||||
return redirect()->to(site_url('admin/access/approvals'))->with('error', '상위 관리자 역할 요청은 승인할 수 없습니다.');
|
||||
if ($requestedLevel === Roles::LEVEL_SUPER_ADMIN) {
|
||||
return redirect()->to(site_url('admin/access/approvals'))->with('error', 'super admin 역할 요청은 승인할 수 없습니다.');
|
||||
}
|
||||
|
||||
$db = db_connect();
|
||||
|
||||
@@ -13,41 +13,11 @@ class BagInventory extends BaseController
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
|
||||
$invModel = model(BagInventoryModel::class);
|
||||
$list = $invModel->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->paginate(20);
|
||||
$pager = $invModel->pager;
|
||||
$list = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '재고 현황',
|
||||
'content' => view('admin/bag_inventory/index', ['list' => $list, 'pager' => $pager]),
|
||||
'content' => view('admin/bag_inventory/index', ['list' => $list]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function export()
|
||||
{
|
||||
helper(['admin', 'export']);
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) {
|
||||
return redirect()->to(site_url('admin/bag-inventory'))->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
$list = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->findAll();
|
||||
|
||||
$rows = [];
|
||||
foreach ($list as $row) {
|
||||
$rows[] = [
|
||||
$row->bi_idx,
|
||||
$row->bi_bag_code,
|
||||
$row->bi_bag_name,
|
||||
(int) $row->bi_qty,
|
||||
$row->bi_updated_at,
|
||||
];
|
||||
}
|
||||
|
||||
export_csv(
|
||||
'재고현황_' . date('Ymd') . '.csv',
|
||||
['번호', '봉투코드', '봉투명', '현재재고(낱장)', '최종갱신'],
|
||||
$rows
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,11 @@ class BagIssue extends BaseController
|
||||
if ($startDate) $builder->where('bi2_issue_date >=', $startDate);
|
||||
if ($endDate) $builder->where('bi2_issue_date <=', $endDate);
|
||||
|
||||
$list = $builder->orderBy('bi2_issue_date', 'DESC')->orderBy('bi2_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->issueModel->pager;
|
||||
$list = $builder->orderBy('bi2_issue_date', 'DESC')->orderBy('bi2_idx', 'DESC')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '무료용 불출 관리',
|
||||
'content' => view('admin/bag_issue/index', compact('list', 'startDate', 'endDate', 'pager')),
|
||||
'content' => view('admin/bag_issue/index', compact('list', 'startDate', 'endDate')),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -78,7 +77,7 @@ class BagIssue extends BaseController
|
||||
$db = \Config\Database::connect();
|
||||
$db->transStart();
|
||||
|
||||
$issueData = [
|
||||
$this->issueModel->insert([
|
||||
'bi2_lg_idx' => $lgIdx,
|
||||
'bi2_year' => (int) $this->request->getPost('bi2_year'),
|
||||
'bi2_quarter' => (int) $this->request->getPost('bi2_quarter'),
|
||||
@@ -91,13 +90,7 @@ class BagIssue extends BaseController
|
||||
'bi2_qty' => $qty,
|
||||
'bi2_status' => 'normal',
|
||||
'bi2_regdate' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
$this->issueModel->insert($issueData);
|
||||
$bi2Idx = (int) $this->issueModel->getInsertID();
|
||||
|
||||
// CT-05: 감사 로그
|
||||
helper('audit');
|
||||
audit_log('create', 'bag_issue', $bi2Idx, null, array_merge($issueData, ['bi2_idx' => $bi2Idx]));
|
||||
]);
|
||||
|
||||
// 재고 감산
|
||||
model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $bagName, -$qty);
|
||||
@@ -118,11 +111,7 @@ class BagIssue extends BaseController
|
||||
$db = \Config\Database::connect();
|
||||
$db->transStart();
|
||||
|
||||
$before = (array) $item;
|
||||
$this->issueModel->update($id, ['bi2_status' => 'cancelled']);
|
||||
// CT-05: 감사 로그
|
||||
helper('audit');
|
||||
audit_log('update', 'bag_issue', $id, $before, ['bi2_status' => 'cancelled']);
|
||||
// 재고 복원
|
||||
model(BagInventoryModel::class)->adjustQty((int) $item->bi2_lg_idx, $item->bi2_bag_code, $item->bi2_bag_name, (int) $item->bi2_qty);
|
||||
|
||||
|
||||
@@ -42,8 +42,7 @@ class BagOrder extends BaseController
|
||||
if ($endDate) $builder->where('bo_order_date <=', $endDate);
|
||||
if ($status) $builder->where('bo_status', $status);
|
||||
|
||||
$list = $builder->orderBy('bo_order_date', 'DESC')->orderBy('bo_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->orderModel->pager;
|
||||
$list = $builder->orderBy('bo_order_date', 'DESC')->orderBy('bo_idx', 'DESC')->findAll();
|
||||
|
||||
// 발주별 품목 합계
|
||||
$itemSummary = [];
|
||||
@@ -61,56 +60,10 @@ class BagOrder extends BaseController
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '발주 현황',
|
||||
'content' => view('admin/bag_order/index', compact('list', 'itemSummary', 'companyMap', 'agencyMap', 'startDate', 'endDate', 'status', 'pager')),
|
||||
'content' => view('admin/bag_order/index', compact('list', 'itemSummary', 'companyMap', 'agencyMap', 'startDate', 'endDate', 'status')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function export()
|
||||
{
|
||||
helper(['admin', 'export']);
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) {
|
||||
return redirect()->to(site_url('admin/bag-orders'))->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
$builder = $this->orderModel->where('bo_lg_idx', $lgIdx);
|
||||
$startDate = $this->request->getGet('start_date');
|
||||
$endDate = $this->request->getGet('end_date');
|
||||
$status = $this->request->getGet('status');
|
||||
if ($startDate) $builder->where('bo_order_date >=', $startDate);
|
||||
if ($endDate) $builder->where('bo_order_date <=', $endDate);
|
||||
if ($status) $builder->where('bo_status', $status);
|
||||
|
||||
$list = $builder->orderBy('bo_order_date', 'DESC')->orderBy('bo_idx', 'DESC')->findAll();
|
||||
|
||||
$rows = [];
|
||||
$statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제'];
|
||||
foreach ($list as $row) {
|
||||
$items = $this->itemModel->where('boi_bo_idx', $row->bo_idx)->findAll();
|
||||
$totalQty = 0;
|
||||
$totalAmt = 0;
|
||||
foreach ($items as $it) {
|
||||
$totalQty += (int) $it->boi_qty_sheet;
|
||||
$totalAmt += (float) $it->boi_amount;
|
||||
}
|
||||
$rows[] = [
|
||||
$row->bo_idx,
|
||||
$row->bo_lot_no,
|
||||
$row->bo_order_date,
|
||||
count($items),
|
||||
$totalQty,
|
||||
$totalAmt,
|
||||
$statusMap[$row->bo_status] ?? $row->bo_status,
|
||||
];
|
||||
}
|
||||
|
||||
export_csv(
|
||||
'발주현황_' . date('Ymd') . '.csv',
|
||||
['번호', 'LOT번호', '발주일', '품목수', '총수량', '총금액', '상태'],
|
||||
$rows
|
||||
);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
helper('admin');
|
||||
@@ -179,10 +132,6 @@ class BagOrder extends BaseController
|
||||
$this->orderModel->insert($orderData);
|
||||
$boIdx = (int) $this->orderModel->getInsertID();
|
||||
|
||||
// CT-05: 감사 로그
|
||||
helper('audit');
|
||||
audit_log('create', 'bag_order', $boIdx, null, array_merge($orderData, ['bo_idx' => $boIdx]));
|
||||
|
||||
// 품목 저장
|
||||
$bagCodes = $this->request->getPost('item_bag_code') ?? [];
|
||||
$qtyBoxes = $this->request->getPost('item_qty_box') ?? [];
|
||||
@@ -254,10 +203,7 @@ class BagOrder extends BaseController
|
||||
return redirect()->to(site_url('admin/bag-orders'))->with('error', '발주를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
$before = (array) $order;
|
||||
$this->orderModel->update($id, ['bo_status' => 'cancelled', 'bo_moddate' => date('Y-m-d H:i:s')]);
|
||||
helper('audit');
|
||||
audit_log('update', 'bag_order', $id, $before, ['bo_status' => 'cancelled']);
|
||||
return redirect()->to(site_url('admin/bag-orders'))->with('success', '발주가 취소되었습니다.');
|
||||
}
|
||||
|
||||
@@ -269,10 +215,7 @@ class BagOrder extends BaseController
|
||||
return redirect()->to(site_url('admin/bag-orders'))->with('error', '발주를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
$before = (array) $order;
|
||||
$this->orderModel->update($id, ['bo_status' => 'deleted', 'bo_moddate' => date('Y-m-d H:i:s')]);
|
||||
helper('audit');
|
||||
audit_log('delete', 'bag_order', $id, $before, ['bo_status' => 'deleted']);
|
||||
return redirect()->to(site_url('admin/bag-orders'))->with('success', '발주가 삭제 처리되었습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +42,7 @@ class BagPrice extends BaseController
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$list = $builder->orderBy('bp_bag_code', 'ASC')->orderBy('bp_start_date', 'DESC')->paginate(20);
|
||||
$pager = $this->priceModel->pager;
|
||||
$list = $builder->orderBy('bp_bag_code', 'ASC')->orderBy('bp_start_date', 'DESC')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '봉투 단가 관리',
|
||||
@@ -51,7 +50,6 @@ class BagPrice extends BaseController
|
||||
'list' => $list,
|
||||
'startDate' => $startDate,
|
||||
'endDate' => $endDate,
|
||||
'pager' => $pager,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -30,12 +30,11 @@ class BagReceiving extends BaseController
|
||||
if ($startDate) $builder->where('br_receive_date >=', $startDate);
|
||||
if ($endDate) $builder->where('br_receive_date <=', $endDate);
|
||||
|
||||
$list = $builder->orderBy('br_receive_date', 'DESC')->orderBy('br_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->recvModel->pager;
|
||||
$list = $builder->orderBy('br_receive_date', 'DESC')->orderBy('br_idx', 'DESC')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '입고 현황',
|
||||
'content' => view('admin/bag_receiving/index', compact('list', 'startDate', 'endDate', 'pager')),
|
||||
'content' => view('admin/bag_receiving/index', compact('list', 'startDate', 'endDate')),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,56 +33,14 @@ class BagSale extends BaseController
|
||||
if ($endDate) $builder->where('bs_sale_date <=', $endDate);
|
||||
if ($type) $builder->where('bs_type', $type);
|
||||
|
||||
$list = $builder->orderBy('bs_sale_date', 'DESC')->orderBy('bs_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->saleModel->pager;
|
||||
$list = $builder->orderBy('bs_sale_date', 'DESC')->orderBy('bs_idx', 'DESC')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '판매/반품 관리',
|
||||
'content' => view('admin/bag_sale/index', compact('list', 'startDate', 'endDate', 'type', 'pager')),
|
||||
'content' => view('admin/bag_sale/index', compact('list', 'startDate', 'endDate', 'type')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function export()
|
||||
{
|
||||
helper(['admin', 'export']);
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) {
|
||||
return redirect()->to(site_url('admin/bag-sales'))->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
$builder = $this->saleModel->where('bs_lg_idx', $lgIdx);
|
||||
$startDate = $this->request->getGet('start_date');
|
||||
$endDate = $this->request->getGet('end_date');
|
||||
$type = $this->request->getGet('type');
|
||||
if ($startDate) $builder->where('bs_sale_date >=', $startDate);
|
||||
if ($endDate) $builder->where('bs_sale_date <=', $endDate);
|
||||
if ($type) $builder->where('bs_type', $type);
|
||||
|
||||
$list = $builder->orderBy('bs_sale_date', 'DESC')->orderBy('bs_idx', 'DESC')->findAll();
|
||||
|
||||
$typeMap = ['sale' => '판매', 'return' => '반품', 'cancel' => '취소'];
|
||||
$rows = [];
|
||||
foreach ($list as $row) {
|
||||
$rows[] = [
|
||||
$row->bs_idx,
|
||||
$row->bs_ds_name,
|
||||
$row->bs_sale_date,
|
||||
$row->bs_bag_code,
|
||||
$row->bs_bag_name,
|
||||
(int) $row->bs_qty,
|
||||
(int) $row->bs_unit_price,
|
||||
(int) $row->bs_amount,
|
||||
$typeMap[$row->bs_type] ?? $row->bs_type,
|
||||
];
|
||||
}
|
||||
|
||||
export_csv(
|
||||
'판매반품_' . date('Ymd') . '.csv',
|
||||
['번호', '판매소', '판매일', '봉투코드', '봉투명', '수량', '단가', '금액', '구분'],
|
||||
$rows
|
||||
);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
helper('admin');
|
||||
@@ -131,7 +89,7 @@ class BagSale extends BaseController
|
||||
$db = \Config\Database::connect();
|
||||
$db->transStart();
|
||||
|
||||
$saleData = [
|
||||
$this->saleModel->insert([
|
||||
'bs_lg_idx' => $lgIdx,
|
||||
'bs_ds_idx' => $dsIdx,
|
||||
'bs_ds_name' => $shop ? $shop->ds_name : '',
|
||||
@@ -143,13 +101,7 @@ class BagSale extends BaseController
|
||||
'bs_amount' => $unitPrice * abs($actualQty),
|
||||
'bs_type' => $type,
|
||||
'bs_regdate' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
$this->saleModel->insert($saleData);
|
||||
$bsIdx = (int) $this->saleModel->getInsertID();
|
||||
|
||||
// CT-05: 감사 로그
|
||||
helper('audit');
|
||||
audit_log('create', 'bag_sale', $bsIdx, null, array_merge($saleData, ['bs_idx' => $bsIdx]));
|
||||
]);
|
||||
|
||||
// 재고 감산(판매) / 가산(반품)
|
||||
model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $detail ? $detail->cd_name : '', -$actualQty);
|
||||
|
||||
@@ -24,15 +24,13 @@ class CodeDetail extends BaseController
|
||||
return redirect()->to(site_url('admin/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
$list = $this->detailModel->where('cd_ck_idx', $ckIdx)->orderBy('cd_sort', 'ASC')->paginate(20);
|
||||
$pager = $this->detailModel->pager;
|
||||
$list = $this->detailModel->getByKind($ckIdx);
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '세부코드 관리 — ' . $kind->ck_name . ' (' . $kind->ck_code . ')',
|
||||
'content' => view('admin/code_detail/index', [
|
||||
'kind' => $kind,
|
||||
'list' => $list,
|
||||
'pager' => $pager,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ class CodeKind extends BaseController
|
||||
|
||||
public function index()
|
||||
{
|
||||
$list = $this->kindModel->orderBy('ck_code', 'ASC')->paginate(20);
|
||||
$pager = $this->kindModel->pager;
|
||||
$list = $this->kindModel->orderBy('ck_code', 'ASC')->findAll();
|
||||
|
||||
// 세부코드 수 매핑
|
||||
$detailModel = model(CodeDetailModel::class);
|
||||
@@ -33,7 +32,6 @@ class CodeKind extends BaseController
|
||||
'content' => view('admin/code_kind/index', [
|
||||
'list' => $list,
|
||||
'countMap' => $countMap,
|
||||
'pager' => $pager,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -22,12 +22,11 @@ class Company extends BaseController
|
||||
return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
$list = $this->model->where('cp_lg_idx', $lgIdx)->orderBy('cp_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->model->pager;
|
||||
$list = $this->model->where('cp_lg_idx', $lgIdx)->orderBy('cp_idx', 'DESC')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '업체 관리',
|
||||
'content' => view('admin/company/index', ['list' => $list, 'pager' => $pager]),
|
||||
'content' => view('admin/company/index', ['list' => $list]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
@@ -10,82 +8,9 @@ class Dashboard extends BaseController
|
||||
{
|
||||
public function index(): string
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
|
||||
$stats = [
|
||||
'order_count' => 0,
|
||||
'order_amount' => 0,
|
||||
'sale_count' => 0,
|
||||
'sale_amount' => 0,
|
||||
'inventory_count' => 0,
|
||||
'issue_count_month'=> 0,
|
||||
'recent_orders' => [],
|
||||
'recent_sales' => [],
|
||||
];
|
||||
|
||||
if ($lgIdx) {
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
// 총 발주 건수/금액
|
||||
$orderStats = $db->query("
|
||||
SELECT COUNT(*) as cnt,
|
||||
COALESCE(SUM(sub.total_amt), 0) as total_amount
|
||||
FROM bag_order bo
|
||||
LEFT JOIN (
|
||||
SELECT boi_bo_idx, SUM(boi_amount) as total_amt
|
||||
FROM bag_order_item GROUP BY boi_bo_idx
|
||||
) sub ON sub.boi_bo_idx = bo.bo_idx
|
||||
WHERE bo.bo_lg_idx = ? AND bo.bo_status = 'normal'
|
||||
", [$lgIdx])->getRow();
|
||||
$stats['order_count'] = (int) ($orderStats->cnt ?? 0);
|
||||
$stats['order_amount'] = (int) ($orderStats->total_amount ?? 0);
|
||||
|
||||
// 총 판매 건수/금액
|
||||
$saleStats = $db->query("
|
||||
SELECT COUNT(*) as cnt, COALESCE(SUM(bs_amount), 0) as total_amount
|
||||
FROM bag_sale
|
||||
WHERE bs_lg_idx = ? AND bs_type = 'sale'
|
||||
", [$lgIdx])->getRow();
|
||||
$stats['sale_count'] = (int) ($saleStats->cnt ?? 0);
|
||||
$stats['sale_amount'] = (int) ($saleStats->total_amount ?? 0);
|
||||
|
||||
// 현재 재고 품목 수
|
||||
$invCount = $db->query("
|
||||
SELECT COUNT(*) as cnt FROM bag_inventory WHERE bi_lg_idx = ? AND bi_qty > 0
|
||||
", [$lgIdx])->getRow();
|
||||
$stats['inventory_count'] = (int) ($invCount->cnt ?? 0);
|
||||
|
||||
// 이번 달 불출 건수
|
||||
$monthStart = date('Y-m-01');
|
||||
$issueCount = $db->query("
|
||||
SELECT COUNT(*) as cnt FROM bag_issue
|
||||
WHERE bi2_lg_idx = ? AND bi2_status = 'normal' AND bi2_issue_date >= ?
|
||||
", [$lgIdx, $monthStart])->getRow();
|
||||
$stats['issue_count_month'] = (int) ($issueCount->cnt ?? 0);
|
||||
|
||||
// 최근 발주 5건
|
||||
$stats['recent_orders'] = $db->query("
|
||||
SELECT bo_idx, bo_lot_no, bo_order_date, bo_status
|
||||
FROM bag_order
|
||||
WHERE bo_lg_idx = ?
|
||||
ORDER BY bo_order_date DESC, bo_idx DESC
|
||||
LIMIT 5
|
||||
", [$lgIdx])->getResult();
|
||||
|
||||
// 최근 판매 5건
|
||||
$stats['recent_sales'] = $db->query("
|
||||
SELECT bs_idx, bs_ds_name, bs_bag_name, bs_qty, bs_amount, bs_sale_date, bs_type
|
||||
FROM bag_sale
|
||||
WHERE bs_lg_idx = ?
|
||||
ORDER BY bs_sale_date DESC, bs_idx DESC
|
||||
LIMIT 5
|
||||
", [$lgIdx])->getResult();
|
||||
}
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '대시보드',
|
||||
'content' => view('admin/dashboard/index', ['stats' => $stats, 'lgIdx' => $lgIdx]),
|
||||
'content' => view('admin/dashboard/index'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class DesignatedShop extends BaseController
|
||||
|
||||
private function isSuperAdmin(): bool
|
||||
{
|
||||
return Roles::isSuperAdminEquivalent((int) session()->get('mb_level'));
|
||||
return (int) session()->get('mb_level') === Roles::LEVEL_SUPER_ADMIN;
|
||||
}
|
||||
|
||||
private function isLocalAdmin(): bool
|
||||
@@ -43,25 +43,10 @@ class DesignatedShop extends BaseController
|
||||
->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;
|
||||
$list = $this->shopModel
|
||||
->where('ds_lg_idx', $lgIdx)
|
||||
->orderBy('ds_idx', 'DESC')
|
||||
->findAll();
|
||||
|
||||
// 지자체 이름 매핑용
|
||||
$lgMap = [];
|
||||
@@ -69,58 +54,15 @@ class DesignatedShop extends BaseController
|
||||
$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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 지정판매소 등록 폼 (효과 지자체 기준)
|
||||
*/
|
||||
@@ -339,78 +281,6 @@ class DesignatedShop extends BaseController
|
||||
->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
|
||||
|
||||
@@ -30,12 +30,11 @@ class FreeRecipient extends BaseController
|
||||
return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
$list = $this->model->where('fr_lg_idx', $lgIdx)->orderBy('fr_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->model->pager;
|
||||
$list = $this->model->where('fr_lg_idx', $lgIdx)->orderBy('fr_idx', 'DESC')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '무료용 대상자 관리',
|
||||
'content' => view('admin/free_recipient/index', ['list' => $list, 'pager' => $pager]),
|
||||
'content' => view('admin/free_recipient/index', ['list' => $list]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class LocalGovernment extends BaseController
|
||||
|
||||
private function isSuperAdmin(): bool
|
||||
{
|
||||
return Roles::isSuperAdminEquivalent((int) session()->get('mb_level'));
|
||||
return (int) session()->get('mb_level') === Roles::LEVEL_SUPER_ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,15 +29,14 @@ class LocalGovernment extends BaseController
|
||||
{
|
||||
if (! $this->isSuperAdmin()) {
|
||||
return redirect()->to(site_url('admin'))
|
||||
->with('error', '지자체 관리는 상위 관리자만 접근할 수 있습니다.');
|
||||
->with('error', '지자체 관리는 super admin만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
$list = $this->lgModel->orderBy('lg_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->lgModel->pager;
|
||||
$list = $this->lgModel->orderBy('lg_idx', 'DESC')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '지자체 관리',
|
||||
'content' => view('admin/local_government/index', ['list' => $list, 'pager' => $pager]),
|
||||
'content' => view('admin/local_government/index', ['list' => $list]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -48,7 +47,7 @@ class LocalGovernment extends BaseController
|
||||
{
|
||||
if (! $this->isSuperAdmin()) {
|
||||
return redirect()->to(site_url('admin/local-governments'))
|
||||
->with('error', '지자체 등록은 상위 관리자만 가능합니다.');
|
||||
->with('error', '지자체 등록은 super admin만 가능합니다.');
|
||||
}
|
||||
|
||||
return view('admin/layout', [
|
||||
@@ -64,7 +63,7 @@ class LocalGovernment extends BaseController
|
||||
{
|
||||
if (! $this->isSuperAdmin()) {
|
||||
return redirect()->to(site_url('admin/local-governments'))
|
||||
->with('error', '지자체 등록은 상위 관리자만 가능합니다.');
|
||||
->with('error', '지자체 등록은 super admin만 가능합니다.');
|
||||
}
|
||||
|
||||
$rules = [
|
||||
|
||||
@@ -30,12 +30,11 @@ class Manager extends BaseController
|
||||
return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
$list = $this->model->where('mg_lg_idx', $lgIdx)->orderBy('mg_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->model->pager;
|
||||
$list = $this->model->where('mg_lg_idx', $lgIdx)->orderBy('mg_idx', 'DESC')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '담당자 관리',
|
||||
'content' => view('admin/manager/index', ['list' => $list, 'pager' => $pager]),
|
||||
'content' => view('admin/manager/index', ['list' => $list]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -205,8 +205,8 @@ class Menu extends BaseController
|
||||
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));
|
||||
// super admin(4)은 DB 저장 대상 아님. 1,2,3은 그대로 저장
|
||||
$levels = array_filter($levels, static fn ($v) => $v > 0 && $v !== \Config\Roles::LEVEL_SUPER_ADMIN);
|
||||
|
||||
return implode(',', array_values($levels));
|
||||
}
|
||||
|
||||
@@ -38,13 +38,12 @@ class PackagingUnit extends BaseController
|
||||
$builder->groupStart()->where('pu_end_date IS NULL')->orWhere('pu_end_date <=', $endDate)->groupEnd();
|
||||
}
|
||||
|
||||
$list = $builder->orderBy('pu_bag_code', 'ASC')->orderBy('pu_start_date', 'DESC')->paginate(20);
|
||||
$pager = $this->unitModel->pager;
|
||||
$list = $builder->orderBy('pu_bag_code', 'ASC')->orderBy('pu_start_date', 'DESC')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '포장 단위 관리',
|
||||
'content' => view('admin/packaging_unit/index', [
|
||||
'list' => $list, 'startDate' => $startDate, 'endDate' => $endDate, 'pager' => $pager,
|
||||
'list' => $list, 'startDate' => $startDate, 'endDate' => $endDate,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -22,12 +22,11 @@ class SalesAgency extends BaseController
|
||||
return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
$list = $this->model->where('sa_lg_idx', $lgIdx)->orderBy('sa_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->model->pager;
|
||||
$list = $this->model->where('sa_lg_idx', $lgIdx)->orderBy('sa_idx', 'DESC')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '판매 대행소 관리',
|
||||
'content' => view('admin/sales_agency/index', ['list' => $list, 'pager' => $pager]),
|
||||
'content' => view('admin/sales_agency/index', ['list' => $list]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -128,264 +128,6 @@ class SalesReport extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* P5-04: 년 판매 현황 (월별)
|
||||
*/
|
||||
public function yearlySales()
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
|
||||
$year = $this->request->getGet('year') ?? date('Y');
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$result = $db->query("
|
||||
SELECT bs_bag_code, bs_bag_name,
|
||||
SUM(CASE WHEN MONTH(bs_sale_date)=1 THEN ABS(bs_qty) ELSE 0 END) as m01,
|
||||
SUM(CASE WHEN MONTH(bs_sale_date)=2 THEN ABS(bs_qty) ELSE 0 END) as m02,
|
||||
SUM(CASE WHEN MONTH(bs_sale_date)=3 THEN ABS(bs_qty) ELSE 0 END) as m03,
|
||||
SUM(CASE WHEN MONTH(bs_sale_date)=4 THEN ABS(bs_qty) ELSE 0 END) as m04,
|
||||
SUM(CASE WHEN MONTH(bs_sale_date)=5 THEN ABS(bs_qty) ELSE 0 END) as m05,
|
||||
SUM(CASE WHEN MONTH(bs_sale_date)=6 THEN ABS(bs_qty) ELSE 0 END) as m06,
|
||||
SUM(CASE WHEN MONTH(bs_sale_date)=7 THEN ABS(bs_qty) ELSE 0 END) as m07,
|
||||
SUM(CASE WHEN MONTH(bs_sale_date)=8 THEN ABS(bs_qty) ELSE 0 END) as m08,
|
||||
SUM(CASE WHEN MONTH(bs_sale_date)=9 THEN ABS(bs_qty) ELSE 0 END) as m09,
|
||||
SUM(CASE WHEN MONTH(bs_sale_date)=10 THEN ABS(bs_qty) ELSE 0 END) as m10,
|
||||
SUM(CASE WHEN MONTH(bs_sale_date)=11 THEN ABS(bs_qty) ELSE 0 END) as m11,
|
||||
SUM(CASE WHEN MONTH(bs_sale_date)=12 THEN ABS(bs_qty) ELSE 0 END) as m12,
|
||||
SUM(ABS(bs_qty)) as total
|
||||
FROM bag_sale
|
||||
WHERE bs_lg_idx = ? AND YEAR(bs_sale_date) = ? AND bs_type = 'sale'
|
||||
GROUP BY bs_bag_code, bs_bag_name
|
||||
ORDER BY bs_bag_code
|
||||
", [$lgIdx, $year])->getResult();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '년 판매 현황',
|
||||
'content' => view('admin/sales_report/yearly_sales', compact('result', 'year')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* P5-05: 지정판매소별 판매현황
|
||||
*/
|
||||
public function shopSales()
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
|
||||
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
||||
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$result = $db->query("
|
||||
SELECT bs_ds_name,
|
||||
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
||||
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount,
|
||||
SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty,
|
||||
SUM(CASE WHEN bs_type='return' THEN ABS(bs_amount) ELSE 0 END) as return_amount
|
||||
FROM bag_sale
|
||||
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
|
||||
GROUP BY bs_ds_name
|
||||
ORDER BY bs_ds_name
|
||||
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '지정판매소별 판매현황',
|
||||
'content' => view('admin/sales_report/shop_sales', compact('result', 'startDate', 'endDate')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* P5-06: 홈택스 세금계산서 엑셀 내보내기
|
||||
*/
|
||||
public function hometaxExport()
|
||||
{
|
||||
helper(['admin', 'export']);
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
|
||||
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
||||
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$rows = $db->query("
|
||||
SELECT bs.bs_sale_date, ds.ds_biz_no as buyer_biz_no, ds.ds_name as buyer_name,
|
||||
bs.bs_bag_name, ABS(bs.bs_qty) as qty, bs.bs_unit_price, bs.bs_amount
|
||||
FROM bag_sale bs
|
||||
LEFT JOIN designated_shop ds ON bs.bs_ds_idx = ds.ds_idx
|
||||
WHERE bs.bs_lg_idx = ? AND bs.bs_sale_date BETWEEN ? AND ? AND bs.bs_type = 'sale'
|
||||
ORDER BY bs.bs_sale_date, ds.ds_name
|
||||
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||
|
||||
// 지자체 정보 (공급자)
|
||||
$lg = model(\App\Models\LocalGovernmentModel::class)->find($lgIdx);
|
||||
$supplierBizNo = $lg->lg_biz_no ?? '';
|
||||
$supplierName = $lg->lg_name ?? '';
|
||||
|
||||
$csvRows = [];
|
||||
foreach ($rows as $row) {
|
||||
$amount = (int) $row->bs_amount;
|
||||
$tax = (int) round($amount * 0.1);
|
||||
$csvRows[] = [
|
||||
str_replace('-', '', $row->bs_sale_date), // 작성일자 (YYYYMMDD)
|
||||
$supplierBizNo, // 공급자사업자번호
|
||||
$supplierName, // 공급자상호
|
||||
$row->buyer_biz_no ?? '', // 공급받는자사업자번호
|
||||
$row->buyer_name ?? '', // 공급받는자상호
|
||||
$row->bs_bag_name, // 품목
|
||||
(int) $row->qty, // 수량
|
||||
(int) $row->bs_unit_price, // 단가
|
||||
$amount, // 공급가액
|
||||
$tax, // 세액
|
||||
];
|
||||
}
|
||||
|
||||
export_csv(
|
||||
'홈택스_세금계산서_' . date('Ymd') . '.csv',
|
||||
['작성일자', '공급자사업자번호', '공급자상호', '공급받는자사업자번호', '공급받는자상호', '품목', '수량', '단가', '공급가액', '세액'],
|
||||
$csvRows
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* P5-08: 반품/파기 현황
|
||||
*/
|
||||
public function returns()
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
|
||||
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
||||
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$result = $db->query("
|
||||
SELECT bs_sale_date, bs_ds_name, bs_bag_code, bs_bag_name, bs_type,
|
||||
ABS(bs_qty) as qty, ABS(bs_amount) as amount
|
||||
FROM bag_sale
|
||||
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ? AND bs_type IN('return','cancel')
|
||||
ORDER BY bs_sale_date DESC, bs_ds_name
|
||||
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '반품/파기 현황',
|
||||
'content' => view('admin/sales_report/returns', compact('result', 'startDate', 'endDate')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* P5-10: LOT 수불 조회
|
||||
*/
|
||||
public function lotFlow()
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
|
||||
$lotNo = $this->request->getGet('lot_no') ?? '';
|
||||
$order = null;
|
||||
$items = [];
|
||||
$receivings = [];
|
||||
|
||||
if ($lotNo !== '') {
|
||||
$db = \Config\Database::connect();
|
||||
$order = $db->query("SELECT * FROM bag_order WHERE bo_lg_idx = ? AND bo_lot_no = ?", [$lgIdx, $lotNo])->getRow();
|
||||
if ($order) {
|
||||
$items = $db->query("SELECT * FROM bag_order_item WHERE boi_bo_idx = ? ORDER BY boi_bag_code", [(int) $order->bo_idx])->getResult();
|
||||
$receivings = $db->query("SELECT * FROM bag_receiving WHERE br_bo_idx = ? ORDER BY br_receive_date", [(int) $order->bo_idx])->getResult();
|
||||
}
|
||||
}
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => 'LOT 수불 조회',
|
||||
'content' => view('admin/sales_report/lot_flow', compact('lotNo', 'order', 'items', 'receivings')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* P5-11: 기타 입출고 목록
|
||||
*/
|
||||
public function miscFlow()
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
|
||||
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
||||
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
// bag_misc_flow 테이블이 존재하는지 확인
|
||||
$tableExists = $db->query("SHOW TABLES LIKE 'bag_misc_flow'")->getNumRows() > 0;
|
||||
$result = [];
|
||||
if ($tableExists) {
|
||||
$result = $db->query("
|
||||
SELECT * FROM bag_misc_flow
|
||||
WHERE bmf_lg_idx = ? AND bmf_date BETWEEN ? AND ?
|
||||
ORDER BY bmf_date DESC, bmf_idx DESC
|
||||
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||
}
|
||||
|
||||
// 봉투 코드 목록
|
||||
$kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||
$bagCodes = $kindO ? model(\App\Models\CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true) : [];
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '기타 입출고',
|
||||
'content' => view('admin/sales_report/misc_flow', compact('result', 'startDate', 'endDate', 'bagCodes', 'tableExists')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* P5-11: 기타 입출고 등록 처리
|
||||
*/
|
||||
public function miscFlowStore()
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) return redirect()->to(site_url('admin/reports/misc-flow'))->with('error', '지자체를 선택해 주세요.');
|
||||
|
||||
$rules = [
|
||||
'bmf_type' => 'required|in_list[in,out]',
|
||||
'bmf_bag_code' => 'required|max_length[50]',
|
||||
'bmf_qty' => 'required|is_natural_no_zero',
|
||||
'bmf_date' => 'required|valid_date[Y-m-d]',
|
||||
'bmf_reason' => 'required|max_length[200]',
|
||||
];
|
||||
if (! $this->validate($rules)) {
|
||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$bagCode = $this->request->getPost('bmf_bag_code');
|
||||
$qty = (int) $this->request->getPost('bmf_qty');
|
||||
$type = $this->request->getPost('bmf_type');
|
||||
|
||||
// 봉투명 조회
|
||||
$kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||
$detail = $kindO ? model(\App\Models\CodeDetailModel::class)->where('cd_ck_idx', $kindO->ck_idx)->where('cd_code', $bagCode)->first() : null;
|
||||
$bagName = $detail ? $detail->cd_name : '';
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$db->transStart();
|
||||
|
||||
$db->query("
|
||||
INSERT INTO bag_misc_flow (bmf_lg_idx, bmf_type, bmf_bag_code, bmf_bag_name, bmf_qty, bmf_date, bmf_reason, bmf_regdate)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
", [$lgIdx, $type, $bagCode, $bagName, $qty, $this->request->getPost('bmf_date'), $this->request->getPost('bmf_reason'), date('Y-m-d H:i:s')]);
|
||||
|
||||
// 재고 조정
|
||||
$delta = ($type === 'in') ? $qty : -$qty;
|
||||
model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $bagName, $delta);
|
||||
|
||||
$db->transComplete();
|
||||
|
||||
return redirect()->to(site_url('admin/reports/misc-flow'))->with('success', '기타 입출고가 등록되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* P5-07: 봉투 수불 현황
|
||||
*/
|
||||
|
||||
@@ -9,12 +9,12 @@ use Config\Roles;
|
||||
class SelectLocalGovernment extends BaseController
|
||||
{
|
||||
/**
|
||||
* 지자체 선택 화면 (super·본부 관리자)
|
||||
* 지자체 선택 화면 (super admin 전용)
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
if (! Roles::isSuperAdminEquivalent((int) session()->get('mb_level'))) {
|
||||
return redirect()->to(site_url('admin'))->with('error', '지자체 선택은 상위 관리자만 사용할 수 있습니다.');
|
||||
if ((int) session()->get('mb_level') !== Roles::LEVEL_SUPER_ADMIN) {
|
||||
return redirect()->to(site_url('admin'))->with('error', '지자체 선택은 super admin만 사용할 수 있습니다.');
|
||||
}
|
||||
|
||||
$list = model(LocalGovernmentModel::class)
|
||||
@@ -35,8 +35,8 @@ class SelectLocalGovernment extends BaseController
|
||||
*/
|
||||
public function store()
|
||||
{
|
||||
if (! Roles::isSuperAdminEquivalent((int) session()->get('mb_level'))) {
|
||||
return redirect()->to(site_url('admin'))->with('error', '지자체 선택은 상위 관리자만 사용할 수 있습니다.');
|
||||
if ((int) session()->get('mb_level') !== Roles::LEVEL_SUPER_ADMIN) {
|
||||
return redirect()->to(site_url('admin'))->with('error', '지자체 선택은 super admin만 사용할 수 있습니다.');
|
||||
}
|
||||
|
||||
$lgIdx = (int) $this->request->getPost('lg_idx');
|
||||
|
||||
@@ -34,12 +34,11 @@ class ShopOrder extends BaseController
|
||||
if ($startDate) $builder->where('so_delivery_date >=', $startDate);
|
||||
if ($endDate) $builder->where('so_delivery_date <=', $endDate);
|
||||
|
||||
$list = $builder->orderBy('so_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->orderModel->pager;
|
||||
$list = $builder->orderBy('so_idx', 'DESC')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '주문 접수 관리',
|
||||
'content' => view('admin/shop_order/index', compact('list', 'startDate', 'endDate', 'pager')),
|
||||
'content' => view('admin/shop_order/index', compact('list', 'startDate', 'endDate')),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,7 @@ class User extends BaseController
|
||||
*/
|
||||
public function index(): string
|
||||
{
|
||||
$list = $this->memberModel->orderBy('mb_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->memberModel->pager;
|
||||
$list = $this->memberModel->orderBy('mb_idx', 'DESC')->findAll();
|
||||
$approvalMap = [];
|
||||
try {
|
||||
$memberIds = array_map(static fn ($row) => (int) $row->mb_idx, $list);
|
||||
@@ -57,7 +56,6 @@ class User extends BaseController
|
||||
'list' => $list,
|
||||
'roles' => $this->roles,
|
||||
'approvalMap' => $approvalMap,
|
||||
'pager' => $pager,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -179,7 +177,7 @@ class User extends BaseController
|
||||
|
||||
/**
|
||||
* 현재 로그인한 관리자가 부여 가능한 역할 목록.
|
||||
* super/본부만 4·5 부여 가능, 지자체 관리자는 1~3만.
|
||||
* super admin만 super admin(4) 부여 가능, 그 외는 1~3만 허용.
|
||||
*
|
||||
* @return array<int,string>
|
||||
*/
|
||||
@@ -187,11 +185,10 @@ class User extends BaseController
|
||||
{
|
||||
$levelNames = $this->roles->levelNames;
|
||||
$myLevel = (int) session()->get('mb_level');
|
||||
if (Roles::isSuperAdminEquivalent($myLevel)) {
|
||||
if ($myLevel === Roles::LEVEL_SUPER_ADMIN) {
|
||||
return $levelNames;
|
||||
}
|
||||
unset($levelNames[Roles::LEVEL_SUPER_ADMIN], $levelNames[Roles::LEVEL_HEADQUARTERS_ADMIN]);
|
||||
|
||||
unset($levelNames[Roles::LEVEL_SUPER_ADMIN]);
|
||||
return $levelNames;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Libraries\TotpService;
|
||||
use App\Models\LocalGovernmentModel;
|
||||
use App\Models\MemberApprovalRequestModel;
|
||||
use App\Models\MemberLogModel;
|
||||
use App\Models\MemberModel;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
|
||||
class Auth extends BaseController
|
||||
{
|
||||
@@ -125,177 +123,35 @@ class Auth extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->needsTotpStep($member)) {
|
||||
$this->beginPending2faSession((int) $member->mb_idx);
|
||||
$enabled = (int) ($member->mb_totp_enabled ?? 0) === 1;
|
||||
if ($enabled) {
|
||||
return redirect()->to(site_url('login/two-factor'));
|
||||
}
|
||||
session()->set('pending_totp_setup', true);
|
||||
// 로그인 성공
|
||||
$sessionData = [
|
||||
'mb_idx' => $member->mb_idx,
|
||||
'mb_id' => $member->mb_id,
|
||||
'mb_name' => $member->mb_name,
|
||||
'mb_level' => $member->mb_level,
|
||||
'mb_lg_idx' => $member->mb_lg_idx ?? null,
|
||||
'logged_in' => true,
|
||||
];
|
||||
session()->set($sessionData);
|
||||
|
||||
return redirect()->to(site_url('login/totp-setup'));
|
||||
}
|
||||
|
||||
return $this->completeLogin($member, $logData);
|
||||
}
|
||||
|
||||
public function showTwoFactor()
|
||||
{
|
||||
if (session()->get('logged_in')) {
|
||||
return redirect()->to('/');
|
||||
}
|
||||
$member = $this->ensurePending2faContext();
|
||||
if ($member === null) {
|
||||
return redirect()->to(site_url('login'))->with('error', '로그인 세션이 만료되었습니다. 다시 로그인해 주세요.');
|
||||
}
|
||||
if (session()->get('pending_totp_setup')) {
|
||||
return redirect()->to(site_url('login/totp-setup'));
|
||||
}
|
||||
if ((int) ($member->mb_totp_enabled ?? 0) !== 1) {
|
||||
return redirect()->to(site_url('login/totp-setup'));
|
||||
}
|
||||
|
||||
return view('auth/login_two_factor', [
|
||||
'memberId' => $member->mb_id,
|
||||
$memberModel->update($member->mb_idx, [
|
||||
'mb_latestdate' => date('Y-m-d H:i:s'),
|
||||
'mb_login_fail_count' => 0,
|
||||
'mb_locked_until' => null,
|
||||
]);
|
||||
|
||||
$this->insertMemberLog($logData, true, '로그인 성공', $member->mb_idx);
|
||||
|
||||
// 지자체 관리자 → 관리자 대시보드로 이동
|
||||
if ((int) $member->mb_level === \Config\Roles::LEVEL_LOCAL_ADMIN) {
|
||||
return redirect()->to(site_url('admin'))->with('success', '로그인되었습니다.');
|
||||
}
|
||||
// super admin → 지자체 선택 페이지로 이동 (선택 후 관리자 페이지 사용)
|
||||
if ((int) $member->mb_level === \Config\Roles::LEVEL_SUPER_ADMIN) {
|
||||
return redirect()->to(site_url('admin/select-local-government'))->with('success', '로그인되었습니다.');
|
||||
}
|
||||
|
||||
public function verifyTwoFactor()
|
||||
{
|
||||
if (session()->get('logged_in')) {
|
||||
return redirect()->to('/');
|
||||
}
|
||||
$member = $this->ensurePending2faContext();
|
||||
if ($member === null) {
|
||||
return redirect()->to(site_url('login'))->with('error', '로그인 세션이 만료되었습니다. 다시 로그인해 주세요.');
|
||||
}
|
||||
if (session()->get('pending_totp_setup') || (int) ($member->mb_totp_enabled ?? 0) !== 1) {
|
||||
return redirect()->to(site_url('login/totp-setup'));
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'totp_code' => 'required|exact_length[6]|numeric',
|
||||
];
|
||||
$messages = [
|
||||
'totp_code' => [
|
||||
'required' => '인증 코드 6자리를 입력해 주세요.',
|
||||
'exact_length' => '인증 코드는 6자리 숫자입니다.',
|
||||
'numeric' => '인증 코드는 숫자만 입력해 주세요.',
|
||||
],
|
||||
];
|
||||
if (! $this->validate($rules, $messages)) {
|
||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$code = (string) $this->request->getPost('totp_code');
|
||||
helper('pii_encryption');
|
||||
$secret = pii_decrypt((string) ($member->mb_totp_secret ?? ''));
|
||||
if ($secret === '') {
|
||||
$this->clearPending2faSession();
|
||||
|
||||
return redirect()->to(site_url('login'))->with('error', '2차 인증 설정이 올바르지 않습니다. 관리자에게 문의해 주세요.');
|
||||
}
|
||||
|
||||
$totp = new TotpService();
|
||||
if (! $totp->verify($secret, $code)) {
|
||||
return $this->handleTotpFailure($member, $this->buildLogData($member->mb_id, (int) $member->mb_idx));
|
||||
}
|
||||
|
||||
return $this->completeLogin($member, $this->buildLogData($member->mb_id, (int) $member->mb_idx));
|
||||
}
|
||||
|
||||
public function showTotpSetup()
|
||||
{
|
||||
if (session()->get('logged_in')) {
|
||||
return redirect()->to('/');
|
||||
}
|
||||
$member = $this->ensurePending2faContext();
|
||||
if ($member === null) {
|
||||
return redirect()->to(site_url('login'))->with('error', '로그인 세션이 만료되었습니다. 다시 로그인해 주세요.');
|
||||
}
|
||||
if (! session()->get('pending_totp_setup')) {
|
||||
if ((int) ($member->mb_totp_enabled ?? 0) === 1) {
|
||||
return redirect()->to(site_url('login/two-factor'));
|
||||
}
|
||||
|
||||
return redirect()->to(site_url('login'));
|
||||
}
|
||||
|
||||
$totp = new TotpService();
|
||||
$secret = session()->get('pending_totp_secret');
|
||||
if (! is_string($secret) || $secret === '') {
|
||||
$secret = $totp->createSecret();
|
||||
session()->set('pending_totp_secret', $secret);
|
||||
}
|
||||
|
||||
$qrDataUri = null;
|
||||
try {
|
||||
$qrDataUri = $totp->getQrDataUri((string) $member->mb_id, $secret);
|
||||
} catch (\Throwable) {
|
||||
$qrDataUri = null;
|
||||
}
|
||||
|
||||
return view('auth/totp_setup', [
|
||||
'memberId' => $member->mb_id,
|
||||
'qrDataUri' => $qrDataUri,
|
||||
'secret' => $secret,
|
||||
]);
|
||||
}
|
||||
|
||||
public function completeTotpSetup()
|
||||
{
|
||||
if (session()->get('logged_in')) {
|
||||
return redirect()->to('/');
|
||||
}
|
||||
$member = $this->ensurePending2faContext();
|
||||
if ($member === null) {
|
||||
return redirect()->to(site_url('login'))->with('error', '로그인 세션이 만료되었습니다. 다시 로그인해 주세요.');
|
||||
}
|
||||
if (! session()->get('pending_totp_setup')) {
|
||||
return redirect()->to(site_url('login/two-factor'));
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'totp_code' => 'required|exact_length[6]|numeric',
|
||||
];
|
||||
$messages = [
|
||||
'totp_code' => [
|
||||
'required' => '인증 코드 6자리를 입력해 주세요.',
|
||||
'exact_length' => '인증 코드는 6자리 숫자입니다.',
|
||||
'numeric' => '인증 코드는 숫자만 입력해 주세요.',
|
||||
],
|
||||
];
|
||||
if (! $this->validate($rules, $messages)) {
|
||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$secret = session()->get('pending_totp_secret');
|
||||
if (! is_string($secret) || $secret === '') {
|
||||
return redirect()->to(site_url('login/totp-setup'))->with('error', '설정 정보가 없습니다. 페이지를 새로고침해 주세요.');
|
||||
}
|
||||
|
||||
$code = (string) $this->request->getPost('totp_code');
|
||||
$totp = new TotpService();
|
||||
if (! $totp->verify($secret, $code)) {
|
||||
return $this->handleTotpFailure($member, $this->buildLogData($member->mb_id, (int) $member->mb_idx));
|
||||
}
|
||||
|
||||
helper('pii_encryption');
|
||||
model(MemberModel::class)->update((int) $member->mb_idx, [
|
||||
'mb_totp_secret' => pii_encrypt($secret),
|
||||
'mb_totp_enabled' => 1,
|
||||
]);
|
||||
session()->remove('pending_totp_setup');
|
||||
session()->remove('pending_totp_secret');
|
||||
|
||||
$fresh = model(MemberModel::class)->find((int) $member->mb_idx);
|
||||
if ($fresh === null) {
|
||||
$this->clearPending2faSession();
|
||||
|
||||
return redirect()->to(site_url('login'))->with('error', '회원 정보를 다시 확인할 수 없습니다.');
|
||||
}
|
||||
|
||||
return $this->completeLogin($fresh, $this->buildLogData($fresh->mb_id, (int) $fresh->mb_idx));
|
||||
return redirect()->to(site_url('/'))->with('success', '로그인되었습니다.');
|
||||
}
|
||||
|
||||
public function logout()
|
||||
@@ -326,7 +182,6 @@ class Auth extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
$this->clearPending2faSession();
|
||||
session()->destroy();
|
||||
|
||||
return redirect()->to('login')->with('success', '로그아웃되었습니다.');
|
||||
@@ -443,130 +298,6 @@ class Auth extends BaseController
|
||||
return redirect()->to('login')->with('success', '회원가입이 완료되었습니다. 관리자 승인 후 로그인 가능합니다.');
|
||||
}
|
||||
|
||||
private function needsTotpStep(object $member): bool
|
||||
{
|
||||
if (! config('Auth')->requireTotp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \Config\Roles::requiresTotp((int) $member->mb_level);
|
||||
}
|
||||
|
||||
private function beginPending2faSession(int $mbIdx): void
|
||||
{
|
||||
session()->set([
|
||||
'pending_2fa' => true,
|
||||
'pending_mb_idx' => $mbIdx,
|
||||
'pending_2fa_started' => time(),
|
||||
'totp_attempts' => 0,
|
||||
]);
|
||||
session()->remove('pending_totp_setup');
|
||||
session()->remove('pending_totp_secret');
|
||||
}
|
||||
|
||||
private function clearPending2faSession(): void
|
||||
{
|
||||
session()->remove([
|
||||
'pending_2fa',
|
||||
'pending_mb_idx',
|
||||
'pending_2fa_started',
|
||||
'pending_totp_setup',
|
||||
'pending_totp_secret',
|
||||
'totp_attempts',
|
||||
]);
|
||||
}
|
||||
|
||||
private function pending2faExpired(): bool
|
||||
{
|
||||
$started = (int) session()->get('pending_2fa_started');
|
||||
if ($started <= 0) {
|
||||
return true;
|
||||
}
|
||||
$ttl = config('Auth')->pending2faTtlSeconds;
|
||||
|
||||
return (time() - $started) > $ttl;
|
||||
}
|
||||
|
||||
private function ensurePending2faContext(): ?object
|
||||
{
|
||||
if (! session()->get('pending_2fa')) {
|
||||
return null;
|
||||
}
|
||||
if ($this->pending2faExpired()) {
|
||||
$this->clearPending2faSession();
|
||||
|
||||
return null;
|
||||
}
|
||||
$mbIdx = (int) session()->get('pending_mb_idx');
|
||||
if ($mbIdx <= 0) {
|
||||
$this->clearPending2faSession();
|
||||
|
||||
return null;
|
||||
}
|
||||
$member = model(MemberModel::class)->find($mbIdx);
|
||||
if ($member === null) {
|
||||
$this->clearPending2faSession();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $member;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $logData
|
||||
*/
|
||||
private function handleTotpFailure(object $member, array $logData): RedirectResponse
|
||||
{
|
||||
$this->insertMemberLog($logData, false, '2차 인증 실패', (int) $member->mb_idx);
|
||||
$attempts = (int) session()->get('totp_attempts') + 1;
|
||||
session()->set('totp_attempts', $attempts);
|
||||
$max = config('Auth')->totpMaxAttempts;
|
||||
if ($attempts >= $max) {
|
||||
$this->clearPending2faSession();
|
||||
|
||||
return redirect()->to(site_url('login'))->with('error', "인증 코드가 {$max}회 틀려 세션이 종료되었습니다. 처음부터 로그인해 주세요.");
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
->withInput()
|
||||
->with('error', '인증 코드가 올바르지 않습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $logData
|
||||
*/
|
||||
private function completeLogin(object $member, array $logData): RedirectResponse
|
||||
{
|
||||
$this->clearPending2faSession();
|
||||
$sessionData = [
|
||||
'mb_idx' => $member->mb_idx,
|
||||
'mb_id' => $member->mb_id,
|
||||
'mb_name' => $member->mb_name,
|
||||
'mb_level' => $member->mb_level,
|
||||
'mb_lg_idx' => $member->mb_lg_idx ?? null,
|
||||
'logged_in' => true,
|
||||
];
|
||||
session()->set($sessionData);
|
||||
|
||||
model(MemberModel::class)->update($member->mb_idx, [
|
||||
'mb_latestdate' => date('Y-m-d H:i:s'),
|
||||
'mb_login_fail_count' => 0,
|
||||
'mb_locked_until' => null,
|
||||
]);
|
||||
|
||||
$this->insertMemberLog($logData, true, '로그인 성공', (int) $member->mb_idx);
|
||||
|
||||
if ((int) $member->mb_level === \Config\Roles::LEVEL_LOCAL_ADMIN) {
|
||||
return redirect()->to(site_url('admin'))->with('success', '로그인되었습니다.');
|
||||
}
|
||||
if (\Config\Roles::isSuperAdminEquivalent((int) $member->mb_level)) {
|
||||
return redirect()->to(site_url('admin/select-local-government'))->with('success', '로그인되었습니다.');
|
||||
}
|
||||
|
||||
return redirect()->to(site_url('/'))->with('success', '로그인되었습니다.');
|
||||
}
|
||||
|
||||
private function buildLogData(string $mbId, ?int $mbIdx): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -17,7 +17,6 @@ use App\Models\CompanyModel;
|
||||
use App\Models\PackagingUnitModel;
|
||||
use App\Models\SalesAgencyModel;
|
||||
use App\Models\ShopOrderModel;
|
||||
use App\Models\DesignatedShopModel;
|
||||
|
||||
class Bag extends BaseController
|
||||
{
|
||||
@@ -73,8 +72,7 @@ class Bag extends BaseController
|
||||
$orderBuilder = model(BagOrderModel::class)->where('bo_lg_idx', $lgIdx);
|
||||
if ($startDate) $orderBuilder->where('bo_order_date >=', $startDate);
|
||||
if ($endDate) $orderBuilder->where('bo_order_date <=', $endDate);
|
||||
$data['orders'] = $orderBuilder->orderBy('bo_order_date', 'DESC')->paginate(20, 'orders');
|
||||
$data['orderPager'] = model(BagOrderModel::class)->pager;
|
||||
$data['orders'] = $orderBuilder->orderBy('bo_order_date', 'DESC')->findAll();
|
||||
|
||||
// 발주별 품목 합계
|
||||
$itemSummary = [];
|
||||
@@ -94,8 +92,7 @@ class Bag extends BaseController
|
||||
$recvBuilder = model(BagReceivingModel::class)->where('br_lg_idx', $lgIdx);
|
||||
if ($startDate) $recvBuilder->where('br_receive_date >=', $startDate);
|
||||
if ($endDate) $recvBuilder->where('br_receive_date <=', $endDate);
|
||||
$data['receivings'] = $recvBuilder->orderBy('br_receive_date', 'DESC')->paginate(20, 'receivings');
|
||||
$data['recvPager'] = model(BagReceivingModel::class)->pager;
|
||||
$data['receivings'] = $recvBuilder->orderBy('br_receive_date', 'DESC')->findAll();
|
||||
}
|
||||
|
||||
return $this->render('발주 입고 관리', 'bag/purchase_inbound', $data);
|
||||
@@ -118,8 +115,7 @@ class Bag extends BaseController
|
||||
$builder = model(BagIssueModel::class)->where('bi2_lg_idx', $lgIdx);
|
||||
if ($startDate) $builder->where('bi2_issue_date >=', $startDate);
|
||||
if ($endDate) $builder->where('bi2_issue_date <=', $endDate);
|
||||
$data['list'] = $builder->orderBy('bi2_issue_date', 'DESC')->paginate(20);
|
||||
$data['pager'] = model(BagIssueModel::class)->pager;
|
||||
$data['list'] = $builder->orderBy('bi2_issue_date', 'DESC')->findAll();
|
||||
}
|
||||
|
||||
return $this->render('불출 관리', 'bag/issue', $data);
|
||||
@@ -134,9 +130,7 @@ class Bag extends BaseController
|
||||
$data = ['list' => []];
|
||||
|
||||
if ($lgIdx) {
|
||||
$invModel = model(BagInventoryModel::class);
|
||||
$data['list'] = $invModel->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->paginate(20);
|
||||
$data['pager'] = $invModel->pager;
|
||||
$data['list'] = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->findAll();
|
||||
}
|
||||
|
||||
return $this->render('재고 관리', 'bag/inventory', $data);
|
||||
@@ -160,15 +154,13 @@ class Bag extends BaseController
|
||||
$saleBuilder = model(BagSaleModel::class)->where('bs_lg_idx', $lgIdx);
|
||||
if ($startDate) $saleBuilder->where('bs_sale_date >=', $startDate);
|
||||
if ($endDate) $saleBuilder->where('bs_sale_date <=', $endDate);
|
||||
$data['salesList'] = $saleBuilder->orderBy('bs_sale_date', 'DESC')->paginate(20, 'sales');
|
||||
$data['salesPager'] = model(BagSaleModel::class)->pager;
|
||||
$data['salesList'] = $saleBuilder->orderBy('bs_sale_date', 'DESC')->findAll();
|
||||
|
||||
// 주문 접수
|
||||
$orderBuilder = model(ShopOrderModel::class)->where('so_lg_idx', $lgIdx);
|
||||
if ($startDate) $orderBuilder->where('so_delivery_date >=', $startDate);
|
||||
if ($endDate) $orderBuilder->where('so_delivery_date <=', $endDate);
|
||||
$data['orderList'] = $orderBuilder->orderBy('so_idx', 'DESC')->paginate(20, 'shoporders');
|
||||
$data['orderPager'] = model(ShopOrderModel::class)->pager;
|
||||
$data['orderList'] = $orderBuilder->orderBy('so_idx', 'DESC')->findAll();
|
||||
}
|
||||
|
||||
return $this->render('판매 관리', 'bag/sales', $data);
|
||||
@@ -191,8 +183,7 @@ class Bag extends BaseController
|
||||
$builder = model(BagSaleModel::class)->where('bs_lg_idx', $lgIdx)->where('bs_type', 'sale');
|
||||
if ($startDate) $builder->where('bs_sale_date >=', $startDate);
|
||||
if ($endDate) $builder->where('bs_sale_date <=', $endDate);
|
||||
$data['result'] = $builder->orderBy('bs_sale_date', 'DESC')->paginate(20);
|
||||
$data['pager'] = model(BagSaleModel::class)->pager;
|
||||
$data['result'] = $builder->orderBy('bs_sale_date', 'DESC')->findAll();
|
||||
}
|
||||
|
||||
return $this->render('판매 현황', 'bag/sales_stats', $data);
|
||||
@@ -256,173 +247,4 @@ class Bag extends BaseController
|
||||
{
|
||||
return $this->render('도움말', 'bag/help', []);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 재고 조정 (실사)
|
||||
// ──────────────────────────────────────────────
|
||||
public function inventoryAdjust(): string
|
||||
{
|
||||
$lgIdx = $this->lgIdx();
|
||||
$inventory = $lgIdx ? model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code')->findAll() : [];
|
||||
return $this->render('재고 조정', 'bag/inventory_adjust', compact('inventory'));
|
||||
}
|
||||
|
||||
public function inventoryAdjustStore()
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = $this->lgIdx();
|
||||
if (! $lgIdx) {
|
||||
return redirect()->to(site_url('bag/inventory'))->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'bag_code' => 'required|max_length[50]',
|
||||
'adjust_type' => 'required|in_list[set,add,sub]',
|
||||
'qty' => 'required|is_natural',
|
||||
];
|
||||
if (! $this->validate($rules)) {
|
||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$bagCode = $this->request->getPost('bag_code');
|
||||
$type = $this->request->getPost('adjust_type');
|
||||
$qty = (int) $this->request->getPost('qty');
|
||||
|
||||
$invModel = model(BagInventoryModel::class);
|
||||
$existing = $invModel->where('bi_lg_idx', $lgIdx)->where('bi_bag_code', $bagCode)->first();
|
||||
|
||||
if ($type === 'set') {
|
||||
if ($existing) {
|
||||
$invModel->update($existing->bi_idx, ['bi_qty' => $qty, 'bi_updated_at' => date('Y-m-d H:i:s')]);
|
||||
}
|
||||
} elseif ($type === 'add') {
|
||||
$bagName = $existing ? $existing->bi_bag_name : '';
|
||||
$invModel->adjustQty($lgIdx, $bagCode, $bagName, $qty);
|
||||
} elseif ($type === 'sub') {
|
||||
$bagName = $existing ? $existing->bi_bag_name : '';
|
||||
$invModel->adjustQty($lgIdx, $bagCode, $bagName, -$qty);
|
||||
}
|
||||
|
||||
return redirect()->to(site_url('bag/inventory'))->with('success', '재고가 조정되었습니다.');
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// CRUD — 사이트 레이아웃으로 등록/처리 폼 제공
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// --- 불출 등록 ---
|
||||
public function issueCreate(): string
|
||||
{
|
||||
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||
$bagCodes = $kind ? model(CodeDetailModel::class)->where('cd_ck_idx', $kind->ck_idx)->where('cd_state', 1)->orderBy('cd_sort')->findAll() : [];
|
||||
return $this->render('불출 처리', 'bag/create_bag_issue', compact('bagCodes'));
|
||||
}
|
||||
|
||||
public function issueStore()
|
||||
{
|
||||
$admin = new \App\Controllers\Admin\BagIssue();
|
||||
$admin->initController($this->request, $this->response, service('logger'));
|
||||
$result = $admin->store();
|
||||
if ($result instanceof \CodeIgniter\HTTP\RedirectResponse) {
|
||||
$to = (string) $result->getHeaderLine('Location');
|
||||
$to = str_replace('/admin/bag-issues', '/bag/issue', $to);
|
||||
return redirect()->to($to)->with('success', session()->getFlashdata('success'))->with('errors', session()->getFlashdata('errors'));
|
||||
}
|
||||
return redirect()->to(site_url('bag/issue'))->with('success', '불출 처리되었습니다.');
|
||||
}
|
||||
|
||||
public function issueCancel(int $id)
|
||||
{
|
||||
$admin = new \App\Controllers\Admin\BagIssue();
|
||||
$admin->initController($this->request, $this->response, service('logger'));
|
||||
$admin->cancel($id);
|
||||
return redirect()->to(site_url('bag/issue'))->with('success', session()->getFlashdata('success') ?? '취소되었습니다.');
|
||||
}
|
||||
|
||||
// --- 발주 등록 ---
|
||||
public function orderCreate(): string
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = $this->lgIdx();
|
||||
$companies = $lgIdx ? model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', 'manufacturer')->where('cp_state', 1)->findAll() : [];
|
||||
$agencies = $lgIdx ? model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->where('sa_state', 1)->findAll() : [];
|
||||
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||
$bagCodes = $kind ? model(CodeDetailModel::class)->where('cd_ck_idx', $kind->ck_idx)->where('cd_state', 1)->orderBy('cd_sort')->findAll() : [];
|
||||
return $this->render('발주 등록', 'bag/create_bag_order', compact('companies', 'agencies', 'bagCodes'));
|
||||
}
|
||||
|
||||
public function orderStore()
|
||||
{
|
||||
$admin = new \App\Controllers\Admin\BagOrder();
|
||||
$admin->initController($this->request, $this->response, service('logger'));
|
||||
$result = $admin->store();
|
||||
if ($result instanceof \CodeIgniter\HTTP\RedirectResponse) {
|
||||
return redirect()->to(site_url('bag/purchase-inbound'))->with('success', session()->getFlashdata('success'))->with('errors', session()->getFlashdata('errors'));
|
||||
}
|
||||
return redirect()->to(site_url('bag/purchase-inbound'))->with('success', '발주 등록되었습니다.');
|
||||
}
|
||||
|
||||
// --- 입고 처리 ---
|
||||
public function receivingCreate(): string
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = $this->lgIdx();
|
||||
$orders = $lgIdx ? model(BagOrderModel::class)->where('bo_lg_idx', $lgIdx)->where('bo_status', 'normal')->orderBy('bo_order_date', 'DESC')->findAll() : [];
|
||||
return $this->render('입고 처리', 'bag/create_bag_receiving', compact('orders'));
|
||||
}
|
||||
|
||||
public function receivingStore()
|
||||
{
|
||||
$admin = new \App\Controllers\Admin\BagReceiving();
|
||||
$admin->initController($this->request, $this->response, service('logger'));
|
||||
$result = $admin->store();
|
||||
if ($result instanceof \CodeIgniter\HTTP\RedirectResponse) {
|
||||
return redirect()->to(site_url('bag/purchase-inbound'))->with('success', session()->getFlashdata('success'))->with('errors', session()->getFlashdata('errors'));
|
||||
}
|
||||
return redirect()->to(site_url('bag/purchase-inbound'))->with('success', '입고 처리되었습니다.');
|
||||
}
|
||||
|
||||
// --- 판매 등록 ---
|
||||
public function saleCreate(): string
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = $this->lgIdx();
|
||||
$shops = $lgIdx ? model(DesignatedShopModel::class)->where('ds_lg_idx', $lgIdx)->where('ds_state', 1)->findAll() : [];
|
||||
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||
$bagCodes = $kind ? model(CodeDetailModel::class)->where('cd_ck_idx', $kind->ck_idx)->where('cd_state', 1)->orderBy('cd_sort')->findAll() : [];
|
||||
return $this->render('판매 등록', 'bag/create_bag_sale', compact('shops', 'bagCodes'));
|
||||
}
|
||||
|
||||
public function saleStore()
|
||||
{
|
||||
$admin = new \App\Controllers\Admin\BagSale();
|
||||
$admin->initController($this->request, $this->response, service('logger'));
|
||||
$result = $admin->store();
|
||||
if ($result instanceof \CodeIgniter\HTTP\RedirectResponse) {
|
||||
return redirect()->to(site_url('bag/sales'))->with('success', session()->getFlashdata('success'))->with('errors', session()->getFlashdata('errors'));
|
||||
}
|
||||
return redirect()->to(site_url('bag/sales'))->with('success', '판매 등록되었습니다.');
|
||||
}
|
||||
|
||||
// --- 주문 접수 ---
|
||||
public function shopOrderCreate(): string
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = $this->lgIdx();
|
||||
$shops = $lgIdx ? model(DesignatedShopModel::class)->where('ds_lg_idx', $lgIdx)->where('ds_state', 1)->findAll() : [];
|
||||
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||
$bagCodes = $kind ? model(CodeDetailModel::class)->where('cd_ck_idx', $kind->ck_idx)->where('cd_state', 1)->orderBy('cd_sort')->findAll() : [];
|
||||
return $this->render('주문 접수', 'bag/create_shop_order', compact('shops', 'bagCodes'));
|
||||
}
|
||||
|
||||
public function shopOrderStore()
|
||||
{
|
||||
$admin = new \App\Controllers\Admin\ShopOrder();
|
||||
$admin->initController($this->request, $this->response, service('logger'));
|
||||
$result = $admin->store();
|
||||
if ($result instanceof \CodeIgniter\HTTP\RedirectResponse) {
|
||||
return redirect()->to(site_url('bag/sales'))->with('success', session()->getFlashdata('success'))->with('errors', session()->getFlashdata('errors'));
|
||||
}
|
||||
return redirect()->to(site_url('bag/sales'))->with('success', '주문 접수되었습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,16 +61,6 @@ class Home extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* dense(표·KPI) + charts(Chart.js) 혼합. URL: /dashboard/blend
|
||||
*/
|
||||
public function dashboardBlend()
|
||||
{
|
||||
return view('bag/lg_dashboard_blend', [
|
||||
'lgLabel' => $this->resolveLgLabel(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 재고 조회(수불) 화면 (목업)
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,7 @@ use Config\Roles;
|
||||
|
||||
/**
|
||||
* 관리자 전용 접근 필터.
|
||||
* logged_in 이고 mb_level 이 SUPER_ADMIN(4)·HEADQUARTERS_ADMIN(5)·LOCAL_ADMIN(3) 일 때만 통과.
|
||||
* logged_in 이고 mb_level 이 SUPER_ADMIN(4) 또는 LOCAL_ADMIN(3) 일 때만 통과.
|
||||
*/
|
||||
class AdminAuthFilter implements FilterInterface
|
||||
{
|
||||
@@ -22,16 +22,15 @@ class AdminAuthFilter implements FilterInterface
|
||||
}
|
||||
|
||||
$level = (int) session()->get('mb_level');
|
||||
$isAdminLevel = Roles::isSuperAdminEquivalent($level) || $level === Roles::LEVEL_LOCAL_ADMIN;
|
||||
if (! $isAdminLevel) {
|
||||
if ($level !== Roles::LEVEL_SUPER_ADMIN && $level !== Roles::LEVEL_LOCAL_ADMIN) {
|
||||
return redirect()->to(site_url('/'))->with('error', '관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
// Super/본부: 지자체 미선택 시 지자체 선택 페이지로 유도 (지자체 선택·지자체 CRUD는 미선택도 허용)
|
||||
// Super admin: 지자체 미선택 시 지자체 선택 페이지로 유도 (지자체 선택·지자체 CRUD는 미선택도 허용)
|
||||
$uri = $request->getUri();
|
||||
$seg2 = $uri->getSegment(2);
|
||||
$allowedWithoutSelection = ['select-local-government', 'local-governments'];
|
||||
if (Roles::isSuperAdminEquivalent($level) && ! in_array($seg2, $allowedWithoutSelection, true)) {
|
||||
if ($level === Roles::LEVEL_SUPER_ADMIN && ! in_array($seg2, $allowedWithoutSelection, true)) {
|
||||
$selected = session()->get('admin_selected_lg_idx');
|
||||
if ($selected === null || $selected === '') {
|
||||
return redirect()->to(site_url('admin/select-local-government'))->with('error', '작업할 지자체를 먼저 선택해 주세요.');
|
||||
|
||||
@@ -7,12 +7,12 @@ use Config\Roles;
|
||||
if (! function_exists('admin_effective_lg_idx')) {
|
||||
/**
|
||||
* 현재 로그인한 관리자가 작업 대상으로 사용하는 지자체 PK.
|
||||
* Super/본부 관리자 → admin_selected_lg_idx, 지자체 관리자 → mb_lg_idx, 그 외 null.
|
||||
* Super admin → admin_selected_lg_idx, 지자체 관리자 → mb_lg_idx, 그 외 null.
|
||||
*/
|
||||
function admin_effective_lg_idx(): ?int
|
||||
{
|
||||
$level = (int) session()->get('mb_level');
|
||||
if (Roles::isSuperAdminEquivalent($level)) {
|
||||
if ($level === Roles::LEVEL_SUPER_ADMIN) {
|
||||
$idx = session()->get('admin_selected_lg_idx');
|
||||
return $idx !== null && $idx !== '' ? (int) $idx : null;
|
||||
}
|
||||
@@ -27,7 +27,7 @@ if (! function_exists('admin_effective_lg_idx')) {
|
||||
if (! function_exists('get_admin_nav_items')) {
|
||||
/**
|
||||
* 관리자 상단 메뉴 항목 (DB menu 테이블, admin 타입, 현재 지자체·mb_level 기준, 평면 배열).
|
||||
* 지자체 미선택(super/본부)이면 빈 배열. 테이블/조회 실패 시에도 빈 배열.
|
||||
* 지자체 미선택(super admin)이면 빈 배열. 테이블/조회 실패 시에도 빈 배열.
|
||||
*
|
||||
* 하위 메뉴 포함 트리 구조가 필요하면 get_admin_nav_tree() 사용.
|
||||
*/
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (! function_exists('audit_log')) {
|
||||
/**
|
||||
* CRUD 활동 로그 기록
|
||||
*
|
||||
* @param string $action 'create', 'update', 'delete'
|
||||
* @param string $table 대상 테이블명
|
||||
* @param int $recordId 대상 레코드 PK
|
||||
* @param array|null $before 변경 전 데이터 (update/delete 시)
|
||||
* @param array|null $after 변경 후 데이터 (create/update 시)
|
||||
*/
|
||||
function audit_log(string $action, string $table, int $recordId, ?array $before = null, ?array $after = null): void
|
||||
{
|
||||
try {
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
// 테이블 존재 여부 확인 (없으면 skip)
|
||||
if ($db->query("SHOW TABLES LIKE 'activity_log'")->getNumRows() === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mbIdx = session()->get('mb_idx');
|
||||
$ip = service('request')->getIPAddress();
|
||||
|
||||
model(\App\Models\ActivityLogModel::class)->insert([
|
||||
'al_mb_idx' => $mbIdx ? (int) $mbIdx : null,
|
||||
'al_action' => $action,
|
||||
'al_table' => $table,
|
||||
'al_record_id' => $recordId,
|
||||
'al_data_before' => $before !== null ? json_encode($before, JSON_UNESCAPED_UNICODE) : null,
|
||||
'al_data_after' => $after !== null ? json_encode($after, JSON_UNESCAPED_UNICODE) : null,
|
||||
'al_ip' => $ip,
|
||||
'al_regdate' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// 로깅 실패 시 본 로직 방해하지 않음
|
||||
log_message('error', 'audit_log failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CSV 엑셀 내보내기 헬퍼
|
||||
*
|
||||
* UTF-8 BOM 포함으로 한글 엑셀 호환성 보장
|
||||
*/
|
||||
|
||||
if (! function_exists('export_csv')) {
|
||||
/**
|
||||
* CSV 파일을 브라우저로 다운로드 전송
|
||||
*
|
||||
* @param string $filename 파일명 (확장자 포함, 예: 'export.csv')
|
||||
* @param string[] $headers 컬럼 헤더 배열
|
||||
* @param array $rows 데이터 행 배열 (각 행은 배열)
|
||||
*/
|
||||
function export_csv(string $filename, array $headers, array $rows): void
|
||||
{
|
||||
// 파일명에 .csv 확장자 보장
|
||||
if (! str_ends_with($filename, '.csv')) {
|
||||
$filename .= '.csv';
|
||||
}
|
||||
|
||||
$response = service('response');
|
||||
$response->setHeader('Content-Type', 'text/csv; charset=UTF-8');
|
||||
$response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
|
||||
$response->setHeader('Pragma', 'no-cache');
|
||||
$response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||
|
||||
// UTF-8 BOM (한글 엑셀 호환)
|
||||
$output = "\xEF\xBB\xBF";
|
||||
|
||||
// 헤더 행
|
||||
$output .= csv_encode_row($headers);
|
||||
|
||||
// 데이터 행
|
||||
foreach ($rows as $row) {
|
||||
$output .= csv_encode_row(array_values((array) $row));
|
||||
}
|
||||
|
||||
$response->setBody($output);
|
||||
$response->send();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('csv_encode_row')) {
|
||||
/**
|
||||
* 배열 한 행을 CSV 문자열로 변환
|
||||
*
|
||||
* @param array $fields
|
||||
* @return string
|
||||
*/
|
||||
function csv_encode_row(array $fields): string
|
||||
{
|
||||
$escaped = [];
|
||||
foreach ($fields as $field) {
|
||||
$val = (string) ($field ?? '');
|
||||
// 쌍따옴표 이스케이프 및 감싸기
|
||||
if (str_contains($val, '"') || str_contains($val, ',') || str_contains($val, "\n") || str_contains($val, "\r")) {
|
||||
$val = '"' . str_replace('"', '""', $val) . '"';
|
||||
}
|
||||
$escaped[] = $val;
|
||||
}
|
||||
return implode(',', $escaped) . "\r\n";
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use Config\Auth as AuthConfig;
|
||||
use RobThree\Auth\Providers\Qr\QRServerProvider;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
|
||||
/**
|
||||
* TOTP 생성·검증·QR (robthree/twofactorauth)
|
||||
*/
|
||||
class TotpService
|
||||
{
|
||||
private TwoFactorAuth $tfa;
|
||||
|
||||
public function __construct(?AuthConfig $authConfig = null)
|
||||
{
|
||||
$authConfig ??= config('Auth');
|
||||
$this->tfa = new TwoFactorAuth(
|
||||
new QRServerProvider(),
|
||||
$authConfig->totpIssuer,
|
||||
);
|
||||
}
|
||||
|
||||
public function createSecret(): string
|
||||
{
|
||||
return $this->tfa->createSecret();
|
||||
}
|
||||
|
||||
public function verify(string $plainSecret, string $code): bool
|
||||
{
|
||||
$code = preg_replace('/\s+/', '', $code) ?? '';
|
||||
if (strlen($code) !== 6 || ! ctype_digit($code)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->tfa->verifyCode($plainSecret, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* PNG data URI (외부 QR API 호출 — 네트워크 필요)
|
||||
*/
|
||||
public function getQrDataUri(string $accountLabel, string $secret): string
|
||||
{
|
||||
return $this->tfa->getQRCodeImageAsDataUri($accountLabel, $secret);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ActivityLogModel extends Model
|
||||
{
|
||||
protected $table = 'activity_log';
|
||||
protected $primaryKey = 'al_idx';
|
||||
protected $returnType = 'object';
|
||||
protected $useTimestamps = false;
|
||||
protected $allowedFields = [
|
||||
'al_mb_idx',
|
||||
'al_action',
|
||||
'al_table',
|
||||
'al_record_id',
|
||||
'al_data_before',
|
||||
'al_data_after',
|
||||
'al_ip',
|
||||
'al_regdate',
|
||||
];
|
||||
}
|
||||
@@ -13,8 +13,6 @@ class MemberModel extends Model
|
||||
protected $allowedFields = [
|
||||
'mb_id',
|
||||
'mb_passwd',
|
||||
'mb_totp_secret',
|
||||
'mb_totp_enabled',
|
||||
'mb_name',
|
||||
'mb_email',
|
||||
'mb_phone',
|
||||
|
||||
@@ -28,12 +28,12 @@ class MenuModel extends Model
|
||||
|
||||
/**
|
||||
* 특정 mb_level에 노출할 메뉴만 필터링 (mm_is_view=Y, mm_level에 해당 레벨 포함 또는 빈값).
|
||||
* lg_idx 기준 해당 지자체 메뉴만 대상. super/본부(4·5)는 mm_level 무관하게 해당 지자체 메뉴 전체 노출.
|
||||
* lg_idx 기준 해당 지자체 메뉴만 대상. super admin(4)은 mm_level 무관하게 해당 지자체 메뉴 전체 노출.
|
||||
*/
|
||||
public function getVisibleByLevel(int $mtIdx, int $mbLevel, int $lgIdx): array
|
||||
{
|
||||
$all = $this->getAllByType($mtIdx, $lgIdx);
|
||||
if (\Config\Roles::isSuperAdminEquivalent($mbLevel)) {
|
||||
if ($mbLevel === \Config\Roles::LEVEL_SUPER_ADMIN) {
|
||||
return array_values(array_filter($all, static fn ($row) => (string) $row->mm_is_view === 'Y'));
|
||||
}
|
||||
$levelStr = (string) $mbLevel;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<?= view('components/print_header', ['printTitle' => '권한 승인 대기']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">권한 승인 대기</span>
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
</div>
|
||||
</section>
|
||||
<div class="border border-gray-300 p-4 mt-2">
|
||||
<form method="get" action="<?= base_url('admin/access/approvals') ?>" class="mb-4 flex flex-wrap items-center gap-2 text-sm">
|
||||
@@ -68,4 +64,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<?= view('components/print_header', ['printTitle' => '로그인 이력']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<div class="flex items-center gap-4 text-sm">
|
||||
@@ -10,7 +9,6 @@
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">조회</button>
|
||||
</form>
|
||||
</div>
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
</div>
|
||||
</section>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
@@ -37,4 +35,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
<?= view('components/print_header', ['printTitle' => '재고 현황']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">재고 현황</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="<?= base_url('admin/bag-inventory/export') ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
@@ -35,4 +28,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<?= view('components/print_header', ['printTitle' => '무료용 불출 관리']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">무료용 불출 관리</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/bag-issues/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">불출 처리</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/bag-issues') ?>" class="flex flex-wrap items-center gap-2">
|
||||
@@ -62,4 +58,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
<?= view('components/print_header', ['printTitle' => '발주 현황']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">발주 현황</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="<?= base_url('admin/bag-orders/export') . '?' . http_build_query(array_filter(['start_date' => $startDate ?? '', 'end_date' => $endDate ?? '', 'status' => $status ?? ''])) ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/bag-orders/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">발주 등록</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/bag-orders') ?>" class="flex flex-wrap items-center gap-2">
|
||||
@@ -78,4 +73,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<?= view('components/print_header', ['printTitle' => '봉투 단가 관리']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">봉투 단가 관리</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/bag-prices/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">단가 등록</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/bag-prices') ?>" class="flex flex-wrap items-center gap-2">
|
||||
@@ -62,4 +58,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<?= view('components/print_header', ['printTitle' => '입고 현황']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">입고 현황</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/bag-receivings/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">입고 처리</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/bag-receivings') ?>" class="flex flex-wrap items-center gap-2">
|
||||
@@ -51,4 +47,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
<?= view('components/print_header', ['printTitle' => '판매/반품 관리']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">판매/반품 관리</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="<?= base_url('admin/bag-sales/export') . '?' . http_build_query(array_filter(['start_date' => $startDate ?? '', 'end_date' => $endDate ?? '', 'type' => $type ?? ''])) ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/bag-sales/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">판매 등록</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/bag-sales') ?>" class="flex flex-wrap items-center gap-2">
|
||||
@@ -66,4 +61,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<?= view('components/print_header', ['printTitle' => '세부코드 관리 - ' . esc($kind->ck_name)]) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -6,11 +5,8 @@
|
||||
<span class="text-gray-400">|</span>
|
||||
<span class="text-sm font-bold text-gray-700">세부코드 — <?= esc($kind->ck_name) ?> (<?= esc($kind->ck_code) ?>)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/code-details/' . (int) $kind->ck_idx . '/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">세부코드 등록</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
@@ -46,4 +42,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<?= view('components/print_header', ['printTitle' => '기본코드 종류 관리']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">기본코드 종류 관리</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/code-kinds/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">코드 종류 등록</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
@@ -45,4 +41,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<?= view('components/print_header', ['printTitle' => '업체 관리']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">업체 관리</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/companies/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">업체 등록</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
@@ -49,4 +45,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,118 +1,3 @@
|
||||
<?php $s = $stats ?? []; ?>
|
||||
|
||||
<?php if (!($lgIdx ?? null)): ?>
|
||||
<div class="border border-orange-300 bg-orange-50 p-4 text-sm text-orange-700">
|
||||
작업할 지자체가 선택되지 않았습니다. 상단에서 지자체를 선택해 주세요.
|
||||
<div class="border border-gray-300 p-4">
|
||||
<p class="text-sm text-gray-600">관리자 메인 화면입니다. 상단 메뉴에서 기능을 선택하세요.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
<!-- 통계 카드 -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
||||
<div class="border border-gray-300 p-4 bg-white">
|
||||
<div class="text-xs text-gray-500">총 발주 건수</div>
|
||||
<div class="text-2xl font-bold text-blue-700"><?= number_format($s['order_count'] ?? 0) ?></div>
|
||||
<div class="text-xs text-gray-400 mt-1">금액: <?= number_format($s['order_amount'] ?? 0) ?>원</div>
|
||||
</div>
|
||||
<div class="border border-gray-300 p-4 bg-white">
|
||||
<div class="text-xs text-gray-500">총 판매 건수</div>
|
||||
<div class="text-2xl font-bold text-green-700"><?= number_format($s['sale_count'] ?? 0) ?></div>
|
||||
<div class="text-xs text-gray-400 mt-1">금액: <?= number_format($s['sale_amount'] ?? 0) ?>원</div>
|
||||
</div>
|
||||
<div class="border border-gray-300 p-4 bg-white">
|
||||
<div class="text-xs text-gray-500">재고 품목 수</div>
|
||||
<div class="text-2xl font-bold text-purple-700"><?= number_format($s['inventory_count'] ?? 0) ?></div>
|
||||
<div class="text-xs text-gray-400 mt-1">현재 재고가 있는 봉투 품목</div>
|
||||
</div>
|
||||
<div class="border border-gray-300 p-4 bg-white">
|
||||
<div class="text-xs text-gray-500">이번 달 불출</div>
|
||||
<div class="text-2xl font-bold text-orange-700"><?= number_format($s['issue_count_month'] ?? 0) ?></div>
|
||||
<div class="text-xs text-gray-400 mt-1"><?= date('Y년 n월') ?> 무료용 불출</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 최근 내역 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 최근 발주 -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<h3 class="text-sm font-bold text-gray-700">최근 발주 5건</h3>
|
||||
<a href="<?= base_url('admin/bag-orders') ?>" class="text-xs text-blue-600 hover:underline">전체보기</a>
|
||||
</div>
|
||||
<div class="border border-gray-300 overflow-auto">
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>LOT번호</th>
|
||||
<th>발주일</th>
|
||||
<th>상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-center">
|
||||
<?php
|
||||
$statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제'];
|
||||
foreach (($s['recent_orders'] ?? []) as $order):
|
||||
?>
|
||||
<tr>
|
||||
<td class="font-mono text-sm">
|
||||
<a href="<?= base_url('admin/bag-orders/detail/' . (int) $order->bo_idx) ?>" class="text-blue-600 hover:underline"><?= esc($order->bo_lot_no) ?></a>
|
||||
</td>
|
||||
<td><?= esc($order->bo_order_date) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$stClass = match($order->bo_status) {
|
||||
'cancelled' => 'text-red-600',
|
||||
'deleted' => 'text-gray-400',
|
||||
default => 'text-green-600',
|
||||
};
|
||||
?>
|
||||
<span class="<?= $stClass ?>"><?= esc($statusMap[$order->bo_status] ?? $order->bo_status) ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($s['recent_orders'])): ?>
|
||||
<tr><td colspan="3" class="text-gray-400 py-3">발주 내역이 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 최근 판매 -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<h3 class="text-sm font-bold text-gray-700">최근 판매 5건</h3>
|
||||
<a href="<?= base_url('admin/bag-sales') ?>" class="text-xs text-blue-600 hover:underline">전체보기</a>
|
||||
</div>
|
||||
<div class="border border-gray-300 overflow-auto">
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>판매소</th>
|
||||
<th>봉투명</th>
|
||||
<th>수량</th>
|
||||
<th>금액</th>
|
||||
<th>구분</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php
|
||||
$typeMap = ['sale' => '판매', 'return' => '반품', 'cancel' => '취소'];
|
||||
foreach (($s['recent_sales'] ?? []) as $sale):
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-left pl-2"><?= esc($sale->bs_ds_name) ?></td>
|
||||
<td class="text-left pl-2"><?= esc($sale->bs_bag_name) ?></td>
|
||||
<td><?= number_format(abs((int) $sale->bs_qty)) ?></td>
|
||||
<td><?= number_format((int) $sale->bs_amount) ?></td>
|
||||
<td class="text-center"><?= esc($typeMap[$sale->bs_type] ?? $sale->bs_type) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($s['recent_sales'])): ?>
|
||||
<tr><td colspan="5" class="text-center text-gray-400 py-3">판매 내역이 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -1,36 +1,8 @@
|
||||
<?= view('components/print_header', ['printTitle' => '지정판매소 목록']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">지정판매소 목록</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="<?= base_url('admin/designated-shops/export') ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/designated-shops/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">지정판매소 등록</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- P2-15: 다조건 검색 -->
|
||||
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
||||
<form method="GET" action="<?= base_url('admin/designated-shops') ?>" class="flex flex-wrap items-center gap-2">
|
||||
<label class="text-sm text-gray-600">상호명</label>
|
||||
<input type="text" name="ds_name" value="<?= esc($dsName ?? '') ?>" placeholder="상호명 검색" class="border border-gray-300 rounded px-2 py-1 text-sm w-40"/>
|
||||
<label class="text-sm text-gray-600">구군코드</label>
|
||||
<select name="ds_gugun_code" class="border border-gray-300 rounded px-2 py-1 text-sm">
|
||||
<option value="">전체</option>
|
||||
<?php foreach (($gugunCodes ?? []) as $gc): ?>
|
||||
<option value="<?= esc($gc->ds_gugun_code) ?>" <?= ($dsGugunCode ?? '') === $gc->ds_gugun_code ? 'selected' : '' ?>><?= esc($gc->ds_gugun_code) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<label class="text-sm text-gray-600">상태</label>
|
||||
<select name="ds_state" class="border border-gray-300 rounded px-2 py-1 text-sm">
|
||||
<option value="">전체</option>
|
||||
<option value="1" <?= ($dsState ?? '') === '1' ? 'selected' : '' ?>>정상</option>
|
||||
<option value="2" <?= ($dsState ?? '') === '2' ? 'selected' : '' ?>>폐업</option>
|
||||
<option value="3" <?= ($dsState ?? '') === '3' ? 'selected' : '' ?>>직권해지</option>
|
||||
</select>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||
<a href="<?= base_url('admin/designated-shops') ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">초기화</a>
|
||||
</form>
|
||||
</section>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
@@ -72,5 +44,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<?= view('components/print_header', ['printTitle' => '지정판매소 지도']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">지정판매소 지도</span>
|
||||
<a href="<?= base_url('admin/designated-shops') ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">목록으로</a>
|
||||
</div>
|
||||
</section>
|
||||
<div id="kakao-map" class="w-full border border-gray-300 mt-2" style="height:600px;"></div>
|
||||
<div class="mt-2 text-sm text-gray-500">총 <?= count($shops) ?>개 판매소 표시</div>
|
||||
|
||||
<script src="//dapi.kakao.com/v2/maps/sdk.js?appkey=KAKAO_APP_KEY&libraries=services"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var mapContainer = document.getElementById('kakao-map');
|
||||
if (typeof kakao === 'undefined' || typeof kakao.maps === 'undefined') {
|
||||
mapContainer.innerHTML = '<div class="flex items-center justify-center h-full text-gray-400">카카오맵 API 키를 설정해 주세요.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var mapOption = {
|
||||
center: new kakao.maps.LatLng(35.8714, 128.6014), // 대구 기본 좌표
|
||||
level: 8
|
||||
};
|
||||
var map = new kakao.maps.Map(mapContainer, mapOption);
|
||||
|
||||
var geocoder = new kakao.maps.services.Geocoder();
|
||||
var shops = <?= json_encode(array_map(function($s) {
|
||||
return ['name' => $s->ds_name, 'addr' => $s->ds_addr ?? '', 'rep' => $s->ds_rep_name ?? '', 'tel' => $s->ds_tel ?? ''];
|
||||
}, $shops), JSON_UNESCAPED_UNICODE) ?>;
|
||||
|
||||
var bounds = new kakao.maps.LatLngBounds();
|
||||
var markerCount = 0;
|
||||
|
||||
shops.forEach(function(shop) {
|
||||
if (!shop.addr) return;
|
||||
geocoder.addressSearch(shop.addr, function(result, status) {
|
||||
if (status === kakao.maps.services.Status.OK) {
|
||||
var coords = new kakao.maps.LatLng(result[0].y, result[0].x);
|
||||
var marker = new kakao.maps.Marker({ map: map, position: coords });
|
||||
var infoContent = '<div style="padding:5px;font-size:12px;min-width:150px;">' +
|
||||
'<strong>' + shop.name + '</strong><br/>' +
|
||||
(shop.rep ? '대표: ' + shop.rep + '<br/>' : '') +
|
||||
(shop.tel ? 'TEL: ' + shop.tel + '<br/>' : '') +
|
||||
'<span style="color:#888;">' + shop.addr + '</span></div>';
|
||||
var infowindow = new kakao.maps.InfoWindow({ content: infoContent });
|
||||
kakao.maps.event.addListener(marker, 'click', function() {
|
||||
infowindow.open(map, marker);
|
||||
});
|
||||
bounds.extend(coords);
|
||||
markerCount++;
|
||||
if (markerCount > 0) map.setBounds(bounds);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -1,80 +0,0 @@
|
||||
<?= view('components/print_header', ['printTitle' => '지정판매소 현황']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">지정판매소 현황 (신규/취소)</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/designated-shops') ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">목록으로</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 전체 현황 요약 -->
|
||||
<div class="flex gap-4 mt-2 mb-2">
|
||||
<div class="border border-gray-300 p-3 flex-1 text-center">
|
||||
<div class="text-sm text-gray-500">활성 판매소</div>
|
||||
<div class="text-2xl font-bold text-green-600"><?= number_format($totalActive) ?></div>
|
||||
</div>
|
||||
<div class="border border-gray-300 p-3 flex-1 text-center">
|
||||
<div class="text-sm text-gray-500">비활성/취소 판매소</div>
|
||||
<div class="text-2xl font-bold text-red-600"><?= number_format($totalInactive) ?></div>
|
||||
</div>
|
||||
<div class="border border-gray-300 p-3 flex-1 text-center">
|
||||
<div class="text-sm text-gray-500">전체</div>
|
||||
<div class="text-2xl font-bold text-gray-700"><?= number_format($totalActive + $totalInactive) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
|
||||
<!-- 연도별 신규등록 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-gray-700 mb-1">연도별 신규등록 건수</h3>
|
||||
<div class="border border-gray-300 overflow-auto">
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>연도</th>
|
||||
<th>신규등록 건수</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php foreach ($newByYear as $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= esc($row->yr) ?>년</td>
|
||||
<td><?= number_format((int) $row->cnt) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($newByYear)): ?>
|
||||
<tr><td colspan="2" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 연도별 취소/비활성 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-gray-700 mb-1">연도별 취소/비활성 건수</h3>
|
||||
<div class="border border-gray-300 overflow-auto">
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>연도</th>
|
||||
<th>취소/비활성 건수</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php foreach ($cancelByYear as $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= esc($row->yr) ?>년</td>
|
||||
<td><?= number_format((int) $row->cnt) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($cancelByYear)): ?>
|
||||
<tr><td colspan="2" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,12 +1,8 @@
|
||||
<?= view('components/print_header', ['printTitle' => '무료용 대상자 관리']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">무료용 대상자 관리</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/free-recipients/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">대상자 등록</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
@@ -51,4 +47,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -5,7 +5,7 @@ $n = $uriObj->getTotalSegments();
|
||||
$uri = $n >= 2 ? $uriObj->getSegment(2) : '';
|
||||
$seg3 = $n >= 3 ? $uriObj->getSegment(3) : '';
|
||||
$mbLevel = (int) session()->get('mb_level');
|
||||
$isSuperAdmin = \Config\Roles::isSuperAdminEquivalent($mbLevel);
|
||||
$isSuperAdmin = ($mbLevel === \Config\Roles::LEVEL_SUPER_ADMIN);
|
||||
$effectiveLgIdx = admin_effective_lg_idx();
|
||||
$effectiveLgName = null;
|
||||
if ($effectiveLgIdx) {
|
||||
@@ -70,14 +70,6 @@ tailwind.config = {
|
||||
.data-table tbody tr:hover td { background-color: #e6f7ff !important; }
|
||||
.main-content-area { height: calc(100vh - 170px); overflow: auto; }
|
||||
body { overflow: hidden; }
|
||||
@media print {
|
||||
header, footer, .no-print, nav { display: none !important; }
|
||||
.main-content-area { height: auto !important; overflow: visible !important; }
|
||||
body { overflow: visible !important; }
|
||||
.bg-title-bar { display: none !important; }
|
||||
.bg-control-panel { break-inside: avoid; }
|
||||
.print-header { display: block !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased select-none">
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<?= view('components/print_header', ['printTitle' => '지자체 목록']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">지자체 목록</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/local-governments/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">지자체 등록</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
@@ -44,5 +40,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<?= view('components/print_header', ['printTitle' => '담당자 관리']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">담당자 관리</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/managers/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">담당자 등록</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
@@ -49,4 +45,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -4,6 +4,7 @@ $list = $list ?? [];
|
||||
$mtIdx = (int) ($mtIdx ?? 0);
|
||||
$mtCode = (string) ($mtCode ?? '');
|
||||
$levelNames = $levelNames ?? [];
|
||||
$superAdminLevel = \Config\Roles::LEVEL_SUPER_ADMIN;
|
||||
?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
@@ -75,7 +76,7 @@ $levelNames = $levelNames ?? [];
|
||||
if ((string) $row->mm_level === '') {
|
||||
echo '전체';
|
||||
} else {
|
||||
$levels = array_filter(explode(',', $row->mm_level), fn ($lv) => ! \Config\Roles::isSuperAdminEquivalent((int) trim($lv)));
|
||||
$levels = array_filter(explode(',', $row->mm_level), fn ($lv) => (int) trim($lv) !== $superAdminLevel);
|
||||
$labels = array_map(fn ($lv) => $levelNames[trim($lv)] ?? trim($lv), $levels);
|
||||
echo esc(implode(', ', $labels) ?: '전체');
|
||||
}
|
||||
@@ -145,7 +146,7 @@ $levelNames = $levelNames ?? [];
|
||||
<span class="text-sm">전체</span>
|
||||
</label>
|
||||
<?php foreach ($levelNames as $lv => $name): ?>
|
||||
<?php if (\Config\Roles::isSuperAdminEquivalent((int) $lv)) { continue; } ?>
|
||||
<?php if ((int) $lv === $superAdminLevel) { continue; } ?>
|
||||
<label class="inline-flex items-center gap-1 mm-level-label">
|
||||
<input type="checkbox" name="mm_level[]" value="<?= (int) $lv ?>" class="mm-level-cb"/>
|
||||
<span class="text-sm"><?= esc($name) ?></span>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<?= view('components/print_header', ['printTitle' => '포장 단위 관리']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">포장 단위 관리</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/packaging-units/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">포장 단위 등록</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/packaging-units') ?>" class="flex flex-wrap items-center gap-2">
|
||||
@@ -62,4 +58,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<?= view('components/print_header', ['printTitle' => '판매 대행소 관리']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">판매 대행소 관리</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/sales-agencies/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">대행소 등록</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
@@ -47,4 +43,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<?= view('components/print_header', ['printTitle' => '일계표']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">일계표</span>
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/reports/daily-summary') ?>" class="flex flex-wrap items-center gap-2">
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
<?= view('components/print_header', ['printTitle' => 'LOT 수불 조회']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">LOT 수불 조회</span>
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/reports/lot-flow') ?>" class="flex flex-wrap items-center gap-2">
|
||||
<label class="text-sm text-gray-600">LOT 번호</label>
|
||||
<input type="text" name="lot_no" value="<?= esc($lotNo ?? '') ?>" placeholder="LOT-YYYYMMDD-XXXXXX" class="border border-gray-300 rounded px-2 py-1 text-sm w-64"/>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<?php if ($lotNo !== '' && $order): ?>
|
||||
<!-- 발주 정보 -->
|
||||
<div class="border border-gray-300 p-3 mt-2 bg-gray-50">
|
||||
<h3 class="text-sm font-bold text-gray-700 mb-2">발주 정보</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm">
|
||||
<div><span class="text-gray-500">LOT번호:</span> <span class="font-mono"><?= esc($order->bo_lot_no) ?></span></div>
|
||||
<div><span class="text-gray-500">발주일:</span> <?= esc($order->bo_order_date) ?></div>
|
||||
<div><span class="text-gray-500">상태:</span>
|
||||
<?php $statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제']; ?>
|
||||
<?= esc($statusMap[$order->bo_status] ?? $order->bo_status) ?>
|
||||
</div>
|
||||
<div><span class="text-gray-500">등록일:</span> <?= esc($order->bo_regdate) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 발주 품목 -->
|
||||
<h3 class="text-sm font-bold text-gray-700 mt-3 mb-1">발주 품목</h3>
|
||||
<div class="border border-gray-300 overflow-auto">
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>봉투코드</th>
|
||||
<th>봉투명</th>
|
||||
<th>발주수량(박스)</th>
|
||||
<th>발주수량(매)</th>
|
||||
<th>단가</th>
|
||||
<th>금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<tr>
|
||||
<td class="text-center font-mono"><?= esc($item->boi_bag_code) ?></td>
|
||||
<td class="text-left pl-2"><?= esc($item->boi_bag_name) ?></td>
|
||||
<td><?= number_format((int) $item->boi_qty_box) ?></td>
|
||||
<td><?= number_format((int) $item->boi_qty_sheet) ?></td>
|
||||
<td><?= number_format((int) $item->boi_unit_price) ?></td>
|
||||
<td><?= number_format((int) $item->boi_amount) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($items)): ?>
|
||||
<tr><td colspan="6" class="text-center text-gray-400 py-4">품목이 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 입고 내역 -->
|
||||
<h3 class="text-sm font-bold text-gray-700 mt-3 mb-1">입고 내역</h3>
|
||||
<div class="border border-gray-300 overflow-auto">
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>입고일</th>
|
||||
<th>봉투코드</th>
|
||||
<th>봉투명</th>
|
||||
<th>입고수량(박스)</th>
|
||||
<th>입고수량(매)</th>
|
||||
<th>납품자</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php foreach ($receivings as $recv): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= esc($recv->br_receive_date) ?></td>
|
||||
<td class="text-center font-mono"><?= esc($recv->br_bag_code) ?></td>
|
||||
<td class="text-left pl-2"><?= esc($recv->br_bag_name) ?></td>
|
||||
<td><?= number_format((int) $recv->br_qty_box) ?></td>
|
||||
<td><?= number_format((int) $recv->br_qty_sheet) ?></td>
|
||||
<td class="text-left pl-2"><?= esc($recv->br_sender_name ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($receivings)): ?>
|
||||
<tr><td colspan="6" class="text-center text-gray-400 py-4">입고 내역이 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php elseif ($lotNo !== '' && !$order): ?>
|
||||
<div class="border border-gray-300 p-4 mt-2 text-center text-gray-400">해당 LOT 번호의 발주를 찾을 수 없습니다.</div>
|
||||
<?php else: ?>
|
||||
<div class="border border-gray-300 p-4 mt-2 text-center text-gray-400">LOT 번호를 입력하고 조회해 주세요.</div>
|
||||
<?php endif; ?>
|
||||
@@ -1,84 +0,0 @@
|
||||
<?= view('components/print_header', ['printTitle' => '기타 입출고']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">기타 입출고</span>
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if (!($tableExists ?? false)): ?>
|
||||
<div class="border border-orange-300 bg-orange-50 p-3 mt-2 text-sm text-orange-700">
|
||||
bag_misc_flow 테이블이 생성되지 않았습니다. <code>writable/database/bag_misc_flow_tables.sql</code>을 실행해 주세요.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 등록 폼 -->
|
||||
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
||||
<form method="POST" action="<?= base_url('admin/reports/misc-flow') ?>" class="flex flex-wrap items-center gap-2">
|
||||
<?= csrf_field() ?>
|
||||
<label class="text-sm text-gray-600">구분</label>
|
||||
<select name="bmf_type" class="border border-gray-300 rounded px-2 py-1 text-sm" required>
|
||||
<option value="in">입고</option>
|
||||
<option value="out">출고</option>
|
||||
</select>
|
||||
<label class="text-sm text-gray-600">봉투</label>
|
||||
<select name="bmf_bag_code" class="border border-gray-300 rounded px-2 py-1 text-sm" required>
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($bagCodes as $bc): ?>
|
||||
<option value="<?= esc($bc->cd_code) ?>"><?= esc($bc->cd_code . ' - ' . $bc->cd_name) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<label class="text-sm text-gray-600">수량</label>
|
||||
<input type="number" name="bmf_qty" min="1" class="border border-gray-300 rounded px-2 py-1 text-sm w-24" required/>
|
||||
<label class="text-sm text-gray-600">일자</label>
|
||||
<input type="date" name="bmf_date" value="<?= date('Y-m-d') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm" required/>
|
||||
<label class="text-sm text-gray-600">사유</label>
|
||||
<input type="text" name="bmf_reason" placeholder="입출고 사유" class="border border-gray-300 rounded px-2 py-1 text-sm w-48" required/>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">등록</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- 조회 필터 -->
|
||||
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
||||
<form method="GET" action="<?= base_url('admin/reports/misc-flow') ?>" class="flex flex-wrap items-center gap-2">
|
||||
<label class="text-sm text-gray-600">시작일</label>
|
||||
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||
<label class="text-sm text-gray-600">~</label>
|
||||
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>번호</th>
|
||||
<th>구분</th>
|
||||
<th>일자</th>
|
||||
<th>봉투코드</th>
|
||||
<th>봉투명</th>
|
||||
<th>수량</th>
|
||||
<th>사유</th>
|
||||
<th>등록일</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php foreach ($result as $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= (int) $row->bmf_idx ?></td>
|
||||
<td class="text-center"><?= $row->bmf_type === 'in' ? '<span class="text-blue-600">입고</span>' : '<span class="text-red-600">출고</span>' ?></td>
|
||||
<td class="text-center"><?= esc($row->bmf_date) ?></td>
|
||||
<td class="text-center font-mono"><?= esc($row->bmf_bag_code) ?></td>
|
||||
<td class="text-left pl-2"><?= esc($row->bmf_bag_name) ?></td>
|
||||
<td><?= number_format((int) $row->bmf_qty) ?></td>
|
||||
<td class="text-left pl-2"><?= esc($row->bmf_reason) ?></td>
|
||||
<td class="text-center"><?= esc($row->bmf_regdate) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($result)): ?>
|
||||
<tr><td colspan="8" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,9 +1,5 @@
|
||||
<?= view('components/print_header', ['printTitle' => '기간별 판매현황']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">기간별 판매현황</span>
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/reports/period-sales') ?>" class="flex flex-wrap items-center gap-2">
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
<?= view('components/print_header', ['printTitle' => '반품/파기 현황']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">반품/파기 현황</span>
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/reports/returns') ?>" class="flex flex-wrap items-center gap-2">
|
||||
<label class="text-sm text-gray-600">시작일</label>
|
||||
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||
<label class="text-sm text-gray-600">~</label>
|
||||
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||
</form>
|
||||
</section>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>일자</th>
|
||||
<th>판매소</th>
|
||||
<th>봉투코드</th>
|
||||
<th>봉투명</th>
|
||||
<th>구분</th>
|
||||
<th>수량</th>
|
||||
<th>금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php
|
||||
$totalQty = 0; $totalAmt = 0;
|
||||
$typeMap = ['return' => '반품', 'cancel' => '취소/파기'];
|
||||
foreach ($result as $row):
|
||||
$totalQty += (int) $row->qty;
|
||||
$totalAmt += (int) $row->amount;
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-center"><?= esc($row->bs_sale_date) ?></td>
|
||||
<td class="text-left pl-2"><?= esc($row->bs_ds_name) ?></td>
|
||||
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
|
||||
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
|
||||
<td class="text-center"><?= esc($typeMap[$row->bs_type] ?? $row->bs_type) ?></td>
|
||||
<td><?= number_format((int) $row->qty) ?></td>
|
||||
<td><?= number_format((int) $row->amount) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($result)): ?>
|
||||
<tr><td colspan="7" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
|
||||
<?php else: ?>
|
||||
<tr class="font-bold bg-gray-100">
|
||||
<td colspan="5" class="text-center">합계</td>
|
||||
<td><?= number_format($totalQty) ?></td>
|
||||
<td><?= number_format($totalAmt) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,9 +1,5 @@
|
||||
<?= view('components/print_header', ['printTitle' => '판매 대장']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">판매 대장</span>
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/reports/sales-ledger') ?>" class="flex flex-wrap items-center gap-2">
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
<?= view('components/print_header', ['printTitle' => '지정판매소별 판매현황']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">지정판매소별 판매현황</span>
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/reports/shop-sales') ?>" class="flex flex-wrap items-center gap-2">
|
||||
<label class="text-sm text-gray-600">시작일</label>
|
||||
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||
<label class="text-sm text-gray-600">~</label>
|
||||
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||
</form>
|
||||
</section>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>판매소명</th>
|
||||
<th>판매수량</th>
|
||||
<th>판매금액</th>
|
||||
<th>반품수량</th>
|
||||
<th>반품금액</th>
|
||||
<th>순판매수량</th>
|
||||
<th>순판매금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php
|
||||
$totSaleQty = 0; $totSaleAmt = 0; $totRetQty = 0; $totRetAmt = 0;
|
||||
foreach ($result as $row):
|
||||
$totSaleQty += (int) $row->sale_qty;
|
||||
$totSaleAmt += (int) $row->sale_amount;
|
||||
$totRetQty += (int) $row->return_qty;
|
||||
$totRetAmt += (int) $row->return_amount;
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-left pl-2"><?= esc($row->bs_ds_name) ?></td>
|
||||
<td><?= number_format((int) $row->sale_qty) ?></td>
|
||||
<td><?= number_format((int) $row->sale_amount) ?></td>
|
||||
<td><?= number_format((int) $row->return_qty) ?></td>
|
||||
<td><?= number_format((int) $row->return_amount) ?></td>
|
||||
<td class="font-bold"><?= number_format((int) $row->sale_qty - (int) $row->return_qty) ?></td>
|
||||
<td class="font-bold"><?= number_format((int) $row->sale_amount - (int) $row->return_amount) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($result)): ?>
|
||||
<tr><td colspan="7" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
|
||||
<?php else: ?>
|
||||
<tr class="font-bold bg-gray-100">
|
||||
<td class="text-center">합계</td>
|
||||
<td><?= number_format($totSaleQty) ?></td>
|
||||
<td><?= number_format($totSaleAmt) ?></td>
|
||||
<td><?= number_format($totRetQty) ?></td>
|
||||
<td><?= number_format($totRetAmt) ?></td>
|
||||
<td><?= number_format($totSaleQty - $totRetQty) ?></td>
|
||||
<td><?= number_format($totSaleAmt - $totRetAmt) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,9 +1,5 @@
|
||||
<?= view('components/print_header', ['printTitle' => '봉투 수불 현황']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">봉투 수불 현황</span>
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/reports/supply-demand') ?>" class="flex flex-wrap items-center gap-2">
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<?= view('components/print_header', ['printTitle' => '년 판매 현황']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">년 판매 현황 (월별)</span>
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/reports/yearly-sales') ?>" class="flex flex-wrap items-center gap-2">
|
||||
<label class="text-sm text-gray-600">연도</label>
|
||||
<select name="year" class="border border-gray-300 rounded px-2 py-1 text-sm">
|
||||
<?php for ($y = (int) date('Y'); $y >= 2020; $y--): ?>
|
||||
<option value="<?= $y ?>" <?= (int)($year ?? date('Y')) === $y ? 'selected' : '' ?>><?= $y ?>년</option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||
</form>
|
||||
</section>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>봉투코드</th>
|
||||
<th>봉투명</th>
|
||||
<th>1월</th><th>2월</th><th>3월</th><th>4월</th><th>5월</th><th>6월</th>
|
||||
<th>7월</th><th>8월</th><th>9월</th><th>10월</th><th>11월</th><th>12월</th>
|
||||
<th class="bg-gray-100">합계</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php
|
||||
$grandTotal = array_fill(1, 13, 0); // 1~12 + 13=total
|
||||
foreach ($result as $row):
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
|
||||
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
|
||||
<?php for ($m = 1; $m <= 12; $m++):
|
||||
$key = 'm' . sprintf('%02d', $m);
|
||||
$val = (int) $row->$key;
|
||||
$grandTotal[$m] += $val;
|
||||
?>
|
||||
<td><?= $val > 0 ? number_format($val) : '-' ?></td>
|
||||
<?php endfor; ?>
|
||||
<?php $grandTotal[13] += (int) $row->total; ?>
|
||||
<td class="font-bold bg-gray-50"><?= number_format((int) $row->total) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($result)): ?>
|
||||
<tr><td colspan="15" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
|
||||
<?php else: ?>
|
||||
<tr class="font-bold bg-gray-100">
|
||||
<td colspan="2" class="text-center">합계</td>
|
||||
<?php for ($m = 1; $m <= 12; $m++): ?>
|
||||
<td><?= $grandTotal[$m] > 0 ? number_format($grandTotal[$m]) : '-' ?></td>
|
||||
<?php endfor; ?>
|
||||
<td class="bg-gray-200"><?= number_format($grandTotal[13]) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,12 +1,8 @@
|
||||
<?= view('components/print_header', ['printTitle' => '주문 접수 관리']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">주문 접수 관리</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/shop-orders/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">주문 접수</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/shop-orders') ?>" class="flex flex-wrap items-center gap-2">
|
||||
@@ -77,4 +73,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<?= view('components/print_header', ['printTitle' => '회원 목록']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">회원 목록</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<a href="<?= base_url('admin/users/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">회원 등록</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
@@ -57,4 +53,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>2차 인증 - 쓰레기봉투 물류시스템</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet"/>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: { sans: ['"Malgun Gothic"', '"Noto Sans KR"', 'sans-serif'] },
|
||||
colors: {
|
||||
'system-header': '#ffffff',
|
||||
'title-bar': '#2c3e50',
|
||||
'control-panel': '#f8f9fa',
|
||||
'btn-search': '#1c4e80',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 text-gray-800 flex flex-col min-h-screen font-sans antialiased">
|
||||
<header class="bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="<?= base_url() ?>" class="text-base font-semibold text-gray-800 tracking-tight hover:text-blue-600">쓰레기봉투 물류시스템</a>
|
||||
</div>
|
||||
</header>
|
||||
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
|
||||
2차 인증 (TOTP)
|
||||
</div>
|
||||
<?php if (session()->getFlashdata('error')): ?>
|
||||
<div class="mx-4 mt-2 p-3 rounded-lg bg-red-50 text-red-700 text-sm" role="alert"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (session()->getFlashdata('errors')): ?>
|
||||
<div class="mx-4 mt-2 p-3 rounded-lg bg-red-50 text-red-700 text-sm space-y-1" role="alert">
|
||||
<?php foreach (session()->getFlashdata('errors') as $error): ?><p><?= esc($error) ?></p><?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<main class="flex-grow bg-control-panel border-b border-gray-300 p-6 flex items-center justify-center">
|
||||
<section class="w-full max-w-md bg-white border border-gray-300 rounded shadow-sm p-6 space-y-4">
|
||||
<p class="text-sm text-gray-600">계정 <strong class="text-gray-800"><?= esc($memberId) ?></strong> 에 대해 인증 앱의 6자리 코드를 입력해 주세요.</p>
|
||||
<form action="<?= base_url('login/two-factor') ?>" method="POST" class="space-y-4">
|
||||
<?= csrf_field() ?>
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-1" for="totp_code">인증 코드</label>
|
||||
<input class="block w-full border border-gray-300 rounded px-3 py-2 text-sm tracking-widest focus:ring-blue-500 focus:border-blue-500" id="totp_code" name="totp_code" type="text" inputmode="numeric" pattern="[0-9]*" maxlength="6" autocomplete="one-time-code" autofocus placeholder="000000" value="<?= esc(old('totp_code')) ?>"/>
|
||||
</div>
|
||||
<div class="flex gap-2 pt-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-2 rounded-sm text-sm font-medium shadow hover:opacity-90 transition border border-transparent">확인</button>
|
||||
<a href="<?= base_url('login') ?>" class="bg-white text-gray-700 border border-gray-300 px-4 py-2 rounded-sm text-sm shadow hover:bg-gray-50 transition">처음으로</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 shrink-0">쓰레기봉투 물류시스템</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -89,7 +89,7 @@ tailwind.config = {
|
||||
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_level">사용자 역할 <span class="text-red-500">*</span></label>
|
||||
<select class="block w-full border border-gray-300 rounded px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500" id="mb_level" name="mb_level">
|
||||
<?php foreach (config('Roles')->levelNames as $level => $name): ?>
|
||||
<?php if (\Config\Roles::isSuperAdminEquivalent((int) $level)) continue; ?>
|
||||
<?php if ((int) $level === \Config\Roles::LEVEL_SUPER_ADMIN) continue; ?>
|
||||
<option value="<?= $level ?>" <?= old('mb_level', config('Roles')->defaultLevelForSelfRegister) == $level ? 'selected' : '' ?>><?= esc($name) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>2차 인증 등록 - 쓰레기봉투 물류시스템</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet"/>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: { sans: ['"Malgun Gothic"', '"Noto Sans KR"', 'sans-serif'] },
|
||||
colors: {
|
||||
'title-bar': '#2c3e50',
|
||||
'control-panel': '#f8f9fa',
|
||||
'btn-search': '#1c4e80',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 text-gray-800 flex flex-col min-h-screen font-sans antialiased">
|
||||
<header class="bg-white border-b border-gray-300 h-12 flex items-center px-4 shrink-0">
|
||||
<a href="<?= base_url() ?>" class="text-base font-semibold text-gray-800 tracking-tight hover:text-blue-600">쓰레기봉투 물류시스템</a>
|
||||
</header>
|
||||
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
|
||||
2차 인증 앱 등록
|
||||
</div>
|
||||
<?php if (session()->getFlashdata('error')): ?>
|
||||
<div class="mx-4 mt-2 p-3 rounded-lg bg-red-50 text-red-700 text-sm" role="alert"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (session()->getFlashdata('errors')): ?>
|
||||
<div class="mx-4 mt-2 p-3 rounded-lg bg-red-50 text-red-700 text-sm space-y-1" role="alert">
|
||||
<?php foreach (session()->getFlashdata('errors') as $error): ?><p><?= esc($error) ?></p><?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<main class="flex-grow bg-control-panel border-b border-gray-300 p-6 flex items-center justify-center">
|
||||
<section class="w-full max-w-lg bg-white border border-gray-300 rounded shadow-sm p-6 space-y-4">
|
||||
<p class="text-sm text-gray-600">관리자 계정 <strong class="text-gray-800"><?= esc($memberId) ?></strong> 에 Google Authenticator, Microsoft Authenticator 등으로 아래 시크릿 또는 QR을 등록한 뒤, 표시되는 6자리 코드를 입력해 주세요.</p>
|
||||
<?php if (! empty($qrDataUri)): ?>
|
||||
<div class="flex justify-center">
|
||||
<img src="<?= esc($qrDataUri, 'attr') ?>" alt="TOTP QR 코드" class="border border-gray-200 rounded max-w-[200px] h-auto"/>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p class="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded p-2">QR 이미지를 불러올 수 없습니다. 아래 시크릿을 앱에 직접 입력해 주세요.</p>
|
||||
<?php endif; ?>
|
||||
<div>
|
||||
<span class="block text-xs font-semibold text-gray-500 mb-1">수동 입력용 시크릿</span>
|
||||
<code class="block text-sm bg-gray-100 border border-gray-200 rounded px-3 py-2 break-all select-all"><?= esc($secret) ?></code>
|
||||
</div>
|
||||
<form action="<?= base_url('login/totp-setup') ?>" method="POST" class="space-y-4 pt-2 border-t border-gray-200">
|
||||
<?= csrf_field() ?>
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-1" for="totp_code">확인용 인증 코드</label>
|
||||
<input class="block w-full border border-gray-300 rounded px-3 py-2 text-sm tracking-widest focus:ring-blue-500 focus:border-blue-500" id="totp_code" name="totp_code" type="text" inputmode="numeric" pattern="[0-9]*" maxlength="6" autocomplete="one-time-code" autofocus placeholder="000000" value="<?= esc(old('totp_code')) ?>"/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-2 rounded-sm text-sm font-medium shadow hover:opacity-90 transition border border-transparent">등록 완료</button>
|
||||
<a href="<?= base_url('login') ?>" class="bg-white text-gray-700 border border-gray-300 px-4 py-2 rounded-sm text-sm shadow hover:bg-gray-50 transition">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 shrink-0">쓰레기봉투 물류시스템</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,70 +0,0 @@
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<span class="text-sm font-bold text-gray-700">무료용 불출 처리</span>
|
||||
</section>
|
||||
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
||||
<form action="<?= base_url('bag/issue/store') ?>" method="POST" class="space-y-4">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">연도 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 text-right" name="bi2_year" type="number" min="2000" max="2099" value="<?= esc(old('bi2_year', date('Y'))) ?>" required/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">분기 <span class="text-red-500">*</span></label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32" name="bi2_quarter" required>
|
||||
<option value="">선택</option>
|
||||
<option value="1" <?= old('bi2_quarter') === '1' ? 'selected' : '' ?>>1</option>
|
||||
<option value="2" <?= old('bi2_quarter') === '2' ? 'selected' : '' ?>>2</option>
|
||||
<option value="3" <?= old('bi2_quarter') === '3' ? 'selected' : '' ?>>3</option>
|
||||
<option value="4" <?= old('bi2_quarter') === '4' ? 'selected' : '' ?>>4</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">구분 <span class="text-red-500">*</span></label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bi2_issue_type" required>
|
||||
<option value="">선택</option>
|
||||
<option value="무료용" <?= old('bi2_issue_type') === '무료용' ? 'selected' : '' ?>>무료용</option>
|
||||
<option value="공공용" <?= old('bi2_issue_type') === '공공용' ? 'selected' : '' ?>>공공용</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">불출일 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bi2_issue_date" type="date" value="<?= esc(old('bi2_issue_date', date('Y-m-d'))) ?>" required/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">불출처 유형</label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bi2_dest_type" type="text" placeholder="동사무소" value="<?= esc(old('bi2_dest_type')) ?>"/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">불출처명 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bi2_dest_name" type="text" value="<?= esc(old('bi2_dest_name')) ?>" required/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">봉투코드 <span class="text-red-500">*</span></label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bi2_bag_code" required>
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($bagCodes as $cd): ?>
|
||||
<option value="<?= esc($cd->cd_code) ?>" <?= old('bi2_bag_code') === $cd->cd_code ? 'selected' : '' ?>>
|
||||
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_name) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">수량 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 text-right" name="bi2_qty" type="number" min="0" value="<?= esc(old('bi2_qty', '0')) ?>" required/>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
|
||||
<a href="<?= base_url('bag/issue') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,83 +0,0 @@
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<span class="text-sm font-bold text-gray-700">발주 등록</span>
|
||||
</section>
|
||||
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-4xl">
|
||||
<form action="<?= base_url('bag/order/store') ?>" method="POST" class="space-y-4">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">발주일 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bo_order_date" type="date" value="<?= esc(old('bo_order_date', date('Y-m-d'))) ?>" required/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">수수료율</label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 text-right" name="bo_fee_rate" type="number" step="0.01" value="<?= esc(old('bo_fee_rate', '0')) ?>"/>
|
||||
<span class="text-sm text-gray-500">%</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">제작업체</label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bo_company_idx">
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($companies as $cp): ?>
|
||||
<option value="<?= esc($cp->cp_idx) ?>" <?= (int) old('bo_company_idx') === (int) $cp->cp_idx ? 'selected' : '' ?>>
|
||||
<?= esc($cp->cp_name) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">입고처</label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bo_agency_idx">
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($agencies as $ag): ?>
|
||||
<option value="<?= esc($ag->sa_idx) ?>" <?= (int) old('bo_agency_idx') === (int) $ag->sa_idx ? 'selected' : '' ?>>
|
||||
<?= esc($ag->sa_name) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">발주 품목</label>
|
||||
<div class="border border-gray-300 overflow-auto">
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-16">순번</th>
|
||||
<th>봉투</th>
|
||||
<th class="w-32">박스수</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php for ($i = 0; $i < 3; $i++): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $i + 1 ?></td>
|
||||
<td>
|
||||
<select class="border border-gray-300 rounded px-2 py-1 text-sm w-full" name="item_bag_code[]">
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($bagCodes as $cd): ?>
|
||||
<option value="<?= esc($cd->cd_code) ?>">
|
||||
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_name) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right" name="item_qty_box[]" type="number" min="0" value="0"/>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endfor; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
|
||||
<a href="<?= base_url('bag/purchase-inbound') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,53 +0,0 @@
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<span class="text-sm font-bold text-gray-700">입고 처리</span>
|
||||
</section>
|
||||
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
||||
<form action="<?= base_url('bag/receiving/store') ?>" method="POST" class="space-y-4">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">발주건</label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-72" name="br_bo_idx">
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($orders as $od): ?>
|
||||
<option value="<?= esc($od->bo_idx) ?>" <?= (int) old('br_bo_idx') === (int) $od->bo_idx ? 'selected' : '' ?>>
|
||||
<?= esc($od->bo_lot_no) ?> (<?= esc($od->bo_order_date) ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">봉투코드</label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="br_bag_code" type="text" value="<?= esc(old('br_bag_code')) ?>"/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">박스수 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 text-right" name="br_qty_box" type="number" min="0" value="<?= esc(old('br_qty_box', '0')) ?>" required/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">입고일 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="br_receive_date" type="date" value="<?= esc(old('br_receive_date', date('Y-m-d'))) ?>" required/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">보내는분</label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="br_sender_name" type="text" value="<?= esc(old('br_sender_name')) ?>"/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">구분</label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="br_type">
|
||||
<option value="batch" <?= old('br_type') === 'batch' ? 'selected' : '' ?>>batch</option>
|
||||
<option value="scanner" <?= old('br_type') === 'scanner' ? 'selected' : '' ?>>scanner</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
|
||||
<a href="<?= base_url('bag/purchase-inbound') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,56 +0,0 @@
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<span class="text-sm font-bold text-gray-700">판매 등록</span>
|
||||
</section>
|
||||
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
||||
<form action="<?= base_url('bag/sale/store') ?>" method="POST" class="space-y-4">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">판매소 <span class="text-red-500">*</span></label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bs_ds_idx" required>
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($shops as $shop): ?>
|
||||
<option value="<?= esc($shop->ds_idx) ?>" <?= (int) old('bs_ds_idx') === (int) $shop->ds_idx ? 'selected' : '' ?>>
|
||||
<?= esc($shop->ds_name) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">봉투코드 <span class="text-red-500">*</span></label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bs_bag_code" required>
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($bagCodes as $cd): ?>
|
||||
<option value="<?= esc($cd->cd_code) ?>" <?= old('bs_bag_code') === $cd->cd_code ? 'selected' : '' ?>>
|
||||
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_name) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">수량 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 text-right" name="bs_qty" type="number" min="0" value="<?= esc(old('bs_qty', '0')) ?>" required/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">판매일 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bs_sale_date" type="date" value="<?= esc(old('bs_sale_date', date('Y-m-d'))) ?>" required/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">구분 <span class="text-red-500">*</span></label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bs_type" required>
|
||||
<option value="">선택</option>
|
||||
<option value="sale" <?= old('bs_type') === 'sale' ? 'selected' : '' ?>>판매</option>
|
||||
<option value="return" <?= old('bs_type') === 'return' ? 'selected' : '' ?>>반품</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
|
||||
<a href="<?= base_url('bag/sales') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,74 +0,0 @@
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<span class="text-sm font-bold text-gray-700">주문 접수</span>
|
||||
</section>
|
||||
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-4xl">
|
||||
<form action="<?= base_url('bag/shop-order/store') ?>" method="POST" class="space-y-4">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">판매소 <span class="text-red-500">*</span></label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="so_ds_idx" required>
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($shops as $shop): ?>
|
||||
<option value="<?= esc($shop->ds_idx) ?>" <?= (int) old('so_ds_idx') === (int) $shop->ds_idx ? 'selected' : '' ?>>
|
||||
<?= esc($shop->ds_name) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">배달일 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="so_delivery_date" type="date" value="<?= esc(old('so_delivery_date', date('Y-m-d', strtotime('+1 day')))) ?>" required/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">결제방법 <span class="text-red-500">*</span></label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="so_payment_type" required>
|
||||
<option value="">선택</option>
|
||||
<option value="이체" <?= old('so_payment_type') === '이체' ? 'selected' : '' ?>>이체</option>
|
||||
<option value="가상계좌" <?= old('so_payment_type') === '가상계좌' ? 'selected' : '' ?>>가상계좌</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">주문 품목</label>
|
||||
<div class="border border-gray-300 overflow-auto">
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-16">순번</th>
|
||||
<th>봉투</th>
|
||||
<th class="w-32">수량</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php for ($i = 0; $i < 3; $i++): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $i + 1 ?></td>
|
||||
<td>
|
||||
<select class="border border-gray-300 rounded px-2 py-1 text-sm w-full" name="item_bag_code[]">
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($bagCodes as $cd): ?>
|
||||
<option value="<?= esc($cd->cd_code) ?>">
|
||||
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_name) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right" name="item_qty[]" type="number" min="0" value="0"/>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endfor; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
|
||||
<a href="<?= base_url('bag/sales') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -147,7 +147,7 @@ $siteNavTree = get_site_nav_tree();
|
||||
</nav>
|
||||
<?php
|
||||
$mbLevel = (int) session()->get('mb_level');
|
||||
$isAdmin = (\Config\Roles::isSuperAdminEquivalent($mbLevel) || $mbLevel === \Config\Roles::LEVEL_LOCAL_ADMIN);
|
||||
$isAdmin = ($mbLevel === \Config\Roles::LEVEL_SUPER_ADMIN || $mbLevel === \Config\Roles::LEVEL_LOCAL_ADMIN);
|
||||
?>
|
||||
<!-- 관리자 이동 버튼(관리자만) · 종료 -->
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
@@ -7,11 +7,6 @@
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm">조회</button>
|
||||
<a href="<?= base_url('bag/flow') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
|
||||
</form>
|
||||
<div class="flex gap-2 mb-2">
|
||||
<a href="<?= base_url('bag/receiving/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">입고 처리</a>
|
||||
<a href="<?= base_url('bag/sale/create') ?>" class="bg-white text-blue-600 border border-blue-300 px-3 py-1.5 rounded-sm text-sm">판매 등록</a>
|
||||
<a href="<?= base_url('bag/issue/create') ?>" class="bg-white text-blue-600 border border-blue-300 px-3 py-1.5 rounded-sm text-sm">불출 처리</a>
|
||||
</div>
|
||||
|
||||
<!-- 수불 요약 -->
|
||||
<table class="data-table">
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span></span>
|
||||
<a href="<?= base_url('bag/inventory/adjust') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">재고 조정</a>
|
||||
</div>
|
||||
|
||||
<table class="data-table">
|
||||
<thead><tr>
|
||||
<th class="w-16">번호</th><th>봉투코드</th><th>봉투명</th><th>현재재고(낱장)</th><th>최종갱신</th>
|
||||
@@ -15,8 +9,8 @@
|
||||
<td class="text-center"><?= $i + 1 ?></td>
|
||||
<td class="text-center"><?= esc($row->bi_bag_code ?? '') ?></td>
|
||||
<td><?= esc($row->bi_bag_name ?? '') ?></td>
|
||||
<td class="text-right"><?= number_format((int)($row->bi_qty ?? 0)) ?></td>
|
||||
<td class="text-center"><?= esc($row->bi_updated_at ?? '') ?></td>
|
||||
<td class="text-right"><?= number_format((int)($row->bi_qty_sheet ?? 0)) ?></td>
|
||||
<td class="text-center"><?= esc($row->bi_updated_at ?? $row->updated_at ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
@@ -24,4 +18,3 @@
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<div class="max-w-lg">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-base font-bold text-gray-700">재고 수량 조정 (실사)</h3>
|
||||
</div>
|
||||
|
||||
<form action="<?= base_url('bag/inventory/adjust') ?>" method="POST" class="space-y-4">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-1">봉투코드 <span class="text-red-500">*</span></label>
|
||||
<select name="bag_code" required class="w-full border border-gray-300 rounded px-3 py-2 text-sm">
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($inventory as $item): ?>
|
||||
<option value="<?= esc($item->bi_bag_code) ?>"><?= esc($item->bi_bag_code) ?> — <?= esc($item->bi_bag_name) ?> (현재: <?= number_format((int)$item->bi_qty) ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-1">조정 유형 <span class="text-red-500">*</span></label>
|
||||
<select name="adjust_type" required class="w-full border border-gray-300 rounded px-3 py-2 text-sm">
|
||||
<option value="set">실사 수량으로 설정</option>
|
||||
<option value="add">증가 (+)</option>
|
||||
<option value="sub">감소 (-)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-1">수량 <span class="text-red-500">*</span></label>
|
||||
<input type="number" name="qty" required min="0" value="0" class="w-full border border-gray-300 rounded px-3 py-2 text-sm"/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-1">사유</label>
|
||||
<input type="text" name="reason" placeholder="실사 조정, 오류 수정 등" class="w-full border border-gray-300 rounded px-3 py-2 text-sm"/>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-6 py-2 rounded-sm text-sm">조정</button>
|
||||
<a href="<?= base_url('bag/inventory') ?>" class="bg-gray-200 text-gray-700 px-6 py-2 rounded-sm text-sm">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -8,7 +8,7 @@
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm">조회</button>
|
||||
<a href="<?= base_url('bag/issue') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
|
||||
</form>
|
||||
<a href="<?= base_url('bag/issue/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">불출 처리</a>
|
||||
<a href="<?= base_url('admin/bag-issues/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">불출 처리</a>
|
||||
</div>
|
||||
|
||||
<table class="data-table">
|
||||
@@ -36,7 +36,7 @@
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<?php if (($row->bi2_status ?? '') === 'normal'): ?>
|
||||
<form method="post" action="<?= base_url('bag/issue/cancel/' . $row->bi2_idx) ?>" class="inline" onsubmit="return confirm('취소하시겠습니까?')">
|
||||
<form method="post" action="<?= base_url('admin/bag-issues/cancel/' . $row->bi2_idx) ?>" class="inline" onsubmit="return confirm('취소하시겠습니까?')">
|
||||
<?= csrf_field() ?>
|
||||
<button class="text-orange-600 hover:underline text-xs">취소</button>
|
||||
</form>
|
||||
|
||||
@@ -52,14 +52,6 @@ tailwind.config = {
|
||||
.data-table tbody tr:hover td { background-color: #e6f7ff !important; }
|
||||
.main-content-area { height: calc(100vh - 130px); overflow: auto; }
|
||||
body { overflow: hidden; }
|
||||
@media print {
|
||||
header, footer, .no-print, nav { display: none !important; }
|
||||
.main-content-area { height: auto !important; overflow: visible !important; }
|
||||
body { overflow: visible !important; }
|
||||
.bg-title-bar { display: none !important; }
|
||||
.bg-control-panel { break-inside: avoid; }
|
||||
.print-header { display: block !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased select-none">
|
||||
|
||||
@@ -70,8 +70,6 @@ $mbName = session()->get('mb_name') ?? '담당자';
|
||||
<span class="text-gray-300 hidden sm:inline">|</span>
|
||||
<a href="<?= base_url('dashboard/dense') ?>" class="text-xs text-[#2b4c8c] hover:underline whitespace-nowrap hidden sm:inline" title="정보 집약 종합">종합</a>
|
||||
<span class="text-gray-300 hidden sm:inline">|</span>
|
||||
<a href="<?= base_url('dashboard/blend') ?>" class="text-xs text-[#2b4c8c] hover:underline whitespace-nowrap hidden sm:inline" title="표+차트 혼합">혼합</a>
|
||||
<span class="text-gray-300 hidden sm:inline">|</span>
|
||||
<a href="<?= base_url('dashboard/charts') ?>" class="text-xs text-[#2b4c8c] hover:underline whitespace-nowrap hidden sm:inline" title="그래프 대시보드">차트</a>
|
||||
<a href="<?= base_url('logout') ?>" class="text-gray-500 hover:text-gray-800 p-1" title="로그아웃">
|
||||
<i class="fa-solid fa-arrow-right-from-bracket text-lg"></i>
|
||||
@@ -261,6 +259,11 @@ $mbName = session()->get('mb_name') ?? '담당자';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-[11px] text-gray-400 border-t border-gray-200 pt-3">
|
||||
차장님 요청 반영: <strong>봉투별 재고</strong>·<strong>구매신청 리스트</strong>·그래프 /
|
||||
추가 시안: <strong>발주·입고</strong>, <strong>승인 대기</strong>, <strong>수불 추이</strong>.
|
||||
레이아웃은 <strong>수불 엔터프라이즈 화면</strong>과 동일한 상단 메뉴·제목바 스타일입니다.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -1,700 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* dense(정보 집약 표·KPI) + charts(Chart.js) 혼합 대시보드
|
||||
*
|
||||
* @var string $lgLabel
|
||||
*/
|
||||
$lgLabel = $lgLabel ?? '북구';
|
||||
$mbName = session()->get('mb_name') ?? '담당자';
|
||||
$dashClassic = base_url('dashboard/classic-mock');
|
||||
$dashModern = base_url('dashboard/modern');
|
||||
$dashDense = base_url('dashboard/dense');
|
||||
$dashCharts = base_url('dashboard/charts');
|
||||
$dashBlend = base_url('dashboard/blend');
|
||||
|
||||
$kpiTop = [
|
||||
['icon' => 'fa-triangle-exclamation', 'c' => 'text-amber-700', 'bg' => 'bg-amber-50', 'v' => '3', 'l' => '재고부족', 'sub' => '품목'],
|
||||
['icon' => 'fa-cart-shopping', 'c' => 'text-sky-700', 'bg' => 'bg-sky-50', 'v' => '12', 'l' => '구매신청', 'sub' => '미처리'],
|
||||
['icon' => 'fa-truck', 'c' => 'text-emerald-700', 'bg' => 'bg-emerald-50', 'v' => '8', 'l' => '발주·입고', 'sub' => '금주'],
|
||||
['icon' => 'fa-user-clock', 'c' => 'text-violet-700', 'bg' => 'bg-violet-50', 'v' => '4', 'l' => '회원승인', 'sub' => '대기'],
|
||||
['icon' => 'fa-store', 'c' => 'text-rose-700', 'bg' => 'bg-rose-50', 'v' => '127', 'l' => '지정판매소', 'sub' => '등록'],
|
||||
['icon' => 'fa-boxes-stacked', 'c' => 'text-slate-700', 'bg' => 'bg-slate-100', 'v' => '48.2k', 'l' => '봉투재고', 'sub' => '장 합계'],
|
||||
['icon' => 'fa-file-invoice', 'c' => 'text-orange-700', 'bg' => 'bg-orange-50', 'v' => '6', 'l' => '세금계산서', 'sub' => '발행대기'],
|
||||
['icon' => 'fa-headset', 'c' => 'text-cyan-700', 'bg' => 'bg-cyan-50', 'v' => '2', 'l' => '민원·문의', 'sub' => '오늘'],
|
||||
];
|
||||
|
||||
$stockRows = [
|
||||
['일반 5L', '12,400', '안전', '3.2주'],
|
||||
['일반 10L', '8,200', '주의', '1.8주'],
|
||||
['일반 20L', '2,100', '부족', '0.6주'],
|
||||
['음식물 스티커', '15,000', '안전', '5.1주'],
|
||||
['재사용봉투', '4,300', '안전', '2.4주'],
|
||||
['특수규격 A', '890', '부족', '0.3주'],
|
||||
];
|
||||
|
||||
$orderRows = [
|
||||
['PO-2025-0218', '○○상사', '일반 5L×2박스', '발주확인', '02-26 10:20'],
|
||||
['PO-2025-0217', '△△유통', '스티커 500매', '납품중', '02-26 09:05'],
|
||||
['PO-2025-0216', '□□종량제', '20L 혼합', '입고완료', '02-25 16:40'],
|
||||
['REQ-8841', '행복마트 북구점', '5L 2,000장', '접수', '02-26 09:12'],
|
||||
['REQ-8839', '○○슈퍼', '스티커 500', '처리중', '02-26 08:45'],
|
||||
];
|
||||
|
||||
$logRows = [
|
||||
['10:42', 'system', '일일 재고 스냅샷 생성 완료'],
|
||||
['10:18', 'user', esc($mbName) . ' 로그인 (IP 마스킹)'],
|
||||
['09:55', 'batch', '구매신청 자동 분배 3건'],
|
||||
['09:30', 'admin', '판매소 코드 2건 갱신'],
|
||||
['08:12', 'api', '세금계산서 연동 응답 정상'],
|
||||
];
|
||||
|
||||
$storeSummary = [
|
||||
['행복마트 북구점', '42', '정상', '02-26'],
|
||||
['○○슈퍼', '38', '정상', '02-25'],
|
||||
['△△상회', '15', '연체1건', '02-20'],
|
||||
['□□마트', '29', '정상', '02-26'],
|
||||
['◇◇할인점', '51', '정상', '02-26'],
|
||||
];
|
||||
|
||||
$approvals = [
|
||||
['김○○', '판매소', '02-26', '서류검토'],
|
||||
['이○○', '일반', '02-25', '본인확인'],
|
||||
['박○○', '판매소', '02-25', '주소불일치'],
|
||||
];
|
||||
|
||||
$notices = [
|
||||
'2월 말 정기 재고 실사 안내 — 2/28 17:00 마감',
|
||||
'봉투 단가 조정 예고 — 3/1 적용 예정 (안내문 배포 완료)',
|
||||
];
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>종량제 시스템 — 종합·그래프 혼합</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
}
|
||||
.nav-top a.nav-active {
|
||||
color: #2b4c8c;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #2b4c8c;
|
||||
padding-bottom: 2px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
.dense-table th, .dense-table td { padding: 0.25rem 0.4rem; line-height: 1.25; }
|
||||
.dense-table thead th { font-size: 11px; font-weight: 600; color: #555; background: #f3f4f6; border-bottom: 1px solid #d1d5db; }
|
||||
.dense-table tbody td { border-bottom: 1px solid #eee; font-size: 11px; }
|
||||
.spark { display: flex; align-items: flex-end; gap: 2px; height: 36px; }
|
||||
.spark span { flex: 1; background: linear-gradient(180deg, #3b82f6, #93c5fd); border-radius: 1px; min-width: 4px; }
|
||||
.chart-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.04);
|
||||
}
|
||||
.chart-card h2 {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
padding: 0.4rem 0.5rem;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
background: #fafafa;
|
||||
}
|
||||
.chart-wrap { position: relative; height: 180px; padding: 0.4rem 0.5rem 0.5rem; }
|
||||
.chart-wrap.tall { height: 260px; }
|
||||
.chart-wrap.wide { height: 220px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-[#f0f2f5] flex flex-col min-h-screen">
|
||||
<header class="border-b border-gray-300 bg-white shadow-sm shrink-0" data-purpose="top-navigation">
|
||||
<div class="flex items-center justify-between px-3 py-1.5 gap-3 flex-wrap">
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<div class="flex items-center gap-2 text-green-700 font-bold text-base">
|
||||
<i class="fa-solid fa-recycle text-lg"></i>
|
||||
<span>종량제 시스템</span>
|
||||
</div>
|
||||
<span class="hidden sm:inline text-[11px] text-gray-500 border-l border-gray-300 pl-2">
|
||||
<?= esc($lgLabel) ?> · <strong class="text-gray-700"><?= esc($mbName) ?></strong>님
|
||||
</span>
|
||||
</div>
|
||||
<nav class="nav-top hidden lg:flex flex-wrap items-center gap-3 xl:gap-4 text-[13px] font-medium text-gray-700">
|
||||
<a class="nav-active flex items-center gap-1 whitespace-nowrap" href="<?= esc($dashBlend) ?>">
|
||||
<i class="fa-solid fa-gauge-high"></i> 업무 현황
|
||||
</a>
|
||||
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="#"><i class="fa-regular fa-file-lines"></i> 문서 관리</a>
|
||||
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="#"><i class="fa-solid fa-box-open"></i> 규격</a>
|
||||
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="#"><i class="fa-solid fa-bag-shopping"></i> 봉투 양식</a>
|
||||
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="#"><i class="fa-solid fa-table"></i> 데이터 양식</a>
|
||||
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="#"><i class="fa-solid fa-clock-rotate-left"></i> 사용 내역</a>
|
||||
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="<?= base_url('bag/inventory-inquiry') ?>"><i class="fa-solid fa-boxes-stacked"></i> 재고 현황</a>
|
||||
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="<?= base_url('bag/waste-suibal-enterprise') ?>"><i class="fa-solid fa-table-list"></i> 수불 현황</a>
|
||||
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="#"><i class="fa-solid fa-chart-line"></i> 통계 분석</a>
|
||||
<a class="flex items-center gap-1 hover:text-blue-600 whitespace-nowrap" href="#"><i class="fa-solid fa-gear"></i> 설정</a>
|
||||
</nav>
|
||||
<div class="flex items-center gap-1.5 shrink-0 text-[11px]">
|
||||
<a href="<?= esc($dashClassic) ?>" class="text-[#2b4c8c] hover:underline whitespace-nowrap hidden md:inline">클래식</a>
|
||||
<span class="text-gray-300 hidden md:inline">|</span>
|
||||
<a href="<?= esc($dashModern) ?>" class="text-[#2b4c8c] hover:underline whitespace-nowrap hidden md:inline">모던</a>
|
||||
<span class="text-gray-300 hidden md:inline">|</span>
|
||||
<a href="<?= esc($dashDense) ?>" class="text-[#2b4c8c] hover:underline whitespace-nowrap hidden md:inline" title="정보 집약">종합</a>
|
||||
<span class="text-gray-300 hidden md:inline">|</span>
|
||||
<a href="<?= esc($dashCharts) ?>" class="text-[#2b4c8c] hover:underline whitespace-nowrap hidden md:inline" title="그래프만">차트</a>
|
||||
<a href="<?= base_url('logout') ?>" class="text-gray-500 hover:text-gray-800 p-1 ml-1" title="로그아웃">
|
||||
<i class="fa-solid fa-arrow-right-from-bracket"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="bg-gradient-to-r from-[#eff5fb] to-[#e8eef8] border-b border-gray-300 px-3 py-1 flex flex-wrap items-center justify-between gap-2 text-[11px] shrink-0">
|
||||
<span class="font-semibold text-gray-800">
|
||||
<i class="fa-solid fa-layer-group text-[#2b4c8c] mr-1"></i>종합·그래프 혼합 현황
|
||||
<span class="font-normal text-gray-500 ml-1">· dense 표/KPI + Chart.js</span>
|
||||
</span>
|
||||
<div class="flex flex-wrap items-center gap-2 text-gray-600">
|
||||
<span><i class="fa-regular fa-calendar mr-0.5"></i><?= date('Y-m-d (D) H:i') ?></span>
|
||||
<span class="text-gray-300">|</span>
|
||||
<span>기준지자체 <strong class="text-gray-800"><?= esc($lgLabel) ?></strong></span>
|
||||
<button type="button" class="bg-[#2b4c8c] text-white px-2 py-0.5 rounded text-[11px]"><i class="fa-solid fa-rotate mr-0.5"></i>새로고침</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="flex-1 overflow-y-auto p-2 sm:p-3">
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="mb-2 p-2 rounded border border-green-200 bg-green-50 text-green-800 text-[11px]"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mb-2 flex flex-wrap gap-2">
|
||||
<?php foreach ($notices as $n): ?>
|
||||
<div class="flex-1 min-w-[200px] flex items-center gap-2 bg-amber-50 border border-amber-200 text-amber-900 px-2 py-1 rounded text-[11px]">
|
||||
<i class="fa-solid fa-bullhorn shrink-0"></i>
|
||||
<span class="truncate" title="<?= esc($n) ?>"><?= esc($n) ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-1.5 mb-2">
|
||||
<?php foreach ($kpiTop as $k): ?>
|
||||
<div class="bg-white border border-gray-200 rounded px-2 py-1.5 flex items-center gap-2 shadow-sm">
|
||||
<div class="w-8 h-8 rounded <?= $k['bg'] ?> <?= $k['c'] ?> flex items-center justify-center shrink-0 text-sm">
|
||||
<i class="fa-solid <?= esc($k['icon'], 'attr') ?>"></i>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-base font-bold text-gray-900 leading-tight"><?= esc($k['v']) ?></div>
|
||||
<div class="text-[10px] text-gray-500 leading-tight"><?= esc($k['l']) ?></div>
|
||||
<div class="text-[9px] text-gray-400"><?= esc($k['sub']) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- dense: 재고 / 발주·신청 / 로그+스파크 -->
|
||||
<div class="grid grid-cols-1 xl:grid-cols-12 gap-2 mb-2">
|
||||
<section class="xl:col-span-4 bg-white border border-gray-200 rounded shadow-sm overflow-hidden">
|
||||
<div class="px-2 py-1 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
|
||||
<h2 class="text-[11px] font-bold text-gray-800"><i class="fa-solid fa-warehouse text-[#2b4c8c] mr-1"></i>품목별 재고·소진예상</h2>
|
||||
<a href="<?= base_url('bag/inventory-inquiry') ?>" class="text-[10px] text-blue-600 hover:underline">상세</a>
|
||||
</div>
|
||||
<div class="overflow-x-auto max-h-[200px] overflow-y-auto">
|
||||
<table class="w-full dense-table text-left">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>품목</th>
|
||||
<th class="text-right">재고(장)</th>
|
||||
<th>상태</th>
|
||||
<th class="text-right">소진</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($stockRows as $r): ?>
|
||||
<tr>
|
||||
<td class="font-medium text-gray-800"><?= esc($r[0]) ?></td>
|
||||
<td class="text-right tabular-nums"><?= esc($r[1]) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$badge = match ($r[2]) {
|
||||
'안전' => 'bg-emerald-100 text-emerald-800',
|
||||
'주의' => 'bg-amber-100 text-amber-800',
|
||||
'부족' => 'bg-red-100 text-red-800',
|
||||
default => 'bg-gray-100 text-gray-700',
|
||||
};
|
||||
?>
|
||||
<span class="text-[10px] px-1 py-0 rounded <?= $badge ?>"><?= esc($r[2]) ?></span>
|
||||
</td>
|
||||
<td class="text-right text-gray-600"><?= esc($r[3]) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="xl:col-span-4 bg-white border border-gray-200 rounded shadow-sm overflow-hidden">
|
||||
<div class="px-2 py-1 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
|
||||
<h2 class="text-[11px] font-bold text-gray-800"><i class="fa-solid fa-list-check text-[#2b4c8c] mr-1"></i>발주 / 구매신청 진행</h2>
|
||||
<span class="text-[10px] text-gray-500">최근 5건</span>
|
||||
</div>
|
||||
<div class="overflow-x-auto max-h-[200px] overflow-y-auto">
|
||||
<table class="w-full dense-table text-left">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>문서</th>
|
||||
<th>상대</th>
|
||||
<th>내용</th>
|
||||
<th>단계</th>
|
||||
<th class="text-right">시각</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($orderRows as $r): ?>
|
||||
<tr>
|
||||
<td class="text-blue-700 font-mono text-[10px]"><?= esc($r[0]) ?></td>
|
||||
<td class="truncate max-w-[4.5rem]" title="<?= esc($r[1]) ?>"><?= esc($r[1]) ?></td>
|
||||
<td class="truncate max-w-[5rem]" title="<?= esc($r[2]) ?>"><?= esc($r[2]) ?></td>
|
||||
<td><span class="text-[10px] bg-slate-100 px-1 rounded"><?= esc($r[3]) ?></span></td>
|
||||
<td class="text-right text-gray-500 text-[10px] whitespace-nowrap"><?= esc($r[4]) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="xl:col-span-4 bg-white border border-gray-200 rounded shadow-sm overflow-hidden flex flex-col">
|
||||
<div class="px-2 py-1 border-b border-gray-200 bg-gray-50">
|
||||
<h2 class="text-[11px] font-bold text-gray-800"><i class="fa-solid fa-clock-rotate-left text-[#2b4c8c] mr-1"></i>최근 이벤트 로그</h2>
|
||||
</div>
|
||||
<ul class="flex-1 max-h-[120px] overflow-y-auto divide-y divide-gray-100 px-2 py-0.5">
|
||||
<?php foreach ($logRows as $L): ?>
|
||||
<li class="py-1 flex gap-2 text-[10px]">
|
||||
<span class="text-gray-400 font-mono shrink-0 w-8"><?= esc($L[0]) ?></span>
|
||||
<span class="shrink-0 w-12 text-center rounded bg-gray-100 text-gray-600"><?= esc($L[1]) ?></span>
|
||||
<span class="text-gray-700"><?= $L[2] ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<div class="border-t border-gray-100 px-2 py-1.5 bg-gray-50/80">
|
||||
<div class="text-[10px] font-semibold text-gray-600 mb-1">주간 봉투 출고(천 장, 목업)</div>
|
||||
<div class="spark" title="주간 추이">
|
||||
<?php foreach ([40, 55, 48, 62, 58, 71, 65] as $h): ?>
|
||||
<span style="height: <?= (int) $h ?>%"></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="flex justify-between text-[9px] text-gray-400 mt-0.5">
|
||||
<span>월</span><span>화</span><span>수</span><span>목</span><span>금</span><span>토</span><span>일</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- charts: 요약 차트 4종 (dense 행 아래) -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-2 mb-2">
|
||||
<section class="chart-card">
|
||||
<h2><i class="fa-solid fa-chart-pie text-[#2b4c8c] mr-1"></i>규격 출고 비중</h2>
|
||||
<div class="chart-wrap"><canvas id="chDoughnutSpec"></canvas></div>
|
||||
</section>
|
||||
<section class="chart-card">
|
||||
<h2><i class="fa-solid fa-circle-notch text-[#2b4c8c] mr-1"></i>구매신청 처리 단계</h2>
|
||||
<div class="chart-wrap"><canvas id="chDoughnutFlow"></canvas></div>
|
||||
</section>
|
||||
<section class="chart-card">
|
||||
<h2><i class="fa-solid fa-calendar-week text-[#2b4c8c] mr-1"></i>금주 일별 출고(천장)</h2>
|
||||
<div class="chart-wrap"><canvas id="chBarWeek"></canvas></div>
|
||||
</section>
|
||||
<section class="chart-card">
|
||||
<h2><i class="fa-solid fa-bullseye text-[#2b4c8c] mr-1"></i>운영 지표 (목업)</h2>
|
||||
<div class="chart-wrap"><canvas id="chRadar"></canvas></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="chart-card mb-2">
|
||||
<h2><i class="fa-solid fa-chart-line text-[#2b4c8c] mr-1"></i>월별 출고 vs 구매신청 건수 (최근 12개월)</h2>
|
||||
<div class="chart-wrap tall"><canvas id="chLineYear"></canvas></div>
|
||||
</section>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-2 mb-2">
|
||||
<section class="chart-card">
|
||||
<h2><i class="fa-solid fa-boxes-stacked text-[#2b4c8c] mr-1"></i>품목별 재고 (천 장)</h2>
|
||||
<div class="chart-wrap wide"><canvas id="chBarSku"></canvas></div>
|
||||
</section>
|
||||
<section class="chart-card">
|
||||
<h2><i class="fa-solid fa-store text-[#2b4c8c] mr-1"></i>판매소별 월 출고 TOP</h2>
|
||||
<div class="chart-wrap wide"><canvas id="chBarHStore"></canvas></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-2 mb-2">
|
||||
<section class="bg-white border border-gray-200 rounded shadow-sm overflow-hidden">
|
||||
<div class="px-2 py-1 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
|
||||
<h2 class="text-[11px] font-bold text-gray-800"><i class="fa-solid fa-store text-[#2b4c8c] mr-1"></i>지정판매소 요약</h2>
|
||||
<span class="text-[10px] text-gray-500">상위 5곳</span>
|
||||
</div>
|
||||
<table class="w-full dense-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>판매소명</th>
|
||||
<th class="text-right">월 봉투(백장)</th>
|
||||
<th>상태</th>
|
||||
<th class="text-right">최종거래</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($storeSummary as $s): ?>
|
||||
<tr>
|
||||
<td class="font-medium"><?= esc($s[0]) ?></td>
|
||||
<td class="text-right tabular-nums"><?= esc($s[1]) ?></td>
|
||||
<td>
|
||||
<?php if ($s[2] === '정상'): ?>
|
||||
<span class="text-[10px] text-emerald-700"><?= esc($s[2]) ?></span>
|
||||
<?php else: ?>
|
||||
<span class="text-[10px] text-red-600"><?= esc($s[2]) ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-right text-gray-500"><?= esc($s[3]) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="bg-white border border-gray-200 rounded shadow-sm overflow-hidden">
|
||||
<div class="px-2 py-1 border-b border-gray-200 bg-gray-50">
|
||||
<h2 class="text-[11px] font-bold text-gray-800"><i class="fa-solid fa-user-check text-[#2b4c8c] mr-1"></i>회원·판매소 승인 대기</h2>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-1 p-1.5 border-b border-gray-100 bg-gray-50/50 text-center">
|
||||
<div>
|
||||
<div class="text-lg font-bold text-[#2b4c8c]">4</div>
|
||||
<div class="text-[9px] text-gray-500">전체 대기</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-lg font-bold text-amber-600">2</div>
|
||||
<div class="text-[9px] text-gray-500">오늘 접수</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-lg font-bold text-gray-600">1.2일</div>
|
||||
<div class="text-[9px] text-gray-500">평균 처리</div>
|
||||
</div>
|
||||
</div>
|
||||
<table class="w-full dense-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>신청자</th>
|
||||
<th>유형</th>
|
||||
<th>접수일</th>
|
||||
<th>메모</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($approvals as $a): ?>
|
||||
<tr>
|
||||
<td><?= esc($a[0]) ?></td>
|
||||
<td><?= esc($a[1]) ?></td>
|
||||
<td class="text-gray-600"><?= esc($a[2]) ?></td>
|
||||
<td class="text-gray-500 truncate max-w-[6rem]"><?= esc($a[3]) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-2 mb-2">
|
||||
<section class="chart-card">
|
||||
<h2><i class="fa-solid fa-layer-group text-[#2b4c8c] mr-1"></i>분기별 입고 / 출고 / 조정</h2>
|
||||
<div class="chart-wrap wide"><canvas id="chStackedQ"></canvas></div>
|
||||
</section>
|
||||
<section class="chart-card">
|
||||
<h2><i class="fa-solid fa-compass text-[#2b4c8c] mr-1"></i>요일·시간대 신청 분포</h2>
|
||||
<div class="chart-wrap wide"><canvas id="chPolarTime"></canvas></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-gray-200 rounded shadow-sm p-2 mb-2">
|
||||
<h3 class="text-[11px] font-bold text-gray-800 mb-1"><i class="fa-solid fa-clipboard-list text-[#2b4c8c] mr-1"></i>운영 브리핑 · 추가 그래프</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<ul class="text-[11px] text-gray-600 space-y-0.5 list-disc list-inside">
|
||||
<li>다음 주 예상 구매신청 <strong class="text-gray-900">약 28건</strong> (전주 대비 +12%)</li>
|
||||
<li><strong class="text-red-700">일반 20L</strong>·<strong class="text-red-700">특수규격 A</strong> 발주 권고</li>
|
||||
<li>세금계산서 <strong>6건</strong> 미발행 — 회계 알림 발송됨</li>
|
||||
</ul>
|
||||
<div class="chart-card border-0 shadow-none">
|
||||
<h2 class="rounded-t"><i class="fa-solid fa-chart-area text-[#2b4c8c] mr-1"></i>누적 출고 추이 (올해)</h2>
|
||||
<div class="chart-wrap"><canvas id="chAreaCum"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-[10px] text-gray-400">
|
||||
<strong class="text-gray-600">/dashboard/blend</strong> (표 + 차트 혼합)
|
||||
· <a href="<?= esc($dashDense) ?>" class="text-[#2b4c8c] hover:underline">/dashboard/dense</a>
|
||||
· <a href="<?= esc($dashCharts) ?>" class="text-[#2b4c8c] hover:underline">/dashboard/charts</a>
|
||||
· <a href="<?= esc($dashClassic) ?>" class="text-[#2b4c8c] hover:underline">클래식</a>
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const C = {
|
||||
primary: '#2b4c8c',
|
||||
blue: '#3b82f6',
|
||||
teal: '#0d9488',
|
||||
emerald: '#059669',
|
||||
amber: '#d97706',
|
||||
rose: '#e11d48',
|
||||
violet: '#7c3aed',
|
||||
slate: '#64748b',
|
||||
grid: 'rgba(0,0,0,.06)',
|
||||
};
|
||||
|
||||
Chart.defaults.font.family = "'Malgun Gothic','Apple SD Gothic Neo','Noto Sans KR',sans-serif";
|
||||
Chart.defaults.font.size = 11;
|
||||
Chart.defaults.color = '#4b5563';
|
||||
|
||||
const commonOpts = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: 'bottom', labels: { boxWidth: 10, padding: 8, font: { size: 10 } } },
|
||||
},
|
||||
};
|
||||
|
||||
const axisOpts = {
|
||||
scales: {
|
||||
x: { grid: { color: C.grid }, ticks: { maxRotation: 45, minRotation: 0, font: { size: 10 } } },
|
||||
y: { grid: { color: C.grid }, ticks: { font: { size: 10 } }, beginAtZero: true },
|
||||
},
|
||||
};
|
||||
|
||||
new Chart(document.getElementById('chDoughnutSpec'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['일반 5L', '일반 10·20L', '음식물 스티커', '재사용', '기타'],
|
||||
datasets: [{
|
||||
data: [38, 22, 28, 9, 3],
|
||||
backgroundColor: [C.primary, C.blue, C.teal, C.emerald, C.slate],
|
||||
borderWidth: 0,
|
||||
}],
|
||||
},
|
||||
options: { ...commonOpts, cutout: '58%' },
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('chDoughnutFlow'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['접수', '검토', '발주', '납품', '완료'],
|
||||
datasets: [{
|
||||
data: [12, 8, 6, 5, 42],
|
||||
backgroundColor: [C.amber, C.blue, C.violet, C.teal, C.emerald],
|
||||
borderWidth: 0,
|
||||
}],
|
||||
},
|
||||
options: { ...commonOpts, cutout: '55%' },
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('chBarWeek'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['월', '화', '수', '목', '금', '토', '일'],
|
||||
datasets: [{
|
||||
label: '출고',
|
||||
data: [42, 55, 48, 61, 58, 22, 8],
|
||||
backgroundColor: C.primary,
|
||||
borderRadius: 4,
|
||||
}],
|
||||
},
|
||||
options: { ...commonOpts, ...axisOpts, plugins: { ...commonOpts.plugins, legend: { display: false } } },
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('chRadar'), {
|
||||
type: 'radar',
|
||||
data: {
|
||||
labels: ['재고안정', '신청처리', '납기준수', '민원응대', '데이터품질'],
|
||||
datasets: [
|
||||
{
|
||||
label: '이번 달',
|
||||
data: [82, 76, 88, 71, 85],
|
||||
borderColor: C.primary,
|
||||
backgroundColor: 'rgba(43, 76, 140, 0.2)',
|
||||
pointBackgroundColor: C.primary,
|
||||
},
|
||||
{
|
||||
label: '전월',
|
||||
data: [78, 80, 84, 75, 80],
|
||||
borderColor: C.blue,
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.12)',
|
||||
pointBackgroundColor: C.blue,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
...commonOpts,
|
||||
scales: {
|
||||
r: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
ticks: { stepSize: 20, font: { size: 9 } },
|
||||
grid: { color: C.grid },
|
||||
pointLabels: { font: { size: 10 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('chLineYear'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월', '1월', '2월'],
|
||||
datasets: [
|
||||
{
|
||||
label: '출고(천 장)',
|
||||
data: [320, 340, 310, 355, 380, 360, 370, 390, 400, 385, 410, 395],
|
||||
borderColor: C.primary,
|
||||
backgroundColor: 'rgba(43, 76, 140, 0.08)',
|
||||
fill: true,
|
||||
tension: 0.35,
|
||||
pointRadius: 3,
|
||||
},
|
||||
{
|
||||
label: '구매신청(건)',
|
||||
data: [118, 125, 112, 130, 142, 128, 135, 140, 155, 148, 160, 152],
|
||||
borderColor: C.teal,
|
||||
backgroundColor: 'transparent',
|
||||
tension: 0.35,
|
||||
yAxisID: 'y1',
|
||||
pointRadius: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
...commonOpts,
|
||||
...axisOpts,
|
||||
scales: {
|
||||
x: axisOpts.scales.x,
|
||||
y: { type: 'linear', position: 'left', grid: { color: C.grid }, title: { display: true, text: '출고', font: { size: 10 } } },
|
||||
y1: {
|
||||
type: 'linear',
|
||||
position: 'right',
|
||||
grid: { drawOnChartArea: false },
|
||||
title: { display: true, text: '건수', font: { size: 10 } },
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('chBarSku'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['5L', '10L', '20L', '스티커', '재사용', '특수'],
|
||||
datasets: [{
|
||||
label: '재고',
|
||||
data: [12.4, 8.2, 2.1, 15.0, 4.3, 0.9],
|
||||
backgroundColor: [C.primary, C.blue, C.amber, C.teal, C.emerald, C.rose],
|
||||
borderRadius: 4,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
...commonOpts,
|
||||
...axisOpts,
|
||||
indexAxis: 'x',
|
||||
plugins: { ...commonOpts.plugins, legend: { display: false } },
|
||||
},
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('chBarHStore'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['행복마트 북구', '◇◇할인점', '□□마트', '○○슈퍼', '△△상회'],
|
||||
datasets: [{
|
||||
label: '천 장',
|
||||
data: [5.2, 4.8, 3.9, 3.5, 2.1],
|
||||
backgroundColor: C.primary,
|
||||
borderRadius: 4,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
...commonOpts,
|
||||
indexAxis: 'y',
|
||||
plugins: { ...commonOpts.plugins, legend: { display: false } },
|
||||
scales: {
|
||||
x: { grid: { color: C.grid }, beginAtZero: true, ticks: { font: { size: 10 } } },
|
||||
y: { grid: { display: false }, ticks: { font: { size: 10 } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('chStackedQ'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['1분기', '2분기', '3분기', '4분기(예)'],
|
||||
datasets: [
|
||||
{ label: '입고', data: [420, 450, 480, 460], backgroundColor: C.emerald, stack: 's' },
|
||||
{ label: '출고', data: [380, 410, 440, 430], backgroundColor: C.primary, stack: 's' },
|
||||
{ label: '조정', data: [12, 8, 15, 10], backgroundColor: C.amber, stack: 's' },
|
||||
],
|
||||
},
|
||||
options: {
|
||||
...commonOpts,
|
||||
scales: {
|
||||
x: { stacked: true, grid: { display: false }, ticks: { font: { size: 10 } } },
|
||||
y: { stacked: true, grid: { color: C.grid }, ticks: { font: { size: 10 } }, beginAtZero: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('chAreaCum'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
|
||||
datasets: [{
|
||||
label: '누적 출고(만 장)',
|
||||
data: [3.2, 6.8, 10.5, 14.2, 18.0, 21.5, 25.1, 28.9, 32.4, 36.0, 39.8, 43.5],
|
||||
borderColor: C.blue,
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.25)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
}],
|
||||
},
|
||||
options: { ...commonOpts, ...axisOpts, plugins: { ...commonOpts.plugins, legend: { display: true } } },
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('chPolarTime'), {
|
||||
type: 'polarArea',
|
||||
data: {
|
||||
labels: ['평일 오전', '평일 오후', '평일 야간', '주말'],
|
||||
datasets: [{
|
||||
data: [28, 45, 8, 19],
|
||||
backgroundColor: [
|
||||
'rgba(43, 76, 140, 0.75)',
|
||||
'rgba(13, 148, 136, 0.7)',
|
||||
'rgba(217, 119, 6, 0.65)',
|
||||
'rgba(124, 58, 237, 0.65)',
|
||||
],
|
||||
borderWidth: 1,
|
||||
borderColor: '#fff',
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
...commonOpts,
|
||||
scales: {
|
||||
r: {
|
||||
ticks: { backdropColor: 'transparent', font: { size: 9 } },
|
||||
grid: { color: C.grid },
|
||||
pointLabels: { font: { size: 10 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -11,7 +11,6 @@ $dashClassic = base_url('dashboard/classic-mock');
|
||||
$dashModern = base_url('dashboard/modern');
|
||||
$dashDense = base_url('dashboard/dense');
|
||||
$dashCharts = base_url('dashboard/charts');
|
||||
$dashBlend = base_url('dashboard/blend');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
@@ -86,8 +85,6 @@ $dashBlend = base_url('dashboard/blend');
|
||||
<a href="<?= esc($dashModern) ?>" class="text-[#2b4c8c] hover:underline whitespace-nowrap hidden md:inline">모던</a>
|
||||
<span class="text-gray-300 hidden md:inline">|</span>
|
||||
<a href="<?= esc($dashDense) ?>" class="text-[#2b4c8c] hover:underline whitespace-nowrap hidden md:inline">종합</a>
|
||||
<span class="text-gray-300 hidden md:inline">|</span>
|
||||
<a href="<?= esc($dashBlend) ?>" class="text-[#2b4c8c] hover:underline whitespace-nowrap hidden md:inline" title="표+차트">혼합</a>
|
||||
<a href="<?= base_url('logout') ?>" class="text-gray-500 hover:text-gray-800 p-1 ml-1" title="로그아웃">
|
||||
<i class="fa-solid fa-arrow-right-from-bracket"></i>
|
||||
</a>
|
||||
@@ -168,7 +165,6 @@ $dashBlend = base_url('dashboard/blend');
|
||||
<a href="<?= esc($dashClassic) ?>" class="text-[#2b4c8c] hover:underline">/dashboard</a>
|
||||
· <a href="<?= esc($dashModern) ?>" class="text-[#2b4c8c] hover:underline">/dashboard/modern</a>
|
||||
· <a href="<?= esc($dashDense) ?>" class="text-[#2b4c8c] hover:underline">/dashboard/dense</a>
|
||||
· <a href="<?= esc($dashBlend) ?>" class="text-[#2b4c8c] hover:underline">/dashboard/blend</a>
|
||||
· <strong class="text-gray-600">/dashboard/charts</strong>
|
||||
</p>
|
||||
</main>
|
||||
|
||||
@@ -11,7 +11,6 @@ $dashClassic = base_url('dashboard/classic-mock');
|
||||
$dashModern = base_url('dashboard/modern');
|
||||
$dashDense = base_url('dashboard/dense');
|
||||
$dashCharts = base_url('dashboard/charts');
|
||||
$dashBlend = base_url('dashboard/blend');
|
||||
|
||||
$kpiTop = [
|
||||
['icon' => 'fa-triangle-exclamation', 'c' => 'text-amber-700', 'bg' => 'bg-amber-50', 'v' => '3', 'l' => '재고부족', 'sub' => '품목'],
|
||||
@@ -127,8 +126,6 @@ $notices = [
|
||||
<span class="text-gray-300 hidden md:inline">|</span>
|
||||
<a href="<?= esc($dashModern) ?>" class="text-[#2b4c8c] hover:underline whitespace-nowrap hidden md:inline">모던</a>
|
||||
<span class="text-gray-300 hidden md:inline">|</span>
|
||||
<a href="<?= esc($dashBlend) ?>" class="text-[#2b4c8c] hover:underline whitespace-nowrap hidden md:inline" title="표+차트 혼합">혼합</a>
|
||||
<span class="text-gray-300 hidden md:inline">|</span>
|
||||
<a href="<?= esc($dashCharts) ?>" class="text-[#2b4c8c] hover:underline whitespace-nowrap hidden md:inline" title="그래프 대시보드">차트</a>
|
||||
<a href="<?= base_url('logout') ?>" class="text-gray-500 hover:text-gray-800 p-1 ml-1" title="로그아웃">
|
||||
<i class="fa-solid fa-arrow-right-from-bracket"></i>
|
||||
@@ -387,7 +384,6 @@ $notices = [
|
||||
레이아웃: <a href="<?= esc($dashClassic) ?>" class="text-[#2b4c8c] hover:underline">/dashboard</a>
|
||||
· <a href="<?= esc($dashModern) ?>" class="text-[#2b4c8c] hover:underline">/dashboard/modern</a>
|
||||
· <strong class="text-gray-600">/dashboard/dense</strong> (이 화면)
|
||||
· <a href="<?= esc($dashBlend) ?>" class="text-[#2b4c8c] hover:underline">/dashboard/blend</a>
|
||||
· <a href="<?= esc($dashCharts) ?>" class="text-[#2b4c8c] hover:underline">/dashboard/charts</a>
|
||||
</p>
|
||||
</main>
|
||||
|
||||
@@ -11,7 +11,6 @@ $dashClassic = base_url('dashboard/classic-mock');
|
||||
$dashModern = base_url('dashboard/modern');
|
||||
$dashDense = base_url('dashboard/dense');
|
||||
$dashCharts = base_url('dashboard/charts');
|
||||
$dashBlend = base_url('dashboard/blend');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
@@ -71,8 +70,6 @@ $dashBlend = base_url('dashboard/blend');
|
||||
<span class="text-gray-300 hidden sm:inline">|</span>
|
||||
<a href="<?= esc($dashDense) ?>" class="text-xs text-[#2b4c8c] hover:underline whitespace-nowrap hidden sm:inline" title="정보 집약 종합">종합</a>
|
||||
<span class="text-gray-300 hidden sm:inline">|</span>
|
||||
<a href="<?= esc($dashBlend) ?>" class="text-xs text-[#2b4c8c] hover:underline whitespace-nowrap hidden sm:inline" title="표+차트">혼합</a>
|
||||
<span class="text-gray-300 hidden sm:inline">|</span>
|
||||
<a href="<?= esc($dashCharts) ?>" class="text-xs text-[#2b4c8c] hover:underline whitespace-nowrap hidden sm:inline" title="그래프 대시보드">차트</a>
|
||||
<a href="<?= base_url('logout') ?>" class="text-gray-500 hover:text-gray-800 p-1" title="로그아웃">
|
||||
<i class="fa-solid fa-arrow-right-from-bracket text-lg"></i>
|
||||
@@ -210,7 +207,6 @@ $dashBlend = base_url('dashboard/blend');
|
||||
<p class="text-center text-[11px] text-slate-400">
|
||||
URL 비교 — <strong class="text-slate-600">클래식 레이아웃</strong> <code class="bg-slate-100 px-1 rounded">/dashboard</code>
|
||||
· <strong class="text-slate-600">모던 콘텐츠(이 화면)</strong> <code class="bg-slate-100 px-1 rounded">/dashboard/modern</code>
|
||||
· <a href="<?= esc($dashBlend) ?>" class="text-[#2b4c8c] hover:underline">/dashboard/blend</a>
|
||||
· <a href="<?= esc($dashCharts) ?>" class="text-[#2b4c8c] hover:underline">/dashboard/charts</a>
|
||||
<span class="block sm:inline mt-1 sm:mt-0">· 상단 메뉴는 동일</span>
|
||||
</p>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-2 border-b pb-1">
|
||||
<h3 class="text-base font-bold text-gray-700">발주 현황</h3>
|
||||
<a href="<?= base_url('bag/order/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">발주 등록</a>
|
||||
<a href="<?= base_url('admin/bag-orders/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">발주 등록</a>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead><tr>
|
||||
@@ -37,9 +37,9 @@
|
||||
?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="<?= base_url('bag/purchase-inbound?detail=' . $row->bo_idx) ?>" class="text-blue-600 hover:underline text-xs">상세</a>
|
||||
<a href="<?= base_url('admin/bag-orders/detail/' . $row->bo_idx) ?>" class="text-blue-600 hover:underline text-xs">상세</a>
|
||||
<?php if (($row->bo_status ?? '') === 'normal'): ?>
|
||||
<form method="post" action="<?= base_url('bag/order/cancel/' . $row->bo_idx) ?>" class="inline" onsubmit="return confirm('취소하시겠습니까?')">
|
||||
<form method="post" action="<?= base_url('admin/bag-orders/cancel/' . $row->bo_idx) ?>" class="inline" onsubmit="return confirm('취소하시겠습니까?')">
|
||||
<?= csrf_field() ?>
|
||||
<button class="text-orange-600 hover:underline text-xs ml-1">취소</button>
|
||||
</form>
|
||||
@@ -58,7 +58,7 @@
|
||||
<section class="mt-4">
|
||||
<div class="flex items-center justify-between mb-2 border-b pb-1">
|
||||
<h3 class="text-base font-bold text-gray-700">입고 현황</h3>
|
||||
<a href="<?= base_url('bag/receiving/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">입고 처리</a>
|
||||
<a href="<?= base_url('admin/bag-receivings/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">입고 처리</a>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead><tr>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-2 border-b pb-1">
|
||||
<h3 class="text-base font-bold text-gray-700">주문 접수</h3>
|
||||
<a href="<?= base_url('bag/shop-order/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">주문 등록</a>
|
||||
<a href="<?= base_url('admin/shop-orders/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">주문 등록</a>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead><tr>
|
||||
@@ -47,7 +47,7 @@
|
||||
<section class="mt-4">
|
||||
<div class="flex items-center justify-between mb-2 border-b pb-1">
|
||||
<h3 class="text-base font-bold text-gray-700">판매/반품</h3>
|
||||
<a href="<?= base_url('bag/sale/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">판매 등록</a>
|
||||
<a href="<?= base_url('admin/bag-sales/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">판매 등록</a>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead><tr>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Custom Tailwind CSS Pagination View for CodeIgniter 4
|
||||
*
|
||||
* @var \CodeIgniter\Pager\PagerRenderer $pager
|
||||
*/
|
||||
|
||||
$pager->setSurroundCount(2);
|
||||
?>
|
||||
|
||||
<?php if ($pager->hasPreviousPage() || $pager->hasNextPage()): ?>
|
||||
<nav aria-label="Page navigation" class="flex items-center justify-center gap-1 mt-3 mb-2 no-print">
|
||||
<?php if ($pager->hasPreviousPage()): ?>
|
||||
<a href="<?= $pager->getFirst() ?>" class="px-2 py-1 text-xs border border-gray-300 rounded hover:bg-gray-100 text-gray-600" title="처음">«</a>
|
||||
<a href="<?= $pager->getPreviousPage() ?>" class="px-2 py-1 text-xs border border-gray-300 rounded hover:bg-gray-100 text-gray-600" title="이전">‹</a>
|
||||
<?php else: ?>
|
||||
<span class="px-2 py-1 text-xs border border-gray-200 rounded text-gray-300">«</span>
|
||||
<span class="px-2 py-1 text-xs border border-gray-200 rounded text-gray-300">‹</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($pager->links() as $link): ?>
|
||||
<?php if ($link['active']): ?>
|
||||
<span class="px-3 py-1 text-xs border border-blue-600 rounded bg-blue-600 text-white font-bold"><?= $link['title'] ?></span>
|
||||
<?php else: ?>
|
||||
<a href="<?= $link['uri'] ?>" class="px-3 py-1 text-xs border border-gray-300 rounded hover:bg-gray-100 text-gray-700"><?= $link['title'] ?></a>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if ($pager->hasNextPage()): ?>
|
||||
<a href="<?= $pager->getNextPage() ?>" class="px-2 py-1 text-xs border border-gray-300 rounded hover:bg-gray-100 text-gray-600" title="다음">›</a>
|
||||
<a href="<?= $pager->getLast() ?>" class="px-2 py-1 text-xs border border-gray-300 rounded hover:bg-gray-100 text-gray-600" title="마지막">»</a>
|
||||
<?php else: ?>
|
||||
<span class="px-2 py-1 text-xs border border-gray-200 rounded text-gray-300">›</span>
|
||||
<span class="px-2 py-1 text-xs border border-gray-200 rounded text-gray-300">»</span>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 인쇄용 문서 헤더 컴포넌트
|
||||
*
|
||||
* 사용 변수:
|
||||
* $printTitle - 문서 제목 (필수)
|
||||
* $printLgName - 지자체명 (선택, 미지정 시 세션에서 조회)
|
||||
* $printDate - 날짜 (선택, 기본 오늘)
|
||||
*/
|
||||
|
||||
if (! isset($printLgName)) {
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
$printLgName = '';
|
||||
if ($lgIdx) {
|
||||
$lgRow = model(\App\Models\LocalGovernmentModel::class)->find($lgIdx);
|
||||
$printLgName = $lgRow ? $lgRow->lg_name : '';
|
||||
}
|
||||
}
|
||||
$printDate = $printDate ?? date('Y-m-d');
|
||||
$printTitle = $printTitle ?? '';
|
||||
?>
|
||||
|
||||
<div class="print-header" style="display:none;">
|
||||
<table style="width:100%; border-collapse:collapse; margin-bottom:10px;">
|
||||
<tr>
|
||||
<td style="width:60%; vertical-align:bottom;">
|
||||
<div style="font-size:12px; color:#666; margin-bottom:4px;"><?= esc($printLgName) ?></div>
|
||||
<div style="font-size:20px; font-weight:bold; letter-spacing:2px;"><?= esc($printTitle) ?></div>
|
||||
<div style="font-size:11px; color:#888; margin-top:4px;">출력일: <?= esc($printDate) ?></div>
|
||||
</td>
|
||||
<td style="width:40%; vertical-align:top;">
|
||||
<table style="border-collapse:collapse; float:right; font-size:11px;">
|
||||
<tr>
|
||||
<th style="border:1px solid #333; padding:4px 12px; background:#f0f0f0; text-align:center;">담당</th>
|
||||
<th style="border:1px solid #333; padding:4px 12px; background:#f0f0f0; text-align:center;">팀장</th>
|
||||
<th style="border:1px solid #333; padding:4px 12px; background:#f0f0f0; text-align:center;">과장</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #333; padding:8px 12px; height:40px; min-width:60px;"> </td>
|
||||
<td style="border:1px solid #333; padding:8px 12px; height:40px; min-width:60px;"> </td>
|
||||
<td style="border:1px solid #333; padding:8px 12px; height:40px; min-width:60px;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr style="border:1px solid #333; margin-bottom:10px;"/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@media print {
|
||||
.print-header { display: block !important; }
|
||||
}
|
||||
</style>
|
||||
@@ -11,8 +11,7 @@
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"codeigniter4/framework": "^4.7",
|
||||
"robthree/twofactorauth": "^3.0"
|
||||
"codeigniter4/framework": "^4.7"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.9",
|
||||
|
||||
83
composer.lock
generated
83
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "62775ba19440bda6e4bb1fbe91908932",
|
||||
"content-hash": "f5cce40800fa5dae1504b9364f585e6a",
|
||||
"packages": [
|
||||
{
|
||||
"name": "codeigniter4/framework",
|
||||
@@ -193,87 +193,6 @@
|
||||
"source": "https://github.com/php-fig/log/tree/3.0.2"
|
||||
},
|
||||
"time": "2024-09-11T13:17:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "robthree/twofactorauth",
|
||||
"version": "v3.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/RobThree/TwoFactorAuth.git",
|
||||
"reference": "85408c4e775dba7c0802f2d928efd921d530bc5b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/85408c4e775dba7c0802f2d928efd921d530bc5b",
|
||||
"reference": "85408c4e775dba7c0802f2d928efd921d530bc5b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.13",
|
||||
"phpstan/phpstan": "^1.9",
|
||||
"phpunit/phpunit": "^9"
|
||||
},
|
||||
"suggest": {
|
||||
"bacon/bacon-qr-code": "Needed for BaconQrCodeProvider provider",
|
||||
"endroid/qr-code": "Needed for EndroidQrCodeProvider"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"RobThree\\Auth\\": "lib"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Rob Janssen",
|
||||
"homepage": "http://robiii.me",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Nicolas CARPi",
|
||||
"homepage": "https://github.com/NicolasCARPi",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Will Power",
|
||||
"homepage": "https://github.com/willpower232",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Two Factor Authentication",
|
||||
"homepage": "https://github.com/RobThree/TwoFactorAuth",
|
||||
"keywords": [
|
||||
"Authentication",
|
||||
"MFA",
|
||||
"Multi Factor Authentication",
|
||||
"Two Factor Authentication",
|
||||
"authenticator",
|
||||
"authy",
|
||||
"php",
|
||||
"tfa"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/RobThree/TwoFactorAuth/issues",
|
||||
"source": "https://github.com/RobThree/TwoFactorAuth"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://paypal.me/robiii",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/RobThree",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-05T13:17:41+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
|
||||
186
docs/SERVER_DEPLOY.md
Normal file
186
docs/SERVER_DEPLOY.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# 서버 배포 가이드
|
||||
|
||||
## 서버 정보
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|------|
|
||||
| 서버 IP | `116.122.157.166` |
|
||||
| OS | Ubuntu 22.04.5 LTS |
|
||||
| SSH 접속 | `ssh wixon@116.122.157.166` (비밀번호: `1111`) |
|
||||
| 도메인 | `trash.wxn.co.kr` |
|
||||
| 호스팅 | 카페24 (qm391-0435.cafe24.com) |
|
||||
|
||||
## Gitea (Git 서버)
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|------|
|
||||
| 내부 URL | `http://localhost:3001` |
|
||||
| 컨테이너 | `gitea` (Docker) |
|
||||
| 관리자 계정 | `wixon` / `wixon1234!` |
|
||||
| 관리자 이메일 | `admin@wxn.co.kr` |
|
||||
| 레포 URL (내부) | `http://localhost:3001/wixon/jongryangje` |
|
||||
| SSH 포트 (내부) | `2222` |
|
||||
| DB | SQLite (`/srv/gitea/data/gitea/gitea.db`) |
|
||||
| 데이터 경로 | `/srv/gitea/data` |
|
||||
| docker-compose | `/srv/gitea/docker-compose.yml` |
|
||||
|
||||
### Gitea 관리 명령
|
||||
|
||||
```bash
|
||||
# 컨테이너 상태 확인
|
||||
sudo docker ps | grep gitea
|
||||
|
||||
# 재시작
|
||||
cd /srv/gitea && sudo docker compose restart
|
||||
|
||||
# 로그 확인
|
||||
sudo docker logs gitea --tail 50
|
||||
|
||||
# 사용자 추가
|
||||
sudo docker exec -u git gitea gitea admin user create \
|
||||
--username <name> --password <pass> --email <email> --admin
|
||||
```
|
||||
|
||||
## 웹 서비스 구조
|
||||
|
||||
```
|
||||
[클라이언트] → [NPM (80/443)] → [nginx (8045)] → [PHP-FPM 8.2] → [CodeIgniter 4]
|
||||
↓
|
||||
[MySQL 3.36.27.239]
|
||||
```
|
||||
|
||||
| 컴포넌트 | 포트 | 설명 |
|
||||
|----------|------|------|
|
||||
| Nginx Proxy Manager | 80/443 | 도메인 프록시 (Docker) |
|
||||
| nginx | 8045 | PHP-FPM 연동 vhost |
|
||||
| PHP-FPM 8.2 | unix socket | `/run/php/php8.2-fpm.sock` |
|
||||
| Gitea | 3001 | Git 서버 (Docker) |
|
||||
| MySQL | 3306 (원격 3.36.27.239) | 데이터베이스 |
|
||||
|
||||
### 프로젝트 배포 경로
|
||||
|
||||
```
|
||||
/srv/jongryangje/ # 프로젝트 루트 (git clone)
|
||||
/srv/jongryangje/public/ # nginx document root
|
||||
/srv/jongryangje/.env # 환경 설정 (gitignore)
|
||||
/srv/gitea/ # Gitea Docker 설정
|
||||
```
|
||||
|
||||
### nginx 설정
|
||||
|
||||
```
|
||||
/etc/nginx/sites-available/jongryangje # vhost 설정
|
||||
/etc/nginx/sites-enabled/jongryangje # symlink
|
||||
```
|
||||
|
||||
### NPM 프록시 설정
|
||||
|
||||
NPM 컨테이너 내부에서 `trash.wxn.co.kr → 116.122.157.166:8045` 프록시:
|
||||
```
|
||||
/data/nginx/proxy_host/7.conf
|
||||
```
|
||||
|
||||
## 자동 배포 (CI/CD)
|
||||
|
||||
### 플로우
|
||||
|
||||
```
|
||||
[로컬 git push] → [Gitea] → [Webhook (port 9000)] → [deploy.sh] → [git pull + composer install]
|
||||
```
|
||||
|
||||
### 관련 파일
|
||||
|
||||
| 파일 | 용도 |
|
||||
|------|------|
|
||||
| `/srv/jongryangje/deploy.sh` | 배포 스크립트 (git pull + composer) |
|
||||
| `/srv/jongryangje/webhook-server.sh` | Webhook 수신 서버 |
|
||||
| `/etc/systemd/system/jongryangje-webhook.service` | Webhook systemd 서비스 |
|
||||
| `/srv/jongryangje/writable/logs/deploy.log` | 배포 로그 |
|
||||
|
||||
### 배포 명령
|
||||
|
||||
```bash
|
||||
# 수동 배포
|
||||
ssh wixon@116.122.157.166 "/srv/jongryangje/deploy.sh"
|
||||
|
||||
# 자동 배포 (로컬에서 push만 하면 됨)
|
||||
git push gitea main
|
||||
|
||||
# 배포 로그 확인
|
||||
ssh wixon@116.122.157.166 "tail -20 /srv/jongryangje/writable/logs/deploy.log"
|
||||
|
||||
# webhook 서비스 상태
|
||||
ssh wixon@116.122.157.166 "sudo systemctl status jongryangje-webhook"
|
||||
```
|
||||
|
||||
## 로컬 Git 설정
|
||||
|
||||
### Remote 목록
|
||||
|
||||
| Remote | URL | 용도 |
|
||||
|--------|-----|------|
|
||||
| `origin` | `github.com/wixon-associates/jongryangje` | GitHub (메인) |
|
||||
| `gitea` | `localhost:13001/wixon/jongryangje` (SSH 터널) | 서버 배포용 |
|
||||
|
||||
### SSH 터널 (Gitea push용)
|
||||
|
||||
외부에서 Gitea 3001 포트에 직접 접근할 수 없어 SSH 터널을 사용합니다.
|
||||
|
||||
```bash
|
||||
# 터널 열기
|
||||
sshpass -p '1111' ssh -f -N -L 13001:localhost:3001 wixon@116.122.157.166
|
||||
|
||||
# push (터널 열린 상태에서)
|
||||
git push gitea main
|
||||
|
||||
# 또는 origin(GitHub)과 gitea 모두 push
|
||||
git push origin main && git push gitea main
|
||||
```
|
||||
|
||||
### push 자동화 스크립트
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# push-all.sh — GitHub + Gitea 동시 push
|
||||
git push origin main
|
||||
# SSH 터널 확인/열기
|
||||
if ! nc -z localhost 13001 2>/dev/null; then
|
||||
sshpass -p '1111' ssh -f -N -L 13001:localhost:3001 wixon@116.122.157.166
|
||||
sleep 2
|
||||
fi
|
||||
git push gitea main
|
||||
echo "Push complete to GitHub + Gitea (auto-deploy triggered)"
|
||||
```
|
||||
|
||||
## DNS 설정
|
||||
|
||||
`trash.wxn.co.kr` A 레코드를 `116.122.157.166`으로 설정 필요.
|
||||
|
||||
```
|
||||
trash.wxn.co.kr → A → 116.122.157.166
|
||||
```
|
||||
|
||||
DNS가 적용되면 `http://trash.wxn.co.kr`으로 접속 가능합니다.
|
||||
|
||||
## 서비스 관리
|
||||
|
||||
```bash
|
||||
# nginx 재시작
|
||||
sudo systemctl restart nginx
|
||||
|
||||
# PHP-FPM 재시작
|
||||
sudo systemctl restart php8.2-fpm
|
||||
|
||||
# Gitea 재시작
|
||||
cd /srv/gitea && sudo docker compose restart
|
||||
|
||||
# NPM 재시작
|
||||
sudo docker restart nginx-proxy-manager
|
||||
|
||||
# Webhook 재시작
|
||||
sudo systemctl restart jongryangje-webhook
|
||||
|
||||
# 전체 상태 확인
|
||||
sudo systemctl status nginx php8.2-fpm jongryangje-webhook
|
||||
sudo docker ps
|
||||
```
|
||||
261
docs/server.md
261
docs/server.md
@@ -1,261 +0,0 @@
|
||||
# 서버 인프라 / 배포 가이드
|
||||
|
||||
## 접속 정보 요약
|
||||
|
||||
| 서비스 | URL | 계정 |
|
||||
|--------|-----|------|
|
||||
| 웹 서비스 | https://trash.wxn.co.kr | (아래 테스트 계정 참조) |
|
||||
| Gitea | https://gitea.wxn.co.kr | `wixon` / `wixon1234!` |
|
||||
| SSH | `ssh wixon@116.122.157.166` | `wixon` / `1111` |
|
||||
|
||||
### 웹 테스트 계정 (비밀번호: `test1234!`)
|
||||
|
||||
| ID | 역할 | Level | 소속 |
|
||||
|----|------|-------|------|
|
||||
| `tester_admin` | Super Admin | 4 | 전체 (지자체 선택 필요) |
|
||||
| `tester_local` | 지자체관리자 | 3 | 중구청 |
|
||||
| `tester_shop` | 지정판매소 | 2 | - |
|
||||
| `tester_user` | 일반 사용자 | 1 | - |
|
||||
|
||||
---
|
||||
|
||||
## 1. 서버 환경
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|------|
|
||||
| IP | `116.122.157.166` |
|
||||
| OS | Ubuntu 22.04.5 LTS |
|
||||
| 호스팅 | 카페24 (`qm391-0435.cafe24.com`) |
|
||||
| PHP | 8.2.30 (PHP-FPM) |
|
||||
| nginx | 호스트 설치 (80/443/8045) |
|
||||
| Docker | 28.5.1 |
|
||||
| DB | MySQL (원격 `3.36.27.239`) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 서비스 구조
|
||||
|
||||
```
|
||||
┌─── https://trash.wxn.co.kr
|
||||
[클라이언트] ─── nginx ───┤
|
||||
(80/443) └─── https://gitea.wxn.co.kr
|
||||
│ │
|
||||
▼ ▼
|
||||
nginx (8045) Gitea Docker (3001)
|
||||
│
|
||||
PHP-FPM 8.2
|
||||
│
|
||||
CodeIgniter 4
|
||||
│
|
||||
MySQL 3.36.27.239
|
||||
```
|
||||
|
||||
### 포트 구성
|
||||
|
||||
| 포트 | 서비스 | 설명 |
|
||||
|------|--------|------|
|
||||
| 80 | nginx | HTTP → HTTPS 리다이렉트 |
|
||||
| 443 | nginx | HTTPS (Let's Encrypt) |
|
||||
| 8045 | nginx | PHP-FPM vhost (내부) |
|
||||
| 3001 | Gitea | Git 서버 (Docker, 내부) |
|
||||
| 9000 | webhook | 배포 트리거 수신 (내부) |
|
||||
| 3306 | MySQL | 원격 DB (`3.36.27.239`) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Gitea (Git 서버)
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|------|
|
||||
| 외부 URL | https://gitea.wxn.co.kr |
|
||||
| 관리자 | `wixon` / `wixon1234!` (`admin@wxn.co.kr`) |
|
||||
| 레포 | https://gitea.wxn.co.kr/wixon/jongryangje |
|
||||
| 컨테이너 | `gitea` (Docker) |
|
||||
| DB | SQLite (`/srv/gitea/data/gitea/gitea.db`) |
|
||||
| docker-compose | `/srv/gitea/docker-compose.yml` |
|
||||
| 데이터 | `/srv/gitea/data` |
|
||||
|
||||
### 관리 명령
|
||||
|
||||
```bash
|
||||
# 상태 확인
|
||||
sudo docker ps | grep gitea
|
||||
|
||||
# 재시작
|
||||
cd /srv/gitea && sudo docker compose restart
|
||||
|
||||
# 로그
|
||||
sudo docker logs gitea --tail 50
|
||||
|
||||
# 사용자 추가
|
||||
sudo docker exec -u git gitea gitea admin user create \
|
||||
--username <name> --password <pass> --email <email> --admin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 자동 배포 (CI/CD)
|
||||
|
||||
### 플로우
|
||||
|
||||
```
|
||||
로컬 git push → Gitea → Webhook (9000) → deploy.sh → 서버 반영
|
||||
```
|
||||
|
||||
`git push gitea main` 한 번이면 서버에 자동 반영됩니다.
|
||||
|
||||
### 배포 스크립트 (`/srv/jongryangje/deploy.sh`)
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
cd /srv/jongryangje
|
||||
git fetch origin main && git reset --hard origin/main
|
||||
composer install --no-dev --no-interaction | tail -3
|
||||
chmod -R 777 writable/
|
||||
echo "Deployed at $(date)"
|
||||
```
|
||||
|
||||
### 관련 파일
|
||||
|
||||
| 파일 | 용도 |
|
||||
|------|------|
|
||||
| `/srv/jongryangje/deploy.sh` | 배포 실행 (git fetch + reset + composer) |
|
||||
| `/srv/jongryangje/webhook-server.sh` | Webhook 수신 (nc 기반) |
|
||||
| `/etc/systemd/system/jongryangje-webhook.service` | Webhook systemd 서비스 |
|
||||
| `/srv/jongryangje/writable/logs/deploy.log` | 배포 로그 |
|
||||
|
||||
### 수동 배포
|
||||
|
||||
```bash
|
||||
ssh wixon@116.122.157.166 "/srv/jongryangje/deploy.sh"
|
||||
```
|
||||
|
||||
### 배포 로그 확인
|
||||
|
||||
```bash
|
||||
ssh wixon@116.122.157.166 "tail -20 /srv/jongryangje/writable/logs/deploy.log"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 로컬 Git 설정
|
||||
|
||||
### Remote
|
||||
|
||||
| Remote | URL | 용도 |
|
||||
|--------|-----|------|
|
||||
| `origin` | `github.com/wixon-associates/jongryangje` | GitHub (소스 관리) |
|
||||
| `gitea` | `gitea.wxn.co.kr/wixon/jongryangje` | 서버 배포 (auto-deploy) |
|
||||
|
||||
### Push 방법
|
||||
|
||||
```bash
|
||||
# GitHub + 서버 동시 배포
|
||||
git push origin main && git push gitea main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 서버 파일 구조
|
||||
|
||||
```
|
||||
/srv/
|
||||
├── jongryangje/ # 프로젝트 (git clone)
|
||||
│ ├── public/ # nginx document root
|
||||
│ ├── .env # 환경 설정 (gitignore)
|
||||
│ ├── deploy.sh # 배포 스크립트
|
||||
│ └── webhook-server.sh # Webhook 수신
|
||||
└── gitea/ # Gitea Docker
|
||||
├── docker-compose.yml
|
||||
└── data/ # Gitea 데이터
|
||||
|
||||
/etc/nginx/sites-available/
|
||||
├── jongryangje # PHP-FPM vhost (:8045)
|
||||
├── jongryangje-ssl # trash.wxn.co.kr (80→443, SSL)
|
||||
└── gitea # gitea.wxn.co.kr (80→443, SSL → :3001)
|
||||
|
||||
/etc/letsencrypt/live/
|
||||
├── trash.wxn.co.kr/ # 웹 SSL 인증서
|
||||
└── gitea.wxn.co.kr/ # Gitea SSL 인증서
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. SSL 인증서 (Let's Encrypt)
|
||||
|
||||
| 도메인 | 인증서 경로 | 만료 |
|
||||
|--------|-------------|------|
|
||||
| `trash.wxn.co.kr` | `/etc/letsencrypt/live/trash.wxn.co.kr/` | 자동 갱신 (certbot) |
|
||||
| `gitea.wxn.co.kr` | `/etc/letsencrypt/live/gitea.wxn.co.kr/` | 자동 갱신 (certbot) |
|
||||
|
||||
```bash
|
||||
# 인증서 상태 확인
|
||||
sudo certbot certificates
|
||||
|
||||
# 수동 갱신
|
||||
sudo certbot renew
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 서비스 관리
|
||||
|
||||
```bash
|
||||
# nginx
|
||||
sudo systemctl restart nginx
|
||||
sudo nginx -t # 설정 테스트
|
||||
|
||||
# PHP-FPM
|
||||
sudo systemctl restart php8.2-fpm
|
||||
|
||||
# Gitea
|
||||
cd /srv/gitea && sudo docker compose restart
|
||||
|
||||
# Webhook
|
||||
sudo systemctl restart jongryangje-webhook
|
||||
|
||||
# 전체 상태
|
||||
sudo systemctl status nginx php8.2-fpm jongryangje-webhook
|
||||
sudo docker ps
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. DNS 설정
|
||||
|
||||
| 도메인 | 타입 | 값 |
|
||||
|--------|------|------|
|
||||
| `trash.wxn.co.kr` | A | `116.122.157.166` |
|
||||
| `gitea.wxn.co.kr` | A | `116.122.157.166` |
|
||||
|
||||
---
|
||||
|
||||
## 10. 트러블슈팅
|
||||
|
||||
### 사이트 접속 안 됨
|
||||
```bash
|
||||
# nginx 상태 확인
|
||||
sudo systemctl status nginx
|
||||
sudo tail -20 /var/log/nginx/error.log
|
||||
|
||||
# PHP-FPM 확인
|
||||
sudo systemctl status php8.2-fpm
|
||||
```
|
||||
|
||||
### 배포 안 됨
|
||||
```bash
|
||||
# webhook 서비스 확인
|
||||
sudo systemctl status jongryangje-webhook
|
||||
|
||||
# 수동 배포로 테스트
|
||||
/srv/jongryangje/deploy.sh
|
||||
|
||||
# 배포 로그
|
||||
tail -30 /srv/jongryangje/writable/logs/deploy.log
|
||||
```
|
||||
|
||||
### Gitea 접속 안 됨
|
||||
```bash
|
||||
sudo docker ps | grep gitea
|
||||
sudo docker logs gitea --tail 30
|
||||
```
|
||||
@@ -1,297 +0,0 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { login } = require('./helpers/auth');
|
||||
|
||||
async function loginAsAdmin(page) {
|
||||
await login(page, 'admin');
|
||||
await page.locator('input[name="lg_idx"]').first().check();
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
|
||||
}
|
||||
|
||||
async function loginAsLocal(page) {
|
||||
await login(page, 'local');
|
||||
}
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// CT-01: 페이지네이션
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
test.describe('CT-01: 페이지네이션', () => {
|
||||
test('발주 목록에 데이터 테이블 존재', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/bag-orders');
|
||||
await expect(page.locator('table.data-table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('판매 목록에 데이터 테이블 존재', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/bag-sales');
|
||||
await expect(page.locator('table.data-table')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// CT-02: 엑셀 저장
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
test.describe('CT-02: 엑셀 저장', () => {
|
||||
test('발주 엑셀 다운로드', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/bag-orders');
|
||||
const downloadPromise = page.waitForEvent('download', { timeout: 10000 });
|
||||
await page.locator('a[href*="export"]').first().click();
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toContain('.csv');
|
||||
});
|
||||
|
||||
test('재고 엑셀 다운로드', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/bag-inventory');
|
||||
const downloadPromise = page.waitForEvent('download', { timeout: 10000 });
|
||||
await page.locator('a[href*="export"]').first().click();
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toContain('.csv');
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// CT-03: 인쇄 버튼
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
test.describe('CT-03: 인쇄 버튼', () => {
|
||||
test('발주 목록에 인쇄 버튼 존재', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/bag-orders');
|
||||
await expect(page.locator('button:has-text("인쇄"), a:has-text("인쇄")')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// CT-06: 대시보드 실 데이터
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
test.describe('CT-06: 대시보드 실 데이터', () => {
|
||||
test('대시보드에 통계 표시', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin');
|
||||
const content = await page.textContent('main');
|
||||
expect(content).toBeTruthy();
|
||||
expect(content.length).toBeGreaterThan(50);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// P2-15: 지정판매소 다조건 조회
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
test.describe('P2-15: 지정판매소 다조건 조회', () => {
|
||||
test('이름 검색 필터', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/designated-shops?ds_name=CU');
|
||||
await expect(page).toHaveURL(/ds_name=CU/);
|
||||
await expect(page.locator('table.data-table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('상태 필터', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/designated-shops?ds_state=1');
|
||||
await expect(page.locator('table.data-table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('검색 폼에서 이름 입력 후 조회', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/designated-shops');
|
||||
const nameInput = page.locator('input[name="ds_name"]');
|
||||
if (await nameInput.count() > 0) {
|
||||
await nameInput.fill('GS');
|
||||
await page.click('button:has-text("조회")');
|
||||
await expect(page).toHaveURL(/ds_name=GS/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// P2-17: 지정판매소 지도
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
test.describe('P2-17: 지정판매소 지도', () => {
|
||||
test('지도 페이지 접근', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/designated-shops/map');
|
||||
await expect(page).toHaveURL(/\/map/);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// P2-18: 지정판매소 현황
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
test.describe('P2-18: 지정판매소 현황', () => {
|
||||
test('현황 페이지 접근', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/designated-shops/status');
|
||||
await expect(page).toHaveURL(/\/status/);
|
||||
await expect(page.locator('table.data-table').first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// P5-04: 년 판매 현황
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
test.describe('P5-04: 년 판매 현황', () => {
|
||||
test('년 판매 현황 접근', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/reports/yearly-sales');
|
||||
await expect(page).toHaveURL(/yearly-sales/);
|
||||
await expect(page.locator('table.data-table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('연도 변경 조회', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/reports/yearly-sales?year=2025');
|
||||
await expect(page).toHaveURL(/year=2025/);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// P5-05: 지정판매소별 판매현황
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
test.describe('P5-05: 판매소별 판매현황', () => {
|
||||
test('판매소별 현황 접근', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/reports/shop-sales');
|
||||
await expect(page).toHaveURL(/shop-sales/);
|
||||
await expect(page.locator('table.data-table')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// P5-06: 홈택스 엑셀
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
test.describe('P5-06: 홈택스 세금계산서 엑셀', () => {
|
||||
test('홈택스 엑셀 내보내기', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
// 다운로드를 트리거하는 URL이므로 evaluate로 fetch 테스트
|
||||
const status = await page.evaluate(async () => {
|
||||
const res = await fetch('/admin/reports/hometax-export');
|
||||
return res.status;
|
||||
});
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// P5-08: 반품/파기 현황
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
test.describe('P5-08: 반품/파기 현황', () => {
|
||||
test('반품/파기 목록 접근', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/reports/returns');
|
||||
await expect(page).toHaveURL(/returns/);
|
||||
await expect(page.locator('table.data-table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('기간 필터 조회', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/reports/returns?start_date=2026-01-01&end_date=2026-12-31');
|
||||
await expect(page.locator('table.data-table')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// P5-10: LOT 수불 조회
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
test.describe('P5-10: LOT 수불 조회', () => {
|
||||
test('LOT 수불 페이지 접근', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/reports/lot-flow');
|
||||
await expect(page).toHaveURL(/lot-flow/);
|
||||
});
|
||||
|
||||
test('LOT 번호 검색', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/reports/lot-flow?lot=LOT-2025');
|
||||
await expect(page).toHaveURL(/lot=LOT/);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// P5-11: 기타 입출고
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
test.describe('P5-11: 기타 입출고', () => {
|
||||
test('기타 입출고 페이지 접근', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/reports/misc-flow');
|
||||
await expect(page).toHaveURL(/misc-flow/);
|
||||
});
|
||||
|
||||
test('기타 입출고 등록 폼 표시', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/reports/misc-flow');
|
||||
await expect(page.locator('select[name="bmf_type"]')).toBeVisible();
|
||||
await expect(page.locator('input[name="bmf_qty"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// 사이트 메뉴 CRUD (DOM 조작)
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
test.describe('사이트 메뉴 CRUD 동작', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
});
|
||||
|
||||
test('불출 처리 폼 → 사이트 레이아웃', async ({ page }) => {
|
||||
await page.goto('/bag/issue/create');
|
||||
await expect(page.locator('a:has-text("발주 입고 관리")')).toBeVisible();
|
||||
expect(await page.locator('a:has-text("회원 관리")').count()).toBe(0);
|
||||
await expect(page.locator('select[name="bi2_bag_code"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('발주 등록 폼 → 사이트 레이아웃', async ({ page }) => {
|
||||
await page.goto('/bag/order/create');
|
||||
await expect(page.locator('a:has-text("불출 관리")')).toBeVisible();
|
||||
await expect(page.locator('input[name="bo_order_date"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('입고 처리 폼 → 사이트 레이아웃', async ({ page }) => {
|
||||
await page.goto('/bag/receiving/create');
|
||||
await expect(page.locator('a:has-text("재고 관리")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('판매 등록 폼 → 사이트 레이아웃', async ({ page }) => {
|
||||
await page.goto('/bag/sale/create');
|
||||
await expect(page.locator('a:has-text("판매 현황")')).toBeVisible();
|
||||
await expect(page.locator('select[name="bs_ds_idx"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('주문 접수 폼 → 사이트 레이아웃', async ({ page }) => {
|
||||
await page.goto('/bag/shop-order/create');
|
||||
await expect(page.locator('a:has-text("봉투 수불 관리")')).toBeVisible();
|
||||
await expect(page.locator('select[name="so_ds_idx"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('재고 조정 폼', async ({ page }) => {
|
||||
await page.goto('/bag/inventory/adjust');
|
||||
await expect(page.locator('select[name="bag_code"]')).toBeVisible();
|
||||
await expect(page.locator('select[name="adjust_type"]')).toBeVisible();
|
||||
await expect(page.locator('input[name="qty"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// 엑셀 내보내기 다운로드
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
test.describe('엑셀 내보내기 다운로드', () => {
|
||||
test('지정판매소 엑셀', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/designated-shops');
|
||||
const downloadPromise = page.waitForEvent('download', { timeout: 10000 });
|
||||
await page.locator('a[href*="export"]').first().click();
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toContain('.csv');
|
||||
});
|
||||
|
||||
test('판매 엑셀', async ({ page }) => {
|
||||
await loginAsLocal(page);
|
||||
await page.goto('/admin/bag-sales');
|
||||
const downloadPromise = page.waitForEvent('download', { timeout: 10000 });
|
||||
await page.locator('a[href*="export"]').first().click();
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toContain('.csv');
|
||||
});
|
||||
});
|
||||
9
env
9
env
@@ -55,15 +55,6 @@
|
||||
|
||||
# encryption.key =
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# AUTH (TOTP 2차 인증) — 관리자(mb_level 3·4·5)만 적용, 로컬은 false 권장
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
# auth.requireTotp = true
|
||||
# auth.totpIssuer = "쓰레기봉투 물류시스템"
|
||||
# auth.totpMaxAttempts = 5
|
||||
# auth.pending2faTtlSeconds = 600
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# SESSION
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user