diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 8edc38e..16440e9 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -74,6 +74,15 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo $routes->post('bag-prices/delete/(:num)', 'Admin\BagPrice::delete/$1'); $routes->get('bag-prices/history/(:num)', 'Admin\BagPrice::history/$1'); + // 포장 단위 관리 (P2-05/06) + $routes->get('packaging-units', 'Admin\PackagingUnit::index'); + $routes->get('packaging-units/create', 'Admin\PackagingUnit::create'); + $routes->post('packaging-units/store', 'Admin\PackagingUnit::store'); + $routes->get('packaging-units/edit/(:num)', 'Admin\PackagingUnit::edit/$1'); + $routes->post('packaging-units/update/(:num)', 'Admin\PackagingUnit::update/$1'); + $routes->post('packaging-units/delete/(:num)', 'Admin\PackagingUnit::delete/$1'); + $routes->get('packaging-units/history/(:num)', 'Admin\PackagingUnit::history/$1'); + $routes->get('designated-shops', 'Admin\DesignatedShop::index'); $routes->get('designated-shops/create', 'Admin\DesignatedShop::create'); $routes->post('designated-shops/store', 'Admin\DesignatedShop::store'); diff --git a/app/Controllers/Admin/PackagingUnit.php b/app/Controllers/Admin/PackagingUnit.php new file mode 100644 index 0000000..8e6c385 --- /dev/null +++ b/app/Controllers/Admin/PackagingUnit.php @@ -0,0 +1,212 @@ +unitModel = model(PackagingUnitModel::class); + $this->historyModel = model(PackagingUnitHistoryModel::class); + } + + public function index() + { + helper('admin'); + $lgIdx = admin_effective_lg_idx(); + if (!$lgIdx) { + return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); + } + + $builder = $this->unitModel->where('pu_lg_idx', $lgIdx); + + $startDate = $this->request->getGet('start_date'); + $endDate = $this->request->getGet('end_date'); + if ($startDate) { + $builder->where('pu_start_date >=', $startDate); + } + if ($endDate) { + $builder->groupStart()->where('pu_end_date IS NULL')->orWhere('pu_end_date <=', $endDate)->groupEnd(); + } + + $list = $builder->orderBy('pu_bag_code', 'ASC')->orderBy('pu_start_date', 'DESC')->findAll(); + + return view('admin/layout', [ + 'title' => '포장 단위 관리', + 'content' => view('admin/packaging_unit/index', [ + 'list' => $list, 'startDate' => $startDate, 'endDate' => $endDate, + ]), + ]); + } + + public function create() + { + helper('admin'); + if (!admin_effective_lg_idx()) { + return redirect()->to(site_url('admin/packaging-units'))->with('error', '지자체를 선택해 주세요.'); + } + + $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first(); + $bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true) : []; + + return view('admin/layout', [ + 'title' => '포장 단위 등록', + 'content' => view('admin/packaging_unit/create', ['bagCodes' => $bagCodes]), + ]); + } + + public function store() + { + helper('admin'); + $lgIdx = admin_effective_lg_idx(); + + $rules = [ + 'pu_bag_code' => 'required|max_length[50]', + 'pu_box_per_pack' => 'required|is_natural_no_zero', + 'pu_pack_per_sheet' => 'required|is_natural_no_zero', + 'pu_start_date' => 'required|valid_date[Y-m-d]', + 'pu_end_date' => 'permit_empty|valid_date[Y-m-d]', + ]; + + if (! $this->validate($rules)) { + return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); + } + + $bagCode = $this->request->getPost('pu_bag_code'); + $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first(); + $bagName = ''; + if ($kind) { + $detail = model(CodeDetailModel::class)->where('cd_ck_idx', $kind->ck_idx)->where('cd_code', $bagCode)->first(); + $bagName = $detail ? $detail->cd_name : ''; + } + + $boxPerPack = (int) $this->request->getPost('pu_box_per_pack'); + $packPerSheet = (int) $this->request->getPost('pu_pack_per_sheet'); + + $this->unitModel->insert([ + 'pu_lg_idx' => $lgIdx, + 'pu_bag_code' => $bagCode, + 'pu_bag_name' => $bagName, + 'pu_box_per_pack' => $boxPerPack, + 'pu_pack_per_sheet' => $packPerSheet, + 'pu_total_per_box' => $boxPerPack * $packPerSheet, + 'pu_start_date' => $this->request->getPost('pu_start_date'), + 'pu_end_date' => $this->request->getPost('pu_end_date') ?: null, + 'pu_state' => 1, + 'pu_regdate' => date('Y-m-d H:i:s'), + 'pu_reg_mb_idx' => session()->get('mb_idx'), + ]); + + return redirect()->to(site_url('admin/packaging-units'))->with('success', '포장 단위가 등록되었습니다.'); + } + + public function edit(int $id) + { + helper('admin'); + $item = $this->unitModel->find($id); + if (!$item || (int) $item->pu_lg_idx !== admin_effective_lg_idx()) { + return redirect()->to(site_url('admin/packaging-units'))->with('error', '포장 단위를 찾을 수 없습니다.'); + } + + $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first(); + $bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true) : []; + + return view('admin/layout', [ + 'title' => '포장 단위 수정', + 'content' => view('admin/packaging_unit/edit', ['item' => $item, 'bagCodes' => $bagCodes]), + ]); + } + + public function update(int $id) + { + helper('admin'); + $item = $this->unitModel->find($id); + if (!$item || (int) $item->pu_lg_idx !== admin_effective_lg_idx()) { + return redirect()->to(site_url('admin/packaging-units'))->with('error', '포장 단위를 찾을 수 없습니다.'); + } + + $rules = [ + 'pu_box_per_pack' => 'required|is_natural_no_zero', + 'pu_pack_per_sheet' => 'required|is_natural_no_zero', + 'pu_start_date' => 'required|valid_date[Y-m-d]', + 'pu_end_date' => 'permit_empty|valid_date[Y-m-d]', + 'pu_state' => 'required|in_list[0,1]', + ]; + + if (! $this->validate($rules)) { + return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); + } + + $db = \Config\Database::connect(); + $db->transStart(); + + $trackFields = ['pu_box_per_pack', 'pu_pack_per_sheet']; + foreach ($trackFields as $field) { + $oldVal = (string) $item->$field; + $newVal = (string) $this->request->getPost($field); + if ($oldVal !== $newVal) { + $this->historyModel->insert([ + 'puh_pu_idx' => $id, + 'puh_field' => $field, + 'puh_old_value' => $oldVal, + 'puh_new_value' => $newVal, + 'puh_changed_at'=> date('Y-m-d H:i:s'), + 'puh_changed_by'=> session()->get('mb_idx'), + ]); + } + } + + $boxPerPack = (int) $this->request->getPost('pu_box_per_pack'); + $packPerSheet = (int) $this->request->getPost('pu_pack_per_sheet'); + + $this->unitModel->update($id, [ + 'pu_box_per_pack' => $boxPerPack, + 'pu_pack_per_sheet' => $packPerSheet, + 'pu_total_per_box' => $boxPerPack * $packPerSheet, + 'pu_start_date' => $this->request->getPost('pu_start_date'), + 'pu_end_date' => $this->request->getPost('pu_end_date') ?: null, + 'pu_state' => (int) $this->request->getPost('pu_state'), + 'pu_moddate' => date('Y-m-d H:i:s'), + ]); + + $db->transComplete(); + return redirect()->to(site_url('admin/packaging-units'))->with('success', '포장 단위가 수정되었습니다.'); + } + + public function delete(int $id) + { + helper('admin'); + $item = $this->unitModel->find($id); + if (!$item || (int) $item->pu_lg_idx !== admin_effective_lg_idx()) { + return redirect()->to(site_url('admin/packaging-units'))->with('error', '포장 단위를 찾을 수 없습니다.'); + } + + $this->unitModel->delete($id); + return redirect()->to(site_url('admin/packaging-units'))->with('success', '포장 단위가 삭제되었습니다.'); + } + + public function history(int $puIdx) + { + helper('admin'); + $item = $this->unitModel->find($puIdx); + if (!$item || (int) $item->pu_lg_idx !== admin_effective_lg_idx()) { + return redirect()->to(site_url('admin/packaging-units'))->with('error', '포장 단위를 찾을 수 없습니다.'); + } + + $list = $this->historyModel->where('puh_pu_idx', $puIdx)->orderBy('puh_changed_at', 'DESC')->findAll(); + + return view('admin/layout', [ + 'title' => '포장 단위 변경 이력 — ' . $item->pu_bag_name, + 'content' => view('admin/packaging_unit/history', ['item' => $item, 'list' => $list]), + ]); + } +} diff --git a/app/Models/PackagingUnitHistoryModel.php b/app/Models/PackagingUnitHistoryModel.php new file mode 100644 index 0000000..6206787 --- /dev/null +++ b/app/Models/PackagingUnitHistoryModel.php @@ -0,0 +1,17 @@ + + 포장 단위 등록 + +
+
+ + +
+ + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ + + 비워두면 현재 적용중 +
+ +
+ + 취소 +
+
+
diff --git a/app/Views/admin/packaging_unit/edit.php b/app/Views/admin/packaging_unit/edit.php new file mode 100644 index 0000000..e88ee1f --- /dev/null +++ b/app/Views/admin/packaging_unit/edit.php @@ -0,0 +1,49 @@ +
+ 포장 단위 수정 +
+
+
+ + +
+ + pu_bag_code) ?> + pu_bag_name) ?> +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + 취소 +
+
+
diff --git a/app/Views/admin/packaging_unit/history.php b/app/Views/admin/packaging_unit/history.php new file mode 100644 index 0000000..3348d03 --- /dev/null +++ b/app/Views/admin/packaging_unit/history.php @@ -0,0 +1,34 @@ +
+
+ ← 포장 단위 목록 + | + 변경 이력 — pu_bag_name) ?> (pu_bag_code) ?>) +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
번호변경 필드이전 값변경 값변경일시
puh_idx) ?>puh_field) ?>puh_old_value) ?>puh_new_value) ?>puh_changed_at) ?>
변경 이력이 없습니다.
+
diff --git a/app/Views/admin/packaging_unit/index.php b/app/Views/admin/packaging_unit/index.php new file mode 100644 index 0000000..211e830 --- /dev/null +++ b/app/Views/admin/packaging_unit/index.php @@ -0,0 +1,60 @@ +
+
+ 포장 단위 관리 + 포장 단위 등록 +
+
+
+
+ + + + + + 초기화 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
번호봉투코드봉투명박스당 팩 수팩당 낱장 수1박스 총 낱장적용시작적용종료상태작업
pu_idx) ?>pu_bag_code) ?>pu_bag_name) ?>pu_box_per_pack) ?>pu_pack_per_sheet) ?>pu_total_per_box) ?>pu_start_date) ?>pu_end_date ?? '현재') ?>pu_state === 1 ? '사용' : '미사용' ?> + 이력 + 수정 +
+ + +
+
등록된 포장 단위가 없습니다.
+
diff --git a/e2e/packaging-unit.spec.js b/e2e/packaging-unit.spec.js new file mode 100644 index 0000000..dda30d9 --- /dev/null +++ b/e2e/packaging-unit.spec.js @@ -0,0 +1,34 @@ +// @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('P2-05/06: 포장 단위 관리', () => { + + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + }); + + test('포장 단위 목록 접근', async ({ page }) => { + await page.goto('/admin/packaging-units'); + await expect(page).toHaveURL(/\/admin\/packaging-units/); + }); + + test('포장 단위 등록 폼 표시', async ({ page }) => { + await page.goto('/admin/packaging-units/create'); + await expect(page.locator('select[name="pu_bag_code"]')).toBeVisible(); + await expect(page.locator('input[name="pu_box_per_pack"]')).toBeVisible(); + await expect(page.locator('input[name="pu_pack_per_sheet"]')).toBeVisible(); + }); + + test('기간 필터 조회', async ({ page }) => { + await page.goto('/admin/packaging-units?start_date=2026-01-01&end_date=2026-12-31'); + await expect(page).toHaveURL(/start_date/); + }); +}); diff --git a/writable/database/packaging_unit_tables.sql b/writable/database/packaging_unit_tables.sql new file mode 100644 index 0000000..77f258d --- /dev/null +++ b/writable/database/packaging_unit_tables.sql @@ -0,0 +1,34 @@ +-- ============================================ +-- 포장 단위 관리 테이블 (P2-05, P2-06) +-- ============================================ + +CREATE TABLE IF NOT EXISTS `packaging_unit` ( + `pu_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `pu_lg_idx` INT UNSIGNED NOT NULL COMMENT '지자체 FK', + `pu_bag_code` VARCHAR(50) NOT NULL COMMENT '봉투코드(code_detail cd_code, ck=O)', + `pu_bag_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '봉투명(스냅샷)', + `pu_box_per_pack` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '박스당 팩 수', + `pu_pack_per_sheet` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '팩당 낱장 수', + `pu_total_per_box` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '1박스당 총 낱장 수', + `pu_start_date` DATE NOT NULL, + `pu_end_date` DATE NULL DEFAULT NULL, + `pu_state` TINYINT UNSIGNED NOT NULL DEFAULT 1, + `pu_regdate` DATETIME NOT NULL, + `pu_moddate` DATETIME NULL DEFAULT NULL, + `pu_reg_mb_idx` INT UNSIGNED NULL, + PRIMARY KEY (`pu_idx`), + KEY `idx_pu_lg_bag` (`pu_lg_idx`, `pu_bag_code`), + KEY `idx_pu_dates` (`pu_start_date`, `pu_end_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='포장 단위'; + +CREATE TABLE IF NOT EXISTS `packaging_unit_history` ( + `puh_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `puh_pu_idx` INT UNSIGNED NOT NULL, + `puh_field` VARCHAR(30) NOT NULL, + `puh_old_value` VARCHAR(100) NOT NULL DEFAULT '', + `puh_new_value` VARCHAR(100) NOT NULL DEFAULT '', + `puh_changed_at` DATETIME NOT NULL, + `puh_changed_by` INT UNSIGNED NULL, + PRIMARY KEY (`puh_idx`), + KEY `idx_puh_pu_idx` (`puh_pu_idx`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='포장 단위 변경 이력';