From f451f0ff3b883cf96abfc7a87ab92aa2b25fa254 Mon Sep 17 00:00:00 2001 From: javamon1174 Date: Wed, 25 Mar 2026 18:29:31 +0900 Subject: [PATCH] =?UTF-8?q?Phase=205=20=ED=8C=90=EB=A7=A4=EB=8C=80?= =?UTF-8?q?=EC=9E=A5/=EC=9D=BC=EA=B3=84=ED=91=9C/=EA=B8=B0=EA=B0=84?= =?UTF-8?q?=EB=B3=84=ED=98=84=ED=99=A9/=EC=88=98=EB=B6=88=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=EB=A6=AC=ED=8F=AC=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SalesReport 컨트롤러 (판매대장, 일계표, 기간별판매, 수불현황) - 판매대장: 일자별/기간별 모드 전환, 판매소별 품목 집계 - 일계표: 당일 판매 + 당월 누계 병렬 표시 - 기간별 판매현황: 품목별 판매/반품/합계 + 총합계 - 봉투 수불현황: 현재재고/입고/판매/불출 4섹션 그리드 레이아웃 - E2E 테스트 6개 전체 통과 Co-Authored-By: Claude Opus 4.6 (1M context) --- app/Config/Routes.php | 6 + app/Controllers/Admin/SalesReport.php | 180 ++++++++++++++++++ .../admin/sales_report/daily_summary.php | 102 ++++++++++ app/Views/admin/sales_report/period_sales.php | 69 +++++++ app/Views/admin/sales_report/sales_ledger.php | 93 +++++++++ .../admin/sales_report/supply_demand.php | 130 +++++++++++++ e2e/phase5-reports.spec.js | 57 ++++++ 7 files changed, 637 insertions(+) create mode 100644 app/Controllers/Admin/SalesReport.php create mode 100644 app/Views/admin/sales_report/daily_summary.php create mode 100644 app/Views/admin/sales_report/period_sales.php create mode 100644 app/Views/admin/sales_report/sales_ledger.php create mode 100644 app/Views/admin/sales_report/supply_demand.php create mode 100644 e2e/phase5-reports.spec.js diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 64b1ab0..a2e8f6a 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -123,6 +123,12 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo $routes->post('packaging-units/delete/(:num)', 'Admin\PackagingUnit::delete/$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) $routes->get('sales-agencies', 'Admin\SalesAgency::index'); $routes->get('sales-agencies/create', 'Admin\SalesAgency::create'); diff --git a/app/Controllers/Admin/SalesReport.php b/app/Controllers/Admin/SalesReport.php new file mode 100644 index 0000000..77a1389 --- /dev/null +++ b/app/Controllers/Admin/SalesReport.php @@ -0,0 +1,180 @@ +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')), + ]); + } +} diff --git a/app/Views/admin/sales_report/daily_summary.php b/app/Views/admin/sales_report/daily_summary.php new file mode 100644 index 0000000..0539b58 --- /dev/null +++ b/app/Views/admin/sales_report/daily_summary.php @@ -0,0 +1,102 @@ +
+ 일계표 +
+
+
+ + + +
+
+ +
+ +
+
+ 당일 () +
+ + + + + + + + + + + + + sale_qty; + $dailySaleAmountTotal += (int) $row->sale_amount; + ?> + + + + + + + + + + + + + + + + + + +
봉투코드봉투명판매수량판매금액
bs_bag_code) ?>bs_bag_name) ?>sale_qty) ?>sale_amount) ?>
조회된 데이터가 없습니다.
합계
+
+ + +
+
+ 당월 누계 ( ~ ) +
+ + + + + + + + + + + + + sale_qty; + $monthlySaleAmountTotal += (int) $row->sale_amount; + ?> + + + + + + + + + + + + + + + + + + +
봉투코드봉투명판매수량판매금액
bs_bag_code) ?>bs_bag_name) ?>sale_qty) ?>sale_amount) ?>
조회된 데이터가 없습니다.
합계
+
+
diff --git a/app/Views/admin/sales_report/period_sales.php b/app/Views/admin/sales_report/period_sales.php new file mode 100644 index 0000000..ebe307b --- /dev/null +++ b/app/Views/admin/sales_report/period_sales.php @@ -0,0 +1,69 @@ +
+ 기간별 판매현황 +
+
+
+ + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + sale_qty; + $grandSaleAmount += (int) $row->sale_amount; + $grandReturnQty += (int) $row->return_qty; + $grandReturnAmount += (int) $row->return_amount; + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + +
봉투코드봉투명판매수량판매금액반품수량반품금액합계수량합계금액
bs_bag_code) ?>bs_bag_name) ?>sale_qty) ?>sale_amount) ?>return_qty) ?>return_amount) ?>sale_qty - (int) $row->return_qty) ?>sale_amount - (int) $row->return_amount) ?>
조회된 데이터가 없습니다.
합계
+
diff --git a/app/Views/admin/sales_report/sales_ledger.php b/app/Views/admin/sales_report/sales_ledger.php new file mode 100644 index 0000000..38d6ba1 --- /dev/null +++ b/app/Views/admin/sales_report/sales_ledger.php @@ -0,0 +1,93 @@ +
+ 판매 대장 +
+
+
+ + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
판매일판매소봉투코드봉투명구분수량금액
bs_sale_date) ?>bs_ds_name) ?>bs_bag_code) ?>bs_bag_name) ?> + '판매', 'return' => '반품']; + echo esc($typeMap[$row->bs_type] ?? $row->bs_type); + ?> + total_qty) ?>total_amount) ?>
조회된 데이터가 없습니다.
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
판매소봉투코드봉투명판매수량판매금액반품수량반품금액계(수량)계(금액)
bs_ds_name) ?>bs_bag_code) ?>bs_bag_name) ?>sale_qty) ?>sale_amount) ?>return_qty) ?>return_amount) ?>sale_qty - (int) $row->return_qty) ?>sale_amount - (int) $row->return_amount) ?>
조회된 데이터가 없습니다.
+
+ diff --git a/app/Views/admin/sales_report/supply_demand.php b/app/Views/admin/sales_report/supply_demand.php new file mode 100644 index 0000000..f79e526 --- /dev/null +++ b/app/Views/admin/sales_report/supply_demand.php @@ -0,0 +1,130 @@ +
+ 봉투 수불 현황 +
+
+
+ + + + + +
+
+ +
+ +
+
+ 현재 재고 +
+ + + + + + + + + + + + + + + + + + + + +
봉투코드봉투명재고수량
bi_bag_code) ?>bi_bag_name) ?>bi_qty) ?>
데이터가 없습니다.
+
+ + +
+
+ 기간 입고 +
+ + + + + + + + + + + + + + + + + + + + +
봉투코드봉투명입고수량
br_bag_code) ?>br_bag_name) ?>recv_qty) ?>
데이터가 없습니다.
+
+ + +
+
+ 기간 판매 +
+ + + + + + + + + + + + + + + + + + + + + + + + +
봉투코드봉투명판매수량반품수량순판매
bs_bag_code) ?>bs_bag_name) ?>sale_qty) ?>return_qty) ?>sale_qty - (int) $row->return_qty) ?>
데이터가 없습니다.
+
+ + +
+
+ 기간 불출 +
+ + + + + + + + + + + + + + + + + + + + +
봉투코드봉투명불출수량
bi2_bag_code) ?>bi2_bag_name) ?>issue_qty) ?>
데이터가 없습니다.
+
+
diff --git a/e2e/phase5-reports.spec.js b/e2e/phase5-reports.spec.js new file mode 100644 index 0000000..80d848d --- /dev/null +++ b/e2e/phase5-reports.spec.js @@ -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/); + }); +});