diff --git a/app/Config/Routes.php b/app/Config/Routes.php index b805077..16f19ab 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -49,6 +49,22 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo $routes->get('local-governments/create', 'Admin\LocalGovernment::create'); $routes->post('local-governments/store', 'Admin\LocalGovernment::store'); + // 기본코드 종류 관리 (P2-01) + $routes->get('code-kinds', 'Admin\CodeKind::index'); + $routes->get('code-kinds/create', 'Admin\CodeKind::create'); + $routes->post('code-kinds/store', 'Admin\CodeKind::store'); + $routes->get('code-kinds/edit/(:num)', 'Admin\CodeKind::edit/$1'); + $routes->post('code-kinds/update/(:num)', 'Admin\CodeKind::update/$1'); + $routes->post('code-kinds/delete/(:num)', 'Admin\CodeKind::delete/$1'); + + // 세부코드 관리 (P2-02) + $routes->get('code-details/(:num)', 'Admin\CodeDetail::index/$1'); + $routes->get('code-details/(:num)/create', 'Admin\CodeDetail::create/$1'); + $routes->post('code-details/store', 'Admin\CodeDetail::store'); + $routes->get('code-details/edit/(:num)', 'Admin\CodeDetail::edit/$1'); + $routes->post('code-details/update/(:num)', 'Admin\CodeDetail::update/$1'); + $routes->post('code-details/delete/(:num)', 'Admin\CodeDetail::delete/$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/CodeDetail.php b/app/Controllers/Admin/CodeDetail.php new file mode 100644 index 0000000..d8a6ed3 --- /dev/null +++ b/app/Controllers/Admin/CodeDetail.php @@ -0,0 +1,134 @@ +kindModel = model(CodeKindModel::class); + $this->detailModel = model(CodeDetailModel::class); + } + + public function index(int $ckIdx) + { + $kind = $this->kindModel->find($ckIdx); + if ($kind === null) { + return redirect()->to(site_url('admin/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.'); + } + + $list = $this->detailModel->getByKind($ckIdx); + + return view('admin/layout', [ + 'title' => '세부코드 관리 — ' . $kind->ck_name . ' (' . $kind->ck_code . ')', + 'content' => view('admin/code_detail/index', [ + 'kind' => $kind, + 'list' => $list, + ]), + ]); + } + + public function create(int $ckIdx) + { + $kind = $this->kindModel->find($ckIdx); + if ($kind === null) { + return redirect()->to(site_url('admin/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.'); + } + + return view('admin/layout', [ + 'title' => '세부코드 등록 — ' . $kind->ck_name, + 'content' => view('admin/code_detail/create', ['kind' => $kind]), + ]); + } + + public function store() + { + $rules = [ + 'cd_ck_idx' => 'required|is_natural_no_zero', + 'cd_code' => 'required|max_length[50]', + 'cd_name' => 'required|max_length[100]', + 'cd_sort' => 'permit_empty|is_natural', + ]; + + if (! $this->validate($rules)) { + return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); + } + + $ckIdx = (int) $this->request->getPost('cd_ck_idx'); + + $this->detailModel->insert([ + 'cd_ck_idx' => $ckIdx, + 'cd_code' => $this->request->getPost('cd_code'), + 'cd_name' => $this->request->getPost('cd_name'), + 'cd_sort' => (int) ($this->request->getPost('cd_sort') ?: 0), + 'cd_state' => 1, + 'cd_regdate' => date('Y-m-d H:i:s'), + ]); + + return redirect()->to(site_url('admin/code-details/' . $ckIdx))->with('success', '세부코드가 등록되었습니다.'); + } + + public function edit(int $id) + { + $item = $this->detailModel->find($id); + if ($item === null) { + return redirect()->to(site_url('admin/code-kinds'))->with('error', '세부코드를 찾을 수 없습니다.'); + } + + $kind = $this->kindModel->find($item->cd_ck_idx); + + return view('admin/layout', [ + 'title' => '세부코드 수정 — ' . ($kind->ck_name ?? ''), + 'content' => view('admin/code_detail/edit', [ + 'item' => $item, + 'kind' => $kind, + ]), + ]); + } + + public function update(int $id) + { + $item = $this->detailModel->find($id); + if ($item === null) { + return redirect()->to(site_url('admin/code-kinds'))->with('error', '세부코드를 찾을 수 없습니다.'); + } + + $rules = [ + 'cd_name' => 'required|max_length[100]', + 'cd_sort' => 'permit_empty|is_natural', + 'cd_state' => 'required|in_list[0,1]', + ]; + + if (! $this->validate($rules)) { + return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); + } + + $this->detailModel->update($id, [ + 'cd_name' => $this->request->getPost('cd_name'), + 'cd_sort' => (int) ($this->request->getPost('cd_sort') ?: 0), + 'cd_state' => (int) $this->request->getPost('cd_state'), + ]); + + return redirect()->to(site_url('admin/code-details/' . $item->cd_ck_idx))->with('success', '세부코드가 수정되었습니다.'); + } + + public function delete(int $id) + { + $item = $this->detailModel->find($id); + if ($item === null) { + return redirect()->to(site_url('admin/code-kinds'))->with('error', '세부코드를 찾을 수 없습니다.'); + } + + $ckIdx = $item->cd_ck_idx; + $this->detailModel->delete($id); + + return redirect()->to(site_url('admin/code-details/' . $ckIdx))->with('success', '세부코드가 삭제되었습니다.'); + } +} diff --git a/app/Controllers/Admin/CodeKind.php b/app/Controllers/Admin/CodeKind.php new file mode 100644 index 0000000..181be5e --- /dev/null +++ b/app/Controllers/Admin/CodeKind.php @@ -0,0 +1,122 @@ +kindModel = model(CodeKindModel::class); + } + + public function index() + { + $list = $this->kindModel->orderBy('ck_code', 'ASC')->findAll(); + + // 세부코드 수 매핑 + $detailModel = model(CodeDetailModel::class); + $countMap = []; + foreach ($list as $row) { + $countMap[$row->ck_idx] = $detailModel->where('cd_ck_idx', $row->ck_idx)->countAllResults(false); + } + + return view('admin/layout', [ + 'title' => '기본코드 종류 관리', + 'content' => view('admin/code_kind/index', [ + 'list' => $list, + 'countMap' => $countMap, + ]), + ]); + } + + public function create() + { + return view('admin/layout', [ + 'title' => '기본코드 종류 등록', + 'content' => view('admin/code_kind/create'), + ]); + } + + public function store() + { + $rules = [ + 'ck_code' => 'required|max_length[20]|is_unique[code_kind.ck_code]', + 'ck_name' => 'required|max_length[100]', + ]; + + if (! $this->validate($rules)) { + return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); + } + + $this->kindModel->insert([ + 'ck_code' => $this->request->getPost('ck_code'), + 'ck_name' => $this->request->getPost('ck_name'), + 'ck_state' => 1, + 'ck_regdate' => date('Y-m-d H:i:s'), + ]); + + return redirect()->to(site_url('admin/code-kinds'))->with('success', '코드 종류가 등록되었습니다.'); + } + + public function edit(int $id) + { + $item = $this->kindModel->find($id); + if ($item === null) { + return redirect()->to(site_url('admin/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.'); + } + + return view('admin/layout', [ + 'title' => '기본코드 종류 수정', + 'content' => view('admin/code_kind/edit', ['item' => $item]), + ]); + } + + public function update(int $id) + { + $item = $this->kindModel->find($id); + if ($item === null) { + return redirect()->to(site_url('admin/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.'); + } + + $rules = [ + 'ck_name' => 'required|max_length[100]', + 'ck_state' => 'required|in_list[0,1]', + ]; + + if (! $this->validate($rules)) { + return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); + } + + $this->kindModel->update($id, [ + 'ck_name' => $this->request->getPost('ck_name'), + 'ck_state' => (int) $this->request->getPost('ck_state'), + ]); + + return redirect()->to(site_url('admin/code-kinds'))->with('success', '코드 종류가 수정되었습니다.'); + } + + public function delete(int $id) + { + $item = $this->kindModel->find($id); + if ($item === null) { + return redirect()->to(site_url('admin/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.'); + } + + // 세부코드가 있으면 삭제 불가 + $detailCount = model(CodeDetailModel::class)->where('cd_ck_idx', $id)->countAllResults(); + if ($detailCount > 0) { + return redirect()->to(site_url('admin/code-kinds')) + ->with('error', '세부코드가 ' . $detailCount . '개 존재하여 삭제할 수 없습니다. 먼저 세부코드를 삭제해 주세요.'); + } + + $this->kindModel->delete($id); + return redirect()->to(site_url('admin/code-kinds'))->with('success', '코드 종류가 삭제되었습니다.'); + } +} diff --git a/app/Models/CodeDetailModel.php b/app/Models/CodeDetailModel.php new file mode 100644 index 0000000..268069c --- /dev/null +++ b/app/Models/CodeDetailModel.php @@ -0,0 +1,33 @@ +where('cd_ck_idx', $ckIdx); + if ($activeOnly) { + $builder->where('cd_state', 1); + } + return $builder->orderBy('cd_sort', 'ASC')->findAll(); + } +} diff --git a/app/Models/CodeKindModel.php b/app/Models/CodeKindModel.php new file mode 100644 index 0000000..c34bb93 --- /dev/null +++ b/app/Models/CodeKindModel.php @@ -0,0 +1,19 @@ + +
+ ck_name) ?> + | + 세부코드 등록 +
+ +
+
+ + + +
+ + ck_name) ?> (ck_code) ?>) +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + 취소 +
+
+
diff --git a/app/Views/admin/code_detail/edit.php b/app/Views/admin/code_detail/edit.php new file mode 100644 index 0000000..0244fcf --- /dev/null +++ b/app/Views/admin/code_detail/edit.php @@ -0,0 +1,45 @@ +
+
+ ck_name) ?> + | + 세부코드 수정 +
+
+
+
+ + +
+ + ck_name) ?> (ck_code) ?>) +
+ +
+ + cd_code) ?> +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + 취소 +
+
+
diff --git a/app/Views/admin/code_detail/index.php b/app/Views/admin/code_detail/index.php new file mode 100644 index 0000000..3e5cde3 --- /dev/null +++ b/app/Views/admin/code_detail/index.php @@ -0,0 +1,44 @@ +
+
+
+ ← 코드 종류 + | + 세부코드 — ck_name) ?> (ck_code) ?>) +
+ 세부코드 등록 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
번호코드코드명정렬순서상태등록일작업
cd_idx) ?>cd_code) ?>cd_name) ?>cd_sort ?>cd_state === 1 ? '사용' : '미사용' ?>cd_regdate ?? '') ?> + 수정 +
+ + +
+
+
diff --git a/app/Views/admin/code_kind/create.php b/app/Views/admin/code_kind/create.php new file mode 100644 index 0000000..a1ff7b1 --- /dev/null +++ b/app/Views/admin/code_kind/create.php @@ -0,0 +1,23 @@ +
+ 기본코드 종류 등록 +
+
+
+ + +
+ + +
+ +
+ + +
+ +
+ + 취소 +
+
+
diff --git a/app/Views/admin/code_kind/edit.php b/app/Views/admin/code_kind/edit.php new file mode 100644 index 0000000..7825d9e --- /dev/null +++ b/app/Views/admin/code_kind/edit.php @@ -0,0 +1,31 @@ +
+ 기본코드 종류 수정 +
+
+
+ + +
+ + ck_code) ?> +
+ +
+ + +
+ +
+ + +
+ +
+ + 취소 +
+
+
diff --git a/app/Views/admin/code_kind/index.php b/app/Views/admin/code_kind/index.php new file mode 100644 index 0000000..35c8525 --- /dev/null +++ b/app/Views/admin/code_kind/index.php @@ -0,0 +1,43 @@ +
+
+ 기본코드 종류 관리 + 코드 종류 등록 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
번호코드코드명세부코드 수상태등록일작업
ck_idx) ?>ck_code) ?>ck_name) ?> + ck_idx] ?? 0) ?>개 + ck_state === 1 ? '사용' : '미사용' ?>ck_regdate ?? '') ?> + 세부코드 + 수정 +
+ + +
+
+
diff --git a/e2e/admin.spec.js b/e2e/admin.spec.js index 72c0b15..78ec52c 100644 --- a/e2e/admin.spec.js +++ b/e2e/admin.spec.js @@ -16,8 +16,7 @@ test.describe('관리자 패널 — 지자체관리자', () => { test('회원 관리 목록 접근', async ({ page }) => { await page.goto('/admin/users'); await expect(page).toHaveURL(/\/admin\/users/); - const content = await page.content(); - expect(content).toContain('tester_'); + await expect(page.locator('td:has-text("tester_")')).toBeVisible({ timeout: 10000 }); }); test('로그인 이력 접근', async ({ page }) => { diff --git a/e2e/code-management.spec.js b/e2e/code-management.spec.js new file mode 100644 index 0000000..ced9a66 --- /dev/null +++ b/e2e/code-management.spec.js @@ -0,0 +1,71 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); +const { login } = require('./helpers/auth'); + +/** Super Admin 로그인 + 지자체 선택 */ +async function loginAsAdmin(page) { + await login(page, '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 }); +} + +test.describe('P2-01: 기본코드 종류 관리', () => { + + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + }); + + test('코드 종류 목록 접근', async ({ page }) => { + await page.goto('/admin/code-kinds'); + await expect(page).toHaveURL(/\/admin\/code-kinds/); + await expect(page.locator('td:has-text("도/특별시/광역시 구분")').first()).toBeVisible({ timeout: 10000 }); + }); + + test('코드 종류 등록 폼 표시', async ({ page }) => { + await page.goto('/admin/code-kinds/create'); + await expect(page.locator('input[name="ck_code"]')).toBeVisible(); + await expect(page.locator('input[name="ck_name"]')).toBeVisible(); + }); + + test('코드 종류 수정', async ({ page }) => { + // 기존 코드 A의 수정 테스트 + await page.goto('/admin/code-kinds/edit/1'); + await expect(page.locator('input[name="ck_name"]')).toBeVisible(); + // 값 확인만 (실제 수정은 하지 않음 - 기존 데이터 보존) + }); +}); + +test.describe('P2-02: 세부코드 관리', () => { + + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + }); + + test('세부코드 목록 접근 (봉투구분 E)', async ({ page }) => { + await page.goto('/admin/code-details/5'); + await expect(page).toHaveURL(/\/admin\/code-details\/5/); + await expect(page.locator('td:has-text("일반용")').first()).toBeVisible({ timeout: 10000 }); + }); + + test('세부코드 등록 폼 표시', async ({ page }) => { + await page.goto('/admin/code-details/5/create'); + await expect(page.locator('input[name="cd_code"]')).toBeVisible(); + await expect(page.locator('input[name="cd_name"]')).toBeVisible(); + }); + + test('세부코드 수정 폼', async ({ page }) => { + // 기존 세부코드 35(일반용)의 수정 폼 확인 + await page.goto('/admin/code-details/edit/35'); + await expect(page.locator('input[name="cd_name"]')).toBeVisible(); + }); + + test('코드 종류에서 세부코드 링크 이동', async ({ page }) => { + await page.goto('/admin/code-kinds'); + // "세부코드" 링크 클릭 + const link = page.locator('a:has-text("세부코드")').first(); + await link.click(); + await expect(page).toHaveURL(/\/admin\/code-details\/\d+/); + }); +}); diff --git a/screenshots/23_admin_code_kinds.png b/screenshots/23_admin_code_kinds.png new file mode 100644 index 0000000..c2187ef Binary files /dev/null and b/screenshots/23_admin_code_kinds.png differ diff --git a/screenshots/24_admin_code_details.png b/screenshots/24_admin_code_details.png new file mode 100644 index 0000000..a93f6a8 Binary files /dev/null and b/screenshots/24_admin_code_details.png differ