사이트 메뉴 /bag/* 10개 페이지 구현 + E2E 테스트 timeout 보강

- Bag 컨트롤러 신규 (기본정보/발주입고/불출/재고/판매/판매현황/수불/통계/창/도움말)
- 사이트 공통 레이아웃 bag/layout/main.php 추출
- /bag/* 라우트 10개 등록 (Routes.php)
- bag-site.spec.js E2E 테스트 11개 추가
- Playwright timeout 30s→60s, waitForURL 15s→30s
- P4 지자체관리자 접근 테스트 3개로 분리

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
javamon1174
2026-03-26 14:30:45 +09:00
parent 466f6fe085
commit a0103eb95d
27 changed files with 951 additions and 18 deletions

View File

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

250
app/Controllers/Bag.php Normal file
View File

@@ -0,0 +1,250 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Models\BagInventoryModel;
use App\Models\BagIssueModel;
use App\Models\BagOrderModel;
use App\Models\BagOrderItemModel;
use App\Models\BagPriceModel;
use App\Models\BagReceivingModel;
use App\Models\BagSaleModel;
use App\Models\CodeKindModel;
use App\Models\CodeDetailModel;
use App\Models\CompanyModel;
use App\Models\PackagingUnitModel;
use App\Models\SalesAgencyModel;
use App\Models\ShopOrderModel;
class Bag extends BaseController
{
/**
* 로그인 사용자의 지자체 PK 반환 (미로그인/미지정 시 null)
*/
private function lgIdx(): ?int
{
helper('admin');
return admin_effective_lg_idx();
}
private function render(string $title, string $viewFile, array $data = []): string
{
return view('bag/layout/main', [
'title' => $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', []);
}
}

View File

@@ -0,0 +1,9 @@
<div class="flex items-center justify-center h-full text-gray-400">
<div class="text-center">
<svg class="w-16 h-16 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"/>
</svg>
<p class="text-lg font-medium">통계 분석 관리</p>
<p class="text-sm mt-1">Phase 6에서 구현 예정입니다.</p>
</div>
</div>

View File

@@ -0,0 +1,83 @@
<div class="space-y-6">
<!-- 기본코드 종류 -->
<section>
<h3 class="text-base font-bold text-gray-700 mb-2 border-b pb-1">기본코드 종류</h3>
<table class="data-table">
<thead><tr>
<th class="w-16">번호</th><th>코드</th><th>코드명</th><th>상태</th>
</tr></thead>
<tbody>
<?php if (! empty($codeKinds)): ?>
<?php foreach ($codeKinds as $i => $row): ?>
<tr>
<td class="text-center"><?= $i + 1 ?></td>
<td class="text-center"><?= esc($row->ck_code) ?></td>
<td><?= esc($row->ck_name) ?></td>
<td class="text-center"><?= ($row->ck_status ?? 'active') === 'active' ? '사용' : '미사용' ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="4" class="text-center text-gray-400 py-4">등록된 코드 종류가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</section>
<!-- 봉투 단가 -->
<section>
<h3 class="text-base font-bold text-gray-700 mb-2 border-b pb-1">봉투 단가</h3>
<table class="data-table">
<thead><tr>
<th class="w-16">번호</th><th>봉투코드</th><th>봉투명</th><th>발주단가</th><th>도매가</th><th>소비자가</th><th>적용시작</th><th>적용종료</th><th>상태</th>
</tr></thead>
<tbody>
<?php if (! empty($bagPrices)): ?>
<?php foreach ($bagPrices as $i => $row): ?>
<tr>
<td class="text-center"><?= $i + 1 ?></td>
<td class="text-center"><?= esc($row->bp_bag_code) ?></td>
<td><?= esc($row->bp_bag_name ?? '') ?></td>
<td class="text-right"><?= number_format((float)($row->bp_order_price ?? 0)) ?></td>
<td class="text-right"><?= number_format((float)($row->bp_wholesale_price ?? 0)) ?></td>
<td class="text-right"><?= number_format((float)($row->bp_consumer_price ?? 0)) ?></td>
<td class="text-center"><?= esc($row->bp_start_date ?? '') ?></td>
<td class="text-center"><?= ($row->bp_end_date ?? '') ?: '현재' ?></td>
<td class="text-center"><?= ($row->bp_status ?? 'active') === 'active' ? '사용' : '만료' ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="9" class="text-center text-gray-400 py-4">등록된 단가 정보가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</section>
<!-- 포장 단위 -->
<section>
<h3 class="text-base font-bold text-gray-700 mb-2 border-b pb-1">포장 단위</h3>
<table class="data-table">
<thead><tr>
<th class="w-16">번호</th><th>봉투코드</th><th>봉투명</th><th>박스당 팩 수</th><th>팩당 낱장 수</th><th>1박스 총 낱장</th><th>적용시작</th><th>적용종료</th><th>상태</th>
</tr></thead>
<tbody>
<?php if (! empty($packagingUnits)): ?>
<?php foreach ($packagingUnits as $i => $row): ?>
<tr>
<td class="text-center"><?= $i + 1 ?></td>
<td class="text-center"><?= esc($row->pu_bag_code) ?></td>
<td><?= esc($row->pu_bag_name ?? '') ?></td>
<td class="text-right"><?= number_format((int)($row->pu_packs_per_box ?? 0)) ?></td>
<td class="text-right"><?= number_format((int)($row->pu_sheets_per_pack ?? 0)) ?></td>
<td class="text-right"><?= number_format((int)($row->pu_packs_per_box ?? 0) * (int)($row->pu_sheets_per_pack ?? 0)) ?></td>
<td class="text-center"><?= esc($row->pu_start_date ?? '') ?></td>
<td class="text-center"><?= ($row->pu_end_date ?? '') ?: '현재' ?></td>
<td class="text-center"><?= ($row->pu_status ?? 'active') === 'active' ? '사용' : '만료' ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="9" class="text-center text-gray-400 py-4">등록된 포장 단위가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</section>
</div>

88
app/Views/bag/flow.php Normal file
View File

@@ -0,0 +1,88 @@
<div class="space-y-1">
<form method="get" class="flex items-center gap-3 text-sm mb-3">
<label class="font-bold text-gray-700">조회기간</label>
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<span>~</span>
<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.5 rounded-sm text-sm">조회</button>
<a href="<?= base_url('bag/flow') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
</form>
<!-- 수불 요약 -->
<table class="data-table">
<thead>
<tr>
<th rowspan="2">봉투코드</th>
<th rowspan="2">봉투명</th>
<th rowspan="2">현재재고</th>
<th colspan="2">입고</th>
<th colspan="2">출고</th>
</tr>
<tr>
<th>입고수량</th><th>반품수량</th>
<th>판매수량</th><th>불출수량</th>
</tr>
</thead>
<tbody>
<?php
// 봉투코드별 수불 집계
$summary = [];
// 재고
foreach ($inventory as $inv) {
$code = $inv->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);
?>
<?php if (! empty($summary)): ?>
<?php $idx = 0; foreach ($summary as $code => $s): $idx++; ?>
<tr>
<td class="text-center"><?= esc($code) ?></td>
<td><?= esc($s['name']) ?></td>
<td class="text-right"><?= number_format($s['stock']) ?></td>
<td class="text-right"><?= number_format($s['recv']) ?></td>
<td class="text-right"><?= number_format($s['return']) ?></td>
<td class="text-right"><?= number_format($s['sale']) ?></td>
<td class="text-right"><?= number_format($s['issue']) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="7" class="text-center text-gray-400 py-4">수불 데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>

31
app/Views/bag/help.php Normal file
View File

@@ -0,0 +1,31 @@
<div class="max-w-3xl mx-auto py-4">
<h2 class="text-lg font-bold text-gray-700 mb-4">도움말</h2>
<div class="space-y-4 text-sm text-gray-600">
<section>
<h3 class="font-bold text-gray-700 mb-1">시스템 개요</h3>
<p>쓰레기봉투 물류시스템은 지자체 종량제 쓰레기봉투의 발주, 입고, 재고, 판매, 불출 전체 물류 프로세스를 관리합니다.</p>
</section>
<section>
<h3 class="font-bold text-gray-700 mb-1">메뉴 안내</h3>
<table class="data-table">
<thead><tr><th>메뉴</th><th>설명</th></tr></thead>
<tbody>
<tr><td class="font-medium">기본정보관리</td><td>코드 종류, 봉투 단가, 포장 단위 기본 데이터 조회</td></tr>
<tr><td class="font-medium">발주 입고 관리</td><td>봉투 발주 현황 입고 현황 조회</td></tr>
<tr><td class="font-medium">불출 관리</td><td>무료용 봉투 불출 내역 조회</td></tr>
<tr><td class="font-medium">재고 관리</td><td>봉투별 현재 재고(낱장) 조회</td></tr>
<tr><td class="font-medium">판매 관리</td><td>주문 접수, 판매/반품 내역 조회</td></tr>
<tr><td class="font-medium">판매 현황</td><td>기간별 판매 데이터 조회</td></tr>
<tr><td class="font-medium">봉투 수불 관리</td><td>봉투코드별 입출고 수불 요약 조회</td></tr>
</tbody>
</table>
</section>
<section>
<h3 class="font-bold text-gray-700 mb-1">문의</h3>
<p>시스템 사용 문의사항은 소속 지자체 담당자에게 연락하시기 바랍니다.</p>
</section>
</div>
</div>

View File

@@ -0,0 +1,20 @@
<table class="data-table">
<thead><tr>
<th class="w-16">번호</th><th>봉투코드</th><th>봉투명</th><th>현재재고(낱장)</th><th>최종갱신</th>
</tr></thead>
<tbody>
<?php if (! empty($list)): ?>
<?php foreach ($list as $i => $row): ?>
<tr>
<td class="text-center"><?= $i + 1 ?></td>
<td class="text-center"><?= esc($row->bi_bag_code ?? '') ?></td>
<td><?= esc($row->bi_bag_name ?? '') ?></td>
<td class="text-right"><?= number_format((int)($row->bi_qty_sheet ?? 0)) ?></td>
<td class="text-center"><?= esc($row->bi_updated_at ?? $row->updated_at ?? '') ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="5" class="text-center text-gray-400 py-4">재고 데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>

41
app/Views/bag/issue.php Normal file
View File

@@ -0,0 +1,41 @@
<div class="space-y-1">
<form method="get" class="flex items-center gap-3 text-sm mb-3">
<label class="font-bold text-gray-700">불출일</label>
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<span>~</span>
<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.5 rounded-sm text-sm">조회</button>
<a href="<?= base_url('bag/issue') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
</form>
<table class="data-table">
<thead><tr>
<th class="w-16">번호</th><th>연도</th><th>분기</th><th>구분</th><th>불출일</th><th>불출처</th><th>봉투코드</th><th>봉투명</th><th>수량</th><th>상태</th>
</tr></thead>
<tbody>
<?php if (! empty($list)): ?>
<?php foreach ($list as $i => $row): ?>
<tr>
<td class="text-center"><?= $i + 1 ?></td>
<td class="text-center"><?= esc($row->bi2_year ?? '') ?></td>
<td class="text-center"><?= esc($row->bi2_quarter ?? '') ?></td>
<td class="text-center"><?= esc($row->bi2_type ?? '') ?></td>
<td class="text-center"><?= esc($row->bi2_issue_date ?? '') ?></td>
<td><?= esc($row->bi2_destination ?? '') ?></td>
<td class="text-center"><?= esc($row->bi2_bag_code ?? '') ?></td>
<td><?= esc($row->bi2_bag_name ?? '') ?></td>
<td class="text-right"><?= number_format((int)($row->bi2_qty ?? 0)) ?></td>
<td class="text-center">
<?php
$st = $row->bi2_status ?? 'normal';
echo match($st) { 'normal' => '정상', 'cancelled' => '<span class="text-orange-600">취소</span>', default => esc($st) };
?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="10" class="text-center text-gray-400 py-4">등록된 불출이 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
helper('admin');
$siteNavTree = get_site_nav_tree();
$uriObj = service('request')->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;
}
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title><?= esc($title ?? '쓰레기봉투 물류시스템') ?></title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&amp;display=swap" rel="stylesheet"/>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: { sans: ['"Malgun Gothic"', '"Noto Sans KR"', 'sans-serif'] },
colors: {
'system-header': '#ffffff',
'title-bar': '#2c3e50',
'control-panel': '#f8f9fa',
'btn-search': '#1c4e80',
'btn-excel-border': '#28a745',
'btn-excel-text': '#28a745',
'btn-print-border': '#ced4da',
'btn-exit': '#d9534f',
},
fontSize: { 'xxs': '0.65rem' }
}
}
}
</script>
<style data-purpose="table-layout">
.data-table { width: 100%; border-collapse: collapse; font-family: 'Malgun Gothic', 'Noto Sans KR', sans-serif; }
.data-table th, .data-table td { border: 1px solid #ccc; padding: 4px 8px; white-space: nowrap; font-size: 13px; }
.data-table th { background-color: #e9ecef; text-align: center; vertical-align: middle; font-weight: bold; color: #333; }
.data-table tbody tr:nth-child(even) td { background-color: #f9f9f9; }
.data-table tbody tr:hover td { background-color: #e6f7ff !important; }
.main-content-area { height: calc(100vh - 130px); overflow: auto; }
body { overflow: hidden; }
</style>
</head>
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased select-none">
<!-- BEGIN: Top Navigation -->
<header class="bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0 z-20">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<div class="w-6 h-6 flex items-center justify-center shrink-0">
<svg class="h-5 w-5" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" fill="#2563eb"/><rect x="2" y="2" width="7" height="7" fill="white"/><rect x="5" y="5" width="9" height="9" fill="white"/>
</svg>
</div>
<a href="<?= base_url() ?>" class="text-base font-semibold text-gray-800 tracking-tight hover:text-blue-600">쓰레기봉투 물류시스템</a>
</div>
</div>
<nav class="hidden md:flex gap-5 text-sm font-medium text-gray-600">
<?php if (! empty($siteNavTree)): ?>
<?php foreach ($siteNavTree as $navItem): ?>
<?php $isActive = ($currentPath === trim((string) $navItem->mm_link, '/')); ?>
<div class="relative group">
<a class="<?= $isActive ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>"
href="<?= base_url($navItem->mm_link) ?>">
<?= esc($navItem->mm_name) ?>
</a>
<?php if (! empty($navItem->children)): ?>
<div class="absolute left-0 top-full hidden group-hover:block bg-white border border-gray-200 rounded shadow-lg min-w-[10rem] z-30">
<?php foreach ($navItem->children as $child): ?>
<a href="<?= base_url($child->mm_link) ?>"
class="block px-3 py-1.5 text-sm text-gray-700 hover:bg-blue-50 whitespace-nowrap">
<?= esc($child->mm_name) ?>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</nav>
<div class="flex items-center gap-2">
<?php if ($effectiveLgName !== null): ?>
<span class="text-sm text-gray-600" title="현재 작업 지자체"><?= esc($effectiveLgName) ?></span>
<?php endif; ?>
<?php if ($isAdmin): ?>
<a href="<?= base_url('admin') ?>" class="text-gray-500 hover:text-blue-600 text-sm">관리자</a>
<?php endif; ?>
<a href="<?= base_url('logout') ?>" class="text-gray-500 hover:text-red-600 transition-colors inline-block p-1 rounded hover:bg-red-50" title="로그아웃">
<svg class="h-5 w-5 inline" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/></svg> 종료
</a>
</div>
</header>
<!-- END: Top Navigation -->
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
<?= esc($title ?? '') ?>
</div>
<?php if (session()->getFlashdata('success')): ?>
<div class="mx-4 mt-2 p-3 rounded-lg bg-green-50 text-green-700 text-sm" role="alert"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="mx-4 mt-2 p-3 rounded-lg bg-red-50 text-red-700 text-sm" role="alert"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<main class="main-content-area flex-grow bg-white p-4">
<?= $content ?>
</main>
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 flex items-center justify-between shrink-0">
<span>쓰레기봉투 물류시스템</span>
<span><?= date('Y.m.d (D) g:i:sA') ?></span>
</footer>
</body>
</html>

View File

@@ -0,0 +1,71 @@
<div class="space-y-1">
<!-- 필터 -->
<form method="get" class="flex items-center gap-3 text-sm mb-3">
<label class="font-bold text-gray-700">조회기간</label>
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<span>~</span>
<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.5 rounded-sm text-sm">조회</button>
<a href="<?= base_url('bag/purchase-inbound') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
</form>
<!-- 발주 현황 -->
<section>
<h3 class="text-base font-bold text-gray-700 mb-2 border-b pb-1">발주 현황</h3>
<table class="data-table">
<thead><tr>
<th class="w-16">번호</th><th>LOT번호</th><th>발주일</th><th>품목수</th><th>총수량(낱장)</th><th>총금액</th><th>상태</th>
</tr></thead>
<tbody>
<?php if (! empty($orders)): ?>
<?php foreach ($orders as $i => $row): ?>
<?php $summary = $itemSummary[$row->bo_idx] ?? ['qty' => 0, 'amount' => 0, 'count' => 0]; ?>
<tr>
<td class="text-center"><?= $i + 1 ?></td>
<td class="text-center"><?= esc($row->bo_lot_number ?? '') ?></td>
<td class="text-center"><?= esc($row->bo_order_date ?? '') ?></td>
<td class="text-right"><?= number_format($summary['count']) ?></td>
<td class="text-right"><?= number_format($summary['qty']) ?></td>
<td class="text-right"><?= number_format($summary['amount']) ?></td>
<td class="text-center">
<?php
$st = $row->bo_status ?? 'normal';
echo match($st) { 'normal' => '정상', 'cancelled' => '<span class="text-orange-600">취소</span>', 'deleted' => '<span class="text-red-600">삭제</span>', default => esc($st) };
?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="7" class="text-center text-gray-400 py-4">등록된 발주가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</section>
<!-- 입고 현황 -->
<section class="mt-4">
<h3 class="text-base font-bold text-gray-700 mb-2 border-b pb-1">입고 현황</h3>
<table class="data-table">
<thead><tr>
<th class="w-16">번호</th><th>봉투코드</th><th>봉투명</th><th>박스수</th><th>낱장수</th><th>입고일</th><th>구분</th>
</tr></thead>
<tbody>
<?php if (! empty($receivings)): ?>
<?php foreach ($receivings as $i => $row): ?>
<tr>
<td class="text-center"><?= $i + 1 ?></td>
<td class="text-center"><?= esc($row->br_bag_code ?? '') ?></td>
<td><?= esc($row->br_bag_name ?? '') ?></td>
<td class="text-right"><?= number_format((int)($row->br_qty_box ?? 0)) ?></td>
<td class="text-right"><?= number_format((int)($row->br_qty_sheet ?? 0)) ?></td>
<td class="text-center"><?= esc($row->br_receive_date ?? '') ?></td>
<td class="text-center"><?= esc($row->br_type ?? '정상입고') ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="7" class="text-center text-gray-400 py-4">등록된 입고가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</section>
</div>

76
app/Views/bag/sales.php Normal file
View File

@@ -0,0 +1,76 @@
<div class="space-y-1">
<form method="get" class="flex items-center gap-3 text-sm mb-3">
<label class="font-bold text-gray-700">조회기간</label>
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<span>~</span>
<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.5 rounded-sm text-sm">조회</button>
<a href="<?= base_url('bag/sales') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
</form>
<!-- 주문 접수 -->
<section>
<h3 class="text-base font-bold text-gray-700 mb-2 border-b pb-1">주문 접수</h3>
<table class="data-table">
<thead><tr>
<th class="w-16">번호</th><th>판매소</th><th>접수일</th><th>배달일</th><th>수량</th><th>금액</th><th>상태</th>
</tr></thead>
<tbody>
<?php if (! empty($orderList)): ?>
<?php foreach ($orderList as $i => $row): ?>
<tr>
<td class="text-center"><?= $i + 1 ?></td>
<td><?= esc($row->so_shop_name ?? '') ?></td>
<td class="text-center"><?= esc($row->so_order_date ?? '') ?></td>
<td class="text-center"><?= esc($row->so_delivery_date ?? '') ?></td>
<td class="text-right"><?= number_format((int)($row->so_qty ?? 0)) ?></td>
<td class="text-right"><?= number_format((float)($row->so_amount ?? 0)) ?></td>
<td class="text-center">
<?php
$st = $row->so_status ?? 'normal';
echo match($st) { 'normal' => '정상', 'cancelled' => '<span class="text-orange-600">취소</span>', default => esc($st) };
?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="7" class="text-center text-gray-400 py-4">등록된 주문이 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</section>
<!-- 판매/반품 -->
<section class="mt-4">
<h3 class="text-base font-bold text-gray-700 mb-2 border-b pb-1">판매/반품</h3>
<table class="data-table">
<thead><tr>
<th class="w-16">번호</th><th>판매소</th><th>판매일</th><th>봉투코드</th><th>봉투명</th><th>수량</th><th>단가</th><th>금액</th><th>구분</th>
</tr></thead>
<tbody>
<?php if (! empty($salesList)): ?>
<?php foreach ($salesList as $i => $row): ?>
<tr>
<td class="text-center"><?= $i + 1 ?></td>
<td><?= esc($row->bs_shop_name ?? '') ?></td>
<td class="text-center"><?= esc($row->bs_sale_date ?? '') ?></td>
<td class="text-center"><?= esc($row->bs_bag_code ?? '') ?></td>
<td><?= esc($row->bs_bag_name ?? '') ?></td>
<td class="text-right"><?= number_format((int)($row->bs_qty ?? 0)) ?></td>
<td class="text-right"><?= number_format((float)($row->bs_unit_price ?? 0)) ?></td>
<td class="text-right"><?= number_format((float)($row->bs_amount ?? 0)) ?></td>
<td class="text-center">
<?php
$t = $row->bs_type ?? 'sale';
echo match($t) { 'sale' => '판매', 'return' => '<span class="text-blue-600">반품</span>', 'cancel' => '<span class="text-red-600">취소</span>', default => esc($t) };
?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="9" class="text-center text-gray-400 py-4">등록된 판매/반품이 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</section>
</div>

View File

@@ -0,0 +1,34 @@
<div class="space-y-1">
<form method="get" class="flex items-center gap-3 text-sm mb-3">
<label class="font-bold text-gray-700">조회기간</label>
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<span>~</span>
<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.5 rounded-sm text-sm">조회</button>
<a href="<?= base_url('bag/sales-stats') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
</form>
<table class="data-table">
<thead><tr>
<th class="w-16">번호</th><th>판매소</th><th>판매일</th><th>봉투코드</th><th>봉투명</th><th>수량</th><th>단가</th><th>금액</th>
</tr></thead>
<tbody>
<?php if (! empty($result)): ?>
<?php foreach ($result as $i => $row): ?>
<tr>
<td class="text-center"><?= $i + 1 ?></td>
<td><?= esc($row->bs_shop_name ?? '') ?></td>
<td class="text-center"><?= esc($row->bs_sale_date ?? '') ?></td>
<td class="text-center"><?= esc($row->bs_bag_code ?? '') ?></td>
<td><?= esc($row->bs_bag_name ?? '') ?></td>
<td class="text-right"><?= number_format((int)($row->bs_qty ?? 0)) ?></td>
<td class="text-right"><?= number_format((float)($row->bs_unit_price ?? 0)) ?></td>
<td class="text-right"><?= number_format((float)($row->bs_amount ?? 0)) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="8" class="text-center text-gray-400 py-4">판매 데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>

9
app/Views/bag/window.php Normal file
View File

@@ -0,0 +1,9 @@
<div class="flex items-center justify-center h-full text-gray-400">
<div class="text-center">
<svg class="w-16 h-16 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 8.25V18a2.25 2.25 0 002.25 2.25h13.5A2.25 2.25 0 0021 18V8.25m-18 0V6a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 6v2.25m-18 0h18M5.25 6h.008v.008H5.25V6zM7.5 6h.008v.008H7.5V6zm2.25 0h.008v.008H9.75V6z"/>
</svg>
<p class="text-lg font-medium"> 관리</p>
<p class="text-sm mt-1">Phase 6에서 구현 예정입니다.</p>
</div>
</div>

View File

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

View File

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

View File

@@ -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: 봉투 단가 관리', () => {

80
e2e/bag-site.spec.js Normal file
View File

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

View File

@@ -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: 기본코드 종류 관리', () => {

View File

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

View File

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

View File

@@ -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: 포장 단위 관리', () => {

View File

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

View File

@@ -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회 이후 실패 카운트 표시 확인

View File

@@ -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: 발주 관리', () => {

View File

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

View File

@@ -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: 판매 대장', () => {

View File

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