diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index a2e8f6a..27ddbfc 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -14,6 +14,18 @@ $routes->get('dashboard/charts', 'Home::dashboardCharts');
$routes->get('bag/inventory-inquiry', 'Home::inventoryInquiry');
$routes->get('bag/waste-suibal-enterprise', 'Home::wasteSuibalEnterprise');
+// 사이트 메뉴 (/bag/*)
+$routes->get('bag/basic-info', 'Bag::basicInfo');
+$routes->get('bag/purchase-inbound', 'Bag::purchaseInbound');
+$routes->get('bag/issue', 'Bag::issue');
+$routes->get('bag/inventory', 'Bag::inventory');
+$routes->get('bag/sales', 'Bag::sales');
+$routes->get('bag/sales-stats', 'Bag::salesStats');
+$routes->get('bag/flow', 'Bag::flow');
+$routes->get('bag/analytics', 'Bag::analytics');
+$routes->get('bag/window', 'Bag::window');
+$routes->get('bag/help', 'Bag::help');
+
// Auth
$routes->get('login', 'Auth::showLoginForm');
$routes->post('login', 'Auth::login');
diff --git a/app/Controllers/Bag.php b/app/Controllers/Bag.php
new file mode 100644
index 0000000..88a26b0
--- /dev/null
+++ b/app/Controllers/Bag.php
@@ -0,0 +1,250 @@
+ $title,
+ 'content' => view($viewFile, $data),
+ ]);
+ }
+
+ // ──────────────────────────────────────────────
+ // 기본정보관리
+ // ──────────────────────────────────────────────
+ public function basicInfo(): string
+ {
+ $lgIdx = $this->lgIdx();
+ $data = [];
+
+ if ($lgIdx) {
+ $data['codeKinds'] = model(CodeKindModel::class)->orderBy('ck_code', 'ASC')->findAll();
+ $data['bagPrices'] = model(BagPriceModel::class)->where('bp_lg_idx', $lgIdx)->orderBy('bp_bag_code', 'ASC')->findAll();
+ $data['packagingUnits'] = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->orderBy('pu_bag_code', 'ASC')->findAll();
+ }
+
+ return $this->render('기본정보관리', 'bag/basic_info', $data);
+ }
+
+ // ──────────────────────────────────────────────
+ // 발주 입고 관리
+ // ──────────────────────────────────────────────
+ public function purchaseInbound(): string
+ {
+ $lgIdx = $this->lgIdx();
+ $data = ['orders' => [], 'receivings' => [], 'startDate' => null, 'endDate' => null];
+
+ if ($lgIdx) {
+ $startDate = $this->request->getGet('start_date');
+ $endDate = $this->request->getGet('end_date');
+ $data['startDate'] = $startDate;
+ $data['endDate'] = $endDate;
+
+ // 발주 목록
+ $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();
+
+ // 발주별 품목 합계
+ $itemSummary = [];
+ foreach ($data['orders'] as $order) {
+ $items = model(BagOrderItemModel::class)->where('boi_bo_idx', $order->bo_idx)->findAll();
+ $totalQty = 0;
+ $totalAmt = 0;
+ foreach ($items as $it) {
+ $totalQty += (int) $it->boi_qty_sheet;
+ $totalAmt += (float) $it->boi_amount;
+ }
+ $itemSummary[$order->bo_idx] = ['qty' => $totalQty, 'amount' => $totalAmt, 'count' => count($items)];
+ }
+ $data['itemSummary'] = $itemSummary;
+
+ // 입고 목록
+ $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();
+ }
+
+ return $this->render('발주 입고 관리', 'bag/purchase_inbound', $data);
+ }
+
+ // ──────────────────────────────────────────────
+ // 불출 관리
+ // ──────────────────────────────────────────────
+ public function issue(): string
+ {
+ $lgIdx = $this->lgIdx();
+ $data = ['list' => [], 'startDate' => null, 'endDate' => null];
+
+ if ($lgIdx) {
+ $startDate = $this->request->getGet('start_date');
+ $endDate = $this->request->getGet('end_date');
+ $data['startDate'] = $startDate;
+ $data['endDate'] = $endDate;
+
+ $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();
+ }
+
+ return $this->render('불출 관리', 'bag/issue', $data);
+ }
+
+ // ──────────────────────────────────────────────
+ // 재고 관리
+ // ──────────────────────────────────────────────
+ public function inventory(): string
+ {
+ $lgIdx = $this->lgIdx();
+ $data = ['list' => []];
+
+ if ($lgIdx) {
+ $data['list'] = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->findAll();
+ }
+
+ return $this->render('재고 관리', 'bag/inventory', $data);
+ }
+
+ // ──────────────────────────────────────────────
+ // 판매 관리
+ // ──────────────────────────────────────────────
+ public function sales(): string
+ {
+ $lgIdx = $this->lgIdx();
+ $data = ['salesList' => [], 'orderList' => [], 'startDate' => null, 'endDate' => null];
+
+ if ($lgIdx) {
+ $startDate = $this->request->getGet('start_date');
+ $endDate = $this->request->getGet('end_date');
+ $data['startDate'] = $startDate;
+ $data['endDate'] = $endDate;
+
+ // 판매/반품
+ $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();
+
+ // 주문 접수
+ $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();
+ }
+
+ return $this->render('판매 관리', 'bag/sales', $data);
+ }
+
+ // ──────────────────────────────────────────────
+ // 판매 현황
+ // ──────────────────────────────────────────────
+ public function salesStats(): string
+ {
+ $lgIdx = $this->lgIdx();
+ $data = ['result' => [], 'startDate' => null, 'endDate' => null];
+
+ if ($lgIdx) {
+ $startDate = $this->request->getGet('start_date');
+ $endDate = $this->request->getGet('end_date');
+ $data['startDate'] = $startDate;
+ $data['endDate'] = $endDate;
+
+ $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();
+ }
+
+ return $this->render('판매 현황', 'bag/sales_stats', $data);
+ }
+
+ // ──────────────────────────────────────────────
+ // 봉투 수불 관리
+ // ──────────────────────────────────────────────
+ public function flow(): string
+ {
+ $lgIdx = $this->lgIdx();
+ $data = ['receiving' => [], 'sales' => [], 'issues' => [], 'inventory' => [], 'startDate' => null, 'endDate' => null];
+
+ if ($lgIdx) {
+ $startDate = $this->request->getGet('start_date');
+ $endDate = $this->request->getGet('end_date');
+ $data['startDate'] = $startDate;
+ $data['endDate'] = $endDate;
+
+ $data['inventory'] = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->findAll();
+
+ $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['receiving'] = $recvBuilder->findAll();
+
+ $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['sales'] = $saleBuilder->findAll();
+
+ $issueBuilder = model(BagIssueModel::class)->where('bi2_lg_idx', $lgIdx);
+ if ($startDate) $issueBuilder->where('bi2_issue_date >=', $startDate);
+ if ($endDate) $issueBuilder->where('bi2_issue_date <=', $endDate);
+ $data['issues'] = $issueBuilder->findAll();
+ }
+
+ return $this->render('봉투 수불 관리', 'bag/flow', $data);
+ }
+
+ // ──────────────────────────────────────────────
+ // 통계 분석 관리
+ // ──────────────────────────────────────────────
+ public function analytics(): string
+ {
+ return $this->render('통계 분석 관리', 'bag/analytics', []);
+ }
+
+ // ──────────────────────────────────────────────
+ // 창 (프로그램 창 관리 - 추후)
+ // ──────────────────────────────────────────────
+ public function window(): string
+ {
+ return $this->render('창', 'bag/window', []);
+ }
+
+ // ──────────────────────────────────────────────
+ // 도움말
+ // ──────────────────────────────────────────────
+ public function help(): string
+ {
+ return $this->render('도움말', 'bag/help', []);
+ }
+}
diff --git a/app/Views/bag/analytics.php b/app/Views/bag/analytics.php
new file mode 100644
index 0000000..f16e228
--- /dev/null
+++ b/app/Views/bag/analytics.php
@@ -0,0 +1,9 @@
+
+
+
+
통계 분석 관리
+
Phase 6에서 구현 예정입니다.
+
+
diff --git a/app/Views/bag/basic_info.php b/app/Views/bag/basic_info.php
new file mode 100644
index 0000000..0cc68cd
--- /dev/null
+++ b/app/Views/bag/basic_info.php
@@ -0,0 +1,83 @@
+
+
+
+ 기본코드 종류
+
+
+ | 번호 | 코드 | 코드명 | 상태 |
+
+
+
+ $row): ?>
+
+ | = $i + 1 ?> |
+ = esc($row->ck_code) ?> |
+ = esc($row->ck_name) ?> |
+ = ($row->ck_status ?? 'active') === 'active' ? '사용' : '미사용' ?> |
+
+
+
+ | 등록된 코드 종류가 없습니다. |
+
+
+
+
+
+
+
+ 봉투 단가
+
+
+ | 번호 | 봉투코드 | 봉투명 | 발주단가 | 도매가 | 소비자가 | 적용시작 | 적용종료 | 상태 |
+
+
+
+ $row): ?>
+
+ | = $i + 1 ?> |
+ = esc($row->bp_bag_code) ?> |
+ = esc($row->bp_bag_name ?? '') ?> |
+ = number_format((float)($row->bp_order_price ?? 0)) ?> |
+ = number_format((float)($row->bp_wholesale_price ?? 0)) ?> |
+ = number_format((float)($row->bp_consumer_price ?? 0)) ?> |
+ = esc($row->bp_start_date ?? '') ?> |
+ = ($row->bp_end_date ?? '') ?: '현재' ?> |
+ = ($row->bp_status ?? 'active') === 'active' ? '사용' : '만료' ?> |
+
+
+
+ | 등록된 단가 정보가 없습니다. |
+
+
+
+
+
+
+
+ 포장 단위
+
+
+ | 번호 | 봉투코드 | 봉투명 | 박스당 팩 수 | 팩당 낱장 수 | 1박스 총 낱장 | 적용시작 | 적용종료 | 상태 |
+
+
+
+ $row): ?>
+
+ | = $i + 1 ?> |
+ = esc($row->pu_bag_code) ?> |
+ = esc($row->pu_bag_name ?? '') ?> |
+ = number_format((int)($row->pu_packs_per_box ?? 0)) ?> |
+ = number_format((int)($row->pu_sheets_per_pack ?? 0)) ?> |
+ = number_format((int)($row->pu_packs_per_box ?? 0) * (int)($row->pu_sheets_per_pack ?? 0)) ?> |
+ = esc($row->pu_start_date ?? '') ?> |
+ = ($row->pu_end_date ?? '') ?: '현재' ?> |
+ = ($row->pu_status ?? 'active') === 'active' ? '사용' : '만료' ?> |
+
+
+
+ | 등록된 포장 단위가 없습니다. |
+
+
+
+
+
diff --git a/app/Views/bag/flow.php b/app/Views/bag/flow.php
new file mode 100644
index 0000000..265a898
--- /dev/null
+++ b/app/Views/bag/flow.php
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+ | 봉투코드 |
+ 봉투명 |
+ 현재재고 |
+ 입고 |
+ 출고 |
+
+
+ | 입고수량 | 반품수량 |
+ 판매수량 | 불출수량 |
+
+
+
+ bi_bag_code ?? '';
+ if (! isset($summary[$code])) {
+ $summary[$code] = ['name' => $inv->bi_bag_name ?? '', 'stock' => 0, 'recv' => 0, 'return' => 0, 'sale' => 0, 'issue' => 0];
+ }
+ $summary[$code]['stock'] += (int)($inv->bi_qty_sheet ?? 0);
+ }
+ // 입고
+ foreach ($receiving as $r) {
+ $code = $r->br_bag_code ?? '';
+ if (! isset($summary[$code])) {
+ $summary[$code] = ['name' => $r->br_bag_name ?? '', 'stock' => 0, 'recv' => 0, 'return' => 0, 'sale' => 0, 'issue' => 0];
+ }
+ $summary[$code]['recv'] += (int)($r->br_qty_sheet ?? 0);
+ }
+ // 판매/반품
+ foreach ($sales as $s) {
+ $code = $s->bs_bag_code ?? '';
+ if (! isset($summary[$code])) {
+ $summary[$code] = ['name' => $s->bs_bag_name ?? '', 'stock' => 0, 'recv' => 0, 'return' => 0, 'sale' => 0, 'issue' => 0];
+ }
+ $type = $s->bs_type ?? 'sale';
+ if ($type === 'return') {
+ $summary[$code]['return'] += (int)($s->bs_qty ?? 0);
+ } else {
+ $summary[$code]['sale'] += (int)($s->bs_qty ?? 0);
+ }
+ }
+ // 불출
+ foreach ($issues as $iss) {
+ $code = $iss->bi2_bag_code ?? '';
+ if (! isset($summary[$code])) {
+ $summary[$code] = ['name' => $iss->bi2_bag_name ?? '', 'stock' => 0, 'recv' => 0, 'return' => 0, 'sale' => 0, 'issue' => 0];
+ }
+ if (($iss->bi2_status ?? 'normal') === 'normal') {
+ $summary[$code]['issue'] += (int)($iss->bi2_qty ?? 0);
+ }
+ }
+ ksort($summary);
+ ?>
+
+ $s): $idx++; ?>
+
+ | = esc($code) ?> |
+ = esc($s['name']) ?> |
+ = number_format($s['stock']) ?> |
+ = number_format($s['recv']) ?> |
+ = number_format($s['return']) ?> |
+ = number_format($s['sale']) ?> |
+ = number_format($s['issue']) ?> |
+
+
+
+ | 수불 데이터가 없습니다. |
+
+
+
+
diff --git a/app/Views/bag/help.php b/app/Views/bag/help.php
new file mode 100644
index 0000000..feb637b
--- /dev/null
+++ b/app/Views/bag/help.php
@@ -0,0 +1,31 @@
+
+
도움말
+
+
+
+ 시스템 개요
+ 쓰레기봉투 물류시스템은 지자체 종량제 쓰레기봉투의 발주, 입고, 재고, 판매, 불출 등 전체 물류 프로세스를 관리합니다.
+
+
+
+ 메뉴 안내
+
+ | 메뉴 | 설명 |
+
+ | 기본정보관리 | 코드 종류, 봉투 단가, 포장 단위 등 기본 데이터 조회 |
+ | 발주 입고 관리 | 봉투 발주 현황 및 입고 현황 조회 |
+ | 불출 관리 | 무료용 봉투 불출 내역 조회 |
+ | 재고 관리 | 봉투별 현재 재고(낱장) 조회 |
+ | 판매 관리 | 주문 접수, 판매/반품 내역 조회 |
+ | 판매 현황 | 기간별 판매 데이터 조회 |
+ | 봉투 수불 관리 | 봉투코드별 입출고 수불 요약 조회 |
+
+
+
+
+
+ 문의
+ 시스템 사용 중 문의사항은 소속 지자체 담당자에게 연락하시기 바랍니다.
+
+
+
diff --git a/app/Views/bag/inventory.php b/app/Views/bag/inventory.php
new file mode 100644
index 0000000..a7bc3e3
--- /dev/null
+++ b/app/Views/bag/inventory.php
@@ -0,0 +1,20 @@
+
+
+ | 번호 | 봉투코드 | 봉투명 | 현재재고(낱장) | 최종갱신 |
+
+
+
+ $row): ?>
+
+ | = $i + 1 ?> |
+ = esc($row->bi_bag_code ?? '') ?> |
+ = esc($row->bi_bag_name ?? '') ?> |
+ = number_format((int)($row->bi_qty_sheet ?? 0)) ?> |
+ = esc($row->bi_updated_at ?? $row->updated_at ?? '') ?> |
+
+
+
+ | 재고 데이터가 없습니다. |
+
+
+
diff --git a/app/Views/bag/issue.php b/app/Views/bag/issue.php
new file mode 100644
index 0000000..b7a3468
--- /dev/null
+++ b/app/Views/bag/issue.php
@@ -0,0 +1,41 @@
+
+
+
+
+
+ | 번호 | 연도 | 분기 | 구분 | 불출일 | 불출처 | 봉투코드 | 봉투명 | 수량 | 상태 |
+
+
+
+ $row): ?>
+
+ | = $i + 1 ?> |
+ = esc($row->bi2_year ?? '') ?> |
+ = esc($row->bi2_quarter ?? '') ?> |
+ = esc($row->bi2_type ?? '') ?> |
+ = esc($row->bi2_issue_date ?? '') ?> |
+ = esc($row->bi2_destination ?? '') ?> |
+ = esc($row->bi2_bag_code ?? '') ?> |
+ = esc($row->bi2_bag_name ?? '') ?> |
+ = number_format((int)($row->bi2_qty ?? 0)) ?> |
+
+ bi2_status ?? 'normal';
+ echo match($st) { 'normal' => '정상', 'cancelled' => '취소', default => esc($st) };
+ ?>
+ |
+
+
+
+ | 등록된 불출이 없습니다. |
+
+
+
+
diff --git a/app/Views/bag/layout/main.php b/app/Views/bag/layout/main.php
new file mode 100644
index 0000000..8f63056
--- /dev/null
+++ b/app/Views/bag/layout/main.php
@@ -0,0 +1,123 @@
+getUri();
+$currentPath = trim((string) $uriObj->getPath(), '/');
+if (str_starts_with($currentPath, 'index.php/')) {
+ $currentPath = substr($currentPath, strlen('index.php/'));
+}
+$mbLevel = (int) session()->get('mb_level');
+$isAdmin = ($mbLevel === \Config\Roles::LEVEL_SUPER_ADMIN || $mbLevel === \Config\Roles::LEVEL_LOCAL_ADMIN);
+$effectiveLgIdx = admin_effective_lg_idx();
+$effectiveLgName = null;
+if ($effectiveLgIdx) {
+ $lgRow = model(\App\Models\LocalGovernmentModel::class)->find($effectiveLgIdx);
+ $effectiveLgName = $lgRow ? $lgRow->lg_name : null;
+}
+?>
+
+
+
+
+
+= esc($title ?? '쓰레기봉투 물류시스템') ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
= esc($effectiveLgName) ?>
+
+
+
관리자
+
+
+ 종료
+
+
+
+
+
+ = esc($title ?? '') ?>
+
+getFlashdata('success')): ?>
+= esc(session()->getFlashdata('success')) ?>
+
+getFlashdata('error')): ?>
+= esc(session()->getFlashdata('error')) ?>
+
+
+= $content ?>
+
+
+
+
diff --git a/app/Views/bag/purchase_inbound.php b/app/Views/bag/purchase_inbound.php
new file mode 100644
index 0000000..a93a060
--- /dev/null
+++ b/app/Views/bag/purchase_inbound.php
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+ 발주 현황
+
+
+ | 번호 | LOT번호 | 발주일 | 품목수 | 총수량(낱장) | 총금액 | 상태 |
+
+
+
+ $row): ?>
+ bo_idx] ?? ['qty' => 0, 'amount' => 0, 'count' => 0]; ?>
+
+ | = $i + 1 ?> |
+ = esc($row->bo_lot_number ?? '') ?> |
+ = esc($row->bo_order_date ?? '') ?> |
+ = number_format($summary['count']) ?> |
+ = number_format($summary['qty']) ?> |
+ = number_format($summary['amount']) ?> |
+
+ bo_status ?? 'normal';
+ echo match($st) { 'normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제', default => esc($st) };
+ ?>
+ |
+
+
+
+ | 등록된 발주가 없습니다. |
+
+
+
+
+
+
+
+ 입고 현황
+
+
+ | 번호 | 봉투코드 | 봉투명 | 박스수 | 낱장수 | 입고일 | 구분 |
+
+
+
+ $row): ?>
+
+ | = $i + 1 ?> |
+ = esc($row->br_bag_code ?? '') ?> |
+ = esc($row->br_bag_name ?? '') ?> |
+ = number_format((int)($row->br_qty_box ?? 0)) ?> |
+ = number_format((int)($row->br_qty_sheet ?? 0)) ?> |
+ = esc($row->br_receive_date ?? '') ?> |
+ = esc($row->br_type ?? '정상입고') ?> |
+
+
+
+ | 등록된 입고가 없습니다. |
+
+
+
+
+
diff --git a/app/Views/bag/sales.php b/app/Views/bag/sales.php
new file mode 100644
index 0000000..94b238d
--- /dev/null
+++ b/app/Views/bag/sales.php
@@ -0,0 +1,76 @@
+
+
+
+
+
+ 주문 접수
+
+
+ | 번호 | 판매소 | 접수일 | 배달일 | 수량 | 금액 | 상태 |
+
+
+
+ $row): ?>
+
+ | = $i + 1 ?> |
+ = esc($row->so_shop_name ?? '') ?> |
+ = esc($row->so_order_date ?? '') ?> |
+ = esc($row->so_delivery_date ?? '') ?> |
+ = number_format((int)($row->so_qty ?? 0)) ?> |
+ = number_format((float)($row->so_amount ?? 0)) ?> |
+
+ so_status ?? 'normal';
+ echo match($st) { 'normal' => '정상', 'cancelled' => '취소', default => esc($st) };
+ ?>
+ |
+
+
+
+ | 등록된 주문이 없습니다. |
+
+
+
+
+
+
+
+ 판매/반품
+
+
+ | 번호 | 판매소 | 판매일 | 봉투코드 | 봉투명 | 수량 | 단가 | 금액 | 구분 |
+
+
+
+ $row): ?>
+
+ | = $i + 1 ?> |
+ = esc($row->bs_shop_name ?? '') ?> |
+ = esc($row->bs_sale_date ?? '') ?> |
+ = esc($row->bs_bag_code ?? '') ?> |
+ = esc($row->bs_bag_name ?? '') ?> |
+ = number_format((int)($row->bs_qty ?? 0)) ?> |
+ = number_format((float)($row->bs_unit_price ?? 0)) ?> |
+ = number_format((float)($row->bs_amount ?? 0)) ?> |
+
+ bs_type ?? 'sale';
+ echo match($t) { 'sale' => '판매', 'return' => '반품', 'cancel' => '취소', default => esc($t) };
+ ?>
+ |
+
+
+
+ | 등록된 판매/반품이 없습니다. |
+
+
+
+
+
diff --git a/app/Views/bag/sales_stats.php b/app/Views/bag/sales_stats.php
new file mode 100644
index 0000000..c9b0ee2
--- /dev/null
+++ b/app/Views/bag/sales_stats.php
@@ -0,0 +1,34 @@
+
+
+
+
+
+ | 번호 | 판매소 | 판매일 | 봉투코드 | 봉투명 | 수량 | 단가 | 금액 |
+
+
+
+ $row): ?>
+
+ | = $i + 1 ?> |
+ = esc($row->bs_shop_name ?? '') ?> |
+ = esc($row->bs_sale_date ?? '') ?> |
+ = esc($row->bs_bag_code ?? '') ?> |
+ = esc($row->bs_bag_name ?? '') ?> |
+ = number_format((int)($row->bs_qty ?? 0)) ?> |
+ = number_format((float)($row->bs_unit_price ?? 0)) ?> |
+ = number_format((float)($row->bs_amount ?? 0)) ?> |
+
+
+
+ | 판매 데이터가 없습니다. |
+
+
+
+
diff --git a/app/Views/bag/window.php b/app/Views/bag/window.php
new file mode 100644
index 0000000..ecab4e2
--- /dev/null
+++ b/app/Views/bag/window.php
@@ -0,0 +1,9 @@
+
+
+
+
창 관리
+
Phase 6에서 구현 예정입니다.
+
+
diff --git a/e2e/admin.spec.js b/e2e/admin.spec.js
index 2696ce0..8d128ca 100644
--- a/e2e/admin.spec.js
+++ b/e2e/admin.spec.js
@@ -63,7 +63,7 @@ test.describe('관리자 패널 — Super Admin', () => {
await page.click('button[type="submit"]');
// 선택 후 관리자 대시보드로 이동
- await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
+ await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
await page.goto('/admin');
await expect(page).not.toHaveURL(/\/select-local-government/);
});
@@ -74,7 +74,7 @@ test.describe('관리자 패널 — Super Admin', () => {
const radio = page.locator('input[name="lg_idx"]').first();
await radio.check();
await page.click('button[type="submit"]');
- await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
+ await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
await page.goto('/admin/local-governments');
await expect(page).toHaveURL(/\/admin\/local-governments/);
diff --git a/e2e/auth.spec.js b/e2e/auth.spec.js
index a30d178..349ad8b 100644
--- a/e2e/auth.spec.js
+++ b/e2e/auth.spec.js
@@ -49,7 +49,7 @@ test.describe('인증 시스템', () => {
test('로그아웃', async ({ page }) => {
await login(page, 'local');
await page.goto('/logout');
- await page.waitForURL(/\/login/, { timeout: 10000 });
+ await page.waitForURL(/\/login/, { timeout: 30000 });
await expect(page).toHaveURL(/\/login/);
// 로그아웃 후 관리자 접근 불가 확인
await page.goto('/admin');
diff --git a/e2e/bag-price.spec.js b/e2e/bag-price.spec.js
index 7df91b9..50f0992 100644
--- a/e2e/bag-price.spec.js
+++ b/e2e/bag-price.spec.js
@@ -6,7 +6,7 @@ async function loginAsAdmin(page) {
await login(page, 'admin');
await page.locator('input[name="lg_idx"]').first().check();
await page.click('button[type="submit"]');
- await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
+ await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
}
test.describe('P2-03/04: 봉투 단가 관리', () => {
diff --git a/e2e/bag-site.spec.js b/e2e/bag-site.spec.js
new file mode 100644
index 0000000..297cda6
--- /dev/null
+++ b/e2e/bag-site.spec.js
@@ -0,0 +1,80 @@
+// @ts-check
+const { test, expect } = require('@playwright/test');
+const { login } = require('./helpers/auth');
+
+test.describe('사이트 메뉴 (/bag/*) 페이지 접근', () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page, 'local');
+ });
+
+ test('기본정보관리', async ({ page }) => {
+ await page.goto('/bag/basic-info');
+ await expect(page).toHaveURL(/\/bag\/basic-info/);
+ await expect(page.locator('text=기본코드 종류')).toBeVisible();
+ });
+
+ test('발주 입고 관리', async ({ page }) => {
+ await page.goto('/bag/purchase-inbound');
+ await expect(page).toHaveURL(/\/bag\/purchase-inbound/);
+ await expect(page.locator('text=발주 현황')).toBeVisible();
+ });
+
+ test('불출 관리', async ({ page }) => {
+ await page.goto('/bag/issue');
+ await expect(page).toHaveURL(/\/bag\/issue/);
+ await expect(page.locator('th:has-text("불출일")')).toBeVisible();
+ });
+
+ test('재고 관리', async ({ page }) => {
+ await page.goto('/bag/inventory');
+ await expect(page).toHaveURL(/\/bag\/inventory/);
+ await expect(page.locator('th:has-text("현재재고")')).toBeVisible();
+ });
+
+ test('판매 관리', async ({ page }) => {
+ await page.goto('/bag/sales');
+ await expect(page).toHaveURL(/\/bag\/sales/);
+ await expect(page.locator('text=주문 접수')).toBeVisible();
+ });
+
+ test('판매 현황', async ({ page }) => {
+ await page.goto('/bag/sales-stats');
+ await expect(page).toHaveURL(/\/bag\/sales-stats/);
+ await expect(page.locator('th:has-text("봉투코드")')).toBeVisible();
+ });
+
+ test('봉투 수불 관리', async ({ page }) => {
+ await page.goto('/bag/flow');
+ await expect(page).toHaveURL(/\/bag\/flow/);
+ await expect(page.locator('th:has-text("현재재고")')).toBeVisible();
+ });
+
+ test('통계 분석 관리', async ({ page }) => {
+ await page.goto('/bag/analytics');
+ await expect(page).toHaveURL(/\/bag\/analytics/);
+ await expect(page.locator('main >> text=Phase 6에서 구현 예정')).toBeVisible();
+ });
+
+ test('창', async ({ page }) => {
+ await page.goto('/bag/window');
+ await expect(page).toHaveURL(/\/bag\/window/);
+ await expect(page.locator('text=창 관리')).toBeVisible();
+ });
+
+ test('도움말', async ({ page }) => {
+ await page.goto('/bag/help');
+ await expect(page).toHaveURL(/\/bag\/help/);
+ await expect(page.locator('text=시스템 개요')).toBeVisible();
+ });
+});
+
+test.describe('홈페이지 네비게이션 메뉴 링크', () => {
+ test('메뉴 클릭으로 각 페이지 이동', async ({ page }) => {
+ await login(page, 'local');
+ await page.goto('/');
+
+ // 발주 입고 관리 메뉴 클릭
+ await page.click('a:has-text("발주 입고 관리")');
+ await expect(page).toHaveURL(/\/bag\/purchase-inbound/);
+ });
+});
diff --git a/e2e/code-management.spec.js b/e2e/code-management.spec.js
index ced9a66..dc3c551 100644
--- a/e2e/code-management.spec.js
+++ b/e2e/code-management.spec.js
@@ -8,7 +8,7 @@ async function loginAsAdmin(page) {
const radio = page.locator('input[name="lg_idx"]').first();
await radio.check();
await page.click('button[type="submit"]');
- await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
+ await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
}
test.describe('P2-01: 기본코드 종류 관리', () => {
diff --git a/e2e/helpers/screenshots-phase2-5.js b/e2e/helpers/screenshots-phase2-5.js
index 9de12c0..23922c5 100644
--- a/e2e/helpers/screenshots-phase2-5.js
+++ b/e2e/helpers/screenshots-phase2-5.js
@@ -15,10 +15,10 @@ async function run() {
await page.fill('input[name="login_id"]', 'tester_admin');
await page.fill('input[name="password"]', 'test1234!');
await page.click('button[type="submit"]');
- await page.waitForURL(url => !url.pathname.includes('/login'), { timeout: 15000 });
+ await page.waitForURL(url => !url.pathname.includes('/login'), { timeout: 30000 });
await page.locator('input[name="lg_idx"]').first().check();
await page.click('button[type="submit"]');
- await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
+ await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
const pages = [
// Phase 2
diff --git a/e2e/helpers/screenshots.js b/e2e/helpers/screenshots.js
index 5588e99..4e6d732 100644
--- a/e2e/helpers/screenshots.js
+++ b/e2e/helpers/screenshots.js
@@ -20,7 +20,7 @@ async function login(page, account) {
await page.fill('input[name="login_id"]', account.id);
await page.fill('input[name="password"]', account.password);
await page.click('button[type="submit"]');
- await page.waitForURL(url => !url.pathname.includes('/login'), { timeout: 15000 });
+ await page.waitForURL(url => !url.pathname.includes('/login'), { timeout: 30000 });
}
async function screenshot(page, name, url) {
@@ -52,7 +52,7 @@ async function run() {
const radio = page.locator('input[name="lg_idx"]').first();
await radio.check();
await page.click('button[type="submit"]');
- await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
+ await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
console.log('\n=== 관리자 패널 (Super Admin) ===');
await screenshot(page, '05_admin_dashboard', `${BASE_URL}/admin`);
diff --git a/e2e/packaging-unit.spec.js b/e2e/packaging-unit.spec.js
index dda30d9..ca78ff8 100644
--- a/e2e/packaging-unit.spec.js
+++ b/e2e/packaging-unit.spec.js
@@ -6,7 +6,7 @@ async function loginAsAdmin(page) {
await login(page, 'admin');
await page.locator('input[name="lg_idx"]').first().check();
await page.click('button[type="submit"]');
- await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
+ await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
}
test.describe('P2-05/06: 포장 단위 관리', () => {
diff --git a/e2e/phase2-entities.spec.js b/e2e/phase2-entities.spec.js
index 67b4632..09ef136 100644
--- a/e2e/phase2-entities.spec.js
+++ b/e2e/phase2-entities.spec.js
@@ -6,7 +6,7 @@ async function loginAsAdmin(page) {
await login(page, 'admin');
await page.locator('input[name="lg_idx"]').first().check();
await page.click('button[type="submit"]');
- await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
+ await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
}
async function loginAsLocal(page) {
diff --git a/e2e/phase2-extra.spec.js b/e2e/phase2-extra.spec.js
index 2f12b97..ae997a5 100644
--- a/e2e/phase2-extra.spec.js
+++ b/e2e/phase2-extra.spec.js
@@ -6,7 +6,7 @@ async function loginAsAdmin(page) {
await login(page, 'admin');
await page.locator('input[name="lg_idx"]').first().check();
await page.click('button[type="submit"]');
- await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
+ await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
}
test.describe('P2-19: 지자체 수정/삭제', () => {
@@ -42,7 +42,7 @@ test.describe('P2-21: 로그인 실패 lock', () => {
await page.fill('input[name="login_id"]', 'tester_user');
await page.fill('input[name="password"]', 'wrong_password');
await page.click('button[type="submit"]');
- await page.waitForURL(/\/login/, { timeout: 15000 });
+ await page.waitForURL(/\/login/, { timeout: 30000 });
await page.waitForTimeout(500);
}
// 3회 이후 실패 카운트 표시 확인
diff --git a/e2e/phase3-order.spec.js b/e2e/phase3-order.spec.js
index f8b1a1a..a0f5a3c 100644
--- a/e2e/phase3-order.spec.js
+++ b/e2e/phase3-order.spec.js
@@ -6,7 +6,7 @@ async function loginAsAdmin(page) {
await login(page, 'admin');
await page.locator('input[name="lg_idx"]').first().check();
await page.click('button[type="submit"]');
- await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
+ await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
}
test.describe('P3: 발주 관리', () => {
diff --git a/e2e/phase4-sales.spec.js b/e2e/phase4-sales.spec.js
index 9da4cdd..daaa4c5 100644
--- a/e2e/phase4-sales.spec.js
+++ b/e2e/phase4-sales.spec.js
@@ -6,7 +6,7 @@ async function loginAsAdmin(page) {
await login(page, 'admin');
await page.locator('input[name="lg_idx"]').first().check();
await page.click('button[type="submit"]');
- await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
+ await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
}
test.describe('P4: 주문 접수 관리', () => {
@@ -49,12 +49,18 @@ test.describe('P4: 무료용 불출 관리', () => {
});
test.describe('P4: 지자체관리자 접근', () => {
- test('주문/판매/불출 접근 가능', async ({ page }) => {
+ test.beforeEach(async ({ page }) => {
await login(page, 'local');
+ });
+ test('주문 접수 접근 가능', async ({ page }) => {
await page.goto('/admin/shop-orders');
await expect(page).toHaveURL(/\/admin\/shop-orders/);
+ });
+ test('판매 접근 가능', async ({ page }) => {
await page.goto('/admin/bag-sales');
await expect(page).toHaveURL(/\/admin\/bag-sales/);
+ });
+ test('불출 접근 가능', async ({ page }) => {
await page.goto('/admin/bag-issues');
await expect(page).toHaveURL(/\/admin\/bag-issues/);
});
diff --git a/e2e/phase5-reports.spec.js b/e2e/phase5-reports.spec.js
index 80d848d..0024ae9 100644
--- a/e2e/phase5-reports.spec.js
+++ b/e2e/phase5-reports.spec.js
@@ -6,7 +6,7 @@ async function loginAsAdmin(page) {
await login(page, 'admin');
await page.locator('input[name="lg_idx"]').first().check();
await page.click('button[type="submit"]');
- await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
+ await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
}
test.describe('P5: 판매 대장', () => {
diff --git a/playwright.config.js b/playwright.config.js
index ca96c71..f57c4d8 100644
--- a/playwright.config.js
+++ b/playwright.config.js
@@ -8,7 +8,7 @@ module.exports = defineConfig({
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: [['html', { open: 'never' }], ['list']],
- timeout: 30000,
+ timeout: 60000,
use: {
baseURL: 'http://localhost:8045',