Compare commits
8 Commits
a0c75a4a31
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac88ebdedb | ||
|
|
6fdd040d4d | ||
|
|
1e8bf1eeeb | ||
|
|
704141a1f0 | ||
|
|
35561b414b | ||
|
|
39ee71cc80 | ||
|
|
f6a64e07b8 | ||
|
|
56661ed5dc |
10
README.md
10
README.md
@@ -6,7 +6,15 @@
|
||||
백엔드는 **[CodeIgniter 4](https://codeigniter.com/)** 기반입니다.
|
||||
|
||||
**저장소:** [wixon-associates/jongryangje](https://github.com/wixon-associates/jongryangje)
|
||||
| **[구현 화면 스크린샷](./docs/SCREENSHOTS.md)** | **[Notion 진행상황](https://www.notion.so/31b42b987c3780baba32ded04a1d41bb)** |
|
||||
| **[구현 화면 스크린샷](./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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ class Pager extends BaseConfig
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public array $templates = [
|
||||
'default_full' => 'CodeIgniter\Pager\Views\default_full',
|
||||
'default_simple' => 'CodeIgniter\Pager\Views\default_simple',
|
||||
'default_full' => 'App\Views\components\pager',
|
||||
'default_simple' => 'App\Views\components\pager',
|
||||
'default_head' => 'CodeIgniter\Pager\Views\default_head',
|
||||
];
|
||||
|
||||
|
||||
@@ -27,6 +27,21 @@ $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');
|
||||
@@ -99,6 +114,7 @@ $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');
|
||||
@@ -112,6 +128,7 @@ $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)
|
||||
@@ -121,6 +138,7 @@ $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');
|
||||
@@ -145,6 +163,13 @@ $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');
|
||||
@@ -178,6 +203,9 @@ $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,8 +31,7 @@ class Access extends BaseController
|
||||
{
|
||||
$start = $this->request->getGet('start');
|
||||
$end = $this->request->getGet('end');
|
||||
$builder = $this->memberLogModel->builder();
|
||||
$builder->select('member_log.*');
|
||||
$builder = $this->memberLogModel;
|
||||
$builder->orderBy('mll_regdate', 'DESC');
|
||||
if ($start !== null && $start !== '') {
|
||||
$builder->where('mll_regdate >=', $start . ' 00:00:00');
|
||||
@@ -40,10 +39,11 @@ class Access extends BaseController
|
||||
if ($end !== null && $end !== '') {
|
||||
$builder->where('mll_regdate <=', $end . ' 23:59:59');
|
||||
}
|
||||
$list = $builder->get()->getResult();
|
||||
$list = $builder->paginate(20);
|
||||
$pager = $this->memberLogModel->pager;
|
||||
return view('admin/layout', [
|
||||
'title' => '로그인 이력',
|
||||
'content' => view('admin/access/login_history', ['list' => $list, 'start' => $start, 'end' => $end]),
|
||||
'content' => view('admin/access/login_history', ['list' => $list, 'start' => $start, 'end' => $end, 'pager' => $pager]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -59,15 +59,14 @@ class Access extends BaseController
|
||||
$status = MemberApprovalRequestModel::STATUS_PENDING;
|
||||
}
|
||||
|
||||
$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();
|
||||
$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;
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '승인 대기',
|
||||
@@ -75,6 +74,7 @@ class Access extends BaseController
|
||||
'list' => $list,
|
||||
'status' => $status,
|
||||
'roles' => $this->roles,
|
||||
'pager' => $pager,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -13,11 +13,41 @@ class BagInventory extends BaseController
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
|
||||
$list = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->findAll();
|
||||
$invModel = model(BagInventoryModel::class);
|
||||
$list = $invModel->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->paginate(20);
|
||||
$pager = $invModel->pager;
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '재고 현황',
|
||||
'content' => view('admin/bag_inventory/index', ['list' => $list]),
|
||||
'content' => view('admin/bag_inventory/index', ['list' => $list, 'pager' => $pager]),
|
||||
]);
|
||||
}
|
||||
|
||||
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,11 +29,12 @@ 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')->findAll();
|
||||
$list = $builder->orderBy('bi2_issue_date', 'DESC')->orderBy('bi2_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->issueModel->pager;
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '무료용 불출 관리',
|
||||
'content' => view('admin/bag_issue/index', compact('list', 'startDate', 'endDate')),
|
||||
'content' => view('admin/bag_issue/index', compact('list', 'startDate', 'endDate', 'pager')),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -77,7 +78,7 @@ class BagIssue extends BaseController
|
||||
$db = \Config\Database::connect();
|
||||
$db->transStart();
|
||||
|
||||
$this->issueModel->insert([
|
||||
$issueData = [
|
||||
'bi2_lg_idx' => $lgIdx,
|
||||
'bi2_year' => (int) $this->request->getPost('bi2_year'),
|
||||
'bi2_quarter' => (int) $this->request->getPost('bi2_quarter'),
|
||||
@@ -90,7 +91,13 @@ 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);
|
||||
@@ -111,7 +118,11 @@ 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,7 +42,8 @@ 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')->findAll();
|
||||
$list = $builder->orderBy('bo_order_date', 'DESC')->orderBy('bo_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->orderModel->pager;
|
||||
|
||||
// 발주별 품목 합계
|
||||
$itemSummary = [];
|
||||
@@ -60,10 +61,56 @@ class BagOrder extends BaseController
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '발주 현황',
|
||||
'content' => view('admin/bag_order/index', compact('list', 'itemSummary', 'companyMap', 'agencyMap', 'startDate', 'endDate', 'status')),
|
||||
'content' => view('admin/bag_order/index', compact('list', 'itemSummary', 'companyMap', 'agencyMap', 'startDate', 'endDate', 'status', 'pager')),
|
||||
]);
|
||||
}
|
||||
|
||||
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');
|
||||
@@ -132,6 +179,10 @@ 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') ?? [];
|
||||
@@ -203,7 +254,10 @@ 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', '발주가 취소되었습니다.');
|
||||
}
|
||||
|
||||
@@ -215,7 +269,10 @@ 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,7 +42,8 @@ class BagPrice extends BaseController
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$list = $builder->orderBy('bp_bag_code', 'ASC')->orderBy('bp_start_date', 'DESC')->findAll();
|
||||
$list = $builder->orderBy('bp_bag_code', 'ASC')->orderBy('bp_start_date', 'DESC')->paginate(20);
|
||||
$pager = $this->priceModel->pager;
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '봉투 단가 관리',
|
||||
@@ -50,6 +51,7 @@ class BagPrice extends BaseController
|
||||
'list' => $list,
|
||||
'startDate' => $startDate,
|
||||
'endDate' => $endDate,
|
||||
'pager' => $pager,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -30,11 +30,12 @@ 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')->findAll();
|
||||
$list = $builder->orderBy('br_receive_date', 'DESC')->orderBy('br_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->recvModel->pager;
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '입고 현황',
|
||||
'content' => view('admin/bag_receiving/index', compact('list', 'startDate', 'endDate')),
|
||||
'content' => view('admin/bag_receiving/index', compact('list', 'startDate', 'endDate', 'pager')),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,14 +33,56 @@ 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')->findAll();
|
||||
$list = $builder->orderBy('bs_sale_date', 'DESC')->orderBy('bs_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->saleModel->pager;
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '판매/반품 관리',
|
||||
'content' => view('admin/bag_sale/index', compact('list', 'startDate', 'endDate', 'type')),
|
||||
'content' => view('admin/bag_sale/index', compact('list', 'startDate', 'endDate', 'type', 'pager')),
|
||||
]);
|
||||
}
|
||||
|
||||
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');
|
||||
@@ -89,7 +131,7 @@ class BagSale extends BaseController
|
||||
$db = \Config\Database::connect();
|
||||
$db->transStart();
|
||||
|
||||
$this->saleModel->insert([
|
||||
$saleData = [
|
||||
'bs_lg_idx' => $lgIdx,
|
||||
'bs_ds_idx' => $dsIdx,
|
||||
'bs_ds_name' => $shop ? $shop->ds_name : '',
|
||||
@@ -101,7 +143,13 @@ 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,13 +24,15 @@ class CodeDetail extends BaseController
|
||||
return redirect()->to(site_url('admin/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
$list = $this->detailModel->getByKind($ckIdx);
|
||||
$list = $this->detailModel->where('cd_ck_idx', $ckIdx)->orderBy('cd_sort', 'ASC')->paginate(20);
|
||||
$pager = $this->detailModel->pager;
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '세부코드 관리 — ' . $kind->ck_name . ' (' . $kind->ck_code . ')',
|
||||
'content' => view('admin/code_detail/index', [
|
||||
'kind' => $kind,
|
||||
'list' => $list,
|
||||
'pager' => $pager,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ class CodeKind extends BaseController
|
||||
|
||||
public function index()
|
||||
{
|
||||
$list = $this->kindModel->orderBy('ck_code', 'ASC')->findAll();
|
||||
$list = $this->kindModel->orderBy('ck_code', 'ASC')->paginate(20);
|
||||
$pager = $this->kindModel->pager;
|
||||
|
||||
// 세부코드 수 매핑
|
||||
$detailModel = model(CodeDetailModel::class);
|
||||
@@ -32,6 +33,7 @@ class CodeKind extends BaseController
|
||||
'content' => view('admin/code_kind/index', [
|
||||
'list' => $list,
|
||||
'countMap' => $countMap,
|
||||
'pager' => $pager,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -22,11 +22,12 @@ class Company extends BaseController
|
||||
return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
$list = $this->model->where('cp_lg_idx', $lgIdx)->orderBy('cp_idx', 'DESC')->findAll();
|
||||
$list = $this->model->where('cp_lg_idx', $lgIdx)->orderBy('cp_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->model->pager;
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '업체 관리',
|
||||
'content' => view('admin/company/index', ['list' => $list]),
|
||||
'content' => view('admin/company/index', ['list' => $list, 'pager' => $pager]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
@@ -8,9 +10,82 @@ 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'),
|
||||
'content' => view('admin/dashboard/index', ['stats' => $stats, 'lgIdx' => $lgIdx]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +43,25 @@ class DesignatedShop extends BaseController
|
||||
->with('error', '작업할 지자체가 선택되지 않았습니다. 지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
$list = $this->shopModel
|
||||
->where('ds_lg_idx', $lgIdx)
|
||||
->orderBy('ds_idx', 'DESC')
|
||||
->findAll();
|
||||
$builder = $this->shopModel->where('ds_lg_idx', $lgIdx);
|
||||
|
||||
// 다조건 검색 (P2-15)
|
||||
$dsName = $this->request->getGet('ds_name');
|
||||
$dsGugunCode = $this->request->getGet('ds_gugun_code');
|
||||
$dsState = $this->request->getGet('ds_state');
|
||||
|
||||
if ($dsName !== null && $dsName !== '') {
|
||||
$builder->like('ds_name', $dsName);
|
||||
}
|
||||
if ($dsGugunCode !== null && $dsGugunCode !== '') {
|
||||
$builder->where('ds_gugun_code', $dsGugunCode);
|
||||
}
|
||||
if ($dsState !== null && $dsState !== '') {
|
||||
$builder->where('ds_state', (int) $dsState);
|
||||
}
|
||||
|
||||
$list = $builder->orderBy('ds_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->shopModel->pager;
|
||||
|
||||
// 지자체 이름 매핑용
|
||||
$lgMap = [];
|
||||
@@ -54,15 +69,58 @@ 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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 지정판매소 등록 폼 (효과 지자체 기준)
|
||||
*/
|
||||
@@ -281,6 +339,78 @@ 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,11 +30,12 @@ class FreeRecipient extends BaseController
|
||||
return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
$list = $this->model->where('fr_lg_idx', $lgIdx)->orderBy('fr_idx', 'DESC')->findAll();
|
||||
$list = $this->model->where('fr_lg_idx', $lgIdx)->orderBy('fr_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->model->pager;
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '무료용 대상자 관리',
|
||||
'content' => view('admin/free_recipient/index', ['list' => $list]),
|
||||
'content' => view('admin/free_recipient/index', ['list' => $list, 'pager' => $pager]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,11 +32,12 @@ class LocalGovernment extends BaseController
|
||||
->with('error', '지자체 관리는 상위 관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
$list = $this->lgModel->orderBy('lg_idx', 'DESC')->findAll();
|
||||
$list = $this->lgModel->orderBy('lg_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->lgModel->pager;
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '지자체 관리',
|
||||
'content' => view('admin/local_government/index', ['list' => $list]),
|
||||
'content' => view('admin/local_government/index', ['list' => $list, 'pager' => $pager]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,11 +30,12 @@ class Manager extends BaseController
|
||||
return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
$list = $this->model->where('mg_lg_idx', $lgIdx)->orderBy('mg_idx', 'DESC')->findAll();
|
||||
$list = $this->model->where('mg_lg_idx', $lgIdx)->orderBy('mg_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->model->pager;
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '담당자 관리',
|
||||
'content' => view('admin/manager/index', ['list' => $list]),
|
||||
'content' => view('admin/manager/index', ['list' => $list, 'pager' => $pager]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,12 +38,13 @@ 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')->findAll();
|
||||
$list = $builder->orderBy('pu_bag_code', 'ASC')->orderBy('pu_start_date', 'DESC')->paginate(20);
|
||||
$pager = $this->unitModel->pager;
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '포장 단위 관리',
|
||||
'content' => view('admin/packaging_unit/index', [
|
||||
'list' => $list, 'startDate' => $startDate, 'endDate' => $endDate,
|
||||
'list' => $list, 'startDate' => $startDate, 'endDate' => $endDate, 'pager' => $pager,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -22,11 +22,12 @@ class SalesAgency extends BaseController
|
||||
return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
$list = $this->model->where('sa_lg_idx', $lgIdx)->orderBy('sa_idx', 'DESC')->findAll();
|
||||
$list = $this->model->where('sa_lg_idx', $lgIdx)->orderBy('sa_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->model->pager;
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '판매 대행소 관리',
|
||||
'content' => view('admin/sales_agency/index', ['list' => $list]),
|
||||
'content' => view('admin/sales_agency/index', ['list' => $list, 'pager' => $pager]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,264 @@ 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: 봉투 수불 현황
|
||||
*/
|
||||
|
||||
@@ -34,11 +34,12 @@ 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')->findAll();
|
||||
$list = $builder->orderBy('so_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->orderModel->pager;
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '주문 접수 관리',
|
||||
'content' => view('admin/shop_order/index', compact('list', 'startDate', 'endDate')),
|
||||
'content' => view('admin/shop_order/index', compact('list', 'startDate', 'endDate', 'pager')),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ class User extends BaseController
|
||||
*/
|
||||
public function index(): string
|
||||
{
|
||||
$list = $this->memberModel->orderBy('mb_idx', 'DESC')->findAll();
|
||||
$list = $this->memberModel->orderBy('mb_idx', 'DESC')->paginate(20);
|
||||
$pager = $this->memberModel->pager;
|
||||
$approvalMap = [];
|
||||
try {
|
||||
$memberIds = array_map(static fn ($row) => (int) $row->mb_idx, $list);
|
||||
@@ -56,6 +57,7 @@ class User extends BaseController
|
||||
'list' => $list,
|
||||
'roles' => $this->roles,
|
||||
'approvalMap' => $approvalMap,
|
||||
'pager' => $pager,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Models\CompanyModel;
|
||||
use App\Models\PackagingUnitModel;
|
||||
use App\Models\SalesAgencyModel;
|
||||
use App\Models\ShopOrderModel;
|
||||
use App\Models\DesignatedShopModel;
|
||||
|
||||
class Bag extends BaseController
|
||||
{
|
||||
@@ -72,7 +73,8 @@ 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')->findAll();
|
||||
$data['orders'] = $orderBuilder->orderBy('bo_order_date', 'DESC')->paginate(20, 'orders');
|
||||
$data['orderPager'] = model(BagOrderModel::class)->pager;
|
||||
|
||||
// 발주별 품목 합계
|
||||
$itemSummary = [];
|
||||
@@ -92,7 +94,8 @@ 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')->findAll();
|
||||
$data['receivings'] = $recvBuilder->orderBy('br_receive_date', 'DESC')->paginate(20, 'receivings');
|
||||
$data['recvPager'] = model(BagReceivingModel::class)->pager;
|
||||
}
|
||||
|
||||
return $this->render('발주 입고 관리', 'bag/purchase_inbound', $data);
|
||||
@@ -115,7 +118,8 @@ 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')->findAll();
|
||||
$data['list'] = $builder->orderBy('bi2_issue_date', 'DESC')->paginate(20);
|
||||
$data['pager'] = model(BagIssueModel::class)->pager;
|
||||
}
|
||||
|
||||
return $this->render('불출 관리', 'bag/issue', $data);
|
||||
@@ -130,7 +134,9 @@ class Bag extends BaseController
|
||||
$data = ['list' => []];
|
||||
|
||||
if ($lgIdx) {
|
||||
$data['list'] = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->findAll();
|
||||
$invModel = model(BagInventoryModel::class);
|
||||
$data['list'] = $invModel->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->paginate(20);
|
||||
$data['pager'] = $invModel->pager;
|
||||
}
|
||||
|
||||
return $this->render('재고 관리', 'bag/inventory', $data);
|
||||
@@ -154,13 +160,15 @@ 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')->findAll();
|
||||
$data['salesList'] = $saleBuilder->orderBy('bs_sale_date', 'DESC')->paginate(20, 'sales');
|
||||
$data['salesPager'] = model(BagSaleModel::class)->pager;
|
||||
|
||||
// 주문 접수
|
||||
$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')->findAll();
|
||||
$data['orderList'] = $orderBuilder->orderBy('so_idx', 'DESC')->paginate(20, 'shoporders');
|
||||
$data['orderPager'] = model(ShopOrderModel::class)->pager;
|
||||
}
|
||||
|
||||
return $this->render('판매 관리', 'bag/sales', $data);
|
||||
@@ -183,7 +191,8 @@ 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')->findAll();
|
||||
$data['result'] = $builder->orderBy('bs_sale_date', 'DESC')->paginate(20);
|
||||
$data['pager'] = model(BagSaleModel::class)->pager;
|
||||
}
|
||||
|
||||
return $this->render('판매 현황', 'bag/sales_stats', $data);
|
||||
@@ -247,4 +256,173 @@ 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', '주문 접수되었습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
43
app/Helpers/audit_helper.php
Normal file
43
app/Helpers/audit_helper.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
||||
}
|
||||
69
app/Helpers/export_helper.php
Normal file
69
app/Helpers/export_helper.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?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";
|
||||
}
|
||||
}
|
||||
25
app/Models/ActivityLogModel.php
Normal file
25
app/Models/ActivityLogModel.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
<?= 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">
|
||||
@@ -64,3 +68,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<?= 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">
|
||||
@@ -9,6 +10,7 @@
|
||||
<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">
|
||||
@@ -35,3 +37,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<?= 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">
|
||||
@@ -28,3 +35,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?= 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">
|
||||
@@ -58,3 +62,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<?= 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">
|
||||
@@ -73,3 +78,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?= 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">
|
||||
@@ -58,3 +62,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?= 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">
|
||||
@@ -47,3 +51,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<?= 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">
|
||||
@@ -61,3 +66,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<?= 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">
|
||||
@@ -5,8 +6,11 @@
|
||||
<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">
|
||||
@@ -42,3 +46,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?= 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">
|
||||
@@ -41,3 +45,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?= 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">
|
||||
@@ -45,3 +49,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,3 +1,118 @@
|
||||
<div class="border border-gray-300 p-4">
|
||||
<p class="text-sm text-gray-600">관리자 메인 화면입니다. 상단 메뉴에서 기능을 선택하세요.</p>
|
||||
<?php $s = $stats ?? []; ?>
|
||||
|
||||
<?php if (!($lgIdx ?? null)): ?>
|
||||
<div class="border border-orange-300 bg-orange-50 p-4 text-sm text-orange-700">
|
||||
작업할 지자체가 선택되지 않았습니다. 상단에서 지자체를 선택해 주세요.
|
||||
</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,8 +1,36 @@
|
||||
<?= 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">
|
||||
@@ -44,4 +72,5 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
|
||||
56
app/Views/admin/designated_shop/map.php
Normal file
56
app/Views/admin/designated_shop/map.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?= 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>
|
||||
80
app/Views/admin/designated_shop/status.php
Normal file
80
app/Views/admin/designated_shop/status.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?= 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,8 +1,12 @@
|
||||
<?= 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">
|
||||
@@ -47,3 +51,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -70,6 +70,14 @@ 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,8 +1,12 @@
|
||||
<?= 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">
|
||||
@@ -40,4 +44,5 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?= 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">
|
||||
@@ -45,3 +49,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?= 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">
|
||||
@@ -58,3 +62,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?= 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">
|
||||
@@ -43,3 +47,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<?= 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">
|
||||
|
||||
99
app/Views/admin/sales_report/lot_flow.php
Normal file
99
app/Views/admin/sales_report/lot_flow.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?= 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; ?>
|
||||
84
app/Views/admin/sales_report/misc_flow.php
Normal file
84
app/Views/admin/sales_report/misc_flow.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?= 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,5 +1,9 @@
|
||||
<?= 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">
|
||||
|
||||
59
app/Views/admin/sales_report/returns.php
Normal file
59
app/Views/admin/sales_report/returns.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?= 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,5 +1,9 @@
|
||||
<?= 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">
|
||||
|
||||
64
app/Views/admin/sales_report/shop_sales.php
Normal file
64
app/Views/admin/sales_report/shop_sales.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?= 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,5 +1,9 @@
|
||||
<?= 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">
|
||||
|
||||
62
app/Views/admin/sales_report/yearly_sales.php
Normal file
62
app/Views/admin/sales_report/yearly_sales.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?= 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,8 +1,12 @@
|
||||
<?= 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">
|
||||
@@ -73,3 +77,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?= 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">
|
||||
@@ -53,3 +57,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||
|
||||
70
app/Views/bag/create_bag_issue.php
Normal file
70
app/Views/bag/create_bag_issue.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<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>
|
||||
83
app/Views/bag/create_bag_order.php
Normal file
83
app/Views/bag/create_bag_order.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<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>
|
||||
53
app/Views/bag/create_bag_receiving.php
Normal file
53
app/Views/bag/create_bag_receiving.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<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>
|
||||
56
app/Views/bag/create_bag_sale.php
Normal file
56
app/Views/bag/create_bag_sale.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<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>
|
||||
74
app/Views/bag/create_shop_order.php
Normal file
74
app/Views/bag/create_shop_order.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<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>
|
||||
@@ -7,6 +7,11 @@
|
||||
<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,3 +1,9 @@
|
||||
<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>
|
||||
@@ -9,8 +15,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_sheet ?? 0)) ?></td>
|
||||
<td class="text-center"><?= esc($row->bi_updated_at ?? $row->updated_at ?? '') ?></td>
|
||||
<td class="text-right"><?= number_format((int)($row->bi_qty ?? 0)) ?></td>
|
||||
<td class="text-center"><?= esc($row->bi_updated_at ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
@@ -18,3 +24,4 @@
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
43
app/Views/bag/inventory_adjust.php
Normal file
43
app/Views/bag/inventory_adjust.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<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('admin/bag-issues/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">불출 처리</a>
|
||||
<a href="<?= base_url('bag/issue/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('admin/bag-issues/cancel/' . $row->bi2_idx) ?>" class="inline" onsubmit="return confirm('취소하시겠습니까?')">
|
||||
<form method="post" action="<?= base_url('bag/issue/cancel/' . $row->bi2_idx) ?>" class="inline" onsubmit="return confirm('취소하시겠습니까?')">
|
||||
<?= csrf_field() ?>
|
||||
<button class="text-orange-600 hover:underline text-xs">취소</button>
|
||||
</form>
|
||||
|
||||
@@ -52,6 +52,14 @@ 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">
|
||||
|
||||
@@ -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('admin/bag-orders/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">발주 등록</a>
|
||||
<a href="<?= base_url('bag/order/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('admin/bag-orders/detail/' . $row->bo_idx) ?>" class="text-blue-600 hover:underline text-xs">상세</a>
|
||||
<a href="<?= base_url('bag/purchase-inbound?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('admin/bag-orders/cancel/' . $row->bo_idx) ?>" class="inline" onsubmit="return confirm('취소하시겠습니까?')">
|
||||
<form method="post" action="<?= base_url('bag/order/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('admin/bag-receivings/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">입고 처리</a>
|
||||
<a href="<?= base_url('bag/receiving/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('admin/shop-orders/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">주문 등록</a>
|
||||
<a href="<?= base_url('bag/shop-order/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('admin/bag-sales/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-btn-search text-white px-3 py-1.5 rounded-sm text-sm">판매 등록</a>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead><tr>
|
||||
|
||||
38
app/Views/components/pager.php
Normal file
38
app/Views/components/pager.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?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; ?>
|
||||
55
app/Views/components/print_header.php
Normal file
55
app/Views/components/print_header.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?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>
|
||||
@@ -1,186 +0,0 @@
|
||||
# 서버 배포 가이드
|
||||
|
||||
## 서버 정보
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|------|
|
||||
| 서버 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
Normal file
261
docs/server.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# 서버 인프라 / 배포 가이드
|
||||
|
||||
## 접속 정보 요약
|
||||
|
||||
| 서비스 | 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
|
||||
```
|
||||
297
e2e/new-features.spec.js
Normal file
297
e2e/new-features.spec.js
Normal file
@@ -0,0 +1,297 @@
|
||||
// @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');
|
||||
});
|
||||
});
|
||||
34
jobs.md
34
jobs.md
@@ -47,10 +47,10 @@
|
||||
| P2-12 | 업체 조회 / 인쇄 | 낮음 | P2-11 | **완료** |
|
||||
| P2-13 | 무료용 대상자 관리 (CRUD) | 중간 | — | **완료** |
|
||||
| P2-14 | 무료용 대상자 조회 / 인쇄 | 낮음 | P2-13 | **완료** |
|
||||
| P2-15 | 지정판매소 다조건 조회 + 엑셀 + 인쇄 | 중간 | — | 대기 |
|
||||
| P2-16 | 지정판매소 바코드 출력 | 낮음 | P2-15 | 대기 |
|
||||
| P2-17 | 지정판매소 지도 표시 | 낮음 | — | 대기 |
|
||||
| P2-18 | 지정판매소 현황 (신규/취소) | 낮음 | — | 대기 |
|
||||
| P2-15 | 지정판매소 다조건 조회 + 엑셀 + 인쇄 | 중간 | — | **완료** |
|
||||
| P2-16 | 지정판매소 바코드 출력 | 낮음 | P2-15 | Phase 6+ |
|
||||
| P2-17 | 지정판매소 지도 표시 | 낮음 | — | **완료** |
|
||||
| P2-18 | 지정판매소 현황 (신규/취소) | 낮음 | — | **완료** |
|
||||
| P2-19 | 지자체 수정/삭제 기능 | 중간 | — | **완료** |
|
||||
| P2-20 | PASSWORD 변경 기능 | 중간 | — | **완료** |
|
||||
| P2-21 | 로그인 5회 실패 lock | 중간 | — | **완료** |
|
||||
@@ -94,14 +94,14 @@
|
||||
| P5-01 | 판매 대장 (일자별/기간별) | 높음 | P4-04 | **완료** |
|
||||
| P5-02 | 일계표 (일계 + 월간 누계) | 높음 | P4-04 | **완료** |
|
||||
| P5-03 | 기간별 판매현황 | 중간 | P4-04 | **완료** |
|
||||
| P5-04 | 년 판매 현황 (월별/분기별) | 중간 | P4-04 | Phase 6+ |
|
||||
| P5-05 | 지정판매소별 판매현황 (수량/금액) | 중간 | P4-04 | Phase 6+ |
|
||||
| P5-06 | 홈택스 세금계산서 엑셀 생성 | 높음 | P4-04 | Phase 6+ |
|
||||
| P5-04 | 년 판매 현황 (월별/분기별) | 중간 | P4-04 | **완료** |
|
||||
| P5-05 | 지정판매소별 판매현황 (수량/금액) | 중간 | P4-04 | **완료** |
|
||||
| P5-06 | 홈택스 세금계산서 엑셀 생성 | 높음 | P4-04 | **완료** |
|
||||
| P5-07 | 봉투 수불 현황 (재고/입고/판매/불출) | 높음 | P3-08, P4-04 | **완료** |
|
||||
| P5-08 | 반품/파기 현황 | 중간 | P4-06 | Phase 6+ |
|
||||
| P5-08 | 반품/파기 현황 | 중간 | P4-06 | **완료** |
|
||||
| P5-09 | 봉투 수급 계획 | 낮음 | P5-07 | Phase 6+ |
|
||||
| P5-10 | LOT 수불 조회 | 중간 | P3-02 | Phase 6+ |
|
||||
| P5-11 | 기타 입출고 | 낮음 | P3-08 | Phase 6+ |
|
||||
| P5-10 | LOT 수불 조회 | 중간 | P3-02 | **완료** |
|
||||
| P5-11 | 기타 입출고 | 낮음 | P3-08 | **완료** |
|
||||
| P5-12 | 봉투 스캔 현황 (횟수/위치) | 낮음 | — | Phase 6+ |
|
||||
|
||||
### Phase 6 — 모바일앱
|
||||
@@ -119,13 +119,13 @@
|
||||
|
||||
| ID | 작업 | 우선순위 | 상태 |
|
||||
|----|------|---------|------|
|
||||
| CT-01 | 페이지네이션 공통 구현 | 높음 | 대기 |
|
||||
| CT-02 | 엑셀 저장 공통 컴포넌트 | 높음 | 대기 |
|
||||
| CT-03 | 인쇄 공통 컴포넌트 (결재란 포함) | 높음 | 대기 |
|
||||
| CT-04 | 바코드 스캐너 연동 (Electron + serialport) | 중간 | 대기 |
|
||||
| CT-05 | CRUD 로깅 (전체 데이터 변경 이력) | 중간 | 대기 |
|
||||
| CT-06 | 대시보드 실 데이터 연동 | 낮음 | 대기 |
|
||||
| CT-07 | 2차 인증 적용 | 중간 | 대기 |
|
||||
| CT-01 | 페이지네이션 공통 구현 | 높음 | **완료** |
|
||||
| CT-02 | 엑셀 저장 공통 컴포넌트 | 높음 | **완료** |
|
||||
| CT-03 | 인쇄 공통 컴포넌트 (결재란 포함) | 높음 | **완료** |
|
||||
| CT-04 | 바코드 스캐너 연동 (Electron + serialport) | 중간 | Phase 6+ |
|
||||
| CT-05 | CRUD 로깅 (전체 데이터 변경 이력) | 중간 | **완료** |
|
||||
| CT-06 | 대시보드 실 데이터 연동 | 낮음 | **완료** |
|
||||
| CT-07 | 2차 인증 적용 | 중간 | **완료** |
|
||||
|
||||
---
|
||||
|
||||
|
||||
22
playwright.production.config.js
Normal file
22
playwright.production.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// @ts-check
|
||||
const { defineConfig } = require('@playwright/test');
|
||||
|
||||
module.exports = defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: false,
|
||||
workers: 1,
|
||||
timeout: 60000,
|
||||
reporter: 'list',
|
||||
use: {
|
||||
baseURL: 'https://trash.wxn.co.kr',
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'only-on-failure',
|
||||
locale: 'ko-KR',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { browserName: 'chromium' },
|
||||
},
|
||||
],
|
||||
});
|
||||
16
writable/database/activity_log_tables.sql
Normal file
16
writable/database/activity_log_tables.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- CT-05: CRUD 활동 로그 테이블
|
||||
CREATE TABLE IF NOT EXISTS `activity_log` (
|
||||
`al_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`al_mb_idx` INT UNSIGNED DEFAULT NULL COMMENT '회원 PK',
|
||||
`al_action` VARCHAR(20) NOT NULL COMMENT 'create/update/delete',
|
||||
`al_table` VARCHAR(100) NOT NULL COMMENT '대상 테이블명',
|
||||
`al_record_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '대상 레코드 PK',
|
||||
`al_data_before` JSON DEFAULT NULL COMMENT '변경 전 데이터',
|
||||
`al_data_after` JSON DEFAULT NULL COMMENT '변경 후 데이터',
|
||||
`al_ip` VARCHAR(45) NOT NULL DEFAULT '' COMMENT 'IP 주소',
|
||||
`al_regdate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '기록일시',
|
||||
PRIMARY KEY (`al_idx`),
|
||||
KEY `idx_al_table_record` (`al_table`, `al_record_id`),
|
||||
KEY `idx_al_mb` (`al_mb_idx`),
|
||||
KEY `idx_al_regdate` (`al_regdate`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='CRUD 활동 로그';
|
||||
14
writable/database/bag_misc_flow_tables.sql
Normal file
14
writable/database/bag_misc_flow_tables.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- P5-11: 기타 입출고 테이블
|
||||
CREATE TABLE IF NOT EXISTS `bag_misc_flow` (
|
||||
`bmf_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`bmf_lg_idx` INT UNSIGNED NOT NULL COMMENT '지자체 PK',
|
||||
`bmf_type` ENUM('in','out') NOT NULL COMMENT '입고/출고',
|
||||
`bmf_bag_code` VARCHAR(50) NOT NULL COMMENT '봉투 코드',
|
||||
`bmf_bag_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '봉투명',
|
||||
`bmf_qty` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '수량',
|
||||
`bmf_date` DATE NOT NULL COMMENT '입출고 일자',
|
||||
`bmf_reason` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '사유',
|
||||
`bmf_regdate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '등록일',
|
||||
PRIMARY KEY (`bmf_idx`),
|
||||
KEY `idx_bmf_lg_date` (`bmf_lg_idx`, `bmf_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='기타 입출고';
|
||||
Reference in New Issue
Block a user