Compare commits

..

2 Commits

Author SHA1 Message Date
javamon1174
1e8bf1eeeb P2-15~18, P5-04~11, CT-05~06 웹 미구현 기능 전체 구현
P2-15: 지정판매소 다조건 조회 (이름/구군/상태 필터)
P2-17: 지정판매소 지도 표시 (Kakao Maps)
P2-18: 지정판매소 현황 (연도별 신규/취소 통계)
P5-04: 년 판매 현황 (월별 피벗 테이블)
P5-05: 지정판매소별 판매현황 (판매소별 수량/금액)
P5-06: 홈택스 세금계산서 엑셀 내보내기
P5-08: 반품/파기 현황 (기간별 조회)
P5-10: LOT 수불 조회 (LOT 번호 검색)
P5-11: 기타 입출고 (등록 + 재고 연동)
CT-05: CRUD 로깅 (activity_log 테이블 + audit_helper)
CT-06: 대시보드 실 데이터 (발주/판매/재고/불출 통계)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:50:28 +09:00
javamon1174
704141a1f0 CT-01/02/03 공통 컴포넌트 구현 — 페이지네이션/엑셀/인쇄
CT-01: 페이지네이션
- 커스텀 Tailwind 페이저 뷰 (components/pager.php)
- 18개 admin 컨트롤러 findAll() → paginate(20) 전환
- Bag 컨트롤러 7개 리스트도 paginate 적용
- 19개 admin index 뷰에 페이저 링크 추가

CT-02: 엑셀 저장
- export_helper.php (UTF-8 BOM CSV)
- 발주/판매/지정판매소/재고 4개 엑셀 내보내기 라우트+메서드
- 해당 뷰에 "엑셀저장" 버튼 추가

CT-03: 인쇄
- print_header.php (지자체명/제목/결재란 컴포넌트)
- admin/bag 레이아웃에 @media print CSS 추가
- 23개 뷰에 인쇄 버튼 + print_header 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:40:49 +09:00
63 changed files with 1766 additions and 87 deletions

View File

@@ -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',
];

View File

@@ -114,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');
@@ -127,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)
@@ -136,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');
@@ -160,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');
@@ -193,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');

View File

@@ -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,
]),
]);
}

View File

@@ -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
);
}
}

View File

@@ -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);

View File

@@ -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', '발주가 삭제 처리되었습니다.');
}
}

View File

@@ -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,
]),
]);
}

View File

@@ -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')),
]);
}

View File

@@ -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);

View File

@@ -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,
]),
]);
}

View File

@@ -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,
]),
]);
}

View File

@@ -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]),
]);
}

View File

@@ -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]),
]);
}
}

View File

@@ -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

View File

@@ -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]),
]);
}

View File

@@ -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]),
]);
}

View File

@@ -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]),
]);
}

View File

@@ -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,
]),
]);
}

View File

@@ -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]),
]);
}

View File

@@ -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: 봉투 수불 현황
*/

View File

@@ -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')),
]);
}

View File

@@ -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,
]),
]);
}

View File

@@ -73,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 = [];
@@ -93,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);
@@ -116,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);
@@ -131,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);
@@ -155,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);
@@ -184,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);

View 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());
}
}
}

View 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";
}
}

View 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',
];
}

View File

@@ -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; ?>

View File

@@ -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; ?>

View File

@@ -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; ?>

View File

@@ -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; ?>

View File

@@ -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; ?>

View File

@@ -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; ?>

View File

@@ -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; ?>

View File

@@ -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; ?>

View File

@@ -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; ?>

View File

@@ -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; ?>

View File

@@ -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; ?>

View File

@@ -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; ?>

View File

@@ -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; ?>

View 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>

View 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>

View File

@@ -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; ?>

View File

@@ -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">

View File

@@ -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; ?>

View File

@@ -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; ?>

View File

@@ -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; ?>

View File

@@ -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; ?>

View File

@@ -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">

View 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; ?>

View 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>

View File

@@ -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">

View 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>

View File

@@ -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">

View 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>

View File

@@ -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">

View 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>

View File

@@ -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; ?>

View File

@@ -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; ?>

View File

@@ -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">

View 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="처음">&laquo;</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="이전">&lsaquo;</a>
<?php else: ?>
<span class="px-2 py-1 text-xs border border-gray-200 rounded text-gray-300">&laquo;</span>
<span class="px-2 py-1 text-xs border border-gray-200 rounded text-gray-300">&lsaquo;</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="다음">&rsaquo;</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="마지막">&raquo;</a>
<?php else: ?>
<span class="px-2 py-1 text-xs border border-gray-200 rounded text-gray-300">&rsaquo;</span>
<span class="px-2 py-1 text-xs border border-gray-200 rounded text-gray-300">&raquo;</span>
<?php endif; ?>
</nav>
<?php endif; ?>

View 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;">&nbsp;</td>
<td style="border:1px solid #333; padding:8px 12px; height:40px; min-width:60px;">&nbsp;</td>
<td style="border:1px solid #333; padding:8px 12px; height:40px; min-width:60px;">&nbsp;</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>

View 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 활동 로그';

View 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='기타 입출고';