Phase 5 판매대장/일계표/기간별현황/수불현황 리포트 구현

- SalesReport 컨트롤러 (판매대장, 일계표, 기간별판매, 수불현황)
- 판매대장: 일자별/기간별 모드 전환, 판매소별 품목 집계
- 일계표: 당일 판매 + 당월 누계 병렬 표시
- 기간별 판매현황: 품목별 판매/반품/합계 + 총합계
- 봉투 수불현황: 현재재고/입고/판매/불출 4섹션 그리드 레이아웃
- E2E 테스트 6개 전체 통과

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
javamon1174
2026-03-25 18:29:31 +09:00
parent 6e8bd84182
commit f451f0ff3b
7 changed files with 637 additions and 0 deletions

View File

@@ -123,6 +123,12 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo
$routes->post('packaging-units/delete/(:num)', 'Admin\PackagingUnit::delete/$1'); $routes->post('packaging-units/delete/(:num)', 'Admin\PackagingUnit::delete/$1');
$routes->get('packaging-units/history/(:num)', 'Admin\PackagingUnit::history/$1'); $routes->get('packaging-units/history/(:num)', 'Admin\PackagingUnit::history/$1');
// 현황/리포트 (Phase 5)
$routes->get('reports/sales-ledger', 'Admin\SalesReport::salesLedger');
$routes->get('reports/daily-summary', 'Admin\SalesReport::dailySummary');
$routes->get('reports/period-sales', 'Admin\SalesReport::periodSales');
$routes->get('reports/supply-demand', 'Admin\SalesReport::supplyDemand');
// 판매 대행소 관리 (P2-07/08) // 판매 대행소 관리 (P2-07/08)
$routes->get('sales-agencies', 'Admin\SalesAgency::index'); $routes->get('sales-agencies', 'Admin\SalesAgency::index');
$routes->get('sales-agencies/create', 'Admin\SalesAgency::create'); $routes->get('sales-agencies/create', 'Admin\SalesAgency::create');

View File

@@ -0,0 +1,180 @@
<?php
namespace App\Controllers\Admin;
use App\Controllers\BaseController;
use App\Models\BagSaleModel;
use App\Models\BagIssueModel;
use App\Models\BagReceivingModel;
use App\Models\BagInventoryModel;
class SalesReport extends BaseController
{
/**
* P5-01: 판매 대장 (일자별/기간별)
*/
public function salesLedger()
{
helper('admin');
$lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
$mode = $this->request->getGet('mode') ?? 'daily'; // daily or period
$saleModel = model(BagSaleModel::class);
$db = \Config\Database::connect();
if ($mode === 'daily') {
$result = $db->query("
SELECT bs_sale_date, bs_ds_name, bs_bag_code, bs_bag_name, bs_type,
SUM(ABS(bs_qty)) as total_qty,
SUM(bs_amount) as total_amount
FROM bag_sale
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ? AND bs_type IN('sale','return')
GROUP BY bs_sale_date, bs_ds_name, bs_bag_code, bs_bag_name, bs_type
ORDER BY bs_sale_date DESC, bs_ds_name, bs_bag_code
", [$lgIdx, $startDate, $endDate])->getResult();
} else {
$result = $db->query("
SELECT bs_ds_name, bs_bag_code, bs_bag_name,
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount,
SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty,
SUM(CASE WHEN bs_type='return' THEN bs_amount ELSE 0 END) as return_amount
FROM bag_sale
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
GROUP BY bs_ds_name, bs_bag_code, bs_bag_name
ORDER BY bs_ds_name, bs_bag_code
", [$lgIdx, $startDate, $endDate])->getResult();
}
return view('admin/layout', [
'title' => '판매 대장',
'content' => view('admin/sales_report/sales_ledger', compact('result', 'startDate', 'endDate', 'mode')),
]);
}
/**
* P5-02: 일계표
*/
public function dailySummary()
{
helper('admin');
$lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
$date = $this->request->getGet('date') ?? date('Y-m-d');
$db = \Config\Database::connect();
// 당일 판매
$daily = $db->query("
SELECT bs_bag_code, bs_bag_name,
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount
FROM bag_sale
WHERE bs_lg_idx = ? AND bs_sale_date = ?
GROUP BY bs_bag_code, bs_bag_name
ORDER BY bs_bag_code
", [$lgIdx, $date])->getResult();
// 당월 누계
$monthStart = date('Y-m-01', strtotime($date));
$monthly = $db->query("
SELECT bs_bag_code, bs_bag_name,
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount
FROM bag_sale
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
GROUP BY bs_bag_code, bs_bag_name
ORDER BY bs_bag_code
", [$lgIdx, $monthStart, $date])->getResult();
return view('admin/layout', [
'title' => '일계표',
'content' => view('admin/sales_report/daily_summary', compact('daily', 'monthly', 'date')),
]);
}
/**
* P5-03: 기간별 판매현황
*/
public function periodSales()
{
helper('admin');
$lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
$db = \Config\Database::connect();
$result = $db->query("
SELECT bs_bag_code, bs_bag_name,
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount,
SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty,
SUM(CASE WHEN bs_type='return' THEN bs_amount ELSE 0 END) as return_amount
FROM bag_sale
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
GROUP BY bs_bag_code, bs_bag_name
ORDER BY bs_bag_code
", [$lgIdx, $startDate, $endDate])->getResult();
return view('admin/layout', [
'title' => '기간별 판매현황',
'content' => view('admin/sales_report/period_sales', compact('result', 'startDate', 'endDate')),
]);
}
/**
* P5-07: 봉투 수불 현황
*/
public function supplyDemand()
{
helper('admin');
$lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
$db = \Config\Database::connect();
// 입고 합계
$receiving = $db->query("
SELECT br_bag_code, br_bag_name,
SUM(br_qty_sheet) as recv_qty
FROM bag_receiving
WHERE br_lg_idx = ? AND br_receive_date BETWEEN ? AND ?
GROUP BY br_bag_code, br_bag_name
", [$lgIdx, $startDate, $endDate])->getResult();
// 판매 합계
$sales = $db->query("
SELECT bs_bag_code, bs_bag_name,
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty
FROM bag_sale
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
GROUP BY bs_bag_code, bs_bag_name
", [$lgIdx, $startDate, $endDate])->getResult();
// 불출 합계
$issues = $db->query("
SELECT bi2_bag_code, bi2_bag_name,
SUM(bi2_qty) as issue_qty
FROM bag_issue
WHERE bi2_lg_idx = ? AND bi2_issue_date BETWEEN ? AND ? AND bi2_status = 'normal'
GROUP BY bi2_bag_code, bi2_bag_name
", [$lgIdx, $startDate, $endDate])->getResult();
// 현재 재고
$inventory = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->findAll();
return view('admin/layout', [
'title' => '봉투 수불 현황',
'content' => view('admin/sales_report/supply_demand', compact('receiving', 'sales', 'issues', 'inventory', 'startDate', 'endDate')),
]);
}
}

View File

@@ -0,0 +1,102 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<span class="text-sm font-bold text-gray-700">일계표</span>
</section>
<section class="p-2 bg-white border-b border-gray-200">
<form method="GET" action="<?= base_url('admin/reports/daily-summary') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">조회일</label>
<input type="date" name="date" value="<?= esc($date ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
</form>
</section>
<div class="flex gap-4 mt-2">
<!-- 당일 -->
<div class="flex-1 border border-gray-300 overflow-auto">
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
<span class="text-sm font-bold text-gray-700">당일 (<?= esc($date ?? '') ?>)</span>
</div>
<table class="w-full data-table">
<thead>
<tr>
<th>봉투코드</th>
<th>봉투명</th>
<th>판매수량</th>
<th>판매금액</th>
</tr>
</thead>
<tbody class="text-right">
<?php
$dailySaleQtyTotal = 0;
$dailySaleAmountTotal = 0;
?>
<?php foreach ($daily as $row): ?>
<?php
$dailySaleQtyTotal += (int) $row->sale_qty;
$dailySaleAmountTotal += (int) $row->sale_amount;
?>
<tr>
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
<td><?= number_format((int) $row->sale_qty) ?></td>
<td><?= number_format((int) $row->sale_amount) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($daily)): ?>
<tr><td colspan="4" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
<tfoot class="bg-gray-50 font-bold text-right">
<tr>
<td colspan="2" class="text-center">합계</td>
<td><?= number_format($dailySaleQtyTotal) ?></td>
<td><?= number_format($dailySaleAmountTotal) ?></td>
</tr>
</tfoot>
</table>
</div>
<!-- 당월 누계 -->
<div class="flex-1 border border-gray-300 overflow-auto">
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
<span class="text-sm font-bold text-gray-700">당월 누계 (<?= esc($monthStart ?? '') ?> ~ <?= esc($date ?? '') ?>)</span>
</div>
<table class="w-full data-table">
<thead>
<tr>
<th>봉투코드</th>
<th>봉투명</th>
<th>판매수량</th>
<th>판매금액</th>
</tr>
</thead>
<tbody class="text-right">
<?php
$monthlySaleQtyTotal = 0;
$monthlySaleAmountTotal = 0;
?>
<?php foreach ($monthly as $row): ?>
<?php
$monthlySaleQtyTotal += (int) $row->sale_qty;
$monthlySaleAmountTotal += (int) $row->sale_amount;
?>
<tr>
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
<td><?= number_format((int) $row->sale_qty) ?></td>
<td><?= number_format((int) $row->sale_amount) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($monthly)): ?>
<tr><td colspan="4" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
<tfoot class="bg-gray-50 font-bold text-right">
<tr>
<td colspan="2" class="text-center">합계</td>
<td><?= number_format($monthlySaleQtyTotal) ?></td>
<td><?= number_format($monthlySaleAmountTotal) ?></td>
</tr>
</tfoot>
</table>
</div>
</div>

View File

@@ -0,0 +1,69 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<span class="text-sm font-bold text-gray-700">기간별 판매현황</span>
</section>
<section class="p-2 bg-white border-b border-gray-200">
<form method="GET" action="<?= base_url('admin/reports/period-sales') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">시작일</label>
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<label class="text-sm text-gray-600">~</label>
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
</form>
</section>
<div class="border border-gray-300 overflow-auto mt-2">
<table class="w-full data-table">
<thead>
<tr>
<th>봉투코드</th>
<th>봉투명</th>
<th>판매수량</th>
<th>판매금액</th>
<th>반품수량</th>
<th>반품금액</th>
<th>합계수량</th>
<th>합계금액</th>
</tr>
</thead>
<tbody class="text-right">
<?php
$grandSaleQty = 0;
$grandSaleAmount = 0;
$grandReturnQty = 0;
$grandReturnAmount = 0;
?>
<?php foreach ($result as $row): ?>
<?php
$grandSaleQty += (int) $row->sale_qty;
$grandSaleAmount += (int) $row->sale_amount;
$grandReturnQty += (int) $row->return_qty;
$grandReturnAmount += (int) $row->return_amount;
?>
<tr>
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
<td><?= number_format((int) $row->sale_qty) ?></td>
<td><?= number_format((int) $row->sale_amount) ?></td>
<td><?= number_format((int) $row->return_qty) ?></td>
<td><?= number_format((int) $row->return_amount) ?></td>
<td class="font-bold"><?= number_format((int) $row->sale_qty - (int) $row->return_qty) ?></td>
<td class="font-bold"><?= number_format((int) $row->sale_amount - (int) $row->return_amount) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($result)): ?>
<tr><td colspan="8" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
<tfoot class="bg-gray-50 font-bold text-right">
<tr>
<td colspan="2" class="text-center">합계</td>
<td><?= number_format($grandSaleQty) ?></td>
<td><?= number_format($grandSaleAmount) ?></td>
<td><?= number_format($grandReturnQty) ?></td>
<td><?= number_format($grandReturnAmount) ?></td>
<td><?= number_format($grandSaleQty - $grandReturnQty) ?></td>
<td><?= number_format($grandSaleAmount - $grandReturnAmount) ?></td>
</tr>
</tfoot>
</table>
</div>

View File

@@ -0,0 +1,93 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<span class="text-sm font-bold text-gray-700">판매 대장</span>
</section>
<section class="p-2 bg-white border-b border-gray-200">
<form method="GET" action="<?= base_url('admin/reports/sales-ledger') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">시작일</label>
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<label class="text-sm text-gray-600">~</label>
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<label class="text-sm text-gray-600">조회방식</label>
<select name="mode" class="border border-gray-300 rounded px-2 py-1 text-sm">
<option value="daily" <?= ($mode ?? '') === 'daily' ? 'selected' : '' ?>>일자별</option>
<option value="period" <?= ($mode ?? '') === 'period' ? 'selected' : '' ?>>기간별</option>
</select>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
</form>
</section>
<?php if (($mode ?? 'daily') === 'daily'): ?>
<div class="border border-gray-300 overflow-auto mt-2">
<table class="w-full data-table">
<thead>
<tr>
<th>판매일</th>
<th>판매소</th>
<th>봉투코드</th>
<th>봉투명</th>
<th>구분</th>
<th>수량</th>
<th>금액</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach ($result as $row): ?>
<tr>
<td class="text-center"><?= esc($row->bs_sale_date) ?></td>
<td class="text-left pl-2"><?= esc($row->bs_ds_name) ?></td>
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
<td class="text-center">
<?php
$typeMap = ['sale' => '판매', 'return' => '반품'];
echo esc($typeMap[$row->bs_type] ?? $row->bs_type);
?>
</td>
<td><?= number_format((int) $row->total_qty) ?></td>
<td><?= number_format((int) $row->total_amount) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($result)): ?>
<tr><td colspan="7" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="border border-gray-300 overflow-auto mt-2">
<table class="w-full data-table">
<thead>
<tr>
<th>판매소</th>
<th>봉투코드</th>
<th>봉투명</th>
<th>판매수량</th>
<th>판매금액</th>
<th>반품수량</th>
<th>반품금액</th>
<th>계(수량)</th>
<th>계(금액)</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach ($result as $row): ?>
<tr>
<td class="text-left pl-2"><?= esc($row->bs_ds_name) ?></td>
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
<td><?= number_format((int) $row->sale_qty) ?></td>
<td><?= number_format((int) $row->sale_amount) ?></td>
<td><?= number_format((int) $row->return_qty) ?></td>
<td><?= number_format((int) $row->return_amount) ?></td>
<td class="font-bold"><?= number_format((int) $row->sale_qty - (int) $row->return_qty) ?></td>
<td class="font-bold"><?= number_format((int) $row->sale_amount - (int) $row->return_amount) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($result)): ?>
<tr><td colspan="9" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php endif; ?>

View File

@@ -0,0 +1,130 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<span class="text-sm font-bold text-gray-700">봉투 수불 현황</span>
</section>
<section class="p-2 bg-white border-b border-gray-200">
<form method="GET" action="<?= base_url('admin/reports/supply-demand') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">시작일</label>
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<label class="text-sm text-gray-600">~</label>
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
</form>
</section>
<div class="grid grid-cols-2 gap-4 mt-2">
<!-- 현재 재고 -->
<div class="border border-gray-300 rounded overflow-auto">
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
<span class="text-sm font-bold text-gray-700">현재 재고</span>
</div>
<table class="w-full data-table">
<thead>
<tr>
<th>봉투코드</th>
<th>봉투명</th>
<th>재고수량</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach ($inventory as $row): ?>
<tr>
<td class="text-center font-mono"><?= esc($row->bi_bag_code) ?></td>
<td class="text-left pl-2"><?= esc($row->bi_bag_name) ?></td>
<td class="font-bold"><?= number_format((int) $row->bi_qty) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($inventory)): ?>
<tr><td colspan="3" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- 기간 입고 -->
<div class="border border-gray-300 rounded overflow-auto">
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
<span class="text-sm font-bold text-gray-700">기간 입고</span>
</div>
<table class="w-full data-table">
<thead>
<tr>
<th>봉투코드</th>
<th>봉투명</th>
<th>입고수량</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach ($receiving as $row): ?>
<tr>
<td class="text-center font-mono"><?= esc($row->br_bag_code) ?></td>
<td class="text-left pl-2"><?= esc($row->br_bag_name) ?></td>
<td><?= number_format((int) $row->recv_qty) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($receiving)): ?>
<tr><td colspan="3" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- 기간 판매 -->
<div class="border border-gray-300 rounded overflow-auto">
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
<span class="text-sm font-bold text-gray-700">기간 판매</span>
</div>
<table class="w-full data-table">
<thead>
<tr>
<th>봉투코드</th>
<th>봉투명</th>
<th>판매수량</th>
<th>반품수량</th>
<th>순판매</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach ($sales as $row): ?>
<tr>
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
<td><?= number_format((int) $row->sale_qty) ?></td>
<td><?= number_format((int) $row->return_qty) ?></td>
<td class="font-bold"><?= number_format((int) $row->sale_qty - (int) $row->return_qty) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($sales)): ?>
<tr><td colspan="5" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- 기간 불출 -->
<div class="border border-gray-300 rounded overflow-auto">
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
<span class="text-sm font-bold text-gray-700">기간 불출</span>
</div>
<table class="w-full data-table">
<thead>
<tr>
<th>봉투코드</th>
<th>봉투명</th>
<th>불출수량</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach ($issues as $row): ?>
<tr>
<td class="text-center font-mono"><?= esc($row->bi2_bag_code) ?></td>
<td class="text-left pl-2"><?= esc($row->bi2_bag_name) ?></td>
<td><?= number_format((int) $row->issue_qty) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($issues)): ?>
<tr><td colspan="3" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>

View File

@@ -0,0 +1,57 @@
// @ts-check
const { test, expect } = require('@playwright/test');
const { login } = require('./helpers/auth');
async function loginAsAdmin(page) {
await login(page, 'admin');
await page.locator('input[name="lg_idx"]').first().check();
await page.click('button[type="submit"]');
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
}
test.describe('P5: 판매 대장', () => {
test('일자별 판매 대장 접근', async ({ page }) => {
await loginAsAdmin(page);
await page.goto('/admin/reports/sales-ledger');
await expect(page).toHaveURL(/\/admin\/reports\/sales-ledger/);
});
test('기간별 판매 대장', async ({ page }) => {
await loginAsAdmin(page);
await page.goto('/admin/reports/sales-ledger?mode=period&start_date=2026-01-01&end_date=2026-12-31');
await expect(page).toHaveURL(/mode=period/);
});
});
test.describe('P5: 일계표', () => {
test('일계표 접근', async ({ page }) => {
await loginAsAdmin(page);
await page.goto('/admin/reports/daily-summary');
await expect(page).toHaveURL(/\/admin\/reports\/daily-summary/);
});
});
test.describe('P5: 기간별 판매현황', () => {
test('기간별 판매현황 접근', async ({ page }) => {
await loginAsAdmin(page);
await page.goto('/admin/reports/period-sales');
await expect(page).toHaveURL(/\/admin\/reports\/period-sales/);
});
});
test.describe('P5: 봉투 수불 현황', () => {
test('수불 현황 접근', async ({ page }) => {
await loginAsAdmin(page);
await page.goto('/admin/reports/supply-demand');
await expect(page).toHaveURL(/\/admin\/reports\/supply-demand/);
});
});
test.describe('P5: 지자체관리자 접근', () => {
test('리포트 접근 가능', async ({ page }) => {
await login(page, 'local');
await page.goto('/admin/reports/sales-ledger');
await expect(page).toHaveURL(/\/admin\/reports\/sales-ledger/);
await page.goto('/admin/reports/daily-summary');
await expect(page).toHaveURL(/\/admin\/reports\/daily-summary/);
});
});