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): ?> + + + + + + + + + + + +
번호코드코드명상태
ck_code) ?>ck_name) ?>ck_status ?? 'active') === 'active' ? '사용' : '미사용' ?>
등록된 코드 종류가 없습니다.
+
+ + +
+

봉투 단가

+ + + + + + + $row): ?> + + + + + + + + + + + + + + + + +
번호봉투코드봉투명발주단가도매가소비자가적용시작적용종료상태
bp_bag_code) ?>bp_bag_name ?? '') ?>bp_order_price ?? 0)) ?>bp_wholesale_price ?? 0)) ?>bp_consumer_price ?? 0)) ?>bp_start_date ?? '') ?>bp_end_date ?? '') ?: '현재' ?>bp_status ?? 'active') === 'active' ? '사용' : '만료' ?>
등록된 단가 정보가 없습니다.
+
+ + +
+

포장 단위

+ + + + + + + $row): ?> + + + + + + + + + + + + + + + + +
번호봉투코드봉투명박스당 팩 수팩당 낱장 수1박스 총 낱장적용시작적용종료상태
pu_bag_code) ?>pu_bag_name ?? '') ?>pu_packs_per_box ?? 0)) ?>pu_sheets_per_pack ?? 0)) ?>pu_packs_per_box ?? 0) * (int)($row->pu_sheets_per_pack ?? 0)) ?>pu_start_date ?? '') ?>pu_end_date ?? '') ?: '현재' ?>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++; ?> + + + + + + + + + + + + + + +
봉투코드봉투명현재재고입고출고
입고수량반품수량판매수량불출수량
수불 데이터가 없습니다.
+
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): ?> + + + + + + + + + + + + +
번호봉투코드봉투명현재재고(낱장)최종갱신
bi_bag_code ?? '') ?>bi_bag_name ?? '') ?>bi_qty_sheet ?? 0)) ?>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): ?> + + + + + + + + + + + + + + + + + +
번호연도분기구분불출일불출처봉투코드봉투명수량상태
bi2_year ?? '') ?>bi2_quarter ?? '') ?>bi2_type ?? '') ?>bi2_issue_date ?? '') ?>bi2_destination ?? '') ?>bi2_bag_code ?? '') ?>bi2_bag_name ?? '') ?>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 ?? '쓰레기봉투 물류시스템') ?> + + + + + + + +
+
+ +
+ +
+ + + + + 관리자 + + + 종료 + +
+
+ +
+ +
+getFlashdata('success')): ?> + + +getFlashdata('error')): ?> + + +
+ +
+ + + 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 @@ +
+ +
+ + + ~ + + + 초기화 +
+ + +
+

발주 현황

+ + + + + + + $row): ?> + bo_idx] ?? ['qty' => 0, 'amount' => 0, 'count' => 0]; ?> + + + + + + + + + + + + + + +
번호LOT번호발주일품목수총수량(낱장)총금액상태
bo_lot_number ?? '') ?>bo_order_date ?? '') ?> + bo_status ?? 'normal'; + echo match($st) { 'normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제', default => esc($st) }; + ?> +
등록된 발주가 없습니다.
+
+ + +
+

입고 현황

+ + + + + + + $row): ?> + + + + + + + + + + + + + + +
번호봉투코드봉투명박스수낱장수입고일구분
br_bag_code ?? '') ?>br_bag_name ?? '') ?>br_qty_box ?? 0)) ?>br_qty_sheet ?? 0)) ?>br_receive_date ?? '') ?>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): ?> + + + + + + + + + + + + + + +
번호판매소접수일배달일수량금액상태
so_shop_name ?? '') ?>so_order_date ?? '') ?>so_delivery_date ?? '') ?>so_qty ?? 0)) ?>so_amount ?? 0)) ?> + so_status ?? 'normal'; + echo match($st) { 'normal' => '정상', 'cancelled' => '취소', default => esc($st) }; + ?> +
등록된 주문이 없습니다.
+
+ + +
+

판매/반품

+ + + + + + + $row): ?> + + + + + + + + + + + + + + + + +
번호판매소판매일봉투코드봉투명수량단가금액구분
bs_shop_name ?? '') ?>bs_sale_date ?? '') ?>bs_bag_code ?? '') ?>bs_bag_name ?? '') ?>bs_qty ?? 0)) ?>bs_unit_price ?? 0)) ?>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): ?> + + + + + + + + + + + + + + + +
번호판매소판매일봉투코드봉투명수량단가금액
bs_shop_name ?? '') ?>bs_sale_date ?? '') ?>bs_bag_code ?? '') ?>bs_bag_name ?? '') ?>bs_qty ?? 0)) ?>bs_unit_price ?? 0)) ?>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',