Compare commits

...

38 Commits

Author SHA1 Message Date
taekyoungc
21e7b91871 실사/불출/lot-seed 관련 사이트 라우트를 운영 반영한다.
운영 404를 유발한 inspection-select 및 inspection-work 라우트 누락을 포함해 사이트 경로 변경분을 Routes.php에 반영한다.

Made-with: Cursor
2026-04-29 15:15:48 +09:00
taekyoungc
c708d30660 실사 저장 안정화와 메뉴 운영 정책을 일관되게 반영한다.
실사 저장값이 페이지 이동 후 원복되지 않도록 저장/조회 경로를 보강하고, 코드 범위 보정과 bis 간 동기화를 추가했다. 또한 메뉴 관리를 레벨4 이상으로 제한하고 메뉴 변경 사항을 모든 지자체에 일괄 반영하도록 동기화 로직을 도입했다.

Made-with: Cursor
2026-04-29 14:59:49 +09:00
taekyoungc
215d4d2c7c 발주 변경·입고 화면을 사이트 흐름에 맞게 반영한다.
발주 등록/변경 및 스캐너·일괄·입고현황 화면의 라우팅과 화면 구성을 운영과 동일한 최신 형태로 정리한다.

Made-with: Cursor
2026-04-23 15:53:33 +09:00
taekyoungc
6db9d119c1 발주 최신 헤드 조회 메서드 누락을 복구한다.
/bag/order/create 경로에서 발생한 undefined method(whereLatestHead) 오류를 막기 위해 BagOrderModel에 조회 스코프를 추가한다.

Made-with: Cursor
2026-04-23 15:43:19 +09:00
taekyoungc
5c89c963ee 단가 기간이 겹칠 때 최신 등록 단가를 우선 적용한다.
단가 조회 공통 로직을 모델로 통합하고 발주·판매·주문·사이트 화면의 단가 계산이 모두 최신 등록 순서(bp_regdate, bp_idx DESC)를 따르도록 맞춘다.

Made-with: Cursor
2026-04-22 15:35:36 +09:00
taekyoungc
05c479397b 업체·담당자·단가·지정판매소 관리 화면의 조회 및 표시를 개선한다.
관리 화면에서 유형별 조회와 순번 표기를 통일하고, 지정판매소 주소/구군 표시와 포장단위 이력 표현을 사용자 관점으로 정리한다.

Made-with: Cursor
2026-04-22 15:35:28 +09:00
taekyoungc
647d5f919d 지정판매소 주소·지도 연동과 관련 설정을 반영
지정판매소 등록/수정/목록에 카카오 주소 검색 및 지도 연동 컴포넌트를 적용하고, 관련 모델·SQL 스크립트·테스트 설정을 함께 정리해 기능 동작 기반을 맞췄다.

Made-with: Cursor
2026-04-14 14:55:12 +09:00
taekyoungc
0b4c622b99 기본코드관리 2분할 조회와 무료용 목록 컬럼 정리
기본코드관리에서 코드종류 선택 시 같은 화면 우측에 세부코드가 즉시 보이도록 2분할 UI로 전환하고, 무료용 대상자 목록의 불필요한 구분 컬럼을 숨겨 화면 구성을 단순화했다.

Made-with: Cursor
2026-04-14 14:49:15 +09:00
taekyoungc
40db578e85 지정판매소 소메뉴 활성 상태를 단일 선택으로 보정
지정판매소 관련 형제 소메뉴가 동시에 활성화되던 문제를 해결하고, bag/admin 레이아웃 모두에서 현재 경로 기준으로 가장 구체적인 하위 메뉴 하나만 활성화되도록 통일했다.

Made-with: Cursor
2026-04-14 11:59:33 +09:00
taekyoungc
5d733ac0d8 Revert "운영 메뉴에서 지정판매소 활성 상태가 중복되지 않도록 보정"
This reverts commit 48e5578611.
2026-04-14 00:41:14 +09:00
taekyoungc
2629644f90 Revert "운영 Whoops 방지를 위해 메뉴 활성 계산 의존성을 단순화"
This reverts commit c8d1612f0e.
2026-04-14 00:41:14 +09:00
taekyoungc
c8d1612f0e 운영 Whoops 방지를 위해 메뉴 활성 계산 의존성을 단순화
레이아웃에서 내부 헬퍼 함수를 직접 호출하지 않고 공개 메뉴 매칭 함수만 사용하도록 변경해 운영 환경 차이에 따른 오류 가능성을 줄였습니다.
2026-04-14 00:38:51 +09:00
taekyoungc
48e5578611 운영 메뉴에서 지정판매소 활성 상태가 중복되지 않도록 보정
상단 메뉴 활성 판정을 최장 경로 1건 우선으로 통일해 조회 화면에서 관리 메뉴가 함께 활성화되는 문제를 막았습니다.
2026-04-14 00:33:24 +09:00
taekyoungc
078fa5d0c2 운영 URL에서도 bag 화면은 사이트 메뉴 레이아웃을 사용하도록 수정
요청 경로를 정규화해 bag 접두를 판별하도록 변경하고 지정판매소 경로의 관리자 레이아웃 강제 분기를 제거했습니다.
2026-04-14 00:28:02 +09:00
taekyoungc
734a55833b 지정판매소 현황·바코드 출력 기능을 전용 화면으로 확장
지정판매소 신규/취소 현황을 사용자 지자체 기준으로 고정 조회하도록 정리하고, 동별 요약과 컬럼 설명 툴팁을 추가했습니다.
또한 지정판매소 바코드 출력 메뉴를 전용 URL로 분리하고 선택 인쇄/출력 레이아웃을 GBMS 형태에 맞춰 구현했습니다.
2026-04-14 00:14:53 +09:00
taekyoungc
72578f200c chore: remove temporary db_diag from packaging units
Drop the public db diagnostic panel now that the runtime DB endpoint was identified.
2026-04-09 13:01:31 +09:00
taekyoungc
8e859f420d chore: show runtime DB server identity in db_diag
Add config and server-level DB identity fields (host/port/user, @@hostname/@@port/@@version) to packaging-units db_diag so production can verify the exact runtime DB endpoint.
2026-04-09 12:54:35 +09:00
taekyoungc
cd2d41b3d7 chore: add db diagnostic mode on packaging units page
Expose a temporary db_diag=1 view for /bag/packaging-units so we can verify runtime DB connectivity and required table counts directly on production.
2026-04-09 12:48:37 +09:00
taekyoungc
f22b1480a3 fix: add runtime logging for code-kind lookup failures
Capture detailed runtime context when /bag/code-kinds and /bag/code-details fail so production logs reveal the exact exception source and request/session scope.
2026-04-09 12:48:37 +09:00
taekyoungc
7580c31ab0 fix: restore site nav rendering with menu type fallback
Fallback to legacy site mt_idx=4 when site menu type mapping is inconsistent or missing so top navigation renders on trash.wxn.co.kr.

Made-with: Cursor
2026-04-08 17:31:55 +09:00
taekyoungc
6fddf15000 fix: keep selected menu type while applying site fallback
Preserve the selected site mt_idx in the UI and use a separate effective mt_idx for data fallback, so choosing site no longer appears as admin.

Made-with: Cursor
2026-04-08 17:29:40 +09:00
taekyoungc
b99c108aeb fix: fallback site menu mt_idx when mapping is inconsistent
When site menu resolves to an id with no rows, retry with legacy site mt_idx=4 and surface fallback state in debug output.

Made-with: Cursor
2026-04-08 17:26:04 +09:00
taekyoungc
f68f135446 chore: add temporary menu debug diagnostics
Show current lg_idx and resolved menu type values on admin menu page when debug=1 to diagnose empty menu rendering.

Made-with: Cursor
2026-04-08 17:23:10 +09:00
taekyoungc
0d512bd21d fix: normalize legacy menu type query parameter
Map legacy /admin/menus?mt_idx=2 requests to the actual site menu type id and apply the same normalization to JSON list responses.

Made-with: Cursor
2026-04-08 17:14:19 +09:00
taekyoungc
12cd052c40 chore: revert logo label suffix
Remove the temporary "1" suffix from the header brand label and title.

Made-with: Cursor
2026-04-08 15:52:47 +09:00
taekyoungc
aaf7b4c66e chore: update dashboard logo label text
Append "1" to the bag dashboard logo-adjacent "종량제 시스템" label across dashboard variants for consistent UI wording.

Made-with: Cursor
2026-04-08 15:51:08 +09:00
javamon1174
d551dfa87e .gitignore에 deploy.log 추가
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 15:43:10 +09:00
taekyoungc
84026f8072 feat: add sales agency list search filters
판매 대행소 관리 목록에 번호·구분·코드·명 조회 조건을 추가하고 페이징에서도 검색 조건이 유지되도록 반영한다.

Made-with: Cursor
2026-04-08 11:59:00 +09:00
taekyoungc
1a8d4bb9da chore: untrack docs directory 2026-04-08 00:33:05 +09:00
taekyoungc
06aa401048 docs: add project docs and test updates 2026-04-08 00:23:55 +09:00
taekyoungc
06fedc866a feat: update auth and security flow 2026-04-08 00:23:21 +09:00
taekyoungc
b5eed31b94 feat: improve bag price and packaging unit management 2026-04-08 00:21:53 +09:00
taekyoungc
c2dc2fd38a feat: enhance order sales inventory workflows 2026-04-08 00:20:09 +09:00
taekyoungc
984ddb403e feat: improve admin master data management 2026-04-08 00:19:00 +09:00
taekyoungc
89f80edc5d refactor: unify bag and admin layout routing 2026-04-08 00:18:01 +09:00
taekyoungc
5b0c3fac97 Add tester_badmin account with role and level 2026-04-07 15:09:46 +09:00
taekyoungc
c4d30b204b docs: README 기본코드(코드 관리) URL·권한·SQL 보강
- bag 목록(/bag/code-kinds, code-details) vs admin CRUD 분리, loginAuth·canManageCodeMaster
- 선택 실행 SQL·CSV 동기화 도구, 보안·E2E·스크립트 목록 정리

Made-with: Cursor
2026-03-30 15:26:31 +09:00
taekyoungc
ab40a90f69 feat: 기본코드 bag 목록과 관리자 CRUD 분리
- /bag/code-kinds, /bag/code-details/{ck_idx} 조회 (LoginAuthFilter, Roles::canManageCodeMaster)
- admin에서는 종류·세부 목록 제거, 등록·수정·삭제만 유지 후 bag으로 리다이렉트
- 사이트 메뉴·기본코드 링크 SQL, CSV 동기화 스크립트·README 보강
- 관리자 대시보드: 발주·판매 테이블 미존재 시 통계 비활성화
- 회원 로그인 잠금(mb_login_fail_count, mb_locked_until) 및 관리자 잠금 해제

Made-with: Cursor
2026-03-30 15:07:09 +09:00
190 changed files with 16280 additions and 2611 deletions

View File

@@ -0,0 +1,10 @@
---
description: 패키지 설치 전 승인·안정성 확인 및 공급망 보안 습관
alwaysApply: true
---
# 의존성·패키지 보안
- **새 패키지(npm, Composer 등)를 설치·추가하기 전에 반드시 사용자에게 먼저 물어본다.** 자동으로 `npm install`, `composer require` 등을 실행하지 않는다(사용자가 명시적으로 요청한 경우만).
- 새 버전을 제안할 때는 **공식 레지스트리(npmjs.org, packagist.org) 출처**인지 확인하고, **출시된 지 최소 며칠(가이드: 7일) 이상 지난 안정(stable) 버전**을 우선 제안한다. 방금 출시된 버전은 typosquat·피싱 패키지 위험이 있어 사용자에게 그 점을 짚어 준다.
- 락 파일(`package-lock.json`, `composer.lock`)을 대량 수정하거나 생소한 패키지를 넣지 않는다. 이상하면 사용자에게 중단하고 확인을 요청한다.

2
.gitignore vendored
View File

@@ -102,6 +102,7 @@ writable/debugbar/*
!writable/debugbar/index.html !writable/debugbar/index.html
php_errors.log php_errors.log
deploy.log
#------------------------- #-------------------------
# User Guide Temp Files # User Guide Temp Files
@@ -173,3 +174,4 @@ blob-report/
/results/ /results/
/phpunit*.xml /phpunit*.xml
docs/

View File

@@ -39,7 +39,7 @@ app/
├── Config/ # Routes, Database, Roles, Filters, Session 등 (45개) ├── Config/ # Routes, Database, Roles, Filters, Session 등 (45개)
├── Controllers/ # 28개 컨트롤러 ├── Controllers/ # 28개 컨트롤러
│ ├── Auth.php # 로그인/로그아웃/회원가입 │ ├── Auth.php # 로그인/로그아웃/회원가입
│ ├── Bag.php # 사이트 메뉴 페이지 (10개 메뉴) │ ├── Bag.php # 사이트 메뉴 페이지 (기본정보·기본코드 목록 등)
│ ├── Home.php # 홈/대시보드 │ ├── Home.php # 홈/대시보드
│ └── Admin/ # 관리자 컨트롤러 24개 │ └── Admin/ # 관리자 컨트롤러 24개
│ ├── BagOrder.php # 발주 관리 │ ├── BagOrder.php # 발주 관리
@@ -69,7 +69,7 @@ app/
│ ├── bag/ # 사이트 메뉴 뷰 (17개 + 레이아웃) │ ├── bag/ # 사이트 메뉴 뷰 (17개 + 레이아웃)
│ ├── auth/ # 로그인/회원가입 (2개) │ ├── auth/ # 로그인/회원가입 (2개)
│ └── home/ # 대시보드 (1개) │ └── home/ # 대시보드 (1개)
├── Filters/ # AdminAuthFilter (관리자 접근 제어) ├── Filters/ # AdminAuthFilter, LoginAuthFilter (`/bag/code-kinds` 등)
├── Helpers/ # admin_helper, pii_encryption_helper ├── Helpers/ # admin_helper, pii_encryption_helper
└── Database/ # Migrations, Seeds └── Database/ # Migrations, Seeds
public/ # 웹 루트 public/ # 웹 루트
@@ -160,6 +160,7 @@ assets/ # 기획 문서 (엑셀)
- 역할 상수: `Config\Roles` -- `LEVEL_SUPER_ADMIN(4)`, `LEVEL_LOCAL_ADMIN(3)`, `LEVEL_SHOP(2)`, `LEVEL_CITIZEN(1)` - 역할 상수: `Config\Roles` -- `LEVEL_SUPER_ADMIN(4)`, `LEVEL_LOCAL_ADMIN(3)`, `LEVEL_SHOP(2)`, `LEVEL_CITIZEN(1)`
- `AdminAuthFilter`가 로그인 + 레벨 3/4 + 지자체 선택 여부 검증 - `AdminAuthFilter`가 로그인 + 레벨 3/4 + 지자체 선택 여부 검증
- **기본코드 마스터 CRUD**는 `Roles::canManageCodeMaster()`(지자체관리자·Super Admin 등)로 제한. **종류·세부 목록 조회**는 로그인 사용자 전원 (`/bag/code-kinds`, `/bag/code-details/{ck_idx}`, `loginAuth` 필터)
## 멀티테넌시 ## 멀티테넌시
@@ -185,7 +186,9 @@ assets/ # 기획 문서 (엑셀)
| 경로 | 설명 | 기능 | | 경로 | 설명 | 기능 |
|------|------|------| |------|------|------|
| `/bag/basic-info` | 기본정보관리 | 코드/단가/포장단위 조회 + 관리 링크 | | `/bag/basic-info` | 기본정보관리 | 단가·포장단위 등 링크 허브 (`/bag/code-kinds`로 기본코드 조회) |
| `/bag/code-kinds` | 기본코드 종류 | 종류 목록·세부코드 링크 (조회; CRUD는 관리자만) |
| `/bag/code-details/{ck_idx}` | 기본코드 세부 | 해당 종류의 세부코드 목록 (조회; CRUD는 관리자만) |
| `/bag/purchase-inbound` | 발주 입고 관리 | 발주/입고 목록 + 등록 버튼 | | `/bag/purchase-inbound` | 발주 입고 관리 | 발주/입고 목록 + 등록 버튼 |
| `/bag/issue` | 불출 관리 | 불출 목록 + 처리/취소 | | `/bag/issue` | 불출 관리 | 불출 목록 + 처리/취소 |
| `/bag/inventory` | 재고 관리 | 봉투별 현재 재고 조회 | | `/bag/inventory` | 재고 관리 | 봉투별 현재 재고 조회 |
@@ -210,46 +213,39 @@ assets/ # 기획 문서 (엑셀)
| `/admin/menus/*` | 메뉴 관리 (트리 CRUD) | | `/admin/menus/*` | 메뉴 관리 (트리 CRUD) |
| `/admin/local-governments/*` | 지자체 관리 (CRUD) | | `/admin/local-governments/*` | 지자체 관리 (CRUD) |
| `/admin/select-local-government` | 작업 지자체 선택 (Super Admin) | | `/admin/select-local-government` | 작업 지자체 선택 (Super Admin) |
| `/admin/password-change` | 비밀번호 변경 |
| `/admin/designated-shops/*` | 지정판매소 관리 (CRUD) |
**기본정보관리 (Phase 2)** **기본코드 CRUD만 관리자 경로 (목록·조회는 `/bag/*`)**
| 경로 | 기능 | | 경로 | 기능 |
|------|------| |------|------|
| `/admin/code-kinds/*` | 기본코드 종류 (CRUD) | | `/admin/code-kinds/*` | 기본코드 종류 **CRUD** (create/edit/store/update/delete; **목록 없음** — 조회는 `/bag/code-kinds`) |
| `/admin/code-details/*` | 세부코드 (CRUD) | | `/admin/code-details/*` | 세부코드 **CRUD** (**목록 없음** — 조회는 `/bag/code-details/{ck_idx}`) |
| `/admin/bag-prices/*` | 봉투 단가 (CRUD + 이력) | | (호환) `GET /admin/code-details/{ck_idx}` | `/bag/code-details/{ck_idx}` 로 리다이렉트 |
| `/admin/packaging-units/*` | 포장 단위 (CRUD + 이력) |
| `/admin/sales-agencies/*` | 판매 대행소 (CRUD) |
| `/admin/managers/*` | 담당자 (CRUD) |
| `/admin/companies/*` | 업체 (CRUD) |
| `/admin/free-recipients/*` | 무료용 대상자 (CRUD) |
**발주/입고/재고 (Phase 3)** ### 업무 화면 (`/bag/*`, adminAuth 필터)
동일 `Admin\*` 컨트롤러·뷰를 쓰며 메인 사이트 레이아웃으로 렌더된다. `GET /admin/managers` 등 옛 업무 URL은 `301`·`POST``307``/bag/...`에 리다이렉트된다.
| 경로 | 기능 | | 경로 | 기능 |
|------|------| |------|------|
| `/admin/bag-orders/*` | 발주 관리 (등록/상세/취소/삭제) | | `/bag/password-change` | 비밀번호 변경 |
| `/admin/bag-receivings/*` | 입고 관리 (등록, 재고 자동 반영) | | `/bag/designated-shops/*` | 지정판매소 관리 (CRUD) |
| `/admin/bag-inventory` | 재고 현황 조회 | | `/bag/bag-prices/*` | 봉투 단가 (CRUD + 이력) |
| `/bag/packaging-units/manage/*` | 포장 단위 (CRUD + 이력; 조회 전용은 `/bag/packaging-units`) |
**판매/주문/불출 (Phase 4)** | `/bag/sales-agencies/*` | 판매 대행소 (CRUD) |
| `/bag/managers/*` | 담당자 (CRUD) |
| 경로 | 기능 | | `/bag/companies/*` | 업체 (CRUD) |
|------|------| | `/bag/free-recipients/*` | 무료용 대상자 (CRUD) |
| `/admin/shop-orders/*` | 주문 접수 (등록/취소) | | `/bag/bag-orders/*` | 발주 관리 (등록/상세/취소/삭제) |
| `/admin/bag-sales/*` | 판매/반품 (등록) | | `/bag/bag-receivings/*` | 입고 관리 (등록, 재고 자동 반영) |
| `/admin/bag-issues/*` | 무료용 불출 (등록/취소, 재고 연동) | | `/bag/bag-inventory` | 재고 현황 조회 |
| `/bag/shop-orders/*` | 주문 접수 (등록/취소) |
**리포트 (Phase 5)** | `/bag/bag-sales/*` | 판매/반품 (등록) |
| `/bag/bag-issues/*` | 무료용 불출 (등록/취소, 재고 연동) |
| 경로 | 기능 | | `/bag/reports/sales-ledger` | 판매 대장 (일자별/기간별) |
|------|------| | `/bag/reports/daily-summary` | 일계표 (일계 + 월간 누계) |
| `/admin/reports/sales-ledger` | 판매 대장 (일자별/기간별) | | `/bag/reports/period-sales` | 기간별 판매현황 |
| `/admin/reports/daily-summary` | 일계표 (일계 + 월간 누계) | | `/bag/reports/supply-demand` | 봉투 수불 현황 |
| `/admin/reports/period-sales` | 기간별 판매현황 |
| `/admin/reports/supply-demand` | 봉투 수불 현황 |
--- ---
@@ -289,7 +285,7 @@ assets/ # 기획 문서 (엑셀)
| 항목 | 구현 | | 항목 | 구현 |
|------|------| |------|------|
| 인증 | 세션 기반 로그인, AdminAuthFilter로 관리자 접근 제어 | | 인증 | 세션 기반 로그인, AdminAuthFilter로 관리자 접근 제어, 기본코드 목록 경로는 LoginAuthFilter(`loginAuth`) |
| RBAC | 4단계 역할 (Config\Roles), 메뉴별 역할 노출 | | RBAC | 4단계 역할 (Config\Roles), 메뉴별 역할 노출 |
| PII 암호화 | `pii_encryption_helper` (AES, `ENC:` prefix) - 이메일/전화번호 | | PII 암호화 | `pii_encryption_helper` (AES, `ENC:` prefix) - 이메일/전화번호 |
| 비밀번호 | `password_hash()` + `password_verify()` (bcrypt) | | 비밀번호 | `password_hash()` + `password_verify()` (bcrypt) |
@@ -349,6 +345,8 @@ SQL 스크립트 실행 순서:
| 17 | `seed_test_accounts.sql` | 테스터 계정 4개 | | 17 | `seed_test_accounts.sql` | 테스터 계정 4개 |
| 18 | `seed_realistic_data.sql` | 실제형 시범 데이터 | | 18 | `seed_realistic_data.sql` | 실제형 시범 데이터 |
**기본코드 전용 보강 (선택·기존 DB):** `menu_fix_basic_code_link.sql`, `menu_site_add_basic_code_child.sql`, 개발목록 CSV 반영 시 `code_master_sync_from_csv.sql` 또는 `writable/tools/sync_basic_codes_from_csv.py`.
### 4) 개발 서버 실행 ### 4) 개발 서버 실행
```bash ```bash
@@ -384,6 +382,7 @@ npx playwright test -g "로그인 페이지"
| ID | 역할 | Level | | ID | 역할 | Level |
|----|------|-------| |----|------|-------|
| `tester_badmin` | 본부 관리자 | 5 |
| `tester_admin` | Super Admin | 4 | | `tester_admin` | Super Admin | 4 |
| `tester_local` | 지자체관리자 (중구청) | 3 | | `tester_local` | 지자체관리자 (중구청) | 3 |
| `tester_shop` | 지정판매소 | 2 | | `tester_shop` | 지정판매소 | 2 |
@@ -397,7 +396,7 @@ npx playwright test -g "로그인 페이지"
| admin.spec.js | 10 | 관리자 패널 접근 | | admin.spec.js | 10 | 관리자 패널 접근 |
| public.spec.js | 4 | 공개 페이지 | | public.spec.js | 4 | 공개 페이지 |
| bag-site.spec.js | 11 | 사이트 메뉴 /bag/* | | bag-site.spec.js | 11 | 사이트 메뉴 /bag/* |
| code-management.spec.js | 7 | 기본코드 CRUD | | code-management.spec.js | 7 | 기본코드 CRUD (`/bag/*` 목록 + `/admin/*` 폼) |
| bag-price.spec.js | 6 | 봉투 단가 | | bag-price.spec.js | 6 | 봉투 단가 |
| packaging-unit.spec.js | 3 | 포장 단위 | | packaging-unit.spec.js | 3 | 포장 단위 |
| phase2-entities.spec.js | 8 | 대행소/담당자/업체/무료대상자 | | phase2-entities.spec.js | 8 | 대행소/담당자/업체/무료대상자 |
@@ -410,6 +409,14 @@ npx playwright test -g "로그인 페이지"
## 기본코드 체계 ## 기본코드 체계
### 코드 관리 URL·동작
| 구분 | 경로 | 설명 |
|------|------|------|
| 목록·조회 | `/bag/code-kinds`, `/bag/code-details/{ck_idx}` | 사이트(`bag`) 레이아웃. 시민·판매소는 열람만; 코드 마스터 관리 권한이 있으면 CRUD용 링크(관리자 화면) 노출 |
| 등록·수정·삭제 | `/admin/code-kinds/*`, `/admin/code-details/*` | `adminAuth` + `canManageCodeMaster`. 처리 후 flash 메시지와 함께 위 `bag` 목록으로 되돌아감 |
| 데이터 보강 (선택) | `writable/tools/sync_basic_codes_from_csv.py` → 생성 SQL 또는 `code_master_sync_from_csv.sql` | 개발목록 CSV와 DB 종류·세부코드 맞출 때 참고 |
A~T 총 20종의 코드 체계 (`code_kind` + `code_detail`): A~T 총 20종의 코드 체계 (`code_kind` + `code_detail`):
| 코드 | 코드명 | 코드 | 코드명 | | 코드 | 코드명 | 코드 | 코드명 |
@@ -473,6 +480,9 @@ A~T 총 20종의 코드 체계 (`code_kind` + `code_detail`):
| `menu_site_seed_from_csv.sql` | 사이트 네비게이션 시드 | | `menu_site_seed_from_csv.sql` | 사이트 네비게이션 시드 |
| `local_government_init_daegu.sql` | 대구 8개 구군 지자체 | | `local_government_init_daegu.sql` | 대구 8개 구군 지자체 |
| `code_master_init_daegu.sql` | 기본코드 20종 + 세부코드 | | `code_master_init_daegu.sql` | 기본코드 20종 + 세부코드 |
| `menu_fix_basic_code_link.sql` | 사이트 메뉴에서 기본코드 링크 보정 (기존 DB용, 선택) |
| `menu_site_add_basic_code_child.sql` | 사이트 메뉴에 기본코드 하위 항목 (선택) |
| `code_master_sync_from_csv.sql` | CSV 기준 기본코드 보강 (선택) |
| `bag_price_tables.sql` | bag_price, bag_price_history | | `bag_price_tables.sql` | bag_price, bag_price_history |
| `packaging_unit_tables.sql` | packaging_unit, packaging_unit_history | | `packaging_unit_tables.sql` | packaging_unit, packaging_unit_history |
| `sales_agency_tables.sql` | sales_agency | | `sales_agency_tables.sql` | sales_agency |

View File

@@ -40,7 +40,7 @@ class App extends BaseConfig
* something else. If you have configured your web server to remove this file * something else. If you have configured your web server to remove this file
* from your site URIs, set this variable to an empty string. * from your site URIs, set this variable to an empty string.
*/ */
public string $indexPage = 'index.php'; public string $indexPage = '';
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------

View File

@@ -7,17 +7,19 @@ use CodeIgniter\Config\BaseConfig;
/** /**
* 로그인·2차 인증(TOTP) 관련 설정 * 로그인·2차 인증(TOTP) 관련 설정
* *
* .env 예: * .env 의 auth.requireTotp 가 Config 기본값보다 우선합니다. 끄려면 반드시 false 로 두세요.
* auth.requireTotp = true * 예:
* auth.totpIssuer = "쓰레기봉투 물류시스템" * auth.requireTotp = false
* auth.requireTotp = true # 운영에서 2FA 켤 때
* auth.totpIssuer = "종량제 시스템"
*/ */
class Auth extends BaseConfig class Auth extends BaseConfig
{ {
/** 운영·스테이징 true 권장. 로컬 개발 시 false 로 1단계만 로그인 가능 */ /** false 이면 로그인 시 TOTP·등록 유도 없음. 운영에서 켤 때 .env 에 auth.requireTotp = true */
public bool $requireTotp = true; public bool $requireTotp = false;
/** 인증 앱에 표시되는 발급자(issuer) */ /** 인증 앱에 표시되는 발급자(issuer) */
public string $totpIssuer = '쓰레기봉투 물류시스템'; public string $totpIssuer = '종량제 시스템';
/** TOTP 연속 실패 시 세션 종료 전 허용 횟수 */ /** TOTP 연속 실패 시 세션 종료 전 허용 횟수 */
public int $totpMaxAttempts = 5; public int $totpMaxAttempts = 5;

View File

@@ -27,8 +27,40 @@ class Encryption extends BaseConfig
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
$hex = (string) env('encryption.key', ''); $hex = trim((string) env('encryption.key', ''));
if (
(str_starts_with($hex, "'") && str_ends_with($hex, "'"))
|| (str_starts_with($hex, '"') && str_ends_with($hex, '"'))
) {
$hex = substr($hex, 1, -1);
}
$this->key = (strlen($hex) === 64 && ctype_xdigit($hex)) ? hex2bin($hex) : ''; $this->key = (strlen($hex) === 64 && ctype_xdigit($hex)) ? hex2bin($hex) : '';
$prev = trim((string) env('encryption.previousKeys', ''));
if ($prev !== '') {
$parsed = [];
$parts = array_map('trim', explode(',', $prev));
foreach ($parts as $part) {
if ($part === '') {
continue;
}
if (str_starts_with($part, 'hex2bin:')) {
$part = substr($part, 8);
}
if (
(str_starts_with($part, "'") && str_ends_with($part, "'"))
|| (str_starts_with($part, '"') && str_ends_with($part, '"'))
) {
$part = substr($part, 1, -1);
}
if (strlen($part) === 64 && ctype_xdigit($part)) {
$parsed[] = 'hex2bin:' . $part;
}
}
if (! empty($parsed)) {
$this->previousKeys = $parsed;
}
}
} }
/** /**

View File

@@ -26,6 +26,7 @@ class Filters extends BaseFilters
*/ */
public array $aliases = [ public array $aliases = [
'adminAuth' => \App\Filters\AdminAuthFilter::class, 'adminAuth' => \App\Filters\AdminAuthFilter::class,
'loginAuth' => \App\Filters\LoginAuthFilter::class,
'csrf' => CSRF::class, 'csrf' => CSRF::class,
'toolbar' => DebugToolbar::class, 'toolbar' => DebugToolbar::class,
'honeypot' => Honeypot::class, 'honeypot' => Honeypot::class,

26
app/Config/Kakao.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
/**
* 카카오 Developers — 내 애플리케이션 — 앱 키 — JavaScript 키
* .env 예: kakao.javascriptKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
*/
class Kakao extends BaseConfig
{
public string $javascriptKey = '';
public function __construct()
{
parent::__construct();
$v = env('kakao.javascriptKey');
if (is_string($v) && $v !== '') {
$this->javascriptKey = $v;
}
}
}

View File

@@ -42,6 +42,52 @@ class Roles extends BaseConfig
return $level === self::LEVEL_SUPER_ADMIN || $level === self::LEVEL_HEADQUARTERS_ADMIN; return $level === self::LEVEL_SUPER_ADMIN || $level === self::LEVEL_HEADQUARTERS_ADMIN;
} }
/**
* 기본코드(종류·세부) 등록·수정·삭제 가능 (super admin(4) · 본부 관리자(5)만)
*/
public static function canManageCodeMaster(int $level): bool
{
return self::isSuperAdminEquivalent($level);
}
/**
* 기본코드 종류(code_kind) CRUD — super·본부만
*/
public static function canManageCodeKindMaster(int $level): bool
{
return self::isSuperAdminEquivalent($level);
}
/**
* 플랫폼 공통 세부코드(CSV·시드) 수정·삭제 — super·본부만
*/
public static function canEditPlatformCodeDetail(int $level): bool
{
return self::isSuperAdminEquivalent($level);
}
/**
* 세부코드 행 단위 수정/삭제 가능 여부
*
* @param object $row code_detail (cd_source, cd_lg_idx)
*/
public static function canEditCodeDetailRow(int $level, object $row, ?int $adminEffectiveLgIdx): bool
{
if (! self::canManageCodeMaster($level)) {
return false;
}
$src = $row->cd_source ?? 'platform';
$lg = (int) ($row->cd_lg_idx ?? 0);
if ($src === 'platform' && $lg === 0) {
return self::canEditPlatformCodeDetail($level);
}
if (self::isSuperAdminEquivalent($level)) {
return true;
}
return $adminEffectiveLgIdx !== null && $adminEffectiveLgIdx > 0 && $lg === $adminEffectiveLgIdx;
}
/** /**
* TOTP 2차 인증 적용 대상 (지자체·super·본부 관리자) * TOTP 2차 인증 적용 대상 (지자체·super·본부 관리자)
*/ */

View File

@@ -17,9 +17,27 @@ $routes->get('bag/waste-suibal-enterprise', 'Home::wasteSuibalEnterprise');
// 사이트 메뉴 (/bag/*) // 사이트 메뉴 (/bag/*)
$routes->get('bag/basic-info', 'Bag::basicInfo'); $routes->get('bag/basic-info', 'Bag::basicInfo');
$routes->get('bag/prices', 'Bag::prices');
$routes->post('bag/prices', 'Bag::prices');
$routes->get('bag/packaging-units', 'Bag::packagingUnits');
$routes->get('bag/code-kinds', 'Bag::codeKinds');
$routes->get('bag/code-details/(:num)', 'Bag::codeDetails/$1');
// 옛 주소 호환: 세부 목록만 사이트로 이동
$routes->get('admin/code-details/(:num)', 'Admin\CodeDetail::index/$1');
$routes->get('bag/purchase-inbound', 'Bag::purchaseInbound'); $routes->get('bag/purchase-inbound', 'Bag::purchaseInbound');
$routes->get('bag/issue', 'Bag::issue'); $routes->get('bag/issue', 'Bag::issueLegacy');
$routes->get('bag/issue/cancel', 'Bag::issue');
$routes->get('bag/inventory', 'Bag::inventory'); $routes->get('bag/inventory', 'Bag::inventory');
$routes->get('bag/inventory/export', 'Bag::inventoryExport');
$routes->get('bag/inventory/inspection-select', 'Bag::inspectionSelect');
$routes->get('bag/inventory/inspection-work', 'Bag::inspectionWork');
$routes->post('bag/inventory/inspection-run', 'Bag::inspectionRun');
$routes->post('bag/inventory/inspection-select/save', 'Bag::inspectionSelectSave');
$routes->post('bag/inventory/inspection-select/confirm', 'Bag::inspectionSelectConfirm');
$routes->get('bag/inventory/inspection/(:num)', 'Bag::inspectionDetail/$1');
$routes->post('bag/inventory/inspection/(:num)/save', 'Bag::inspectionSave/$1');
$routes->post('bag/inventory/inspection/(:num)/apply', 'Bag::inspectionApply/$1');
$routes->get('bag/sales', 'Bag::sales'); $routes->get('bag/sales', 'Bag::sales');
$routes->get('bag/sales-stats', 'Bag::salesStats'); $routes->get('bag/sales-stats', 'Bag::salesStats');
$routes->get('bag/flow', 'Bag::flow'); $routes->get('bag/flow', 'Bag::flow');
@@ -33,15 +51,138 @@ $routes->post('bag/inventory/adjust', 'Bag::inventoryAdjustStore');
$routes->get('bag/issue/create', 'Bag::issueCreate'); $routes->get('bag/issue/create', 'Bag::issueCreate');
$routes->post('bag/issue/store', 'Bag::issueStore'); $routes->post('bag/issue/store', 'Bag::issueStore');
$routes->post('bag/issue/cancel/(:num)', 'Bag::issueCancel/$1'); $routes->post('bag/issue/cancel/(:num)', 'Bag::issueCancel/$1');
$routes->post('bag/issue/cancel-save', 'Bag::issueCancelSave');
$routes->get('bag/order/create', 'Bag::orderCreate'); $routes->get('bag/order/create', 'Bag::orderCreate');
$routes->get('bag/order/change', 'Bag::orderChange');
$routes->get('bag/order/revise/(:num)', 'Bag::orderRevise/$1');
$routes->get('bag/order/lot-seed', 'Bag::orderLotSeed');
$routes->post('bag/order/lot-seed/generate', 'Bag::orderLotSeedGenerate');
$routes->post('bag/order/store', 'Bag::orderStore'); $routes->post('bag/order/store', 'Bag::orderStore');
$routes->post('bag/order/cancel/(:num)', 'Bag::orderCancel/$1');
$routes->post('bag/order/delete', 'Bag::orderDeletePost');
$routes->post('bag/order/delete/(:num)', 'Bag::orderDelete/$1');
$routes->get('bag/receiving/create', 'Bag::receivingCreate'); $routes->get('bag/receiving/create', 'Bag::receivingCreate');
$routes->post('bag/receiving/store', 'Bag::receivingStore'); $routes->post('bag/receiving/store', 'Bag::receivingStore');
$routes->get('bag/receiving/scanner', 'Bag::receivingScanner');
$routes->post('bag/receiving/scanner/store', 'Bag::receivingScannerStore');
$routes->get('bag/receiving/batch', 'Bag::receivingBatch');
$routes->post('bag/receiving/batch/store', 'Bag::receivingBatchStore');
$routes->get('bag/receiving/status', 'Bag::receivingStatus');
$routes->get('bag/receiving/status/export', 'Bag::receivingStatusExport');
$routes->get('bag/sale/create', 'Bag::saleCreate'); $routes->get('bag/sale/create', 'Bag::saleCreate');
$routes->post('bag/sale/store', 'Bag::saleStore'); $routes->post('bag/sale/store', 'Bag::saleStore');
$routes->get('bag/shop-order/create', 'Bag::shopOrderCreate'); $routes->get('bag/shop-order/create', 'Bag::shopOrderCreate');
$routes->post('bag/shop-order/store', 'Bag::shopOrderStore'); $routes->post('bag/shop-order/store', 'Bag::shopOrderStore');
// 메인 사이트 메뉴용 업무 URL (관리자 권한). 동일 컨트롤러가 URI 가 bag 이면 메인 사이트 레이아웃으로 렌더.
$routes->group('bag', ['filter' => 'adminAuth'], static function ($routes): void {
$routes->get('managers', 'Admin\Manager::index');
$routes->get('managers/create', 'Admin\Manager::create');
$routes->post('managers/store', 'Admin\Manager::store');
$routes->get('managers/edit/(:num)', 'Admin\Manager::edit/$1');
$routes->post('managers/update/(:num)', 'Admin\Manager::update/$1');
$routes->post('managers/delete/(:num)', 'Admin\Manager::delete/$1');
$routes->get('sales-agencies', 'Admin\SalesAgency::index');
$routes->get('sales-agencies/create', 'Admin\SalesAgency::create');
$routes->post('sales-agencies/store', 'Admin\SalesAgency::store');
$routes->get('sales-agencies/edit/(:num)', 'Admin\SalesAgency::edit/$1');
$routes->post('sales-agencies/update/(:num)', 'Admin\SalesAgency::update/$1');
$routes->post('sales-agencies/delete/(:num)', 'Admin\SalesAgency::delete/$1');
$routes->get('companies', 'Admin\Company::index');
$routes->get('companies/create', 'Admin\Company::create');
$routes->post('companies/store', 'Admin\Company::store');
$routes->get('companies/edit/(:num)', 'Admin\Company::edit/$1');
$routes->post('companies/update/(:num)', 'Admin\Company::update/$1');
$routes->post('companies/delete/(:num)', 'Admin\Company::delete/$1');
$routes->get('free-recipients', 'Admin\FreeRecipient::index');
$routes->get('free-recipients/create', 'Admin\FreeRecipient::create');
$routes->post('free-recipients/store', 'Admin\FreeRecipient::store');
$routes->get('free-recipients/edit/(:num)', 'Admin\FreeRecipient::edit/$1');
$routes->post('free-recipients/update/(:num)', 'Admin\FreeRecipient::update/$1');
$routes->post('free-recipients/delete/(:num)', 'Admin\FreeRecipient::delete/$1');
$routes->get('designated-shops/export', 'Admin\DesignatedShop::export');
$routes->get('designated-shops/map', 'Admin\DesignatedShop::map');
$routes->get('designated-shops/status/export', 'Admin\DesignatedShop::statusExport');
$routes->get('designated-shops/status', 'Admin\DesignatedShop::status');
$routes->get('designated-shops/barcode', 'Admin\DesignatedShop::barcode');
$routes->post('designated-shops/barcode/print', 'Admin\DesignatedShop::barcodePrint');
$routes->get('designated-shops/district-new-cancel/export', 'Admin\DesignatedShop::districtNewCancelExport');
$routes->get('designated-shops/district-new-cancel', 'Admin\DesignatedShop::districtNewCancel');
$routes->get('designated-shops/browse', 'Admin\DesignatedShop::browse');
$routes->get('designated-shops', 'Admin\DesignatedShop::index');
$routes->get('designated-shops/create', 'Admin\DesignatedShop::create');
$routes->post('designated-shops/store', 'Admin\DesignatedShop::store');
$routes->get('designated-shops/edit/(:num)', 'Admin\DesignatedShop::edit/$1');
$routes->post('designated-shops/update/(:num)', 'Admin\DesignatedShop::update/$1');
$routes->post('designated-shops/delete/(:num)', 'Admin\DesignatedShop::delete/$1');
$routes->get('bag-prices', 'Admin\BagPrice::index');
$routes->get('bag-prices/create', 'Admin\BagPrice::create');
$routes->post('bag-prices/store', 'Admin\BagPrice::store');
$routes->get('bag-prices/edit/(:num)', 'Admin\BagPrice::edit/$1');
$routes->post('bag-prices/update/(:num)', 'Admin\BagPrice::update/$1');
$routes->post('bag-prices/delete/(:num)', 'Admin\BagPrice::delete/$1');
$routes->get('bag-prices/history/(:num)', 'Admin\BagPrice::history/$1');
$routes->get('bag-orders/export', 'Admin\BagOrder::export');
$routes->get('bag-orders', 'Admin\BagOrder::index');
$routes->get('bag-orders/create', 'Admin\BagOrder::create');
$routes->get('bag-orders/revise/(:num)', 'Admin\BagOrder::revise/$1');
$routes->post('bag-orders/store', 'Admin\BagOrder::store');
$routes->get('bag-orders/detail/(:num)', 'Admin\BagOrder::detail/$1');
$routes->post('bag-orders/cancel/(:num)', 'Admin\BagOrder::cancel/$1');
$routes->post('bag-orders/delete/(:num)', 'Admin\BagOrder::delete/$1');
$routes->get('bag-receivings', 'Admin\BagReceiving::index');
$routes->get('bag-receivings/create', 'Admin\BagReceiving::create');
$routes->post('bag-receivings/store', 'Admin\BagReceiving::store');
$routes->get('bag-inventory/export', 'Admin\BagInventory::export');
$routes->get('bag-inventory', 'Admin\BagInventory::index');
$routes->get('shop-orders', 'Admin\ShopOrder::index');
$routes->get('shop-orders/create', 'Admin\ShopOrder::create');
$routes->post('shop-orders/store', 'Admin\ShopOrder::store');
$routes->post('shop-orders/cancel/(:num)', 'Admin\ShopOrder::cancel/$1');
$routes->get('bag-sales/export', 'Admin\BagSale::export');
$routes->get('bag-sales', 'Admin\BagSale::index');
$routes->get('bag-sales/create', 'Admin\BagSale::create');
$routes->post('bag-sales/store', 'Admin\BagSale::store');
$routes->get('bag-issues', 'Admin\BagIssue::index');
$routes->get('bag-issues/create', 'Admin\BagIssue::create');
$routes->post('bag-issues/store', 'Admin\BagIssue::store');
$routes->post('bag-issues/cancel/(:num)', 'Admin\BagIssue::cancel/$1');
$routes->get('packaging-units/manage', 'Admin\PackagingUnit::index');
$routes->get('packaging-units/manage/create', 'Admin\PackagingUnit::create');
$routes->post('packaging-units/manage/store', 'Admin\PackagingUnit::store');
$routes->get('packaging-units/manage/edit/(:num)', 'Admin\PackagingUnit::edit/$1');
$routes->post('packaging-units/manage/update/(:num)', 'Admin\PackagingUnit::update/$1');
$routes->post('packaging-units/manage/delete/(:num)', 'Admin\PackagingUnit::delete/$1');
$routes->get('packaging-units/manage/history/(:num)', 'Admin\PackagingUnit::history/$1');
$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');
$routes->get('reports/yearly-sales', 'Admin\SalesReport::yearlySales');
$routes->get('reports/shop-sales', 'Admin\SalesReport::shopSales');
$routes->get('reports/hometax-export', 'Admin\SalesReport::hometaxExport');
$routes->get('reports/returns', 'Admin\SalesReport::returns');
$routes->get('reports/lot-flow', 'Admin\SalesReport::lotFlow');
$routes->get('reports/misc-flow', 'Admin\SalesReport::miscFlow');
$routes->post('reports/misc-flow', 'Admin\SalesReport::miscFlowStore');
$routes->get('password-change', 'Admin\PasswordChange::index');
$routes->post('password-change', 'Admin\PasswordChange::update');
});
// Auth // Auth
$routes->get('login', 'Auth::showLoginForm'); $routes->get('login', 'Auth::showLoginForm');
$routes->post('login', 'Auth::login'); $routes->post('login', 'Auth::login');
@@ -63,6 +204,7 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo
$routes->post('users/store', 'Admin\User::store'); $routes->post('users/store', 'Admin\User::store');
$routes->get('users/edit/(:num)', 'Admin\User::edit/$1'); $routes->get('users/edit/(:num)', 'Admin\User::edit/$1');
$routes->post('users/update/(:num)', 'Admin\User::update/$1'); $routes->post('users/update/(:num)', 'Admin\User::update/$1');
$routes->post('users/unlock-login/(:num)', 'Admin\User::unlockLogin/$1');
$routes->post('users/delete/(:num)', 'Admin\User::delete/$1'); $routes->post('users/delete/(:num)', 'Admin\User::delete/$1');
$routes->get('access/login-history', 'Admin\Access::loginHistory'); $routes->get('access/login-history', 'Admin\Access::loginHistory');
$routes->get('access/approvals', 'Admin\Access::approvals'); $routes->get('access/approvals', 'Admin\Access::approvals');
@@ -84,12 +226,7 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo
$routes->post('local-governments/update/(:num)', 'Admin\LocalGovernment::update/$1'); $routes->post('local-governments/update/(:num)', 'Admin\LocalGovernment::update/$1');
$routes->post('local-governments/delete/(:num)', 'Admin\LocalGovernment::delete/$1'); $routes->post('local-governments/delete/(:num)', 'Admin\LocalGovernment::delete/$1');
// 비밀번호 변경 (P2-20) // 기본코드 종류 관리 (P2-01) — 등록·수정·삭제는 관리자 전용
$routes->get('password-change', 'Admin\PasswordChange::index');
$routes->post('password-change', 'Admin\PasswordChange::update');
// 기본코드 종류 관리 (P2-01)
$routes->get('code-kinds', 'Admin\CodeKind::index');
$routes->get('code-kinds/create', 'Admin\CodeKind::create'); $routes->get('code-kinds/create', 'Admin\CodeKind::create');
$routes->post('code-kinds/store', 'Admin\CodeKind::store'); $routes->post('code-kinds/store', 'Admin\CodeKind::store');
$routes->get('code-kinds/edit/(:num)', 'Admin\CodeKind::edit/$1'); $routes->get('code-kinds/edit/(:num)', 'Admin\CodeKind::edit/$1');
@@ -97,119 +234,32 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo
$routes->post('code-kinds/delete/(:num)', 'Admin\CodeKind::delete/$1'); $routes->post('code-kinds/delete/(:num)', 'Admin\CodeKind::delete/$1');
// 세부코드 관리 (P2-02) // 세부코드 관리 (P2-02)
$routes->get('code-details/(:num)', 'Admin\CodeDetail::index/$1');
$routes->get('code-details/(:num)/create', 'Admin\CodeDetail::create/$1'); $routes->get('code-details/(:num)/create', 'Admin\CodeDetail::create/$1');
$routes->post('code-details/store', 'Admin\CodeDetail::store'); $routes->post('code-details/store', 'Admin\CodeDetail::store');
$routes->get('code-details/edit/(:num)', 'Admin\CodeDetail::edit/$1'); $routes->get('code-details/edit/(:num)', 'Admin\CodeDetail::edit/$1');
$routes->post('code-details/update/(:num)', 'Admin\CodeDetail::update/$1'); $routes->post('code-details/update/(:num)', 'Admin\CodeDetail::update/$1');
$routes->post('code-details/delete/(:num)', 'Admin\CodeDetail::delete/$1'); $routes->post('code-details/delete/(:num)', 'Admin\CodeDetail::delete/$1');
// 봉투 단가 관리 (P2-03/04) // 구 업무 URL → /bag/* (실제 처리는 bag 그룹). GET 301, POST 307.
$routes->get('bag-prices', 'Admin\BagPrice::index'); $adminToBagPrefixes = [
$routes->get('bag-prices/create', 'Admin\BagPrice::create'); 'managers',
$routes->post('bag-prices/store', 'Admin\BagPrice::store'); 'sales-agencies',
$routes->get('bag-prices/edit/(:num)', 'Admin\BagPrice::edit/$1'); 'companies',
$routes->post('bag-prices/update/(:num)', 'Admin\BagPrice::update/$1'); 'free-recipients',
$routes->post('bag-prices/delete/(:num)', 'Admin\BagPrice::delete/$1'); 'designated-shops',
$routes->get('bag-prices/history/(:num)', 'Admin\BagPrice::history/$1'); 'bag-prices',
'bag-orders',
// 발주 관리 (P3-01~05) 'bag-receivings',
$routes->get('bag-orders/export', 'Admin\BagOrder::export'); 'bag-inventory',
$routes->get('bag-orders', 'Admin\BagOrder::index'); 'shop-orders',
$routes->get('bag-orders/create', 'Admin\BagOrder::create'); 'bag-sales',
$routes->post('bag-orders/store', 'Admin\BagOrder::store'); 'bag-issues',
$routes->get('bag-orders/detail/(:num)', 'Admin\BagOrder::detail/$1'); 'packaging-units',
$routes->post('bag-orders/cancel/(:num)', 'Admin\BagOrder::cancel/$1'); 'reports',
$routes->post('bag-orders/delete/(:num)', 'Admin\BagOrder::delete/$1'); 'password-change',
];
// 입고 관리 (P3-06~09) foreach ($adminToBagPrefixes as $p) {
$routes->get('bag-receivings', 'Admin\BagReceiving::index'); $routes->match(['get', 'post'], $p, 'Admin\WorkMovedToBag::toBag/' . $p);
$routes->get('bag-receivings/create', 'Admin\BagReceiving::create'); $routes->match(['get', 'post'], $p . '/(:any)', 'Admin\WorkMovedToBag::toBag/' . $p . '/$1');
$routes->post('bag-receivings/store', 'Admin\BagReceiving::store'); }
// 재고 현황 (P3-10)
$routes->get('bag-inventory/export', 'Admin\BagInventory::export');
$routes->get('bag-inventory', 'Admin\BagInventory::index');
// 주문 접수 관리 (P4-01~03)
$routes->get('shop-orders', 'Admin\ShopOrder::index');
$routes->get('shop-orders/create', 'Admin\ShopOrder::create');
$routes->post('shop-orders/store', 'Admin\ShopOrder::store');
$routes->post('shop-orders/cancel/(:num)', 'Admin\ShopOrder::cancel/$1');
// 판매/반품 관리 (P4-04~07)
$routes->get('bag-sales/export', 'Admin\BagSale::export');
$routes->get('bag-sales', 'Admin\BagSale::index');
$routes->get('bag-sales/create', 'Admin\BagSale::create');
$routes->post('bag-sales/store', 'Admin\BagSale::store');
// 무료용 불출 관리 (P4-08~10)
$routes->get('bag-issues', 'Admin\BagIssue::index');
$routes->get('bag-issues/create', 'Admin\BagIssue::create');
$routes->post('bag-issues/store', 'Admin\BagIssue::store');
$routes->post('bag-issues/cancel/(:num)', 'Admin\BagIssue::cancel/$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');
// 현황/리포트 (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');
$routes->get('reports/yearly-sales', 'Admin\SalesReport::yearlySales');
$routes->get('reports/shop-sales', 'Admin\SalesReport::shopSales');
$routes->get('reports/hometax-export', 'Admin\SalesReport::hometaxExport');
$routes->get('reports/returns', 'Admin\SalesReport::returns');
$routes->get('reports/lot-flow', 'Admin\SalesReport::lotFlow');
$routes->get('reports/misc-flow', 'Admin\SalesReport::miscFlow');
$routes->post('reports/misc-flow', 'Admin\SalesReport::miscFlowStore');
// 판매 대행소 관리 (P2-07/08)
$routes->get('sales-agencies', 'Admin\SalesAgency::index');
$routes->get('sales-agencies/create', 'Admin\SalesAgency::create');
$routes->post('sales-agencies/store', 'Admin\SalesAgency::store');
$routes->get('sales-agencies/edit/(:num)', 'Admin\SalesAgency::edit/$1');
$routes->post('sales-agencies/update/(:num)', 'Admin\SalesAgency::update/$1');
$routes->post('sales-agencies/delete/(:num)', 'Admin\SalesAgency::delete/$1');
// 담당자 관리 (P2-09/10)
$routes->get('managers', 'Admin\Manager::index');
$routes->get('managers/create', 'Admin\Manager::create');
$routes->post('managers/store', 'Admin\Manager::store');
$routes->get('managers/edit/(:num)', 'Admin\Manager::edit/$1');
$routes->post('managers/update/(:num)', 'Admin\Manager::update/$1');
$routes->post('managers/delete/(:num)', 'Admin\Manager::delete/$1');
// 업체 관리 (P2-11/12)
$routes->get('companies', 'Admin\Company::index');
$routes->get('companies/create', 'Admin\Company::create');
$routes->post('companies/store', 'Admin\Company::store');
$routes->get('companies/edit/(:num)', 'Admin\Company::edit/$1');
$routes->post('companies/update/(:num)', 'Admin\Company::update/$1');
$routes->post('companies/delete/(:num)', 'Admin\Company::delete/$1');
// 무료용 대상자 관리 (P2-13/14)
$routes->get('free-recipients', 'Admin\FreeRecipient::index');
$routes->get('free-recipients/create', 'Admin\FreeRecipient::create');
$routes->post('free-recipients/store', 'Admin\FreeRecipient::store');
$routes->get('free-recipients/edit/(:num)', 'Admin\FreeRecipient::edit/$1');
$routes->post('free-recipients/update/(:num)', 'Admin\FreeRecipient::update/$1');
$routes->post('free-recipients/delete/(:num)', 'Admin\FreeRecipient::delete/$1');
$routes->get('designated-shops/export', 'Admin\DesignatedShop::export');
$routes->get('designated-shops/map', 'Admin\DesignatedShop::map');
$routes->get('designated-shops/status', 'Admin\DesignatedShop::status');
$routes->get('designated-shops', 'Admin\DesignatedShop::index');
$routes->get('designated-shops/create', 'Admin\DesignatedShop::create');
$routes->post('designated-shops/store', 'Admin\DesignatedShop::store');
$routes->get('designated-shops/edit/(:num)', 'Admin\DesignatedShop::edit/$1');
$routes->post('designated-shops/update/(:num)', 'Admin\DesignatedShop::update/$1');
$routes->post('designated-shops/delete/(:num)', 'Admin\DesignatedShop::delete/$1');
}); });

View File

@@ -11,24 +11,23 @@ class BagInventory extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
}
$invModel = model(BagInventoryModel::class); $invModel = model(BagInventoryModel::class);
$list = $invModel->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->paginate(20); $list = $invModel->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->paginate(20);
$pager = $invModel->pager; $pager = $invModel->pager;
return view('admin/layout', [ return $this->renderWorkPage('재고 현황', 'admin/bag_inventory/index', ['list' => $list, 'pager' => $pager]);
'title' => '재고 현황',
'content' => view('admin/bag_inventory/index', ['list' => $list, 'pager' => $pager]),
]);
} }
public function export() public function export()
{ {
helper(['admin', 'export']); helper(['admin', 'export']);
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) { if (! $lgIdx) {
return redirect()->to(site_url('admin/bag-inventory'))->with('error', '지자체를 선택해 주세요.'); return redirect()->to(mgmt_url('bag-inventory'))->with('error', '지자체를 선택해 주세요.');
} }
$list = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->findAll(); $list = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->findAll();

View File

@@ -21,33 +21,34 @@ class BagIssue extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
}
$builder = $this->issueModel->where('bi2_lg_idx', $lgIdx); $builder = $this->issueModel->where('bi2_lg_idx', $lgIdx);
$startDate = $this->request->getGet('start_date'); $startDate = $this->request->getGet('start_date');
$endDate = $this->request->getGet('end_date'); $endDate = $this->request->getGet('end_date');
if ($startDate) $builder->where('bi2_issue_date >=', $startDate); if ($startDate) {
if ($endDate) $builder->where('bi2_issue_date <=', $endDate); $builder->where('bi2_issue_date >=', $startDate);
}
if ($endDate) {
$builder->where('bi2_issue_date <=', $endDate);
}
$list = $builder->orderBy('bi2_issue_date', 'DESC')->orderBy('bi2_idx', 'DESC')->paginate(20); $list = $builder->orderBy('bi2_issue_date', 'DESC')->orderBy('bi2_idx', 'DESC')->paginate(20);
$pager = $this->issueModel->pager; $pager = $this->issueModel->pager;
return view('admin/layout', [ return $this->renderWorkPage('무료용 불출 관리', 'admin/bag_issue/index', compact('list', 'startDate', 'endDate', 'pager'));
'title' => '무료용 불출 관리',
'content' => view('admin/bag_issue/index', compact('list', 'startDate', 'endDate', 'pager')),
]);
} }
public function create() public function create()
{ {
helper('admin'); helper('admin');
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $lgIdx = admin_effective_lg_idx();
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true) : []; $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
return view('admin/layout', [ return $this->renderWorkPage('무료용 불출 처리', 'admin/bag_issue/create', compact('bagCodes'));
'title' => '무료용 불출 처리',
'content' => view('admin/bag_issue/create', compact('bagCodes')),
]);
} }
public function store() public function store()
@@ -71,8 +72,8 @@ class BagIssue extends BaseController
$bagCode = $this->request->getPost('bi2_bag_code'); $bagCode = $this->request->getPost('bi2_bag_code');
$qty = (int) $this->request->getPost('bi2_qty'); $qty = (int) $this->request->getPost('bi2_qty');
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
$detail = $kindO ? model(CodeDetailModel::class)->where('cd_ck_idx', $kindO->ck_idx)->where('cd_code', $bagCode)->first() : null; $detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $bagCode, $lgIdx) : null;
$bagName = $detail ? $detail->cd_name : ''; $bagName = $detail ? $detail->cd_name : '';
$db = \Config\Database::connect(); $db = \Config\Database::connect();
@@ -95,24 +96,22 @@ class BagIssue extends BaseController
$this->issueModel->insert($issueData); $this->issueModel->insert($issueData);
$bi2Idx = (int) $this->issueModel->getInsertID(); $bi2Idx = (int) $this->issueModel->getInsertID();
// CT-05: 감사 로그
helper('audit'); helper('audit');
audit_log('create', 'bag_issue', $bi2Idx, null, array_merge($issueData, ['bi2_idx' => $bi2Idx])); audit_log('create', 'bag_issue', $bi2Idx, null, array_merge($issueData, ['bi2_idx' => $bi2Idx]));
// 재고 감산
model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $bagName, -$qty); model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $bagName, -$qty);
$db->transComplete(); $db->transComplete();
return redirect()->to(site_url('admin/bag-issues'))->with('success', '불출 처리되었습니다.'); return redirect()->to(mgmt_url('bag-issues'))->with('success', '불출 처리되었습니다.');
} }
public function cancel(int $id) public function cancel(int $id)
{ {
helper('admin'); helper('admin');
$item = $this->issueModel->find($id); $item = $this->issueModel->find($id);
if (!$item || (int) $item->bi2_lg_idx !== admin_effective_lg_idx()) { if (! $item || (int) $item->bi2_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/bag-issues'))->with('error', '불출 내역을 찾을 수 없습니다.'); return redirect()->to(mgmt_url('bag-issues'))->with('error', '불출 내역을 찾을 수 없습니다.');
} }
$db = \Config\Database::connect(); $db = \Config\Database::connect();
@@ -120,14 +119,12 @@ class BagIssue extends BaseController
$before = (array) $item; $before = (array) $item;
$this->issueModel->update($id, ['bi2_status' => 'cancelled']); $this->issueModel->update($id, ['bi2_status' => 'cancelled']);
// CT-05: 감사 로그
helper('audit'); helper('audit');
audit_log('update', 'bag_issue', $id, $before, ['bi2_status' => 'cancelled']); audit_log('update', 'bag_issue', $id, $before, ['bi2_status' => 'cancelled']);
// 재고 복원
model(BagInventoryModel::class)->adjustQty((int) $item->bi2_lg_idx, $item->bi2_bag_code, $item->bi2_bag_name, (int) $item->bi2_qty); model(BagInventoryModel::class)->adjustQty((int) $item->bi2_lg_idx, $item->bi2_bag_code, $item->bi2_bag_name, (int) $item->bi2_qty);
$db->transComplete(); $db->transComplete();
return redirect()->to(site_url('admin/bag-issues'))->with('success', '불출이 취소되었습니다.'); return redirect()->to(mgmt_url('bag-issues'))->with('success', '불출이 취소되었습니다.');
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,10 @@
namespace App\Controllers\Admin; namespace App\Controllers\Admin;
use App\Controllers\BaseController; use App\Controllers\BaseController;
use App\Models\BagPriceModel;
use App\Models\BagPriceHistoryModel; use App\Models\BagPriceHistoryModel;
use App\Models\CodeKindModel; use App\Models\BagPriceModel;
use App\Models\CodeDetailModel; use App\Models\CodeDetailModel;
use App\Models\CodeKindModel;
class BagPrice extends BaseController class BagPrice extends BaseController
{ {
@@ -23,36 +23,147 @@ class BagPrice extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) { if ($lgIdx === null) {
return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
} }
$get = $this->request->getGet();
$readSrc = static function (array $src, string $key): ?string {
if (! array_key_exists($key, $src)) {
return null;
}
$v = $src[$key];
if ($v === null || is_array($v)) {
return null;
}
$s = trim((string) $v);
return $s === '' ? null : $s;
};
$sy = $readSrc($get, 'start_y');
$sm = $readSrc($get, 'start_m');
$sd = $readSrc($get, 'start_d');
$ey = $readSrc($get, 'end_y');
$em = $readSrc($get, 'end_m');
$ed = $readSrc($get, 'end_d');
$startDate = null;
if ($sy !== null && $sy !== '' && $sm !== null && $sm !== '' && $sd !== null && $sd !== '') {
$startDate = parse_ymd_from_triple($sy, $sm, $sd);
}
if ($startDate === null) {
$legacyStart = $readSrc($get, 'start_date');
$startDate = ($legacyStart !== null && $legacyStart !== '') ? $legacyStart : null;
}
$endDate = null;
if ($ey !== null && $ey !== '' && $em !== null && $em !== '' && $ed !== null && $ed !== '') {
$endDate = parse_ymd_from_triple($ey, $em, $ed);
}
if ($endDate === null) {
$legacyEnd = $readSrc($get, 'end_date');
$endDate = ($legacyEnd !== null && $legacyEnd !== '') ? $legacyEnd : null;
}
$startParts = ['y' => '', 'm' => '', 'd' => ''];
$endParts = ['y' => '', 'm' => '', 'd' => ''];
if ($startDate !== null && preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $startDate, $m)) {
$startParts = ['y' => $m[1], 'm' => (int) $m[2], 'd' => (int) $m[3]];
}
if ($endDate !== null && preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $endDate, $m)) {
$endParts = ['y' => $m[1], 'm' => (int) $m[2], 'd' => (int) $m[3]];
}
$bagKindE = $readSrc($get, 'bag_kind_e');
$bagCode = $readSrc($get, 'bag_code');
$builder = $this->priceModel->where('bp_lg_idx', $lgIdx); $builder = $this->priceModel->where('bp_lg_idx', $lgIdx);
if (($startDate !== null && $startDate !== '') || ($endDate !== null && $endDate !== '')) {
// 기간 필터 (P2-04) $qStart = ($startDate !== null && $startDate !== '') ? $startDate : $endDate;
$startDate = $this->request->getGet('start_date'); $qEnd = ($endDate !== null && $endDate !== '') ? $endDate : $startDate;
$endDate = $this->request->getGet('end_date'); if (strcmp((string) $qStart, (string) $qEnd) > 0) {
if ($startDate) { [$qStart, $qEnd] = [$qEnd, $qStart];
$builder->where('bp_start_date >=', $startDate); }
} $builder->where('bp_start_date <=', $qEnd);
if ($endDate) {
$builder->groupStart() $builder->groupStart()
->where('bp_end_date IS NULL') ->where('bp_end_date IS NULL')
->orWhere('bp_end_date <=', $endDate) ->orWhere('bp_end_date >=', $qStart)
->groupEnd(); ->groupEnd();
} }
$list = $builder->orderBy('bp_bag_code', 'ASC')->orderBy('bp_start_date', 'DESC')->paginate(20); if ($bagKindE !== null && $bagKindE !== '') {
$pager = $this->priceModel->pager; $kindE = model(CodeKindModel::class)->where('ck_code', 'E')->first();
if ($kindE) {
$detailE = model(CodeDetailModel::class)
->where('cd_ck_idx', (int) $kindE->ck_idx)
->where('cd_code', $bagKindE)
->where('cd_state', 1)
->first();
if ($detailE !== null) {
$builder->like('bp_bag_code', $bagKindE, 'after');
}
}
}
return view('admin/layout', [ if ($bagCode !== null && $bagCode !== '') {
'title' => '봉투 단가 관리', $kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
'content' => view('admin/bag_price/index', [ if ($kindO) {
'list' => $list, $detailO = model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, $bagCode, $lgIdx);
'startDate' => $startDate, if ($detailO !== null) {
'endDate' => $endDate, $builder->where('bp_bag_code', $bagCode);
'pager' => $pager, }
]), }
}
$list = $builder
->orderBy('bp_bag_code', 'ASC')
->orderBy('bp_start_date', 'DESC')
->paginate(20);
$queryForPager = [];
if ($sy !== null && $sm !== null && $sd !== null && $sy !== '' && $sm !== '' && $sd !== '') {
$queryForPager['start_y'] = $sy;
$queryForPager['start_m'] = $sm;
$queryForPager['start_d'] = $sd;
}
if ($ey !== null && $em !== null && $ed !== null && $ey !== '' && $em !== '' && $ed !== '') {
$queryForPager['end_y'] = $ey;
$queryForPager['end_m'] = $em;
$queryForPager['end_d'] = $ed;
}
if ($bagKindE !== null && $bagKindE !== '') {
$queryForPager['bag_kind_e'] = $bagKindE;
}
if ($bagCode !== null && $bagCode !== '') {
$queryForPager['bag_code'] = $bagCode;
}
$pagerPath = mgmt_url('bag-prices');
if ($queryForPager !== []) {
$pagerPath .= '?' . http_build_query($queryForPager);
}
$this->priceModel->pager->setPath($pagerPath);
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
$bagCodes = $kindO
? model(CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true, $lgIdx)
: [];
$kindE = model(CodeKindModel::class)->where('ck_code', 'E')->first();
$bagKindOptions = $kindE
? model(CodeDetailModel::class)->getByKind((int) $kindE->ck_idx, true, null)
: [];
return $this->renderWorkPage('봉투 단가 관리', 'admin/bag_price/index', [
'list' => $list,
'pager' => $this->priceModel->pager,
'startParts' => $startParts,
'endParts' => $endParts,
'dateYearMin' => (int) date('Y') - 12,
'dateYearMax' => (int) date('Y') + 2,
'bag_kind_e' => $bagKindE,
'bag_code' => $bagCode,
'bag_codes' => $bagCodes,
'bag_kind_options' => $bagKindOptions,
]); ]);
} }
@@ -60,22 +171,18 @@ class BagPrice extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) { if (! $lgIdx) {
return redirect()->to(site_url('admin/bag-prices'))->with('error', '지자체를 선택해 주세요.'); return redirect()->to(mgmt_url('bag-prices'))->with('error', '지자체를 선택해 주세요.');
} }
// 봉투명 코드(O) 목록
$kindModel = model(CodeKindModel::class); $kindModel = model(CodeKindModel::class);
$kind = $kindModel->where('ck_code', 'O')->first(); $kind = $kindModel->where('ck_code', 'O')->first();
$bagCodes = []; $bagCodes = [];
if ($kind) { if ($kind) {
$bagCodes = model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true); $bagCodes = model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx);
} }
return view('admin/layout', [ return $this->renderWorkPage('봉투 단가 등록', 'admin/bag_price/create', ['bagCodes' => $bagCodes]);
'title' => '봉투 단가 등록',
'content' => view('admin/bag_price/create', ['bagCodes' => $bagCodes]),
]);
} }
public function store() public function store()
@@ -96,13 +203,12 @@ class BagPrice extends BaseController
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
} }
// 봉투명 스냅샷 $bagCode = $this->request->getPost('bp_bag_code');
$bagCode = $this->request->getPost('bp_bag_code');
$kindModel = model(CodeKindModel::class); $kindModel = model(CodeKindModel::class);
$kind = $kindModel->where('ck_code', 'O')->first(); $kind = $kindModel->where('ck_code', 'O')->first();
$bagName = ''; $bagName = '';
if ($kind) { if ($kind) {
$detail = model(CodeDetailModel::class)->where('cd_ck_idx', $kind->ck_idx)->where('cd_code', $bagCode)->first(); $detail = model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kind->ck_idx, (string) $bagCode, $lgIdx);
$bagName = $detail ? $detail->cd_name : ''; $bagName = $detail ? $detail->cd_name : '';
} }
@@ -120,36 +226,34 @@ class BagPrice extends BaseController
'bp_reg_mb_idx' => session()->get('mb_idx'), 'bp_reg_mb_idx' => session()->get('mb_idx'),
]); ]);
return redirect()->to(site_url('admin/bag-prices'))->with('success', '봉투 단가가 등록되었습니다.'); return redirect()->to(mgmt_url('bag-prices'))->with('success', '봉투 단가가 등록되었습니다.');
} }
public function edit(int $id) public function edit(int $id)
{ {
helper('admin'); helper('admin');
$item = $this->priceModel->find($id); $lgIdx = admin_effective_lg_idx();
if (!$item || (int) $item->bp_lg_idx !== admin_effective_lg_idx()) { $item = $this->priceModel->find($id);
return redirect()->to(site_url('admin/bag-prices'))->with('error', '단가 정보를 찾을 수 없습니다.'); if (! $item || (int) $item->bp_lg_idx !== $lgIdx) {
return redirect()->to(mgmt_url('bag-prices'))->with('error', '단가 정보를 찾을 수 없습니다.');
} }
$kindModel = model(CodeKindModel::class); $kindModel = model(CodeKindModel::class);
$kind = $kindModel->where('ck_code', 'O')->first(); $kind = $kindModel->where('ck_code', 'O')->first();
$bagCodes = []; $bagCodes = [];
if ($kind) { if ($kind) {
$bagCodes = model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true); $bagCodes = model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx);
} }
return view('admin/layout', [ return $this->renderWorkPage('봉투 단가 수정', 'admin/bag_price/edit', ['item' => $item, 'bagCodes' => $bagCodes]);
'title' => '봉투 단가 수정',
'content' => view('admin/bag_price/edit', ['item' => $item, 'bagCodes' => $bagCodes]),
]);
} }
public function update(int $id) public function update(int $id)
{ {
helper('admin'); helper('admin');
$item = $this->priceModel->find($id); $item = $this->priceModel->find($id);
if (!$item || (int) $item->bp_lg_idx !== admin_effective_lg_idx()) { if (! $item || (int) $item->bp_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/bag-prices'))->with('error', '단가 정보를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('bag-prices'))->with('error', '단가 정보를 찾을 수 없습니다.');
} }
$rules = [ $rules = [
@@ -165,7 +269,6 @@ class BagPrice extends BaseController
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
} }
// 이력 기록
$db = \Config\Database::connect(); $db = \Config\Database::connect();
$db->transStart(); $db->transStart();
@@ -175,12 +278,12 @@ class BagPrice extends BaseController
$newVal = (string) $this->request->getPost($field); $newVal = (string) $this->request->getPost($field);
if ($oldVal !== $newVal) { if ($oldVal !== $newVal) {
$this->historyModel->insert([ $this->historyModel->insert([
'bph_bp_idx' => $id, 'bph_bp_idx' => $id,
'bph_field' => $field, 'bph_field' => $field,
'bph_old_value' => $oldVal, 'bph_old_value' => $oldVal,
'bph_new_value' => $newVal, 'bph_new_value' => $newVal,
'bph_changed_at'=> date('Y-m-d H:i:s'), 'bph_changed_at' => date('Y-m-d H:i:s'),
'bph_changed_by'=> session()->get('mb_idx'), 'bph_changed_by' => session()->get('mb_idx'),
]); ]);
} }
} }
@@ -197,34 +300,32 @@ class BagPrice extends BaseController
$db->transComplete(); $db->transComplete();
return redirect()->to(site_url('admin/bag-prices'))->with('success', '봉투 단가가 수정되었습니다.'); return redirect()->to(mgmt_url('bag-prices'))->with('success', '봉투 단가가 수정되었습니다.');
} }
public function delete(int $id) public function delete(int $id)
{ {
helper('admin'); helper('admin');
$item = $this->priceModel->find($id); $item = $this->priceModel->find($id);
if (!$item || (int) $item->bp_lg_idx !== admin_effective_lg_idx()) { if (! $item || (int) $item->bp_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/bag-prices'))->with('error', '단가 정보를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('bag-prices'))->with('error', '단가 정보를 찾을 수 없습니다.');
} }
$this->priceModel->delete($id); $this->priceModel->delete($id);
return redirect()->to(site_url('admin/bag-prices'))->with('success', '봉투 단가가 삭제되었습니다.');
return redirect()->to(mgmt_url('bag-prices'))->with('success', '봉투 단가가 삭제되었습니다.');
} }
public function history(int $bpIdx) public function history(int $bpIdx)
{ {
helper('admin'); helper('admin');
$item = $this->priceModel->find($bpIdx); $item = $this->priceModel->find($bpIdx);
if (!$item || (int) $item->bp_lg_idx !== admin_effective_lg_idx()) { if (! $item || (int) $item->bp_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/bag-prices'))->with('error', '단가 정보를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('bag-prices'))->with('error', '단가 정보를 찾을 수 없습니다.');
} }
$list = $this->historyModel->where('bph_bp_idx', $bpIdx)->orderBy('bph_changed_at', 'DESC')->findAll(); $list = $this->historyModel->where('bph_bp_idx', $bpIdx)->orderBy('bph_changed_at', 'DESC')->findAll();
return view('admin/layout', [ return $this->renderWorkPage('단가 변경 이력 — ' . $item->bp_bag_name, 'admin/bag_price/history', ['item' => $item, 'list' => $list]);
'title' => '단가 변경 이력 — ' . $item->bp_bag_name,
'content' => view('admin/bag_price/history', ['item' => $item, 'list' => $list]),
]);
} }
} }

View File

@@ -22,36 +22,37 @@ class BagReceiving extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
}
$builder = $this->recvModel->where('br_lg_idx', $lgIdx); $builder = $this->recvModel->where('br_lg_idx', $lgIdx);
$startDate = $this->request->getGet('start_date'); $startDate = $this->request->getGet('start_date');
$endDate = $this->request->getGet('end_date'); $endDate = $this->request->getGet('end_date');
if ($startDate) $builder->where('br_receive_date >=', $startDate); if ($startDate) {
if ($endDate) $builder->where('br_receive_date <=', $endDate); $builder->where('br_receive_date >=', $startDate);
}
if ($endDate) {
$builder->where('br_receive_date <=', $endDate);
}
$list = $builder->orderBy('br_receive_date', 'DESC')->orderBy('br_idx', 'DESC')->paginate(20); $list = $builder->orderBy('br_receive_date', 'DESC')->orderBy('br_idx', 'DESC')->paginate(20);
$pager = $this->recvModel->pager; $pager = $this->recvModel->pager;
return view('admin/layout', [ return $this->renderWorkPage('입고 현황', 'admin/bag_receiving/index', compact('list', 'startDate', 'endDate', 'pager'));
'title' => '입고 현황',
'content' => view('admin/bag_receiving/index', compact('list', 'startDate', 'endDate', 'pager')),
]);
} }
public function create() public function create()
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin/bag-receivings'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(mgmt_url('bag-receivings'))->with('error', '지자체를 선택해 주세요.');
}
// 미입고 발주 목록 $orders = model(BagOrderModel::class)->where('bo_lg_idx', $lgIdx)->whereLatestHead($lgIdx)->where('bo_status', 'normal')->orderBy('bo_order_date', 'DESC')->findAll();
$orders = model(BagOrderModel::class)->where('bo_lg_idx', $lgIdx)->where('bo_status', 'normal')->orderBy('bo_order_date', 'DESC')->findAll();
return view('admin/layout', [ return $this->renderWorkPage('입고 처리', 'admin/bag_receiving/create', compact('orders'));
'title' => '입고 처리',
'content' => view('admin/bag_receiving/create', compact('orders')),
]);
} }
public function store() public function store()
@@ -73,14 +74,12 @@ class BagReceiving extends BaseController
$bagCode = $this->request->getPost('br_bag_code'); $bagCode = $this->request->getPost('br_bag_code');
$qtyBox = (int) $this->request->getPost('br_qty_box'); $qtyBox = (int) $this->request->getPost('br_qty_box');
// 포장단위로 낱장 환산
$unit = model(\App\Models\PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_bag_code', $bagCode)->where('pu_state', 1)->first(); $unit = model(\App\Models\PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_bag_code', $bagCode)->where('pu_state', 1)->first();
$totalPerBox = $unit ? (int) $unit->pu_total_per_box : 1; $totalPerBox = $unit ? (int) $unit->pu_total_per_box : 1;
$qtySheet = $qtyBox * $totalPerBox; $qtySheet = $qtyBox * $totalPerBox;
// 봉투명 $kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first();
$kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first(); $detail = $kindO ? model(\App\Models\CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $bagCode, $lgIdx) : null;
$detail = $kindO ? model(\App\Models\CodeDetailModel::class)->where('cd_ck_idx', $kindO->ck_idx)->where('cd_code', $bagCode)->first() : null;
$bagName = $detail ? $detail->cd_name : ''; $bagName = $detail ? $detail->cd_name : '';
$db = \Config\Database::connect(); $db = \Config\Database::connect();
@@ -100,11 +99,10 @@ class BagReceiving extends BaseController
'br_regdate' => date('Y-m-d H:i:s'), 'br_regdate' => date('Y-m-d H:i:s'),
]); ]);
// 재고 가산
model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $bagName, $qtySheet); model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $bagName, $qtySheet);
$db->transComplete(); $db->transComplete();
return redirect()->to(site_url('admin/bag-receivings'))->with('success', '입고 처리되었습니다. (' . $bagName . ' ' . $qtyBox . '박스)'); return redirect()->to(mgmt_url('bag-receivings'))->with('success', '입고 처리되었습니다. (' . $bagName . ' ' . $qtyBox . '박스)');
} }
} }

View File

@@ -23,45 +23,56 @@ class BagSale extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
$builder = $this->saleModel->where('bs_lg_idx', $lgIdx);
$startDate = $this->request->getGet('start_date');
$endDate = $this->request->getGet('end_date');
$type = $this->request->getGet('type');
if ($startDate) $builder->where('bs_sale_date >=', $startDate);
if ($endDate) $builder->where('bs_sale_date <=', $endDate);
if ($type) $builder->where('bs_type', $type);
$list = $builder->orderBy('bs_sale_date', 'DESC')->orderBy('bs_idx', 'DESC')->paginate(20);
$pager = $this->saleModel->pager;
return view('admin/layout', [
'title' => '판매/반품 관리',
'content' => view('admin/bag_sale/index', compact('list', 'startDate', 'endDate', 'type', 'pager')),
]);
}
public function export()
{
helper(['admin', 'export']);
$lgIdx = admin_effective_lg_idx();
if (!$lgIdx) {
return redirect()->to(site_url('admin/bag-sales'))->with('error', '지자체를 선택해 주세요.');
} }
$builder = $this->saleModel->where('bs_lg_idx', $lgIdx); $builder = $this->saleModel->where('bs_lg_idx', $lgIdx);
$startDate = $this->request->getGet('start_date'); $startDate = $this->request->getGet('start_date');
$endDate = $this->request->getGet('end_date'); $endDate = $this->request->getGet('end_date');
$type = $this->request->getGet('type'); $type = $this->request->getGet('type');
if ($startDate) $builder->where('bs_sale_date >=', $startDate); if ($startDate) {
if ($endDate) $builder->where('bs_sale_date <=', $endDate); $builder->where('bs_sale_date >=', $startDate);
if ($type) $builder->where('bs_type', $type); }
if ($endDate) {
$builder->where('bs_sale_date <=', $endDate);
}
if ($type) {
$builder->where('bs_type', $type);
}
$list = $builder->orderBy('bs_sale_date', 'DESC')->orderBy('bs_idx', 'DESC')->paginate(20);
$pager = $this->saleModel->pager;
return $this->renderWorkPage('판매/반품 관리', 'admin/bag_sale/index', compact('list', 'startDate', 'endDate', 'type', 'pager'));
}
public function export()
{
helper(['admin', 'export']);
$lgIdx = admin_effective_lg_idx();
if (! $lgIdx) {
return redirect()->to(mgmt_url('bag-sales'))->with('error', '지자체를 선택해 주세요.');
}
$builder = $this->saleModel->where('bs_lg_idx', $lgIdx);
$startDate = $this->request->getGet('start_date');
$endDate = $this->request->getGet('end_date');
$type = $this->request->getGet('type');
if ($startDate) {
$builder->where('bs_sale_date >=', $startDate);
}
if ($endDate) {
$builder->where('bs_sale_date <=', $endDate);
}
if ($type) {
$builder->where('bs_type', $type);
}
$list = $builder->orderBy('bs_sale_date', 'DESC')->orderBy('bs_idx', 'DESC')->findAll(); $list = $builder->orderBy('bs_sale_date', 'DESC')->orderBy('bs_idx', 'DESC')->findAll();
$typeMap = ['sale' => '판매', 'return' => '반품', 'cancel' => '취소']; $typeMap = ['sale' => '판매', 'return' => '반품', 'cancel' => '취소'];
$rows = []; $rows = [];
foreach ($list as $row) { foreach ($list as $row) {
$rows[] = [ $rows[] = [
$row->bs_idx, $row->bs_idx,
@@ -87,16 +98,15 @@ class BagSale extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin/bag-sales'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(mgmt_url('bag-sales'))->with('error', '지자체를 선택해 주세요.');
}
$shops = model(DesignatedShopModel::class)->where('ds_lg_idx', $lgIdx)->where('ds_state', 1)->findAll(); $shops = model(DesignatedShopModel::class)->where('ds_lg_idx', $lgIdx)->where('ds_state', 1)->findAll();
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true) : []; $bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
return view('admin/layout', [ return $this->renderWorkPage('판매 등록', 'admin/bag_sale/create', compact('shops', 'bagCodes'));
'title' => '판매 등록',
'content' => view('admin/bag_sale/create', compact('shops', 'bagCodes')),
]);
} }
public function store() public function store()
@@ -120,10 +130,10 @@ class BagSale extends BaseController
$qty = (int) $this->request->getPost('bs_qty'); $qty = (int) $this->request->getPost('bs_qty');
$type = $this->request->getPost('bs_type'); $type = $this->request->getPost('bs_type');
$shop = model(DesignatedShopModel::class)->find($dsIdx); $shop = model(DesignatedShopModel::class)->find($dsIdx);
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
$detail = $kindO ? model(CodeDetailModel::class)->where('cd_ck_idx', $kindO->ck_idx)->where('cd_code', $bagCode)->first() : null; $detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $bagCode, $lgIdx) : null;
$price = model(BagPriceModel::class)->where('bp_lg_idx', $lgIdx)->where('bp_bag_code', $bagCode)->where('bp_state', 1)->first(); $price = model(BagPriceModel::class)->latestActiveByBagCode($lgIdx, (string) $bagCode);
$unitPrice = $price ? (float) $price->bp_consumer : 0; $unitPrice = $price ? (float) $price->bp_consumer : 0;
$actualQty = ($type === 'return') ? -$qty : $qty; $actualQty = ($type === 'return') ? -$qty : $qty;
@@ -132,31 +142,30 @@ class BagSale extends BaseController
$db->transStart(); $db->transStart();
$saleData = [ $saleData = [
'bs_lg_idx' => $lgIdx, 'bs_lg_idx' => $lgIdx,
'bs_ds_idx' => $dsIdx, 'bs_ds_idx' => $dsIdx,
'bs_ds_name' => $shop ? $shop->ds_name : '', 'bs_ds_name' => $shop ? $shop->ds_name : '',
'bs_sale_date' => $this->request->getPost('bs_sale_date'), 'bs_sale_date' => $this->request->getPost('bs_sale_date'),
'bs_bag_code' => $bagCode, 'bs_bag_code' => $bagCode,
'bs_bag_name' => $detail ? $detail->cd_name : '', 'bs_bag_name' => $detail ? $detail->cd_name : '',
'bs_qty' => $actualQty, 'bs_qty' => $actualQty,
'bs_unit_price'=> $unitPrice, 'bs_unit_price' => $unitPrice,
'bs_amount' => $unitPrice * abs($actualQty), 'bs_amount' => $unitPrice * abs($actualQty),
'bs_type' => $type, 'bs_type' => $type,
'bs_regdate' => date('Y-m-d H:i:s'), 'bs_regdate' => date('Y-m-d H:i:s'),
]; ];
$this->saleModel->insert($saleData); $this->saleModel->insert($saleData);
$bsIdx = (int) $this->saleModel->getInsertID(); $bsIdx = (int) $this->saleModel->getInsertID();
// CT-05: 감사 로그
helper('audit'); helper('audit');
audit_log('create', 'bag_sale', $bsIdx, null, array_merge($saleData, ['bs_idx' => $bsIdx])); audit_log('create', 'bag_sale', $bsIdx, null, array_merge($saleData, ['bs_idx' => $bsIdx]));
// 재고 감산(판매) / 가산(반품)
model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $detail ? $detail->cd_name : '', -$actualQty); model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $detail ? $detail->cd_name : '', -$actualQty);
$db->transComplete(); $db->transComplete();
$msg = ($type === 'sale') ? '판매 처리되었습니다.' : '반품 처리되었습니다.'; $msg = ($type === 'sale') ? '판매 처리되었습니다.' : '반품 처리되었습니다.';
return redirect()->to(site_url('admin/bag-sales'))->with('success', $msg);
return redirect()->to(mgmt_url('bag-sales'))->with('success', $msg);
} }
} }

View File

@@ -1,10 +1,15 @@
<?php <?php
declare(strict_types=1);
namespace App\Controllers\Admin; namespace App\Controllers\Admin;
use App\Controllers\BaseController; use App\Controllers\BaseController;
use App\Models\CodeKindModel; use App\Models\CodeKindModel;
use App\Models\CodeDetailModel; use App\Models\CodeDetailModel;
use App\Models\LocalGovernmentModel;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Roles;
class CodeDetail extends BaseController class CodeDetail extends BaseController
{ {
@@ -17,41 +22,57 @@ class CodeDetail extends BaseController
$this->detailModel = model(CodeDetailModel::class); $this->detailModel = model(CodeDetailModel::class);
} }
public function index(int $ckIdx) private function redirectIfCannotManageCodeMaster(): ?RedirectResponse
{ {
$kind = $this->kindModel->find($ckIdx); if (! Roles::canManageCodeMaster((int) session()->get('mb_level'))) {
if ($kind === null) { return redirect()->to(site_url('bag/code-kinds'))->with('error', '코드 관리 권한이 없습니다.');
return redirect()->to(site_url('admin/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.');
} }
$list = $this->detailModel->where('cd_ck_idx', $ckIdx)->orderBy('cd_sort', 'ASC')->paginate(20); return null;
$pager = $this->detailModel->pager; }
return view('admin/layout', [ /** @deprecated 사이트 URL 유지용 — 세부 목록은 /bag/code-details/{ck_idx} */
'title' => '세부코드 관리 — ' . $kind->ck_name . ' (' . $kind->ck_code . ')', public function index(int $ckIdx): RedirectResponse
'content' => view('admin/code_detail/index', [ {
'kind' => $kind, return redirect()->to(site_url('bag/code-details/' . $ckIdx));
'list' => $list,
'pager' => $pager,
]),
]);
} }
public function create(int $ckIdx) public function create(int $ckIdx)
{ {
if ($r = $this->redirectIfCannotManageCodeMaster()) {
return $r;
}
$kind = $this->kindModel->find($ckIdx); $kind = $this->kindModel->find($ckIdx);
if ($kind === null) { if ($kind === null) {
return redirect()->to(site_url('admin/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.'); return redirect()->to(site_url('bag/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.');
} }
$level = (int) session()->get('mb_level');
$canPlatformScope = Roles::isSuperAdminEquivalent($level);
$govs = $canPlatformScope
? model(LocalGovernmentModel::class)->where('lg_state', 1)->orderBy('lg_name', 'ASC')->findAll()
: [];
helper('admin');
return view('admin/layout', [ return view('admin/layout', [
'title' => '세부코드 등록 — ' . $kind->ck_name, 'title' => '세부코드 등록 — ' . $kind->ck_name,
'content' => view('admin/code_detail/create', ['kind' => $kind]), 'content' => view('admin/code_detail/create', [
'kind' => $kind,
'canPlatformScope' => $canPlatformScope,
'localGovernments' => $govs,
'effectiveLgIdx' => admin_effective_lg_idx(),
]),
]); ]);
} }
public function store() public function store()
{ {
if ($r = $this->redirectIfCannotManageCodeMaster()) {
return $r;
}
$rules = [ $rules = [
'cd_ck_idx' => 'required|is_natural_no_zero', 'cd_ck_idx' => 'required|is_natural_no_zero',
'cd_code' => 'required|max_length[50]', 'cd_code' => 'required|max_length[50]',
@@ -64,24 +85,73 @@ class CodeDetail extends BaseController
} }
$ckIdx = (int) $this->request->getPost('cd_ck_idx'); $ckIdx = (int) $this->request->getPost('cd_ck_idx');
$kind = $this->kindModel->find($ckIdx);
if ($kind === null) {
return redirect()->to(site_url('bag/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.');
}
helper('admin');
$level = (int) session()->get('mb_level');
if (Roles::isSuperAdminEquivalent($level)) {
$scope = $this->request->getPost('cd_scope') === 'local' ? 'local' : 'platform';
if ($scope === 'platform') {
$cdSource = 'platform';
$cdLgIdx = 0;
} else {
$cdLgIdx = (int) $this->request->getPost('cd_lg_idx');
if ($cdLgIdx < 1) {
return redirect()->back()->withInput()->with('error', '지자체 전용인 경우 소속 지자체를 선택해 주세요.');
}
$gov = model(LocalGovernmentModel::class)->find($cdLgIdx);
if ($gov === null) {
return redirect()->back()->withInput()->with('error', '유효하지 않은 지자체입니다.');
}
$cdSource = 'local';
}
} else {
$lg = admin_effective_lg_idx();
if ($lg === null || (int) $lg < 1) {
return redirect()->to(site_url('bag/code-kinds'))->with('error', '지자체를 선택한 뒤 등록해 주세요.');
}
$cdSource = 'local';
$cdLgIdx = (int) $lg;
}
$cdCode = (string) $this->request->getPost('cd_code');
$dup = $this->detailModel->where('cd_ck_idx', $ckIdx)->where('cd_code', $cdCode)->where('cd_lg_idx', $cdLgIdx)->first();
if ($dup !== null) {
return redirect()->back()->withInput()->with('error', '같은 종류·코드값·소속 범위에 이미 등록된 행이 있습니다.');
}
$this->detailModel->insert([ $this->detailModel->insert([
'cd_ck_idx' => $ckIdx, 'cd_ck_idx' => $ckIdx,
'cd_code' => $this->request->getPost('cd_code'), 'cd_source' => $cdSource,
'cd_lg_idx' => $cdLgIdx,
'cd_code' => $cdCode,
'cd_name' => $this->request->getPost('cd_name'), 'cd_name' => $this->request->getPost('cd_name'),
'cd_sort' => (int) ($this->request->getPost('cd_sort') ?: 0), 'cd_sort' => (int) ($this->request->getPost('cd_sort') ?: 0),
'cd_state' => 1, 'cd_state' => 1,
'cd_regdate' => date('Y-m-d H:i:s'), 'cd_regdate' => date('Y-m-d H:i:s'),
]); ]);
return redirect()->to(site_url('admin/code-details/' . $ckIdx))->with('success', '세부코드가 등록되었습니다.'); return redirect()->to(site_url('bag/code-details/' . $ckIdx))->with('success', '세부코드가 등록되었습니다.');
} }
public function edit(int $id) public function edit(int $id)
{ {
if ($r = $this->redirectIfCannotManageCodeMaster()) {
return $r;
}
$item = $this->detailModel->find($id); $item = $this->detailModel->find($id);
if ($item === null) { if ($item === null) {
return redirect()->to(site_url('admin/code-kinds'))->with('error', '세부코드를 찾을 수 없습니다.'); return redirect()->to(site_url('bag/code-kinds'))->with('error', '세부코드를 찾을 수 없습니다.');
}
helper('admin');
if (! Roles::canEditCodeDetailRow((int) session()->get('mb_level'), $item, admin_effective_lg_idx())) {
return redirect()->to(site_url('bag/code-details/' . $item->cd_ck_idx))->with('error', '이 세부코드를 수정할 권한이 없습니다.');
} }
$kind = $this->kindModel->find($item->cd_ck_idx); $kind = $this->kindModel->find($item->cd_ck_idx);
@@ -97,9 +167,18 @@ class CodeDetail extends BaseController
public function update(int $id) public function update(int $id)
{ {
if ($r = $this->redirectIfCannotManageCodeMaster()) {
return $r;
}
$item = $this->detailModel->find($id); $item = $this->detailModel->find($id);
if ($item === null) { if ($item === null) {
return redirect()->to(site_url('admin/code-kinds'))->with('error', '세부코드를 찾을 수 없습니다.'); return redirect()->to(site_url('bag/code-kinds'))->with('error', '세부코드를 찾을 수 없습니다.');
}
helper('admin');
if (! Roles::canEditCodeDetailRow((int) session()->get('mb_level'), $item, admin_effective_lg_idx())) {
return redirect()->to(site_url('bag/code-details/' . $item->cd_ck_idx))->with('error', '이 세부코드를 수정할 권한이 없습니다.');
} }
$rules = [ $rules = [
@@ -118,19 +197,28 @@ class CodeDetail extends BaseController
'cd_state' => (int) $this->request->getPost('cd_state'), 'cd_state' => (int) $this->request->getPost('cd_state'),
]); ]);
return redirect()->to(site_url('admin/code-details/' . $item->cd_ck_idx))->with('success', '세부코드가 수정되었습니다.'); return redirect()->to(site_url('bag/code-details/' . $item->cd_ck_idx))->with('success', '세부코드가 수정되었습니다.');
} }
public function delete(int $id) public function delete(int $id)
{ {
if ($r = $this->redirectIfCannotManageCodeMaster()) {
return $r;
}
$item = $this->detailModel->find($id); $item = $this->detailModel->find($id);
if ($item === null) { if ($item === null) {
return redirect()->to(site_url('admin/code-kinds'))->with('error', '세부코드를 찾을 수 없습니다.'); return redirect()->to(site_url('bag/code-kinds'))->with('error', '세부코드를 찾을 수 없습니다.');
}
helper('admin');
if (! Roles::canEditCodeDetailRow((int) session()->get('mb_level'), $item, admin_effective_lg_idx())) {
return redirect()->to(site_url('bag/code-details/' . $item->cd_ck_idx))->with('error', '이 세부코드를 삭제할 권한이 없습니다.');
} }
$ckIdx = $item->cd_ck_idx; $ckIdx = $item->cd_ck_idx;
$this->detailModel->delete($id); $this->detailModel->delete($id);
return redirect()->to(site_url('admin/code-details/' . $ckIdx))->with('success', '세부코드가 삭제되었습니다.'); return redirect()->to(site_url('bag/code-details/' . $ckIdx))->with('success', '세부코드가 삭제되었습니다.');
} }
} }

View File

@@ -1,10 +1,13 @@
<?php <?php
declare(strict_types=1);
namespace App\Controllers\Admin; namespace App\Controllers\Admin;
use App\Controllers\BaseController; use App\Controllers\BaseController;
use App\Models\CodeKindModel; use App\Models\CodeKindModel;
use App\Models\CodeDetailModel; use App\Models\CodeDetailModel;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Roles; use Config\Roles;
class CodeKind extends BaseController class CodeKind extends BaseController
@@ -16,30 +19,21 @@ class CodeKind extends BaseController
$this->kindModel = model(CodeKindModel::class); $this->kindModel = model(CodeKindModel::class);
} }
public function index() private function redirectIfCannotManageCodeKindMaster(): ?RedirectResponse
{ {
$list = $this->kindModel->orderBy('ck_code', 'ASC')->paginate(20); if (! Roles::canManageCodeKindMaster((int) session()->get('mb_level'))) {
$pager = $this->kindModel->pager; return redirect()->to(site_url('bag/code-kinds'))->with('error', '코드 종류는 super admin·본부 관리자만 관리할 수 있습니다.');
// 세부코드 수 매핑
$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', [ return null;
'title' => '기본코드 종류 관리',
'content' => view('admin/code_kind/index', [
'list' => $list,
'countMap' => $countMap,
'pager' => $pager,
]),
]);
} }
public function create() public function create()
{ {
if ($r = $this->redirectIfCannotManageCodeKindMaster()) {
return $r;
}
return view('admin/layout', [ return view('admin/layout', [
'title' => '기본코드 종류 등록', 'title' => '기본코드 종류 등록',
'content' => view('admin/code_kind/create'), 'content' => view('admin/code_kind/create'),
@@ -48,6 +42,10 @@ class CodeKind extends BaseController
public function store() public function store()
{ {
if ($r = $this->redirectIfCannotManageCodeKindMaster()) {
return $r;
}
$rules = [ $rules = [
'ck_code' => 'required|max_length[20]|is_unique[code_kind.ck_code]', 'ck_code' => 'required|max_length[20]|is_unique[code_kind.ck_code]',
'ck_name' => 'required|max_length[100]', 'ck_name' => 'required|max_length[100]',
@@ -64,14 +62,18 @@ class CodeKind extends BaseController
'ck_regdate' => date('Y-m-d H:i:s'), 'ck_regdate' => date('Y-m-d H:i:s'),
]); ]);
return redirect()->to(site_url('admin/code-kinds'))->with('success', '코드 종류가 등록되었습니다.'); return redirect()->to(site_url('bag/code-kinds'))->with('success', '코드 종류가 등록되었습니다.');
} }
public function edit(int $id) public function edit(int $id)
{ {
if ($r = $this->redirectIfCannotManageCodeKindMaster()) {
return $r;
}
$item = $this->kindModel->find($id); $item = $this->kindModel->find($id);
if ($item === null) { if ($item === null) {
return redirect()->to(site_url('admin/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.'); return redirect()->to(site_url('bag/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.');
} }
return view('admin/layout', [ return view('admin/layout', [
@@ -82,9 +84,13 @@ class CodeKind extends BaseController
public function update(int $id) public function update(int $id)
{ {
if ($r = $this->redirectIfCannotManageCodeKindMaster()) {
return $r;
}
$item = $this->kindModel->find($id); $item = $this->kindModel->find($id);
if ($item === null) { if ($item === null) {
return redirect()->to(site_url('admin/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.'); return redirect()->to(site_url('bag/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.');
} }
$rules = [ $rules = [
@@ -101,24 +107,28 @@ class CodeKind extends BaseController
'ck_state' => (int) $this->request->getPost('ck_state'), 'ck_state' => (int) $this->request->getPost('ck_state'),
]); ]);
return redirect()->to(site_url('admin/code-kinds'))->with('success', '코드 종류가 수정되었습니다.'); return redirect()->to(site_url('bag/code-kinds'))->with('success', '코드 종류가 수정되었습니다.');
} }
public function delete(int $id) public function delete(int $id)
{ {
$item = $this->kindModel->find($id); if ($r = $this->redirectIfCannotManageCodeKindMaster()) {
if ($item === null) { return $r;
return redirect()->to(site_url('admin/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.'); }
$item = $this->kindModel->find($id);
if ($item === null) {
return redirect()->to(site_url('bag/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.');
} }
// 세부코드가 있으면 삭제 불가
$detailCount = model(CodeDetailModel::class)->where('cd_ck_idx', $id)->countAllResults(); $detailCount = model(CodeDetailModel::class)->where('cd_ck_idx', $id)->countAllResults();
if ($detailCount > 0) { if ($detailCount > 0) {
return redirect()->to(site_url('admin/code-kinds')) return redirect()->to(site_url('bag/code-kinds'))
->with('error', '세부코드가 ' . $detailCount . '개 존재하여 삭제할 수 없습니다. 먼저 세부코드를 삭제해 주세요.'); ->with('error', '세부코드가 ' . $detailCount . '개 존재하여 삭제할 수 없습니다. 먼저 세부코드를 삭제해 주세요.');
} }
$this->kindModel->delete($id); $this->kindModel->delete($id);
return redirect()->to(site_url('admin/code-kinds'))->with('success', '코드 종류가 삭제되었습니다.');
return redirect()->to(site_url('bag/code-kinds'))->with('success', '코드 종류가 삭제되었습니다.');
} }
} }

View File

@@ -9,6 +9,11 @@ class Company extends BaseController
{ {
private CompanyModel $model; private CompanyModel $model;
private function companyTypeOptions(): array
{
return ['협회', '제작업체', '회수업체'];
}
public function __construct() public function __construct()
{ {
$this->model = model(CompanyModel::class); $this->model = model(CompanyModel::class);
@@ -18,25 +23,42 @@ class Company extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) { if (! $lgIdx) {
return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
} }
$list = $this->model->where('cp_lg_idx', $lgIdx)->orderBy('cp_idx', 'DESC')->paginate(20); $companyType = trim((string) ($this->request->getGet('cp_type') ?? ''));
$typeOptions = $this->companyTypeOptions();
$builder = $this->model->where('cp_lg_idx', $lgIdx);
if ($companyType !== '' && in_array($companyType, $typeOptions, true)) {
$builder->where('cp_type', $companyType);
}
$list = $builder->orderBy('cp_idx', 'DESC')->paginate(20);
$pager = $this->model->pager; $pager = $this->model->pager;
return view('admin/layout', [ $queryForPager = [];
'title' => '업체 관리', if ($companyType !== '' && in_array($companyType, $typeOptions, true)) {
'content' => view('admin/company/index', ['list' => $list, 'pager' => $pager]), $queryForPager['cp_type'] = $companyType;
}
$pagerPath = mgmt_url('companies');
if ($queryForPager !== []) {
$pagerPath .= '?' . http_build_query($queryForPager);
}
$pager->setPath($pagerPath);
return $this->renderWorkPage('업체 관리', 'admin/company/index', [
'list' => $list,
'pager' => $pager,
'cpType' => $companyType,
'typeOptions' => $typeOptions,
]); ]);
} }
public function create() public function create()
{ {
return view('admin/layout', [ return $this->renderWorkPage('업체 등록', 'admin/company/create');
'title' => '업체 등록',
'content' => view('admin/company/create'),
]);
} }
public function store() public function store()
@@ -66,29 +88,26 @@ class Company extends BaseController
'cp_regdate' => date('Y-m-d H:i:s'), 'cp_regdate' => date('Y-m-d H:i:s'),
]); ]);
return redirect()->to(site_url('admin/companies'))->with('success', '업체가 등록되었습니다.'); return redirect()->to(mgmt_url('companies'))->with('success', '업체가 등록되었습니다.');
} }
public function edit(int $id) public function edit(int $id)
{ {
helper('admin'); helper('admin');
$item = $this->model->find($id); $item = $this->model->find($id);
if (!$item || (int) $item->cp_lg_idx !== admin_effective_lg_idx()) { if (! $item || (int) $item->cp_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/companies'))->with('error', '업체를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('companies'))->with('error', '업체를 찾을 수 없습니다.');
} }
return view('admin/layout', [ return $this->renderWorkPage('업체 수정', 'admin/company/edit', ['item' => $item]);
'title' => '업체 수정',
'content' => view('admin/company/edit', ['item' => $item]),
]);
} }
public function update(int $id) public function update(int $id)
{ {
helper('admin'); helper('admin');
$item = $this->model->find($id); $item = $this->model->find($id);
if (!$item || (int) $item->cp_lg_idx !== admin_effective_lg_idx()) { if (! $item || (int) $item->cp_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/companies'))->with('error', '업체를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('companies'))->with('error', '업체를 찾을 수 없습니다.');
} }
$rules = [ $rules = [
@@ -110,18 +129,19 @@ class Company extends BaseController
'cp_state' => (int) $this->request->getPost('cp_state'), 'cp_state' => (int) $this->request->getPost('cp_state'),
]); ]);
return redirect()->to(site_url('admin/companies'))->with('success', '업체가 수정되었습니다.'); return redirect()->to(mgmt_url('companies'))->with('success', '업체가 수정되었습니다.');
} }
public function delete(int $id) public function delete(int $id)
{ {
helper('admin'); helper('admin');
$item = $this->model->find($id); $item = $this->model->find($id);
if (!$item || (int) $item->cp_lg_idx !== admin_effective_lg_idx()) { if (! $item || (int) $item->cp_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/companies'))->with('error', '업체를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('companies'))->with('error', '업체를 찾을 수 없습니다.');
} }
$this->model->delete($id); $this->model->delete($id);
return redirect()->to(site_url('admin/companies'))->with('success', '업체가 삭제되었습니다.');
return redirect()->to(mgmt_url('companies'))->with('success', '업체가 삭제되었습니다.');
} }
} }

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Controllers\Admin; namespace App\Controllers\Admin;
use App\Controllers\BaseController; use App\Controllers\BaseController;
use CodeIgniter\Database\Exceptions\DatabaseException;
class Dashboard extends BaseController class Dashboard extends BaseController
{ {
@@ -22,65 +23,71 @@ class Dashboard extends BaseController
'issue_count_month'=> 0, 'issue_count_month'=> 0,
'recent_orders' => [], 'recent_orders' => [],
'recent_sales' => [], 'recent_sales' => [],
'stats_unavailable'=> false,
]; ];
if ($lgIdx) { if ($lgIdx) {
$db = \Config\Database::connect(); $db = \Config\Database::connect();
// 총 발주 건수/금액 try {
$orderStats = $db->query(" // 총 발주 건수/금액
SELECT COUNT(*) as cnt, $orderStats = $db->query("
COALESCE(SUM(sub.total_amt), 0) as total_amount SELECT COUNT(*) as cnt,
FROM bag_order bo COALESCE(SUM(sub.total_amt), 0) as total_amount
LEFT JOIN ( FROM bag_order bo
SELECT boi_bo_idx, SUM(boi_amount) as total_amt LEFT JOIN (
FROM bag_order_item GROUP BY boi_bo_idx SELECT boi_bo_idx, SUM(boi_amount) as total_amt
) sub ON sub.boi_bo_idx = bo.bo_idx FROM bag_order_item GROUP BY boi_bo_idx
WHERE bo.bo_lg_idx = ? AND bo.bo_status = 'normal' ) sub ON sub.boi_bo_idx = bo.bo_idx
", [$lgIdx])->getRow(); WHERE bo.bo_lg_idx = ? AND bo.bo_status = 'normal'
$stats['order_count'] = (int) ($orderStats->cnt ?? 0); ", [$lgIdx])->getRow();
$stats['order_amount'] = (int) ($orderStats->total_amount ?? 0); $stats['order_count'] = (int) ($orderStats->cnt ?? 0);
$stats['order_amount'] = (int) ($orderStats->total_amount ?? 0);
// 총 판매 건수/금액 // 총 판매 건수/금액
$saleStats = $db->query(" $saleStats = $db->query("
SELECT COUNT(*) as cnt, COALESCE(SUM(bs_amount), 0) as total_amount SELECT COUNT(*) as cnt, COALESCE(SUM(bs_amount), 0) as total_amount
FROM bag_sale FROM bag_sale
WHERE bs_lg_idx = ? AND bs_type = 'sale' WHERE bs_lg_idx = ? AND bs_type = 'sale'
", [$lgIdx])->getRow(); ", [$lgIdx])->getRow();
$stats['sale_count'] = (int) ($saleStats->cnt ?? 0); $stats['sale_count'] = (int) ($saleStats->cnt ?? 0);
$stats['sale_amount'] = (int) ($saleStats->total_amount ?? 0); $stats['sale_amount'] = (int) ($saleStats->total_amount ?? 0);
// 현재 재고 품목 수 // 현재 재고 품목 수
$invCount = $db->query(" $invCount = $db->query("
SELECT COUNT(*) as cnt FROM bag_inventory WHERE bi_lg_idx = ? AND bi_qty > 0 SELECT COUNT(*) as cnt FROM bag_inventory WHERE bi_lg_idx = ? AND bi_qty > 0
", [$lgIdx])->getRow(); ", [$lgIdx])->getRow();
$stats['inventory_count'] = (int) ($invCount->cnt ?? 0); $stats['inventory_count'] = (int) ($invCount->cnt ?? 0);
// 이번 달 불출 건수 // 이번 달 불출 건수
$monthStart = date('Y-m-01'); $monthStart = date('Y-m-01');
$issueCount = $db->query(" $issueCount = $db->query("
SELECT COUNT(*) as cnt FROM bag_issue SELECT COUNT(*) as cnt FROM bag_issue
WHERE bi2_lg_idx = ? AND bi2_status = 'normal' AND bi2_issue_date >= ? WHERE bi2_lg_idx = ? AND bi2_status = 'normal' AND bi2_issue_date >= ?
", [$lgIdx, $monthStart])->getRow(); ", [$lgIdx, $monthStart])->getRow();
$stats['issue_count_month'] = (int) ($issueCount->cnt ?? 0); $stats['issue_count_month'] = (int) ($issueCount->cnt ?? 0);
// 최근 발주 5건 // 최근 발주 5건
$stats['recent_orders'] = $db->query(" $stats['recent_orders'] = $db->query("
SELECT bo_idx, bo_lot_no, bo_order_date, bo_status SELECT bo_idx, bo_lot_no, bo_order_date, bo_status
FROM bag_order FROM bag_order
WHERE bo_lg_idx = ? WHERE bo_lg_idx = ?
ORDER BY bo_order_date DESC, bo_idx DESC ORDER BY bo_order_date DESC, bo_idx DESC
LIMIT 5 LIMIT 5
", [$lgIdx])->getResult(); ", [$lgIdx])->getResult();
// 최근 판매 5건 // 최근 판매 5건
$stats['recent_sales'] = $db->query(" $stats['recent_sales'] = $db->query("
SELECT bs_idx, bs_ds_name, bs_bag_name, bs_qty, bs_amount, bs_sale_date, bs_type SELECT bs_idx, bs_ds_name, bs_bag_name, bs_qty, bs_amount, bs_sale_date, bs_type
FROM bag_sale FROM bag_sale
WHERE bs_lg_idx = ? WHERE bs_lg_idx = ?
ORDER BY bs_sale_date DESC, bs_idx DESC ORDER BY bs_sale_date DESC, bs_idx DESC
LIMIT 5 LIMIT 5
", [$lgIdx])->getResult(); ", [$lgIdx])->getResult();
} catch (DatabaseException $e) {
$stats['stats_unavailable'] = true;
log_message('error', '[Dashboard] 통계 조회 실패(테이블 미생성 등): ' . $e->getMessage());
}
} }
return view('admin/layout', [ return view('admin/layout', [

File diff suppressed because it is too large Load Diff

View File

@@ -18,35 +18,32 @@ class FreeRecipient extends BaseController
private function getCodeOptions(string $ckCode): array private function getCodeOptions(string $ckCode): array
{ {
$kind = model(CodeKindModel::class)->where('ck_code', $ckCode)->first(); helper('admin');
return $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true) : []; $lgIdx = admin_effective_lg_idx();
$kind = model(CodeKindModel::class)->where('ck_code', $ckCode)->first();
return $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
} }
public function index() public function index()
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) { if (! $lgIdx) {
return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
} }
$list = $this->model->where('fr_lg_idx', $lgIdx)->orderBy('fr_idx', 'DESC')->paginate(20); $list = $this->model->where('fr_lg_idx', $lgIdx)->orderBy('fr_idx', 'DESC')->paginate(20);
$pager = $this->model->pager; $pager = $this->model->pager;
return view('admin/layout', [ return $this->renderWorkPage('무료용 대상자 관리', 'admin/free_recipient/index', ['list' => $list, 'pager' => $pager]);
'title' => '무료용 대상자 관리',
'content' => view('admin/free_recipient/index', ['list' => $list, 'pager' => $pager]),
]);
} }
public function create() public function create()
{ {
return view('admin/layout', [ return $this->renderWorkPage('무료용 대상자 등록', 'admin/free_recipient/create', [
'title' => '무료용 대상자 등록', 'typeCodes' => $this->getCodeOptions('H'),
'content' => view('admin/free_recipient/create', [ 'dongCodes' => $this->getCodeOptions('D'),
'typeCodes' => $this->getCodeOptions('H'),
'dongCodes' => $this->getCodeOptions('D'),
]),
]); ]);
} }
@@ -75,24 +72,21 @@ class FreeRecipient extends BaseController
'fr_regdate' => date('Y-m-d H:i:s'), 'fr_regdate' => date('Y-m-d H:i:s'),
]); ]);
return redirect()->to(site_url('admin/free-recipients'))->with('success', '무료용 대상자가 등록되었습니다.'); return redirect()->to(mgmt_url('free-recipients'))->with('success', '무료용 대상자가 등록되었습니다.');
} }
public function edit(int $id) public function edit(int $id)
{ {
helper('admin'); helper('admin');
$item = $this->model->find($id); $item = $this->model->find($id);
if (!$item || (int) $item->fr_lg_idx !== admin_effective_lg_idx()) { if (! $item || (int) $item->fr_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/free-recipients'))->with('error', '대상자를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('free-recipients'))->with('error', '대상자를 찾을 수 없습니다.');
} }
return view('admin/layout', [ return $this->renderWorkPage('무료용 대상자 수정', 'admin/free_recipient/edit', [
'title' => '무료용 대상자 수정', 'item' => $item,
'content' => view('admin/free_recipient/edit', [ 'typeCodes' => $this->getCodeOptions('H'),
'item' => $item, 'dongCodes' => $this->getCodeOptions('D'),
'typeCodes' => $this->getCodeOptions('H'),
'dongCodes' => $this->getCodeOptions('D'),
]),
]); ]);
} }
@@ -100,8 +94,8 @@ class FreeRecipient extends BaseController
{ {
helper('admin'); helper('admin');
$item = $this->model->find($id); $item = $this->model->find($id);
if (!$item || (int) $item->fr_lg_idx !== admin_effective_lg_idx()) { if (! $item || (int) $item->fr_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/free-recipients'))->with('error', '대상자를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('free-recipients'))->with('error', '대상자를 찾을 수 없습니다.');
} }
$rules = [ $rules = [
@@ -123,18 +117,19 @@ class FreeRecipient extends BaseController
'fr_state' => (int) $this->request->getPost('fr_state'), 'fr_state' => (int) $this->request->getPost('fr_state'),
]); ]);
return redirect()->to(site_url('admin/free-recipients'))->with('success', '무료용 대상자가 수정되었습니다.'); return redirect()->to(mgmt_url('free-recipients'))->with('success', '무료용 대상자가 수정되었습니다.');
} }
public function delete(int $id) public function delete(int $id)
{ {
helper('admin'); helper('admin');
$item = $this->model->find($id); $item = $this->model->find($id);
if (!$item || (int) $item->fr_lg_idx !== admin_effective_lg_idx()) { if (! $item || (int) $item->fr_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/free-recipients'))->with('error', '대상자를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('free-recipients'))->with('error', '대상자를 찾을 수 없습니다.');
} }
$this->model->delete($id); $this->model->delete($id);
return redirect()->to(site_url('admin/free-recipients'))->with('success', '무료용 대상자가 삭제되었습니다.');
return redirect()->to(mgmt_url('free-recipients'))->with('success', '무료용 대상자가 삭제되었습니다.');
} }
} }

View File

@@ -18,8 +18,20 @@ class Manager extends BaseController
private function getCodeOptions(string $ckCode): array private function getCodeOptions(string $ckCode): array
{ {
$kind = model(CodeKindModel::class)->where('ck_code', $ckCode)->first(); helper('admin');
return $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true) : []; $lgIdx = admin_effective_lg_idx();
$kind = model(CodeKindModel::class)->where('ck_code', $ckCode)->first();
return $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
}
private function managerCategoryOptions(): array
{
return [
'company' => '제작업체',
'district' => '구·군',
'agency' => '대행소',
];
} }
public function index() public function index()
@@ -27,34 +39,44 @@ class Manager extends BaseController
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) { if (!$lgIdx) {
return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); helper('admin');
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
} }
$list = $this->model->where('mg_lg_idx', $lgIdx)->orderBy('mg_idx', 'DESC')->paginate(20); $category = (string) ($this->request->getGet('category') ?? '');
$categories = $this->managerCategoryOptions();
$builder = $this->model->where('mg_lg_idx', $lgIdx);
if ($category !== '' && isset($categories[$category])) {
$builder->where('mg_dept_code', $category);
}
$list = $builder->orderBy('mg_idx', 'DESC')->paginate(20);
$pager = $this->model->pager; $pager = $this->model->pager;
return view('admin/layout', [ return $this->renderWorkPage('담당자 관리', 'admin/manager/index', [
'title' => '담당자 관리', 'list' => $list,
'content' => view('admin/manager/index', ['list' => $list, 'pager' => $pager]), 'pager' => $pager,
'categories' => $categories,
'category' => $category,
]); ]);
} }
public function create() public function create()
{ {
return view('admin/layout', [ return $this->renderWorkPage('담당자 등록', 'admin/manager/create', [
'title' => '담당자 등록', 'categories' => $this->managerCategoryOptions(),
'content' => view('admin/manager/create', [ 'positionCodes' => $this->getCodeOptions('T'),
'deptCodes' => $this->getCodeOptions('S'),
'positionCodes' => $this->getCodeOptions('T'),
]),
]); ]);
} }
public function store() public function store()
{ {
helper('admin'); helper(['admin', 'url']);
$rules = [ $rules = [
'mg_name' => 'required|max_length[50]', 'mg_name' => 'required|max_length[50]',
'mg_category' => 'required|in_list[company,district,agency]',
'mg_tel' => 'permit_empty|max_length[20]', 'mg_tel' => 'permit_empty|max_length[20]',
'mg_phone' => 'permit_empty|max_length[20]', 'mg_phone' => 'permit_empty|max_length[20]',
'mg_email' => 'permit_empty|valid_email|max_length[100]', 'mg_email' => 'permit_empty|valid_email|max_length[100]',
@@ -66,7 +88,7 @@ class Manager extends BaseController
$this->model->insert([ $this->model->insert([
'mg_lg_idx' => admin_effective_lg_idx(), 'mg_lg_idx' => admin_effective_lg_idx(),
'mg_name' => $this->request->getPost('mg_name'), 'mg_name' => $this->request->getPost('mg_name'),
'mg_dept_code' => $this->request->getPost('mg_dept_code') ?? '', 'mg_dept_code' => (string) ($this->request->getPost('mg_category') ?? ''),
'mg_position_code' => $this->request->getPost('mg_position_code') ?? '', 'mg_position_code' => $this->request->getPost('mg_position_code') ?? '',
'mg_tel' => $this->request->getPost('mg_tel') ?? '', 'mg_tel' => $this->request->getPost('mg_tel') ?? '',
'mg_phone' => $this->request->getPost('mg_phone') ?? '', 'mg_phone' => $this->request->getPost('mg_phone') ?? '',
@@ -75,37 +97,35 @@ class Manager extends BaseController
'mg_regdate' => date('Y-m-d H:i:s'), 'mg_regdate' => date('Y-m-d H:i:s'),
]); ]);
return redirect()->to(site_url('admin/managers'))->with('success', '담당자가 등록되었습니다.'); return redirect()->to(mgmt_url('managers'))->with('success', '담당자가 등록되었습니다.');
} }
public function edit(int $id) public function edit(int $id)
{ {
helper('admin'); helper(['admin', 'url']);
$item = $this->model->find($id); $item = $this->model->find($id);
if (!$item || (int) $item->mg_lg_idx !== admin_effective_lg_idx()) { if (!$item || (int) $item->mg_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/managers'))->with('error', '담당자를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('managers'))->with('error', '담당자를 찾을 수 없습니다.');
} }
return view('admin/layout', [ return $this->renderWorkPage('담당자 수정', 'admin/manager/edit', [
'title' => '담당자 수정', 'item' => $item,
'content' => view('admin/manager/edit', [ 'categories' => $this->managerCategoryOptions(),
'item' => $item, 'positionCodes' => $this->getCodeOptions('T'),
'deptCodes' => $this->getCodeOptions('S'),
'positionCodes' => $this->getCodeOptions('T'),
]),
]); ]);
} }
public function update(int $id) public function update(int $id)
{ {
helper('admin'); helper(['admin', 'url']);
$item = $this->model->find($id); $item = $this->model->find($id);
if (!$item || (int) $item->mg_lg_idx !== admin_effective_lg_idx()) { if (!$item || (int) $item->mg_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/managers'))->with('error', '담당자를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('managers'))->with('error', '담당자를 찾을 수 없습니다.');
} }
$rules = [ $rules = [
'mg_name' => 'required|max_length[50]', 'mg_name' => 'required|max_length[50]',
'mg_category' => 'required|in_list[company,district,agency]',
'mg_state' => 'required|in_list[0,1]', 'mg_state' => 'required|in_list[0,1]',
]; ];
if (! $this->validate($rules)) { if (! $this->validate($rules)) {
@@ -114,7 +134,7 @@ class Manager extends BaseController
$this->model->update($id, [ $this->model->update($id, [
'mg_name' => $this->request->getPost('mg_name'), 'mg_name' => $this->request->getPost('mg_name'),
'mg_dept_code' => $this->request->getPost('mg_dept_code') ?? '', 'mg_dept_code' => (string) ($this->request->getPost('mg_category') ?? ''),
'mg_position_code' => $this->request->getPost('mg_position_code') ?? '', 'mg_position_code' => $this->request->getPost('mg_position_code') ?? '',
'mg_tel' => $this->request->getPost('mg_tel') ?? '', 'mg_tel' => $this->request->getPost('mg_tel') ?? '',
'mg_phone' => $this->request->getPost('mg_phone') ?? '', 'mg_phone' => $this->request->getPost('mg_phone') ?? '',
@@ -122,18 +142,19 @@ class Manager extends BaseController
'mg_state' => (int) $this->request->getPost('mg_state'), 'mg_state' => (int) $this->request->getPost('mg_state'),
]); ]);
return redirect()->to(site_url('admin/managers'))->with('success', '담당자가 수정되었습니다.'); return redirect()->to(mgmt_url('managers'))->with('success', '담당자가 수정되었습니다.');
} }
public function delete(int $id) public function delete(int $id)
{ {
helper('admin'); helper(['admin', 'url']);
$item = $this->model->find($id); $item = $this->model->find($id);
if (!$item || (int) $item->mg_lg_idx !== admin_effective_lg_idx()) { if (!$item || (int) $item->mg_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/managers'))->with('error', '담당자를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('managers'))->with('error', '담당자를 찾을 수 없습니다.');
} }
$this->model->delete($id); $this->model->delete($id);
return redirect()->to(site_url('admin/managers'))->with('success', '담당자가 삭제되었습니다.');
return redirect()->to(mgmt_url('managers'))->with('success', '담당자가 삭제되었습니다.');
} }
} }

View File

@@ -23,6 +23,9 @@ class Menu extends BaseController
*/ */
public function index() public function index()
{ {
if ($deny = $this->denyUnlessLevel4Plus()) {
return $deny;
}
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) { if ($lgIdx === null) {
@@ -30,18 +33,34 @@ class Menu extends BaseController
->with('error', '메뉴를 관리하려면 먼저 지자체를 선택하세요.'); ->with('error', '메뉴를 관리하려면 먼저 지자체를 선택하세요.');
} }
$types = $this->typeModel->orderBy('mt_sort', 'ASC')->findAll(); $types = $this->typeModel->orderBy('mt_sort', 'ASC')->findAll();
$mtIdx = (int) ($this->request->getGet('mt_idx') ?? 0); $requestedMtIdx = (int) ($this->request->getGet('mt_idx') ?? 0);
if ($mtIdx <= 0 && ! empty($types)) { $mtIdx = $this->resolveMtIdx($requestedMtIdx, $types);
// 기본 선택: 사이트 메뉴(mt_code=site), 없으면 첫 번째 타입 $effectiveMtIdx = $mtIdx;
$siteType = $this->typeModel->where('mt_code', 'site')->first(); $debugMode = $this->request->getGet('debug') === '1';
$mtIdx = $siteType ? (int) $siteType->mt_idx : (int) $types[0]->mt_idx; $fallbackApplied = false;
} $list = $effectiveMtIdx > 0 ? $this->menuModel->getAllByType($effectiveMtIdx, $lgIdx) : [];
$list = $mtIdx > 0 ? $this->menuModel->getAllByType($mtIdx, $lgIdx) : []; $currentType = $mtIdx > 0 ? $this->typeModel->find($mtIdx) : null;
$currentTypeCode = (string) ($currentType->mt_code ?? '');
// 현재 지자체에 메뉴가 없으면, mt_idx별로 기본 지자체(lg_idx=1)의 메뉴를 한 번 복사한다. // 현재 지자체에 메뉴가 없으면, mt_idx별로 기본 지자체(lg_idx=1)의 메뉴를 한 번 복사한다.
if ($mtIdx > 0 && empty($list)) { if ($effectiveMtIdx > 0 && empty($list)) {
$this->menuModel->copyDefaultsFromLg($mtIdx, 1, $lgIdx); $this->menuModel->copyDefaultsFromLg($effectiveMtIdx, 1, $lgIdx);
$list = $this->menuModel->getAllByType($mtIdx, $lgIdx); $list = $this->menuModel->getAllByType($effectiveMtIdx, $lgIdx);
}
// 운영 DB 불일치 대응: site 타입인데 mt_idx 매핑이 어긋난 경우(예: menu_type=2, menu는 4 사용)
if (empty($list) && $currentTypeCode === 'site' && $effectiveMtIdx !== 4) {
$fallbackMtIdx = 4;
$fallbackList = $this->menuModel->getAllByType($fallbackMtIdx, $lgIdx);
if (empty($fallbackList)) {
$this->menuModel->copyDefaultsFromLg($fallbackMtIdx, 1, $lgIdx);
$fallbackList = $this->menuModel->getAllByType($fallbackMtIdx, $lgIdx);
}
if (! empty($fallbackList)) {
$effectiveMtIdx = $fallbackMtIdx;
$list = $fallbackList;
$fallbackApplied = true;
}
} }
// 트리 순서대로 상위 메뉴 바로 아래에 하위 메뉴가 오도록 평탄화 // 트리 순서대로 상위 메뉴 바로 아래에 하위 메뉴가 오도록 평탄화
@@ -50,16 +69,24 @@ class Menu extends BaseController
$list = flatten_menu_tree($tree); $list = flatten_menu_tree($tree);
} }
$currentType = $mtIdx > 0 ? $this->typeModel->find($mtIdx) : null;
return view('admin/layout', [ return view('admin/layout', [
'title' => '메뉴 관리', 'title' => '메뉴 관리',
'content' => view('admin/menu/index', [ 'content' => view('admin/menu/index', [
'types' => $types, 'types' => $types,
'mtIdx' => $mtIdx, 'mtIdx' => $mtIdx,
'mtCode' => $currentType->mt_code ?? '', 'mtCode' => $currentTypeCode,
'list' => $list, 'list' => $list,
'levelNames' => config('Roles')->levelNames, 'levelNames' => config('Roles')->levelNames,
'debug_mode' => $debugMode,
'debug_info' => [
'lg_idx' => $lgIdx,
'requested_mt_idx' => $requestedMtIdx,
'resolved_mt_idx' => $mtIdx,
'effective_mt_idx' => $effectiveMtIdx,
'resolved_mt_code' => $currentTypeCode,
'list_count' => count($list),
'fallback_applied' => $fallbackApplied ? 'Y' : 'N',
],
]), ]),
]); ]);
} }
@@ -69,11 +96,16 @@ class Menu extends BaseController
*/ */
public function list() public function list()
{ {
if ($deny = $this->denyUnlessLevel4Plus(true)) {
return $deny;
}
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) { if ($lgIdx === null) {
return $this->response->setJSON(['status' => 0, 'msg' => '지자체를 선택하세요.']); return $this->response->setJSON(['status' => 0, 'msg' => '지자체를 선택하세요.']);
} }
$mtIdx = (int) $this->request->getGet('mt_idx'); $types = $this->typeModel->orderBy('mt_sort', 'ASC')->findAll();
$requestedMtIdx = (int) $this->request->getGet('mt_idx');
$mtIdx = $this->resolveMtIdx($requestedMtIdx, $types);
if ($mtIdx <= 0) { if ($mtIdx <= 0) {
return $this->response->setJSON(['status' => 0, 'msg' => 'mt_idx required']); return $this->response->setJSON(['status' => 0, 'msg' => 'mt_idx required']);
} }
@@ -86,6 +118,9 @@ class Menu extends BaseController
*/ */
public function store() public function store()
{ {
if ($deny = $this->denyUnlessLevel4Plus()) {
return $deny;
}
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) { if ($lgIdx === null) {
return redirect()->to(base_url('admin/select-local-government')) return redirect()->to(base_url('admin/select-local-government'))
@@ -118,6 +153,7 @@ class Menu extends BaseController
if ($mmPidx > 0) { if ($mmPidx > 0) {
$this->menuModel->updateCnode($mmPidx, 1); $this->menuModel->updateCnode($mmPidx, 1);
} }
$this->menuModel->syncTypeToAllLgs($mtIdx, $lgIdx);
return redirect()->back()->with('success', '메뉴가 등록되었습니다.'); return redirect()->back()->with('success', '메뉴가 등록되었습니다.');
} }
@@ -126,6 +162,9 @@ class Menu extends BaseController
*/ */
public function update(int $id) public function update(int $id)
{ {
if ($deny = $this->denyUnlessLevel4Plus()) {
return $deny;
}
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) { if ($lgIdx === null) {
return redirect()->to(base_url('admin/select-local-government')) return redirect()->to(base_url('admin/select-local-government'))
@@ -145,6 +184,7 @@ class Menu extends BaseController
'mm_is_view' => $this->request->getPost('mm_is_view') ? 'Y' : 'N', 'mm_is_view' => $this->request->getPost('mm_is_view') ? 'Y' : 'N',
]; ];
$this->menuModel->update($id, $data); $this->menuModel->update($id, $data);
$this->menuModel->syncTypeToAllLgs((int) $row->mt_idx, $lgIdx);
return redirect()->back()->with('success', '메뉴가 수정되었습니다.'); return redirect()->back()->with('success', '메뉴가 수정되었습니다.');
} }
@@ -153,6 +193,9 @@ class Menu extends BaseController
*/ */
public function delete(int $id) public function delete(int $id)
{ {
if ($deny = $this->denyUnlessLevel4Plus()) {
return $deny;
}
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) { if ($lgIdx === null) {
return redirect()->to(base_url('admin/select-local-government')) return redirect()->to(base_url('admin/select-local-government'))
@@ -164,6 +207,7 @@ class Menu extends BaseController
} }
$result = $this->menuModel->deleteSafe($id); $result = $this->menuModel->deleteSafe($id);
if ($result['ok']) { if ($result['ok']) {
$this->menuModel->syncTypeToAllLgs((int) $row->mt_idx, $lgIdx);
return redirect()->back()->with('success', '메뉴가 삭제되었습니다.'); return redirect()->back()->with('success', '메뉴가 삭제되었습니다.');
} }
return redirect()->back()->with('error', $result['msg']); return redirect()->back()->with('error', $result['msg']);
@@ -174,6 +218,9 @@ class Menu extends BaseController
*/ */
public function move() public function move()
{ {
if ($deny = $this->denyUnlessLevel4Plus()) {
return $deny;
}
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) { if ($lgIdx === null) {
return redirect()->to(base_url('admin/select-local-government')) return redirect()->to(base_url('admin/select-local-government'))
@@ -183,7 +230,12 @@ class Menu extends BaseController
if (! is_array($ids) || empty($ids)) { if (! is_array($ids) || empty($ids)) {
return redirect()->back()->with('error', '순서를 적용할 메뉴가 없습니다.'); return redirect()->back()->with('error', '순서를 적용할 메뉴가 없습니다.');
} }
$firstId = (int) ($ids[0] ?? 0);
$firstRow = $firstId > 0 ? $this->menuModel->find($firstId) : null;
$this->menuModel->setOrder($ids, $lgIdx); $this->menuModel->setOrder($ids, $lgIdx);
if ($firstRow && (int) $firstRow->lg_idx === $lgIdx) {
$this->menuModel->syncTypeToAllLgs((int) $firstRow->mt_idx, $lgIdx);
}
return redirect()->back()->with('success', '순서가 적용되었습니다.'); return redirect()->back()->with('success', '순서가 적용되었습니다.');
} }
@@ -210,4 +262,57 @@ class Menu extends BaseController
return implode(',', array_values($levels)); return implode(',', array_values($levels));
} }
/**
* 요청된 mt_idx를 현재 DB 상태에 맞게 보정.
* - 유효한 mt_idx면 그대로 사용
* - 레거시 site 값(2) 요청 시 site 타입의 실제 mt_idx로 치환
* - 그 외 미지정/잘못된 값은 site 우선, 없으면 첫 타입으로 보정
*
* @param array<int,object> $types
*/
private function resolveMtIdx(int $requestedMtIdx, array $types): int
{
if (empty($types)) {
return 0;
}
$validTypeIds = array_map(static fn ($t): int => (int) ($t->mt_idx ?? 0), $types);
if ($requestedMtIdx > 0 && in_array($requestedMtIdx, $validTypeIds, true)) {
return $requestedMtIdx;
}
$siteType = $this->typeModel->where('mt_code', 'site')->first();
if ($siteType !== null) {
// 과거 링크(/admin/menus?mt_idx=2) 호환
if ($requestedMtIdx === 2 || $requestedMtIdx <= 0 || ! in_array($requestedMtIdx, $validTypeIds, true)) {
return (int) $siteType->mt_idx;
}
}
return (int) $types[0]->mt_idx;
}
/**
* 메뉴 관리는 레벨4 이상(슈퍼/본부 관리자)만 허용.
*
* @return \CodeIgniter\HTTP\RedirectResponse|\CodeIgniter\HTTP\ResponseInterface|null
*/
private function denyUnlessLevel4Plus(bool $json = false)
{
$level = (int) session()->get('mb_level');
if (Roles::isSuperAdminEquivalent($level)) {
return null;
}
if ($json) {
return $this->response->setJSON([
'status' => 0,
'msg' => '메뉴 관리는 레벨4 이상만 접근할 수 있습니다.',
]);
}
return redirect()->to(base_url('admin/dashboard'))
->with('error', '메뉴 관리는 레벨4 이상만 접근할 수 있습니다.');
}
} }

View File

@@ -23,8 +23,8 @@ class PackagingUnit extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) { if (! $lgIdx) {
return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
} }
$builder = $this->unitModel->where('pu_lg_idx', $lgIdx); $builder = $this->unitModel->where('pu_lg_idx', $lgIdx);
@@ -38,31 +38,26 @@ class PackagingUnit extends BaseController
$builder->groupStart()->where('pu_end_date IS NULL')->orWhere('pu_end_date <=', $endDate)->groupEnd(); $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')->paginate(20); $list = $builder->orderBy('pu_bag_code', 'ASC')->orderBy('pu_start_date', 'DESC')->paginate(20);
$pager = $this->unitModel->pager; $pager = $this->unitModel->pager;
return view('admin/layout', [ return $this->renderWorkPage('포장 단위 관리', 'admin/packaging_unit/index', [
'title' => '포장 단위 관리', 'list' => $list, 'startDate' => $startDate, 'endDate' => $endDate, 'pager' => $pager,
'content' => view('admin/packaging_unit/index', [
'list' => $list, 'startDate' => $startDate, 'endDate' => $endDate, 'pager' => $pager,
]),
]); ]);
} }
public function create() public function create()
{ {
helper('admin'); helper('admin');
if (!admin_effective_lg_idx()) { if (! admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/packaging-units'))->with('error', '지자체를 선택해 주세요.'); return redirect()->to(mgmt_url('packaging-units'))->with('error', '지자체를 선택해 주세요.');
} }
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $lgIdx = admin_effective_lg_idx();
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true) : []; $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
return view('admin/layout', [ return $this->renderWorkPage('포장 단위 등록', 'admin/packaging_unit/create', ['bagCodes' => $bagCodes]);
'title' => '포장 단위 등록',
'content' => view('admin/packaging_unit/create', ['bagCodes' => $bagCodes]),
]);
} }
public function store() public function store()
@@ -83,10 +78,10 @@ class PackagingUnit extends BaseController
} }
$bagCode = $this->request->getPost('pu_bag_code'); $bagCode = $this->request->getPost('pu_bag_code');
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
$bagName = ''; $bagName = '';
if ($kind) { if ($kind) {
$detail = model(CodeDetailModel::class)->where('cd_ck_idx', $kind->ck_idx)->where('cd_code', $bagCode)->first(); $detail = model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kind->ck_idx, (string) $bagCode, $lgIdx);
$bagName = $detail ? $detail->cd_name : ''; $bagName = $detail ? $detail->cd_name : '';
} }
@@ -107,32 +102,30 @@ class PackagingUnit extends BaseController
'pu_reg_mb_idx' => session()->get('mb_idx'), 'pu_reg_mb_idx' => session()->get('mb_idx'),
]); ]);
return redirect()->to(site_url('admin/packaging-units'))->with('success', '포장 단위가 등록되었습니다.'); return redirect()->to(mgmt_url('packaging-units'))->with('success', '포장 단위가 등록되었습니다.');
} }
public function edit(int $id) public function edit(int $id)
{ {
helper('admin'); helper('admin');
$item = $this->unitModel->find($id); $item = $this->unitModel->find($id);
if (!$item || (int) $item->pu_lg_idx !== admin_effective_lg_idx()) { if (! $item || (int) $item->pu_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/packaging-units'))->with('error', '포장 단위를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('packaging-units'))->with('error', '포장 단위를 찾을 수 없습니다.');
} }
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $lgIdx = admin_effective_lg_idx();
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true) : []; $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
return view('admin/layout', [ return $this->renderWorkPage('포장 단위 수정', 'admin/packaging_unit/edit', ['item' => $item, 'bagCodes' => $bagCodes]);
'title' => '포장 단위 수정',
'content' => view('admin/packaging_unit/edit', ['item' => $item, 'bagCodes' => $bagCodes]),
]);
} }
public function update(int $id) public function update(int $id)
{ {
helper('admin'); helper('admin');
$item = $this->unitModel->find($id); $item = $this->unitModel->find($id);
if (!$item || (int) $item->pu_lg_idx !== admin_effective_lg_idx()) { if (! $item || (int) $item->pu_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/packaging-units'))->with('error', '포장 단위를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('packaging-units'))->with('error', '포장 단위를 찾을 수 없습니다.');
} }
$rules = [ $rules = [
@@ -150,18 +143,35 @@ class PackagingUnit extends BaseController
$db = \Config\Database::connect(); $db = \Config\Database::connect();
$db->transStart(); $db->transStart();
$trackFields = ['pu_box_per_pack', 'pu_pack_per_sheet']; $trackFields = ['pu_box_per_pack', 'pu_pack_per_sheet', 'pu_start_date', 'pu_end_date', 'pu_state'];
$fieldLabels = [
'pu_box_per_pack' => '박스당 팩 수',
'pu_pack_per_sheet' => '팩당 낱장 수',
'pu_start_date' => '적용시작일',
'pu_end_date' => '적용종료일',
'pu_state' => '상태',
];
foreach ($trackFields as $field) { foreach ($trackFields as $field) {
$oldVal = (string) $item->$field; $oldRaw = $item->$field;
$newVal = (string) $this->request->getPost($field); $newRaw = $this->request->getPost($field);
if ($field === 'pu_end_date') {
$oldRaw = $oldRaw ?: '';
$newRaw = $newRaw ?: '';
}
if ($field === 'pu_state') {
$oldRaw = (int) $oldRaw === 1 ? '사용' : '미사용';
$newRaw = (int) $newRaw === 1 ? '사용' : '미사용';
}
$oldVal = (string) $oldRaw;
$newVal = (string) $newRaw;
if ($oldVal !== $newVal) { if ($oldVal !== $newVal) {
$this->historyModel->insert([ $this->historyModel->insert([
'puh_pu_idx' => $id, 'puh_pu_idx' => $id,
'puh_field' => $field, 'puh_field' => $fieldLabels[$field] ?? $field,
'puh_old_value' => $oldVal, 'puh_old_value' => $oldVal,
'puh_new_value' => $newVal, 'puh_new_value' => $newVal,
'puh_changed_at'=> date('Y-m-d H:i:s'), 'puh_changed_at' => date('Y-m-d H:i:s'),
'puh_changed_by'=> session()->get('mb_idx'), 'puh_changed_by' => session()->get('mb_idx'),
]); ]);
} }
} }
@@ -180,34 +190,33 @@ class PackagingUnit extends BaseController
]); ]);
$db->transComplete(); $db->transComplete();
return redirect()->to(site_url('admin/packaging-units'))->with('success', '포장 단위가 수정되었습니다.');
return redirect()->to(mgmt_url('packaging-units'))->with('success', '포장 단위가 수정되었습니다.');
} }
public function delete(int $id) public function delete(int $id)
{ {
helper('admin'); helper('admin');
$item = $this->unitModel->find($id); $item = $this->unitModel->find($id);
if (!$item || (int) $item->pu_lg_idx !== admin_effective_lg_idx()) { if (! $item || (int) $item->pu_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/packaging-units'))->with('error', '포장 단위를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('packaging-units'))->with('error', '포장 단위를 찾을 수 없습니다.');
} }
$this->unitModel->delete($id); $this->unitModel->delete($id);
return redirect()->to(site_url('admin/packaging-units'))->with('success', '포장 단위가 삭제되었습니다.');
return redirect()->to(mgmt_url('packaging-units'))->with('success', '포장 단위가 삭제되었습니다.');
} }
public function history(int $puIdx) public function history(int $puIdx)
{ {
helper('admin'); helper('admin');
$item = $this->unitModel->find($puIdx); $item = $this->unitModel->find($puIdx);
if (!$item || (int) $item->pu_lg_idx !== admin_effective_lg_idx()) { if (! $item || (int) $item->pu_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/packaging-units'))->with('error', '포장 단위를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('packaging-units'))->with('error', '포장 단위를 찾을 수 없습니다.');
} }
$list = $this->historyModel->where('puh_pu_idx', $puIdx)->orderBy('puh_changed_at', 'DESC')->findAll(); $list = $this->historyModel->where('puh_pu_idx', $puIdx)->orderBy('puh_changed_at', 'DESC')->findAll();
return view('admin/layout', [ return $this->renderWorkPage('포장 단위 변경 이력 — ' . $item->pu_bag_name, 'admin/packaging_unit/history', ['item' => $item, 'list' => $list]);
'title' => '포장 단위 변경 이력 — ' . $item->pu_bag_name,
'content' => view('admin/packaging_unit/history', ['item' => $item, 'list' => $list]),
]);
} }
} }

View File

@@ -9,14 +9,14 @@ class PasswordChange extends BaseController
{ {
public function index() public function index()
{ {
return view('admin/layout', [ helper('admin');
'title' => '비밀번호 변경',
'content' => view('admin/password_change/index'), return $this->renderWorkPage('비밀번호 변경', 'admin/password_change/index');
]);
} }
public function update() public function update()
{ {
helper('admin');
$rules = [ $rules = [
'current_password' => 'required', 'current_password' => 'required',
'new_password' => 'required|min_length[4]|max_length[255]', 'new_password' => 'required|min_length[4]|max_length[255]',
@@ -50,6 +50,6 @@ class PasswordChange extends BaseController
'mb_passwd' => password_hash($this->request->getPost('new_password'), PASSWORD_DEFAULT), 'mb_passwd' => password_hash($this->request->getPost('new_password'), PASSWORD_DEFAULT),
]); ]);
return redirect()->to(site_url('admin/password-change'))->with('success', '비밀번호가 변경되었습니다.'); return redirect()->to(mgmt_url('password-change'))->with('success', '비밀번호가 변경되었습니다.');
} }
} }

View File

@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Controllers\Admin; namespace App\Controllers\Admin;
use App\Controllers\BaseController; use App\Controllers\BaseController;
@@ -7,6 +9,8 @@ use App\Models\SalesAgencyModel;
class SalesAgency extends BaseController class SalesAgency extends BaseController
{ {
private const SCHEMA_ERROR = '판매 대행소 테이블에 sa_kind·sa_code 컬럼이 없습니다. DB에 writable/database/sales_agency_migrate_to_kind_code_name.sql(또는 신규용 sales_agency_tables.sql)을 적용한 뒤 다시 시도해 주세요.';
private SalesAgencyModel $model; private SalesAgencyModel $model;
public function __construct() public function __construct()
@@ -18,106 +22,161 @@ class SalesAgency extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) { if (! $lgIdx) {
return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
} }
$list = $this->model->where('sa_lg_idx', $lgIdx)->orderBy('sa_idx', 'DESC')->paginate(20); $saKind = trim((string) ($this->request->getGet('sa_kind') ?? ''));
$saCode = trim((string) ($this->request->getGet('sa_code') ?? ''));
$saName = trim((string) ($this->request->getGet('sa_name') ?? ''));
$saIdx = trim((string) ($this->request->getGet('sa_idx') ?? ''));
$builder = $this->model->where('sa_lg_idx', $lgIdx);
if ($saKind !== '') {
$builder->like('sa_kind', $saKind);
}
if ($saCode !== '') {
$builder->like('sa_code', $saCode);
}
if ($saName !== '') {
$builder->like('sa_name', $saName);
}
if ($saIdx !== '' && ctype_digit($saIdx)) {
$builder->where('sa_idx', (int) $saIdx);
}
$list = $builder->orderForDisplay()->paginate(20);
$pager = $this->model->pager; $pager = $this->model->pager;
return view('admin/layout', [ $queryForPager = [
'title' => '판매 대행소 관리', 'sa_kind' => $saKind,
'content' => view('admin/sales_agency/index', ['list' => $list, 'pager' => $pager]), 'sa_code' => $saCode,
'sa_name' => $saName,
'sa_idx' => $saIdx,
];
$queryForPager = array_filter($queryForPager, static fn ($v) => $v !== null && $v !== '');
$pagerPath = mgmt_url('sales-agencies');
if ($queryForPager !== []) {
$pagerPath .= '?' . http_build_query($queryForPager);
}
$pager->setPath($pagerPath);
return $this->renderWorkPage('판매 대행소 관리', 'admin/sales_agency/index', [
'list' => $list,
'pager' => $pager,
'sa_kind' => $saKind,
'sa_code' => $saCode,
'sa_name' => $saName,
'sa_idx' => $saIdx,
]); ]);
} }
public function create() public function create()
{ {
return view('admin/layout', [ helper('admin');
'title' => '판매 대행소 등록', if (! admin_effective_lg_idx()) {
'content' => view('admin/sales_agency/create'), return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
]); }
return $this->renderWorkPage('판매 대행소 등록', 'admin/sales_agency/create');
} }
public function store() public function store()
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx();
if (! $lgIdx) {
return redirect()->to(mgmt_url('sales-agencies'))->with('error', '지자체를 선택해 주세요.');
}
if (! $this->model->hasKindCodeColumns()) {
return redirect()->back()->withInput()->with('error', self::SCHEMA_ERROR);
}
$rules = [ $rules = [
'sa_name' => 'required|max_length[100]', 'sa_kind' => 'required|max_length[50]',
'sa_biz_no' => 'permit_empty|max_length[20]', 'sa_code' => 'required|max_length[50]',
'sa_rep_name' => 'permit_empty|max_length[50]', 'sa_name' => 'required|max_length[100]',
'sa_tel' => 'permit_empty|max_length[20]',
'sa_addr' => 'permit_empty|max_length[255]',
]; ];
if (! $this->validate($rules)) { if (! $this->validate($rules)) {
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
} }
$code = trim((string) $this->request->getPost('sa_code'));
if ($this->model->where('sa_lg_idx', $lgIdx)->where('sa_code', $code)->first() !== null) {
return redirect()->back()->withInput()->with('error', '동일 지자체에 같은 대행소 코드가 이미 있습니다.');
}
$this->model->insert([ $this->model->insert([
'sa_lg_idx' => admin_effective_lg_idx(), 'sa_lg_idx' => $lgIdx,
'sa_name' => $this->request->getPost('sa_name'), 'sa_kind' => trim((string) $this->request->getPost('sa_kind')),
'sa_biz_no' => $this->request->getPost('sa_biz_no') ?? '', 'sa_code' => $code,
'sa_rep_name' => $this->request->getPost('sa_rep_name') ?? '', 'sa_name' => trim((string) $this->request->getPost('sa_name')),
'sa_tel' => $this->request->getPost('sa_tel') ?? '', 'sa_regdate' => date('Y-m-d H:i:s'),
'sa_addr' => $this->request->getPost('sa_addr') ?? '',
'sa_state' => 1,
'sa_regdate' => date('Y-m-d H:i:s'),
]); ]);
return redirect()->to(site_url('admin/sales-agencies'))->with('success', '판매 대행소가 등록되었습니다.'); return redirect()->to(mgmt_url('sales-agencies'))->with('success', '판매 대행소가 등록되었습니다.');
} }
public function edit(int $id) public function edit(int $id)
{ {
helper('admin'); helper('admin');
$item = $this->model->find($id); $item = $this->model->find($id);
if (!$item || (int) $item->sa_lg_idx !== admin_effective_lg_idx()) { if (! $item || (int) $item->sa_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/sales-agencies'))->with('error', '대행소를 찾을 수 없습니다.'); return redirect()->to(mgmt_url('sales-agencies'))->with('error', '대행소를 찾을 수 없습니다.');
} }
return view('admin/layout', [ return $this->renderWorkPage('판매 대행소 수정', 'admin/sales_agency/edit', ['item' => $item]);
'title' => '판매 대행소 수정',
'content' => view('admin/sales_agency/edit', ['item' => $item]),
]);
} }
public function update(int $id) public function update(int $id)
{ {
helper('admin'); helper('admin');
$item = $this->model->find($id); $lgIdx = admin_effective_lg_idx();
if (!$item || (int) $item->sa_lg_idx !== admin_effective_lg_idx()) { $item = $this->model->find($id);
return redirect()->to(site_url('admin/sales-agencies'))->with('error', '대행소를 찾을 수 없습니다.'); if (! $item || ! $lgIdx || (int) $item->sa_lg_idx !== $lgIdx) {
return redirect()->to(mgmt_url('sales-agencies'))->with('error', '대행소를 찾을 수 없습니다.');
}
if (! $this->model->hasKindCodeColumns()) {
return redirect()->back()->withInput()->with('error', self::SCHEMA_ERROR);
} }
$rules = [ $rules = [
'sa_name' => 'required|max_length[100]', 'sa_kind' => 'required|max_length[50]',
'sa_state' => 'required|in_list[0,1]', 'sa_code' => 'required|max_length[50]',
'sa_name' => 'required|max_length[100]',
]; ];
if (! $this->validate($rules)) { if (! $this->validate($rules)) {
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
} }
$code = trim((string) $this->request->getPost('sa_code'));
$dup = $this->model->where('sa_lg_idx', $lgIdx)->where('sa_code', $code)->where('sa_idx !=', $id)->first();
if ($dup !== null) {
return redirect()->back()->withInput()->with('error', '동일 지자체에 같은 대행소 코드가 이미 있습니다.');
}
$this->model->update($id, [ $this->model->update($id, [
'sa_name' => $this->request->getPost('sa_name'), 'sa_kind' => trim((string) $this->request->getPost('sa_kind')),
'sa_biz_no' => $this->request->getPost('sa_biz_no') ?? '', 'sa_code' => $code,
'sa_rep_name' => $this->request->getPost('sa_rep_name') ?? '', 'sa_name' => trim((string) $this->request->getPost('sa_name')),
'sa_tel' => $this->request->getPost('sa_tel') ?? '',
'sa_addr' => $this->request->getPost('sa_addr') ?? '',
'sa_state' => (int) $this->request->getPost('sa_state'),
]); ]);
return redirect()->to(site_url('admin/sales-agencies'))->with('success', '판매 대행소가 수정되었습니다.'); return redirect()->to(mgmt_url('sales-agencies'))->with('success', '판매 대행소가 수정되었습니다.');
} }
public function delete(int $id) public function delete(int $id)
{ {
helper('admin'); helper('admin');
$item = $this->model->find($id); $lgIdx = admin_effective_lg_idx();
if (!$item || (int) $item->sa_lg_idx !== admin_effective_lg_idx()) { $item = $this->model->find($id);
return redirect()->to(site_url('admin/sales-agencies'))->with('error', '대행소를 찾을 수 없습니다.'); if (! $item || ! $lgIdx || (int) $item->sa_lg_idx !== $lgIdx) {
return redirect()->to(mgmt_url('sales-agencies'))->with('error', '대행소를 찾을 수 없습니다.');
} }
$this->model->delete($id); $this->model->delete($id);
return redirect()->to(site_url('admin/sales-agencies'))->with('success', '판매 대행소가 삭제되었습니다.');
return redirect()->to(mgmt_url('sales-agencies'))->with('success', '삭제되었습니다.');
} }
} }

View File

@@ -17,7 +17,9 @@ class SalesReport extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
}
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01'); $startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d'); $endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
@@ -50,10 +52,7 @@ class SalesReport extends BaseController
", [$lgIdx, $startDate, $endDate])->getResult(); ", [$lgIdx, $startDate, $endDate])->getResult();
} }
return view('admin/layout', [ return $this->renderWorkPage('판매 대장', 'admin/sales_report/sales_ledger', compact('result', 'startDate', 'endDate', 'mode'));
'title' => '판매 대장',
'content' => view('admin/sales_report/sales_ledger', compact('result', 'startDate', 'endDate', 'mode')),
]);
} }
/** /**
@@ -63,7 +62,9 @@ class SalesReport extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
}
$date = $this->request->getGet('date') ?? date('Y-m-d'); $date = $this->request->getGet('date') ?? date('Y-m-d');
$db = \Config\Database::connect(); $db = \Config\Database::connect();
@@ -91,10 +92,7 @@ class SalesReport extends BaseController
ORDER BY bs_bag_code ORDER BY bs_bag_code
", [$lgIdx, $monthStart, $date])->getResult(); ", [$lgIdx, $monthStart, $date])->getResult();
return view('admin/layout', [ return $this->renderWorkPage('일계표', 'admin/sales_report/daily_summary', compact('daily', 'monthly', 'date'));
'title' => '일계표',
'content' => view('admin/sales_report/daily_summary', compact('daily', 'monthly', 'date')),
]);
} }
/** /**
@@ -104,7 +102,9 @@ class SalesReport extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
}
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01'); $startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d'); $endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
@@ -122,10 +122,7 @@ class SalesReport extends BaseController
ORDER BY bs_bag_code ORDER BY bs_bag_code
", [$lgIdx, $startDate, $endDate])->getResult(); ", [$lgIdx, $startDate, $endDate])->getResult();
return view('admin/layout', [ return $this->renderWorkPage('기간별 판매현황', 'admin/sales_report/period_sales', compact('result', 'startDate', 'endDate'));
'title' => '기간별 판매현황',
'content' => view('admin/sales_report/period_sales', compact('result', 'startDate', 'endDate')),
]);
} }
/** /**
@@ -135,7 +132,9 @@ class SalesReport extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
}
$year = $this->request->getGet('year') ?? date('Y'); $year = $this->request->getGet('year') ?? date('Y');
$db = \Config\Database::connect(); $db = \Config\Database::connect();
@@ -161,10 +160,7 @@ class SalesReport extends BaseController
ORDER BY bs_bag_code ORDER BY bs_bag_code
", [$lgIdx, $year])->getResult(); ", [$lgIdx, $year])->getResult();
return view('admin/layout', [ return $this->renderWorkPage('년 판매 현황', 'admin/sales_report/yearly_sales', compact('result', 'year'));
'title' => '년 판매 현황',
'content' => view('admin/sales_report/yearly_sales', compact('result', 'year')),
]);
} }
/** /**
@@ -174,7 +170,9 @@ class SalesReport extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
}
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01'); $startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d'); $endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
@@ -192,10 +190,7 @@ class SalesReport extends BaseController
ORDER BY bs_ds_name ORDER BY bs_ds_name
", [$lgIdx, $startDate, $endDate])->getResult(); ", [$lgIdx, $startDate, $endDate])->getResult();
return view('admin/layout', [ return $this->renderWorkPage('지정판매소별 판매현황', 'admin/sales_report/shop_sales', compact('result', 'startDate', 'endDate'));
'title' => '지정판매소별 판매현황',
'content' => view('admin/sales_report/shop_sales', compact('result', 'startDate', 'endDate')),
]);
} }
/** /**
@@ -205,7 +200,9 @@ class SalesReport extends BaseController
{ {
helper(['admin', 'export']); helper(['admin', 'export']);
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
}
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01'); $startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d'); $endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
@@ -257,7 +254,9 @@ class SalesReport extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
}
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01'); $startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d'); $endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
@@ -271,10 +270,7 @@ class SalesReport extends BaseController
ORDER BY bs_sale_date DESC, bs_ds_name ORDER BY bs_sale_date DESC, bs_ds_name
", [$lgIdx, $startDate, $endDate])->getResult(); ", [$lgIdx, $startDate, $endDate])->getResult();
return view('admin/layout', [ return $this->renderWorkPage('반품/파기 현황', 'admin/sales_report/returns', compact('result', 'startDate', 'endDate'));
'title' => '반품/파기 현황',
'content' => view('admin/sales_report/returns', compact('result', 'startDate', 'endDate')),
]);
} }
/** /**
@@ -284,7 +280,9 @@ class SalesReport extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
}
$lotNo = $this->request->getGet('lot_no') ?? ''; $lotNo = $this->request->getGet('lot_no') ?? '';
$order = null; $order = null;
@@ -300,10 +298,7 @@ class SalesReport extends BaseController
} }
} }
return view('admin/layout', [ return $this->renderWorkPage('LOT 수불 조회', 'admin/sales_report/lot_flow', compact('lotNo', 'order', 'items', 'receivings'));
'title' => 'LOT 수불 조회',
'content' => view('admin/sales_report/lot_flow', compact('lotNo', 'order', 'items', 'receivings')),
]);
} }
/** /**
@@ -313,7 +308,9 @@ class SalesReport extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
}
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01'); $startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d'); $endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
@@ -332,12 +329,9 @@ class SalesReport extends BaseController
// 봉투 코드 목록 // 봉투 코드 목록
$kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first(); $kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first();
$bagCodes = $kindO ? model(\App\Models\CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true) : []; $bagCodes = $kindO ? model(\App\Models\CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true, $lgIdx) : [];
return view('admin/layout', [ return $this->renderWorkPage('기타 입출고', 'admin/sales_report/misc_flow', compact('result', 'startDate', 'endDate', 'bagCodes', 'tableExists'));
'title' => '기타 입출고',
'content' => view('admin/sales_report/misc_flow', compact('result', 'startDate', 'endDate', 'bagCodes', 'tableExists')),
]);
} }
/** /**
@@ -347,7 +341,7 @@ class SalesReport extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin/reports/misc-flow'))->with('error', '지자체를 선택해 주세요.'); if (!$lgIdx) return redirect()->to(mgmt_url('reports/misc-flow'))->with('error', '지자체를 선택해 주세요.');
$rules = [ $rules = [
'bmf_type' => 'required|in_list[in,out]', 'bmf_type' => 'required|in_list[in,out]',
@@ -366,7 +360,7 @@ class SalesReport extends BaseController
// 봉투명 조회 // 봉투명 조회
$kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first(); $kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first();
$detail = $kindO ? model(\App\Models\CodeDetailModel::class)->where('cd_ck_idx', $kindO->ck_idx)->where('cd_code', $bagCode)->first() : null; $detail = $kindO ? model(\App\Models\CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $bagCode, $lgIdx) : null;
$bagName = $detail ? $detail->cd_name : ''; $bagName = $detail ? $detail->cd_name : '';
$db = \Config\Database::connect(); $db = \Config\Database::connect();
@@ -383,7 +377,7 @@ class SalesReport extends BaseController
$db->transComplete(); $db->transComplete();
return redirect()->to(site_url('admin/reports/misc-flow'))->with('success', '기타 입출고가 등록되었습니다.'); return redirect()->to(mgmt_url('reports/misc-flow'))->with('success', '기타 입출고가 등록되었습니다.');
} }
/** /**
@@ -393,7 +387,9 @@ class SalesReport extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
}
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01'); $startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d'); $endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
@@ -430,9 +426,6 @@ class SalesReport extends BaseController
// 현재 재고 // 현재 재고
$inventory = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->findAll(); $inventory = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->findAll();
return view('admin/layout', [ return $this->renderWorkPage('봉투 수불 현황', 'admin/sales_report/supply_demand', compact('receiving', 'sales', 'issues', 'inventory', 'startDate', 'endDate'));
'title' => '봉투 수불 현황',
'content' => view('admin/sales_report/supply_demand', compact('receiving', 'sales', 'issues', 'inventory', 'startDate', 'endDate')),
]);
} }
} }

View File

@@ -26,37 +26,39 @@ class ShopOrder extends BaseController
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
}
$builder = $this->orderModel->where('so_lg_idx', $lgIdx); $builder = $this->orderModel->where('so_lg_idx', $lgIdx);
$startDate = $this->request->getGet('start_date'); $startDate = $this->request->getGet('start_date');
$endDate = $this->request->getGet('end_date'); $endDate = $this->request->getGet('end_date');
if ($startDate) $builder->where('so_delivery_date >=', $startDate); if ($startDate) {
if ($endDate) $builder->where('so_delivery_date <=', $endDate); $builder->where('so_delivery_date >=', $startDate);
}
if ($endDate) {
$builder->where('so_delivery_date <=', $endDate);
}
$list = $builder->orderBy('so_idx', 'DESC')->paginate(20); $list = $builder->orderBy('so_idx', 'DESC')->paginate(20);
$pager = $this->orderModel->pager; $pager = $this->orderModel->pager;
return view('admin/layout', [ return $this->renderWorkPage('주문 접수 관리', 'admin/shop_order/index', compact('list', 'startDate', 'endDate', 'pager'));
'title' => '주문 접수 관리',
'content' => view('admin/shop_order/index', compact('list', 'startDate', 'endDate', 'pager')),
]);
} }
public function create() public function create()
{ {
helper('admin'); helper('admin');
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
if (!$lgIdx) return redirect()->to(site_url('admin/shop-orders'))->with('error', '지자체를 선택해 주세요.'); if (! $lgIdx) {
return redirect()->to(mgmt_url('shop-orders'))->with('error', '지자체를 선택해 주세요.');
}
$shops = model(DesignatedShopModel::class)->where('ds_lg_idx', $lgIdx)->where('ds_state', 1)->findAll(); $shops = model(DesignatedShopModel::class)->where('ds_lg_idx', $lgIdx)->where('ds_state', 1)->findAll();
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true) : []; $bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
return view('admin/layout', [ return $this->renderWorkPage('주문 접수', 'admin/shop_order/create', compact('shops', 'bagCodes'));
'title' => '주문 접수',
'content' => view('admin/shop_order/create', compact('shops', 'bagCodes')),
]);
} }
public function store() public function store()
@@ -65,9 +67,9 @@ class ShopOrder extends BaseController
$lgIdx = admin_effective_lg_idx(); $lgIdx = admin_effective_lg_idx();
$rules = [ $rules = [
'so_ds_idx' => 'required|is_natural_no_zero', 'so_ds_idx' => 'required|is_natural_no_zero',
'so_delivery_date'=> 'required|valid_date[Y-m-d]', 'so_delivery_date' => 'required|valid_date[Y-m-d]',
'so_payment_type' => 'required|in_list[이체,가상계좌]', 'so_payment_type' => 'required|in_list[이체,가상계좌]',
]; ];
if (! $this->validate($rules)) { if (! $this->validate($rules)) {
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
@@ -77,57 +79,62 @@ class ShopOrder extends BaseController
$db->transStart(); $db->transStart();
$dsIdx = (int) $this->request->getPost('so_ds_idx'); $dsIdx = (int) $this->request->getPost('so_ds_idx');
$shop = model(DesignatedShopModel::class)->find($dsIdx); $shop = model(DesignatedShopModel::class)->find($dsIdx);
$this->orderModel->insert([ $this->orderModel->insert([
'so_lg_idx' => $lgIdx, 'so_lg_idx' => $lgIdx,
'so_ds_idx' => $dsIdx, 'so_ds_idx' => $dsIdx,
'so_ds_name' => $shop ? $shop->ds_name : '', 'so_ds_name' => $shop ? $shop->ds_name : '',
'so_order_date' => date('Y-m-d'), 'so_order_date' => date('Y-m-d'),
'so_delivery_date'=> $this->request->getPost('so_delivery_date'), 'so_delivery_date' => $this->request->getPost('so_delivery_date'),
'so_payment_type' => $this->request->getPost('so_payment_type'), 'so_payment_type' => $this->request->getPost('so_payment_type'),
'so_status' => 'normal', 'so_status' => 'normal',
'so_orderer_idx' => session()->get('mb_idx'), 'so_orderer_idx' => session()->get('mb_idx'),
'so_regdate' => date('Y-m-d H:i:s'), 'so_regdate' => date('Y-m-d H:i:s'),
]); ]);
$soIdx = (int) $this->orderModel->getInsertID(); $soIdx = (int) $this->orderModel->getInsertID();
$bagCodes = $this->request->getPost('item_bag_code') ?? []; $bagCodes = $this->request->getPost('item_bag_code') ?? [];
$qtys = $this->request->getPost('item_qty') ?? []; $qtys = $this->request->getPost('item_qty') ?? [];
$totalQty = 0; $totalAmt = 0; $totalQty = 0;
$totalAmt = 0;
foreach ($bagCodes as $i => $code) { foreach ($bagCodes as $i => $code) {
if (empty($code) || empty($qtys[$i])) continue; if (empty($code) || empty($qtys[$i])) {
continue;
}
$qty = (int) $qtys[$i]; $qty = (int) $qtys[$i];
$price = model(BagPriceModel::class)->where('bp_lg_idx', $lgIdx)->where('bp_bag_code', $code)->where('bp_state', 1)->first(); $price = model(BagPriceModel::class)->latestActiveByBagCode($lgIdx, (string) $code);
$unitPrice = $price ? (float) $price->bp_consumer : 0; $unitPrice = $price ? (float) $price->bp_consumer : 0;
$amount = $unitPrice * $qty; $amount = $unitPrice * $qty;
$unit = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_bag_code', $code)->where('pu_state', 1)->first(); $unit = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_bag_code', $code)->where('pu_state', 1)->first();
$boxCount = 0; $packCount = 0; $sheetCount = $qty; $boxCount = 0;
$packCount = 0;
$sheetCount = $qty;
if ($unit && (int) $unit->pu_total_per_box > 0) { if ($unit && (int) $unit->pu_total_per_box > 0) {
$boxCount = intdiv($qty, (int) $unit->pu_total_per_box); $boxCount = intdiv($qty, (int) $unit->pu_total_per_box);
$remainder = $qty % (int) $unit->pu_total_per_box; $remainder = $qty % (int) $unit->pu_total_per_box;
if ((int) $unit->pu_pack_per_sheet > 0) { if ((int) $unit->pu_pack_per_sheet > 0) {
$packCount = intdiv($remainder, (int) $unit->pu_pack_per_sheet); $packCount = intdiv($remainder, (int) $unit->pu_pack_per_sheet);
$sheetCount = $remainder % (int) $unit->pu_pack_per_sheet; $sheetCount = $remainder % (int) $unit->pu_pack_per_sheet;
} }
} }
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
$detail = $kindO ? model(CodeDetailModel::class)->where('cd_ck_idx', $kindO->ck_idx)->where('cd_code', $code)->first() : null; $detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $code, $lgIdx) : null;
$this->itemModel->insert([ $this->itemModel->insert([
'soi_so_idx' => $soIdx, 'soi_so_idx' => $soIdx,
'soi_bag_code' => $code, 'soi_bag_code' => $code,
'soi_bag_name' => $detail ? $detail->cd_name : '', 'soi_bag_name' => $detail ? $detail->cd_name : '',
'soi_unit_price' => $unitPrice, 'soi_unit_price' => $unitPrice,
'soi_qty' => $qty, 'soi_qty' => $qty,
'soi_amount' => $amount, 'soi_amount' => $amount,
'soi_box_count' => $boxCount, 'soi_box_count' => $boxCount,
'soi_pack_count' => $packCount, 'soi_pack_count' => $packCount,
'soi_sheet_count'=> $sheetCount, 'soi_sheet_count' => $sheetCount,
]); ]);
$totalQty += $qty; $totalQty += $qty;
@@ -137,18 +144,19 @@ class ShopOrder extends BaseController
$this->orderModel->update($soIdx, ['so_total_qty' => $totalQty, 'so_total_amount' => $totalAmt]); $this->orderModel->update($soIdx, ['so_total_qty' => $totalQty, 'so_total_amount' => $totalAmt]);
$db->transComplete(); $db->transComplete();
return redirect()->to(site_url('admin/shop-orders'))->with('success', '주문이 접수되었습니다.'); return redirect()->to(mgmt_url('shop-orders'))->with('success', '주문이 접수되었습니다.');
} }
public function cancel(int $id) public function cancel(int $id)
{ {
helper('admin'); helper('admin');
$order = $this->orderModel->find($id); $order = $this->orderModel->find($id);
if (!$order || (int) $order->so_lg_idx !== admin_effective_lg_idx()) { if (! $order || (int) $order->so_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(site_url('admin/shop-orders'))->with('error', '주문을 찾을 수 없습니다.'); return redirect()->to(mgmt_url('shop-orders'))->with('error', '주문을 찾을 수 없습니다.');
} }
$this->orderModel->update($id, ['so_status' => 'cancelled']); $this->orderModel->update($id, ['so_status' => 'cancelled']);
return redirect()->to(site_url('admin/shop-orders'))->with('success', '주문이 취소되었습니다.');
return redirect()->to(mgmt_url('shop-orders'))->with('success', '주문이 취소되었습니다.');
} }
} }

View File

@@ -121,8 +121,10 @@ class User extends BaseController
if (! $member) { if (! $member) {
return redirect()->to(site_url('admin/users'))->with('error', '회원을 찾을 수 없습니다.'); return redirect()->to(site_url('admin/users'))->with('error', '회원을 찾을 수 없습니다.');
} }
$member->mb_email = pii_decrypt($member->mb_email ?? ''); $email = pii_decrypt($member->mb_email ?? '');
$member->mb_phone = pii_decrypt($member->mb_phone ?? ''); $phone = pii_decrypt($member->mb_phone ?? '');
$member->mb_email = $email;
$member->mb_phone = $phone;
return view('admin/layout', [ return view('admin/layout', [
'title' => '회원 수정', 'title' => '회원 수정',
'content' => view('admin/user/edit', [ 'content' => view('admin/user/edit', [
@@ -177,6 +179,23 @@ class User extends BaseController
return redirect()->to(site_url('admin/users'))->with('success', '회원 정보가 수정되었습니다.'); return redirect()->to(site_url('admin/users'))->with('success', '회원 정보가 수정되었습니다.');
} }
/**
* 로그인 실패 누적 잠금(mb_locked_until) 해제 — 비밀번호는 그대로 두고 재시도만 가능하게 함
*/
public function unlockLogin(int $id)
{
$member = $this->memberModel->find($id);
if (! $member) {
return redirect()->to(site_url('admin/users'))->with('error', '회원을 찾을 수 없습니다.');
}
$this->memberModel->update($id, [
'mb_login_fail_count' => 0,
'mb_locked_until' => null,
]);
return redirect()->back()->with('success', '로그인 잠금이 해제되었습니다.');
}
/** /**
* 현재 로그인한 관리자가 부여 가능한 역할 목록. * 현재 로그인한 관리자가 부여 가능한 역할 목록.
* super/본부만 4·5 부여 가능, 지자체 관리자는 1~3만. * super/본부만 4·5 부여 가능, 지자체 관리자는 1~3만.

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Controllers\Admin;
use App\Controllers\BaseController;
/**
* 구 관리자 업무 URL(admin/…) → 메인 사이트 업무 URL(bag/…) 영구 이전.
* POST 폼은 307로 메서드·본문 유지를 시도하고, GET 은 301.
*/
class WorkMovedToBag extends BaseController
{
public function toBag(string $prefix, string $rest = ''): \CodeIgniter\HTTP\RedirectResponse
{
$rest = trim($rest, '/');
if ($prefix === 'packaging-units') {
$path = 'packaging-units/manage';
if ($rest !== '') {
$path .= '/' . $rest;
}
} else {
$path = $prefix;
if ($rest !== '') {
$path .= '/' . $rest;
}
}
$target = site_url('bag/' . $path);
$query = $this->request->getUri()->getQuery();
if ($query !== '') {
$target .= '?' . $query;
}
$code = $this->request->getMethod() === 'post' ? 307 : 301;
return redirect()->to($target)->setStatusCode($code);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -42,4 +42,34 @@ abstract class BaseController extends Controller
// Preload any models, libraries, etc, here. // Preload any models, libraries, etc, here.
// $this->session = service('session'); // $this->session = service('session');
} }
/**
* /admin/* 또는 /bag/* 업무 화면 공통: 요청이 bag 이면 메인 사이트 레이아웃, 아니면 관리자 레이아웃.
*
* @param array<string, mixed> $contentData
*/
protected function renderWorkPage(string $title, string $contentView, array $contentData = []): string
{
$content = view($contentView, $contentData);
helper('admin');
$path = function_exists('current_nav_request_path') ? current_nav_request_path() : '';
if ($path === '') {
$uri = service('request')->getUri();
$path = trim((string) $uri->getPath(), '/');
}
while (str_starts_with($path, 'index.php/')) {
$path = substr($path, strlen('index.php/'));
}
if ($path === 'bag' || str_starts_with($path, 'bag/')) {
return view('bag/layout/main', [
'title' => $title,
'content' => $content,
]);
}
return view('admin/layout', [
'title' => $title,
'content' => $content,
]);
}
} }

View File

@@ -16,11 +16,16 @@ class Home extends BaseController
} }
/** /**
* 로그인 후 원래 메인 화면 (admin 유사 레이아웃 + site 메뉴 호버 드롭다운) * 로그인 후 메인 — site 메뉴 레이아웃 + 종합·그래프(blend) 본문
*/ */
public function dashboard() public function dashboard()
{ {
return view('bag/daily_inventory'); return view('bag/layout/main', [
'title' => '업무 현황 · 종합·그래프',
'content' => view('bag/dashboard_blend_inner', [
'lgLabel' => $this->resolveLgLabel(),
]),
]);
} }
/** /**
@@ -62,13 +67,11 @@ class Home extends BaseController
} }
/** /**
* dense(표·KPI) + charts(Chart.js) 혼합. URL: /dashboard/blend * /dashboard 와 동일 본문(호환 URL)
*/ */
public function dashboardBlend() public function dashboardBlend()
{ {
return view('bag/lg_dashboard_blend', [ return $this->dashboard();
'lgLabel' => $this->resolveLgLabel(),
]);
} }
/** /**
@@ -114,4 +117,5 @@ class Home extends BaseController
return '북구 (데모)'; return '북구 (데모)';
} }
} }

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Filters;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
/**
* 로그인만 필요 (mb_level 무관). 기본코드 조회 등 시민·판매소도 접근 가능한 /admin/* 하위용.
*/
class LoginAuthFilter implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null)
{
if (! session()->get('logged_in')) {
return redirect()->to(site_url('login'))->with('error', '로그인이 필요합니다.');
}
return null;
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
return $response;
}
}

View File

@@ -6,8 +6,9 @@ use Config\Roles;
if (! function_exists('admin_effective_lg_idx')) { if (! function_exists('admin_effective_lg_idx')) {
/** /**
* 현재 로그인한 관리자가 작업 대상으로 사용하는 지자체 PK. * 관리자 화면·사이트 메뉴·Bag 등에서 쓰는 작업 지자체 PK.
* Super/본부 관리자 → admin_selected_lg_idx, 지자체 관리자 → mb_lg_idx, 그 외 null. * Super/본부 → admin_selected_lg_idx(미선택 시 null).
* 지자체관리자·지정판매소·일반 사용자 → mb_lg_idx(없으면 null).
*/ */
function admin_effective_lg_idx(): ?int function admin_effective_lg_idx(): ?int
{ {
@@ -16,7 +17,9 @@ if (! function_exists('admin_effective_lg_idx')) {
$idx = session()->get('admin_selected_lg_idx'); $idx = session()->get('admin_selected_lg_idx');
return $idx !== null && $idx !== '' ? (int) $idx : null; return $idx !== null && $idx !== '' ? (int) $idx : null;
} }
if ($level === Roles::LEVEL_LOCAL_ADMIN) { if ($level === Roles::LEVEL_LOCAL_ADMIN
|| $level === Roles::LEVEL_SHOP
|| $level === Roles::LEVEL_CITIZEN) {
$idx = session()->get('mb_lg_idx'); $idx = session()->get('mb_lg_idx');
return $idx !== null && $idx !== '' ? (int) $idx : null; return $idx !== null && $idx !== '' ? (int) $idx : null;
} }
@@ -24,6 +27,23 @@ if (! function_exists('admin_effective_lg_idx')) {
} }
} }
if (! function_exists('resolve_site_menu_lg_idx')) {
/**
* site 상단 메뉴(menu 테이블) 조회용 지자체 PK.
* admin_effective_lg_idx() 우선(메뉴 관리·Bag과 동일), 없으면 mb_lg_idx, 그다음 기본 1.
*/
function resolve_site_menu_lg_idx(): int
{
$lgIdx = admin_effective_lg_idx();
if ($lgIdx !== null) {
return $lgIdx;
}
$raw = session()->get('mb_lg_idx');
return ($raw !== null && $raw !== '') ? (int) $raw : 1;
}
}
if (! function_exists('get_admin_nav_items')) { if (! function_exists('get_admin_nav_items')) {
/** /**
* 관리자 상단 메뉴 항목 (DB menu 테이블, admin 타입, 현재 지자체·mb_level 기준, 평면 배열). * 관리자 상단 메뉴 항목 (DB menu 테이블, admin 타입, 현재 지자체·mb_level 기준, 평면 배열).
@@ -130,22 +150,31 @@ if (! function_exists('get_site_nav_tree')) {
function get_site_nav_tree(): array function get_site_nav_tree(): array
{ {
try { try {
$lgIdx = session()->get('mb_lg_idx'); $lgIdx = resolve_site_menu_lg_idx();
// 시민 등 지자체 정보가 세션에 없으면 기본 지자체(1) 기준으로 메뉴를 보여 준다.
if ($lgIdx === null || $lgIdx === '') {
$lgIdx = 1;
}
$typeRow = model(\App\Models\MenuTypeModel::class)->getByCode('site');
if (! $typeRow) {
return [];
}
$mbLevel = (int) session()->get('mb_level'); $mbLevel = (int) session()->get('mb_level');
$menuModel = model(\App\Models\MenuModel::class); $menuModel = model(\App\Models\MenuModel::class);
$flat = $menuModel->getVisibleByLevel((int) $typeRow->mt_idx, $mbLevel, (int) $lgIdx); $typeRow = model(\App\Models\MenuTypeModel::class)->getByCode('site');
$siteMtIdx = $typeRow ? (int) $typeRow->mt_idx : 0;
if ($siteMtIdx <= 0) {
// 운영 DB 불일치 대비: menu_type 누락 시 legacy site mt_idx(4)로 시도
$siteMtIdx = 4;
}
$flat = $menuModel->getVisibleByLevel($siteMtIdx, $mbLevel, (int) $lgIdx);
// 현재 지자체에 site 메뉴가 없으면, 기본 지자체(1)의 메뉴를 한 번 복사한 뒤 다시 시도 // 현재 지자체에 site 메뉴가 없으면, 기본 지자체(1)의 메뉴를 한 번 복사한 뒤 다시 시도
if (empty($flat)) { if (empty($flat)) {
$menuModel->copyDefaultsFromLg((int) $typeRow->mt_idx, 1, (int) $lgIdx); $menuModel->copyDefaultsFromLg($siteMtIdx, 1, (int) $lgIdx);
$flat = $menuModel->getVisibleByLevel((int) $typeRow->mt_idx, $mbLevel, (int) $lgIdx); $flat = $menuModel->getVisibleByLevel($siteMtIdx, $mbLevel, (int) $lgIdx);
}
// site 타입 매핑 불일치(예: menu_type=2, menu 데이터=4) 보정
if (empty($flat) && $siteMtIdx !== 4) {
$legacyMtIdx = 4;
$flat = $menuModel->getVisibleByLevel($legacyMtIdx, $mbLevel, (int) $lgIdx);
if (empty($flat)) {
$menuModel->copyDefaultsFromLg($legacyMtIdx, 1, (int) $lgIdx);
$flat = $menuModel->getVisibleByLevel($legacyMtIdx, $mbLevel, (int) $lgIdx);
}
} }
if (empty($flat)) { if (empty($flat)) {
return []; return [];
@@ -156,3 +185,381 @@ if (! function_exists('get_site_nav_tree')) {
} }
} }
} }
if (! function_exists('current_nav_request_path')) {
/**
* 메뉴 활성·mm_link 비교용 현재 경로 (라우트 기준, base_url 뒤 세그먼트).
* request->getPath() · uri_string() · SiteURI::getRoutePath() 중 비어 있지 않은 값을 사용.
*/
function current_nav_request_path(): string
{
helper('url');
$request = service('request');
// 프레임워크 권장: uri_string() = baseURL 기준 경로 (우선)
$candidates = [trim(uri_string(), '/')];
if ($request instanceof \CodeIgniter\HTTP\IncomingRequest) {
$candidates[] = trim((string) $request->getPath(), '/');
}
$uri = $request->getUri();
if ($uri instanceof \CodeIgniter\HTTP\SiteURI) {
$candidates[] = trim($uri->getRoutePath(), '/');
}
$path = '';
foreach ($candidates as $c) {
if ($c !== '') {
$path = $c;
break;
}
}
while (str_starts_with($path, 'index.php/')) {
$path = substr($path, strlen('index.php/'));
}
// baseURL 에 경로가 있으면(서브폴더 설치) URI 앞에 붙은 동일 접두 제거
$basePath = parse_url(config(\Config\App::class)->baseURL, PHP_URL_PATH);
$basePath = is_string($basePath) ? trim($basePath, '/') : '';
if ($basePath !== '' && $path !== '' && ($path === $basePath || str_starts_with($path, $basePath . '/'))) {
$path = $path === $basePath ? '' : substr($path, strlen($basePath) + 1);
}
return $path;
}
}
if (! function_exists('normalize_menu_link_for_url')) {
/**
* menu.mm_link 를 base_url() 인자로 쓸 수 있는 상대 경로로 정규화합니다.
* http(s)://... 전체 URL이면 path 만 사용하고, 앞뒤 공백·슬래시를 정리합니다.
*/
function normalize_menu_link_for_url(?string $mmLink): string
{
$s = trim((string) $mmLink);
if ($s === '') {
return '';
}
if (str_contains($s, '://')) {
$path = parse_url($s, PHP_URL_PATH);
$s = is_string($path) ? trim($path, '/') : '';
} else {
$s = trim($s, '/');
}
while (str_starts_with($s, 'index.php/')) {
$s = substr($s, strlen('index.php/'));
}
if (str_starts_with($s, 'public/')) {
$s = substr($s, strlen('public/'));
}
$basePath = parse_url(config(\Config\App::class)->baseURL, PHP_URL_PATH);
$basePath = is_string($basePath) ? trim($basePath, '/') : '';
if ($basePath !== '' && $s !== '' && ($s === $basePath || str_starts_with($s, $basePath . '/'))) {
$s = $s === $basePath ? '' : substr($s, strlen($basePath) + 1);
}
return $s;
}
}
if (! function_exists('mgmt_url')) {
/**
* 업무 화면 링크: 정식 URL 은 /bag/* (adminAuth). 포장 단위 CRUD 는 packaging-units/manage 로 치환.
*/
function mgmt_url(string $path): string
{
helper('url');
$path = trim($path, '/');
// bag/packaging-units 는 조회 전용(Bag) — CRUD 는 /bag/packaging-units/manage/* 로 분리
if ($path === 'packaging-units') {
$path = 'packaging-units/manage';
} elseif (str_starts_with($path, 'packaging-units/')) {
$path = 'packaging-units/manage/' . substr($path, strlen('packaging-units/'));
}
return site_url('bag/' . $path);
}
}
if (! function_exists('work_area_home_url')) {
/**
* 지자체 미선택 등으로 돌아갈 때: bag 업무 중이면 대시보드, 관리자면 admin 홈.
*/
function work_area_home_url(): string
{
helper('url');
$seg1 = service('request')->getUri()->getSegment(1);
return ($seg1 === 'bag') ? site_url('dashboard') : site_url('admin');
}
}
if (! function_exists('format_ymd_korean')) {
/**
* Y-m-d 날짜를 '2026년 1월 5일' 형식으로 (월·일은 숫자, 월명은 한글 '월').
*/
function format_ymd_korean(?string $ymd): string
{
if ($ymd === null || trim($ymd) === '') {
return '—';
}
$t = \DateTimeImmutable::createFromFormat('Y-m-d', trim($ymd));
if ($t === false) {
return $ymd;
}
return $t->format('Y') . '년 ' . (int) $t->format('n') . '월 ' . (int) $t->format('j') . '일';
}
}
if (! function_exists('parse_ymd_from_triple')) {
/**
* 연·월·일 GET 값으로 Y-m-d 생성 (유효하지 않은 날짜는 null).
*/
function parse_ymd_from_triple(?string $y, ?string $m, ?string $d): ?string
{
if ($y === null || $y === '' || $m === null || $m === '' || $d === null || $d === '') {
return null;
}
$yi = (int) $y;
$mi = (int) $m;
$di = (int) $d;
if ($yi < 1000 || $yi > 9999 || ! checkdate($mi, $di, $yi)) {
return null;
}
return sprintf('%04d-%02d-%02d', $yi, $mi, $di);
}
}
if (! function_exists('site_nav_resolved_link_path')) {
/**
* 사이트 상단 메뉴 URL 세그먼트. mm_link(DB)만 사용 (비어 있으면 빈 문자열).
*
* @param string|null $mmName 호환용(미사용).
*
* @return string base_url() 인자 세그먼트(앞뒤 슬래시 없음)
*/
function site_nav_resolved_link_path(?string $mmLink, ?string $mmName = null): string
{
return normalize_menu_link_for_url($mmLink);
}
}
if (! function_exists('menu_link_candidate_paths')) {
/**
* 활성 비교용 경로 후보. DB에 "menus" 처럼 짧게 넣은 경우 실제 URI가 admin/menus·bag/… 일 수 있어,
* 현재 요청 경로에 맞게 admin/·bag/ 접두를 붙인 후보도 만든다. (슬래시 포함·admin 단독은 그대로 1개만)
*
* @return list<string>
*/
function menu_link_candidate_paths(?string $mmLink, string $currentPath): array
{
$p = normalize_menu_link_for_url($mmLink);
if ($p === '') {
return [];
}
if (str_contains($p, '/') || $p === 'admin') {
$cands = [$p];
if (preg_match('#^bag/packaging-units/manage(/.*)?$#', $p, $m)) {
$cands[] = 'admin/packaging-units' . ($m[1] ?? '');
} elseif (preg_match('#^admin/packaging-units(/.*)?$#', $p, $m)) {
$cands[] = 'bag/packaging-units/manage' . ($m[1] ?? '');
} elseif (str_starts_with($p, 'admin/')) {
$cands[] = 'bag/' . substr($p, strlen('admin/'));
} elseif (str_starts_with($p, 'bag/')) {
$cands[] = 'admin/' . substr($p, strlen('bag/'));
}
return array_values(array_unique($cands));
}
$out = [$p];
if (str_starts_with($currentPath, 'admin/') || $currentPath === 'admin') {
$out[] = 'admin/' . $p;
}
if (str_starts_with($currentPath, 'bag/') || $currentPath === 'bag') {
$out[] = 'bag/' . $p;
}
return array_values(array_unique($out));
}
}
if (! function_exists('menu_link_preferred_href_path')) {
/**
* base_url() 용 경로: 짧게 저장된 mm_link 는 현재 요청 기준 admin/·bag/ 후보 중 가장 알맞은 것 사용.
*/
function menu_link_preferred_href_path(?string $mmLink, string $currentPath): string
{
$cands = menu_link_candidate_paths($mmLink, $currentPath);
if ($cands === []) {
return '';
}
foreach ($cands as $c) {
$cl = strtolower($currentPath);
$cc = strtolower($c);
if ($cl === $cc || str_starts_with($cl, $cc . '/')) {
return $c;
}
}
foreach ($cands as $c) {
if (str_contains($c, '/')) {
return $c;
}
}
return $cands[0];
}
}
if (! function_exists('menu_single_path_matches_request')) {
/**
* 단일 정규 경로가 현재 요청 path 와 일치하는지.
*
* @param list<string> $dashboardPathAliases
*/
function menu_single_path_matches_request(string $path, string $currentPath, array $dashboardPathAliases = []): bool
{
if ($path === '') {
return false;
}
$pathLower = strtolower($path);
$currentLower = strtolower($currentPath);
$aliasesLower = array_map(strtolower(...), $dashboardPathAliases);
if ($dashboardPathAliases !== []
&& in_array($pathLower, $aliasesLower, true)
&& in_array($currentLower, $aliasesLower, true)) {
return true;
}
if ($currentLower === $pathLower) {
return true;
}
if ($pathLower === 'admin') {
return false;
}
return str_starts_with($currentLower, $pathLower . '/');
}
}
if (! function_exists('menu_link_matches_request')) {
/**
* 메뉴 mm_link(DB)가 현재 요청과 같은 메뉴인지. 비어 있으면 false.
*
* @param list<string> $dashboardPathAliases
*/
function menu_link_matches_request(?string $mmLink, string $currentPath, array $dashboardPathAliases = []): bool
{
foreach (menu_link_candidate_paths($mmLink, $currentPath) as $cand) {
if (menu_single_path_matches_request($cand, $currentPath, $dashboardPathAliases)) {
return true;
}
}
return false;
}
}
if (! function_exists('site_nav_link_matches_current')) {
/**
* 사이트 상단 메뉴 활성 여부 (경로 후보·대시보드 별칭은 menu_link_matches_request 와 동일).
*
* @param list<string> $dashboardPathAliases
*/
function site_nav_link_matches_current(?string $mmLink, string $currentPath, array $dashboardPathAliases = []): bool
{
return menu_link_matches_request($mmLink, $currentPath, $dashboardPathAliases);
}
}
if (! function_exists('menu_active_child_for_parent')) {
/**
* 같은 부모 아래 형제 소메뉴 중, 현재 요청에 해당하는 항목을 하나만 고른다.
*
* 짧은 mm_link(예: bag/designated-shops)가 긴 경로(bag/designated-shops/browse)와
* 동시에 prefix 규칙으로 매칭될 때, 가장 구체적인 경로(일치한 후보 문자열 길이 최대)만 활성으로 본다.
* 길이가 같으면 mm_num이 작은 항목을 선택(동일 URL이 여러 메뉴에 매핑된 경우 등).
*
* @param object{children?: array<int, object>} $parentNavItem
* @param list<string> $dashboardPathAliases
*
* @return object|null 활성으로 표시할 자식 노드(mm_idx 등 포함), 없으면 null
*/
function menu_active_child_for_parent(object $parentNavItem, string $currentPath, array $dashboardPathAliases = []): ?object
{
$children = $parentNavItem->children ?? [];
if ($children === []) {
return null;
}
$best = null;
$bestLen = -1;
$bestMmNum = PHP_INT_MAX;
foreach ($children as $child) {
$mmLink = $child->mm_link ?? null;
$maxLen = -1;
foreach (menu_link_candidate_paths($mmLink, $currentPath) as $cand) {
if (menu_single_path_matches_request($cand, $currentPath, $dashboardPathAliases)) {
$maxLen = max($maxLen, strlen($cand));
}
}
if ($maxLen < 0) {
continue;
}
$mmNum = (int) ($child->mm_num ?? 0);
if ($maxLen > $bestLen || ($maxLen === $bestLen && $mmNum < $bestMmNum)) {
$bestLen = $maxLen;
$bestMmNum = $mmNum;
$best = $child;
}
}
return $best;
}
}
if (! function_exists('site_nav_active_child_for_parent')) {
/**
* 사이트 상단 메뉴 전용 호환 래퍼.
*
* @param object{children?: array<int, object>} $parentNavItem
* @param list<string> $dashboardPathAliases
*
* @return object|null
*/
function site_nav_active_child_for_parent(object $parentNavItem, string $currentPath, array $dashboardPathAliases = []): ?object
{
return menu_active_child_for_parent($parentNavItem, $currentPath, $dashboardPathAliases);
}
}
if (! function_exists('session_user_nav_display')) {
/**
* 상단 메뉴바용: 로그인 사용자 이름·역할 표시
*
* @return array{name: string, role_label: string}|null
*/
function session_user_nav_display(): ?array
{
if (! session()->get('logged_in')) {
return null;
}
$name = trim((string) session()->get('mb_name'));
if ($name === '') {
$name = (string) session()->get('mb_id');
}
$level = (int) session()->get('mb_level');
$roleLabel = config('Roles')->getLevelName($level);
return [
'name' => $name,
'role_label' => $roleLabel,
];
}
}

View File

@@ -8,6 +8,7 @@ declare(strict_types=1);
* *
* 저장 형식: 암호화된 값은 "ENC:" + base64(암호문) 으로 저장. "ENC:" 없으면 평문(기존)으로 간주. * 저장 형식: 암호화된 값은 "ENC:" + base64(암호문) 으로 저장. "ENC:" 없으면 평문(기존)으로 간주.
*/ */
if (! function_exists('pii_encrypt')) { if (! function_exists('pii_encrypt')) {
function pii_encrypt(?string $value): string function pii_encrypt(?string $value): string
{ {
@@ -21,9 +22,8 @@ if (! function_exists('pii_encrypt')) {
} }
$encrypter = service('encrypter'); $encrypter = service('encrypter');
$encrypted = $encrypter->encrypt($value); $encrypted = $encrypter->encrypt($value);
return 'ENC:' . base64_encode($encrypted); return 'ENC:' . base64_encode($encrypted);
} catch (Throwable) { } catch (Throwable $e) {
return $value; return $value;
} }
} }
@@ -44,13 +44,23 @@ if (! function_exists('pii_decrypt')) {
return $value; return $value;
} }
$encrypter = service('encrypter'); $encrypter = service('encrypter');
$raw = base64_decode(substr($value, 4), true); $payload = substr($value, 4);
if ($raw === false) {
return $value; // 현재 포맷: ENC: + base64(raw ciphertext)
$raw = base64_decode($payload, true);
if ($raw !== false) {
try {
return $encrypter->decrypt($raw);
} catch (Throwable $e) {
// legacy 포맷 재시도
}
} }
return $encrypter->decrypt($raw); // 레거시 포맷 호환:
} catch (Throwable) { // - ENC: + encrypter 반환값(rawData=false 환경 등) 또는
// - ENC: + 기타 문자열 포맷
return $encrypter->decrypt($payload);
} catch (Throwable $e) {
return $value; return $value;
} }
} }

View File

@@ -10,9 +10,26 @@ class BagOrderModel extends Model
protected $primaryKey = 'bo_idx'; protected $primaryKey = 'bo_idx';
protected $returnType = 'object'; protected $returnType = 'object';
protected $useTimestamps = false; protected $useTimestamps = false;
/**
* 동일 발주 UUID에 대해 bo_version이 최대인 행만 (수정으로 생긴 이전 버전 행은 목록·이력에서 제외).
* DB에는 버전별 행이 그대로 남고, 조회 시에만 필터한다.
*/
public function whereLatestHead(int $lgIdx): self
{
$lg = (int) $lgIdx;
return $this->where(
"(bo_uuid, bo_version) IN (SELECT bo_uuid, MAX(bo_version) FROM {$this->table} WHERE bo_lg_idx = {$lg} GROUP BY bo_uuid)",
null,
false
);
}
protected $allowedFields = [ protected $allowedFields = [
'bo_uuid', 'bo_version', 'bo_lg_idx', 'bo_gugun_code', 'bo_dong_code', 'bo_uuid', 'bo_version', 'bo_lg_idx', 'bo_gugun_code', 'bo_dong_code',
'bo_company_idx', 'bo_agency_idx', 'bo_fee_rate', 'bo_order_date', 'bo_company_idx', 'bo_agency_idx', 'bo_fee_rate', 'bo_order_date',
'bo_bag_types', 'bo_unit_prices', 'bo_qty_boxes',
'bo_lot_no', 'bo_hash', 'bo_status', 'bo_orderer_idx', 'bo_lot_no', 'bo_hash', 'bo_status', 'bo_orderer_idx',
'bo_regdate', 'bo_moddate', 'bo_regdate', 'bo_moddate',
]; ];

View File

@@ -16,4 +16,45 @@ class BagPriceModel extends Model
'bp_start_date', 'bp_end_date', 'bp_state', 'bp_start_date', 'bp_end_date', 'bp_state',
'bp_regdate', 'bp_moddate', 'bp_reg_mb_idx', 'bp_regdate', 'bp_moddate', 'bp_reg_mb_idx',
]; ];
/**
* 같은 봉투코드에 단가 기간이 겹쳐도 "나중 등록 단가"가 우선되도록
* 활성 단가를 등록일/PK 역순으로 정렬해 봉투코드별 1건만 남긴다.
*
* @return array<string, object>
*/
public function latestActiveMapByBagCode(int $lgIdx): array
{
$rows = $this->where('bp_lg_idx', $lgIdx)
->where('bp_state', 1)
->orderBy('bp_regdate', 'DESC')
->orderBy('bp_idx', 'DESC')
->findAll();
$map = [];
foreach ($rows as $row) {
$code = (string) ($row->bp_bag_code ?? '');
if ($code === '' || isset($map[$code])) {
continue;
}
$map[$code] = $row;
}
return $map;
}
public function latestActiveByBagCode(int $lgIdx, string $bagCode): ?object
{
$bagCode = trim($bagCode);
if ($bagCode === '') {
return null;
}
return $this->where('bp_lg_idx', $lgIdx)
->where('bp_bag_code', $bagCode)
->where('bp_state', 1)
->orderBy('bp_regdate', 'DESC')
->orderBy('bp_idx', 'DESC')
->first();
}
} }

View File

@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Models; namespace App\Models;
use CodeIgniter\Model; use CodeIgniter\Model;
@@ -12,6 +14,8 @@ class CodeDetailModel extends Model
protected $useTimestamps = false; protected $useTimestamps = false;
protected $allowedFields = [ protected $allowedFields = [
'cd_ck_idx', 'cd_ck_idx',
'cd_source',
'cd_lg_idx',
'cd_code', 'cd_code',
'cd_name', 'cd_name',
'cd_sort', 'cd_sort',
@@ -20,14 +24,50 @@ class CodeDetailModel extends Model
]; ];
/** /**
* 특정 코드 종류의 세부코드 목록 * 목록 조회: 플랫폼(0) + (선택) 해당 지자체 행
*
* @param int|null $effectiveLgIdx null 또는 1 미만이면 플랫폼 공통만
*/ */
public function getByKind(int $ckIdx, bool $activeOnly = false): array public function filterByTenantScope(?int $effectiveLgIdx): self
{ {
$builder = $this->where('cd_ck_idx', $ckIdx); if ($effectiveLgIdx === null || $effectiveLgIdx < 1) {
if ($activeOnly) { return $this->where('cd_lg_idx', 0);
$builder->where('cd_state', 1);
} }
return $builder->orderBy('cd_sort', 'ASC')->findAll();
return $this->groupStart()
->where('cd_lg_idx', 0)
->orWhere('cd_lg_idx', $effectiveLgIdx)
->groupEnd();
}
/**
* 특정 코드 종류의 세부코드 목록
*
* @param int|null $effectiveLgIdx 테넌트 범위 (null=플랫폼만)
*/
public function getByKind(int $ckIdx, bool $activeOnly = false, ?int $effectiveLgIdx = null): array
{
$this->where('cd_ck_idx', $ckIdx);
$this->filterByTenantScope($effectiveLgIdx);
if ($activeOnly) {
$this->where('cd_state', 1);
}
return $this->orderBy('cd_sort', 'ASC')->orderBy('cd_idx', 'ASC')->findAll();
}
/**
* 동일 세부코드값: 지자체 전용이 있으면 우선, 없으면 플랫폼
*/
public function findResolvedByKindAndCode(int $ckIdx, string $code, ?int $effectiveLgIdx): ?object
{
if ($effectiveLgIdx !== null && $effectiveLgIdx > 0) {
$local = $this->where('cd_ck_idx', $ckIdx)->where('cd_code', $code)->where('cd_lg_idx', $effectiveLgIdx)->first();
if ($local !== null) {
return $local;
}
}
return $this->where('cd_ck_idx', $ckIdx)->where('cd_code', $code)->where('cd_lg_idx', 0)->first();
} }
} }

View File

@@ -17,16 +17,25 @@ class DesignatedShopModel extends Model
'ds_name', 'ds_name',
'ds_biz_no', 'ds_biz_no',
'ds_rep_name', 'ds_rep_name',
'ds_biz_type',
'ds_biz_kind',
'ds_va_number', 'ds_va_number',
'ds_va_bank',
'ds_va_account',
'ds_zip', 'ds_zip',
'ds_addr', 'ds_addr',
'ds_addr_jibun', 'ds_addr_jibun',
'ds_addr_detail',
'ds_tel', 'ds_tel',
'ds_rep_phone', 'ds_rep_phone',
'ds_email', 'ds_email',
'ds_gugun_code', 'ds_gugun_code',
'ds_zone_code',
'ds_branch_no',
'ds_designated_at', 'ds_designated_at',
'ds_state', 'ds_state',
'ds_state_changed_at',
'ds_change_reason',
'ds_regdate', 'ds_regdate',
]; ];
} }

View File

@@ -189,4 +189,67 @@ class MenuModel extends Model
} }
} }
/**
* 특정 메뉴 타입(mt_idx)을 source 지자체 기준으로 모든 지자체에 재배포.
* 기존 대상 지자체의 해당 타입 메뉴는 삭제 후 source 구조로 재생성한다.
*/
public function syncTypeToAllLgs(int $mtIdx, int $sourceLg): void
{
if ($mtIdx <= 0 || $sourceLg <= 0) {
return;
}
$source = $this->where('mt_idx', $mtIdx)
->where('lg_idx', $sourceLg)
->orderBy('mm_dep', 'ASC')
->orderBy('mm_num', 'ASC')
->findAll();
if (empty($source)) {
return;
}
$lgRows = $this->db->table('local_government')
->select('lg_idx')
->orderBy('lg_idx', 'ASC')
->get()
->getResultArray();
foreach ($lgRows as $lgRow) {
$destLg = (int) ($lgRow['lg_idx'] ?? 0);
if ($destLg <= 0 || $destLg === $sourceLg) {
continue;
}
$this->db->transStart();
$this->where('mt_idx', $mtIdx)
->where('lg_idx', $destLg)
->delete();
$idMap = [];
foreach ($source as $row) {
$oldId = (int) ($row->mm_idx ?? 0);
$oldP = (int) ($row->mm_pidx ?? 0);
$newPidx = 0;
if ($oldP > 0 && isset($idMap[$oldP])) {
$newPidx = (int) $idMap[$oldP];
}
$this->insert([
'mt_idx' => $mtIdx,
'lg_idx' => $destLg,
'mm_name' => (string) ($row->mm_name ?? ''),
'mm_link' => (string) ($row->mm_link ?? ''),
'mm_pidx' => $newPidx,
'mm_dep' => (int) ($row->mm_dep ?? 0),
'mm_num' => (int) ($row->mm_num ?? 0),
'mm_cnode' => (int) ($row->mm_cnode ?? 0),
'mm_level' => (string) ($row->mm_level ?? ''),
'mm_is_view' => (string) ($row->mm_is_view ?? 'Y'),
]);
$idMap[$oldId] = (int) $this->getInsertID();
}
$this->db->transComplete();
}
}
} }

View File

@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Models; namespace App\Models;
use CodeIgniter\Model; use CodeIgniter\Model;
@@ -11,7 +13,34 @@ class SalesAgencyModel extends Model
protected $returnType = 'object'; protected $returnType = 'object';
protected $useTimestamps = false; protected $useTimestamps = false;
protected $allowedFields = [ protected $allowedFields = [
'sa_lg_idx', 'sa_name', 'sa_biz_no', 'sa_rep_name', 'sa_lg_idx',
'sa_tel', 'sa_addr', 'sa_state', 'sa_regdate', 'sa_kind',
'sa_code',
'sa_name',
'sa_regdate',
]; ];
/** sales_agency 테이블에 sa_kind, sa_code 컬럼이 있는지(마이그레이션 적용 여부). */
public function hasKindCodeColumns(): bool
{
static $cache = null;
if ($cache === null) {
$cols = db_connect()->getFieldNames($this->table);
$cache = in_array('sa_kind', $cols, true) && in_array('sa_code', $cols, true);
}
return $cache;
}
/**
* 신규 스키마면 구분·코드 순, 아니면 명·PK 순(옛 DB 호환).
*
* @return $this
*/
public function orderForDisplay()
{
return $this->hasKindCodeColumns()
? $this->orderBy('sa_kind', 'ASC')->orderBy('sa_code', 'ASC')
: $this->orderBy('sa_name', 'ASC')->orderBy('sa_idx', 'ASC');
}
} }

View File

@@ -3,7 +3,7 @@
<div class="flex flex-wrap items-center justify-between gap-y-2"> <div class="flex flex-wrap items-center justify-between gap-y-2">
<span class="text-sm font-bold text-gray-700">재고 현황</span> <span class="text-sm font-bold text-gray-700">재고 현황</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a href="<?= base_url('admin/bag-inventory/export') ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a> <a href="<?= mgmt_url('bag-inventory/export') ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button> <button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
</div> </div>
</div> </div>

View File

@@ -2,7 +2,7 @@
<span class="text-sm font-bold text-gray-700">무료용 불출 처리</span> <span class="text-sm font-bold text-gray-700">무료용 불출 처리</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/bag-issues/store') ?>" method="POST" class="space-y-4"> <form action="<?= mgmt_url('bag-issues/store') ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -64,7 +64,7 @@
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
<a href="<?= base_url('admin/bag-issues') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= mgmt_url('bag-issues') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -4,18 +4,18 @@
<span class="text-sm font-bold text-gray-700">무료용 불출 관리</span> <span class="text-sm font-bold text-gray-700">무료용 불출 관리</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button> <button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= base_url('admin/bag-issues/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">불출 처리</a> <a href="<?= mgmt_url('bag-issues/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">불출 처리</a>
</div> </div>
</div> </div>
</section> </section>
<section class="p-2 bg-white border-b border-gray-200"> <section class="p-2 bg-white border-b border-gray-200">
<form method="GET" action="<?= base_url('admin/bag-issues') ?>" class="flex flex-wrap items-center gap-2"> <form method="GET" action="<?= mgmt_url('bag-issues') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">불출일</label> <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"/> <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> <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"/> <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> <button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
<a href="<?= base_url('admin/bag-issues') ?>" class="text-sm text-gray-500 hover:underline">초기화</a> <a href="<?= mgmt_url('bag-issues') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
</form> </form>
</section> </section>
<div class="border border-gray-300 overflow-auto mt-2"> <div class="border border-gray-300 overflow-auto mt-2">
@@ -49,7 +49,7 @@
<td><?= number_format((int) $row->bi2_qty) ?></td> <td><?= number_format((int) $row->bi2_qty) ?></td>
<td class="text-center"><?= esc($row->bi2_status) ?></td> <td class="text-center"><?= esc($row->bi2_status) ?></td>
<td class="text-center"> <td class="text-center">
<form action="<?= base_url('admin/bag-issues/cancel/' . (int) $row->bi2_idx) ?>" method="POST" class="inline" onsubmit="return confirm('취소하시겠습니까?');"> <form action="<?= mgmt_url('bag-issues/cancel/' . (int) $row->bi2_idx) ?>" method="POST" class="inline" onsubmit="return confirm('취소하시겠습니까?');">
<?= csrf_field() ?> <?= csrf_field() ?>
<button type="submit" class="text-orange-600 hover:underline text-sm">취소</button> <button type="submit" class="text-orange-600 hover:underline text-sm">취소</button>
</form> </form>

View File

@@ -1,83 +1,443 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel"> <section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<span class="text-sm font-bold text-gray-700">발주 등록</span> <span class="text-sm font-bold text-gray-700">발주 등록</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-4xl">
<form action="<?= base_url('admin/bag-orders/store') ?>" method="POST" class="space-y-4">
<?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <?php
<label class="block text-sm font-bold text-gray-700 w-28">발주일 <span class="text-red-500">*</span></label> $oldBagCodes = old('item_bag_code');
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bo_order_date" type="date" value="<?= esc(old('bo_order_date', date('Y-m-d'))) ?>" required/> $oldQtyBoxes = old('item_qty_box');
$oldQtySheets = old('item_qty_sheet');
$oldBagCodes = is_array($oldBagCodes) ? $oldBagCodes : [];
$oldQtyBoxes = is_array($oldQtyBoxes) ? $oldQtyBoxes : [];
$oldQtySheets = is_array($oldQtySheets) ? $oldQtySheets : [];
$defaultOrderDate = old('bo_order_date', date('Y-m-d'));
$defaultOrderMonth = old('bo_order_month_ui', substr($defaultOrderDate, 0, 7));
$bagMeta = [];
foreach (($bagReferenceRows ?? []) as $row) {
$bagMeta[$row['code']] = [
'name' => $row['name'],
'orderPrice' => (float) $row['orderPrice'],
'boxPerPack' => (int) $row['boxPerPack'],
'packPerSheet' => (int) $row['packPerSheet'],
'totalPerBox' => max(1, (int) $row['totalPerBox']),
];
}
$initialSelectedItems = [];
$maxOldCount = max(count($oldBagCodes), count($oldQtySheets), count($oldQtyBoxes));
for ($i = 0; $i < $maxOldCount; $i++) {
$code = trim((string) ($oldBagCodes[$i] ?? ''));
if ($code === '' || ! isset($bagMeta[$code])) {
continue;
}
$fallbackQtyBox = (int) ($oldQtyBoxes[$i] ?? 0);
$rawQtySheet = (int) ($oldQtySheets[$i] ?? 0);
$fallbackTotalPerBox = (int) ($bagMeta[$code]['totalPerBox'] ?? 1);
if ($fallbackQtyBox <= 0 && $rawQtySheet > 0) {
$fallbackQtyBox = intdiv($rawQtySheet, max(1, $fallbackTotalPerBox));
}
$initialSelectedItems[] = [
'code' => $code,
'qtyBox' => max(0, $fallbackQtyBox),
];
}
$statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제'];
?>
<form action="<?= mgmt_url('bag-orders/store') ?>" method="POST" class="mt-2 space-y-2">
<?= csrf_field() ?>
<div class="border border-gray-300 bg-white p-2">
<div class="flex flex-wrap items-center gap-4 text-sm">
<div class="flex items-center gap-2">
<label for="bo_order_month_ui" class="font-bold text-gray-700">발주월</label>
<input id="bo_order_month_ui" name="bo_order_month_ui" type="month" value="<?= esc($defaultOrderMonth) ?>" class="border border-gray-300 rounded px-2 py-1" />
</div>
<div class="flex items-center gap-2">
<label for="bo_order_date" class="font-bold text-gray-700">발주일 <span class="text-red-500">*</span></label>
<input id="bo_order_date" name="bo_order_date" type="date" value="<?= esc($defaultOrderDate) ?>" required class="border border-gray-300 rounded px-2 py-1" />
</div>
<p class="text-blue-600 font-bold">※ 발주수량은 박스단위로 입력해 주세요. (발주일은 미래일도 선택 가능)</p>
</div> </div>
</div>
<div class="flex flex-wrap items-center gap-2"> <div class="grid grid-cols-1 xl:grid-cols-12 gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">수수료율</label> <section class="xl:col-span-5 border border-gray-300 bg-white">
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 text-right" name="bo_fee_rate" type="number" step="0.01" value="<?= esc(old('bo_fee_rate', '0')) ?>"/> <div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">발주 이력</div>
<span class="text-sm text-gray-500">%</span> <div class="overflow-auto max-h-[410px]">
</div> <table class="w-full data-table text-sm">
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">제작업체</label>
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bo_company_idx">
<option value="">선택</option>
<?php foreach ($companies as $cp): ?>
<option value="<?= esc($cp->cp_idx) ?>" <?= (int) old('bo_company_idx') === (int) $cp->cp_idx ? 'selected' : '' ?>>
<?= esc($cp->cp_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">입고처</label>
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bo_agency_idx">
<option value="">선택</option>
<?php foreach ($agencies as $ag): ?>
<option value="<?= esc($ag->sa_idx) ?>" <?= (int) old('bo_agency_idx') === (int) $ag->sa_idx ? 'selected' : '' ?>>
<?= esc($ag->sa_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mt-4">
<label class="block text-sm font-bold text-gray-700 mb-2">발주 품목</label>
<div class="border border-gray-300 overflow-auto">
<table class="w-full data-table">
<thead> <thead>
<tr> <tr>
<th class="w-16">순번</th> <th class="w-28">발주일</th>
<th>봉투</th> <th>제작업체</th>
<th class="w-32">박스수</th> <th>입고처</th>
<th class="w-16">상태</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php for ($i = 0; $i < 3; $i++): ?> <?php foreach (($recentOrders ?? []) as $history): ?>
<tr> <tr>
<td class="text-center"><?= $i + 1 ?></td> <td class="text-center"><?= esc((string) $history->bo_order_date) ?></td>
<td> <td class="text-left pl-2"><?= esc((string) ($companyMap[(int) $history->bo_company_idx] ?? '-')) ?></td>
<select class="border border-gray-300 rounded px-2 py-1 text-sm w-full" name="item_bag_code[]"> <td class="text-left pl-2"><?= esc((string) ($agencyMap[(int) $history->bo_agency_idx] ?? '-')) ?></td>
<option value="">선택</option> <td class="text-center"><?= esc((string) ($statusMap[(string) $history->bo_status] ?? $history->bo_status)) ?></td>
<?php foreach ($bagCodes as $cd): ?>
<option value="<?= esc($cd->cd_code) ?>">
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_name) ?>
</option>
<?php endforeach; ?>
</select>
</td>
<td>
<input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right" name="item_qty_box[]" type="number" min="0" value="0"/>
</td>
</tr> </tr>
<?php endfor; ?> <?php endforeach; ?>
<?php if (empty($recentOrders)): ?>
<tr><td colspan="4" class="text-center text-gray-400 py-4">발주 이력이 없습니다.</td></tr>
<?php endif; ?>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </section>
<div class="flex gap-2 pt-2"> <section class="xl:col-span-7 border border-gray-300 bg-white">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button> <div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">발주 Form</div>
<a href="<?= base_url('admin/bag-orders') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <div class="p-2 space-y-2">
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<div class="flex items-center gap-2 text-sm">
<label for="bo_fee_rate" class="w-20 font-bold text-gray-700">수수료</label>
<input id="bo_fee_rate" name="bo_fee_rate" type="number" step="0.01" value="<?= esc(old('bo_fee_rate', '0')) ?>" class="border border-gray-300 rounded px-2 py-1 w-24 text-right" />
<span>%</span>
</div>
<div class="flex items-center gap-2 text-sm">
<label for="bo_association_idx" class="w-20 font-bold text-gray-700">협회</label>
<select id="bo_association_idx" name="bo_association_idx" class="border border-gray-300 rounded px-2 py-1 w-full">
<option value="">선택</option>
<?php foreach (($associations ?? []) as $association): ?>
<option value="<?= esc((string) $association->cp_idx) ?>" <?= (int) old('bo_association_idx') === (int) $association->cp_idx ? 'selected' : '' ?>>
<?= esc((string) $association->cp_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex items-center gap-2 text-sm">
<label for="bo_company_idx" class="w-20 font-bold text-gray-700">제작업체</label>
<select id="bo_company_idx" name="bo_company_idx" class="border border-gray-300 rounded px-2 py-1 w-full">
<option value="">선택</option>
<?php foreach (($companies ?? []) as $company): ?>
<option value="<?= esc((string) $company->cp_idx) ?>" <?= (int) old('bo_company_idx') === (int) $company->cp_idx ? 'selected' : '' ?>>
<?= esc((string) $company->cp_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex items-center gap-2 text-sm">
<label for="bo_agency_idx" class="w-20 font-bold text-gray-700">입고처</label>
<select id="bo_agency_idx" name="bo_agency_idx" class="border border-gray-300 rounded px-2 py-1 w-full">
<option value="">선택</option>
<?php foreach (($agencies ?? []) as $agency): ?>
<option value="<?= esc((string) $agency->sa_idx) ?>" <?= (int) old('bo_agency_idx') === (int) $agency->sa_idx ? 'selected' : '' ?>>
[<?= esc((string) ($agency->sa_kind ?? '')) ?>] <?= esc((string) ($agency->sa_code ?? '')) ?> — <?= esc((string) $agency->sa_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="border border-gray-300 overflow-auto">
<table class="w-full data-table text-sm order-input-table" id="order-item-table">
<thead>
<tr>
<th class="w-12">번호</th>
<th class="w-16">선택</th>
<th>품명</th>
<th class="w-28">수량(BOX)</th>
<th class="w-24">단가</th>
<th class="w-24">환산수량</th>
<th class="w-28">금액</th>
</tr>
</thead>
<tbody id="selected-order-items-body"></tbody>
<tfoot>
<tr>
<th colspan="3" class="text-center">계</th>
<th class="text-right pr-2" id="sum-box-qty">0</th>
<th></th>
<th class="text-right pr-2" id="sum-sheet-qty">0</th>
<th class="text-right pr-2" id="sum-amount">0</th>
</tr>
</tfoot>
</table>
</div>
<div class="flex gap-2 pt-1">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">발주</button>
<a href="<?= mgmt_url('bag-orders') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div>
</div>
</section>
</div>
<section class="border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">발주 등록 종류</div>
<p class="text-xs text-gray-600 px-2 py-1">아래 목록에서 봉투를 선택하면 발주 품목에 추가됩니다. (개수 제한 없음)</p>
<div class="overflow-auto">
<table class="w-full data-table text-sm order-reference-table">
<thead>
<tr>
<th class="w-12">번호</th>
<th class="w-20">선택</th>
<th>봉투 종류</th>
<th class="w-24">발주단가</th>
<th class="w-24">Box당 팩</th>
<th class="w-24">팩당 낱장</th>
<th class="w-28">1박스 총 낱장</th>
</tr>
</thead>
<tbody>
<?php foreach (($bagReferenceRows ?? []) as $idx => $row): ?>
<tr data-reference-row data-code="<?= esc((string) $row['code']) ?>" class="cursor-pointer">
<td class="text-center"><?= $idx + 1 ?></td>
<td class="text-center">
<button type="button" class="js-toggle-bag border border-gray-300 rounded px-2 py-0.5 text-xs hover:bg-gray-100" data-code="<?= esc((string) $row['code']) ?>">선택</button>
</td>
<td class="text-left pl-2"><?= esc((string) $row['name']) ?></td>
<td class="text-right pr-2"><?= number_format((float) $row['orderPrice'], 2) ?></td>
<td class="text-right pr-2"><?= number_format((int) $row['boxPerPack']) ?></td>
<td class="text-right pr-2"><?= number_format((int) $row['packPerSheet']) ?></td>
<td class="text-right pr-2"><?= number_format((int) $row['totalPerBox']) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($bagReferenceRows)): ?>
<tr><td colspan="7" class="text-center text-gray-400 py-4">표시할 봉투 기준 데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div> </div>
</form> </section>
</div> </form>
<style>
.order-input-table tbody tr,
.order-reference-table tbody tr {
height: 34px;
}
.order-input-table tbody td,
.order-reference-table tbody td {
padding-top: 4px;
padding-bottom: 4px;
}
</style>
<script>
(() => {
const bagMeta = <?= json_encode($bagMeta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const initialSelectedItems = <?= json_encode($initialSelectedItems, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const selectedBody = document.getElementById('selected-order-items-body');
const referenceRows = Array.from(document.querySelectorAll('[data-reference-row]'));
const sumBoxQtyEl = document.getElementById('sum-box-qty');
const sumSheetQtyEl = document.getElementById('sum-sheet-qty');
const sumAmountEl = document.getElementById('sum-amount');
const monthInput = document.getElementById('bo_order_month_ui');
const orderDateInput = document.getElementById('bo_order_date');
const orderForm = document.querySelector('form[action*="bag-orders/store"]');
const selectedItems = new Map();
let activeCode = null;
const formatNumber = (value) => new Intl.NumberFormat('ko-KR').format(Number.isFinite(value) ? value : 0);
const escapeHtml = (value) => String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
const syncMonthFromDate = () => {
if (!orderDateInput || !monthInput || !orderDateInput.value) return;
monthInput.value = orderDateInput.value.substring(0, 7);
};
const syncDateFromMonth = () => {
if (!orderDateInput || !monthInput || !monthInput.value) return;
const parts = monthInput.value.split('-');
if (parts.length !== 2) return;
const year = Number(parts[0]);
const month = Number(parts[1]);
if (!Number.isFinite(year) || !Number.isFinite(month)) return;
const currentDay = orderDateInput.value ? Number(orderDateInput.value.split('-')[2]) : 1;
const lastDay = new Date(year, month, 0).getDate();
const day = String(Math.min(Math.max(currentDay, 1), lastDay)).padStart(2, '0');
orderDateInput.value = `${String(year)}-${String(month).padStart(2, '0')}-${day}`;
};
const updateReferenceSelectionUi = () => {
referenceRows.forEach((row) => {
const code = row.dataset.code || '';
const button = row.querySelector('.js-toggle-bag');
const isSelected = selectedItems.has(code);
row.classList.toggle('bg-blue-50', isSelected);
if (button) {
button.textContent = isSelected ? '선택됨' : '선택';
button.classList.toggle('bg-blue-600', isSelected);
button.classList.toggle('text-white', isSelected);
button.classList.toggle('border-blue-600', isSelected);
}
});
};
const updateTotals = () => {
let sumBoxQty = 0;
let sumSheetQty = 0;
let sumAmount = 0;
selectedBody.querySelectorAll('tr[data-item-row]').forEach((row) => {
const code = row.dataset.code || '';
const qtyInput = row.querySelector('.item-qty-box');
const qtyBox = Math.max(0, parseInt(qtyInput?.value || '0', 10));
const meta = bagMeta[code] || { orderPrice: 0, totalPerBox: 1 };
const unitPrice = Number(meta.orderPrice || 0);
const totalPerBox = Math.max(1, Number(meta.totalPerBox || 1));
const qtySheet = qtyBox * totalPerBox;
const amount = qtySheet * unitPrice;
const unitPriceEl = row.querySelector('.item-unit-price');
const qtySheetEl = row.querySelector('.item-qty-sheet');
const sheetHelpEl = row.querySelector('.item-sheet-help');
const amountEl = row.querySelector('.item-amount');
if (unitPriceEl) unitPriceEl.textContent = formatNumber(unitPrice);
if (qtySheetEl) qtySheetEl.textContent = formatNumber(qtySheet);
if (sheetHelpEl) sheetHelpEl.textContent = `낱장 ${formatNumber(qtySheet)}장`;
if (amountEl) amountEl.textContent = formatNumber(amount);
selectedItems.set(code, { qtyBox });
sumBoxQty += qtyBox;
sumSheetQty += qtySheet;
sumAmount += amount;
});
if (sumBoxQtyEl) sumBoxQtyEl.textContent = formatNumber(sumBoxQty);
if (sumSheetQtyEl) sumSheetQtyEl.textContent = formatNumber(sumSheetQty);
if (sumAmountEl) sumAmountEl.textContent = formatNumber(sumAmount);
};
const setActiveRow = (code) => {
activeCode = code || null;
selectedBody.querySelectorAll('tr[data-item-row]').forEach((row) => {
row.classList.toggle('bg-amber-50', row.dataset.code === activeCode);
});
};
const renderSelectedRows = () => {
const codes = Object.keys(bagMeta).filter((code) => selectedItems.has(code));
if (codes.length === 0) {
selectedBody.innerHTML = '<tr><td colspan="7" class="text-center text-gray-400 py-4">아래 "발주 등록 종류"에서 봉투를 선택해 주세요.</td></tr>';
setActiveRow(null);
updateTotals();
updateReferenceSelectionUi();
return;
}
selectedBody.innerHTML = codes.map((code, idx) => {
const meta = bagMeta[code];
const qtyBox = Math.max(0, parseInt(String(selectedItems.get(code)?.qtyBox ?? 0), 10));
const name = meta?.name || code;
return `
<tr data-item-row data-code="${escapeHtml(code)}" class="cursor-pointer">
<td class="text-center">${idx + 1}</td>
<td class="text-center">
<button type="button" class="js-remove-selected text-xs text-red-600 hover:underline" data-code="${escapeHtml(code)}">해제</button>
</td>
<td class="text-left pl-2">
${escapeHtml(name)}
<input type="hidden" name="item_bag_code[]" value="${escapeHtml(code)}" />
</td>
<td>
<input name="item_qty_box[]" type="number" min="0" step="1" value="${qtyBox}" class="item-qty-box border border-gray-300 rounded px-2 py-1 text-sm w-full text-right leading-tight" />
<p class="text-[11px] text-gray-500 mt-1 item-sheet-help">낱장 0장</p>
</td>
<td class="text-right pr-2 item-unit-price">0</td>
<td class="text-right pr-2 item-qty-sheet">0</td>
<td class="text-right pr-2 item-amount">0</td>
</tr>
`;
}).join('');
if (!activeCode || !selectedItems.has(activeCode)) {
activeCode = codes[0];
}
setActiveRow(activeCode);
updateTotals();
updateReferenceSelectionUi();
};
const toggleSelection = (code) => {
if (!code || !bagMeta[code]) return;
if (selectedItems.has(code)) {
selectedItems.delete(code);
if (activeCode === code) activeCode = null;
} else {
selectedItems.set(code, { qtyBox: 0 });
activeCode = code;
}
renderSelectedRows();
};
initialSelectedItems.forEach((item) => {
if (!item || !item.code || !bagMeta[item.code]) return;
selectedItems.set(item.code, { qtyBox: Math.max(0, parseInt(String(item.qtyBox ?? 0), 10)) });
activeCode = item.code;
});
selectedBody.addEventListener('click', (event) => {
const removeButton = event.target.closest('.js-remove-selected');
if (removeButton) {
toggleSelection(removeButton.dataset.code || '');
return;
}
const row = event.target.closest('tr[data-item-row]');
if (!row) return;
const code = row.dataset.code || '';
setActiveRow(code);
const qtyInput = row.querySelector('.item-qty-box');
if (qtyInput) qtyInput.focus();
});
selectedBody.addEventListener('input', (event) => {
const qtyInput = event.target.closest('.item-qty-box');
if (!qtyInput) return;
const row = qtyInput.closest('tr[data-item-row]');
if (!row) return;
const code = row.dataset.code || '';
selectedItems.set(code, { qtyBox: Math.max(0, parseInt(qtyInput.value || '0', 10)) });
updateTotals();
});
referenceRows.forEach((row) => {
row.addEventListener('click', (event) => {
const button = event.target.closest('.js-toggle-bag');
if (button) {
toggleSelection(button.dataset.code || '');
return;
}
if (event.target.closest('td')) {
toggleSelection(row.dataset.code || '');
}
});
});
if (monthInput) monthInput.addEventListener('change', () => { syncDateFromMonth(); updateTotals(); });
if (orderDateInput) orderDateInput.addEventListener('change', syncMonthFromDate);
if (orderForm) {
orderForm.addEventListener('submit', (event) => {
const hasValidItem = Array.from(selectedBody.querySelectorAll('tr[data-item-row]')).some((row) => {
const qtyInput = row.querySelector('.item-qty-box');
return Math.max(0, parseInt(qtyInput?.value || '0', 10)) > 0;
});
if (!hasValidItem) {
event.preventDefault();
alert('봉투를 선택하고 수량을 1 이상 입력해 주세요.');
}
});
}
syncMonthFromDate();
renderSelectedRows();
})();
</script>

View File

@@ -1,6 +1,6 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel"> <section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a href="<?= base_url('admin/bag-orders') ?>" class="text-blue-600 hover:underline text-sm">&larr; 발주 목록</a> <a href="<?= mgmt_url('bag-orders') ?>" class="text-blue-600 hover:underline text-sm">&larr; 발주 목록</a>
<span class="text-gray-400">|</span> <span class="text-gray-400">|</span>
<span class="text-sm font-bold text-gray-700">발주 상세 <?= esc($order->bo_lot_no) ?></span> <span class="text-sm font-bold text-gray-700">발주 상세 <?= esc($order->bo_lot_no) ?></span>
</div> </div>

View File

@@ -1,81 +1,200 @@
<?= view('components/print_header', ['printTitle' => '발주 현황']) ?> <?php
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel"> // 발주기간: native month 입력은 로케일에 따라 Jan 등 영문 표기될 수 있어 YYYY-MM select + 한글 라벨 사용
$bagOrderYmChoices = [];
$bagOrderYmCenterY = (int) date('Y');
for ($by = $bagOrderYmCenterY - 4; $by <= $bagOrderYmCenterY + 2; $by++) {
for ($bm = 1; $bm <= 12; $bm++) {
$bagOrderYmChoices[] = sprintf('%04d-%02d', $by, $bm);
}
}
foreach ([(string) ($startMonth ?? ''), (string) ($endMonth ?? '')] as $ymExtra) {
if (preg_match('/^\d{4}-\d{2}$/', $ymExtra) && ! in_array($ymExtra, $bagOrderYmChoices, true)) {
$bagOrderYmChoices[] = $ymExtra;
}
}
sort($bagOrderYmChoices);
$bagOrderYmLabel = static function (string $ym): string {
if (preg_match('/^(\d{4})-(\d{2})$/', $ym, $m)) {
return $m[1] . '년 ' . (int) $m[2] . '월';
}
return $ym;
};
?>
<?= view('components/print_header', ['printTitle' => '봉투 발주 현황', 'printShowApproval' => false]) ?>
<section class="no-print border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex flex-wrap items-center justify-between gap-y-2"> <div class="flex flex-wrap items-center justify-between gap-y-2">
<span class="text-sm font-bold text-gray-700">발주 현황</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a href="<?= base_url('admin/bag-orders/export') . '?' . http_build_query(array_filter(['start_date' => $startDate ?? '', 'end_date' => $endDate ?? '', 'status' => $status ?? ''])) ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a> <a href="<?= mgmt_url('bag-orders/export') . '?' . http_build_query(array_filter(['start_month' => $startMonth ?? '', 'end_month' => $endMonth ?? '', 'company_idx' => $companyIdx ?? 0, 'bag_code' => $bagCode ?? '', 'receive_type' => $receiveType ?? ''])) ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button> <button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= base_url('admin/bag-orders/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">발주 등록</a> <a href="<?= mgmt_url('bag-orders/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">발주 등록</a>
</div> </div>
</div> </div>
</section> </section>
<section class="p-2 bg-white border-b border-gray-200">
<form method="GET" action="<?= base_url('admin/bag-orders') ?>" class="flex flex-wrap items-center gap-2"> <section class="no-print p-2 bg-white border-b border-gray-200">
<label class="text-sm text-gray-600">발주일</label> <!-- GBMS 발주현황: 발주기간은 [시작] ~ [끝] 한 줄 고정, 필터 블록은 가로 나열 후 좁으면 블록 단위로만 줄바꿈 -->
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/> <form method="GET" action="<?= mgmt_url('bag-orders') ?>" class="flex flex-wrap items-end gap-x-5 gap-y-3 w-full">
<label class="text-sm text-gray-600">~</label> <div class="flex flex-nowrap items-center gap-2 shrink-0">
<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 whitespace-nowrap">발주 기간</label>
<label class="text-sm text-gray-600">상태</label> <select name="start_month" class="border border-gray-300 rounded px-2 py-1 text-sm w-[11.5rem] max-w-[13rem] shrink-0">
<select name="status" class="border border-gray-300 rounded px-2 py-1 text-sm"> <?php foreach ($bagOrderYmChoices as $ym): ?>
<option value="">전체</option> <option value="<?= esc($ym) ?>" <?= ($startMonth ?? date('Y-m')) === $ym ? 'selected' : '' ?>><?= esc($bagOrderYmLabel($ym)) ?></option>
<option value="normal" <?= ($status ?? '') === 'normal' ? 'selected' : '' ?>>정상</option> <?php endforeach; ?>
<option value="cancelled" <?= ($status ?? '') === 'cancelled' ? 'selected' : '' ?>>취소</option> </select>
<option value="deleted" <?= ($status ?? '') === 'deleted' ? 'selected' : '' ?>>삭제</option> <span class="text-sm text-gray-500 select-none">~</span>
</select> <select name="end_month" class="border border-gray-300 rounded px-2 py-1 text-sm w-[11.5rem] max-w-[13rem] shrink-0">
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button> <?php foreach ($bagOrderYmChoices as $ym): ?>
<a href="<?= base_url('admin/bag-orders') ?>" class="text-sm text-gray-500 hover:underline">초기화</a> <option value="<?= esc($ym) ?>" <?= ($endMonth ?? date('Y-m')) === $ym ? 'selected' : '' ?>><?= esc($bagOrderYmLabel($ym)) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-nowrap items-center gap-2 shrink-0">
<label class="text-sm text-gray-600 whitespace-nowrap">제작 업체</label>
<select name="company_idx" class="border border-gray-300 rounded px-2 py-1 text-sm w-[11rem] max-w-[14rem]">
<option value="0">전 체</option>
<?php foreach (($companyOptions ?? []) as $company): ?>
<option value="<?= (int) $company->cp_idx ?>" <?= (int) ($companyIdx ?? 0) === (int) $company->cp_idx ? 'selected' : '' ?>>
<?= esc((string) $company->cp_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-nowrap items-center gap-2 shrink-0">
<label class="text-sm text-gray-600 whitespace-nowrap">품 명</label>
<select name="bag_code" class="border border-gray-300 rounded px-2 py-1 text-sm w-[11rem] max-w-[16rem]">
<option value="">전 체</option>
<?php foreach (($bagCodeOptions ?? []) as $bag): ?>
<option value="<?= esc((string) $bag->cd_code) ?>" <?= (string) ($bagCode ?? '') === (string) $bag->cd_code ? 'selected' : '' ?>>
<?= esc((string) $bag->cd_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-nowrap items-center gap-2 shrink-0">
<label class="text-sm text-gray-600 whitespace-nowrap">입고 구분</label>
<select name="receive_type" class="border border-gray-300 rounded px-2 py-1 text-sm w-[8.5rem]">
<option value="all" <?= ($receiveType ?? 'all') === 'all' ? 'selected' : '' ?>>전 체</option>
<option value="received" <?= ($receiveType ?? 'all') === 'received' ? 'selected' : '' ?>>입고완료</option>
<option value="pending" <?= ($receiveType ?? 'all') === 'pending' ? 'selected' : '' ?>>미입고</option>
</select>
</div>
<div class="flex flex-nowrap items-center gap-2 shrink-0 sm:ml-auto">
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
<a href="<?= mgmt_url('bag-orders') ?>" class="text-sm text-gray-500 hover:underline whitespace-nowrap">초기화</a>
</div>
</form> </form>
</section> </section>
<div class="border border-gray-300 overflow-auto mt-2">
<table class="w-full data-table"> <div class="bag-order-print-wrap border border-gray-300 overflow-auto mt-2">
<table class="bag-order-print-table w-full data-table">
<thead> <thead>
<tr> <tr>
<th class="w-16">번호</th> <th class="w-32">발주일자</th>
<th>LOT번호</th> <th class="min-w-[10rem]">제작 업체</th>
<th>발주일</th> <th class="min-w-[12rem]">품 명</th>
<th>제작업체</th> <th class="w-28">발주 수량</th>
<th>입고처</th> <th class="w-28">입고 수량</th>
<th>품목수</th> <th class="w-28">미입고수량</th>
<th>총수량</th> <th class="w-32">발주 금액</th>
<th>총금액</th> <th class="min-w-[9rem]">입고처</th>
<th class="w-20">상태</th> <th class="min-w-[8rem]">비 고</th>
<th class="w-44">작업</th>
</tr> </tr>
</thead> </thead>
<tbody class="text-right"> <tbody class="text-right">
<?php foreach ($list as $row): ?> <?php $printedGroup = []; ?>
<?php foreach (($rows ?? []) as $row): ?>
<?php if (! empty($row['is_subtotal'])): ?>
<tr class="bg-gray-50 font-semibold">
<td colspan="3" class="text-center"><?= esc((string) ($row['label'] ?? '소계')) ?></td>
<td><?= number_format((int) ($row['order_qty'] ?? 0)) ?></td>
<td><?= number_format((int) ($row['received_qty'] ?? 0)) ?></td>
<td><?= number_format((int) ($row['pending_qty'] ?? 0)) ?></td>
<td><?= number_format((float) ($row['amount'] ?? 0)) ?></td>
<td></td>
<td></td>
</tr>
<?php continue; ?>
<?php endif; ?>
<?php
$boIdx = (int) ($row['bo_idx'] ?? 0);
$showGroup = ! isset($printedGroup[$boIdx]);
$rowspan = (int) (($groupRows[$boIdx] ?? 1));
if ($showGroup) {
$printedGroup[$boIdx] = true;
}
?>
<tr> <tr>
<td class="text-center"><?= esc($row->bo_idx) ?></td> <?php if ($showGroup): ?>
<td class="text-center font-mono"><?= esc($row->bo_lot_no) ?></td> <td class="text-center align-top" rowspan="<?= $rowspan ?>"><?= esc((string) ($row['order_date'] ?? '')) ?></td>
<td class="text-center"><?= esc($row->bo_order_date) ?></td> <td class="text-left pl-2 align-top" rowspan="<?= $rowspan ?>"><?= esc((string) ($row['company_name'] ?? '')) ?></td>
<td class="text-left pl-2"><?= esc($companyMap[$row->bo_company_idx] ?? '') ?></td> <?php endif; ?>
<td class="text-left pl-2"><?= esc($agencyMap[$row->bo_agency_idx] ?? '') ?></td> <td class="text-left pl-2"><?= esc((string) ($row['bag_name'] ?? '')) ?></td>
<td><?= number_format((int) ($itemSummary[$row->bo_idx]['count'] ?? 0)) ?></td> <td><?= number_format((int) ($row['order_qty'] ?? 0)) ?></td>
<td><?= number_format((int) ($itemSummary[$row->bo_idx]['qty'] ?? 0)) ?></td> <td><?= number_format((int) ($row['received_qty'] ?? 0)) ?></td>
<td><?= number_format((int) ($itemSummary[$row->bo_idx]['amount'] ?? 0)) ?></td> <td><?= number_format((int) ($row['pending_qty'] ?? 0)) ?></td>
<td class="text-center"> <td><?= number_format((float) ($row['amount'] ?? 0)) ?></td>
<?php <td class="text-left pl-2"><?= esc((string) ($row['agency_name'] ?? '')) ?></td>
$statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제']; <td></td>
echo esc($statusMap[$row->bo_status] ?? $row->bo_status);
?>
</td>
<td class="text-center">
<a href="<?= base_url('admin/bag-orders/detail/' . (int) $row->bo_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">상세</a>
<form action="<?= base_url('admin/bag-orders/cancel/' . (int) $row->bo_idx) ?>" method="POST" class="inline" onsubmit="return confirm('취소하시겠습니까?');">
<?= csrf_field() ?>
<button type="submit" class="text-orange-600 hover:underline text-sm mr-1">취소</button>
</form>
<form action="<?= base_url('admin/bag-orders/delete/' . (int) $row->bo_idx) ?>" method="POST" class="inline" onsubmit="return confirm('삭제하시겠습니까?');">
<?= csrf_field() ?>
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
</form>
</td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (empty($list)): ?>
<tr><td colspan="10" class="text-center text-gray-400 py-4">등록된 발주가 없습니다.</td></tr> <?php if (empty($rows ?? [])): ?>
<tr><td colspan="9" class="text-center text-gray-400 py-6">조회 조건에 해당하는 발주 내역이 없습니다.</td></tr>
<?php endif; ?> <?php endif; ?>
</tbody> </tbody>
<tfoot>
<tr class="bg-gray-100 font-bold">
<td colspan="3" class="text-center">총계</td>
<td class="text-right"><?= number_format((int) ($grandTotals['order_qty'] ?? 0)) ?></td>
<td class="text-right"><?= number_format((int) ($grandTotals['received_qty'] ?? 0)) ?></td>
<td class="text-right"><?= number_format((int) ($grandTotals['pending_qty'] ?? 0)) ?></td>
<td class="text-right"><?= number_format((float) ($grandTotals['amount'] ?? 0)) ?></td>
<td></td>
<td></td>
</tr>
</tfoot>
</table> </table>
</div> </div>
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
<style>
@media print {
#debug-icon,
#debug-bar,
#debug-bar-contents,
#debug-toolbar,
.debug-toolbar,
.ci-debug-toolbar,
[id^='debug-bar-'],
[id^='debug-icon'],
[class*='debug-toolbar'] {
display: none !important;
visibility: hidden !important;
}
.bag-order-print-wrap {
overflow: visible !important;
border: none !important;
margin-top: 0 !important;
}
.bag-order-print-table {
width: 100% !important;
table-layout: auto !important;
}
.bag-order-print-table th,
.bag-order-print-table td {
white-space: nowrap !important;
word-break: keep-all !important;
overflow-wrap: normal !important;
font-size: 10px !important;
padding: 2px 3px !important;
}
}
</style>

View File

@@ -2,7 +2,7 @@
<span class="text-sm font-bold text-gray-700">봉투 단가 등록</span> <span class="text-sm font-bold text-gray-700">봉투 단가 등록</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/bag-prices/store') ?>" method="POST" class="space-y-4"> <form action="<?= mgmt_url('bag-prices/store') ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -48,7 +48,7 @@
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
<a href="<?= base_url('admin/bag-prices') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= mgmt_url('bag-prices') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -2,7 +2,7 @@
<span class="text-sm font-bold text-gray-700">봉투 단가 수정</span> <span class="text-sm font-bold text-gray-700">봉투 단가 수정</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/bag-prices/update/' . (int) $item->bp_idx) ?>" method="POST" class="space-y-4"> <form action="<?= mgmt_url('bag-prices/update/' . (int) $item->bp_idx) ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -49,7 +49,7 @@
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button>
<a href="<?= base_url('admin/bag-prices') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= mgmt_url('bag-prices') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -1,6 +1,6 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel"> <section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a href="<?= base_url('admin/bag-prices') ?>" class="text-blue-600 hover:underline text-sm">&larr; 단가 목록</a> <a href="<?= mgmt_url('bag-prices') ?>" class="text-blue-600 hover:underline text-sm">&larr; 단가 목록</a>
<span class="text-gray-400">|</span> <span class="text-gray-400">|</span>
<span class="text-sm font-bold text-gray-700">단가 변경 이력 <?= esc($item->bp_bag_name) ?> (<?= esc($item->bp_bag_code) ?>)</span> <span class="text-sm font-bold text-gray-700">단가 변경 이력 <?= esc($item->bp_bag_name) ?> (<?= esc($item->bp_bag_code) ?>)</span>
</div> </div>

View File

@@ -1,21 +1,97 @@
<?= view('components/print_header', ['printTitle' => '봉투 단가 관리']) ?> <?= view('components/print_header', ['printTitle' => '봉투 단가 관리', 'printShowApproval' => false]) ?>
<style>
@media print {
.no-print { display: none !important; }
}
</style>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel"> <section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex flex-wrap items-center justify-between gap-y-2"> <div class="flex flex-wrap items-center justify-between gap-y-2">
<span class="text-sm font-bold text-gray-700">봉투 단가 관리</span> <span class="text-sm font-bold text-gray-700">봉투 단가 관리</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2 no-print">
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button> <button onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= base_url('admin/bag-prices/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">단가 등록</a> <a href="<?= mgmt_url('bag-prices/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">단가 등록</a>
</div> </div>
</div> </div>
</section> </section>
<section class="p-2 bg-white border-b border-gray-200"> <section class="no-print border border-gray-200 rounded-lg bg-white p-3 mt-2">
<form method="GET" action="<?= base_url('admin/bag-prices') ?>" class="flex flex-wrap items-center gap-2"> <form method="get" action="<?= mgmt_url('bag-prices') ?>" class="flex flex-wrap items-end gap-3" autocomplete="off">
<label class="text-sm text-gray-600">적용시작일</label> <div class="flex flex-col gap-0.5">
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/> <label class="text-xs text-gray-500">봉투구분</label>
<label class="text-sm text-gray-600">~</label> <select name="bag_kind_e" class="border border-gray-300 rounded px-2 py-1.5 text-sm min-w-[9rem]">
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/> <option value="">전체</option>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button> <?php foreach ($bag_kind_options ?? [] as $cd): ?>
<a href="<?= base_url('admin/bag-prices') ?>" class="text-sm text-gray-500 hover:underline">초기화</a> <option value="<?= esc($cd->cd_code) ?>" <?= (string) ($cd->cd_code ?? '') === (string) ($bag_kind_e ?? '') ? 'selected' : '' ?>>
<?= esc($cd->cd_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-col gap-0.5">
<label class="text-xs text-gray-500">봉투코드</label>
<select name="bag_code" class="border border-gray-300 rounded px-2 py-1.5 text-sm min-w-[11rem]">
<option value="">전체</option>
<?php foreach ($bag_codes ?? [] as $cd): ?>
<option value="<?= esc($cd->cd_code) ?>" <?= (string) ($cd->cd_code ?? '') === (string) ($bag_code ?? '') ? 'selected' : '' ?>>
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-col gap-0.5">
<label class="text-xs text-gray-500">조회 기간 (적용기간 겹침)</label>
<?php
$sp = $startParts ?? ['y' => '', 'm' => '', 'd' => ''];
$ep = $endParts ?? ['y' => '', 'm' => '', 'd' => ''];
$ymin = (int) ($dateYearMin ?? ((int) date('Y') - 12));
$ymax = (int) ($dateYearMax ?? ((int) date('Y') + 2));
?>
<div class="flex flex-wrap items-center gap-1">
<span class="text-xs text-gray-500 mr-0.5">시작</span>
<select name="start_y" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[4.5rem]">
<option value="">연도</option>
<?php for ($yy = $ymin; $yy <= $ymax; $yy++): ?>
<option value="<?= $yy ?>" <?= (string) ($sp['y'] ?? '') === (string) $yy ? 'selected' : '' ?>><?= $yy ?></option>
<?php endfor; ?>
</select>
<select name="start_m" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[3.75rem]">
<option value="">월</option>
<?php for ($mi = 1; $mi <= 12; $mi++): ?>
<option value="<?= $mi ?>" <?= isset($sp['m']) && (int) $sp['m'] === $mi ? 'selected' : '' ?>><?= $mi ?>월</option>
<?php endfor; ?>
</select>
<select name="start_d" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[3.75rem]">
<option value="">일</option>
<?php for ($di = 1; $di <= 31; $di++): ?>
<option value="<?= $di ?>" <?= isset($sp['d']) && (int) $sp['d'] === $di ? 'selected' : '' ?>><?= $di ?>일</option>
<?php endfor; ?>
</select>
<span class="text-sm text-gray-500 mx-0.5">~</span>
<span class="text-xs text-gray-500 mr-0.5">종료</span>
<select name="end_y" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[4.5rem]">
<option value="">연도</option>
<?php for ($yy = $ymin; $yy <= $ymax; $yy++): ?>
<option value="<?= $yy ?>" <?= (string) ($ep['y'] ?? '') === (string) $yy ? 'selected' : '' ?>><?= $yy ?></option>
<?php endfor; ?>
</select>
<select name="end_m" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[3.75rem]">
<option value="">월</option>
<?php for ($mi = 1; $mi <= 12; $mi++): ?>
<option value="<?= $mi ?>" <?= isset($ep['m']) && (int) $ep['m'] === $mi ? 'selected' : '' ?>><?= $mi ?>월</option>
<?php endfor; ?>
</select>
<select name="end_d" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[3.75rem]">
<option value="">일</option>
<?php for ($di = 1; $di <= 31; $di++): ?>
<option value="<?= $di ?>" <?= isset($ep['d']) && (int) $ep['d'] === $di ? 'selected' : '' ?>><?= $di ?>일</option>
<?php endfor; ?>
</select>
</div>
</div>
<div class="flex items-center gap-2 pb-0.5">
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm">조회</button>
<a href="<?= mgmt_url('bag-prices') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
<button type="button" onclick="window.print()" class="border border-gray-300 text-gray-700 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50">인쇄</button>
</div>
</form> </form>
</section> </section>
<div class="border border-gray-300 overflow-auto mt-2"> <div class="border border-gray-300 overflow-auto mt-2">
@@ -31,13 +107,21 @@
<th>적용시작</th> <th>적용시작</th>
<th>적용종료</th> <th>적용종료</th>
<th class="w-20">상태</th> <th class="w-20">상태</th>
<th class="w-36">작업</th> <th class="w-36 no-print">작업</th>
</tr> </tr>
</thead> </thead>
<tbody class="text-right"> <tbody class="text-right">
<?php foreach ($list as $row): ?> <?php
$startNo = 1;
if (isset($pager) && method_exists($pager, 'getCurrentPage') && method_exists($pager, 'getPerPage')) {
$currentPage = (int) $pager->getCurrentPage();
$perPage = (int) $pager->getPerPage();
$startNo = (($currentPage > 0 ? $currentPage : 1) - 1) * ($perPage > 0 ? $perPage : 20) + 1;
}
?>
<?php foreach (($list ?? []) as $idx => $row): ?>
<tr> <tr>
<td class="text-center"><?= esc($row->bp_idx) ?></td> <td class="text-center"><?= (int) $startNo + (int) $idx ?></td>
<td class="text-center font-mono"><?= esc($row->bp_bag_code) ?></td> <td class="text-center font-mono"><?= esc($row->bp_bag_code) ?></td>
<td class="text-left pl-2"><?= esc($row->bp_bag_name) ?></td> <td class="text-left pl-2"><?= esc($row->bp_bag_name) ?></td>
<td><?= number_format((float) $row->bp_order_price) ?></td> <td><?= number_format((float) $row->bp_order_price) ?></td>
@@ -46,10 +130,10 @@
<td class="text-center"><?= esc($row->bp_start_date) ?></td> <td class="text-center"><?= esc($row->bp_start_date) ?></td>
<td class="text-center"><?= esc($row->bp_end_date ?? '현재') ?></td> <td class="text-center"><?= esc($row->bp_end_date ?? '현재') ?></td>
<td class="text-center"><?= (int) $row->bp_state === 1 ? '사용' : '미사용' ?></td> <td class="text-center"><?= (int) $row->bp_state === 1 ? '사용' : '미사용' ?></td>
<td class="text-center"> <td class="text-center no-print">
<a href="<?= base_url('admin/bag-prices/history/' . (int) $row->bp_idx) ?>" class="text-green-600 hover:underline text-sm mr-1">이력</a> <a href="<?= mgmt_url('bag-prices/history/' . (int) $row->bp_idx) ?>" class="text-green-600 hover:underline text-sm mr-1">이력</a>
<a href="<?= base_url('admin/bag-prices/edit/' . (int) $row->bp_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">수정</a> <a href="<?= mgmt_url('bag-prices/edit/' . (int) $row->bp_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">수정</a>
<form action="<?= base_url('admin/bag-prices/delete/' . (int) $row->bp_idx) ?>" method="POST" class="inline" onsubmit="return confirm('삭제하시겠습니까?');"> <form action="<?= mgmt_url('bag-prices/delete/' . (int) $row->bp_idx) ?>" method="POST" class="inline" onsubmit="return confirm('삭제하시겠습니까?');">
<?= csrf_field() ?> <?= csrf_field() ?>
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button> <button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
</form> </form>
@@ -62,4 +146,4 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?> <?php if (isset($pager)): ?><div class="mt-3 no-print"><?= $pager->links() ?></div><?php endif; ?>

View File

@@ -2,7 +2,7 @@
<span class="text-sm font-bold text-gray-700">입고 처리</span> <span class="text-sm font-bold text-gray-700">입고 처리</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/bag-receivings/store') ?>" method="POST" class="space-y-4"> <form action="<?= mgmt_url('bag-receivings/store') ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -47,7 +47,7 @@
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
<a href="<?= base_url('admin/bag-receivings') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= mgmt_url('bag-receivings') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -4,18 +4,18 @@
<span class="text-sm font-bold text-gray-700">입고 현황</span> <span class="text-sm font-bold text-gray-700">입고 현황</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button> <button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= base_url('admin/bag-receivings/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">입고 처리</a> <a href="<?= mgmt_url('bag-receivings/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">입고 처리</a>
</div> </div>
</div> </div>
</section> </section>
<section class="p-2 bg-white border-b border-gray-200"> <section class="p-2 bg-white border-b border-gray-200">
<form method="GET" action="<?= base_url('admin/bag-receivings') ?>" class="flex flex-wrap items-center gap-2"> <form method="GET" action="<?= mgmt_url('bag-receivings') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">입고일</label> <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"/> <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> <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"/> <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> <button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
<a href="<?= base_url('admin/bag-receivings') ?>" class="text-sm text-gray-500 hover:underline">초기화</a> <a href="<?= mgmt_url('bag-receivings') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
</form> </form>
</section> </section>
<div class="border border-gray-300 overflow-auto mt-2"> <div class="border border-gray-300 overflow-auto mt-2">

View File

@@ -2,7 +2,7 @@
<span class="text-sm font-bold text-gray-700">판매 등록</span> <span class="text-sm font-bold text-gray-700">판매 등록</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/bag-sales/store') ?>" method="POST" class="space-y-4"> <form action="<?= mgmt_url('bag-sales/store') ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -50,7 +50,7 @@
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
<a href="<?= base_url('admin/bag-sales') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= mgmt_url('bag-sales') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -3,14 +3,14 @@
<div class="flex flex-wrap items-center justify-between gap-y-2"> <div class="flex flex-wrap items-center justify-between gap-y-2">
<span class="text-sm font-bold text-gray-700">판매/반품 관리</span> <span class="text-sm font-bold text-gray-700">판매/반품 관리</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a href="<?= base_url('admin/bag-sales/export') . '?' . http_build_query(array_filter(['start_date' => $startDate ?? '', 'end_date' => $endDate ?? '', 'type' => $type ?? ''])) ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a> <a href="<?= mgmt_url('bag-sales/export') . '?' . http_build_query(array_filter(['start_date' => $startDate ?? '', 'end_date' => $endDate ?? '', 'type' => $type ?? ''])) ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button> <button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= base_url('admin/bag-sales/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">판매 등록</a> <a href="<?= mgmt_url('bag-sales/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">판매 등록</a>
</div> </div>
</div> </div>
</section> </section>
<section class="p-2 bg-white border-b border-gray-200"> <section class="p-2 bg-white border-b border-gray-200">
<form method="GET" action="<?= base_url('admin/bag-sales') ?>" class="flex flex-wrap items-center gap-2"> <form method="GET" action="<?= mgmt_url('bag-sales') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">판매일</label> <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"/> <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> <label class="text-sm text-gray-600">~</label>
@@ -23,7 +23,7 @@
<option value="cancel" <?= ($type ?? '') === 'cancel' ? 'selected' : '' ?>>취소</option> <option value="cancel" <?= ($type ?? '') === 'cancel' ? 'selected' : '' ?>>취소</option>
</select> </select>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button> <button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
<a href="<?= base_url('admin/bag-sales') ?>" class="text-sm text-gray-500 hover:underline">초기화</a> <a href="<?= mgmt_url('bag-sales') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
</form> </form>
</section> </section>
<div class="border border-gray-300 overflow-auto mt-2"> <div class="border border-gray-300 overflow-auto mt-2">

View File

@@ -1,6 +1,6 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel"> <section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a href="<?= base_url('admin/code-details/' . (int) $kind->ck_idx) ?>" class="text-blue-600 hover:underline text-sm">&larr; <?= esc($kind->ck_name) ?></a> <a href="<?= base_url('bag/code-details/' . (int) $kind->ck_idx) ?>" class="text-blue-600 hover:underline text-sm">&larr; <?= esc($kind->ck_name) ?></a>
<span class="text-gray-400">|</span> <span class="text-gray-400">|</span>
<span class="text-sm font-bold text-gray-700">세부코드 등록</span> <span class="text-sm font-bold text-gray-700">세부코드 등록</span>
</div> </div>
@@ -30,9 +30,42 @@
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-24" name="cd_sort" type="number" value="<?= esc(old('cd_sort', '0')) ?>" min="0"/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-24" name="cd_sort" type="number" value="<?= esc(old('cd_sort', '0')) ?>" min="0"/>
</div> </div>
<?php if (! empty($canPlatformScope)): ?>
<div class="space-y-2 border-t border-gray-200 pt-3">
<p class="text-sm font-bold text-gray-700">등록 범위</p>
<label class="flex items-center gap-2 text-sm">
<input type="radio" name="cd_scope" value="platform" <?= old('cd_scope', 'platform') === 'platform' ? 'checked' : '' ?> class="cd-scope-radio"/>
플랫폼 공통 (CSV·시드와 동일 — 전 지자체, super/본부만 이후 수정)
</label>
<label class="flex items-center gap-2 text-sm">
<input type="radio" name="cd_scope" value="local" <?= old('cd_scope') === 'local' ? 'checked' : '' ?> class="cd-scope-radio"/>
지자체 전용 (해당 지자체 관리자가 수정·삭제)
</label>
<div id="cd-lg-wrap" class="<?= old('cd_scope') === 'local' ? '' : 'hidden' ?> flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">소속 지자체</label>
<select name="cd_lg_idx" class="border border-gray-300 rounded px-3 py-1.5 text-sm min-w-[14rem]">
<option value="">선택</option>
<?php foreach ($localGovernments as $gov): ?>
<option value="<?= (int) $gov->lg_idx ?>" <?= (string) old('cd_lg_idx') === (string) $gov->lg_idx ? 'selected' : '' ?>><?= esc($gov->lg_name) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<script>
(function () {
document.querySelectorAll('.cd-scope-radio').forEach(function (r) {
r.addEventListener('change', function () {
var w = document.getElementById('cd-lg-wrap');
if (w) w.classList.toggle('hidden', document.querySelector('input[name="cd_scope"][value="local"]:checked') === null);
});
});
})();
</script>
<?php endif; ?>
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
<a href="<?= base_url('admin/code-details/' . (int) $kind->ck_idx) ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= base_url('bag/code-details/' . (int) $kind->ck_idx) ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -1,6 +1,6 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel"> <section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a href="<?= base_url('admin/code-details/' . (int) $kind->ck_idx) ?>" class="text-blue-600 hover:underline text-sm">&larr; <?= esc($kind->ck_name) ?></a> <a href="<?= base_url('bag/code-details/' . (int) $kind->ck_idx) ?>" class="text-blue-600 hover:underline text-sm">&larr; <?= esc($kind->ck_name) ?></a>
<span class="text-gray-400">|</span> <span class="text-gray-400">|</span>
<span class="text-sm font-bold text-gray-700">세부코드 수정</span> <span class="text-sm font-bold text-gray-700">세부코드 수정</span>
</div> </div>
@@ -39,7 +39,7 @@
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button>
<a href="<?= base_url('admin/code-details/' . (int) $kind->ck_idx) ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= base_url('bag/code-details/' . (int) $kind->ck_idx) ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -1,49 +0,0 @@
<?= view('components/print_header', ['printTitle' => '세부코드 관리 - ' . esc($kind->ck_name)]) ?>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex flex-wrap items-center justify-between gap-y-2">
<div class="flex items-center gap-2">
<a href="<?= base_url('admin/code-kinds') ?>" class="text-blue-600 hover:underline text-sm">&larr; 코드 종류</a>
<span class="text-gray-400">|</span>
<span class="text-sm font-bold text-gray-700">세부코드 — <?= esc($kind->ck_name) ?> (<?= esc($kind->ck_code) ?>)</span>
</div>
<div class="flex items-center gap-2">
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= base_url('admin/code-details/' . (int) $kind->ck_idx . '/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">세부코드 등록</a>
</div>
</div>
</section>
<div class="border border-gray-300 overflow-auto mt-2">
<table class="w-full data-table">
<thead>
<tr>
<th class="w-16">번호</th>
<th class="w-24">코드</th>
<th>코드명</th>
<th class="w-20">정렬순서</th>
<th class="w-20">상태</th>
<th>등록일</th>
<th class="w-28">작업</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach ($list as $row): ?>
<tr>
<td class="text-center"><?= esc($row->cd_idx) ?></td>
<td class="text-center font-mono"><?= esc($row->cd_code) ?></td>
<td class="text-left pl-2"><?= esc($row->cd_name) ?></td>
<td class="text-center"><?= (int) $row->cd_sort ?></td>
<td class="text-center"><?= (int) $row->cd_state === 1 ? '사용' : '미사용' ?></td>
<td class="text-left pl-2"><?= esc($row->cd_regdate ?? '') ?></td>
<td class="text-center">
<a href="<?= base_url('admin/code-details/edit/' . (int) $row->cd_idx) ?>" class="text-blue-600 hover:underline text-sm">수정</a>
<form action="<?= base_url('admin/code-details/delete/' . (int) $row->cd_idx) ?>" method="POST" class="inline ml-1" onsubmit="return confirm('이 세부코드를 삭제하시겠습니까?');">
<?= csrf_field() ?>
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>

View File

@@ -17,7 +17,7 @@
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
<a href="<?= base_url('admin/code-kinds') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= base_url('bag/code-kinds') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -25,7 +25,7 @@
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button>
<a href="<?= base_url('admin/code-kinds') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= base_url('bag/code-kinds') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -1,48 +0,0 @@
<?= view('components/print_header', ['printTitle' => '기본코드 종류 관리']) ?>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex flex-wrap items-center justify-between gap-y-2">
<span class="text-sm font-bold text-gray-700">기본코드 종류 관리</span>
<div class="flex items-center gap-2">
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= base_url('admin/code-kinds/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">코드 종류 등록</a>
</div>
</div>
</section>
<div class="border border-gray-300 overflow-auto mt-2">
<table class="w-full data-table">
<thead>
<tr>
<th class="w-16">번호</th>
<th class="w-20">코드</th>
<th>코드명</th>
<th class="w-24">세부코드 수</th>
<th class="w-20">상태</th>
<th>등록일</th>
<th class="w-40">작업</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach ($list as $row): ?>
<tr>
<td class="text-center"><?= esc($row->ck_idx) ?></td>
<td class="text-center font-mono font-bold"><?= esc($row->ck_code) ?></td>
<td class="text-left pl-2"><?= esc($row->ck_name) ?></td>
<td class="text-center">
<a href="<?= base_url('admin/code-details/' . (int) $row->ck_idx) ?>" class="text-blue-600 hover:underline"><?= (int) ($countMap[$row->ck_idx] ?? 0) ?>개</a>
</td>
<td class="text-center"><?= (int) $row->ck_state === 1 ? '사용' : '미사용' ?></td>
<td class="text-left pl-2"><?= esc($row->ck_regdate ?? '') ?></td>
<td class="text-center">
<a href="<?= base_url('admin/code-details/' . (int) $row->ck_idx) ?>" class="text-green-600 hover:underline text-sm mr-1">세부코드</a>
<a href="<?= base_url('admin/code-kinds/edit/' . (int) $row->ck_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">수정</a>
<form action="<?= base_url('admin/code-kinds/delete/' . (int) $row->ck_idx) ?>" method="POST" class="inline" onsubmit="return confirm('이 코드 종류를 삭제하시겠습니까?');">
<?= csrf_field() ?>
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>

View File

@@ -2,7 +2,7 @@
<span class="text-sm font-bold text-gray-700">업체 등록</span> <span class="text-sm font-bold text-gray-700">업체 등록</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/companies/store') ?>" method="POST" class="space-y-4"> <form action="<?= mgmt_url('companies/store') ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -42,7 +42,7 @@
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
<a href="<?= base_url('admin/companies') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= mgmt_url('companies') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -2,7 +2,7 @@
<span class="text-sm font-bold text-gray-700">업체 수정</span> <span class="text-sm font-bold text-gray-700">업체 수정</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/companies/update/' . (int) $item->cp_idx) ?>" method="POST" class="space-y-4"> <form action="<?= mgmt_url('companies/update/' . (int) $item->cp_idx) ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -50,7 +50,7 @@
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button>
<a href="<?= base_url('admin/companies') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= mgmt_url('companies') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -4,10 +4,23 @@
<span class="text-sm font-bold text-gray-700">업체 관리</span> <span class="text-sm font-bold text-gray-700">업체 관리</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button> <button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= base_url('admin/companies/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">업체 등록</a> <a href="<?= mgmt_url('companies/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">업체 등록</a>
</div> </div>
</div> </div>
</section> </section>
<section class="p-2 bg-white border-b border-gray-200 no-print">
<form method="GET" action="<?= mgmt_url('companies') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">업체유형</label>
<select name="cp_type" class="border border-gray-300 rounded px-2 py-1 text-sm w-44">
<option value="">전 체</option>
<?php foreach (($typeOptions ?? []) as $type): ?>
<option value="<?= esc($type) ?>" <?= (string) ($cpType ?? '') === (string) $type ? 'selected' : '' ?>><?= esc($type) ?></option>
<?php endforeach; ?>
</select>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
<a href="<?= mgmt_url('companies') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
</form>
</section>
<div class="border border-gray-300 overflow-auto mt-2"> <div class="border border-gray-300 overflow-auto mt-2">
<table class="w-full data-table"> <table class="w-full data-table">
<thead> <thead>
@@ -24,9 +37,17 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($list as $row): ?> <?php
$startNo = 1;
if (isset($pager) && method_exists($pager, 'getCurrentPage') && method_exists($pager, 'getPerPage')) {
$currentPage = (int) $pager->getCurrentPage();
$perPage = (int) $pager->getPerPage();
$startNo = (($currentPage > 0 ? $currentPage : 1) - 1) * ($perPage > 0 ? $perPage : 20) + 1;
}
?>
<?php foreach (($list ?? []) as $idx => $row): ?>
<tr> <tr>
<td class="text-center"><?= esc($row->cp_idx) ?></td> <td class="text-center"><?= (int) $startNo + (int) $idx ?></td>
<td class="text-center"><?= esc($row->cp_type) ?></td> <td class="text-center"><?= esc($row->cp_type) ?></td>
<td class="text-left pl-2"><?= esc($row->cp_name) ?></td> <td class="text-left pl-2"><?= esc($row->cp_name) ?></td>
<td class="text-center"><?= esc($row->cp_biz_no) ?></td> <td class="text-center"><?= esc($row->cp_biz_no) ?></td>
@@ -35,8 +56,8 @@
<td class="text-left pl-2"><?= esc($row->cp_addr) ?></td> <td class="text-left pl-2"><?= esc($row->cp_addr) ?></td>
<td class="text-center"><?= (int) $row->cp_state === 1 ? '사용' : '미사용' ?></td> <td class="text-center"><?= (int) $row->cp_state === 1 ? '사용' : '미사용' ?></td>
<td class="text-center"> <td class="text-center">
<a href="<?= base_url('admin/companies/edit/' . (int) $row->cp_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">수정</a> <a href="<?= mgmt_url('companies/edit/' . (int) $row->cp_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">수정</a>
<form action="<?= base_url('admin/companies/delete/' . (int) $row->cp_idx) ?>" method="POST" class="inline" onsubmit="return confirm('삭제하시겠습니까?');"> <form action="<?= mgmt_url('companies/delete/' . (int) $row->cp_idx) ?>" method="POST" class="inline" onsubmit="return confirm('삭제하시겠습니까?');">
<?= csrf_field() ?> <?= csrf_field() ?>
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button> <button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
</form> </form>

View File

@@ -6,6 +6,15 @@
</div> </div>
<?php else: ?> <?php else: ?>
<?php if (! empty($s['stats_unavailable'])): ?>
<div class="border border-amber-300 bg-amber-50 p-4 text-sm text-amber-900 mb-4">
발주·판매·재고·불출 통계 테이블이 아직 없거나 조회에 실패했습니다. MySQL에서
<code class="text-xs bg-white px-1 rounded">writable/database/order_tables.sql</code>과
<code class="text-xs bg-white px-1 rounded">writable/database/sales_tables.sql</code>을
<code class="text-xs bg-white px-1 rounded">jongryangje_dev</code> DB에 실행해 주세요.
</div>
<?php endif; ?>
<!-- 통계 카드 --> <!-- 통계 카드 -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4"> <div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
<div class="border border-gray-300 p-4 bg-white"> <div class="border border-gray-300 p-4 bg-white">
@@ -36,7 +45,7 @@
<div> <div>
<div class="flex items-center justify-between mb-1"> <div class="flex items-center justify-between mb-1">
<h3 class="text-sm font-bold text-gray-700">최근 발주 5건</h3> <h3 class="text-sm font-bold text-gray-700">최근 발주 5건</h3>
<a href="<?= base_url('admin/bag-orders') ?>" class="text-xs text-blue-600 hover:underline">전체보기</a> <a href="<?= base_url('bag/bag-orders') ?>" class="text-xs text-blue-600 hover:underline">전체보기</a>
</div> </div>
<div class="border border-gray-300 overflow-auto"> <div class="border border-gray-300 overflow-auto">
<table class="w-full data-table"> <table class="w-full data-table">
@@ -54,7 +63,7 @@
?> ?>
<tr> <tr>
<td class="font-mono text-sm"> <td class="font-mono text-sm">
<a href="<?= base_url('admin/bag-orders/detail/' . (int) $order->bo_idx) ?>" class="text-blue-600 hover:underline"><?= esc($order->bo_lot_no) ?></a> <a href="<?= base_url('bag/bag-orders/detail/' . (int) $order->bo_idx) ?>" class="text-blue-600 hover:underline"><?= esc($order->bo_lot_no) ?></a>
</td> </td>
<td><?= esc($order->bo_order_date) ?></td> <td><?= esc($order->bo_order_date) ?></td>
<td> <td>
@@ -81,7 +90,7 @@
<div> <div>
<div class="flex items-center justify-between mb-1"> <div class="flex items-center justify-between mb-1">
<h3 class="text-sm font-bold text-gray-700">최근 판매 5건</h3> <h3 class="text-sm font-bold text-gray-700">최근 판매 5건</h3>
<a href="<?= base_url('admin/bag-sales') ?>" class="text-xs text-blue-600 hover:underline">전체보기</a> <a href="<?= base_url('bag/bag-sales') ?>" class="text-xs text-blue-600 hover:underline">전체보기</a>
</div> </div>
<div class="border border-gray-300 overflow-auto"> <div class="border border-gray-300 overflow-auto">
<table class="w-full data-table"> <table class="w-full data-table">

View File

@@ -0,0 +1,137 @@
<?= view('components/print_header', ['printTitle' => '지정판매소 바코드 출력']) ?>
<style>
.ds-bc-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.ds-bc-table th, .ds-bc-table td { border: 1px solid #ccc; padding: 4px 6px; }
.ds-bc-table th { background: #e9ecef; color: #2d3748; }
.ds-bc-table td { background: #fff; }
.ds-bc-table td.name-cell { max-width: 14rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ds-bc-table td.addr-cell { max-width: 24rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ds-bc-check { width: 14px; height: 14px; }
</style>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex flex-wrap items-center justify-between gap-y-2">
<span class="text-sm font-bold text-gray-700">지정판매소 바코드 출력</span>
<div class="flex items-center gap-2">
<button type="button" id="ds-bc-print-btn" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">인쇄</button>
</div>
</div>
</section>
<section class="p-2 bg-white border-b border-gray-200 no-print">
<form id="ds-bc-filter-form" method="get" action="<?= mgmt_url('designated-shops/barcode') ?>" class="flex flex-wrap items-end gap-3">
<div class="min-w-[12rem]">
<label class="block text-xs text-gray-600 mb-0.5">군·구</label>
<div class="border border-gray-300 rounded px-3 py-1 text-sm bg-gray-50 text-gray-800 font-medium pointer-events-none select-none">
<?= esc($fixedGugunLabel ?? '현재 지자체') ?>
</div>
</div>
<div>
<label class="block text-xs text-gray-600 mb-0.5">읍·면·동</label>
<select name="ds_zone_code" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[10rem]">
<option value="">전체</option>
<?php foreach (($zones ?? []) as $z): ?>
<?php $zc = trim((string) ($z->zone_code ?? '')); ?>
<option value="<?= esc($zc) ?>" <?= ($zoneFilter ?? '') === $zc ? 'selected' : '' ?>><?= esc($zc) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="block text-xs text-gray-600 mb-0.5">조회순서</label>
<select name="order_by" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[8rem]">
<option value="shop_no" <?= ($orderBy ?? 'shop_no') === 'shop_no' ? 'selected' : '' ?>>판매소 코드</option>
<option value="name" <?= ($orderBy ?? '') === 'name' ? 'selected' : '' ?>>판매소명</option>
</select>
</div>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
</form>
</section>
<section class="mx-2 mt-2 mb-2">
<form id="ds-bc-print-form" method="post" action="<?= mgmt_url('designated-shops/barcode/print') ?>" target="ds-bc-print-frame">
<?= csrf_field() ?>
<input type="hidden" name="zone_label" value="<?= esc(($zoneFilter ?? '') !== '' ? (string) $zoneFilter : '전체') ?>">
<div class="mb-1 text-xs text-gray-600">
<label class="inline-flex items-center gap-1 cursor-pointer"><input type="checkbox" id="ds-bc-check-all" class="ds-bc-check"> 전체선택</label>
<span class="ml-3">선택 건수: <strong id="ds-bc-selected-count">0</strong></span>
</div>
<div class="overflow-auto border border-gray-300 bg-white">
<table class="ds-bc-table">
<thead>
<tr>
<th class="w-14">출력</th>
<th class="w-36">판매소 코드</th>
<th>판매소명</th>
<th class="w-24">대표자명</th>
<th class="w-32">사업자번호</th>
<th>사업장 주소</th>
<th class="w-16">상태</th>
</tr>
</thead>
<tbody>
<?php foreach (($list ?? []) as $row): ?>
<?php
$st = (int) ($row->ds_state ?? 1);
$stLabel = $st === 1 ? '사용' : '정지';
?>
<tr>
<td class="text-center"><input class="ds-bc-row-check ds-bc-check" type="checkbox" name="ds_idx[]" value="<?= (int) $row->ds_idx ?>"></td>
<td class="text-center text-blue-700"><?= esc((string) ($row->ds_shop_no ?? '')) ?></td>
<td class="name-cell text-blue-700" title="<?= esc((string) ($row->ds_name ?? '')) ?>"><?= esc((string) ($row->ds_name ?? '')) ?></td>
<td><?= esc((string) ($row->ds_rep_name ?? '')) ?></td>
<td><?= esc((string) ($row->ds_biz_no ?? '')) ?></td>
<td class="addr-cell" title="<?= esc((string) ($row->ds_addr ?? '')) ?>"><?= esc((string) ($row->ds_addr ?? '')) ?></td>
<td class="<?= $st === 1 ? 'text-blue-700' : 'text-red-600' ?>"><?= esc($stLabel) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($list)): ?>
<tr><td colspan="7" class="text-center text-gray-400 py-8">조회된 지정판매소가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</form>
<iframe name="ds-bc-print-frame" class="hidden" style="display:none;width:0;height:0;border:0;" aria-hidden="true"></iframe>
</section>
<?php if (isset($pager)): ?>
<div class="mt-2 mb-2 mx-2 no-print"><?= $pager->links() ?></div>
<?php endif; ?>
<script>
(function () {
var all = document.getElementById('ds-bc-check-all');
var countEl = document.getElementById('ds-bc-selected-count');
var printBtn = document.getElementById('ds-bc-print-btn');
var printForm = document.getElementById('ds-bc-print-form');
var rows = Array.prototype.slice.call(document.querySelectorAll('.ds-bc-row-check'));
if (!all || !countEl || !rows.length) return;
function refreshCount() {
var n = rows.filter(function (el) { return el.checked; }).length;
countEl.textContent = String(n);
all.checked = n > 0 && n === rows.length;
all.indeterminate = n > 0 && n < rows.length;
}
all.addEventListener('change', function () {
rows.forEach(function (el) { el.checked = all.checked; });
refreshCount();
});
rows.forEach(function (el) { el.addEventListener('change', refreshCount); });
if (printBtn && printForm) {
printBtn.addEventListener('click', function () {
var selected = rows.filter(function (el) { return el.checked; }).length;
if (selected < 1) {
window.alert('출력할 지정판매소를 1개 이상 선택해 주세요.');
return;
}
printForm.action = "<?= esc(mgmt_url('designated-shops/barcode/print')) ?>?autoprint=1";
printForm.submit();
});
}
refreshCount();
})();
</script>

View File

@@ -0,0 +1,94 @@
<?php
$rows = $rows ?? [];
$zoneLabel = trim((string) ($zoneLabel ?? '전체'));
$printedAt = trim((string) ($printedAt ?? date('Y.m.d')));
$chunks = array_chunk($rows, 12);
$totalPages = count($chunks);
?>
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>지정판매소 바코드</title>
<style>
body { margin: 0; font-family: Arial, "Apple SD Gothic Neo", "Malgun Gothic", sans-serif; color: #222; background: #fff; }
.page { width: 210mm; min-height: 297mm; margin: 0 auto; padding: 14mm 12mm 12mm; box-sizing: border-box; }
.title { text-align: center; font-size: 42px; letter-spacing: 1px; font-weight: 500; margin: 0 0 14px; }
.meta { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #555; padding-bottom: 4px; font-size: 13px; margin-bottom: 8px; }
.meta .center { font-weight: 700; }
.cards { display: flex; flex-wrap: wrap; align-content: flex-start; }
.card { width: 33.3333%; padding: 0 8px 12px; box-sizing: border-box; }
.barcode-wrap { min-height: 40px; }
.barcode-svg { width: 100%; max-width: 270px; height: 22px; }
.code-text { text-align: center; margin-top: 1px; font-size: 16px; letter-spacing: 0.35px; }
.name-text { text-align: center; margin-top: 5px; font-size: 14px; line-height: 1.2; word-break: keep-all; }
@media print {
@page { size: A4; margin: 0; }
.page { page-break-after: always; }
.page:last-child { page-break-after: auto; }
}
</style>
</head>
<body>
<?php if ($rows === []): ?>
<div class="page">
<h1 class="title">지정판매소 바코드</h1>
<p style="text-align:center; margin-top:30px; color:#666;">출력할 지정판매소가 없습니다.</p>
</div>
<?php else: ?>
<?php foreach ($chunks as $pageIndex => $pageRows): ?>
<section class="page">
<h1 class="title">지정판매소 바코드</h1>
<div class="meta">
<span>출 력 일 자: <?= esc($printedAt) ?></span>
<span class="center"><?= esc($zoneLabel) ?></span>
<span>페&nbsp;&nbsp;이&nbsp;&nbsp;지: <?= (int) ($pageIndex + 1) ?> / <?= (int) $totalPages ?></span>
</div>
<div class="cards">
<?php foreach ($pageRows as $row): ?>
<?php
$code = trim((string) ($row->ds_shop_no ?? ''));
$nm = trim((string) ($row->ds_name ?? ''));
$rep = trim((string) ($row->ds_rep_name ?? ''));
$label = trim($nm . ($rep !== '' ? ('-' . $rep) : ''));
?>
<div class="card">
<div class="barcode-wrap">
<svg class="barcode-svg" data-barcode="<?= esc($code, 'attr') ?>"></svg>
</div>
<div class="code-text"><?= esc($code) ?></div>
<div class="name-text"><?= esc($label) ?></div>
</div>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
<?php endif; ?>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
var svgs = document.querySelectorAll('svg[data-barcode]');
svgs.forEach(function (svg) {
var code = (svg.getAttribute('data-barcode') || '').trim();
if (!code) return;
try {
JsBarcode(svg, code, {
format: 'CODE128',
displayValue: false,
margin: 0,
height: 16,
width: 1.28
});
} catch (e) {
svg.outerHTML = '<div style="font-size:12px;color:#b91c1c;">바코드 생성 실패: ' + code + '</div>';
}
});
if (window.location.search.indexOf('autoprint=1') >= 0) {
setTimeout(function () { window.print(); }, 200);
}
});
</script>
</body>
</html>

View File

@@ -2,7 +2,7 @@
<span class="text-sm font-bold text-gray-700">지정판매소 등록</span> <span class="text-sm font-bold text-gray-700">지정판매소 등록</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/designated-shops/store') ?>" method="POST" class="space-y-4"> <form id="designated-shop-create-form" action="<?= mgmt_url('designated-shops/store') ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<?php if (! empty($localGovs)): ?> <?php if (! empty($localGovs)): ?>
@@ -23,14 +23,18 @@
<div class="text-sm"> <div class="text-sm">
<span class="font-semibold"><?= esc($currentLg->lg_name) ?></span> <span class="font-semibold"><?= esc($currentLg->lg_name) ?></span>
<span class="text-gray-500 ml-2">(<?= esc($currentLg->lg_code) ?>)</span> <span class="text-gray-500 ml-2">(<?= esc($currentLg->lg_code) ?>)</span>
<span class="text-gray-500 ml-1"><?= esc(trim((string) ($currentLg->lg_sido ?? '') . ' ' . (string) ($currentLg->lg_gugun ?? ''))) ?></span>
</div> </div>
<input type="hidden" name="ds_lg_idx" value="<?= esc($currentLg->lg_idx) ?>"/> <input type="hidden" name="ds_lg_idx" value="<?= esc($currentLg->lg_idx) ?>"/>
</div> </div>
<?php endif; ?> <?php endif; ?>
<input type="hidden" name="addr_search_sido" id="addr_search_sido" value="<?= esc((string) old('addr_search_sido', '')) ?>"/>
<input type="hidden" name="addr_search_sigungu" id="addr_search_sigungu" value="<?= esc((string) old('addr_search_sigungu', '')) ?>"/>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">판매소번호</label> <label class="block text-sm font-bold text-gray-700 w-28">판매소번호</label>
<div class="text-sm text-gray-600">등록 시 자동 부여 (지자체코드 + 일련번호 3자리)</div> <div class="text-sm text-gray-600">등록 시 자동 부여 (주소 기준 기본코드 B·C·D 조합 + 일련번호 3자리)</div>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -49,23 +53,50 @@
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">가상계좌</label> <label class="block text-sm font-bold text-gray-700 w-28">업태</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="ds_va_number" type="text" value="<?= esc(old('ds_va_number')) ?>"/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-48" name="ds_biz_type" type="text" value="<?= esc(old('ds_biz_type')) ?>"/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">업종</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-48" name="ds_biz_kind" type="text" value="<?= esc(old('ds_biz_kind')) ?>"/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">가상계좌(은행)</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-48" name="ds_va_bank" type="text" value="<?= esc(old('ds_va_bank')) ?>" placeholder="예: 농협"/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">계좌번호</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="ds_va_account" type="text" value="<?= esc(old('ds_va_account')) ?>"/>
</div>
<div class="flex flex-wrap items-start gap-2">
<label class="block text-sm font-bold text-gray-700 w-28 pt-1.5">주소</label>
<p class="text-xs text-gray-600 max-w-lg leading-relaxed">우편번호 옆 <strong>주소 검색</strong>으로만 지정합니다(직접 입력 불가). <strong>지도</strong>는 카카오맵에서 현재 입력된 주소를 검색해 엽니다. 관할이 아니면 검색 직후 안내 후 반영되지 않습니다. 동·호 등은 아래 <strong>상세주소</strong>에 입력하세요.</p>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">우편번호</label> <label class="block text-sm font-bold text-gray-700 w-28">우편번호</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32" name="ds_zip" type="text" value="<?= esc(old('ds_zip')) ?>"/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_zip" id="ds_zip" type="text" value="<?= esc(old('ds_zip')) ?>" maxlength="10" autocomplete="postal-code" readonly tabindex="-1"/>
<button type="button" id="btn-ds-kakao-postcode" class="no-print border border-btn-print-border text-gray-700 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50 transition shrink-0">주소 검색</button>
<?= view('components/kakao_map_link_button', ['buttonId' => 'btn-ds-kakao-map-create']) ?>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">도로명주소</label> <label class="block text-sm font-bold text-gray-700 w-28">도로명주소</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96" name="ds_addr" type="text" value="<?= esc(old('ds_addr')) ?>"/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 max-w-full bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_addr" id="ds_addr" type="text" value="<?= esc(old('ds_addr')) ?>" readonly tabindex="-1"/>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">지번주소</label> <label class="block text-sm font-bold text-gray-700 w-28">지번주소</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96" name="ds_addr_jibun" type="text" value="<?= esc(old('ds_addr_jibun')) ?>"/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 max-w-full bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_addr_jibun" id="ds_addr_jibun" type="text" value="<?= esc(old('ds_addr_jibun')) ?>" readonly tabindex="-1"/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">상세주소</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 max-w-full" name="ds_addr_detail" id="ds_addr_detail" type="text" value="<?= esc(old('ds_addr_detail')) ?>" maxlength="200" placeholder="동·호수·층 등"/>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -88,15 +119,49 @@
<div class="text-sm text-gray-600">해당 지자체(구·군) 코드로 등록 시 자동 설정</div> <div class="text-sm text-gray-600">해당 지자체(구·군) 코드로 등록 시 자동 설정</div>
</div> </div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">구역</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-72" name="ds_zone_code" type="text" value="<?= esc(old('ds_zone_code')) ?>"/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">종사업장번호</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-48" name="ds_branch_no" type="text" value="<?= esc(old('ds_branch_no')) ?>"/>
</div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">지정일자</label> <label class="block text-sm font-bold text-gray-700 w-28">지정일자</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-40" name="ds_designated_at" type="date" value="<?= esc(old('ds_designated_at')) ?>"/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-40" name="ds_designated_at" type="date" value="<?= esc(old('ds_designated_at')) ?>"/>
</div> </div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">변경일자</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-40" name="ds_state_changed_at" type="date" value="<?= esc(old('ds_state_changed_at')) ?>"/>
</div>
<div class="flex flex-wrap items-start gap-2">
<label class="block text-sm font-bold text-gray-700 w-28 pt-1.5">변경사유</label>
<textarea class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 min-h-[4rem]" name="ds_change_reason" rows="3"><?= esc(old('ds_change_reason')) ?></textarea>
</div>
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button> <button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
<a href="<?= base_url('admin/designated-shops') ?>" class="bg-white text-black border border-btn-print-border px-4 py-1.5 rounded-sm text-sm shadow hover:bg-gray-50 transition">목록</a> <a href="<?= mgmt_url('designated-shops') ?>" class="bg-white text-black border border-btn-print-border px-4 py-1.5 rounded-sm text-sm shadow hover:bg-gray-50 transition">목록</a>
</div> </div>
</form> </form>
</div> </div>
<?= view('components/kakao_map_modal', ['kakaoJavascriptKey' => $kakaoJavascriptKey ?? '']) ?>
<?= view('components/kakao_address_search', [
'buttonId' => 'btn-ds-kakao-postcode',
'zipName' => 'ds_zip',
'roadName' => 'ds_addr',
'jibunName' => 'ds_addr_jibun',
'sidoFieldName' => 'addr_search_sido',
'sigunguFieldName' => 'addr_search_sigungu',
'tenantScope' => $addrTenantScope ?? ['lg_sido' => '', 'lg_gugun' => ''],
'roadBaseOnly' => true,
'detailFieldName' => 'ds_addr_detail',
]) ?>

View File

@@ -0,0 +1,210 @@
<?php
$ry = (int) ($reportYear ?? (int) date('Y'));
$lg = $currentLg ?? null;
$lgSido = $lg !== null ? trim((string) ($lg->lg_sido ?? '')) : '';
$lgGugun = $lg !== null ? trim((string) ($lg->lg_gugun ?? '')) : '';
$lgName = $lg !== null ? trim((string) ($lg->lg_name ?? '')) : '';
$scopeLabel = $lgSido !== '' && $lgGugun !== ''
? $lgSido . ' ' . $lgGugun
: ($lgName !== '' ? $lgName : '—');
$exportUrl = mgmt_url('designated-shops/district-new-cancel/export') . '?' . http_build_query(['year' => $ry]);
?>
<?= view('components/print_header', ['printTitle' => '지정 판매소 신규/취소 현황 (' . $ry . '년)']) ?>
<style>
.gbms-dnc-wrap { max-width: 100%; }
.gbms-dnc-table { border-collapse: collapse; width: 100%; font-size: 13px; }
.gbms-dnc-table th,
.gbms-dnc-table td {
border: 1px solid #7a8aa0;
padding: 6px 10px;
text-align: center;
}
.gbms-dnc-table thead th {
background: linear-gradient(180deg, #e8eef6 0%, #d4dee9 100%);
font-weight: 700;
color: #1a2a3a;
}
.gbms-dnc-table thead th.gbms-sub {
background: #dce6f0;
font-weight: 600;
}
.gbms-dnc-table tbody td.text-left { text-align: left; }
.gbms-dnc-table tbody tr.gbms-total td {
font-weight: 700;
border: 2px solid #c62828;
background: #fff8f8;
}
.gbms-dnc-caption {
font-size: 13px;
font-weight: 700;
margin: 8px 0 6px;
color: #1a2a3a;
}
.gbms-unit-pill {
display: inline-block;
margin-left: 8px;
padding: 2px 8px;
font-size: 11px;
font-weight: 600;
color: #0d47a1;
background: #e3f2fd;
border: 1px solid #90caf9;
border-radius: 2px;
}
.gbms-tip {
position: relative;
display: inline-flex;
flex-wrap: nowrap;
align-items: center;
gap: 4px;
}
.gbms-help {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1rem;
height: 1rem;
padding: 0 2px;
border: 1px solid #5c6f85;
border-radius: 999px;
background: #fef9c3;
color: #111827;
font-size: 11px;
font-weight: 700;
line-height: 1;
font-family: Arial, sans-serif;
user-select: none;
cursor: help;
}
.gbms-help::after {
content: attr(data-tip);
position: absolute;
left: 50%;
top: calc(100% + 6px);
transform: translateX(-50%);
display: none;
min-width: 12rem;
max-width: 14rem;
padding: 6px 8px;
border: 1px solid #8fa0b3;
border-radius: 4px;
background: #1f2937;
color: #fff;
font-size: 11px;
font-weight: 500;
line-height: 1.35;
text-align: left;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
z-index: 30;
}
.gbms-help:hover::after,
.gbms-help:focus::after {
display: block;
}
@media print {
.gbms-dnc-table { font-size: 11px; }
.gbms-help { display: none !important; }
}
</style>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel no-print">
<div class="flex flex-wrap items-center justify-between gap-y-2">
<span class="text-sm font-bold text-gray-800">[지정 판매소 신규/취소 현황]</span>
<div class="flex items-center gap-2">
<a href="<?= esc($exportUrl) ?>" class="border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
<button type="button" onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= mgmt_url('designated-shops') ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">목록</a>
</div>
</div>
</section>
<section class="p-3 bg-white border-b border-gray-200 no-print">
<form method="get" action="<?= mgmt_url('designated-shops/district-new-cancel') ?>" class="flex flex-wrap items-end gap-4">
<div>
<label class="block text-xs text-gray-600 mb-0.5">조회년도</label>
<select name="year" class="border border-gray-400 rounded px-2 py-1.5 text-sm min-w-[7rem] bg-white">
<?php foreach (($yearChoices ?? []) as $y): ?>
<option value="<?= (int) $y ?>" <?= $ry === (int) $y ? 'selected' : '' ?>><?= (int) $y ?>년</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex-1 min-w-[12rem]">
<span class="block text-xs text-gray-600 mb-0.5">군·구 (소속 지자체)</span>
<div class="border border-gray-300 rounded px-3 py-1.5 text-sm bg-gray-50 text-gray-800 font-medium">
<?= esc($scopeLabel) ?>
</div>
</div>
<span class="gbms-unit-pill self-end mb-0.5">단위: 판매소</span>
<button type="submit" class="bg-btn-search text-white px-5 py-1.5 rounded-sm text-sm font-medium shadow-sm hover:opacity-90">조회</button>
</form>
<p class="text-xs text-gray-500 mt-2">
종전·현행은 각각 <?= $ry - 1 ?>-12-31·<?= $ry ?>-12-31 기준 정상(지정일 이전·비정상 전환일 이후) 건수이며, 지정·취소는 <?= $ry ?>년 달력연도 기준입니다. 구·군 행은 효과 지자체의 기본코드(구 코드) 순서로 표시됩니다.
</p>
</section>
<div class="mx-2 mt-3 mb-4 gbms-dnc-wrap">
<div class="gbms-dnc-caption">지정 판매소 신규/취소 현황 조회 내역</div>
<div class="overflow-x-auto border border-gray-400 bg-white">
<table class="gbms-dnc-table">
<thead>
<tr>
<th rowspan="2" class="min-w-[6rem]">군·구</th>
<th rowspan="2">
<span class="gbms-tip">
종전
<span class="gbms-help" tabindex="0" aria-label="종전 설명" data-tip="전년도 12월 31일 기준 정상 상태 판매소 수">?</span>
</span>
<br><span class="text-xs font-normal">(전년도말)</span>
</th>
<th colspan="2">사용</th>
<th rowspan="2">
<span class="gbms-tip">
현행
<span class="gbms-help" tabindex="0" aria-label="현행 설명" data-tip="조회년도 12월 31일 기준 정상 상태 판매소 수">?</span>
</span>
<br><span class="text-xs font-normal">(금년도말)</span>
</th>
</tr>
<tr>
<th class="gbms-sub">
<span class="gbms-tip">
지정
<span class="gbms-help" tabindex="0" aria-label="지정 설명" data-tip="조회년도 내 지정일이 속한 신규 지정 건수">?</span>
</span>
</th>
<th class="gbms-sub">
<span class="gbms-tip">
취소
<span class="gbms-help" tabindex="0" aria-label="취소 설명" data-tip="조회년도 내 폐업/해지 전환일이 속한 건수">?</span>
</span>
</th>
</tr>
</thead>
<tbody>
<?php foreach (($districtRows ?? []) as $row): ?>
<tr>
<td class="text-left font-medium"><?= esc($row->region_label) ?></td>
<td><?= number_format((int) $row->prev_end) ?></td>
<td><?= number_format((int) $row->designated_y) ?></td>
<td><?= number_format((int) $row->cancelled_y) ?></td>
<td><?= number_format((int) $row->curr_end) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($districtRows)): ?>
<tr>
<td colspan="5" class="text-center text-gray-500 py-8">표시할 구·군 또는 지정판매소 데이터가 없습니다.</td>
</tr>
<?php endif; ?>
<?php if (! empty($districtRows) && isset($districtTotal)): ?>
<tr class="gbms-total">
<td class="text-left"><?= esc($districtTotal->region_label) ?></td>
<td><?= number_format((int) $districtTotal->prev_end) ?></td>
<td><?= number_format((int) $districtTotal->designated_y) ?></td>
<td><?= number_format((int) $districtTotal->cancelled_y) ?></td>
<td><?= number_format((int) $districtTotal->curr_end) ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>

View File

@@ -5,20 +5,35 @@ if ($shop === null) {
return; return;
} }
$v = fn ($key, $default = '') => old($key) !== null && old($key) !== '' ? old($key) : ($shop->{$key} ?? $default); $v = fn ($key, $default = '') => old($key) !== null && old($key) !== '' ? old($key) : ($shop->{$key} ?? $default);
$vaAccountDefault = (isset($shop->ds_va_account) && (string) $shop->ds_va_account !== '')
? (string) $shop->ds_va_account
: (string) ($shop->ds_va_number ?? '');
$dateField = static function (string $key) use ($shop, $v): string {
$s = (string) $v($key);
if ($s === '' || str_starts_with($s, '0000')) {
return '';
}
return $s;
};
?> ?>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel"> <section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<span class="text-sm font-bold text-gray-700">지정판매소 수정</span> <span class="text-sm font-bold text-gray-700">지정판매소 수정</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/designated-shops/update/' . (int) $shop->ds_idx) ?>" method="POST" class="space-y-4"> <form id="designated-shop-edit-form" action="<?= mgmt_url('designated-shops/update/' . (int) $shop->ds_idx) ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<input type="hidden" name="addr_search_sido" id="addr_search_sido" value="<?= esc((string) old('addr_search_sido', $currentLg !== null ? (string) ($currentLg->lg_sido ?? '') : '')) ?>"/>
<input type="hidden" name="addr_search_sigungu" id="addr_search_sigungu" value="<?= esc((string) old('addr_search_sigungu', $currentLg !== null ? (string) ($currentLg->lg_gugun ?? '') : '')) ?>"/>
<?php if ($currentLg !== null): ?> <?php if ($currentLg !== null): ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">지자체</label> <label class="block text-sm font-bold text-gray-700 w-28">지자체</label>
<div class="text-sm"> <div class="text-sm">
<span class="font-semibold"><?= esc($currentLg->lg_name) ?></span> <span class="font-semibold"><?= esc($currentLg->lg_name) ?></span>
<span class="text-gray-500 ml-2">(<?= esc($currentLg->lg_code) ?>)</span> <span class="text-gray-500 ml-2">(<?= esc($currentLg->lg_code) ?>)</span>
<span class="text-gray-500 ml-1"><?= esc(trim((string) ($currentLg->lg_sido ?? '') . ' ' . (string) ($currentLg->lg_gugun ?? ''))) ?></span>
</div> </div>
</div> </div>
<?php endif; ?> <?php endif; ?>
@@ -49,23 +64,50 @@ $v = fn ($key, $default = '') => old($key) !== null && old($key) !== '' ? old($k
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">가상계좌</label> <label class="block text-sm font-bold text-gray-700 w-28">업태</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="ds_va_number" type="text" value="<?= esc($v('ds_va_number')) ?>"/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-48" name="ds_biz_type" type="text" value="<?= esc($v('ds_biz_type')) ?>"/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">업종</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-48" name="ds_biz_kind" type="text" value="<?= esc($v('ds_biz_kind')) ?>"/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">가상계좌(은행)</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-48" name="ds_va_bank" type="text" value="<?= esc($v('ds_va_bank')) ?>"/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">계좌번호</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="ds_va_account" type="text" value="<?= esc((old('ds_va_account') !== null && old('ds_va_account') !== '') ? old('ds_va_account') : $vaAccountDefault) ?>"/>
</div>
<div class="flex flex-wrap items-start gap-2">
<label class="block text-sm font-bold text-gray-700 w-28 pt-1.5">주소</label>
<p class="text-xs text-gray-600 max-w-lg leading-relaxed">우편·도로명·지번은 <strong>주소 검색</strong>으로만 바꿀 수 있습니다(직접 입력 불가). <strong>지도</strong>는 카카오맵에서 현재 입력된 주소를 검색해 엽니다. 관할이 아니면 검색 직후 안내 후 반영되지 않습니다. 동·호 등은 <strong>상세주소</strong>에 입력하세요.</p>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">우편번호</label> <label class="block text-sm font-bold text-gray-700 w-28">우편번호</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32" name="ds_zip" type="text" value="<?= esc($v('ds_zip')) ?>"/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_zip" id="ds_zip" type="text" value="<?= esc($v('ds_zip')) ?>" maxlength="10" readonly tabindex="-1"/>
<button type="button" id="btn-ds-kakao-postcode-edit" class="no-print border border-btn-print-border text-gray-700 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50 transition shrink-0">주소 검색</button>
<?= view('components/kakao_map_link_button', ['buttonId' => 'btn-ds-kakao-map-edit']) ?>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">도로명주소</label> <label class="block text-sm font-bold text-gray-700 w-28">도로명주소</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96" name="ds_addr" type="text" value="<?= esc($v('ds_addr')) ?>"/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 max-w-full bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_addr" id="ds_addr" type="text" value="<?= esc($v('ds_addr')) ?>" readonly tabindex="-1"/>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">지번주소</label> <label class="block text-sm font-bold text-gray-700 w-28">지번주소</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96" name="ds_addr_jibun" type="text" value="<?= esc($v('ds_addr_jibun')) ?>"/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 max-w-full bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_addr_jibun" id="ds_addr_jibun" type="text" value="<?= esc($v('ds_addr_jibun')) ?>" readonly tabindex="-1"/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">상세주소</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 max-w-full" name="ds_addr_detail" id="ds_addr_detail" type="text" value="<?= esc($v('ds_addr_detail')) ?>" maxlength="200" placeholder="동·호수·층 등"/>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -83,6 +125,16 @@ $v = fn ($key, $default = '') => old($key) !== null && old($key) !== '' ? old($k
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="ds_email" type="email" value="<?= esc($v('ds_email')) ?>"/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="ds_email" type="email" value="<?= esc($v('ds_email')) ?>"/>
</div> </div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">구역</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-72" name="ds_zone_code" type="text" value="<?= esc($v('ds_zone_code')) ?>"/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">종사업장번호</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-48" name="ds_branch_no" type="text" value="<?= esc($v('ds_branch_no')) ?>"/>
</div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">지정일자</label> <label class="block text-sm font-bold text-gray-700 w-28">지정일자</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-40" name="ds_designated_at" type="date" value="<?= esc($v('ds_designated_at')) ?>"/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-40" name="ds_designated_at" type="date" value="<?= esc($v('ds_designated_at')) ?>"/>
@@ -97,9 +149,33 @@ $v = fn ($key, $default = '') => old($key) !== null && old($key) !== '' ? old($k
</select> </select>
</div> </div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">변경일자</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-40" name="ds_state_changed_at" type="date" value="<?= esc($dateField('ds_state_changed_at')) ?>"/>
</div>
<div class="flex flex-wrap items-start gap-2">
<label class="block text-sm font-bold text-gray-700 w-28 pt-1.5">변경사유</label>
<textarea class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 min-h-[4rem]" name="ds_change_reason" rows="3"><?= esc($v('ds_change_reason')) ?></textarea>
</div>
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button> <button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button>
<a href="<?= base_url('admin/designated-shops') ?>" class="bg-white text-black border border-btn-print-border px-4 py-1.5 rounded-sm text-sm shadow hover:bg-gray-50 transition">목록</a> <a href="<?= mgmt_url('designated-shops') ?>" class="bg-white text-black border border-btn-print-border px-4 py-1.5 rounded-sm text-sm shadow hover:bg-gray-50 transition">목록</a>
</div> </div>
</form> </form>
</div> </div>
<?= view('components/kakao_map_modal', ['kakaoJavascriptKey' => $kakaoJavascriptKey ?? '']) ?>
<?= view('components/kakao_address_search', [
'buttonId' => 'btn-ds-kakao-postcode-edit',
'zipName' => 'ds_zip',
'roadName' => 'ds_addr',
'jibunName' => 'ds_addr_jibun',
'sidoFieldName' => 'addr_search_sido',
'sigunguFieldName' => 'addr_search_sigungu',
'tenantScope' => $addrTenantScope ?? ['lg_sido' => '', 'lg_gugun' => ''],
'roadBaseOnly' => true,
'detailFieldName' => 'ds_addr_detail',
]) ?>

View File

@@ -1,24 +1,202 @@
<?= view('components/print_header', ['printTitle' => '지정판매소 목록']) ?> <?php
helper('admin');
$currentPath = current_nav_request_path();
if ($currentPath === 'bag/designated-shops') {
$readOnly = false;
} elseif ($currentPath === 'bag/designated-shops/browse') {
$readOnly = true;
} else {
$readOnly = ! empty($readOnly);
}
?>
<?= view('components/print_header', ['printTitle' => $readOnly ? '지정판매소 조회 목록' : '지정판매소 목록']) ?>
<style>
/* 목록 위 → 지정판매소 정보 아래 (가로 2열 없음) */
.ds-split {
display: flex;
flex-direction: column;
gap: 0.5rem;
min-height: 0;
flex: 1 1 auto;
}
.ds-list-panel {
flex: 0 1 auto;
width: 100%;
max-height: 42vh;
display: flex;
flex-direction: column;
min-height: 0;
border: 1px solid #ccc;
background: #fff;
}
.ds-detail-panel {
flex: 1 1 auto;
width: 100%;
min-width: 0;
min-height: 12rem;
border: 1px solid #ccc;
background: #f5f5f5;
display: flex;
flex-direction: column;
}
.ds-panel-title {
font-size: 12px;
font-weight: bold;
padding: 6px 10px;
background: linear-gradient(180deg, #fafafa 0%, #e9ecef 100%);
border-bottom: 1px solid #ccc;
color: #333;
}
.ds-summary-bar {
font-size: 12px;
padding: 6px 10px;
background: #fff3cd;
border: 1px solid #ffc107;
color: #333;
}
.ds-row-selected td { background-color: #cce5ff !important; }
.ds-detail-inner { padding: 10px; overflow: auto; flex: 1; }
/* 원본 지정판매소 정보: 라벨 고정폭 + 2열 값(우측 값이 더 넓음), 주소·개인전화는 전폭 */
.ds-detail-form {
font-size: 12px;
border: 1px solid #bbb;
background: #fff;
}
.ds-row {
display: grid;
gap: 0;
border-bottom: 1px solid #ccc;
}
.ds-detail-form > .ds-row:last-child { border-bottom: none; }
/* 그 외 2+2 동일 비율 (상호명 | 우편번호 등) */
.ds-row-4-even {
grid-template-columns: 5.5rem minmax(0, 1fr) 5.5rem minmax(0, 1fr);
}
/* 판매소번호 전폭 행 — 값을 우편·주소 필드처럼 넓게 */
.ds-value-shop-wide {
font-weight: 600;
font-variant-numeric: tabular-nums;
letter-spacing: 0.04em;
font-size: 13px;
padding-top: 8px;
padding-bottom: 8px;
}
/* 라벨 | 값(나머지 전체) — 도로명·지번·개인전화·이메일 */
.ds-row-wide {
grid-template-columns: 5.5rem minmax(0, 1fr);
}
.ds-row-wide-tall .ds-field-value {
min-height: 3.25rem;
align-content: start;
}
/* 도로명주소 + 카카오맵 버튼 */
.ds-field-value-with-map {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 8px;
}
.ds-field-value-with-map .ds-addr-text {
flex: 1 1 12rem;
min-width: 0;
word-break: break-word;
}
.ds-field-label {
background: #eef2f5;
border-right: 1px solid #ccc;
padding: 5px 8px;
font-weight: 600;
color: #333;
display: flex;
align-items: center;
}
.ds-field-value {
padding: 5px 8px;
background: #fff;
word-break: break-word;
border-right: 1px solid #ccc;
}
.ds-row-4-even > *:nth-child(4n) { border-right: none; }
.ds-row-wide > .ds-field-value { border-right: none; }
.ds-field-hint {
font-size: 11px;
color: #b91c1c;
margin-top: 4px;
line-height: 1.35;
}
@media (max-width: 720px) {
.ds-row-4-even { grid-template-columns: 5rem 1fr; }
}
.ds-detail-actions { padding: 10px; border-top: 1px solid #ccc; background: #eee; }
.ds-detail-info-wrap { overflow-x: auto; }
.ds-detail-info-wrap .data-table th { white-space: nowrap; }
.ds-detail-info-wrap th.ds-col-tight-head,
.ds-detail-info-wrap td.ds-col-tight-cell {
max-width: 6.5rem;
width: 6.5rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ds-list-panel .ds-col-tight {
max-width: 6rem;
width: 6rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ds-list-panel .ds-col-zip {
width: 4.5rem;
max-width: 5rem;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.ds-list-panel .ds-col-addr-list {
max-width: 11rem;
min-width: 6rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ds-list-panel .ds-col-detail-list {
max-width: 8rem;
min-width: 4rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ds-ro-road-btn { margin-left: 6px; vertical-align: middle; }
</style>
<?php
$listBasePath = $readOnly ? 'designated-shops/browse' : 'designated-shops';
?>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel"> <section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex flex-wrap items-center justify-between gap-y-2"> <div class="flex flex-wrap items-center justify-between gap-y-2">
<span class="text-sm font-bold text-gray-700">지정판매소 목록</span> <span class="text-sm font-bold text-gray-700"><?= $readOnly ? '지정판매소 조회' : '지정판매소 관리' ?></span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a href="<?= base_url('admin/designated-shops/export') ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a> <?php if ($readOnly): ?>
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button> <a href="<?= mgmt_url('designated-shops/export') ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
<a href="<?= base_url('admin/designated-shops/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">지정판매소 등록</a> <button type="button" onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<?php endif; ?>
<?php if (! $readOnly): ?>
<a href="<?= mgmt_url('designated-shops/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">지정판매소 등록</a>
<?php endif; ?>
</div> </div>
</div> </div>
</section> </section>
<!-- P2-15: 다조건 검색 -->
<section class="p-2 bg-white border-b border-gray-200 no-print"> <section class="p-2 bg-white border-b border-gray-200 no-print">
<form method="GET" action="<?= base_url('admin/designated-shops') ?>" class="flex flex-wrap items-center gap-2"> <form method="GET" action="<?= mgmt_url($listBasePath) ?>" class="flex flex-wrap items-center gap-2">
<span class="text-sm font-semibold text-gray-700 mr-1">지정판매소 검색</span>
<label class="text-sm text-gray-600">상호명</label> <label class="text-sm text-gray-600">상호명</label>
<input type="text" name="ds_name" value="<?= esc($dsName ?? '') ?>" placeholder="상호명 검색" class="border border-gray-300 rounded px-2 py-1 text-sm w-40"/> <input type="text" name="ds_name" value="<?= esc($dsName ?? '') ?>" placeholder="상호명" class="border border-gray-300 rounded px-2 py-1 text-sm w-36"/>
<label class="text-sm text-gray-600">구코드</label> <label class="text-sm text-gray-600">구·군 코드</label>
<select name="ds_gugun_code" class="border border-gray-300 rounded px-2 py-1 text-sm"> <select name="ds_gugun_code" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[14rem]">
<option value="">전체</option> <option value="">전체</option>
<?php foreach (($gugunCodes ?? []) as $gc): ?> <?php foreach (($gugunCodes ?? []) as $gc): ?>
<option value="<?= esc($gc->ds_gugun_code) ?>" <?= ($dsGugunCode ?? '') === $gc->ds_gugun_code ? 'selected' : '' ?>><?= esc($gc->ds_gugun_code) ?></option> <?php $gCode = (string) ($gc->ds_gugun_code ?? ''); ?>
<option value="<?= esc($gCode) ?>" <?= ($dsGugunCode ?? '') === $gCode ? 'selected' : '' ?>><?= esc((string) (($gugunNameMap[$gCode] ?? '') !== '' ? $gugunNameMap[$gCode] : $gCode)) ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<label class="text-sm text-gray-600">상태</label> <label class="text-sm text-gray-600">상태</label>
@@ -29,48 +207,357 @@
<option value="3" <?= ($dsState ?? '') === '3' ? 'selected' : '' ?>>직권해지</option> <option value="3" <?= ($dsState ?? '') === '3' ? 'selected' : '' ?>>직권해지</option>
</select> </select>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button> <button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
<a href="<?= base_url('admin/designated-shops') ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">초기화</a> <a href="<?= mgmt_url($listBasePath) ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">초기화</a>
</form> </form>
</section> </section>
<div class="border border-gray-300 overflow-auto mt-2">
<table class="w-full data-table"> <?php
$sc = $stateCounts ?? ['total' => 0, 1 => 0, 2 => 0, 3 => 0];
?>
<div class="ds-summary-bar no-print mx-2 mt-2 rounded-sm">
건수 : <?= (int) ($sc['total'] ?? 0) ?>
(정상 : <?= (int) ($sc[1] ?? 0) ?> / 폐업 : <?= (int) ($sc[2] ?? 0) ?> / 해지 : <?= (int) ($sc[3] ?? 0) ?>)
</div>
<div class="ds-split no-print mx-2 mb-2 mt-2 flex-1 min-h-0">
<div class="ds-list-panel">
<div class="ds-panel-title shrink-0">지정판매소 리스트</div>
<div class="overflow-auto flex-1 min-h-0">
<table class="w-full data-table">
<thead>
<tr>
<th class="w-14">번호</th>
<th class="w-24">구·군</th>
<th class="w-24">지정일</th>
<th class="w-24">구역</th>
<th class="ds-col-tight">대표자명</th>
<th class="ds-col-tight">상호명</th>
<th class="ds-col-zip">우편번호</th>
<th class="text-left">주소</th>
<th class="w-28">사업자번호</th>
<th class="w-28">전화</th>
<th class="w-16">상태</th>
</tr>
</thead>
<tbody id="ds-list-body" class="text-right">
<?php foreach ($list as $i => $row): ?>
<?php
$sn = (string) ($row->ds_shop_no ?? '');
if (preg_match('/(\d{3})$/', $sn, $m)) {
$shortNo = $m[1];
} elseif ($sn !== '' && strlen($sn) >= 3) {
$shortNo = substr($sn, -3);
} else {
$shortNo = $sn;
}
$st = (int) ($row->ds_state ?? 1);
$stLabel = $st === 1 ? '' : ($st === 2 ? '폐업' : '해지');
$ggCode = (string) ($row->ds_gugun_code ?? '');
$ggLabel = (string) (($gugunNameMap[$ggCode] ?? '') !== '' ? $gugunNameMap[$ggCode] : $ggCode);
$da = $row->ds_designated_at ?? null;
$daDisp = ($da !== null && $da !== '' && (string) $da !== '0000-00-00') ? substr((string) $da, 0, 10) : '';
$zone = (string) ($row->ds_zone_code ?? '');
$zipList = trim((string) ($row->ds_zip ?? ''));
$roadL = trim((string) ($row->ds_addr ?? ''));
$jibunL = trim((string) ($row->ds_addr_jibun ?? ''));
$addrMainList = $roadL !== '' ? $roadL : $jibunL;
$addrDetailList = trim((string) ($row->ds_addr_detail ?? ''));
$addrCombinedList = trim($addrMainList . ' ' . $addrDetailList);
if ($addrCombinedList === '') {
$addrCombinedList = $addrMainList;
}
?>
<tr class="ds-list-row cursor-pointer" data-row-index="<?= (int) $i ?>" role="button" tabindex="0">
<td class="text-center"><?= esc($shortNo) ?></td>
<td class="text-left pl-1 text-xs"><?= esc($ggLabel) ?></td>
<td class="text-center text-xs"><?= esc($daDisp) ?></td>
<td class="text-left pl-1 text-xs"><?= esc($zone) ?></td>
<td class="text-left pl-1 text-xs ds-col-tight" title="<?= esc($row->ds_rep_name ?? '') ?>"><?= esc($row->ds_rep_name ?? '') ?></td>
<td class="text-left pl-1 text-xs ds-col-tight" title="<?= esc($row->ds_name ?? '') ?>"><?= esc($row->ds_name ?? '') ?></td>
<td class="text-center text-xs ds-col-zip" title="<?= esc($zipList) ?>"><?= esc($zipList) ?></td>
<td class="text-left pl-1 text-xs ds-col-addr-list" title="<?= esc($addrCombinedList) ?>"><?= esc($addrCombinedList) ?></td>
<td class="text-left pl-1 text-xs"><?= esc($row->ds_biz_no ?? '') ?></td>
<td class="text-left pl-1 text-xs"><?= esc($row->ds_tel ?? '') ?></td>
<td class="text-center <?= $st === 2 ? 'text-pink-600 font-medium' : ($st === 3 ? 'text-orange-700' : '') ?>"><?= esc($stLabel) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="ds-detail-panel">
<div class="ds-panel-title shrink-0">지정판매소 정보</div>
<div class="ds-detail-inner" id="ds-detail-box">
<p id="ds-detail-placeholder" class="text-sm text-gray-500 py-6 text-center">위 목록에서 행을 선택하세요.</p>
<div id="ds-detail-fields" class="hidden">
<div class="ds-detail-info-wrap">
<table class="w-full data-table text-sm" id="ds-detail-info-table" aria-label="지정판매소 상세">
<thead>
<tr>
<th>판매소번호</th>
<th class="ds-col-tight-head">상호명</th>
<th>우편번호</th>
<th>사업자번호</th>
<th>일반전화</th>
<th class="ds-col-tight-head">대표자명</th>
<th>이메일</th>
<th>업태</th>
<th>업종</th>
<th>지정일자</th>
<th>지자체</th>
<th>도로명주소</th>
<th>지번주소</th>
<th>상세주소</th>
<th>개인전화</th>
<th>구·군</th>
<th>구역</th>
<th>가상계좌(은행)</th>
<th>계좌번호</th>
<th>종사업장번호</th>
<th>변경일자</th>
<th>영업상태</th>
<th>등록일시</th>
<th>변경사유</th>
<th class="no-print w-14">지도</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-left" data-ro="ds_shop_no">—</td>
<td class="text-left ds-col-tight-cell" data-ro="ds_name">—</td>
<td class="text-left" data-ro="ds_zip">—</td>
<td class="text-left" data-ro="ds_biz_no">—</td>
<td class="text-left" data-ro="ds_tel">—</td>
<td class="text-left ds-col-tight-cell" data-ro="ds_rep_name">—</td>
<td class="text-left" data-ro="ds_email">—</td>
<td class="text-left" data-ro="ds_biz_type">—</td>
<td class="text-left" data-ro="ds_biz_kind">—</td>
<td class="text-left" data-ro="ds_designated_at">—</td>
<td class="text-left" data-ro="lg_name">—</td>
<td class="text-left min-w-[10rem]"><span data-ro="ds_addr">—</span></td>
<td class="text-left" data-ro="ds_addr_jibun">—</td>
<td class="text-left" data-ro="ds_addr_detail">—</td>
<td class="text-left" data-ro="ds_rep_phone">—</td>
<td class="text-left" data-ro="gugun_name">—</td>
<td class="text-left" data-ro="ds_zone_code">—</td>
<td class="text-left" data-ro="ds_va_bank">—</td>
<td class="text-left" data-ro="ds_va_account">—</td>
<td class="text-left" data-ro="ds_branch_no">—</td>
<td class="text-left" data-ro="ds_state_changed_at">—</td>
<td class="text-left" data-ro="state_label">—</td>
<td class="text-left" data-ro="ds_regdate">—</td>
<td class="text-left min-w-[8rem]" data-ro="ds_change_reason">—</td>
<td class="text-center no-print">
<button type="button" class="border border-btn-print-border text-gray-700 px-2 py-0.5 rounded-sm text-xs hover:bg-gray-50" id="ds-ro-map-btn">지도</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<?php if (! $readOnly): ?>
<div class="ds-detail-actions no-print flex flex-wrap items-center gap-3 shrink-0">
<a id="ds-edit-link" href="#" class="text-blue-700 hover:underline text-sm font-medium pointer-events-none opacity-40">수정</a>
<form id="ds-delete-form" method="POST" action="" class="inline" onsubmit="return confirm('이 지정판매소를 삭제하시겠습니까?');">
<?= csrf_field() ?>
<button type="submit" id="ds-delete-btn" class="text-red-600 hover:underline text-sm pointer-events-none opacity-40" disabled>삭제</button>
</form>
</div>
<?php endif; ?>
</div>
</div>
<?php if (isset($pager)): ?>
<div class="mt-2 mb-2 mx-2 no-print"><?= $pager->links() ?></div>
<?php endif; ?>
<?= view('components/kakao_map_modal', ['kakaoJavascriptKey' => $kakaoJavascriptKey ?? '']) ?>
<script type="application/json" id="ds-detail-json"><?= $detailRowsJson ?? '[]' ?></script>
<script>
(function () {
var raw = document.getElementById('ds-detail-json');
var rows = [];
try {
rows = JSON.parse(raw.textContent || '[]');
} catch (e) {
rows = [];
}
var readOnly = <?= json_encode($readOnly) ?>;
var body = document.getElementById('ds-list-body');
var placeholder = document.getElementById('ds-detail-placeholder');
var fieldsWrap = document.getElementById('ds-detail-fields');
var infoTable = document.getElementById('ds-detail-info-table');
var editLink = readOnly ? null : document.getElementById('ds-edit-link');
var delForm = readOnly ? null : document.getElementById('ds-delete-form');
var delBtn = readOnly ? null : document.getElementById('ds-delete-btn');
// mgmt_url() 이 path 를 trim 하므로 'edit/33' 이 아니라 'edit33' 로 붙지 않게 슬래시를 넣음
var editBase = <?= json_encode(mgmt_url('designated-shops/edit')) ?>;
var delBase = <?= json_encode(mgmt_url('designated-shops/delete')) ?>;
function textVal(v) {
return (v === '' || v == null) ? '—' : String(v);
}
function buildKakaoMapSearchQuery(d) {
var road = String(d.ds_addr || '').trim();
var jibun = String(d.ds_addr_jibun || '').trim();
var detail = String(d.ds_addr_detail || '').trim();
var q = road || jibun;
if (detail) {
q = q ? (q + ' ' + detail) : detail;
}
return q;
}
function fillDetailInfoTable(d) {
if (!infoTable) return;
infoTable.querySelectorAll('[data-ro]').forEach(function (el) {
var k = el.getAttribute('data-ro');
var v = d[k];
if (k === 'ds_va_account') {
v = d.ds_va_account || d.ds_va_number || '';
}
el.textContent = textVal(v);
});
window.__dsDetailForMap = d;
}
function selectIndex(idx) {
if (!rows.length || idx < 0 || idx >= rows.length) return;
var d = rows[idx];
Array.prototype.forEach.call(body.querySelectorAll('tr.ds-list-row'), function (tr) {
tr.classList.remove('ds-row-selected');
});
var tr = body.querySelector('tr[data-row-index="' + idx + '"]');
if (tr) tr.classList.add('ds-row-selected');
placeholder.classList.add('hidden');
fieldsWrap.classList.remove('hidden');
fillDetailInfoTable(d);
if (!readOnly && editLink && delForm && delBtn) {
var id = d.ds_idx;
editLink.href = editBase + '/' + id;
editLink.classList.remove('pointer-events-none', 'opacity-40');
delForm.action = delBase + '/' + id;
delBtn.disabled = false;
delBtn.classList.remove('pointer-events-none', 'opacity-40');
}
}
if (body) {
body.addEventListener('click', function (e) {
var tr = e.target.closest('tr.ds-list-row');
if (!tr) return;
var idx = parseInt(tr.getAttribute('data-row-index'), 10);
if (!isNaN(idx)) selectIndex(idx);
});
body.addEventListener('keydown', function (e) {
if (e.key !== 'Enter' && e.key !== ' ') return;
var tr = e.target.closest('tr.ds-list-row');
if (!tr) return;
e.preventDefault();
var idx = parseInt(tr.getAttribute('data-row-index'), 10);
if (!isNaN(idx)) selectIndex(idx);
});
}
var mapBtnRo = document.getElementById('ds-ro-map-btn');
if (mapBtnRo) {
mapBtnRo.addEventListener('click', function (ev) {
ev.preventDefault();
var d = window.__dsDetailForMap;
if (!d) return;
var q = buildKakaoMapSearchQuery(d);
if (!q) {
window.alert('표시할 주소 정보가 없습니다.');
return;
}
if (typeof window.openDesignatedShopKakaoMap === 'function') {
window.openDesignatedShopKakaoMap(q);
} else {
window.open('https://map.kakao.com/link/search/' + encodeURIComponent(q), '_blank', 'noopener,noreferrer');
}
});
}
if (rows.length > 0) {
selectIndex(0);
} else if (!readOnly && editLink && delBtn) {
editLink.classList.add('pointer-events-none', 'opacity-40');
delBtn.disabled = true;
delBtn.classList.add('pointer-events-none', 'opacity-40');
}
})();
</script>
<!-- 인쇄용: 전체 테이블 -->
<div class="hidden print:block print:p-4">
<table class="w-full data-table text-xs">
<thead> <thead>
<tr> <tr>
<th class="w-16">번호</th> <th>번호</th>
<th>지자체</th> <th>지자체</th>
<th>판매소번호</th> <th>구·군</th>
<th>지정일</th>
<th>구역</th>
<th>대표자명</th>
<th>상호명</th> <th>상호명</th>
<th>대표자</th> <th>우편번호</th>
<th>주소</th>
<th>사업자번호</th> <th>사업자번호</th>
<th>전화</th>
<th>판매소번호</th>
<th>가상계좌</th> <th>가상계좌</th>
<th>상태</th> <th>상태</th>
<th>등록일</th> <th>등록일</th>
<th class="w-28">작업</th>
</tr> </tr>
</thead> </thead>
<tbody class="text-right"> <tbody>
<?php foreach ($list as $row): ?> <?php foreach ($list as $row): ?>
<?php
$snP = (string) ($row->ds_shop_no ?? '');
if (preg_match('/(\d{3})$/', $snP, $mP)) {
$shortNoP = $mP[1];
} elseif ($snP !== '' && strlen($snP) >= 3) {
$shortNoP = substr($snP, -3);
} else {
$shortNoP = $snP;
}
$daP = $row->ds_designated_at ?? null;
$daDispP = ($daP !== null && $daP !== '' && (string) $daP !== '0000-00-00') ? substr((string) $daP, 0, 10) : '';
$stP = (int) ($row->ds_state ?? 1);
$stLabP = $stP === 1 ? '정상' : ($stP === 2 ? '폐업' : '직권해지');
$zipP = trim((string) ($row->ds_zip ?? ''));
$roadP = trim((string) ($row->ds_addr ?? ''));
$jibP = trim((string) ($row->ds_addr_jibun ?? ''));
$addrP = $roadP !== '' ? $roadP : $jibP;
$detP = trim((string) ($row->ds_addr_detail ?? ''));
$addrCombinedP = trim($addrP . ' ' . $detP);
if ($addrCombinedP === '') {
$addrCombinedP = $addrP;
}
?>
<tr> <tr>
<td class="text-center"><?= esc($row->ds_idx) ?></td> <td class="text-center"><?= esc($shortNoP) ?></td>
<td class="text-left pl-2"><?= esc($lgMap[$row->ds_lg_idx] ?? '') ?></td> <td class="text-left"><?= esc($lgMap[$row->ds_lg_idx] ?? '') ?></td>
<td class="text-left pl-2"><?= esc($row->ds_shop_no) ?></td> <?php $gCodeP = (string) ($row->ds_gugun_code ?? ''); ?>
<td class="text-left pl-2"><?= esc($row->ds_name) ?></td> <td class="text-left"><?= esc((string) (($gugunNameMap[$gCodeP] ?? '') !== '' ? $gugunNameMap[$gCodeP] : $gCodeP)) ?></td>
<td class="text-left pl-2"><?= esc($row->ds_rep_name) ?></td> <td class="text-center"><?= esc($daDispP) ?></td>
<td class="text-left pl-2"><?= esc($row->ds_biz_no) ?></td> <td class="text-left"><?= esc($row->ds_zone_code ?? '') ?></td>
<td class="text-left pl-2"><?= esc($row->ds_va_number) ?></td> <td class="text-left"><?= esc($row->ds_rep_name ?? '') ?></td>
<td class="text-center"><?= (int) $row->ds_state === 1 ? '정상' : ((int) $row->ds_state === 2 ? '폐업' : '직권해지') ?></td> <td class="text-left"><?= esc($row->ds_name ?? '') ?></td>
<td class="text-left pl-2"><?= esc($row->ds_regdate ?? '') ?></td> <td class="text-left"><?= esc($zipP) ?></td>
<td class="text-center"> <td class="text-left"><?= esc($addrCombinedP) ?></td>
<a href="<?= base_url('admin/designated-shops/edit/' . (int) $row->ds_idx) ?>" class="text-blue-600 hover:underline text-sm">수정</a> <td class="text-left"><?= esc($row->ds_biz_no ?? '') ?></td>
<form action="<?= base_url('admin/designated-shops/delete/' . (int) $row->ds_idx) ?>" method="POST" class="inline ml-1" onsubmit="return confirm('이 지정판매소를 삭제하시겠습니까?');"> <td class="text-left"><?= esc($row->ds_tel ?? '') ?></td>
<?= csrf_field() ?> <td class="text-left"><?= esc($row->ds_shop_no) ?></td>
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button> <td class="text-left"><?= esc($row->ds_va_number) ?></td>
</form> <td class="text-center"><?= esc($stLabP) ?></td>
</td> <td class="text-left"><?= esc($row->ds_regdate ?? '') ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div> </div>
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>

View File

@@ -2,18 +2,18 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel"> <section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex flex-wrap items-center justify-between gap-y-2"> <div class="flex flex-wrap items-center justify-between gap-y-2">
<span class="text-sm font-bold text-gray-700">지정판매소 지도</span> <span class="text-sm font-bold text-gray-700">지정판매소 지도</span>
<a href="<?= base_url('admin/designated-shops') ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">목록으로</a> <a href="<?= mgmt_url('designated-shops') ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">목록으로</a>
</div> </div>
</section> </section>
<div id="kakao-map" class="w-full border border-gray-300 mt-2" style="height:600px;"></div> <div id="kakao-map" class="w-full border border-gray-300 mt-2" style="height:600px;"></div>
<div class="mt-2 text-sm text-gray-500">총 <?= count($shops) ?>개 판매소 표시</div> <div class="mt-2 text-sm text-gray-500">총 <?= count($shops) ?>개 판매소 표시</div>
<script src="//dapi.kakao.com/v2/maps/sdk.js?appkey=KAKAO_APP_KEY&libraries=services"></script> <script src="//dapi.kakao.com/v2/maps/sdk.js?appkey=<?= esc($kakaoJavascriptKey ?? '', 'attr') ?>&libraries=services"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
var mapContainer = document.getElementById('kakao-map'); var mapContainer = document.getElementById('kakao-map');
if (typeof kakao === 'undefined' || typeof kakao.maps === 'undefined') { if (typeof kakao === 'undefined' || typeof kakao.maps === 'undefined') {
mapContainer.innerHTML = '<div class="flex items-center justify-center h-full text-gray-400">카카오맵 API 키를 설정해 주세요.</div>'; mapContainer.innerHTML = '<div class="flex items-center justify-center h-full text-gray-500 text-sm px-4 text-center">카카오맵을 불러올 수 없습니다. Kakao Developers → 제품 설정에서 「Kakao Map」을 켜고, 플랫폼(Web)에 이 사이트 URL을 등록했는지 확인하세요.</div>';
return; return;
} }

View File

@@ -1,80 +1,387 @@
<?= view('components/print_header', ['printTitle' => '지정판매소 현황']) ?> <?php
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel"> $ry = (int) ($reportYear ?? (int) date('Y'));
$exportUrl = mgmt_url('designated-shops/status/export') . '?' . http_build_query([
'year' => $ry,
]);
$fixedGugunLabel = trim((string) ($fixedGugunLabel ?? ''));
$regionColLabel = '군·구';
$sumCurrForPct = (int) ($districtTotal->curr_end ?? 0);
?>
<style>
.ds-status-x-scroll {
overflow-x: auto;
overflow-y: visible;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
max-width: 100%;
}
@media print {
.ds-status-x-scroll { overflow: visible !important; border: none; }
}
.ds-status-x-scroll .ds-status-table {
width: max-content;
min-width: 100%;
border-collapse: collapse;
}
.ds-status-x-scroll .ds-status-table th,
.ds-status-x-scroll .ds-status-table td {
white-space: nowrap;
padding: 6px 10px;
font-size: 12px;
}
.ds-status-x-scroll .ds-status-table thead th {
background: #e9ecef;
border: 1px solid #ccc;
}
.ds-status-x-scroll .ds-status-table tbody td {
border: 1px solid #ccc;
}
.ds-status-x-scroll th.sticky-num,
.ds-status-x-scroll td.sticky-num {
position: sticky;
left: 0;
z-index: 3;
min-width: 3rem;
max-width: 3rem;
width: 3rem;
box-sizing: border-box;
background: #e9ecef;
border-right: 1px solid #bbb;
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.06);
}
.ds-status-x-scroll td.sticky-num {
background: #fff;
text-align: center;
}
.ds-status-x-scroll tr.sum-row td.sticky-num {
background: #f3f4f6;
}
.ds-status-x-scroll th.sticky-region,
.ds-status-x-scroll td.sticky-region {
position: sticky;
left: 3rem;
z-index: 2;
background: #e9ecef;
border-right: 1px solid #bbb;
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.06);
max-width: 16rem;
text-align: left;
}
.ds-status-x-scroll td.sticky-region {
background: #fff;
overflow: hidden;
text-overflow: ellipsis;
}
.ds-status-x-scroll tr.sum-row td.sticky-region {
background: #f3f4f6;
}
.ds-help {
position: relative;
display: inline-flex;
align-items: center;
gap: 4px;
}
.ds-help-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1rem;
height: 1rem;
padding: 0 2px;
border: 1px solid #64748b;
border-radius: 999px;
background: #fef9c3;
color: #111827;
font-size: 11px;
font-weight: 700;
line-height: 1;
font-family: Arial, sans-serif;
cursor: help;
user-select: none;
}
.ds-floating-tip {
position: fixed;
left: 0;
top: 0;
display: none;
max-width: min(22rem, calc(100vw - 16px));
padding: 6px 8px;
border: 1px solid #8fa0b3;
border-radius: 4px;
background: #1f2937;
color: #fff;
font-size: 11px;
font-weight: 500;
line-height: 1.35;
text-align: left;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
z-index: 9999;
pointer-events: none;
}
</style>
<?= view('components/print_header', ['printTitle' => '지정판매소 신규·취소 현황 (' . $ry . '년)']) ?>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel no-print">
<div class="flex flex-wrap items-center justify-between gap-y-2"> <div class="flex flex-wrap items-center justify-between gap-y-2">
<span class="text-sm font-bold text-gray-700">지정판매소 현황 (신규/취소)</span> <span class="text-sm font-bold text-gray-700">지정판매소 현황 (신규/취소)</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button> <a href="<?= esc($exportUrl) ?>" class="border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
<a href="<?= base_url('admin/designated-shops') ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">목록으로</a> <button type="button" onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= mgmt_url('designated-shops') ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">목록으로</a>
</div> </div>
</div> </div>
</section> </section>
<!-- 전체 현황 요약 --> <section class="p-2 bg-white border-b border-gray-200 no-print">
<div class="flex gap-4 mt-2 mb-2"> <form method="get" action="<?= mgmt_url('designated-shops/status') ?>" class="flex flex-wrap items-end gap-3">
<div class="border border-gray-300 p-3 flex-1 text-center"> <div>
<div class="text-sm text-gray-500">활성 판매소</div> <label class="block text-xs text-gray-600 mb-0.5">연도</label>
<div class="text-2xl font-bold text-green-600"><?= number_format($totalActive) ?></div> <select name="year" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[6rem]">
</div> <?php foreach (($yearChoices ?? []) as $y): ?>
<div class="border border-gray-300 p-3 flex-1 text-center"> <option value="<?= (int) $y ?>" <?= $ry === (int) $y ? 'selected' : '' ?>><?= (int) $y ?>년</option>
<div class="text-sm text-gray-500">비활성/취소 판매소</div> <?php endforeach; ?>
<div class="text-2xl font-bold text-red-600"><?= number_format($totalInactive) ?></div> </select>
</div> </div>
<div class="border border-gray-300 p-3 flex-1 text-center"> <div class="min-w-[12rem]">
<div class="text-sm text-gray-500">전체</div> <label class="block text-xs text-gray-600 mb-0.5">군·구</label>
<div class="text-2xl font-bold text-gray-700"><?= number_format($totalActive + $totalInactive) ?></div> <div class="border border-gray-300 rounded px-3 py-1 text-sm bg-gray-50 text-gray-800 font-medium pointer-events-none select-none">
</div> <?= esc($fixedGugunLabel !== '' ? $fixedGugunLabel : '현재 지자체 기준') ?>
</div>
</div>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
</form>
<p class="text-xs text-gray-500 mt-2">
종전·현행은 각각 <?= $ry - 1 ?>-12-31·<?= $ry ?>-12-31 기준 정상(지정일 이전·비정상 전환일 이후) 건수이며, 지정·취소는 <?= $ry ?>년 달력연도 기준입니다. 군·구는 현재 로그인 사용자의 지자체 기준으로 고정 표시됩니다.
</p>
</section>
<!-- 인쇄 시에도 보이는 본표 -->
<div class="mx-2 mt-2 mb-2 ds-status-x-scroll">
<table class="ds-status-table data-table">
<thead>
<tr>
<th class="sticky-num text-center w-12">순번</th>
<th class="sticky-region"><?= esc($regionColLabel) ?></th>
<th class="text-left">
<span class="ds-help">구코드 <span class="ds-help-badge" tabindex="0" data-tip="지정판매소에 저장된 구·군 코드 값">?</span></span>
</th>
<th class="text-right">
<span class="ds-help">종전 <span class="ds-help-badge" tabindex="0" data-tip="전년도 12월 31일 기준 정상 상태 판매소 수">?</span></span>(전년도말)
</th>
<th class="text-right">
<span class="ds-help">지정 <span class="ds-help-badge" tabindex="0" data-tip="<?= esc($ry) ?>년 내 지정일이 속한 신규 지정 건수">?</span></span>(<?= $ry ?>년)
</th>
<th class="text-right">
<span class="ds-help">취소 <span class="ds-help-badge" tabindex="0" data-tip="<?= esc($ry) ?>년 내 폐업/해지 전환일이 속한 건수">?</span></span>(<?= $ry ?>년)
</th>
<th class="text-right">
<span class="ds-help">현행 <span class="ds-help-badge" tabindex="0" data-tip="조회년도 12월 31일 기준 정상 상태 판매소 수">?</span></span>(금년도말)
</th>
<th class="text-right">
<span class="ds-help">증감 <span class="ds-help-badge" tabindex="0" data-tip="현행에서 종전을 뺀 값 (현행−종전)">?</span></span>
<br/><span class="font-normal text-xs">(현행−종전)</span>
</th>
<th class="text-right">
<span class="ds-help">지정−취소 <span class="ds-help-badge" tabindex="0" data-tip="<?= esc($ry) ?>년 지정 건수에서 취소 건수를 뺀 값">?</span></span>
<br/><span class="font-normal text-xs">(<?= $ry ?>년)</span>
</th>
<th class="text-right">
<span class="ds-help">현행비중 <span class="ds-help-badge" tabindex="0" data-tip="전체 현행 합계 대비 해당 행 현행 건수의 비율(%)">?</span></span>
<br/><span class="font-normal text-xs">(%)</span>
</th>
<th class="text-right">
<span class="ds-help">전년대비 <span class="ds-help-badge ds-help-right" tabindex="0" data-tip="((현행−종전) / 종전) × 100, 종전이 0이면 표시 안함">?</span></span>
<br/><span class="font-normal text-xs">증감률(%)</span>
</th>
</tr>
</thead>
<tbody class="text-right">
<?php $rowNo = 0; ?>
<?php foreach (($districtRows ?? []) as $row): ?>
<?php
$rowNo++;
$curr = (int) $row->curr_end;
$prev = (int) $row->prev_end;
$pctShare = $sumCurrForPct > 0 ? round(($curr / $sumCurrForPct) * 100, 1) : 0.0;
$yoyPct = $prev > 0 ? round((($curr - $prev) / $prev) * 100, 1) : null;
?>
<tr>
<td class="sticky-num"><?= $rowNo ?></td>
<td class="sticky-region" title="<?= esc($row->region_label) ?>"><?= esc($row->region_label) ?></td>
<td class="text-left text-xs"><?= esc((string) ($row->gugun_code ?? '')) ?></td>
<td><?= number_format($prev) ?></td>
<td><?= number_format((int) $row->designated_y) ?></td>
<td><?= number_format((int) $row->cancelled_y) ?></td>
<td><?= number_format($curr) ?></td>
<td><?= number_format((int) ($row->delta_curr_prev ?? 0)) ?></td>
<td><?= number_format((int) ($row->delta_des_cancel ?? 0)) ?></td>
<td><?= $pctShare ?></td>
<td><?= $yoyPct !== null ? $yoyPct : '—' ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($districtRows)): ?>
<tr><td colspan="11" class="text-center text-gray-400 py-6">조건에 맞는 데이터가 없습니다.</td></tr>
<?php endif; ?>
<?php if (! empty($districtRows) && isset($districtTotal)): ?>
<tr class="font-bold bg-gray-50 sum-row">
<td class="sticky-num">—</td>
<td class="sticky-region"><?= esc($districtTotal->region_label) ?></td>
<td class="text-left">—</td>
<td><?= number_format((int) $districtTotal->prev_end) ?></td>
<td><?= number_format((int) $districtTotal->designated_y) ?></td>
<td><?= number_format((int) $districtTotal->cancelled_y) ?></td>
<td><?= number_format((int) $districtTotal->curr_end) ?></td>
<td><?= number_format((int) ($districtTotal->delta_curr_prev ?? 0)) ?></td>
<td><?= number_format((int) ($districtTotal->delta_des_cancel ?? 0)) ?></td>
<td>100</td>
<td>
<?php
$tPrev = (int) $districtTotal->prev_end;
$tCurr = (int) $districtTotal->curr_end;
echo $tPrev > 0 ? round((($tCurr - $tPrev) / $tPrev) * 100, 1) : '—';
?>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2"> <?php $zoneRows = $zoneSummaryRows ?? []; ?>
<!-- 연도별 신규등록 --> <section class="mx-2 mb-3 no-print">
<div> <div class="text-xs font-semibold text-gray-700 mb-1">동별 현행 요약 (<?= esc($fixedGugunLabel ?? '군·구') ?>)</div>
<h3 class="text-sm font-bold text-gray-700 mb-1">연도별 신규등록 건수</h3> <?php if (! empty($zoneRows)): ?>
<div class="border border-gray-300 overflow-auto"> <div class="flex flex-wrap gap-1 mb-2">
<table class="w-full data-table"> <?php foreach ($zoneRows as $z): ?>
<thead> <span class="inline-flex items-center px-2 py-0.5 text-xs rounded border border-gray-300 bg-gray-50 text-gray-700">
<tr> <?= esc((string) $z->zone_label) ?> <?= number_format((int) $z->curr_end) ?>
<th>연도</th> </span>
<th>신규등록 건수</th> <?php endforeach; ?>
</tr>
</thead>
<tbody class="text-right">
<?php foreach ($newByYear as $row): ?>
<tr>
<td class="text-center"><?= esc($row->yr) ?>년</td>
<td><?= number_format((int) $row->cnt) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($newByYear)): ?>
<tr><td colspan="2" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div> </div>
<div class="border border-gray-300 bg-white overflow-auto max-h-56">
<table class="w-full data-table text-xs">
<thead>
<tr>
<th class="text-left">동</th>
<th class="text-right">종전</th>
<th class="text-right">지정</th>
<th class="text-right">취소</th>
<th class="text-right">현행</th>
<th class="text-right">증감</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach ($zoneRows as $z): ?>
<tr>
<td class="text-left"><?= esc((string) $z->zone_label) ?></td>
<td><?= number_format((int) $z->prev_end) ?></td>
<td><?= number_format((int) $z->designated_y) ?></td>
<td><?= number_format((int) $z->cancelled_y) ?></td>
<td><?= number_format((int) $z->curr_end) ?></td>
<td><?= number_format((int) $z->delta_curr_prev) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<p class="text-xs text-gray-500">동별 집계 데이터가 없습니다.</p>
<?php endif; ?>
</section>
<!-- 연도별 취소/비활성 --> <div id="ds-floating-tip" class="ds-floating-tip no-print" aria-hidden="true"></div>
<div>
<h3 class="text-sm font-bold text-gray-700 mb-1">연도별 취소/비활성 건수</h3> <script>
<div class="border border-gray-300 overflow-auto"> (function () {
<table class="w-full data-table"> var tipEl = document.getElementById('ds-floating-tip');
<thead> if (!tipEl) return;
<tr> var badges = Array.prototype.slice.call(document.querySelectorAll('.ds-help-badge'));
<th>연도</th> if (!badges.length) return;
<th>취소/비활성 건수</th>
</tr> function placeTip(target) {
</thead> var text = String(target.getAttribute('data-tip') || '').trim();
<tbody class="text-right"> if (!text) return;
<?php foreach ($cancelByYear as $row): ?> tipEl.textContent = text;
<tr> tipEl.style.display = 'block';
<td class="text-center"><?= esc($row->yr) ?>년</td> tipEl.setAttribute('aria-hidden', 'false');
<td><?= number_format((int) $row->cnt) ?></td>
</tr> var rect = target.getBoundingClientRect();
<?php endforeach; ?> var tipRect = tipEl.getBoundingClientRect();
<?php if (empty($cancelByYear)): ?> var vw = window.innerWidth || document.documentElement.clientWidth;
<tr><td colspan="2" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr> var vh = window.innerHeight || document.documentElement.clientHeight;
<?php endif; ?> var gap = 8;
</tbody>
</table> var left = rect.left + (rect.width / 2) - (tipRect.width / 2);
var top = rect.bottom + gap;
if (left < gap) left = gap;
if (left + tipRect.width > vw - gap) left = vw - gap - tipRect.width;
if (top + tipRect.height > vh - gap) top = rect.top - gap - tipRect.height;
if (top < gap) top = gap;
tipEl.style.left = Math.round(left) + 'px';
tipEl.style.top = Math.round(top) + 'px';
}
function hideTip() {
tipEl.style.display = 'none';
tipEl.setAttribute('aria-hidden', 'true');
tipEl.textContent = '';
}
badges.forEach(function (badge) {
badge.addEventListener('mouseenter', function () { placeTip(badge); });
badge.addEventListener('focus', function () { placeTip(badge); });
badge.addEventListener('mouseleave', hideTip);
badge.addEventListener('blur', hideTip);
});
window.addEventListener('scroll', hideTip, true);
window.addEventListener('resize', hideTip);
})();
</script>
<details class="mx-2 mb-4 no-print text-sm">
<summary class="cursor-pointer text-gray-600 hover:text-gray-800">연도별 요약 (참고)</summary>
<div class="flex gap-4 mt-2">
<div class="border border-gray-300 p-2 flex-1">
<div class="text-xs font-bold text-gray-700 mb-1">활성 / 비활성 / 전체</div>
<div class="text-sm">활성 <?= number_format((int) ($totalActive ?? 0)) ?> · 비활성 <?= number_format((int) ($totalInactive ?? 0)) ?> · 합 <?= number_format((int) ($totalActive ?? 0) + (int) ($totalInactive ?? 0)) ?></div>
</div> </div>
</div> </div>
</div> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
<div>
<h3 class="text-xs font-bold text-gray-700 mb-1">연도별 신규등록 (지정일)</h3>
<div class="border border-gray-300 overflow-auto max-h-48">
<table class="w-full data-table text-xs">
<thead><tr><th>연도</th><th>건수</th></tr></thead>
<tbody class="text-right">
<?php foreach (($newByYear ?? []) as $row): ?>
<tr><td class="text-center"><?= esc($row->yr) ?>년</td><td><?= number_format((int) $row->cnt) ?></td></tr>
<?php endforeach; ?>
<?php if (empty($newByYear)): ?>
<tr><td colspan="2" class="text-center text-gray-400 py-2">없음</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div>
<h3 class="text-xs font-bold text-gray-700 mb-1">연도별 취소/비활성 (등록일 기준)</h3>
<div class="border border-gray-300 overflow-auto max-h-48">
<table class="w-full data-table text-xs">
<thead><tr><th>연도</th><th>건수</th></tr></thead>
<tbody class="text-right">
<?php foreach (($cancelByYear ?? []) as $row): ?>
<tr><td class="text-center"><?= esc($row->yr) ?>년</td><td><?= number_format((int) $row->cnt) ?></td></tr>
<?php endforeach; ?>
<?php if (empty($cancelByYear)): ?>
<tr><td colspan="2" class="text-center text-gray-400 py-2">없음</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</details>

View File

@@ -2,7 +2,7 @@
<span class="text-sm font-bold text-gray-700">대상자 등록</span> <span class="text-sm font-bold text-gray-700">대상자 등록</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/free-recipients/store') ?>" method="POST" class="space-y-4"> <form action="<?= mgmt_url('free-recipients/store') ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -56,7 +56,7 @@
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
<a href="<?= base_url('admin/free-recipients') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= mgmt_url('free-recipients') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -2,7 +2,7 @@
<span class="text-sm font-bold text-gray-700">대상자 수정</span> <span class="text-sm font-bold text-gray-700">대상자 수정</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/free-recipients/update/' . (int) $item->fr_idx) ?>" method="POST" class="space-y-4"> <form action="<?= mgmt_url('free-recipients/update/' . (int) $item->fr_idx) ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -64,7 +64,7 @@
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button>
<a href="<?= base_url('admin/free-recipients') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= mgmt_url('free-recipients') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -4,7 +4,7 @@
<span class="text-sm font-bold text-gray-700">무료용 대상자 관리</span> <span class="text-sm font-bold text-gray-700">무료용 대상자 관리</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button> <button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= base_url('admin/free-recipients/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">대상자 등록</a> <a href="<?= mgmt_url('free-recipients/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">대상자 등록</a>
</div> </div>
</div> </div>
</section> </section>
@@ -13,11 +13,9 @@
<thead> <thead>
<tr> <tr>
<th class="w-16">번호</th> <th class="w-16">번호</th>
<th>구분</th>
<th>대상자명</th> <th>대상자명</th>
<th>연락처</th> <th>연락처</th>
<th>주소</th> <th>주소</th>
<th>동코드</th>
<th>비고</th> <th>비고</th>
<th>종료일</th> <th>종료일</th>
<th class="w-20">상태</th> <th class="w-20">상태</th>
@@ -28,17 +26,15 @@
<?php foreach ($list as $row): ?> <?php foreach ($list as $row): ?>
<tr> <tr>
<td class="text-center"><?= esc($row->fr_idx) ?></td> <td class="text-center"><?= esc($row->fr_idx) ?></td>
<td class="text-center"><?= esc($row->fr_type_code) ?></td>
<td class="text-left pl-2"><?= esc($row->fr_name) ?></td> <td class="text-left pl-2"><?= esc($row->fr_name) ?></td>
<td class="text-center"><?= esc($row->fr_phone) ?></td> <td class="text-center"><?= esc($row->fr_phone) ?></td>
<td class="text-left pl-2"><?= esc($row->fr_addr) ?></td> <td class="text-left pl-2"><?= esc($row->fr_addr) ?></td>
<td class="text-center"><?= esc($row->fr_dong_code) ?></td>
<td class="text-left pl-2"><?= esc($row->fr_note) ?></td> <td class="text-left pl-2"><?= esc($row->fr_note) ?></td>
<td class="text-center"><?= esc($row->fr_end_date) ?></td> <td class="text-center"><?= esc($row->fr_end_date) ?></td>
<td class="text-center"><?= (int) $row->fr_state === 1 ? '사용' : '미사용' ?></td> <td class="text-center"><?= (int) $row->fr_state === 1 ? '사용' : '미사용' ?></td>
<td class="text-center"> <td class="text-center">
<a href="<?= base_url('admin/free-recipients/edit/' . (int) $row->fr_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">수정</a> <a href="<?= mgmt_url('free-recipients/edit/' . (int) $row->fr_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">수정</a>
<form action="<?= base_url('admin/free-recipients/delete/' . (int) $row->fr_idx) ?>" method="POST" class="inline" onsubmit="return confirm('삭제하시겠습니까?');"> <form action="<?= mgmt_url('free-recipients/delete/' . (int) $row->fr_idx) ?>" method="POST" class="inline" onsubmit="return confirm('삭제하시겠습니까?');">
<?= csrf_field() ?> <?= csrf_field() ?>
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button> <button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
</form> </form>
@@ -46,7 +42,12 @@
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (empty($list)): ?> <?php if (empty($list)): ?>
<tr><td colspan="10" class="text-center text-gray-400 py-4">등록된 데이터가 없습니다.</td></tr> <tr>
<td colspan="8" class="text-center text-gray-500 py-4 text-sm space-y-1">
<p>등록된 데이터가 없습니다.</p>
<p class="text-gray-400">다른 지자체를 선택 중이면 해당 지자체 기준으로만 조회됩니다. Super Admin 은 상단에서 작업 지자체를 바꿔 보세요.</p>
</td>
</tr>
<?php endif; ?> <?php endif; ?>
</tbody> </tbody>
</table> </table>

View File

@@ -12,15 +12,17 @@ if ($effectiveLgIdx) {
$lgRow = model(\App\Models\LocalGovernmentModel::class)->find($effectiveLgIdx); $lgRow = model(\App\Models\LocalGovernmentModel::class)->find($effectiveLgIdx);
$effectiveLgName = $lgRow ? $lgRow->lg_name : null; $effectiveLgName = $lgRow ? $lgRow->lg_name : null;
} }
$currentPath = trim((string) $uriObj->getPath(), '/'); $userNav = session_user_nav_display();
if (str_starts_with($currentPath, 'index.php/')) { $currentPath = current_nav_request_path();
$currentPath = substr($currentPath, strlen('index.php/'));
}
$adminNavTree = get_admin_nav_tree(); $adminNavTree = get_admin_nav_tree();
$isActive = static function (string $path) use ($uri, $seg3, $currentPath, $adminNavTree) {
if (! empty($adminNavTree)) { /** DB 링크(mm_link)만 사용. 짧게 적은 항목(menus 등)은 실제 URI(admin/menus)와 맞춰 후보 비교 */
return $currentPath === trim($path, '/'); $adminNavItemIsCurrent = static function (?string $mmLink) use ($currentPath): bool {
} return menu_link_matches_request($mmLink, $currentPath, []);
};
/** 메뉴가 DB에서 안 쓰일 때만(폴백 상단바) 세그먼트 기반 활성 */
$isActive = static function (string $path) use ($uri, $seg3) {
if ($path === 'admin' || $path === '') return $uri === ''; if ($path === 'admin' || $path === '') return $uri === '';
if ($path === 'users') return $uri === 'users'; if ($path === 'users') return $uri === 'users';
if ($path === 'login-history') return $uri === 'access' && $seg3 === 'login-history'; if ($path === 'login-history') return $uri === 'access' && $seg3 === 'login-history';
@@ -38,7 +40,7 @@ $isActive = static function (string $path) use ($uri, $seg3, $currentPath, $admi
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/> <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title><?= esc($title ?? '관리자') ?> - 쓰레기봉투 물류시스템</title> <title><?= esc($title ?? '관리자') ?> - 종량제 시스템</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script> <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"/> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&amp;display=swap" rel="stylesheet"/>
<script> <script>
@@ -46,7 +48,7 @@ tailwind.config = {
theme: { theme: {
extend: { extend: {
fontFamily: { sans: ['"Malgun Gothic"', '"Noto Sans KR"', 'sans-serif'] }, fontFamily: { sans: ['"Malgun Gothic"', '"Noto Sans KR"', 'sans-serif'] },
colors: { colors: {
'system-header': '#ffffff', 'system-header': '#ffffff',
'title-bar': '#2c3e50', 'title-bar': '#2c3e50',
'control-panel': '#f8f9fa', 'control-panel': '#f8f9fa',
@@ -81,61 +83,73 @@ body { overflow: hidden; }
</style> </style>
</head> </head>
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased select-none"> <body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased select-none">
<header class="bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0 z-20"> <header class="relative bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0 z-50">
<div class="flex items-center gap-4"> <div class="flex items-center gap-2 shrink-0">
<div class="flex items-center gap-2"> <?= view('components/header_brand', ['href' => base_url('admin')]) ?>
<div class="w-6 h-6 flex items-center justify-center shrink-0"> </div>
<svg class="h-5 w-5" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"> <nav class="hidden md:flex gap-5 text-sm font-medium text-gray-600">
<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"/> <?php if (! empty($adminNavTree)): ?>
</svg> <?php foreach ($adminNavTree as $navItem): ?>
</div> <?php
<a href="<?= base_url('admin') ?>" class="text-base font-semibold text-gray-800 tracking-tight hover:text-blue-600">쓰레기봉투 물류시스템</a> $hasChildren = ! empty($navItem->children);
</div> $parentLink = menu_link_preferred_href_path($navItem->mm_link ?? null, $currentPath);
<nav class="hidden md:flex gap-5 text-sm font-medium text-gray-600"> $activeChild = $hasChildren ? menu_active_child_for_parent($navItem, $currentPath, []) : null;
<?php if (! empty($adminNavTree)): ?> $parentIsCurrent = $adminNavItemIsCurrent($navItem->mm_link ?? null);
<?php foreach ($adminNavTree as $navItem): ?> if (! $parentIsCurrent && $activeChild !== null) {
<?php $hasChildren = ! empty($navItem->children); ?> $parentIsCurrent = true;
<div class="relative group"> }
<a class="<?= $isActive($navItem->mm_link) ? '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) ?>"> <div class="relative group">
<?= esc($navItem->mm_name) ?> <a class="<?= $parentIsCurrent ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>"
</a> href="<?= $parentLink !== '' ? base_url($parentLink) : '#' ?>">
<?php if ($hasChildren): ?> <?= esc($navItem->mm_name) ?>
<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"> </a>
<?php if ($hasChildren): ?>
<?php /* 사이트 메뉴와 동일: 호버 끊김 방지 pt-1, 키보드 포커스, z-index */ ?>
<div class="absolute left-0 top-full z-50 hidden pt-1 min-w-[12rem] group-hover:block group-focus-within:block">
<div class="bg-white border border-gray-200 rounded shadow-lg py-1">
<?php foreach ($navItem->children as $child): ?> <?php foreach ($navItem->children as $child): ?>
<a href="<?= base_url($child->mm_link) ?>" <?php
class="block px-3 py-1.5 text-sm text-gray-700 hover:bg-blue-50 whitespace-nowrap"> $childLink = menu_link_preferred_href_path($child->mm_link ?? null, $currentPath);
$childIsCurrent = $activeChild !== null
&& (int) ($child->mm_idx ?? 0) === (int) ($activeChild->mm_idx ?? -1);
?>
<?php if ($childLink !== ''): ?>
<a href="<?= base_url($childLink) ?>"
class="block px-3 py-1.5 text-sm hover:bg-blue-50 whitespace-nowrap <?= $childIsCurrent ? 'text-blue-700 font-semibold bg-blue-50' : 'text-gray-700' ?>">
<?= esc($child->mm_name) ?> <?= esc($child->mm_name) ?>
</a> </a>
<?php else: ?>
<span class="block px-3 py-1.5 text-sm text-gray-400 cursor-default whitespace-nowrap" title="메뉴 관리에서 링크를 설정해 주세요">
<?= esc($child->mm_name) ?>
</span>
<?php endif; ?>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php endif; ?>
</div> </div>
<?php endforeach; ?> <?php endif; ?>
<?php else: ?> </div>
<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('admin') ?>">대시보드</a> <?php endforeach; ?>
<a class="<?= $isActive('users') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/users') ?>">회원 관리</a> <?php else: ?>
<a class="<?= $isActive('login-history') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/access/login-history') ?>">로그인 이력</a> <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('admin') ?>">대시보드</a>
<a class="<?= $isActive('approvals') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/access/approvals') ?>">승인 대기</a> <a class="<?= $isActive('users') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/users') ?>">회원 관리</a>
<a class="<?= $isActive('roles') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/roles') ?>">역할</a> <a class="<?= $isActive('login-history') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/access/login-history') ?>">로그인 이력</a>
<a class="<?= $isActive('menus') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/menus') ?>">메뉴</a> <a class="<?= $isActive('approvals') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/access/approvals') ?>">승인 대기</a>
<?php if ($isSuperAdmin): ?> <a class="<?= $isActive('roles') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/roles') ?>">역할</a>
<a class="<?= $isActive('select-local-government') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/select-local-government') ?>">지자체 전환</a> <a class="<?= $isActive('menus') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/menus') ?>">메뉴</a>
<a class="<?= $isActive('local-governments') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/local-governments') ?>">지자체</a> <?php if ($isSuperAdmin): ?>
<?php endif; ?> <a class="<?= $isActive('select-local-government') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/select-local-government') ?>">지자체 전환</a>
<a class="<?= $isActive('designated-shops') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/designated-shops') ?>">지정판매소</a> <a class="<?= $isActive('local-governments') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/local-governments') ?>">지자체</a>
<?php endif; ?>
</nav>
</div>
<div class="flex items-center gap-3">
<?php if ($effectiveLgName !== null): ?>
<span class="text-sm text-gray-600" title="현재 작업 지자체"><?= esc($effectiveLgName) ?></span>
<?php endif; ?> <?php endif; ?>
<a href="<?= base_url('/') ?>" class="text-gray-500 hover:text-blue-600 text-sm">사이트</a> <a class="<?= $isActive('designated-shops') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('bag/designated-shops') ?>">지정판매소</a>
<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="로그아웃"> <?php endif; ?>
<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> 종료 </nav>
</a> <?= view('components/header_user_tools', [
</div> 'userNav' => $userNav,
'effectiveLgName' => $effectiveLgName,
'showSiteLink' => true,
'showAdminLink' => false,
]) ?>
</header> </header>
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0"> <div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
<?= esc($title ?? '관리자') ?> <?= esc($title ?? '관리자') ?>
@@ -155,7 +169,7 @@ body { overflow: hidden; }
<?= $content ?> <?= $content ?>
</main> </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"> <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>종량제 시스템 관리자</span>
<span><?= date('Y.m.d (D) g:i:sA') ?></span> <span><?= date('Y.m.d (D) g:i:sA') ?></span>
</footer> </footer>
</body> </body>

View File

@@ -2,7 +2,7 @@
<span class="text-sm font-bold text-gray-700">담당자 등록</span> <span class="text-sm font-bold text-gray-700">담당자 등록</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/managers/store') ?>" method="POST" class="space-y-4"> <form action="<?= mgmt_url('managers/store') ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -11,28 +11,18 @@
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">소속</label> <label class="block text-sm font-bold text-gray-700 w-28">담당자 구분 <span class="text-red-500">*</span></label>
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_dept_code"> <select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_category" required>
<option value="">선택</option> <option value="">선택</option>
<?php foreach ($deptCodes as $cd): ?> <?php foreach (($categories ?? []) as $key => $label): ?>
<option value="<?= esc($cd->cd_code) ?>" <?= old('mg_dept_code') === $cd->cd_code ? 'selected' : '' ?>> <option value="<?= esc($key) ?>" <?= old('mg_category') === $key ? 'selected' : '' ?>>
<?= esc($cd->cd_name) ?> <?= esc($label) ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <input type="hidden" name="mg_position_code" value="<?= esc(old('mg_position_code', '')) ?>"/>
<label class="block text-sm font-bold text-gray-700 w-28">직위</label>
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_position_code">
<option value="">선택</option>
<?php foreach ($positionCodes as $cd): ?>
<option value="<?= esc($cd->cd_code) ?>" <?= old('mg_position_code') === $cd->cd_code ? 'selected' : '' ?>>
<?= esc($cd->cd_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">전화</label> <label class="block text-sm font-bold text-gray-700 w-28">전화</label>
@@ -51,7 +41,7 @@
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
<a href="<?= base_url('admin/managers') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= mgmt_url('managers') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -2,7 +2,7 @@
<span class="text-sm font-bold text-gray-700">담당자 수정</span> <span class="text-sm font-bold text-gray-700">담당자 수정</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/managers/update/' . (int) $item->mg_idx) ?>" method="POST" class="space-y-4"> <form action="<?= mgmt_url('managers/update/' . (int) $item->mg_idx) ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -11,28 +11,18 @@
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">소속</label> <label class="block text-sm font-bold text-gray-700 w-28">담당자 구분 <span class="text-red-500">*</span></label>
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_dept_code"> <select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_category" required>
<option value="">선택</option> <option value="">선택</option>
<?php foreach ($deptCodes as $cd): ?> <?php foreach (($categories ?? []) as $key => $label): ?>
<option value="<?= esc($cd->cd_code) ?>" <?= old('mg_dept_code', $item->mg_dept_code) === $cd->cd_code ? 'selected' : '' ?>> <option value="<?= esc($key) ?>" <?= old('mg_category', (string) ($item->mg_dept_code ?? '')) === $key ? 'selected' : '' ?>>
<?= esc($cd->cd_name) ?> <?= esc($label) ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <input type="hidden" name="mg_position_code" value="<?= esc(old('mg_position_code', $item->mg_position_code)) ?>"/>
<label class="block text-sm font-bold text-gray-700 w-28">직위</label>
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_position_code">
<option value="">선택</option>
<?php foreach ($positionCodes as $cd): ?>
<option value="<?= esc($cd->cd_code) ?>" <?= old('mg_position_code', $item->mg_position_code) === $cd->cd_code ? 'selected' : '' ?>>
<?= esc($cd->cd_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">전화</label> <label class="block text-sm font-bold text-gray-700 w-28">전화</label>
@@ -59,7 +49,7 @@
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button>
<a href="<?= base_url('admin/managers') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= mgmt_url('managers') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -4,18 +4,30 @@
<span class="text-sm font-bold text-gray-700">담당자 관리</span> <span class="text-sm font-bold text-gray-700">담당자 관리</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button> <button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= base_url('admin/managers/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">담당자 등록</a> <a href="<?= mgmt_url('managers/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">담당자 등록</a>
</div> </div>
</div> </div>
</section> </section>
<section class="p-2 bg-white border-b border-gray-200 no-print">
<form method="GET" action="<?= mgmt_url('managers') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">카테고리</label>
<select name="category" class="border border-gray-300 rounded px-2 py-1 text-sm w-44">
<option value="">전 체</option>
<?php foreach (($categories ?? []) as $key => $label): ?>
<option value="<?= esc($key) ?>" <?= ($category ?? '') === $key ? 'selected' : '' ?>><?= esc($label) ?></option>
<?php endforeach; ?>
</select>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
<a href="<?= mgmt_url('managers') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
</form>
</section>
<div class="border border-gray-300 overflow-auto mt-2"> <div class="border border-gray-300 overflow-auto mt-2">
<table class="w-full data-table"> <table class="w-full data-table">
<thead> <thead>
<tr> <tr>
<th class="w-16">번호</th> <th class="w-16">번호</th>
<th>담당자명</th> <th>담당자명</th>
<th>소속</th> <th>카테고리</th>
<th>직위</th>
<th>전화</th> <th>전화</th>
<th>휴대전화</th> <th>휴대전화</th>
<th>이메일</th> <th>이메일</th>
@@ -28,15 +40,20 @@
<tr> <tr>
<td class="text-center"><?= esc($row->mg_idx) ?></td> <td class="text-center"><?= esc($row->mg_idx) ?></td>
<td class="text-center"><?= esc($row->mg_name) ?></td> <td class="text-center"><?= esc($row->mg_name) ?></td>
<td class="text-center"><?= esc($row->mg_dept_code) ?></td> <td class="text-center">
<td class="text-center"><?= esc($row->mg_position_code) ?></td> <?php
$cat = (string) ($row->mg_dept_code ?? '');
$catLabel = $categories[$cat] ?? $cat;
echo esc($catLabel);
?>
</td>
<td class="text-center"><?= esc($row->mg_tel) ?></td> <td class="text-center"><?= esc($row->mg_tel) ?></td>
<td class="text-center"><?= esc($row->mg_phone) ?></td> <td class="text-center"><?= esc($row->mg_phone) ?></td>
<td class="text-center"><?= esc($row->mg_email) ?></td> <td class="text-center"><?= esc($row->mg_email) ?></td>
<td class="text-center"><?= (int) $row->mg_state === 1 ? '사용' : '미사용' ?></td> <td class="text-center"><?= (int) $row->mg_state === 1 ? '사용' : '미사용' ?></td>
<td class="text-center"> <td class="text-center">
<a href="<?= base_url('admin/managers/edit/' . (int) $row->mg_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">수정</a> <a href="<?= mgmt_url('managers/edit/' . (int) $row->mg_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">수정</a>
<form action="<?= base_url('admin/managers/delete/' . (int) $row->mg_idx) ?>" method="POST" class="inline" onsubmit="return confirm('삭제하시겠습니까?');"> <form action="<?= mgmt_url('managers/delete/' . (int) $row->mg_idx) ?>" method="POST" class="inline" onsubmit="return confirm('삭제하시겠습니까?');">
<?= csrf_field() ?> <?= csrf_field() ?>
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button> <button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
</form> </form>
@@ -44,7 +61,7 @@
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (empty($list)): ?> <?php if (empty($list)): ?>
<tr><td colspan="9" class="text-center text-gray-400 py-4">등록된 데이터가 없습니다.</td></tr> <tr><td colspan="8" class="text-center text-gray-400 py-4">등록된 데이터가 없습니다.</td></tr>
<?php endif; ?> <?php endif; ?>
</tbody> </tbody>
</table> </table>

View File

@@ -4,6 +4,29 @@ $list = $list ?? [];
$mtIdx = (int) ($mtIdx ?? 0); $mtIdx = (int) ($mtIdx ?? 0);
$mtCode = (string) ($mtCode ?? ''); $mtCode = (string) ($mtCode ?? '');
$levelNames = $levelNames ?? []; $levelNames = $levelNames ?? [];
$debugMode = (bool) ($debug_mode ?? false);
$debugInfo = is_array($debug_info ?? null) ? $debug_info : [];
helper('admin');
$adminMenusNavPath = current_nav_request_path();
/**
* 메뉴 관리 목록용: 저장된 mm_link → 실제 href (외부 http(s) 또는 base_url).
*/
$adminMenuListResolveHref = static function (string $rawLink) use ($adminMenusNavPath): string {
$rawLink = trim($rawLink);
if ($rawLink === '') {
return '';
}
if (preg_match('#^https?://#i', $rawLink)) {
return $rawLink;
}
$pathSeg = menu_link_preferred_href_path($rawLink, $adminMenusNavPath);
if ($pathSeg === '') {
$pathSeg = normalize_menu_link_for_url($rawLink);
}
return $pathSeg !== '' ? base_url($pathSeg) : '';
};
?> ?>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel"> <section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex flex-wrap items-center justify-between gap-y-2"> <div class="flex flex-wrap items-center justify-between gap-y-2">
@@ -23,6 +46,19 @@ $levelNames = $levelNames ?? [];
</div> </div>
</section> </section>
<?php if ($debugMode): ?>
<section class="mt-2 border border-amber-300 bg-amber-50 text-amber-900 rounded p-2 text-xs">
<strong>[DEBUG]</strong>
lg_idx=<?= esc((string) ($debugInfo['lg_idx'] ?? '')) ?>,
requested_mt_idx=<?= esc((string) ($debugInfo['requested_mt_idx'] ?? '')) ?>,
resolved_mt_idx=<?= esc((string) ($debugInfo['resolved_mt_idx'] ?? '')) ?>,
effective_mt_idx=<?= esc((string) ($debugInfo['effective_mt_idx'] ?? '')) ?>,
resolved_mt_code=<?= esc((string) ($debugInfo['resolved_mt_code'] ?? '')) ?>,
list_count=<?= esc((string) ($debugInfo['list_count'] ?? '')) ?>,
fallback_applied=<?= esc((string) ($debugInfo['fallback_applied'] ?? 'N')) ?>
</section>
<?php endif; ?>
<div class="flex gap-4 mt-2 flex-wrap"> <div class="flex gap-4 mt-2 flex-wrap">
<div class="border border-gray-300 bg-white rounded p-4 flex-1 min-w-0" style="min-width: 320px;"> <div class="border border-gray-300 bg-white rounded p-4 flex-1 min-w-0" style="min-width: 320px;">
<h3 class="text-sm font-bold text-gray-700 mb-2">메뉴 목록</h3> <h3 class="text-sm font-bold text-gray-700 mb-2">메뉴 목록</h3>
@@ -48,6 +84,10 @@ $levelNames = $levelNames ?? [];
</thead> </thead>
<tbody> <tbody>
<?php foreach ($list as $i => $row): ?> <?php foreach ($list as $i => $row): ?>
<?php
$rawLink = trim((string) $row->mm_link);
$listItemHref = $rawLink !== '' ? $adminMenuListResolveHref($rawLink) : '';
?>
<tr class="menu-row" data-mm-idx="<?= (int) $row->mm_idx ?>" data-mm-pidx="<?= (int) $row->mm_pidx ?>" data-mm-dep="<?= (int) $row->mm_dep ?>"> <tr class="menu-row" data-mm-idx="<?= (int) $row->mm_idx ?>" data-mm-pidx="<?= (int) $row->mm_pidx ?>" data-mm-dep="<?= (int) $row->mm_dep ?>">
<td class="text-center align-middle"> <td class="text-center align-middle">
<span class="menu-drag-handle cursor-move text-gray-400 select-none" title="드래그해서 순서를 변경하세요">↕</span> <span class="menu-drag-handle cursor-move text-gray-400 select-none" title="드래그해서 순서를 변경하세요">↕</span>
@@ -67,9 +107,21 @@ $levelNames = $levelNames ?? [];
└─ └─
<?php endif; ?> <?php endif; ?>
</span> </span>
<?php if ($listItemHref !== ''): ?>
<a href="<?= esc($listItemHref) ?>" class="ml-1 text-gray-900 hover:text-blue-700 hover:underline font-medium" target="_blank" rel="noopener noreferrer"><?= esc($row->mm_name) ?></a>
<?php else: ?>
<span class="ml-1"><?= esc($row->mm_name) ?></span> <span class="ml-1"><?= esc($row->mm_name) ?></span>
<?php endif; ?>
</td>
<td class="text-left pl-2 text-xs">
<?php if ($rawLink === ''): ?>
<span class="text-gray-400">—</span>
<?php elseif ($listItemHref !== ''): ?>
<a href="<?= esc($listItemHref) ?>" class="text-blue-600 hover:underline font-medium break-all" target="_blank" rel="noopener noreferrer"><?= esc($rawLink) ?></a>
<?php else: ?>
<span class="text-amber-700" title="URL로 해석되지 않는 링크입니다"><?= esc($rawLink) ?></span>
<?php endif; ?>
</td> </td>
<td class="text-left pl-2 text-xs"><?= esc($row->mm_link) ?></td>
<td class="text-left pl-2 text-xs"> <td class="text-left pl-2 text-xs">
<?php <?php
if ((string) $row->mm_level === '') { if ((string) $row->mm_level === '') {

View File

@@ -2,7 +2,7 @@
<span class="text-sm font-bold text-gray-700">포장 단위 등록</span> <span class="text-sm font-bold text-gray-700">포장 단위 등록</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/packaging-units/store') ?>" method="POST" class="space-y-4"> <form action="<?= mgmt_url('packaging-units/store') ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -42,7 +42,7 @@
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
<a href="<?= base_url('admin/packaging-units') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= mgmt_url('packaging-units') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -2,7 +2,7 @@
<span class="text-sm font-bold text-gray-700">포장 단위 수정</span> <span class="text-sm font-bold text-gray-700">포장 단위 수정</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/packaging-units/update/' . (int) $item->pu_idx) ?>" method="POST" class="space-y-4"> <form action="<?= mgmt_url('packaging-units/update/' . (int) $item->pu_idx) ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -25,7 +25,7 @@
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-32">적용시작일 <span class="text-red-500">*</span></label> <label class="block text-sm font-bold text-gray-700 w-32">적용시작일 <span class="text-red-500">*</span></label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="pu_start_date" type="date" value="<?= esc(old('pu_start_date', $item->pu_start_date)) ?>" required/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="pu_start_date" type="date" value="<?= esc(old('pu_start_date', date('Y-m-d'))) ?>" required/>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@@ -43,7 +43,7 @@
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button>
<a href="<?= base_url('admin/packaging-units') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= mgmt_url('packaging-units') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -1,16 +1,25 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel"> <section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a href="<?= base_url('admin/packaging-units') ?>" class="text-blue-600 hover:underline text-sm">&larr; 포장 단위 목록</a> <a href="<?= mgmt_url('packaging-units') ?>" class="text-blue-600 hover:underline text-sm">&larr; 포장 단위 목록</a>
<span class="text-gray-400">|</span> <span class="text-gray-400">|</span>
<span class="text-sm font-bold text-gray-700">변경 이력 <?= esc($item->pu_bag_name) ?> (<?= esc($item->pu_bag_code) ?>)</span> <span class="text-sm font-bold text-gray-700">변경 이력 <?= esc($item->pu_bag_name) ?> (<?= esc($item->pu_bag_code) ?>)</span>
</div> </div>
</section> </section>
<div class="border border-gray-300 overflow-auto mt-2"> <div class="border border-gray-300 overflow-auto mt-2">
<?php
$fieldLabelMap = [
'pu_box_per_pack' => '박스당 팩 수',
'pu_pack_per_sheet' => '팩당 낱장 수',
'pu_start_date' => '적용시작일',
'pu_end_date' => '적용종료일',
'pu_state' => '상태',
];
?>
<table class="w-full data-table"> <table class="w-full data-table">
<thead> <thead>
<tr> <tr>
<th class="w-16">번호</th> <th class="w-16">번호</th>
<th>변경 필드</th> <th>변경 내용</th>
<th>이전 값</th> <th>이전 값</th>
<th>변경 값</th> <th>변경 값</th>
<th>변경일시</th> <th>변경일시</th>
@@ -20,7 +29,7 @@
<?php foreach ($list as $row): ?> <?php foreach ($list as $row): ?>
<tr> <tr>
<td class="text-center"><?= esc($row->puh_idx) ?></td> <td class="text-center"><?= esc($row->puh_idx) ?></td>
<td class="text-left pl-2"><?= esc($row->puh_field) ?></td> <td class="text-left pl-2"><?= esc($fieldLabelMap[(string) $row->puh_field] ?? $row->puh_field) ?></td>
<td><?= esc($row->puh_old_value) ?></td> <td><?= esc($row->puh_old_value) ?></td>
<td><?= esc($row->puh_new_value) ?></td> <td><?= esc($row->puh_new_value) ?></td>
<td class="text-center"><?= esc($row->puh_changed_at) ?></td> <td class="text-center"><?= esc($row->puh_changed_at) ?></td>

View File

@@ -4,18 +4,18 @@
<span class="text-sm font-bold text-gray-700">포장 단위 관리</span> <span class="text-sm font-bold text-gray-700">포장 단위 관리</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button> <button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= base_url('admin/packaging-units/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">포장 단위 등록</a> <a href="<?= mgmt_url('packaging-units/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">포장 단위 등록</a>
</div> </div>
</div> </div>
</section> </section>
<section class="p-2 bg-white border-b border-gray-200"> <section class="p-2 bg-white border-b border-gray-200">
<form method="GET" action="<?= base_url('admin/packaging-units') ?>" class="flex flex-wrap items-center gap-2"> <form method="GET" action="<?= mgmt_url('packaging-units') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">적용시작일</label> <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"/> <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> <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"/> <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> <button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
<a href="<?= base_url('admin/packaging-units') ?>" class="text-sm text-gray-500 hover:underline">초기화</a> <a href="<?= mgmt_url('packaging-units') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
</form> </form>
</section> </section>
<div class="border border-gray-300 overflow-auto mt-2"> <div class="border border-gray-300 overflow-auto mt-2">
@@ -35,9 +35,17 @@
</tr> </tr>
</thead> </thead>
<tbody class="text-right"> <tbody class="text-right">
<?php foreach ($list as $row): ?> <?php
$startNo = 1;
if (isset($pager) && method_exists($pager, 'getCurrentPage') && method_exists($pager, 'getPerPage')) {
$currentPage = (int) $pager->getCurrentPage();
$perPage = (int) $pager->getPerPage();
$startNo = (($currentPage > 0 ? $currentPage : 1) - 1) * ($perPage > 0 ? $perPage : 20) + 1;
}
?>
<?php foreach (($list ?? []) as $idx => $row): ?>
<tr> <tr>
<td class="text-center"><?= esc($row->pu_idx) ?></td> <td class="text-center"><?= (int) $startNo + (int) $idx ?></td>
<td class="text-center font-mono"><?= esc($row->pu_bag_code) ?></td> <td class="text-center font-mono"><?= esc($row->pu_bag_code) ?></td>
<td class="text-left pl-2"><?= esc($row->pu_bag_name) ?></td> <td class="text-left pl-2"><?= esc($row->pu_bag_name) ?></td>
<td><?= number_format((int) $row->pu_box_per_pack) ?></td> <td><?= number_format((int) $row->pu_box_per_pack) ?></td>
@@ -47,9 +55,9 @@
<td class="text-center"><?= esc($row->pu_end_date ?? '현재') ?></td> <td class="text-center"><?= esc($row->pu_end_date ?? '현재') ?></td>
<td class="text-center"><?= (int) $row->pu_state === 1 ? '사용' : '미사용' ?></td> <td class="text-center"><?= (int) $row->pu_state === 1 ? '사용' : '미사용' ?></td>
<td class="text-center"> <td class="text-center">
<a href="<?= base_url('admin/packaging-units/history/' . (int) $row->pu_idx) ?>" class="text-green-600 hover:underline text-sm mr-1">이력</a> <a href="<?= mgmt_url('packaging-units/history/' . (int) $row->pu_idx) ?>" class="text-green-600 hover:underline text-sm mr-1">이력</a>
<a href="<?= base_url('admin/packaging-units/edit/' . (int) $row->pu_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">수정</a> <a href="<?= mgmt_url('packaging-units/edit/' . (int) $row->pu_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">수정</a>
<form action="<?= base_url('admin/packaging-units/delete/' . (int) $row->pu_idx) ?>" method="POST" class="inline" onsubmit="return confirm('삭제하시겠습니까?');"> <form action="<?= mgmt_url('packaging-units/delete/' . (int) $row->pu_idx) ?>" method="POST" class="inline" onsubmit="return confirm('삭제하시겠습니까?');">
<?= csrf_field() ?> <?= csrf_field() ?>
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button> <button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
</form> </form>

View File

@@ -2,7 +2,7 @@
<span class="text-sm font-bold text-gray-700">비밀번호 변경</span> <span class="text-sm font-bold text-gray-700">비밀번호 변경</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-md"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-md">
<form action="<?= base_url('admin/password-change') ?>" method="POST" class="space-y-4"> <form action="<?= mgmt_url('password-change') ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">

View File

@@ -2,37 +2,27 @@
<span class="text-sm font-bold text-gray-700">대행소 등록</span> <span class="text-sm font-bold text-gray-700">대행소 등록</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/sales-agencies/store') ?>" method="POST" class="space-y-4"> <form action="<?= mgmt_url('sales-agencies/store') ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">대행소 <span class="text-red-500">*</span></label> <label class="block text-sm font-bold text-gray-700 w-28">대행소 구분 <span class="text-red-500">*</span></label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="sa_name" type="text" value="<?= esc(old('sa_name')) ?>" required/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="sa_kind" type="text" value="<?= esc(old('sa_kind')) ?>" required maxlength="50"/>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">사업자번호</label> <label class="block text-sm font-bold text-gray-700 w-28">대행소 코드 <span class="text-red-500">*</span></label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="sa_biz_no" type="text" value="<?= esc(old('sa_biz_no')) ?>"/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="sa_code" type="text" value="<?= esc(old('sa_code')) ?>" required maxlength="50"/>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">대표자</label> <label class="block text-sm font-bold text-gray-700 w-28">대행소 명 <span class="text-red-500">*</span></label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="sa_rep_name" type="text" value="<?= esc(old('sa_rep_name')) ?>"/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="sa_name" type="text" value="<?= esc(old('sa_name')) ?>" required maxlength="100"/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">전화</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="sa_tel" type="text" value="<?= esc(old('sa_tel')) ?>"/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">주소</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96" name="sa_addr" type="text" value="<?= esc(old('sa_addr')) ?>"/>
</div> </div>
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
<a href="<?= base_url('admin/sales-agencies') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= mgmt_url('sales-agencies') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -2,45 +2,27 @@
<span class="text-sm font-bold text-gray-700">대행소 수정</span> <span class="text-sm font-bold text-gray-700">대행소 수정</span>
</section> </section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl"> <div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('admin/sales-agencies/update/' . (int) $item->sa_idx) ?>" method="POST" class="space-y-4"> <form action="<?= mgmt_url('sales-agencies/update/' . (int) $item->sa_idx) ?>" method="POST" class="space-y-4">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">대행소 <span class="text-red-500">*</span></label> <label class="block text-sm font-bold text-gray-700 w-28">대행소 구분 <span class="text-red-500">*</span></label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="sa_name" type="text" value="<?= esc(old('sa_name', $item->sa_name)) ?>" required/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="sa_kind" type="text" value="<?= esc(old('sa_kind', $item->sa_kind ?? '')) ?>" required maxlength="50"/>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">사업자번호</label> <label class="block text-sm font-bold text-gray-700 w-28">대행소 코드 <span class="text-red-500">*</span></label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="sa_biz_no" type="text" value="<?= esc(old('sa_biz_no', $item->sa_biz_no)) ?>"/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="sa_code" type="text" value="<?= esc(old('sa_code', $item->sa_code ?? '')) ?>" required maxlength="50"/>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">대표자</label> <label class="block text-sm font-bold text-gray-700 w-28">대행소 명 <span class="text-red-500">*</span></label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="sa_rep_name" type="text" value="<?= esc(old('sa_rep_name', $item->sa_rep_name)) ?>"/> <input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="sa_name" type="text" value="<?= esc(old('sa_name', $item->sa_name)) ?>" required maxlength="100"/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">전화</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="sa_tel" type="text" value="<?= esc(old('sa_tel', $item->sa_tel)) ?>"/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">주소</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96" name="sa_addr" type="text" value="<?= esc(old('sa_addr', $item->sa_addr)) ?>"/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">상태 <span class="text-red-500">*</span></label>
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32" name="sa_state" required>
<option value="1" <?= (int) old('sa_state', $item->sa_state) === 1 ? 'selected' : '' ?>>사용</option>
<option value="0" <?= (int) old('sa_state', $item->sa_state) === 0 ? 'selected' : '' ?>>미사용</option>
</select>
</div> </div>
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button> <button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button>
<a href="<?= base_url('admin/sales-agencies') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a> <a href="<?= mgmt_url('sales-agencies') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -4,37 +4,63 @@
<span class="text-sm font-bold text-gray-700">판매 대행소 관리</span> <span class="text-sm font-bold text-gray-700">판매 대행소 관리</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button> <button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= base_url('admin/sales-agencies/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">대행소 등록</a> <a href="<?= mgmt_url('sales-agencies/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">대행소 등록</a>
</div> </div>
</div> </div>
</section> </section>
<section class="no-print border border-gray-300 bg-white p-2 mt-2">
<form method="GET" action="<?= mgmt_url('sales-agencies') ?>" class="flex flex-wrap items-end gap-2">
<div class="flex flex-col gap-0.5">
<label class="text-xs text-gray-500">번호</label>
<input type="text" name="sa_idx" value="<?= esc($sa_idx ?? '') ?>" class="border border-gray-300 rounded px-2 py-1.5 text-sm w-24" placeholder="예: 12">
</div>
<div class="flex flex-col gap-0.5">
<label class="text-xs text-gray-500">대행소 구분</label>
<input type="text" name="sa_kind" value="<?= esc($sa_kind ?? '') ?>" class="border border-gray-300 rounded px-2 py-1.5 text-sm min-w-[10rem]" placeholder="구분 입력">
</div>
<div class="flex flex-col gap-0.5">
<label class="text-xs text-gray-500">대행소 코드</label>
<input type="text" name="sa_code" value="<?= esc($sa_code ?? '') ?>" class="border border-gray-300 rounded px-2 py-1.5 text-sm min-w-[10rem]" placeholder="코드 입력">
</div>
<div class="flex flex-col gap-0.5">
<label class="text-xs text-gray-500">대행소 명</label>
<input type="text" name="sa_name" value="<?= esc($sa_name ?? '') ?>" class="border border-gray-300 rounded px-2 py-1.5 text-sm min-w-[14rem]" placeholder="대행소 명 입력">
</div>
<div class="flex items-center gap-2 pb-0.5">
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm">조회</button>
<a href="<?= mgmt_url('sales-agencies') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
</div>
</form>
</section>
<div class="border border-gray-300 overflow-auto mt-2"> <div class="border border-gray-300 overflow-auto mt-2">
<table class="w-full data-table"> <table class="w-full data-table">
<thead> <thead>
<tr> <tr>
<th class="w-16">번호</th> <th class="w-16">번호</th>
<th>대행소</th> <th>대행소 구분</th>
<th>사업자번호</th> <th>대행소 코드</th>
<th>대표자</th> <th>대행소 명</th>
<th>전화</th>
<th>주소</th>
<th class="w-20">상태</th>
<th class="w-36">작업</th> <th class="w-36">작업</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($list as $row): ?> <?php
$startNo = 1;
if (isset($pager) && method_exists($pager, 'getCurrentPage') && method_exists($pager, 'getPerPage')) {
$currentPage = (int) $pager->getCurrentPage();
$perPage = (int) $pager->getPerPage();
$startNo = (($currentPage > 0 ? $currentPage : 1) - 1) * ($perPage > 0 ? $perPage : 20) + 1;
}
?>
<?php foreach (($list ?? []) as $idx => $row): ?>
<tr> <tr>
<td class="text-center"><?= esc($row->sa_idx) ?></td> <td class="text-center"><?= (int) $startNo + (int) $idx ?></td>
<td class="text-left pl-2"><?= esc($row->sa_kind ?? '') ?></td>
<td class="text-center"><?= esc($row->sa_code ?? '') ?></td>
<td class="text-left pl-2"><?= esc($row->sa_name) ?></td> <td class="text-left pl-2"><?= esc($row->sa_name) ?></td>
<td class="text-center"><?= esc($row->sa_biz_no) ?></td>
<td class="text-center"><?= esc($row->sa_rep_name) ?></td>
<td class="text-center"><?= esc($row->sa_tel) ?></td>
<td class="text-left pl-2"><?= esc($row->sa_addr) ?></td>
<td class="text-center"><?= (int) $row->sa_state === 1 ? '정상' : '미사용' ?></td>
<td class="text-center"> <td class="text-center">
<a href="<?= base_url('admin/sales-agencies/edit/' . (int) $row->sa_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">수정</a> <a href="<?= mgmt_url('sales-agencies/edit/' . (int) $row->sa_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">수정</a>
<form action="<?= base_url('admin/sales-agencies/delete/' . (int) $row->sa_idx) ?>" method="POST" class="inline" onsubmit="return confirm('삭제하시겠습니까?');"> <form action="<?= mgmt_url('sales-agencies/delete/' . (int) $row->sa_idx) ?>" method="POST" class="inline" onsubmit="return confirm('삭제하시겠습니까?');">
<?= csrf_field() ?> <?= csrf_field() ?>
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button> <button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
</form> </form>
@@ -42,7 +68,7 @@
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (empty($list)): ?> <?php if (empty($list)): ?>
<tr><td colspan="8" class="text-center text-gray-400 py-4">등록된 데이터가 없습니다.</td></tr> <tr><td colspan="5" class="text-center text-gray-400 py-4">등록된 데이터가 없습니다.</td></tr>
<?php endif; ?> <?php endif; ?>
</tbody> </tbody>
</table> </table>

View File

@@ -6,7 +6,7 @@
</div> </div>
</section> </section>
<section class="p-2 bg-white border-b border-gray-200"> <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"> <form method="GET" action="<?= mgmt_url('reports/daily-summary') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">조회일</label> <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"/> <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> <button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>

View File

@@ -6,7 +6,7 @@
</div> </div>
</section> </section>
<section class="p-2 bg-white border-b border-gray-200"> <section class="p-2 bg-white border-b border-gray-200">
<form method="GET" action="<?= base_url('admin/reports/lot-flow') ?>" class="flex flex-wrap items-center gap-2"> <form method="GET" action="<?= mgmt_url('reports/lot-flow') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">LOT 번호</label> <label class="text-sm text-gray-600">LOT 번호</label>
<input type="text" name="lot_no" value="<?= esc($lotNo ?? '') ?>" placeholder="LOT-YYYYMMDD-XXXXXX" class="border border-gray-300 rounded px-2 py-1 text-sm w-64"/> <input type="text" name="lot_no" value="<?= esc($lotNo ?? '') ?>" placeholder="LOT-YYYYMMDD-XXXXXX" class="border border-gray-300 rounded px-2 py-1 text-sm w-64"/>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button> <button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>

View File

@@ -14,7 +14,7 @@
<!-- 등록 폼 --> <!-- 등록 폼 -->
<section class="p-2 bg-white border-b border-gray-200 no-print"> <section class="p-2 bg-white border-b border-gray-200 no-print">
<form method="POST" action="<?= base_url('admin/reports/misc-flow') ?>" class="flex flex-wrap items-center gap-2"> <form method="POST" action="<?= mgmt_url('reports/misc-flow') ?>" class="flex flex-wrap items-center gap-2">
<?= csrf_field() ?> <?= csrf_field() ?>
<label class="text-sm text-gray-600">구분</label> <label class="text-sm text-gray-600">구분</label>
<select name="bmf_type" class="border border-gray-300 rounded px-2 py-1 text-sm" required> <select name="bmf_type" class="border border-gray-300 rounded px-2 py-1 text-sm" required>
@@ -40,7 +40,7 @@
<!-- 조회 필터 --> <!-- 조회 필터 -->
<section class="p-2 bg-white border-b border-gray-200 no-print"> <section class="p-2 bg-white border-b border-gray-200 no-print">
<form method="GET" action="<?= base_url('admin/reports/misc-flow') ?>" class="flex flex-wrap items-center gap-2"> <form method="GET" action="<?= mgmt_url('reports/misc-flow') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">시작일</label> <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"/> <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> <label class="text-sm text-gray-600">~</label>

View File

@@ -6,7 +6,7 @@
</div> </div>
</section> </section>
<section class="p-2 bg-white border-b border-gray-200"> <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"> <form method="GET" action="<?= mgmt_url('reports/period-sales') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">시작일</label> <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"/> <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> <label class="text-sm text-gray-600">~</label>

View File

@@ -6,7 +6,7 @@
</div> </div>
</section> </section>
<section class="p-2 bg-white border-b border-gray-200"> <section class="p-2 bg-white border-b border-gray-200">
<form method="GET" action="<?= base_url('admin/reports/returns') ?>" class="flex flex-wrap items-center gap-2"> <form method="GET" action="<?= mgmt_url('reports/returns') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">시작일</label> <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"/> <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> <label class="text-sm text-gray-600">~</label>

View File

@@ -6,7 +6,7 @@
</div> </div>
</section> </section>
<section class="p-2 bg-white border-b border-gray-200"> <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"> <form method="GET" action="<?= mgmt_url('reports/sales-ledger') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">시작일</label> <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"/> <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> <label class="text-sm text-gray-600">~</label>

View File

@@ -6,7 +6,7 @@
</div> </div>
</section> </section>
<section class="p-2 bg-white border-b border-gray-200"> <section class="p-2 bg-white border-b border-gray-200">
<form method="GET" action="<?= base_url('admin/reports/shop-sales') ?>" class="flex flex-wrap items-center gap-2"> <form method="GET" action="<?= mgmt_url('reports/shop-sales') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">시작일</label> <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"/> <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> <label class="text-sm text-gray-600">~</label>

View File

@@ -6,7 +6,7 @@
</div> </div>
</section> </section>
<section class="p-2 bg-white border-b border-gray-200"> <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"> <form method="GET" action="<?= mgmt_url('reports/supply-demand') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">시작일</label> <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"/> <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> <label class="text-sm text-gray-600">~</label>

View File

@@ -6,7 +6,7 @@
</div> </div>
</section> </section>
<section class="p-2 bg-white border-b border-gray-200"> <section class="p-2 bg-white border-b border-gray-200">
<form method="GET" action="<?= base_url('admin/reports/yearly-sales') ?>" class="flex flex-wrap items-center gap-2"> <form method="GET" action="<?= mgmt_url('reports/yearly-sales') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">연도</label> <label class="text-sm text-gray-600">연도</label>
<select name="year" class="border border-gray-300 rounded px-2 py-1 text-sm"> <select name="year" class="border border-gray-300 rounded px-2 py-1 text-sm">
<?php for ($y = (int) date('Y'); $y >= 2020; $y--): ?> <?php for ($y = (int) date('Y'); $y >= 2020; $y--): ?>

Some files were not shown because too many files have changed in this diff Show More