Compare commits
44 Commits
da132f0e51
...
clean/push
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8753b1aa68 | ||
|
|
aa50eb72ee | ||
|
|
7580c31ab0 | ||
|
|
6fddf15000 | ||
|
|
b99c108aeb | ||
|
|
f68f135446 | ||
|
|
0d512bd21d | ||
|
|
12cd052c40 | ||
|
|
aaf7b4c66e | ||
|
|
d551dfa87e | ||
|
|
84026f8072 | ||
|
|
1a8d4bb9da | ||
|
|
06aa401048 | ||
|
|
06fedc866a | ||
|
|
b5eed31b94 | ||
|
|
c2dc2fd38a | ||
|
|
984ddb403e | ||
|
|
89f80edc5d | ||
|
|
5b0c3fac97 | ||
|
|
c4d30b204b | ||
|
|
ab40a90f69 | ||
|
|
de8f631ca8 | ||
|
|
ac88ebdedb | ||
|
|
6fdd040d4d | ||
|
|
1e8bf1eeeb | ||
|
|
704141a1f0 | ||
|
|
35561b414b | ||
|
|
39ee71cc80 | ||
|
|
f6a64e07b8 | ||
|
|
56661ed5dc | ||
|
|
a0c75a4a31 | ||
|
|
9193fc587e | ||
|
|
c3c731cda0 | ||
|
|
a3f92cd322 | ||
|
|
d36217920f | ||
|
|
0a982aae96 | ||
|
|
65d8076721 | ||
|
|
a0103eb95d | ||
|
|
466f6fe085 | ||
|
|
2e3b43554c | ||
|
|
f451f0ff3b | ||
|
|
6e8bd84182 | ||
|
|
d9d3ef46c1 | ||
|
|
c2840a9e34 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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/
|
||||||
|
|||||||
760
README.md
760
README.md
@@ -1,12 +1,20 @@
|
|||||||
# 종량제 — 쓰레기봉투 물류시스템 (jongryangje)
|
# 종량제 -- 쓰레기봉투 물류시스템 (jongryangje)
|
||||||
|
|
||||||
**[종량제 개발목록 (엑셀 다운로드)](https://github.com/wixon-associates/jongryangje/raw/main/assets/종량제_개발목록_20260127.xlsx)** — 로컬 복제 후에는 [`assets/종량제_개발목록_20260127.xlsx`](./assets/종량제_개발목록_20260127.xlsx)
|
**[종량제 개발목록 (엑셀 다운로드)](https://github.com/wixon-associates/jongryangje/raw/main/assets/종량제_개발목록_20260127.xlsx)** -- 로컬 복제 후에는 [`assets/종량제_개발목록_20260127.xlsx`](./assets/종량제_개발목록_20260127.xlsx)
|
||||||
|
|
||||||
지자체·지정판매소 등을 대상으로 하는 **종량제 쓰레기봉투 물류·업무 웹 애플리케이션**입니다.
|
지자체/지정판매소 등을 대상으로 하는 **종량제 쓰레기봉투 물류/업무 웹 애플리케이션**입니다.
|
||||||
백엔드는 **[CodeIgniter 4](https://codeigniter.com/)** 기반입니다.
|
백엔드는 **[CodeIgniter 4](https://codeigniter.com/)** 기반입니다.
|
||||||
|
|
||||||
**저장소:** [wixon-associates/jongryangje](https://github.com/wixon-associates/jongryangje)
|
**저장소:** [wixon-associates/jongryangje](https://github.com/wixon-associates/jongryangje)
|
||||||
| **[구현 화면 스크린샷](./docs/SCREENSHOTS.md)** | **[Notion 진행상황](https://www.notion.so/31b42b987c3780baba32ded04a1d41bb)** |
|
| **[구현 화면 스크린샷](./docs/SCREENSHOTS.md)** | **[Notion 진행상황](https://www.notion.so/31b42b987c3780baba32ded04a1d41bb)** | **[서버/배포 가이드](./docs/server.md)** |
|
||||||
|
|
||||||
|
### 운영 환경
|
||||||
|
|
||||||
|
| 서비스 | URL |
|
||||||
|
|--------|-----|
|
||||||
|
| 웹 서비스 | https://trash.wxn.co.kr |
|
||||||
|
| Gitea (Git) | https://gitea.wxn.co.kr |
|
||||||
|
| GitHub | https://github.com/wixon-associates/jongryangje |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -18,77 +26,9 @@
|
|||||||
| Language | PHP 8.2+ (strict types) |
|
| Language | PHP 8.2+ (strict types) |
|
||||||
| Database | MySQL / MariaDB (MySQLi) |
|
| Database | MySQL / MariaDB (MySQLi) |
|
||||||
| 의존성 관리 | Composer 2.x |
|
| 의존성 관리 | Composer 2.x |
|
||||||
| 테스트 | PHPUnit 10.x |
|
| E2E 테스트 | Playwright (Chromium) |
|
||||||
| 세션 | 파일 기반 (`writable/session/`) |
|
| 세션 | 파일 기반 (`writable/session/`) |
|
||||||
|
| 프론트엔드 | Tailwind CSS (CDN), Vanilla JS |
|
||||||
---
|
|
||||||
|
|
||||||
## 요구 사항
|
|
||||||
|
|
||||||
- **PHP** 8.2 이상 (`composer.json` 기준)
|
|
||||||
- **Composer** 2.x
|
|
||||||
- **MySQL / MariaDB** (프로젝트는 `MySQLi` 드라이버 사용)
|
|
||||||
- 권장 PHP 확장: `intl`, `mbstring`, MySQL 사용 시 `mysqlnd`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 빠른 시작 (로컬)
|
|
||||||
|
|
||||||
### 1) 저장소 복제
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/wixon-associates/jongryangje.git
|
|
||||||
cd jongryangje
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2) 의존성 설치
|
|
||||||
|
|
||||||
```bash
|
|
||||||
composer install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3) 환경 설정
|
|
||||||
|
|
||||||
루트에 있는 샘플 파일을 복사해 `.env`를 만듭니다.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp env .env
|
|
||||||
```
|
|
||||||
|
|
||||||
`.env`에서 최소한 다음을 설정합니다.
|
|
||||||
|
|
||||||
| 항목 | 설명 |
|
|
||||||
|------|------|
|
|
||||||
| `app.baseURL` | 예: `http://localhost:8080/` (끝에 `/`) |
|
|
||||||
| `database.default.*` | DB 호스트·DB명·사용자·비밀번호 |
|
|
||||||
| `encryption.key` | 개인정보(이메일·연락처) 암호화용. **64자리 hex** (예: `php -r "echo bin2hex(random_bytes(32));"`) |
|
|
||||||
|
|
||||||
> `.env`는 **Git에 올리지 마세요.** 비밀번호·키가 들어갑니다.
|
|
||||||
|
|
||||||
### 4) 데이터베이스 준비
|
|
||||||
|
|
||||||
아래 순서대로 SQL 스크립트를 실행합니다.
|
|
||||||
|
|
||||||
| 순서 | 파일 | 용도 |
|
|
||||||
|------|------|------|
|
|
||||||
| 1 | `writable/database/init_jongryangje_dev.sql` | DB·DB 사용자 생성 |
|
|
||||||
| 2 | `writable/database/login_tables.sql` | 회원·로그인·지자체·지정판매소 기본 테이블 |
|
|
||||||
| 3 | `writable/database/member_approval_request_add.sql` | 회원가입 역할 승인 요청 테이블 |
|
|
||||||
| 4 | `writable/database/menu_tables.sql` | 메뉴 시스템 (메뉴 타입 + 메뉴 항목 + 시드) |
|
|
||||||
| 5 | `writable/database/local_government_init_daegu.sql` | 대구 시범 지자체 데이터 |
|
|
||||||
| 6 | `writable/database/code_master_init_daegu.sql` | 기본코드 마스터 초기화 |
|
|
||||||
|
|
||||||
### 5) 개발 서버 실행
|
|
||||||
|
|
||||||
```bash
|
|
||||||
php spark serve --port=8045
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6) 테스트
|
|
||||||
|
|
||||||
```bash
|
|
||||||
vendor/bin/phpunit
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -96,21 +36,119 @@ vendor/bin/phpunit
|
|||||||
|
|
||||||
```
|
```
|
||||||
app/
|
app/
|
||||||
├── Config/ # Routes, Database, Roles, Filters, Session 등
|
├── Config/ # Routes, Database, Roles, Filters, Session 등 (45개)
|
||||||
├── Controllers/ # Home, Auth, Admin/* (8개 관리자 컨트롤러)
|
├── Controllers/ # 28개 컨트롤러
|
||||||
├── Models/ # 7개 모델 (Member, LocalGovernment, DesignatedShop 등)
|
│ ├── Auth.php # 로그인/로그아웃/회원가입
|
||||||
├── Views/ # admin/, auth/, bag/, home/ 템플릿
|
│ ├── Bag.php # 사이트 메뉴 페이지 (기본정보·기본코드 목록 등)
|
||||||
├── Filters/ # AdminAuthFilter (관리자 접근 제어)
|
│ ├── Home.php # 홈/대시보드
|
||||||
|
│ └── Admin/ # 관리자 컨트롤러 24개
|
||||||
|
│ ├── BagOrder.php # 발주 관리
|
||||||
|
│ ├── BagReceiving.php # 입고 관리
|
||||||
|
│ ├── BagInventory.php # 재고 현황
|
||||||
|
│ ├── BagSale.php # 판매/반품 관리
|
||||||
|
│ ├── BagIssue.php # 무료용 불출 관리
|
||||||
|
│ ├── ShopOrder.php # 주문 접수 관리
|
||||||
|
│ ├── BagPrice.php # 봉투 단가 관리
|
||||||
|
│ ├── PackagingUnit.php # 포장 단위 관리
|
||||||
|
│ ├── CodeKind.php # 기본코드 종류
|
||||||
|
│ ├── CodeDetail.php # 세부코드
|
||||||
|
│ ├── SalesAgency.php # 판매 대행소
|
||||||
|
│ ├── Manager.php # 담당자
|
||||||
|
│ ├── Company.php # 업체 (제작/협회/회수)
|
||||||
|
│ ├── FreeRecipient.php # 무료용 대상자
|
||||||
|
│ ├── SalesReport.php # 리포트 (판매대장/일계표/수불)
|
||||||
|
│ ├── User.php # 회원 관리
|
||||||
|
│ ├── DesignatedShop.php # 지정판매소
|
||||||
|
│ ├── LocalGovernment.php # 지자체
|
||||||
|
│ ├── Menu.php # 메뉴 관리
|
||||||
|
│ ├── PasswordChange.php # 비밀번호 변경
|
||||||
|
│ └── ...
|
||||||
|
├── Models/ # 25개 모델
|
||||||
|
├── Views/ # 88개 뷰 템플릿
|
||||||
|
│ ├── admin/ # 관리자 뷰 (59개, 엔티티별 하위 디렉토리)
|
||||||
|
│ ├── bag/ # 사이트 메뉴 뷰 (17개 + 레이아웃)
|
||||||
|
│ ├── auth/ # 로그인/회원가입 (2개)
|
||||||
|
│ └── home/ # 대시보드 (1개)
|
||||||
|
├── 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/ # 웹 루트
|
||||||
writable/database/ # SQL 초기화 스크립트
|
writable/database/ # SQL 초기화/시드 스크립트 (21개)
|
||||||
tests/ # unit/, database/, session/
|
e2e/ # Playwright E2E 테스트 (84개 테스트)
|
||||||
assets/ # 기획 문서 (엑셀)
|
assets/ # 기획 문서 (엑셀)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 데이터베이스 (25개 테이블)
|
||||||
|
|
||||||
|
### 회원/인증
|
||||||
|
|
||||||
|
| 테이블 | 용도 |
|
||||||
|
|--------|------|
|
||||||
|
| `member` | 회원 (mb_id, mb_level, mb_state, PII 암호화, 로그인 실패 lock) |
|
||||||
|
| `member_log` | 로그인/로그아웃 감사 로그 (IP, User-Agent) |
|
||||||
|
| `member_approval_request` | 회원가입 역할 승인 요청 (pending/approved/rejected) |
|
||||||
|
|
||||||
|
### 지자체/판매소
|
||||||
|
|
||||||
|
| 테이블 | 용도 |
|
||||||
|
|--------|------|
|
||||||
|
| `local_government` | 지자체 (테넌트 루트, lg_code 기반) |
|
||||||
|
| `designated_shop` | 지정판매소 (지자체별, 판매소번호 자동생성) |
|
||||||
|
|
||||||
|
### 메뉴 시스템
|
||||||
|
|
||||||
|
| 테이블 | 용도 |
|
||||||
|
|--------|------|
|
||||||
|
| `menu_type` | 메뉴 유형 (admin, site) |
|
||||||
|
| `menu` | 메뉴 항목 (트리 구조, 역할별 노출, 지자체별) |
|
||||||
|
|
||||||
|
### 기본코드 마스터
|
||||||
|
|
||||||
|
| 테이블 | 용도 |
|
||||||
|
|--------|------|
|
||||||
|
| `code_kind` | 코드 종류 (A~T, 20종) |
|
||||||
|
| `code_detail` | 세부코드 (행정구역, 봉투구분, 재질, 용량 등) |
|
||||||
|
|
||||||
|
### 단가/포장
|
||||||
|
|
||||||
|
| 테이블 | 용도 |
|
||||||
|
|--------|------|
|
||||||
|
| `bag_price` | 봉투 단가 (발주/도매/소비자가, 적용기간) |
|
||||||
|
| `bag_price_history` | 단가 변경 이력 |
|
||||||
|
| `packaging_unit` | 포장 단위 (박스/팩/낱장) |
|
||||||
|
| `packaging_unit_history` | 포장 단위 변경 이력 |
|
||||||
|
|
||||||
|
### 업체/담당자/대상자
|
||||||
|
|
||||||
|
| 테이블 | 용도 |
|
||||||
|
|--------|------|
|
||||||
|
| `sales_agency` | 판매 대행소 |
|
||||||
|
| `company` | 업체 (manufacturer/association/collector) |
|
||||||
|
| `manager` | 담당자 (소속/직위) |
|
||||||
|
| `free_recipient` | 무료용 대상자 (생보자/시설/수훈자) |
|
||||||
|
|
||||||
|
### 발주/입고/재고
|
||||||
|
|
||||||
|
| 테이블 | 용도 |
|
||||||
|
|--------|------|
|
||||||
|
| `bag_order` | 발주 (UUID, LOT번호, SHA-256 해시) |
|
||||||
|
| `bag_order_item` | 발주 품목 (봉투코드별 수량/금액) |
|
||||||
|
| `bag_receiving` | 입고 (발주 연계, 박스/낱장 수량) |
|
||||||
|
| `bag_inventory` | 재고 현황 (봉투코드별 현재 재고) |
|
||||||
|
|
||||||
|
### 판매/주문/불출
|
||||||
|
|
||||||
|
| 테이블 | 용도 |
|
||||||
|
|--------|------|
|
||||||
|
| `bag_sale` | 판매/반품 (판매소, 봉투코드, 수량/금액) |
|
||||||
|
| `shop_order` | 주문 접수 (배달일, 결제/입금/수령 상태) |
|
||||||
|
| `shop_order_item` | 주문 품목 (박스/팩/낱장 단위) |
|
||||||
|
| `bag_issue` | 무료용 불출 (연도/분기, 불출처, 상태) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 역할 체계 (RBAC)
|
## 역할 체계 (RBAC)
|
||||||
|
|
||||||
| Level | 역할 | 설명 |
|
| Level | 역할 | 설명 |
|
||||||
@@ -120,295 +158,339 @@ assets/ # 기획 문서 (엑셀)
|
|||||||
| 2 | 지정판매소 | 봉투 판매/재고 관리 |
|
| 2 | 지정판매소 | 봉투 판매/재고 관리 |
|
||||||
| 1 | 일반 사용자 | 기본 조회 (시민) |
|
| 1 | 일반 사용자 | 기본 조회 (시민) |
|
||||||
|
|
||||||
- 역할 상수: `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` 필터)
|
||||||
|
|
||||||
## 멀티테넌시
|
## 멀티테넌시
|
||||||
|
|
||||||
- `local_government.lg_idx` 가 테넌트 루트
|
- `local_government.lg_idx`가 테넌트 루트
|
||||||
- 관리자 필터에서 `session('admin_lg_idx')` 기반 테넌트 분리
|
- 관리자 필터에서 `admin_effective_lg_idx()` 기반 테넌트 분리
|
||||||
- Super Admin은 `/admin/select-local-government`에서 작업 지자체 선택
|
- Super Admin은 `/admin/select-local-government`에서 작업 지자체 선택
|
||||||
|
- 지자체관리자는 소속 `mb_lg_idx` 자동 적용
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 주요 URL
|
## 라우트 구조
|
||||||
|
|
||||||
| 경로 | 설명 | 인증 |
|
### 공개 페이지
|
||||||
|
|
||||||
|
| 경로 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| `/` | 홈 (비로그인: 환영, 로그인: 대시보드) |
|
||||||
|
| `/login`, `/logout` | 로그인/로그아웃 |
|
||||||
|
| `/register` | 회원가입 (역할 승인 플로우) |
|
||||||
|
| `/dashboard/*` | 대시보드 시안 (classic/modern/dense/charts) |
|
||||||
|
|
||||||
|
### 사이트 메뉴 (`/bag/*`)
|
||||||
|
|
||||||
|
| 경로 | 설명 | 기능 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `/` | 홈 (비로그인 시 환영 화면) | 공개 |
|
| `/bag/basic-info` | 기본정보관리 | 단가·포장단위 등 링크 허브 (`/bag/code-kinds`로 기본코드 조회) |
|
||||||
| `/login`, `/logout` | 로그인·로그아웃 | 공개 |
|
| `/bag/code-kinds` | 기본코드 종류 | 종류 목록·세부코드 링크 (조회; CRUD는 관리자만) |
|
||||||
| `/register` | 회원가입 (역할 승인 플로우 연동) | 공개 |
|
| `/bag/code-details/{ck_idx}` | 기본코드 세부 | 해당 종류의 세부코드 목록 (조회; CRUD는 관리자만) |
|
||||||
| `/dashboard` | 로그인 후 메인 대시보드 | 인증 |
|
| `/bag/purchase-inbound` | 발주 입고 관리 | 발주/입고 목록 + 등록 버튼 |
|
||||||
| `/dashboard/classic-mock` 등 | UI 시안용 라우트 | 인증 |
|
| `/bag/issue` | 불출 관리 | 불출 목록 + 처리/취소 |
|
||||||
| `/bag/*` | 봉투 관련 페이지 (목업) | 인증 |
|
| `/bag/inventory` | 재고 관리 | 봉투별 현재 재고 조회 |
|
||||||
| `/admin` | 관리자 대시보드 | 관리자 (Lv.3+) |
|
| `/bag/sales` | 판매 관리 | 주문/판매/반품 + 등록 |
|
||||||
| `/admin/users` | 회원 관리 (CRUD) | 관리자 |
|
| `/bag/sales-stats` | 판매 현황 | 기간별 판매 데이터 |
|
||||||
| `/admin/access/login-history` | 로그인 이력 조회 | 관리자 |
|
| `/bag/flow` | 봉투 수불 관리 | 봉투코드별 입출고 수불 요약 |
|
||||||
| `/admin/access/approvals` | 회원가입 역할 승인 대기 처리 | 관리자 |
|
| `/bag/analytics` | 통계 분석 관리 | Phase 6 예정 |
|
||||||
| `/admin/roles` | 역할 목록 조회 | 관리자 |
|
| `/bag/window` | 창 | Phase 6 예정 |
|
||||||
| `/admin/menus` | 메뉴 관리 (트리 구조 CRUD) | 관리자 |
|
| `/bag/help` | 도움말 | 시스템 안내 |
|
||||||
| `/admin/local-governments` | 지자체 관리 | 관리자 |
|
|
||||||
| `/admin/designated-shops` | 지정판매소 관리 (CRUD) | 관리자 |
|
|
||||||
| `/admin/select-local-government` | 작업 지자체 선택 | Super Admin |
|
|
||||||
|
|
||||||
정확한 라우트는 `app/Config/Routes.php`를 확인하세요.
|
### 관리자 (`/admin/*`, adminAuth 필터)
|
||||||
|
|
||||||
|
**시스템 관리**
|
||||||
|
|
||||||
|
| 경로 | 기능 |
|
||||||
|
|------|------|
|
||||||
|
| `/admin` | 관리자 대시보드 |
|
||||||
|
| `/admin/users/*` | 회원 관리 (CRUD) |
|
||||||
|
| `/admin/access/login-history` | 로그인 이력 |
|
||||||
|
| `/admin/access/approvals` | 회원 승인 대기 처리 |
|
||||||
|
| `/admin/roles` | 역할 목록 |
|
||||||
|
| `/admin/menus/*` | 메뉴 관리 (트리 CRUD) |
|
||||||
|
| `/admin/local-governments/*` | 지자체 관리 (CRUD) |
|
||||||
|
| `/admin/select-local-government` | 작업 지자체 선택 (Super Admin) |
|
||||||
|
|
||||||
|
**기본코드 CRUD만 관리자 경로 (목록·조회는 `/bag/*`)**
|
||||||
|
|
||||||
|
| 경로 | 기능 |
|
||||||
|
|------|------|
|
||||||
|
| `/admin/code-kinds/*` | 기본코드 종류 **CRUD만** (create/edit/store/update/delete; **목록 없음** — 조회는 `/bag/code-kinds`) |
|
||||||
|
| `/admin/code-details/*` | 세부코드 **CRUD만** (**목록 없음** — 조회는 `/bag/code-details/{ck_idx}`) |
|
||||||
|
| (호환) `GET /admin/code-details/{ck_idx}` | `/bag/code-details/{ck_idx}` 로 리다이렉트 |
|
||||||
|
|
||||||
|
### 업무 화면 (`/bag/*`, adminAuth 필터)
|
||||||
|
|
||||||
|
동일 `Admin\*` 컨트롤러·뷰를 쓰며 메인 사이트 레이아웃으로 렌더된다. `GET /admin/managers` 등 옛 업무 URL은 `301`·`POST` 는 `307`로 `/bag/...`에 리다이렉트된다.
|
||||||
|
|
||||||
|
| 경로 | 기능 |
|
||||||
|
|------|------|
|
||||||
|
| `/bag/password-change` | 비밀번호 변경 |
|
||||||
|
| `/bag/designated-shops/*` | 지정판매소 관리 (CRUD) |
|
||||||
|
| `/bag/bag-prices/*` | 봉투 단가 (CRUD + 이력) |
|
||||||
|
| `/bag/packaging-units/manage/*` | 포장 단위 (CRUD + 이력; 조회 전용은 `/bag/packaging-units`) |
|
||||||
|
| `/bag/sales-agencies/*` | 판매 대행소 (CRUD) |
|
||||||
|
| `/bag/managers/*` | 담당자 (CRUD) |
|
||||||
|
| `/bag/companies/*` | 업체 (CRUD) |
|
||||||
|
| `/bag/free-recipients/*` | 무료용 대상자 (CRUD) |
|
||||||
|
| `/bag/bag-orders/*` | 발주 관리 (등록/상세/취소/삭제) |
|
||||||
|
| `/bag/bag-receivings/*` | 입고 관리 (등록, 재고 자동 반영) |
|
||||||
|
| `/bag/bag-inventory` | 재고 현황 조회 |
|
||||||
|
| `/bag/shop-orders/*` | 주문 접수 (등록/취소) |
|
||||||
|
| `/bag/bag-sales/*` | 판매/반품 (등록) |
|
||||||
|
| `/bag/bag-issues/*` | 무료용 불출 (등록/취소, 재고 연동) |
|
||||||
|
| `/bag/reports/sales-ledger` | 판매 대장 (일자별/기간별) |
|
||||||
|
| `/bag/reports/daily-summary` | 일계표 (일계 + 월간 누계) |
|
||||||
|
| `/bag/reports/period-sales` | 기간별 판매현황 |
|
||||||
|
| `/bag/reports/supply-demand` | 봉투 수불 현황 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## DB 테이블
|
## 모델 (25개)
|
||||||
|
|
||||||
| 테이블 | 용도 |
|
| 모델 | 테이블 | 용도 |
|
||||||
|--------|------|
|
|------|--------|------|
|
||||||
| `member` | 회원 (mb_id, mb_level, mb_state, PII 암호화) |
|
| MemberModel | member | 회원 계정 |
|
||||||
| `member_log` | 로그인/로그아웃 감사 로그 (IP, User-Agent) |
|
| MemberLogModel | member_log | 로그인 이력 |
|
||||||
| `member_approval_request` | 회원가입 승인 요청 (pending/approved/rejected) |
|
| MemberApprovalRequestModel | member_approval_request | 승인 요청 |
|
||||||
| `local_government` | 지자체 (테넌트 루트) |
|
| LocalGovernmentModel | local_government | 지자체 |
|
||||||
| `designated_shop` | 지정판매소 (지자체별 관리) |
|
| DesignatedShopModel | designated_shop | 지정판매소 |
|
||||||
| `menu_type` | 메뉴 유형 (admin, site) |
|
| MenuModel | menu | 메뉴 항목 |
|
||||||
| `menu` | 메뉴 항목 (트리 구조, 역할별 노출) |
|
| MenuTypeModel | menu_type | 메뉴 유형 |
|
||||||
|
| CodeKindModel | code_kind | 코드 종류 |
|
||||||
|
| CodeDetailModel | code_detail | 세부코드 |
|
||||||
|
| BagPriceModel | bag_price | 봉투 단가 |
|
||||||
|
| BagPriceHistoryModel | bag_price_history | 단가 변경 이력 |
|
||||||
|
| PackagingUnitModel | packaging_unit | 포장 단위 |
|
||||||
|
| PackagingUnitHistoryModel | packaging_unit_history | 포장 단위 이력 |
|
||||||
|
| SalesAgencyModel | sales_agency | 판매 대행소 |
|
||||||
|
| CompanyModel | company | 업체 |
|
||||||
|
| ManagerModel | manager | 담당자 |
|
||||||
|
| FreeRecipientModel | free_recipient | 무료 대상자 |
|
||||||
|
| BagOrderModel | bag_order | 발주 |
|
||||||
|
| BagOrderItemModel | bag_order_item | 발주 품목 |
|
||||||
|
| BagReceivingModel | bag_receiving | 입고 |
|
||||||
|
| BagInventoryModel | bag_inventory | 재고 |
|
||||||
|
| BagSaleModel | bag_sale | 판매/반품 |
|
||||||
|
| ShopOrderModel | shop_order | 주문 접수 |
|
||||||
|
| ShopOrderItemModel | shop_order_item | 주문 품목 |
|
||||||
|
| BagIssueModel | bag_issue | 무료 불출 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 보안
|
||||||
|
|
||||||
|
| 항목 | 구현 |
|
||||||
|
|------|------|
|
||||||
|
| 인증 | 세션 기반 로그인, AdminAuthFilter로 관리자 접근 제어, 기본코드 목록 경로는 LoginAuthFilter(`loginAuth`) |
|
||||||
|
| RBAC | 4단계 역할 (Config\Roles), 메뉴별 역할 노출 |
|
||||||
|
| PII 암호화 | `pii_encryption_helper` (AES, `ENC:` prefix) - 이메일/전화번호 |
|
||||||
|
| 비밀번호 | `password_hash()` + `password_verify()` (bcrypt) |
|
||||||
|
| 로그인 보호 | 5회 실패 시 계정 잠금 (mb_login_fail_count, mb_locked_until) |
|
||||||
|
| CSRF | CodeIgniter 내장 CSRF 필터 |
|
||||||
|
| 멀티테넌시 | lg_idx 기반 데이터 격리, 세션에서 테넌트 관리 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 빠른 시작
|
||||||
|
|
||||||
|
### 1) 저장소 복제 및 의존성 설치
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/wixon-associates/jongryangje.git
|
||||||
|
cd jongryangje
|
||||||
|
composer install
|
||||||
|
npm install # Playwright E2E 테스트용
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2) 환경 설정
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env .env
|
||||||
|
```
|
||||||
|
|
||||||
|
`.env`에서 설정:
|
||||||
|
|
||||||
|
| 항목 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| `app.baseURL` | 예: `http://localhost:8045/` |
|
||||||
|
| `database.default.*` | DB 호스트/DB명/사용자/비밀번호 |
|
||||||
|
| `encryption.key` | PII 암호화용 64자리 hex |
|
||||||
|
|
||||||
|
### 3) 데이터베이스 준비
|
||||||
|
|
||||||
|
SQL 스크립트 실행 순서:
|
||||||
|
|
||||||
|
| 순서 | 파일 | 용도 |
|
||||||
|
|------|------|------|
|
||||||
|
| 1 | `init_jongryangje_dev.sql` | DB/사용자 생성 |
|
||||||
|
| 2 | `login_tables.sql` | 회원/로그인/지자체/지정판매소 테이블 |
|
||||||
|
| 3 | `member_approval_request_add.sql` | 승인 요청 테이블 |
|
||||||
|
| 4 | `member_login_lock_add.sql` | 로그인 잠금 컬럼 |
|
||||||
|
| 5 | `menu_tables.sql` | 메뉴 시스템 + 시드 |
|
||||||
|
| 6 | `menu_type_add_site.sql` | 사이트 메뉴 타입 |
|
||||||
|
| 7 | `local_government_init_daegu.sql` | 대구 8개 구군 지자체 |
|
||||||
|
| 8 | `code_master_init_daegu.sql` | 기본코드 마스터 (20종) |
|
||||||
|
| 9 | `bag_price_tables.sql` | 단가 + 단가 이력 테이블 |
|
||||||
|
| 10 | `packaging_unit_tables.sql` | 포장 단위 + 이력 테이블 |
|
||||||
|
| 11 | `sales_agency_tables.sql` | 판매 대행소 테이블 |
|
||||||
|
| 12 | `manager_tables.sql` | 담당자 테이블 |
|
||||||
|
| 13 | `company_tables.sql` | 업체 테이블 |
|
||||||
|
| 14 | `free_recipient_tables.sql` | 무료 대상자 테이블 |
|
||||||
|
| 15 | `order_tables.sql` | 발주/발주품목 테이블 |
|
||||||
|
| 16 | `sales_tables.sql` | 판매/입고/재고/불출/주문 테이블 |
|
||||||
|
| 17 | `seed_test_accounts.sql` | 테스터 계정 4개 |
|
||||||
|
| 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) 개발 서버 실행
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php spark serve --port=8045
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5) 시드 데이터 (선택)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node e2e/helpers/db-seed.js # 테스터 계정 생성
|
||||||
|
node e2e/helpers/db-seed-realistic.js # 실제형 시범 데이터
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E2E 테스트 (Playwright)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 전체 테스트
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# headed 모드 (브라우저 표시)
|
||||||
|
npm run test:headed
|
||||||
|
|
||||||
|
# 특정 파일
|
||||||
|
npx playwright test e2e/auth.spec.js
|
||||||
|
|
||||||
|
# 특정 테스트
|
||||||
|
npx playwright test -g "로그인 페이지"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 테스터 계정 (비밀번호: `test1234!`)
|
||||||
|
|
||||||
|
| ID | 역할 | Level |
|
||||||
|
|----|------|-------|
|
||||||
|
| `tester_badmin` | 본부 관리자 | 5 |
|
||||||
|
| `tester_admin` | Super Admin | 4 |
|
||||||
|
| `tester_local` | 지자체관리자 (중구청) | 3 |
|
||||||
|
| `tester_shop` | 지정판매소 | 2 |
|
||||||
|
| `tester_user` | 일반 사용자 | 1 |
|
||||||
|
|
||||||
|
### 테스트 파일 (84개 테스트)
|
||||||
|
|
||||||
|
| 파일 | 테스트 수 | 대상 |
|
||||||
|
|------|-----------|------|
|
||||||
|
| auth.spec.js | 9 | 로그인/로그아웃/회원가입 |
|
||||||
|
| admin.spec.js | 10 | 관리자 패널 접근 |
|
||||||
|
| public.spec.js | 4 | 공개 페이지 |
|
||||||
|
| bag-site.spec.js | 11 | 사이트 메뉴 /bag/* |
|
||||||
|
| code-management.spec.js | 7 | 기본코드 CRUD (`/bag/*` 목록 + `/admin/*` 폼) |
|
||||||
|
| bag-price.spec.js | 6 | 봉투 단가 |
|
||||||
|
| packaging-unit.spec.js | 3 | 포장 단위 |
|
||||||
|
| phase2-entities.spec.js | 8 | 대행소/담당자/업체/무료대상자 |
|
||||||
|
| phase2-extra.spec.js | 4 | 지자체 수정/비밀번호/로그인 lock |
|
||||||
|
| phase3-order.spec.js | 8 | 발주/입고/재고 |
|
||||||
|
| phase4-sales.spec.js | 10 | 주문/판매/불출 |
|
||||||
|
| phase5-reports.spec.js | 4 | 리포트 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 기본코드 체계
|
## 기본코드 체계
|
||||||
|
|
||||||
개발목록 엑셀의 "기본코드 종류" 시트 기준, A~Y 총 25종의 코드 체계:
|
### 코드 관리 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 | 도/특별시/광역시 구분 | N | 동판종류 |
|
| A | 도/특별시/광역시 구분 | K | 반품사유 |
|
||||||
| B | 특별시/광역시/시/군코드 | O | 봉투명 |
|
| B | 특별시/광역시/시/군코드 | L | 지정판매소 변경사유 |
|
||||||
| C | 구코드 | P | 작업권한 |
|
| C | 구코드 | M | 수불구분 |
|
||||||
| D | 동코드 | Q | 예산과목 |
|
| D | 동코드 | N | 동판종류 |
|
||||||
| E | 봉투구분 (일반/공공/무료/공동주택/재사용/음식물) | R | 은행목록 |
|
| E | 봉투구분 | O | 봉투명 (상세 봉투코드) |
|
||||||
| F | 봉투재질 (고밀도/PP마대/스티커/용기) | S | 소속 |
|
| F | 봉투재질 | P | 작업권한 |
|
||||||
| G | 용량별 (2L~120L, 1000원~10000원) | T | 직위 |
|
| G | 용량별 | Q | 예산과목 |
|
||||||
| H | 무상지급 대상 | U | 배달 |
|
| H | 무상지급 대상 | R | 은행목록 |
|
||||||
| I | 판매형태 (무상/일반/관내/교환) | V | 구역 |
|
| I | 판매형태 | S | 소속 |
|
||||||
| J | 반품형태 | W | 봉투명(약어) |
|
| J | 반품형태 | T | 직위 |
|
||||||
| K | 반품사유 | X | 봉투구분(대분) |
|
|
||||||
| L | 지정판매소 변경사유 | Y | 분기 |
|
|
||||||
| M | 수불구분 | | |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 개발 현황
|
## 개발 진행 현황
|
||||||
|
|
||||||
### 웹 기능목록 (63개 항목)
|
### Phase별 완료 현황
|
||||||
|
|
||||||
기획 문서(`assets/종량제_개발목록_20260127.xlsx`)의 "웹 기능목록" 시트 기준으로 정리합니다.
|
| Phase | 내용 | 상태 |
|
||||||
|
|-------|------|------|
|
||||||
|
| Phase 1 | 프로젝트 초기 세팅, 로그인/회원가입, RBAC, 멀티테넌시, 메뉴 관리, PII 암호화 | **완료** |
|
||||||
|
| Phase 2 | 기본정보관리 (코드/단가/포장/대행소/담당자/업체/무료대상자/지자체수정/비밀번호/로그인lock) | **완료** |
|
||||||
|
| Phase 3 | 발주/입고/재고 (발주등록/LOT/취소/삭제/현황/입고처리/재고현황) | **완료** |
|
||||||
|
| Phase 4 | 주문/판매/불출 (주문접수/판매/반품/불출처리/취소) | **완료** |
|
||||||
|
| Phase 5 | 리포트 (판매대장/일계표/기간별현황/수불현황) | **완료** |
|
||||||
|
| Phase 6 | 모바일앱 + 고급기능 (바코드/통계/엑셀/인쇄) | 대기 |
|
||||||
|
|
||||||
#### 구현 완료
|
### Phase 6 이후 대기 작업
|
||||||
|
|
||||||
| No | 카테고리 | 기능 | 상태 | 비고 |
|
- 지정판매소 다조건 조회 + 엑셀 + 인쇄 + 바코드 출력
|
||||||
|----|---------|------|------|------|
|
- 지정판매소 지도 표시 / 현황 (신규/취소)
|
||||||
| 1 | 공통 | 로깅 (로그인/로그아웃 이력) | **완료** | `member_log` 테이블, IP/UA 기록 |
|
- 카카오 주소 검색 API 연동
|
||||||
| 2 | 공통 | 개인정보 비식별화 | **완료** | `pii_encryption_helper` (ENC: prefix, AES 암호화) |
|
- 년 판매 현황 (월별/분기별)
|
||||||
| 3 | 공통 | 로그인 | **부분** | 로그인/세션/역할별 리다이렉트 완료. 2차 인증 미구현, 5회 실패 lock 미구현 |
|
- 지정판매소별 판매현황
|
||||||
| 4 | 관리자단 | 사용자 권한 관리 | **완료** | 4단계 RBAC, Config 기반 |
|
- 홈택스 세금계산서 엑셀 생성
|
||||||
| 5 | 관리자단 | 사용자 관리 | **완료** | Full CRUD + soft delete + PII 암복호화 |
|
- 반품/파기 현황, LOT 수불 조회
|
||||||
| 6 | 관리자단 | 사용자 로그인 이력 확인 | **완료** | 기간 지정 조회 구현 |
|
- 바코드 스캐너 연동 (Electron + serialport)
|
||||||
| 7 | 관리자단 | 사용자 권한 승인 | **완료** | 승인/거절 + 사유 입력 + 트랜잭션 처리 |
|
- 실사 선별/등록/조회
|
||||||
| 8 | 관리자단 | 메뉴 관리 | **완료** | 트리 구조 CRUD, 지자체별 메뉴 복사 |
|
- 페이지네이션/엑셀/인쇄 공통 컴포넌트
|
||||||
| 9 | 관리자단 | 메뉴 별 권한 설정 | **완료** | `mm_level` 필드로 역할별 노출 제어 |
|
- CRUD 로깅 (전체 데이터 변경 이력)
|
||||||
| 25 | 기본정보관리 | 지정판매소 관리 (리스트/상세) | **완료** | 지자체별 필터링, 판매소 정보 표시 |
|
- 2차 인증 적용
|
||||||
| 26 | 기본정보관리 | 지정판매소 등록/수정/삭제 | **완료** | Full CRUD, 판매소번호 자동생성 |
|
- 대시보드 실 데이터 연동
|
||||||
| 29 | 기본정보관리 | PASSWORD 변경 | **미구현** | |
|
- 모바일앱 (15개 기능)
|
||||||
|
|
||||||
#### 미구현 — 기본정보관리 (SFR-PWB-003)
|
|
||||||
|
|
||||||
| No | 기능 | 설명 |
|
|
||||||
|----|------|------|
|
|
||||||
| 10-11 | 기본코드 관리 | 코드 종류 및 하위 세부코드 CRUD |
|
|
||||||
| 12-13 | 단가 관리 | 지자체별 봉투 종류별 단가 CRUD, 이력 관리, 기간별 조회 |
|
|
||||||
| 14-15 | 포장 단위 관리 | 박스당 팩/팩당 낱장 수량 CRUD, 기간별 조회 |
|
|
||||||
| 16-18 | 판매 대행소 관리 | 대행소 CRUD, 지자체 연결, 조회 |
|
|
||||||
| 19-20 | 담당자 관리 | 지자체별 담당자 CRUD (소속: 구/군/대행소/제작업체) |
|
|
||||||
| 21-22 | 업체 관리 | 협회/제작업체/회수업체 CRUD, 조회/인쇄 |
|
|
||||||
| 23-24 | 무료용 대상자 관리 | 읍면동/무료대상자/기타 구분별 CRUD |
|
|
||||||
| 27 | 지정판매소 지도 | 지정판매소 지도상 위치 확인 |
|
|
||||||
| 28 | 지정판매소 조회 | 다조건 조회, 엑셀 저장, 인쇄, 바코드 출력 |
|
|
||||||
| 29 | 지정판매소 현황 | 연도별 신규등록/취소 현황 조회 |
|
|
||||||
| 29 | PASSWORD 변경 | 현재 로그인 사용자 비밀번호 변경 |
|
|
||||||
|
|
||||||
#### 미구현 — 발주 입고 관리 (SFR-PWB-004)
|
|
||||||
|
|
||||||
| No | 기능 | 설명 |
|
|
||||||
|----|------|------|
|
|
||||||
| 30 | 발주 등록 | 발주 form (UUID v4 + SHA-256 해싱 + 블록 저장), LOT 번호 생성 |
|
|
||||||
| 31 | LOT번호 및 바코드 생성 | AES-256 + RSA 암호화 seed → PDF417 바코드 생성 |
|
|
||||||
| 32 | 발주 변경 | 동일 UUID 버전 관리, 해시 체인 |
|
|
||||||
| 33 | 발주 삭제 | 상태 변경 방식 삭제 |
|
|
||||||
| 34 | 발주 현황 | 기간/제작업체/품명/입고처 조건 조회, 리포트 |
|
|
||||||
| 35 | 발주 입고 (스캐너) | 바코드 스캐너 연동 (Electron + serialport) |
|
|
||||||
| 36 | 발주 입고 (스캐너) | 제작업체별 미입고 발주 조회, 스캔 입고 처리 |
|
|
||||||
| 37 | 일괄 입고 | LOT 단위 전체 입고 처리 |
|
|
||||||
| 38 | 입고 현황 | 기간/업체/품명/구분별 조회, 리포트 |
|
|
||||||
|
|
||||||
#### 미구현 — 불출 관리 (SFR-PWB-005)
|
|
||||||
|
|
||||||
| No | 기능 | 설명 |
|
|
||||||
|----|------|------|
|
|
||||||
| 39 | 무료용 불출 현황 | 기간별 봉투 종류별 불출 현황 조회 |
|
|
||||||
| 40 | 무료용 불출 처리 | 불출 기록, 바코드 스캔, 재고 감산, 판매 처리 |
|
|
||||||
| 41 | 무료용 불출 취소 | 불출 리스트/품목/코드 3분할 화면, 취소 후 재고 복원 |
|
|
||||||
|
|
||||||
#### 미구현 — 재고/실사 관리 (SFR-PWB-006~007)
|
|
||||||
|
|
||||||
| No | 기능 | 설명 |
|
|
||||||
|----|------|------|
|
|
||||||
| 42 | 재고 조회 | 기준일자 봉투/스티커 종류별 재고량, 결재란 인쇄 |
|
|
||||||
| 43 | 실사 선별 | 바코드 있는 봉투 대상 실사 실시 |
|
|
||||||
| 44 | 실사 선별 조회 | 전체→박스→팩→낱장 drill-down 조회 |
|
|
||||||
|
|
||||||
#### 미구현 — 주문/판매 관리 (SFR-PWB-008)
|
|
||||||
|
|
||||||
| No | 기능 | 설명 |
|
|
||||||
|----|------|------|
|
|
||||||
| 45 | 주문 접수 관리 메인 | 접수 리스트, 상세, 전화 주문 접수 3분할 화면 |
|
|
||||||
| 46 | 전화 주문 접수 | 판매소 자동완성 검색, 가상계좌 안내, 포장단위 자동 계산 |
|
|
||||||
| 47 | 전화 접수 수정/취소 | 접수량 수정, 상태 변경 방식 취소 |
|
|
||||||
| 48 | 지정판매소 판매 | 바코드 스캔 판매 처리, 중복 스캔 방지 |
|
|
||||||
| 49 | 지정판매소 판매 취소 | 품목별/봉투코드별 선택 취소 |
|
|
||||||
| 50 | 지정판매소 반품 | 바코드 스캔 반품 처리 |
|
|
||||||
| 51 | 지정판매소 반품 취소 | 반품 취소 → 판매 상태 복원 |
|
|
||||||
|
|
||||||
#### 미구현 — 판매 현황 (SFR-PWB-009)
|
|
||||||
|
|
||||||
| No | 기능 | 설명 |
|
|
||||||
|----|------|------|
|
|
||||||
| 52 | 판매 대장 | 일자별/기간별 집계, 수수료/총액, 결재란 인쇄 |
|
|
||||||
| 53 | 일계표 | 일계 + 월간 누계 (판매수량/금액/수수료/징수액) |
|
|
||||||
| 54 | 기간별 판매현황 | 일자별/기간별 판매+반품+계, 봉투계/스티커계 소계 |
|
|
||||||
| 55 | 년 판매 현황 | 월별/분기별 품목별 판매 |
|
|
||||||
| 56 | 지정판매소별 판매현황 | 읍면동별, 수량/금액 집계, 1~12월 컬럼 |
|
|
||||||
| 57 | 홈택스 처리 | 세금계산서 일괄발급 엑셀 양식 생성 |
|
|
||||||
|
|
||||||
#### 미구현 — 봉투 수불 관리 (SFR-PWB-010)
|
|
||||||
|
|
||||||
| No | 기능 | 설명 |
|
|
||||||
|----|------|------|
|
|
||||||
| 58 | 기타 입출고 | 상세 기능 확인 필요 |
|
|
||||||
| 59 | 봉투 수불 현황 | 전일재고/입고/출고/잔량, 일자별/기간별 |
|
|
||||||
| 60 | 반품/파기 현황 | 기간별 입출고 구분 조회 |
|
|
||||||
| 61 | 봉투 수급 계획 | 기능 확인 필요 (추가 발주 예정일 산출 방식 불명확) |
|
|
||||||
| 62 | LOT 수불 조회 | 바코드 스캔으로 개별 봉투 수불 이력 조회 |
|
|
||||||
|
|
||||||
#### 미구현 — 봉투 스캔 (SFR-PWB-011)
|
|
||||||
|
|
||||||
| No | 기능 | 설명 |
|
|
||||||
|----|------|------|
|
|
||||||
| 63 | 봉투 스캔 현황 | 앱 바코드 스캔 횟수 확인, 경위도 위치 지도 표시 |
|
|
||||||
|
|
||||||
### 모바일앱 기능목록 (15개 항목) — 전체 미구현
|
|
||||||
|
|
||||||
| No | 카테고리 | 기능 |
|
|
||||||
|----|---------|------|
|
|
||||||
| 1-3 | 공통 | 로깅, 개인정보 비식별화, 로그인 (2차인증) |
|
|
||||||
| 4 | 발주 입고 | PDF417 스캐너 연동 입고 처리 |
|
|
||||||
| 5-6 | 불출 관리 | 무료용/공공용 불출 처리/취소 |
|
|
||||||
| 7-8 | 판매 관리 | 지정판매소 판매/판매취소 |
|
|
||||||
| 9-10 | 판매 관리 | 지정판매소 반품/반품취소 |
|
|
||||||
| 11 | 봉투 수불 | LOT 수불 조회 |
|
|
||||||
| 12-14 | 봉투 주문 | 주문 내역/주문/주문 수정·취소 |
|
|
||||||
| 15 | 정품 인증 | PDF417 스캐너 봉투 정품 인증 |
|
|
||||||
|
|
||||||
### 전체 메뉴 구조
|
|
||||||
|
|
||||||
개발목록 엑셀의 "전체 메뉴" 시트 기준, 10개 대메뉴:
|
|
||||||
|
|
||||||
```
|
|
||||||
기본정보관리 ─ 기본코드, 단가, 포장단위, 대행소, 담당자, 업체, 무료대상자, 지정판매소, 환경설정
|
|
||||||
발주 입고 관리 ─ 발주 등록/변경, LOT, 발주현황, 입고(스캐너/일괄), 입고현황
|
|
||||||
불출 관리 ─ 무료 불출 현황/처리/취소
|
|
||||||
재고 관리 ─ 재고현황, 실사(선별/등록/조회/오류/취소)
|
|
||||||
판매 관리 ─ 전화접수, 판매소 판매/반품/취소
|
|
||||||
판매 현황 ─ 판매대장, 일계표, 기간별/년/판매소별 현황, 홈택스
|
|
||||||
봉투 수불 관리 ─ 기타 입출고, 수불현황, 반품/파기, 수급계획, LOT 수불
|
|
||||||
통계 분석 관리 ─ 전년대비, 월별/계절별 추이 분석
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 개발 진행 요약
|
## SQL 스크립트 목록 (writable/database/)
|
||||||
|
|
||||||
| 구분 | 항목 수 | 완료 | 부분완료 | 미구현 |
|
| 파일 | 용도 |
|
||||||
|------|---------|------|---------|--------|
|
|------|------|
|
||||||
| 공통 | 3 | 2 | 1 | 0 |
|
| `init_jongryangje_dev.sql` | DB/사용자 생성 |
|
||||||
| 관리자단 | 6 | 6 | 0 | 0 |
|
| `login_tables.sql` | member, member_log, local_government, designated_shop |
|
||||||
| 기본정보관리 | 20 | 2 | 0 | 18 |
|
| `member_approval_request_add.sql` | 승인 요청 테이블 |
|
||||||
| 발주 입고 관리 | 9 | 0 | 0 | 9 |
|
| `member_login_lock_add.sql` | 로그인 실패 잠금 컬럼 |
|
||||||
| 불출 관리 | 3 | 0 | 0 | 3 |
|
| `menu_tables.sql` | menu_type, menu + admin/site 시드 |
|
||||||
| 재고/실사 관리 | 3 | 0 | 0 | 3 |
|
| `menu_type_add_site.sql` | 사이트 메뉴 타입 추가 |
|
||||||
| 주문/판매 관리 | 7 | 0 | 0 | 7 |
|
| `menu_add_lg_idx.sql` | 메뉴에 지자체 컬럼 추가 |
|
||||||
| 판매 현황 | 6 | 0 | 0 | 6 |
|
| `menu_site_seed_from_csv.sql` | 사이트 네비게이션 시드 |
|
||||||
| 봉투 수불 관리 | 5 | 0 | 0 | 5 |
|
| `local_government_init_daegu.sql` | 대구 8개 구군 지자체 |
|
||||||
| 봉투 스캔 | 1 | 0 | 0 | 1 |
|
| `code_master_init_daegu.sql` | 기본코드 20종 + 세부코드 |
|
||||||
| **웹 합계** | **63** | **10** | **1** | **52** |
|
| `menu_fix_basic_code_link.sql` | 사이트 메뉴에서 기본코드 링크 보정 (기존 DB용, 선택) |
|
||||||
| 모바일앱 | 15 | 0 | 0 | 15 |
|
| `menu_site_add_basic_code_child.sql` | 사이트 메뉴에 기본코드 하위 항목 (선택) |
|
||||||
| **전체 합계** | **78** | **10** | **1** | **67** |
|
| `code_master_sync_from_csv.sql` | CSV 기준 기본코드 보강 (선택) |
|
||||||
|
| `bag_price_tables.sql` | bag_price, bag_price_history |
|
||||||
> **현재 진척율: 약 13% (웹 기준 약 16%)**
|
| `packaging_unit_tables.sql` | packaging_unit, packaging_unit_history |
|
||||||
|
| `sales_agency_tables.sql` | sales_agency |
|
||||||
---
|
| `manager_tables.sql` | manager |
|
||||||
|
| `company_tables.sql` | company |
|
||||||
## 향후 개발 로드맵
|
| `free_recipient_tables.sql` | free_recipient |
|
||||||
|
| `order_tables.sql` | bag_order, bag_order_item |
|
||||||
### Phase 2 — 기본정보관리 완성
|
| `sales_tables.sql` | bag_sale, bag_receiving, bag_inventory, bag_issue, shop_order, shop_order_item |
|
||||||
|
| `seed_test_accounts.sql` | 테스터 계정 4개 |
|
||||||
- [ ] 기본코드 종류/세부코드 관리 (CRUD)
|
| `seed_realistic_data.sql` | 실제형 시범 데이터 (대구 남구청 기준) |
|
||||||
- [ ] 지자체별 봉투 단가 관리 + 이력 관리
|
| `fix_double_encoding.sql` | UTF-8 이중인코딩 수정 |
|
||||||
- [ ] 포장 단위 관리 (박스/팩/낱장 체계)
|
|
||||||
- [ ] 판매 대행소 관리 + 지자체 연결
|
|
||||||
- [ ] 담당자 관리, 업체 관리 (협회/제작업체/회수업체)
|
|
||||||
- [ ] 무료용 대상자 관리
|
|
||||||
- [ ] 지정판매소 다조건 조회 + 엑셀 저장 + 인쇄 + 바코드 출력
|
|
||||||
- [ ] 지정판매소 현황 (신규/취소) + 지도 표시
|
|
||||||
- [ ] 지자체 수정/삭제 기능
|
|
||||||
- [ ] PASSWORD 변경 기능
|
|
||||||
- [ ] 로그인 5회 실패 lock 처리
|
|
||||||
|
|
||||||
### Phase 3 — 발주·입고·재고 핵심
|
|
||||||
|
|
||||||
- [ ] 발주 등록 (UUID v4 + SHA-256 해싱 + 블록)
|
|
||||||
- [ ] LOT 번호 생성 + PDF417 바코드 (AES-256 + RSA)
|
|
||||||
- [ ] 발주 변경/삭제 (버전 관리)
|
|
||||||
- [ ] 발주 현황 리포트
|
|
||||||
- [ ] 발주 입고 처리 (바코드 스캐너 / 일괄 입고)
|
|
||||||
- [ ] 재고 조회 + 결재란 인쇄
|
|
||||||
- [ ] 실사 선별 / 등록 / 조회
|
|
||||||
|
|
||||||
### Phase 4 — 주문·판매·불출
|
|
||||||
|
|
||||||
- [ ] 전화 주문 접수 + 자동완성 검색
|
|
||||||
- [ ] 주문 수정/취소
|
|
||||||
- [ ] 지정판매소 판매 처리 (바코드 스캔)
|
|
||||||
- [ ] 판매 취소, 반품, 반품 취소
|
|
||||||
- [ ] 무료용 불출 현황/처리/취소
|
|
||||||
|
|
||||||
### Phase 5 — 현황·리포트·수불
|
|
||||||
|
|
||||||
- [ ] 판매 대장 (일자별/기간별)
|
|
||||||
- [ ] 일계표, 기간별 판매현황, 년 판매 현황
|
|
||||||
- [ ] 지정판매소별 판매현황 (수량/금액)
|
|
||||||
- [ ] 홈택스 세금계산서 엑셀 생성
|
|
||||||
- [ ] 봉투 수불 현황 (전일재고/입고/출고/잔량)
|
|
||||||
- [ ] 반품/파기 현황, LOT 수불 조회
|
|
||||||
|
|
||||||
### Phase 6 — 모바일앱
|
|
||||||
|
|
||||||
- [ ] 앱 공통 (로그인, 2차인증, PII)
|
|
||||||
- [ ] 발주 입고 (PDF417 카메라 스캔)
|
|
||||||
- [ ] 불출 관리 (카메라 스캔)
|
|
||||||
- [ ] 판매/반품 처리 (카메라 스캔)
|
|
||||||
- [ ] 봉투 주문/주문 수정·취소
|
|
||||||
- [ ] 봉투 정품 인증
|
|
||||||
|
|
||||||
### 공통 기술 과제
|
|
||||||
|
|
||||||
- [ ] 2차 인증 적용
|
|
||||||
- [ ] 페이지네이션 구현
|
|
||||||
- [ ] 엑셀 저장/인쇄 공통 컴포넌트
|
|
||||||
- [ ] 바코드 스캐너 연동 (Electron + serialport)
|
|
||||||
- [ ] 카카오 주소 검색 API 연동
|
|
||||||
- [ ] CRUD 로깅 (전체 데이터 변경 이력)
|
|
||||||
- [ ] 대시보드 실 데이터 연동
|
|
||||||
|
|||||||
@@ -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 = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
|
|||||||
54
app/Config/Auth.php
Normal file
54
app/Config/Auth.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Config;
|
||||||
|
|
||||||
|
use CodeIgniter\Config\BaseConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인·2차 인증(TOTP) 관련 설정
|
||||||
|
*
|
||||||
|
* .env 의 auth.requireTotp 가 Config 기본값보다 우선합니다. 끄려면 반드시 false 로 두세요.
|
||||||
|
* 예:
|
||||||
|
* auth.requireTotp = false
|
||||||
|
* auth.requireTotp = true # 운영에서 2FA 켤 때
|
||||||
|
* auth.totpIssuer = "종량제 시스템"
|
||||||
|
*/
|
||||||
|
class Auth extends BaseConfig
|
||||||
|
{
|
||||||
|
/** false 이면 로그인 시 TOTP·등록 유도 없음. 운영에서 켤 때 .env 에 auth.requireTotp = true */
|
||||||
|
public bool $requireTotp = false;
|
||||||
|
|
||||||
|
/** 인증 앱에 표시되는 발급자(issuer) */
|
||||||
|
public string $totpIssuer = '종량제 시스템';
|
||||||
|
|
||||||
|
/** TOTP 연속 실패 시 세션 종료 전 허용 횟수 */
|
||||||
|
public int $totpMaxAttempts = 5;
|
||||||
|
|
||||||
|
/** 비밀번호 통과 후 2단계 완료까지 허용 시간(초) */
|
||||||
|
public int $pending2faTtlSeconds = 600;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$require = env('auth.requireTotp');
|
||||||
|
if ($require !== null && $require !== '') {
|
||||||
|
$this->requireTotp = filter_var($require, FILTER_VALIDATE_BOOLEAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
$issuer = env('auth.totpIssuer');
|
||||||
|
if (is_string($issuer) && $issuer !== '') {
|
||||||
|
$this->totpIssuer = $issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
$max = env('auth.totpMaxAttempts');
|
||||||
|
if ($max !== null && $max !== '' && is_numeric($max)) {
|
||||||
|
$this->totpMaxAttempts = max(1, (int) $max);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ttl = env('auth.pending2faTtlSeconds');
|
||||||
|
if ($ttl !== null && $ttl !== '' && is_numeric($ttl)) {
|
||||||
|
$this->pending2faTtlSeconds = max(60, (int) $ttl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ class Pager extends BaseConfig
|
|||||||
* @var array<string, string>
|
* @var array<string, string>
|
||||||
*/
|
*/
|
||||||
public array $templates = [
|
public array $templates = [
|
||||||
'default_full' => 'CodeIgniter\Pager\Views\default_full',
|
'default_full' => 'App\Views\components\pager',
|
||||||
'default_simple' => 'CodeIgniter\Pager\Views\default_simple',
|
'default_simple' => 'App\Views\components\pager',
|
||||||
'default_head' => 'CodeIgniter\Pager\Views\default_head',
|
'default_head' => 'CodeIgniter\Pager\Views\default_head',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class Roles extends BaseConfig
|
|||||||
* mb_level 상수 (member.mb_level)
|
* mb_level 상수 (member.mb_level)
|
||||||
*/
|
*/
|
||||||
public const LEVEL_SUPER_ADMIN = 4;
|
public const LEVEL_SUPER_ADMIN = 4;
|
||||||
|
/** 본부 관리자 — 현재는 super admin과 동일한 관리자 권한(지자체 선택 후 작업). 추후 super 전용 기능 분리 시 여기만 조정 */
|
||||||
|
public const LEVEL_HEADQUARTERS_ADMIN = 5;
|
||||||
public const LEVEL_LOCAL_ADMIN = 3; // 지자체관리자
|
public const LEVEL_LOCAL_ADMIN = 3; // 지자체관리자
|
||||||
public const LEVEL_SHOP = 2; // 지정판매소
|
public const LEVEL_SHOP = 2; // 지정판매소
|
||||||
public const LEVEL_CITIZEN = 1; // 일반 사용자(시민)
|
public const LEVEL_CITIZEN = 1; // 일반 사용자(시민)
|
||||||
@@ -29,8 +31,73 @@ class Roles extends BaseConfig
|
|||||||
self::LEVEL_SHOP => '지정판매소',
|
self::LEVEL_SHOP => '지정판매소',
|
||||||
self::LEVEL_LOCAL_ADMIN => '지자체관리자',
|
self::LEVEL_LOCAL_ADMIN => '지자체관리자',
|
||||||
self::LEVEL_SUPER_ADMIN => 'super admin',
|
self::LEVEL_SUPER_ADMIN => 'super admin',
|
||||||
|
self::LEVEL_HEADQUARTERS_ADMIN => '본부 관리자',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* super admin(4) 또는 본부 관리자(5) — 동일 관리자 UX(지자체 선택 등)에 사용
|
||||||
|
*/
|
||||||
|
public static function isSuperAdminEquivalent(int $level): bool
|
||||||
|
{
|
||||||
|
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·본부 관리자)
|
||||||
|
*/
|
||||||
|
public static function requiresTotp(int $level): bool
|
||||||
|
{
|
||||||
|
return $level === self::LEVEL_LOCAL_ADMIN
|
||||||
|
|| $level === self::LEVEL_SUPER_ADMIN
|
||||||
|
|| $level === self::LEVEL_HEADQUARTERS_ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 자체 회원가입 시 기본 역할 (mb_level)
|
* 자체 회원가입 시 기본 역할 (mb_level)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -11,12 +11,155 @@ $routes->get('dashboard/classic-mock', 'Home::dashboardClassicMock');
|
|||||||
$routes->get('dashboard/modern', 'Home::dashboardModern');
|
$routes->get('dashboard/modern', 'Home::dashboardModern');
|
||||||
$routes->get('dashboard/dense', 'Home::dashboardDense');
|
$routes->get('dashboard/dense', 'Home::dashboardDense');
|
||||||
$routes->get('dashboard/charts', 'Home::dashboardCharts');
|
$routes->get('dashboard/charts', 'Home::dashboardCharts');
|
||||||
|
$routes->get('dashboard/blend', 'Home::dashboardBlend');
|
||||||
$routes->get('bag/inventory-inquiry', 'Home::inventoryInquiry');
|
$routes->get('bag/inventory-inquiry', 'Home::inventoryInquiry');
|
||||||
$routes->get('bag/waste-suibal-enterprise', 'Home::wasteSuibalEnterprise');
|
$routes->get('bag/waste-suibal-enterprise', 'Home::wasteSuibalEnterprise');
|
||||||
|
|
||||||
|
// 사이트 메뉴 (/bag/*)
|
||||||
|
$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/issue', 'Bag::issue');
|
||||||
|
$routes->get('bag/inventory', 'Bag::inventory');
|
||||||
|
$routes->get('bag/sales', 'Bag::sales');
|
||||||
|
$routes->get('bag/sales-stats', 'Bag::salesStats');
|
||||||
|
$routes->get('bag/flow', 'Bag::flow');
|
||||||
|
$routes->get('bag/analytics', 'Bag::analytics');
|
||||||
|
$routes->get('bag/window', 'Bag::window');
|
||||||
|
$routes->get('bag/help', 'Bag::help');
|
||||||
|
|
||||||
|
// 사이트 메뉴 CRUD (사이트 레이아웃)
|
||||||
|
$routes->get('bag/inventory/adjust', 'Bag::inventoryAdjust');
|
||||||
|
$routes->post('bag/inventory/adjust', 'Bag::inventoryAdjustStore');
|
||||||
|
$routes->get('bag/issue/create', 'Bag::issueCreate');
|
||||||
|
$routes->post('bag/issue/store', 'Bag::issueStore');
|
||||||
|
$routes->post('bag/issue/cancel/(:num)', 'Bag::issueCancel/$1');
|
||||||
|
$routes->get('bag/order/create', 'Bag::orderCreate');
|
||||||
|
$routes->post('bag/order/store', 'Bag::orderStore');
|
||||||
|
$routes->post('bag/order/cancel/(:num)', 'Bag::orderCancel/$1');
|
||||||
|
$routes->get('bag/receiving/create', 'Bag::receivingCreate');
|
||||||
|
$routes->post('bag/receiving/store', 'Bag::receivingStore');
|
||||||
|
$routes->get('bag/sale/create', 'Bag::saleCreate');
|
||||||
|
$routes->post('bag/sale/store', 'Bag::saleStore');
|
||||||
|
$routes->get('bag/shop-order/create', 'Bag::shopOrderCreate');
|
||||||
|
$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', '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');
|
||||||
|
|
||||||
|
$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->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');
|
||||||
|
$routes->get('login/two-factor', 'Auth::showTwoFactor');
|
||||||
|
$routes->post('login/two-factor', 'Auth::verifyTwoFactor');
|
||||||
|
$routes->get('login/totp-setup', 'Auth::showTotpSetup');
|
||||||
|
$routes->post('login/totp-setup', 'Auth::completeTotpSetup');
|
||||||
$routes->get('logout', 'Auth::logout');
|
$routes->get('logout', 'Auth::logout');
|
||||||
$routes->get('register', 'Auth::showRegisterForm');
|
$routes->get('register', 'Auth::showRegisterForm');
|
||||||
$routes->post('register', 'Auth::register');
|
$routes->post('register', 'Auth::register');
|
||||||
@@ -31,6 +174,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');
|
||||||
@@ -48,9 +192,11 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo
|
|||||||
$routes->get('local-governments', 'Admin\LocalGovernment::index');
|
$routes->get('local-governments', 'Admin\LocalGovernment::index');
|
||||||
$routes->get('local-governments/create', 'Admin\LocalGovernment::create');
|
$routes->get('local-governments/create', 'Admin\LocalGovernment::create');
|
||||||
$routes->post('local-governments/store', 'Admin\LocalGovernment::store');
|
$routes->post('local-governments/store', 'Admin\LocalGovernment::store');
|
||||||
|
$routes->get('local-governments/edit/(:num)', 'Admin\LocalGovernment::edit/$1');
|
||||||
|
$routes->post('local-governments/update/(:num)', 'Admin\LocalGovernment::update/$1');
|
||||||
|
$routes->post('local-governments/delete/(:num)', 'Admin\LocalGovernment::delete/$1');
|
||||||
|
|
||||||
// 기본코드 종류 관리 (P2-01)
|
// 기본코드 종류 관리 (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');
|
||||||
@@ -58,67 +204,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',
|
||||||
// 포장 단위 관리 (P2-05/06)
|
'bag-receivings',
|
||||||
$routes->get('packaging-units', 'Admin\PackagingUnit::index');
|
'bag-inventory',
|
||||||
$routes->get('packaging-units/create', 'Admin\PackagingUnit::create');
|
'shop-orders',
|
||||||
$routes->post('packaging-units/store', 'Admin\PackagingUnit::store');
|
'bag-sales',
|
||||||
$routes->get('packaging-units/edit/(:num)', 'Admin\PackagingUnit::edit/$1');
|
'bag-issues',
|
||||||
$routes->post('packaging-units/update/(:num)', 'Admin\PackagingUnit::update/$1');
|
'packaging-units',
|
||||||
$routes->post('packaging-units/delete/(:num)', 'Admin\PackagingUnit::delete/$1');
|
'reports',
|
||||||
$routes->get('packaging-units/history/(:num)', 'Admin\PackagingUnit::history/$1');
|
'password-change',
|
||||||
|
];
|
||||||
// 판매 대행소 관리 (P2-07/08)
|
foreach ($adminToBagPrefixes as $p) {
|
||||||
$routes->get('sales-agencies', 'Admin\SalesAgency::index');
|
$routes->match(['get', 'post'], $p, 'Admin\WorkMovedToBag::toBag/' . $p);
|
||||||
$routes->get('sales-agencies/create', 'Admin\SalesAgency::create');
|
$routes->match(['get', 'post'], $p . '/(:any)', 'Admin\WorkMovedToBag::toBag/' . $p . '/$1');
|
||||||
$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', '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');
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ class Access extends BaseController
|
|||||||
{
|
{
|
||||||
$start = $this->request->getGet('start');
|
$start = $this->request->getGet('start');
|
||||||
$end = $this->request->getGet('end');
|
$end = $this->request->getGet('end');
|
||||||
$builder = $this->memberLogModel->builder();
|
$builder = $this->memberLogModel;
|
||||||
$builder->select('member_log.*');
|
|
||||||
$builder->orderBy('mll_regdate', 'DESC');
|
$builder->orderBy('mll_regdate', 'DESC');
|
||||||
if ($start !== null && $start !== '') {
|
if ($start !== null && $start !== '') {
|
||||||
$builder->where('mll_regdate >=', $start . ' 00:00:00');
|
$builder->where('mll_regdate >=', $start . ' 00:00:00');
|
||||||
@@ -40,10 +39,11 @@ class Access extends BaseController
|
|||||||
if ($end !== null && $end !== '') {
|
if ($end !== null && $end !== '') {
|
||||||
$builder->where('mll_regdate <=', $end . ' 23:59:59');
|
$builder->where('mll_regdate <=', $end . ' 23:59:59');
|
||||||
}
|
}
|
||||||
$list = $builder->get()->getResult();
|
$list = $builder->paginate(20);
|
||||||
|
$pager = $this->memberLogModel->pager;
|
||||||
return view('admin/layout', [
|
return view('admin/layout', [
|
||||||
'title' => '로그인 이력',
|
'title' => '로그인 이력',
|
||||||
'content' => view('admin/access/login_history', ['list' => $list, 'start' => $start, 'end' => $end]),
|
'content' => view('admin/access/login_history', ['list' => $list, 'start' => $start, 'end' => $end, 'pager' => $pager]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,15 +59,14 @@ class Access extends BaseController
|
|||||||
$status = MemberApprovalRequestModel::STATUS_PENDING;
|
$status = MemberApprovalRequestModel::STATUS_PENDING;
|
||||||
}
|
}
|
||||||
|
|
||||||
$builder = $this->approvalModel->builder();
|
$list = $this->approvalModel
|
||||||
$builder->select(
|
->select('member_approval_request.*, member.mb_id, member.mb_name, member.mb_lg_idx, local_government.lg_name')
|
||||||
'member_approval_request.*, member.mb_id, member.mb_name, member.mb_lg_idx, local_government.lg_name'
|
->join('member', 'member.mb_idx = member_approval_request.mb_idx', 'left')
|
||||||
);
|
->join('local_government', 'local_government.lg_idx = member.mb_lg_idx', 'left')
|
||||||
$builder->join('member', 'member.mb_idx = member_approval_request.mb_idx', 'left');
|
->where('member_approval_request.mar_status', $status)
|
||||||
$builder->join('local_government', 'local_government.lg_idx = member.mb_lg_idx', 'left');
|
->orderBy('member_approval_request.mar_requested_at', 'DESC')
|
||||||
$builder->where('member_approval_request.mar_status', $status);
|
->paginate(20);
|
||||||
$builder->orderBy('member_approval_request.mar_requested_at', 'DESC');
|
$pager = $this->approvalModel->pager;
|
||||||
$list = $builder->get()->getResult();
|
|
||||||
|
|
||||||
return view('admin/layout', [
|
return view('admin/layout', [
|
||||||
'title' => '승인 대기',
|
'title' => '승인 대기',
|
||||||
@@ -75,6 +74,7 @@ class Access extends BaseController
|
|||||||
'list' => $list,
|
'list' => $list,
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
'roles' => $this->roles,
|
'roles' => $this->roles,
|
||||||
|
'pager' => $pager,
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -90,8 +90,8 @@ class Access extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$requestedLevel = (int) $requestRow->mar_requested_level;
|
$requestedLevel = (int) $requestRow->mar_requested_level;
|
||||||
if ($requestedLevel === Roles::LEVEL_SUPER_ADMIN) {
|
if ($requestedLevel === Roles::LEVEL_SUPER_ADMIN || $requestedLevel === Roles::LEVEL_HEADQUARTERS_ADMIN) {
|
||||||
return redirect()->to(site_url('admin/access/approvals'))->with('error', 'super admin 역할 요청은 승인할 수 없습니다.');
|
return redirect()->to(site_url('admin/access/approvals'))->with('error', '상위 관리자 역할 요청은 승인할 수 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$db = db_connect();
|
$db = db_connect();
|
||||||
|
|||||||
52
app/Controllers/Admin/BagInventory.php
Normal file
52
app/Controllers/Admin/BagInventory.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Controllers\BaseController;
|
||||||
|
use App\Models\BagInventoryModel;
|
||||||
|
|
||||||
|
class BagInventory extends BaseController
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$invModel = model(BagInventoryModel::class);
|
||||||
|
$list = $invModel->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->paginate(20);
|
||||||
|
$pager = $invModel->pager;
|
||||||
|
|
||||||
|
return $this->renderWorkPage('재고 현황', 'admin/bag_inventory/index', ['list' => $list, 'pager' => $pager]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function export()
|
||||||
|
{
|
||||||
|
helper(['admin', 'export']);
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(mgmt_url('bag-inventory'))->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->findAll();
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
foreach ($list as $row) {
|
||||||
|
$rows[] = [
|
||||||
|
$row->bi_idx,
|
||||||
|
$row->bi_bag_code,
|
||||||
|
$row->bi_bag_name,
|
||||||
|
(int) $row->bi_qty,
|
||||||
|
$row->bi_updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export_csv(
|
||||||
|
'재고현황_' . date('Ymd') . '.csv',
|
||||||
|
['번호', '봉투코드', '봉투명', '현재재고(낱장)', '최종갱신'],
|
||||||
|
$rows
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
130
app/Controllers/Admin/BagIssue.php
Normal file
130
app/Controllers/Admin/BagIssue.php
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Controllers\BaseController;
|
||||||
|
use App\Models\BagIssueModel;
|
||||||
|
use App\Models\BagInventoryModel;
|
||||||
|
use App\Models\CodeKindModel;
|
||||||
|
use App\Models\CodeDetailModel;
|
||||||
|
|
||||||
|
class BagIssue extends BaseController
|
||||||
|
{
|
||||||
|
private BagIssueModel $issueModel;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->issueModel = model(BagIssueModel::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder = $this->issueModel->where('bi2_lg_idx', $lgIdx);
|
||||||
|
$startDate = $this->request->getGet('start_date');
|
||||||
|
$endDate = $this->request->getGet('end_date');
|
||||||
|
if ($startDate) {
|
||||||
|
$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);
|
||||||
|
$pager = $this->issueModel->pager;
|
||||||
|
|
||||||
|
return $this->renderWorkPage('무료용 불출 관리', 'admin/bag_issue/index', compact('list', 'startDate', 'endDate', 'pager'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
||||||
|
|
||||||
|
return $this->renderWorkPage('무료용 불출 처리', 'admin/bag_issue/create', compact('bagCodes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'bi2_year' => 'required|is_natural_no_zero',
|
||||||
|
'bi2_quarter' => 'required|in_list[1,2,3,4]',
|
||||||
|
'bi2_issue_type' => 'required|max_length[20]',
|
||||||
|
'bi2_issue_date' => 'required|valid_date[Y-m-d]',
|
||||||
|
'bi2_dest_name' => 'required|max_length[100]',
|
||||||
|
'bi2_bag_code' => 'required|max_length[50]',
|
||||||
|
'bi2_qty' => 'required|is_natural_no_zero',
|
||||||
|
];
|
||||||
|
if (! $this->validate($rules)) {
|
||||||
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
$bagCode = $this->request->getPost('bi2_bag_code');
|
||||||
|
$qty = (int) $this->request->getPost('bi2_qty');
|
||||||
|
|
||||||
|
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
$detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $bagCode, $lgIdx) : null;
|
||||||
|
$bagName = $detail ? $detail->cd_name : '';
|
||||||
|
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$db->transStart();
|
||||||
|
|
||||||
|
$issueData = [
|
||||||
|
'bi2_lg_idx' => $lgIdx,
|
||||||
|
'bi2_year' => (int) $this->request->getPost('bi2_year'),
|
||||||
|
'bi2_quarter' => (int) $this->request->getPost('bi2_quarter'),
|
||||||
|
'bi2_issue_type' => $this->request->getPost('bi2_issue_type'),
|
||||||
|
'bi2_issue_date' => $this->request->getPost('bi2_issue_date'),
|
||||||
|
'bi2_dest_type' => $this->request->getPost('bi2_dest_type') ?? '',
|
||||||
|
'bi2_dest_name' => $this->request->getPost('bi2_dest_name'),
|
||||||
|
'bi2_bag_code' => $bagCode,
|
||||||
|
'bi2_bag_name' => $bagName,
|
||||||
|
'bi2_qty' => $qty,
|
||||||
|
'bi2_status' => 'normal',
|
||||||
|
'bi2_regdate' => date('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
$this->issueModel->insert($issueData);
|
||||||
|
$bi2Idx = (int) $this->issueModel->getInsertID();
|
||||||
|
|
||||||
|
helper('audit');
|
||||||
|
audit_log('create', 'bag_issue', $bi2Idx, null, array_merge($issueData, ['bi2_idx' => $bi2Idx]));
|
||||||
|
|
||||||
|
model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $bagName, -$qty);
|
||||||
|
|
||||||
|
$db->transComplete();
|
||||||
|
|
||||||
|
return redirect()->to(mgmt_url('bag-issues'))->with('success', '불출 처리되었습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancel(int $id)
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$item = $this->issueModel->find($id);
|
||||||
|
if (! $item || (int) $item->bi2_lg_idx !== admin_effective_lg_idx()) {
|
||||||
|
return redirect()->to(mgmt_url('bag-issues'))->with('error', '불출 내역을 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$db->transStart();
|
||||||
|
|
||||||
|
$before = (array) $item;
|
||||||
|
$this->issueModel->update($id, ['bi2_status' => 'cancelled']);
|
||||||
|
helper('audit');
|
||||||
|
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);
|
||||||
|
|
||||||
|
$db->transComplete();
|
||||||
|
|
||||||
|
return redirect()->to(mgmt_url('bag-issues'))->with('success', '불출이 취소되었습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
275
app/Controllers/Admin/BagOrder.php
Normal file
275
app/Controllers/Admin/BagOrder.php
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Controllers\BaseController;
|
||||||
|
use App\Models\BagOrderModel;
|
||||||
|
use App\Models\BagOrderItemModel;
|
||||||
|
use App\Models\BagPriceModel;
|
||||||
|
use App\Models\PackagingUnitModel;
|
||||||
|
use App\Models\CompanyModel;
|
||||||
|
use App\Models\SalesAgencyModel;
|
||||||
|
use App\Models\CodeKindModel;
|
||||||
|
use App\Models\CodeDetailModel;
|
||||||
|
class BagOrder extends BaseController
|
||||||
|
{
|
||||||
|
private BagOrderModel $orderModel;
|
||||||
|
private BagOrderItemModel $itemModel;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->orderModel = model(BagOrderModel::class);
|
||||||
|
$this->itemModel = model(BagOrderItemModel::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder = $this->orderModel->where('bo_lg_idx', $lgIdx);
|
||||||
|
|
||||||
|
// 기간 필터
|
||||||
|
$startDate = $this->request->getGet('start_date');
|
||||||
|
$endDate = $this->request->getGet('end_date');
|
||||||
|
$status = $this->request->getGet('status');
|
||||||
|
if ($startDate) $builder->where('bo_order_date >=', $startDate);
|
||||||
|
if ($endDate) $builder->where('bo_order_date <=', $endDate);
|
||||||
|
if ($status) $builder->where('bo_status', $status);
|
||||||
|
|
||||||
|
$list = $builder->orderBy('bo_order_date', 'DESC')->orderBy('bo_idx', 'DESC')->paginate(20);
|
||||||
|
$pager = $this->orderModel->pager;
|
||||||
|
|
||||||
|
// 발주별 품목 합계
|
||||||
|
$itemSummary = [];
|
||||||
|
foreach ($list as $order) {
|
||||||
|
$items = $this->itemModel->where('boi_bo_idx', $order->bo_idx)->findAll();
|
||||||
|
$totalQty = 0; $totalAmt = 0;
|
||||||
|
foreach ($items as $it) { $totalQty += (int) $it->boi_qty_sheet; $totalAmt += (float) $it->boi_amount; }
|
||||||
|
$itemSummary[$order->bo_idx] = ['qty' => $totalQty, 'amount' => $totalAmt, 'count' => count($items)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 제작업체/대행소 이름 매핑
|
||||||
|
$companyMap = []; $agencyMap = [];
|
||||||
|
foreach (model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->findAll() as $c) $companyMap[$c->cp_idx] = $c->cp_name;
|
||||||
|
foreach (model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll() as $a) {
|
||||||
|
$agencyMap[$a->sa_idx] = '[' . ($a->sa_kind ?? '') . '] ' . ($a->sa_code ?? '') . ' — ' . ($a->sa_name ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->renderWorkPage('발주 현황', 'admin/bag_order/index', compact('list', 'itemSummary', 'companyMap', 'agencyMap', 'startDate', 'endDate', 'status', 'pager'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function export()
|
||||||
|
{
|
||||||
|
helper(['admin', 'export']);
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(mgmt_url('bag-orders'))->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder = $this->orderModel->where('bo_lg_idx', $lgIdx);
|
||||||
|
$startDate = $this->request->getGet('start_date');
|
||||||
|
$endDate = $this->request->getGet('end_date');
|
||||||
|
$status = $this->request->getGet('status');
|
||||||
|
if ($startDate) $builder->where('bo_order_date >=', $startDate);
|
||||||
|
if ($endDate) $builder->where('bo_order_date <=', $endDate);
|
||||||
|
if ($status) $builder->where('bo_status', $status);
|
||||||
|
|
||||||
|
$list = $builder->orderBy('bo_order_date', 'DESC')->orderBy('bo_idx', 'DESC')->findAll();
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
$statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제'];
|
||||||
|
foreach ($list as $row) {
|
||||||
|
$items = $this->itemModel->where('boi_bo_idx', $row->bo_idx)->findAll();
|
||||||
|
$totalQty = 0;
|
||||||
|
$totalAmt = 0;
|
||||||
|
foreach ($items as $it) {
|
||||||
|
$totalQty += (int) $it->boi_qty_sheet;
|
||||||
|
$totalAmt += (float) $it->boi_amount;
|
||||||
|
}
|
||||||
|
$rows[] = [
|
||||||
|
$row->bo_idx,
|
||||||
|
$row->bo_lot_no,
|
||||||
|
$row->bo_order_date,
|
||||||
|
count($items),
|
||||||
|
$totalQty,
|
||||||
|
$totalAmt,
|
||||||
|
$statusMap[$row->bo_status] ?? $row->bo_status,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export_csv(
|
||||||
|
'발주현황_' . date('Ymd') . '.csv',
|
||||||
|
['번호', 'LOT번호', '발주일', '품목수', '총수량', '총금액', '상태'],
|
||||||
|
$rows
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(mgmt_url('bag-orders'))->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 봉투 종류 + 단가 + 포장단위
|
||||||
|
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
||||||
|
$prices = model(BagPriceModel::class)->where('bp_lg_idx', $lgIdx)->where('bp_state', 1)->findAll();
|
||||||
|
$units = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_state', 1)->findAll();
|
||||||
|
$companies = model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '제작업체')->where('cp_state', 1)->findAll();
|
||||||
|
$agencies = model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll();
|
||||||
|
|
||||||
|
return $this->renderWorkPage('발주 등록', 'admin/bag_order/create', compact('bagCodes', 'prices', 'units', 'companies', 'agencies'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'bo_order_date' => 'required|valid_date[Y-m-d]',
|
||||||
|
'bo_company_idx' => 'permit_empty|is_natural_no_zero',
|
||||||
|
'bo_agency_idx' => 'permit_empty|is_natural_no_zero',
|
||||||
|
];
|
||||||
|
if (! $this->validate($rules)) {
|
||||||
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$db->transStart();
|
||||||
|
|
||||||
|
// UUID 생성
|
||||||
|
$uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||||
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
||||||
|
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
|
||||||
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff));
|
||||||
|
|
||||||
|
// LOT 번호 생성
|
||||||
|
$lotNo = 'LOT-' . date('Ymd') . '-' . strtoupper(substr(md5($uuid), 0, 6));
|
||||||
|
|
||||||
|
$orderData = [
|
||||||
|
'bo_uuid' => $uuid,
|
||||||
|
'bo_version' => 1,
|
||||||
|
'bo_lg_idx' => $lgIdx,
|
||||||
|
'bo_gugun_code' => $this->request->getPost('bo_gugun_code') ?? '',
|
||||||
|
'bo_dong_code' => $this->request->getPost('bo_dong_code') ?? '',
|
||||||
|
'bo_company_idx' => $this->request->getPost('bo_company_idx') ?: null,
|
||||||
|
'bo_agency_idx' => $this->request->getPost('bo_agency_idx') ?: null,
|
||||||
|
'bo_fee_rate' => (float) ($this->request->getPost('bo_fee_rate') ?: 0),
|
||||||
|
'bo_order_date' => $this->request->getPost('bo_order_date'),
|
||||||
|
'bo_lot_no' => $lotNo,
|
||||||
|
'bo_status' => 'normal',
|
||||||
|
'bo_orderer_idx' => session()->get('mb_idx'),
|
||||||
|
'bo_regdate' => date('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// SHA-256 해시
|
||||||
|
$orderData['bo_hash'] = hash('sha256', json_encode($orderData));
|
||||||
|
|
||||||
|
$this->orderModel->insert($orderData);
|
||||||
|
$boIdx = (int) $this->orderModel->getInsertID();
|
||||||
|
|
||||||
|
// CT-05: 감사 로그
|
||||||
|
helper('audit');
|
||||||
|
audit_log('create', 'bag_order', $boIdx, null, array_merge($orderData, ['bo_idx' => $boIdx]));
|
||||||
|
|
||||||
|
// 품목 저장
|
||||||
|
$bagCodes = $this->request->getPost('item_bag_code') ?? [];
|
||||||
|
$qtyBoxes = $this->request->getPost('item_qty_box') ?? [];
|
||||||
|
foreach ($bagCodes as $i => $code) {
|
||||||
|
if (empty($code) || empty($qtyBoxes[$i])) continue;
|
||||||
|
$qtyBox = (int) $qtyBoxes[$i];
|
||||||
|
|
||||||
|
// 포장단위에서 낱장 환산
|
||||||
|
$unit = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_bag_code', $code)->where('pu_state', 1)->first();
|
||||||
|
$totalPerBox = $unit ? (int) $unit->pu_total_per_box : 1;
|
||||||
|
$qtySheet = $qtyBox * $totalPerBox;
|
||||||
|
|
||||||
|
// 단가
|
||||||
|
$price = model(BagPriceModel::class)->where('bp_lg_idx', $lgIdx)->where('bp_bag_code', $code)->where('bp_state', 1)->first();
|
||||||
|
$unitPrice = $price ? (float) $price->bp_order_price : 0;
|
||||||
|
|
||||||
|
// 봉투명
|
||||||
|
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
$detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $code, $lgIdx) : null;
|
||||||
|
|
||||||
|
$this->itemModel->insert([
|
||||||
|
'boi_bo_idx' => $boIdx,
|
||||||
|
'boi_bag_code' => $code,
|
||||||
|
'boi_bag_name' => $detail ? $detail->cd_name : '',
|
||||||
|
'boi_unit_price' => $unitPrice,
|
||||||
|
'boi_qty_box' => $qtyBox,
|
||||||
|
'boi_qty_sheet' => $qtySheet,
|
||||||
|
'boi_amount' => $unitPrice * $qtySheet,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->transComplete();
|
||||||
|
|
||||||
|
return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 등록되었습니다. LOT: ' . $lotNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function detail(int $id)
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$order = $this->orderModel->find($id);
|
||||||
|
if (!$order || (int) $order->bo_lg_idx !== admin_effective_lg_idx()) {
|
||||||
|
return redirect()->to(mgmt_url('bag-orders'))->with('error', '발주를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $this->itemModel->where('boi_bo_idx', $id)->findAll();
|
||||||
|
|
||||||
|
$companyName = '';
|
||||||
|
if ($order->bo_company_idx) {
|
||||||
|
$c = model(CompanyModel::class)->find($order->bo_company_idx);
|
||||||
|
$companyName = $c ? $c->cp_name : '';
|
||||||
|
}
|
||||||
|
$agencyName = '';
|
||||||
|
if ($order->bo_agency_idx) {
|
||||||
|
$a = model(SalesAgencyModel::class)->find($order->bo_agency_idx);
|
||||||
|
if ($a) {
|
||||||
|
$agencyName = '[' . ($a->sa_kind ?? '') . '] ' . ($a->sa_code ?? '') . ' — ' . ($a->sa_name ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->renderWorkPage('발주 상세 — ' . $order->bo_lot_no, 'admin/bag_order/detail', compact('order', 'items', 'companyName', 'agencyName'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancel(int $id)
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$order = $this->orderModel->find($id);
|
||||||
|
if (!$order || (int) $order->bo_lg_idx !== admin_effective_lg_idx()) {
|
||||||
|
return redirect()->to(mgmt_url('bag-orders'))->with('error', '발주를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$before = (array) $order;
|
||||||
|
$this->orderModel->update($id, ['bo_status' => 'cancelled', 'bo_moddate' => date('Y-m-d H:i:s')]);
|
||||||
|
helper('audit');
|
||||||
|
audit_log('update', 'bag_order', $id, $before, ['bo_status' => 'cancelled']);
|
||||||
|
|
||||||
|
return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 취소되었습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(int $id)
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$order = $this->orderModel->find($id);
|
||||||
|
if (!$order || (int) $order->bo_lg_idx !== admin_effective_lg_idx()) {
|
||||||
|
return redirect()->to(mgmt_url('bag-orders'))->with('error', '발주를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$before = (array) $order;
|
||||||
|
$this->orderModel->update($id, ['bo_status' => 'deleted', 'bo_moddate' => date('Y-m-d H:i:s')]);
|
||||||
|
helper('audit');
|
||||||
|
audit_log('delete', 'bag_order', $id, $before, ['bo_status' => 'deleted']);
|
||||||
|
|
||||||
|
return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 삭제 처리되었습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,34 +23,18 @@ 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', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$builder = $this->priceModel->where('bp_lg_idx', $lgIdx);
|
$list = $this->priceModel->where('bp_lg_idx', $lgIdx)
|
||||||
|
->orderBy('bp_bag_code', 'ASC')
|
||||||
|
->orderBy('bp_start_date', 'DESC')
|
||||||
|
->paginate(20);
|
||||||
|
|
||||||
// 기간 필터 (P2-04)
|
return $this->renderWorkPage('봉투 단가 관리', 'admin/bag_price/index', [
|
||||||
$startDate = $this->request->getGet('start_date');
|
|
||||||
$endDate = $this->request->getGet('end_date');
|
|
||||||
if ($startDate) {
|
|
||||||
$builder->where('bp_start_date >=', $startDate);
|
|
||||||
}
|
|
||||||
if ($endDate) {
|
|
||||||
$builder->groupStart()
|
|
||||||
->where('bp_end_date IS NULL')
|
|
||||||
->orWhere('bp_end_date <=', $endDate)
|
|
||||||
->groupEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
$list = $builder->orderBy('bp_bag_code', 'ASC')->orderBy('bp_start_date', 'DESC')->findAll();
|
|
||||||
|
|
||||||
return view('admin/layout', [
|
|
||||||
'title' => '봉투 단가 관리',
|
|
||||||
'content' => view('admin/bag_price/index', [
|
|
||||||
'list' => $list,
|
'list' => $list,
|
||||||
'startDate' => $startDate,
|
'pager' => $this->priceModel->pager,
|
||||||
'endDate' => $endDate,
|
|
||||||
]),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,22 +42,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()
|
||||||
@@ -94,13 +74,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 : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,36 +97,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');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
$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 !== $lgIdx) {
|
||||||
return redirect()->to(site_url('admin/bag-prices'))->with('error', '단가 정보를 찾을 수 없습니다.');
|
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 = [
|
||||||
@@ -163,7 +140,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();
|
||||||
|
|
||||||
@@ -177,8 +153,8 @@ class BagPrice extends BaseController
|
|||||||
'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'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,34 +171,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]),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
108
app/Controllers/Admin/BagReceiving.php
Normal file
108
app/Controllers/Admin/BagReceiving.php
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Controllers\BaseController;
|
||||||
|
use App\Models\BagReceivingModel;
|
||||||
|
use App\Models\BagOrderModel;
|
||||||
|
use App\Models\BagOrderItemModel;
|
||||||
|
use App\Models\BagInventoryModel;
|
||||||
|
use App\Models\CompanyModel;
|
||||||
|
|
||||||
|
class BagReceiving extends BaseController
|
||||||
|
{
|
||||||
|
private BagReceivingModel $recvModel;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->recvModel = model(BagReceivingModel::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder = $this->recvModel->where('br_lg_idx', $lgIdx);
|
||||||
|
$startDate = $this->request->getGet('start_date');
|
||||||
|
$endDate = $this->request->getGet('end_date');
|
||||||
|
if ($startDate) {
|
||||||
|
$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);
|
||||||
|
$pager = $this->recvModel->pager;
|
||||||
|
|
||||||
|
return $this->renderWorkPage('입고 현황', 'admin/bag_receiving/index', compact('list', 'startDate', 'endDate', 'pager'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(mgmt_url('bag-receivings'))->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$orders = model(BagOrderModel::class)->where('bo_lg_idx', $lgIdx)->where('bo_status', 'normal')->orderBy('bo_order_date', 'DESC')->findAll();
|
||||||
|
|
||||||
|
return $this->renderWorkPage('입고 처리', 'admin/bag_receiving/create', compact('orders'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'br_bo_idx' => 'required|is_natural_no_zero',
|
||||||
|
'br_bag_code' => 'required|max_length[50]',
|
||||||
|
'br_qty_box' => 'required|is_natural_no_zero',
|
||||||
|
'br_receive_date' => 'required|valid_date[Y-m-d]',
|
||||||
|
];
|
||||||
|
if (! $this->validate($rules)) {
|
||||||
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
$boIdx = (int) $this->request->getPost('br_bo_idx');
|
||||||
|
$bagCode = $this->request->getPost('br_bag_code');
|
||||||
|
$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();
|
||||||
|
$totalPerBox = $unit ? (int) $unit->pu_total_per_box : 1;
|
||||||
|
$qtySheet = $qtyBox * $totalPerBox;
|
||||||
|
|
||||||
|
$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;
|
||||||
|
$bagName = $detail ? $detail->cd_name : '';
|
||||||
|
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$db->transStart();
|
||||||
|
|
||||||
|
$this->recvModel->insert([
|
||||||
|
'br_bo_idx' => $boIdx,
|
||||||
|
'br_lg_idx' => $lgIdx,
|
||||||
|
'br_bag_code' => $bagCode,
|
||||||
|
'br_bag_name' => $bagName,
|
||||||
|
'br_qty_box' => $qtyBox,
|
||||||
|
'br_qty_sheet' => $qtySheet,
|
||||||
|
'br_receive_date' => $this->request->getPost('br_receive_date'),
|
||||||
|
'br_receiver_idx' => session()->get('mb_idx'),
|
||||||
|
'br_sender_name' => $this->request->getPost('br_sender_name') ?? '',
|
||||||
|
'br_type' => $this->request->getPost('br_type') ?? 'batch',
|
||||||
|
'br_regdate' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $bagName, $qtySheet);
|
||||||
|
|
||||||
|
$db->transComplete();
|
||||||
|
|
||||||
|
return redirect()->to(mgmt_url('bag-receivings'))->with('success', '입고 처리되었습니다. (' . $bagName . ' ' . $qtyBox . '박스)');
|
||||||
|
}
|
||||||
|
}
|
||||||
171
app/Controllers/Admin/BagSale.php
Normal file
171
app/Controllers/Admin/BagSale.php
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Controllers\BaseController;
|
||||||
|
use App\Models\BagSaleModel;
|
||||||
|
use App\Models\BagInventoryModel;
|
||||||
|
use App\Models\DesignatedShopModel;
|
||||||
|
use App\Models\CodeKindModel;
|
||||||
|
use App\Models\CodeDetailModel;
|
||||||
|
use App\Models\BagPriceModel;
|
||||||
|
|
||||||
|
class BagSale extends BaseController
|
||||||
|
{
|
||||||
|
private BagSaleModel $saleModel;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->saleModel = model(BagSaleModel::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
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 $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();
|
||||||
|
|
||||||
|
$typeMap = ['sale' => '판매', 'return' => '반품', 'cancel' => '취소'];
|
||||||
|
$rows = [];
|
||||||
|
foreach ($list as $row) {
|
||||||
|
$rows[] = [
|
||||||
|
$row->bs_idx,
|
||||||
|
$row->bs_ds_name,
|
||||||
|
$row->bs_sale_date,
|
||||||
|
$row->bs_bag_code,
|
||||||
|
$row->bs_bag_name,
|
||||||
|
(int) $row->bs_qty,
|
||||||
|
(int) $row->bs_unit_price,
|
||||||
|
(int) $row->bs_amount,
|
||||||
|
$typeMap[$row->bs_type] ?? $row->bs_type,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export_csv(
|
||||||
|
'판매반품_' . date('Ymd') . '.csv',
|
||||||
|
['번호', '판매소', '판매일', '봉투코드', '봉투명', '수량', '단가', '금액', '구분'],
|
||||||
|
$rows
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
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();
|
||||||
|
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
||||||
|
|
||||||
|
return $this->renderWorkPage('판매 등록', 'admin/bag_sale/create', compact('shops', 'bagCodes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'bs_ds_idx' => 'required|is_natural_no_zero',
|
||||||
|
'bs_bag_code' => 'required|max_length[50]',
|
||||||
|
'bs_qty' => 'required|is_natural_no_zero',
|
||||||
|
'bs_sale_date' => 'required|valid_date[Y-m-d]',
|
||||||
|
'bs_type' => 'required|in_list[sale,return]',
|
||||||
|
];
|
||||||
|
if (! $this->validate($rules)) {
|
||||||
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
$dsIdx = (int) $this->request->getPost('bs_ds_idx');
|
||||||
|
$bagCode = $this->request->getPost('bs_bag_code');
|
||||||
|
$qty = (int) $this->request->getPost('bs_qty');
|
||||||
|
$type = $this->request->getPost('bs_type');
|
||||||
|
|
||||||
|
$shop = model(DesignatedShopModel::class)->find($dsIdx);
|
||||||
|
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
$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();
|
||||||
|
$unitPrice = $price ? (float) $price->bp_consumer : 0;
|
||||||
|
|
||||||
|
$actualQty = ($type === 'return') ? -$qty : $qty;
|
||||||
|
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$db->transStart();
|
||||||
|
|
||||||
|
$saleData = [
|
||||||
|
'bs_lg_idx' => $lgIdx,
|
||||||
|
'bs_ds_idx' => $dsIdx,
|
||||||
|
'bs_ds_name' => $shop ? $shop->ds_name : '',
|
||||||
|
'bs_sale_date' => $this->request->getPost('bs_sale_date'),
|
||||||
|
'bs_bag_code' => $bagCode,
|
||||||
|
'bs_bag_name' => $detail ? $detail->cd_name : '',
|
||||||
|
'bs_qty' => $actualQty,
|
||||||
|
'bs_unit_price' => $unitPrice,
|
||||||
|
'bs_amount' => $unitPrice * abs($actualQty),
|
||||||
|
'bs_type' => $type,
|
||||||
|
'bs_regdate' => date('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
$this->saleModel->insert($saleData);
|
||||||
|
$bsIdx = (int) $this->saleModel->getInsertID();
|
||||||
|
|
||||||
|
helper('audit');
|
||||||
|
audit_log('create', 'bag_sale', $bsIdx, null, array_merge($saleData, ['bs_idx' => $bsIdx]));
|
||||||
|
|
||||||
|
model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $detail ? $detail->cd_name : '', -$actualQty);
|
||||||
|
|
||||||
|
$db->transComplete();
|
||||||
|
|
||||||
|
$msg = ($type === 'sale') ? '판매 처리되었습니다.' : '반품 처리되었습니다.';
|
||||||
|
|
||||||
|
return redirect()->to(mgmt_url('bag-sales'))->with('success', $msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,39 +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->getByKind($ckIdx);
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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]',
|
||||||
@@ -62,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);
|
||||||
@@ -95,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 = [
|
||||||
@@ -116,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', '세부코드가 삭제되었습니다.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,28 +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')->findAll();
|
if (! Roles::canManageCodeKindMaster((int) session()->get('mb_level'))) {
|
||||||
|
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,
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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'),
|
||||||
@@ -46,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]',
|
||||||
@@ -62,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', [
|
||||||
@@ -80,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 = [
|
||||||
@@ -99,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', '코드 종류가 삭제되었습니다.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,24 +18,19 @@ 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')->findAll();
|
$list = $this->model->where('cp_lg_idx', $lgIdx)->orderBy('cp_idx', 'DESC')->paginate(20);
|
||||||
|
$pager = $this->model->pager;
|
||||||
|
|
||||||
return view('admin/layout', [
|
return $this->renderWorkPage('업체 관리', 'admin/company/index', ['list' => $list, 'pager' => $pager]);
|
||||||
'title' => '업체 관리',
|
|
||||||
'content' => view('admin/company/index', ['list' => $list]),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
@@ -65,29 +60,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 = [
|
||||||
@@ -109,18 +101,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', '업체가 삭제되었습니다.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,98 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
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
|
||||||
{
|
{
|
||||||
public function index(): string
|
public function index(): string
|
||||||
{
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
|
||||||
|
$stats = [
|
||||||
|
'order_count' => 0,
|
||||||
|
'order_amount' => 0,
|
||||||
|
'sale_count' => 0,
|
||||||
|
'sale_amount' => 0,
|
||||||
|
'inventory_count' => 0,
|
||||||
|
'issue_count_month'=> 0,
|
||||||
|
'recent_orders' => [],
|
||||||
|
'recent_sales' => [],
|
||||||
|
'stats_unavailable'=> false,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($lgIdx) {
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 총 발주 건수/금액
|
||||||
|
$orderStats = $db->query("
|
||||||
|
SELECT COUNT(*) as cnt,
|
||||||
|
COALESCE(SUM(sub.total_amt), 0) as total_amount
|
||||||
|
FROM bag_order bo
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT boi_bo_idx, SUM(boi_amount) as total_amt
|
||||||
|
FROM bag_order_item GROUP BY boi_bo_idx
|
||||||
|
) sub ON sub.boi_bo_idx = bo.bo_idx
|
||||||
|
WHERE bo.bo_lg_idx = ? AND bo.bo_status = 'normal'
|
||||||
|
", [$lgIdx])->getRow();
|
||||||
|
$stats['order_count'] = (int) ($orderStats->cnt ?? 0);
|
||||||
|
$stats['order_amount'] = (int) ($orderStats->total_amount ?? 0);
|
||||||
|
|
||||||
|
// 총 판매 건수/금액
|
||||||
|
$saleStats = $db->query("
|
||||||
|
SELECT COUNT(*) as cnt, COALESCE(SUM(bs_amount), 0) as total_amount
|
||||||
|
FROM bag_sale
|
||||||
|
WHERE bs_lg_idx = ? AND bs_type = 'sale'
|
||||||
|
", [$lgIdx])->getRow();
|
||||||
|
$stats['sale_count'] = (int) ($saleStats->cnt ?? 0);
|
||||||
|
$stats['sale_amount'] = (int) ($saleStats->total_amount ?? 0);
|
||||||
|
|
||||||
|
// 현재 재고 품목 수
|
||||||
|
$invCount = $db->query("
|
||||||
|
SELECT COUNT(*) as cnt FROM bag_inventory WHERE bi_lg_idx = ? AND bi_qty > 0
|
||||||
|
", [$lgIdx])->getRow();
|
||||||
|
$stats['inventory_count'] = (int) ($invCount->cnt ?? 0);
|
||||||
|
|
||||||
|
// 이번 달 불출 건수
|
||||||
|
$monthStart = date('Y-m-01');
|
||||||
|
$issueCount = $db->query("
|
||||||
|
SELECT COUNT(*) as cnt FROM bag_issue
|
||||||
|
WHERE bi2_lg_idx = ? AND bi2_status = 'normal' AND bi2_issue_date >= ?
|
||||||
|
", [$lgIdx, $monthStart])->getRow();
|
||||||
|
$stats['issue_count_month'] = (int) ($issueCount->cnt ?? 0);
|
||||||
|
|
||||||
|
// 최근 발주 5건
|
||||||
|
$stats['recent_orders'] = $db->query("
|
||||||
|
SELECT bo_idx, bo_lot_no, bo_order_date, bo_status
|
||||||
|
FROM bag_order
|
||||||
|
WHERE bo_lg_idx = ?
|
||||||
|
ORDER BY bo_order_date DESC, bo_idx DESC
|
||||||
|
LIMIT 5
|
||||||
|
", [$lgIdx])->getResult();
|
||||||
|
|
||||||
|
// 최근 판매 5건
|
||||||
|
$stats['recent_sales'] = $db->query("
|
||||||
|
SELECT bs_idx, bs_ds_name, bs_bag_name, bs_qty, bs_amount, bs_sale_date, bs_type
|
||||||
|
FROM bag_sale
|
||||||
|
WHERE bs_lg_idx = ?
|
||||||
|
ORDER BY bs_sale_date DESC, bs_idx DESC
|
||||||
|
LIMIT 5
|
||||||
|
", [$lgIdx])->getResult();
|
||||||
|
} catch (DatabaseException $e) {
|
||||||
|
$stats['stats_unavailable'] = true;
|
||||||
|
log_message('error', '[Dashboard] 통계 조회 실패(테이블 미생성 등): ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return view('admin/layout', [
|
return view('admin/layout', [
|
||||||
'title' => '대시보드',
|
'title' => '대시보드',
|
||||||
'content' => view('admin/dashboard/index'),
|
'content' => view('admin/dashboard/index', ['stats' => $stats, 'lgIdx' => $lgIdx]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class DesignatedShop extends BaseController
|
|||||||
|
|
||||||
private function isSuperAdmin(): bool
|
private function isSuperAdmin(): bool
|
||||||
{
|
{
|
||||||
return (int) session()->get('mb_level') === Roles::LEVEL_SUPER_ADMIN;
|
return Roles::isSuperAdminEquivalent((int) session()->get('mb_level'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isLocalAdmin(): bool
|
private function isLocalAdmin(): bool
|
||||||
@@ -39,14 +39,29 @@ class DesignatedShop extends BaseController
|
|||||||
$lgIdx = admin_effective_lg_idx();
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
|
||||||
if ($lgIdx === null || $lgIdx <= 0) {
|
if ($lgIdx === null || $lgIdx <= 0) {
|
||||||
return redirect()->to(site_url('admin'))
|
return redirect()->to(work_area_home_url())
|
||||||
->with('error', '작업할 지자체가 선택되지 않았습니다. 지자체를 선택해 주세요.');
|
->with('error', '작업할 지자체가 선택되지 않았습니다. 지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$list = $this->shopModel
|
$builder = $this->shopModel->where('ds_lg_idx', $lgIdx);
|
||||||
->where('ds_lg_idx', $lgIdx)
|
|
||||||
->orderBy('ds_idx', 'DESC')
|
// 다조건 검색 (P2-15)
|
||||||
->findAll();
|
$dsName = $this->request->getGet('ds_name');
|
||||||
|
$dsGugunCode = $this->request->getGet('ds_gugun_code');
|
||||||
|
$dsState = $this->request->getGet('ds_state');
|
||||||
|
|
||||||
|
if ($dsName !== null && $dsName !== '') {
|
||||||
|
$builder->like('ds_name', $dsName);
|
||||||
|
}
|
||||||
|
if ($dsGugunCode !== null && $dsGugunCode !== '') {
|
||||||
|
$builder->where('ds_gugun_code', $dsGugunCode);
|
||||||
|
}
|
||||||
|
if ($dsState !== null && $dsState !== '') {
|
||||||
|
$builder->where('ds_state', (int) $dsState);
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = $builder->orderBy('ds_idx', 'DESC')->paginate(20);
|
||||||
|
$pager = $this->shopModel->pager;
|
||||||
|
|
||||||
// 지자체 이름 매핑용
|
// 지자체 이름 매핑용
|
||||||
$lgMap = [];
|
$lgMap = [];
|
||||||
@@ -54,15 +69,55 @@ class DesignatedShop extends BaseController
|
|||||||
$lgMap[$lg->lg_idx] = $lg->lg_name;
|
$lgMap[$lg->lg_idx] = $lg->lg_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('admin/layout', [
|
// 구군코드 목록 (검색 필터용)
|
||||||
'title' => '지정판매소 관리',
|
$db = \Config\Database::connect();
|
||||||
'content' => view('admin/designated_shop/index', [
|
$gugunCodes = $db->query("SELECT DISTINCT ds_gugun_code FROM designated_shop WHERE ds_lg_idx = ? AND ds_gugun_code != '' ORDER BY ds_gugun_code", [$lgIdx])->getResult();
|
||||||
|
|
||||||
|
return $this->renderWorkPage('지정판매소 관리', 'admin/designated_shop/index', [
|
||||||
'list' => $list,
|
'list' => $list,
|
||||||
'lgMap' => $lgMap,
|
'lgMap' => $lgMap,
|
||||||
]),
|
'pager' => $pager,
|
||||||
|
'dsName' => $dsName ?? '',
|
||||||
|
'dsGugunCode' => $dsGugunCode ?? '',
|
||||||
|
'dsState' => $dsState ?? '',
|
||||||
|
'gugunCodes' => $gugunCodes,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function export()
|
||||||
|
{
|
||||||
|
helper(['admin', 'export']);
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (!$lgIdx) {
|
||||||
|
return redirect()->to(mgmt_url('designated-shops'))->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = $this->shopModel->where('ds_lg_idx', $lgIdx)->orderBy('ds_idx', 'DESC')->findAll();
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
foreach ($list as $row) {
|
||||||
|
$stateMap = [1 => '정상', 2 => '폐업', 3 => '직권해지'];
|
||||||
|
$rows[] = [
|
||||||
|
$row->ds_idx,
|
||||||
|
$row->ds_shop_no,
|
||||||
|
$row->ds_name,
|
||||||
|
$row->ds_rep_name,
|
||||||
|
$row->ds_biz_no,
|
||||||
|
$row->ds_va_number,
|
||||||
|
$row->ds_tel ?? '',
|
||||||
|
$row->ds_addr ?? '',
|
||||||
|
$stateMap[(int) $row->ds_state] ?? '',
|
||||||
|
$row->ds_regdate ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export_csv(
|
||||||
|
'지정판매소_' . date('Ymd') . '.csv',
|
||||||
|
['번호', '판매소번호', '상호명', '대표자', '사업자번호', '가상계좌', '전화번호', '주소', '상태', '등록일'],
|
||||||
|
$rows
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 지정판매소 등록 폼 (효과 지자체 기준)
|
* 지정판매소 등록 폼 (효과 지자체 기준)
|
||||||
*/
|
*/
|
||||||
@@ -71,22 +126,19 @@ class DesignatedShop extends BaseController
|
|||||||
helper('admin');
|
helper('admin');
|
||||||
$lgIdx = admin_effective_lg_idx();
|
$lgIdx = admin_effective_lg_idx();
|
||||||
if ($lgIdx === null || $lgIdx <= 0) {
|
if ($lgIdx === null || $lgIdx <= 0) {
|
||||||
return redirect()->to(site_url('admin/designated-shops'))
|
return redirect()->to(mgmt_url('designated-shops'))
|
||||||
->with('error', '작업할 지자체가 선택되지 않았습니다. 지자체를 선택해 주세요.');
|
->with('error', '작업할 지자체가 선택되지 않았습니다. 지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$currentLg = $this->lgModel->find($lgIdx);
|
$currentLg = $this->lgModel->find($lgIdx);
|
||||||
if ($currentLg === null) {
|
if ($currentLg === null) {
|
||||||
return redirect()->to(site_url('admin/designated-shops'))
|
return redirect()->to(mgmt_url('designated-shops'))
|
||||||
->with('error', '선택한 지자체 정보를 찾을 수 없습니다.');
|
->with('error', '선택한 지자체 정보를 찾을 수 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('admin/layout', [
|
return $this->renderWorkPage('지정판매소 등록', 'admin/designated_shop/create', [
|
||||||
'title' => '지정판매소 등록',
|
|
||||||
'content' => view('admin/designated_shop/create', [
|
|
||||||
'localGovs' => [],
|
'localGovs' => [],
|
||||||
'currentLg' => $currentLg,
|
'currentLg' => $currentLg,
|
||||||
]),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +148,7 @@ class DesignatedShop extends BaseController
|
|||||||
public function store()
|
public function store()
|
||||||
{
|
{
|
||||||
if (! $this->isSuperAdmin() && ! $this->isLocalAdmin()) {
|
if (! $this->isSuperAdmin() && ! $this->isLocalAdmin()) {
|
||||||
return redirect()->to(site_url('admin/designated-shops'))
|
return redirect()->to(mgmt_url('designated-shops'))
|
||||||
->with('error', '지정판매소 등록은 관리자만 가능합니다.');
|
->with('error', '지정판매소 등록은 관리자만 가능합니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +205,7 @@ class DesignatedShop extends BaseController
|
|||||||
|
|
||||||
$this->shopModel->insert($data);
|
$this->shopModel->insert($data);
|
||||||
|
|
||||||
return redirect()->to(site_url('admin/designated-shops'))
|
return redirect()->to(mgmt_url('designated-shops'))
|
||||||
->with('success', '지정판매소가 등록되었습니다.');
|
->with('success', '지정판매소가 등록되었습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,31 +216,28 @@ class DesignatedShop extends BaseController
|
|||||||
public function edit(int $id)
|
public function edit(int $id)
|
||||||
{
|
{
|
||||||
if (! $this->isSuperAdmin() && ! $this->isLocalAdmin()) {
|
if (! $this->isSuperAdmin() && ! $this->isLocalAdmin()) {
|
||||||
return redirect()->to(site_url('admin/designated-shops'))
|
return redirect()->to(mgmt_url('designated-shops'))
|
||||||
->with('error', '권한이 없습니다.');
|
->with('error', '권한이 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
helper('admin');
|
helper('admin');
|
||||||
$lgIdx = admin_effective_lg_idx();
|
$lgIdx = admin_effective_lg_idx();
|
||||||
if ($lgIdx === null || $lgIdx <= 0) {
|
if ($lgIdx === null || $lgIdx <= 0) {
|
||||||
return redirect()->to(site_url('admin/designated-shops'))
|
return redirect()->to(mgmt_url('designated-shops'))
|
||||||
->with('error', '작업할 지자체가 선택되지 않았습니다.');
|
->with('error', '작업할 지자체가 선택되지 않았습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$shop = $this->shopModel->find($id);
|
$shop = $this->shopModel->find($id);
|
||||||
if ($shop === null || (int) $shop->ds_lg_idx !== $lgIdx) {
|
if ($shop === null || (int) $shop->ds_lg_idx !== $lgIdx) {
|
||||||
return redirect()->to(site_url('admin/designated-shops'))
|
return redirect()->to(mgmt_url('designated-shops'))
|
||||||
->with('error', '해당 지정판매소를 찾을 수 없거나 수정할 수 없습니다.');
|
->with('error', '해당 지정판매소를 찾을 수 없거나 수정할 수 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$currentLg = $this->lgModel->find($lgIdx);
|
$currentLg = $this->lgModel->find($lgIdx);
|
||||||
|
|
||||||
return view('admin/layout', [
|
return $this->renderWorkPage('지정판매소 수정', 'admin/designated_shop/edit', [
|
||||||
'title' => '지정판매소 수정',
|
|
||||||
'content' => view('admin/designated_shop/edit', [
|
|
||||||
'shop' => $shop,
|
'shop' => $shop,
|
||||||
'currentLg' => $currentLg,
|
'currentLg' => $currentLg,
|
||||||
]),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,20 +247,20 @@ class DesignatedShop extends BaseController
|
|||||||
public function update(int $id)
|
public function update(int $id)
|
||||||
{
|
{
|
||||||
if (! $this->isSuperAdmin() && ! $this->isLocalAdmin()) {
|
if (! $this->isSuperAdmin() && ! $this->isLocalAdmin()) {
|
||||||
return redirect()->to(site_url('admin/designated-shops'))
|
return redirect()->to(mgmt_url('designated-shops'))
|
||||||
->with('error', '권한이 없습니다.');
|
->with('error', '권한이 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
helper('admin');
|
helper('admin');
|
||||||
$lgIdx = admin_effective_lg_idx();
|
$lgIdx = admin_effective_lg_idx();
|
||||||
if ($lgIdx === null || $lgIdx <= 0) {
|
if ($lgIdx === null || $lgIdx <= 0) {
|
||||||
return redirect()->to(site_url('admin/designated-shops'))
|
return redirect()->to(mgmt_url('designated-shops'))
|
||||||
->with('error', '작업할 지자체가 선택되지 않았습니다.');
|
->with('error', '작업할 지자체가 선택되지 않았습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$shop = $this->shopModel->find($id);
|
$shop = $this->shopModel->find($id);
|
||||||
if ($shop === null || (int) $shop->ds_lg_idx !== $lgIdx) {
|
if ($shop === null || (int) $shop->ds_lg_idx !== $lgIdx) {
|
||||||
return redirect()->to(site_url('admin/designated-shops'))
|
return redirect()->to(mgmt_url('designated-shops'))
|
||||||
->with('error', '해당 지정판매소를 찾을 수 없거나 수정할 수 없습니다.');
|
->with('error', '해당 지정판매소를 찾을 수 없거나 수정할 수 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +296,7 @@ class DesignatedShop extends BaseController
|
|||||||
|
|
||||||
$this->shopModel->update($id, $data);
|
$this->shopModel->update($id, $data);
|
||||||
|
|
||||||
return redirect()->to(site_url('admin/designated-shops'))
|
return redirect()->to(mgmt_url('designated-shops'))
|
||||||
->with('success', '지정판매소 정보가 수정되었습니다.');
|
->with('success', '지정판매소 정보가 수정되었습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,29 +307,95 @@ class DesignatedShop extends BaseController
|
|||||||
public function delete(int $id)
|
public function delete(int $id)
|
||||||
{
|
{
|
||||||
if (! $this->isSuperAdmin() && ! $this->isLocalAdmin()) {
|
if (! $this->isSuperAdmin() && ! $this->isLocalAdmin()) {
|
||||||
return redirect()->to(site_url('admin/designated-shops'))
|
return redirect()->to(mgmt_url('designated-shops'))
|
||||||
->with('error', '권한이 없습니다.');
|
->with('error', '권한이 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
helper('admin');
|
helper('admin');
|
||||||
$lgIdx = admin_effective_lg_idx();
|
$lgIdx = admin_effective_lg_idx();
|
||||||
if ($lgIdx === null || $lgIdx <= 0) {
|
if ($lgIdx === null || $lgIdx <= 0) {
|
||||||
return redirect()->to(site_url('admin/designated-shops'))
|
return redirect()->to(mgmt_url('designated-shops'))
|
||||||
->with('error', '작업할 지자체가 선택되지 않았습니다.');
|
->with('error', '작업할 지자체가 선택되지 않았습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$shop = $this->shopModel->find($id);
|
$shop = $this->shopModel->find($id);
|
||||||
if ($shop === null || (int) $shop->ds_lg_idx !== $lgIdx) {
|
if ($shop === null || (int) $shop->ds_lg_idx !== $lgIdx) {
|
||||||
return redirect()->to(site_url('admin/designated-shops'))
|
return redirect()->to(mgmt_url('designated-shops'))
|
||||||
->with('error', '해당 지정판매소를 찾을 수 없거나 삭제할 수 없습니다.');
|
->with('error', '해당 지정판매소를 찾을 수 없거나 삭제할 수 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->shopModel->delete($id);
|
$this->shopModel->delete($id);
|
||||||
|
|
||||||
return redirect()->to(site_url('admin/designated-shops'))
|
return redirect()->to(mgmt_url('designated-shops'))
|
||||||
->with('success', '지정판매소가 삭제되었습니다.');
|
->with('success', '지정판매소가 삭제되었습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P2-17: 지정판매소 지도 표시
|
||||||
|
*/
|
||||||
|
public function map()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if ($lgIdx === null || $lgIdx <= 0) {
|
||||||
|
return redirect()->to(work_area_home_url())
|
||||||
|
->with('error', '작업할 지자체가 선택되지 않았습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$shops = $this->shopModel
|
||||||
|
->where('ds_lg_idx', $lgIdx)
|
||||||
|
->where('ds_state', 1)
|
||||||
|
->findAll();
|
||||||
|
|
||||||
|
return $this->renderWorkPage('지정판매소 지도', 'admin/designated_shop/map', [
|
||||||
|
'shops' => $shops,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P2-18: 지정판매소 현황 (연도별 신규/취소)
|
||||||
|
*/
|
||||||
|
public function status()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if ($lgIdx === null || $lgIdx <= 0) {
|
||||||
|
return redirect()->to(work_area_home_url())
|
||||||
|
->with('error', '작업할 지자체가 선택되지 않았습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
// 연도별 신규등록 건수 (ds_designated_at 기준)
|
||||||
|
$newByYear = $db->query("
|
||||||
|
SELECT YEAR(ds_designated_at) as yr, COUNT(*) as cnt
|
||||||
|
FROM designated_shop
|
||||||
|
WHERE ds_lg_idx = ? AND ds_designated_at IS NOT NULL
|
||||||
|
GROUP BY YEAR(ds_designated_at)
|
||||||
|
ORDER BY yr DESC
|
||||||
|
", [$lgIdx])->getResult();
|
||||||
|
|
||||||
|
// 연도별 취소/비활성 건수 (ds_state != 1, ds_regdate 기준)
|
||||||
|
$cancelByYear = $db->query("
|
||||||
|
SELECT YEAR(ds_regdate) as yr, COUNT(*) as cnt
|
||||||
|
FROM designated_shop
|
||||||
|
WHERE ds_lg_idx = ? AND ds_state != 1
|
||||||
|
GROUP BY YEAR(ds_regdate)
|
||||||
|
ORDER BY yr DESC
|
||||||
|
", [$lgIdx])->getResult();
|
||||||
|
|
||||||
|
// 전체 현황 합계
|
||||||
|
$totalActive = $this->shopModel->where('ds_lg_idx', $lgIdx)->where('ds_state', 1)->countAllResults(false);
|
||||||
|
$totalInactive = $this->shopModel->where('ds_lg_idx', $lgIdx)->where('ds_state !=', 1)->countAllResults(false);
|
||||||
|
|
||||||
|
return $this->renderWorkPage('지정판매소 현황', 'admin/designated_shop/status', [
|
||||||
|
'newByYear' => $newByYear,
|
||||||
|
'cancelByYear' => $cancelByYear,
|
||||||
|
'totalActive' => $totalActive,
|
||||||
|
'totalInactive' => $totalInactive,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 지자체별 다음 판매소번호 생성 (lg_code + 3자리 일련번호)
|
* 지자체별 다음 판매소번호 생성 (lg_code + 3자리 일련번호)
|
||||||
* 문서: docs/기본 개발계획/22-판매소번호_일련번호_결정.md §3
|
* 문서: docs/기본 개발계획/22-판매소번호_일련번호_결정.md §3
|
||||||
|
|||||||
@@ -18,34 +18,32 @@ class FreeRecipient extends BaseController
|
|||||||
|
|
||||||
private function getCodeOptions(string $ckCode): array
|
private function getCodeOptions(string $ckCode): array
|
||||||
{
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
$kind = model(CodeKindModel::class)->where('ck_code', $ckCode)->first();
|
$kind = model(CodeKindModel::class)->where('ck_code', $ckCode)->first();
|
||||||
return $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true) : [];
|
|
||||||
|
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')->findAll();
|
$list = $this->model->where('fr_lg_idx', $lgIdx)->orderBy('fr_idx', 'DESC')->paginate(20);
|
||||||
|
$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]),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
return view('admin/layout', [
|
return $this->renderWorkPage('무료용 대상자 등록', 'admin/free_recipient/create', [
|
||||||
'title' => '무료용 대상자 등록',
|
|
||||||
'content' => view('admin/free_recipient/create', [
|
|
||||||
'typeCodes' => $this->getCodeOptions('H'),
|
'typeCodes' => $this->getCodeOptions('H'),
|
||||||
'dongCodes' => $this->getCodeOptions('D'),
|
'dongCodes' => $this->getCodeOptions('D'),
|
||||||
]),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,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' => '무료용 대상자 수정',
|
|
||||||
'content' => view('admin/free_recipient/edit', [
|
|
||||||
'item' => $item,
|
'item' => $item,
|
||||||
'typeCodes' => $this->getCodeOptions('H'),
|
'typeCodes' => $this->getCodeOptions('H'),
|
||||||
'dongCodes' => $this->getCodeOptions('D'),
|
'dongCodes' => $this->getCodeOptions('D'),
|
||||||
]),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,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 = [
|
||||||
@@ -122,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', '무료용 대상자가 삭제되었습니다.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class LocalGovernment extends BaseController
|
|||||||
|
|
||||||
private function isSuperAdmin(): bool
|
private function isSuperAdmin(): bool
|
||||||
{
|
{
|
||||||
return (int) session()->get('mb_level') === Roles::LEVEL_SUPER_ADMIN;
|
return Roles::isSuperAdminEquivalent((int) session()->get('mb_level'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,14 +29,15 @@ class LocalGovernment extends BaseController
|
|||||||
{
|
{
|
||||||
if (! $this->isSuperAdmin()) {
|
if (! $this->isSuperAdmin()) {
|
||||||
return redirect()->to(site_url('admin'))
|
return redirect()->to(site_url('admin'))
|
||||||
->with('error', '지자체 관리는 super admin만 접근할 수 있습니다.');
|
->with('error', '지자체 관리는 상위 관리자만 접근할 수 있습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$list = $this->lgModel->orderBy('lg_idx', 'DESC')->findAll();
|
$list = $this->lgModel->orderBy('lg_idx', 'DESC')->paginate(20);
|
||||||
|
$pager = $this->lgModel->pager;
|
||||||
|
|
||||||
return view('admin/layout', [
|
return view('admin/layout', [
|
||||||
'title' => '지자체 관리',
|
'title' => '지자체 관리',
|
||||||
'content' => view('admin/local_government/index', ['list' => $list]),
|
'content' => view('admin/local_government/index', ['list' => $list, 'pager' => $pager]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ class LocalGovernment extends BaseController
|
|||||||
{
|
{
|
||||||
if (! $this->isSuperAdmin()) {
|
if (! $this->isSuperAdmin()) {
|
||||||
return redirect()->to(site_url('admin/local-governments'))
|
return redirect()->to(site_url('admin/local-governments'))
|
||||||
->with('error', '지자체 등록은 super admin만 가능합니다.');
|
->with('error', '지자체 등록은 상위 관리자만 가능합니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('admin/layout', [
|
return view('admin/layout', [
|
||||||
@@ -63,7 +64,7 @@ class LocalGovernment extends BaseController
|
|||||||
{
|
{
|
||||||
if (! $this->isSuperAdmin()) {
|
if (! $this->isSuperAdmin()) {
|
||||||
return redirect()->to(site_url('admin/local-governments'))
|
return redirect()->to(site_url('admin/local-governments'))
|
||||||
->with('error', '지자체 등록은 super admin만 가능합니다.');
|
->with('error', '지자체 등록은 상위 관리자만 가능합니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$rules = [
|
$rules = [
|
||||||
@@ -95,5 +96,89 @@ class LocalGovernment extends BaseController
|
|||||||
return redirect()->to(site_url('admin/local-governments'))
|
return redirect()->to(site_url('admin/local-governments'))
|
||||||
->with('success', '지자체가 등록되었습니다.');
|
->with('success', '지자체가 등록되었습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지자체 수정 폼 (P2-19)
|
||||||
|
*/
|
||||||
|
public function edit(int $id)
|
||||||
|
{
|
||||||
|
if (! $this->isSuperAdmin()) {
|
||||||
|
return redirect()->to(site_url('admin/local-governments'))
|
||||||
|
->with('error', '지자체 수정은 super admin만 가능합니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = $this->lgModel->find($id);
|
||||||
|
if ($item === null) {
|
||||||
|
return redirect()->to(site_url('admin/local-governments'))
|
||||||
|
->with('error', '지자체를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('admin/layout', [
|
||||||
|
'title' => '지자체 수정',
|
||||||
|
'content' => view('admin/local_government/edit', ['item' => $item]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지자체 수정 처리 (P2-19)
|
||||||
|
*/
|
||||||
|
public function update(int $id)
|
||||||
|
{
|
||||||
|
if (! $this->isSuperAdmin()) {
|
||||||
|
return redirect()->to(site_url('admin/local-governments'))
|
||||||
|
->with('error', '지자체 수정은 super admin만 가능합니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = $this->lgModel->find($id);
|
||||||
|
if ($item === null) {
|
||||||
|
return redirect()->to(site_url('admin/local-governments'))
|
||||||
|
->with('error', '지자체를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'lg_name' => 'required|max_length[100]',
|
||||||
|
'lg_sido' => 'required|max_length[50]',
|
||||||
|
'lg_gugun' => 'required|max_length[50]',
|
||||||
|
'lg_addr' => 'permit_empty|max_length[255]',
|
||||||
|
'lg_state' => 'required|in_list[0,1]',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (! $this->validate($rules)) {
|
||||||
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->lgModel->update($id, [
|
||||||
|
'lg_name' => (string) $this->request->getPost('lg_name'),
|
||||||
|
'lg_sido' => (string) $this->request->getPost('lg_sido'),
|
||||||
|
'lg_gugun' => (string) $this->request->getPost('lg_gugun'),
|
||||||
|
'lg_addr' => (string) $this->request->getPost('lg_addr'),
|
||||||
|
'lg_state' => (int) $this->request->getPost('lg_state'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->to(site_url('admin/local-governments'))
|
||||||
|
->with('success', '지자체가 수정되었습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지자체 삭제 (P2-19)
|
||||||
|
*/
|
||||||
|
public function delete(int $id)
|
||||||
|
{
|
||||||
|
if (! $this->isSuperAdmin()) {
|
||||||
|
return redirect()->to(site_url('admin/local-governments'))
|
||||||
|
->with('error', '지자체 삭제는 super admin만 가능합니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = $this->lgModel->find($id);
|
||||||
|
if ($item === null) {
|
||||||
|
return redirect()->to(site_url('admin/local-governments'))
|
||||||
|
->with('error', '지자체를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->lgModel->update($id, ['lg_state' => 0]);
|
||||||
|
|
||||||
|
return redirect()->to(site_url('admin/local-governments'))
|
||||||
|
->with('success', '지자체가 비활성화되었습니다.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ class Manager extends BaseController
|
|||||||
|
|
||||||
private function getCodeOptions(string $ckCode): array
|
private function getCodeOptions(string $ckCode): array
|
||||||
{
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
$kind = model(CodeKindModel::class)->where('ck_code', $ckCode)->first();
|
$kind = model(CodeKindModel::class)->where('ck_code', $ckCode)->first();
|
||||||
return $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true) : [];
|
|
||||||
|
return $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
@@ -27,31 +30,28 @@ 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')->findAll();
|
$list = $this->model->where('mg_lg_idx', $lgIdx)->orderBy('mg_idx', 'DESC')->paginate(20);
|
||||||
|
$pager = $this->model->pager;
|
||||||
|
|
||||||
return view('admin/layout', [
|
return $this->renderWorkPage('담당자 관리', 'admin/manager/index', ['list' => $list, 'pager' => $pager]);
|
||||||
'title' => '담당자 관리',
|
|
||||||
'content' => view('admin/manager/index', ['list' => $list]),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
return view('admin/layout', [
|
return $this->renderWorkPage('담당자 등록', 'admin/manager/create', [
|
||||||
'title' => '담당자 등록',
|
|
||||||
'content' => view('admin/manager/create', [
|
|
||||||
'deptCodes' => $this->getCodeOptions('S'),
|
'deptCodes' => $this->getCodeOptions('S'),
|
||||||
'positionCodes' => $this->getCodeOptions('T'),
|
'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_tel' => 'permit_empty|max_length[20]',
|
'mg_tel' => 'permit_empty|max_length[20]',
|
||||||
@@ -74,33 +74,30 @@ 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' => '담당자 수정',
|
|
||||||
'content' => view('admin/manager/edit', [
|
|
||||||
'item' => $item,
|
'item' => $item,
|
||||||
'deptCodes' => $this->getCodeOptions('S'),
|
'deptCodes' => $this->getCodeOptions('S'),
|
||||||
'positionCodes' => $this->getCodeOptions('T'),
|
'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 = [
|
||||||
@@ -121,18 +118,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', '담당자가 삭제되었습니다.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,18 +30,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 +66,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',
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -73,7 +97,9 @@ class Menu extends BaseController
|
|||||||
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']);
|
||||||
}
|
}
|
||||||
@@ -205,9 +231,39 @@ class Menu extends BaseController
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
$levels = array_map('intval', $levels);
|
$levels = array_map('intval', $levels);
|
||||||
// super admin(4)은 DB 저장 대상 아님. 1,2,3은 그대로 저장
|
// super/본부(4·5)는 mm_level 저장 대상 아님. 1,2,3은 그대로 저장
|
||||||
$levels = array_filter($levels, static fn ($v) => $v > 0 && $v !== \Config\Roles::LEVEL_SUPER_ADMIN);
|
$levels = array_filter($levels, static fn ($v) => $v > 0 && ! \Config\Roles::isSuperAdminEquivalent($v));
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,30 +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')->findAll();
|
$list = $builder->orderBy('pu_bag_code', 'ASC')->orderBy('pu_start_date', 'DESC')->paginate(20);
|
||||||
|
$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,
|
|
||||||
]),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
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', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
$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/packaging_unit/create', ['bagCodes' => $bagCodes]);
|
||||||
'title' => '포장 단위 등록',
|
|
||||||
'content' => view('admin/packaging_unit/create', ['bagCodes' => $bagCodes]),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store()
|
public function store()
|
||||||
@@ -85,7 +81,7 @@ class PackagingUnit extends BaseController
|
|||||||
$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 : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,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', '포장 단위를 찾을 수 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
$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/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 = [
|
||||||
@@ -159,8 +153,8 @@ class PackagingUnit extends BaseController
|
|||||||
'puh_field' => $field,
|
'puh_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'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,34 +173,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]),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
55
app/Controllers/Admin/PasswordChange.php
Normal file
55
app/Controllers/Admin/PasswordChange.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Controllers\BaseController;
|
||||||
|
use App\Models\MemberModel;
|
||||||
|
|
||||||
|
class PasswordChange extends BaseController
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
|
||||||
|
return $this->renderWorkPage('비밀번호 변경', 'admin/password_change/index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$rules = [
|
||||||
|
'current_password' => 'required',
|
||||||
|
'new_password' => 'required|min_length[4]|max_length[255]',
|
||||||
|
'new_password_confirm' => 'required|matches[new_password]',
|
||||||
|
];
|
||||||
|
$messages = [
|
||||||
|
'current_password' => ['required' => '현재 비밀번호를 입력해 주세요.'],
|
||||||
|
'new_password' => [
|
||||||
|
'required' => '새 비밀번호를 입력해 주세요.',
|
||||||
|
'min_length' => '비밀번호는 4자 이상이어야 합니다.',
|
||||||
|
],
|
||||||
|
'new_password_confirm' => [
|
||||||
|
'required' => '비밀번호 확인을 입력해 주세요.',
|
||||||
|
'matches' => '새 비밀번호가 일치하지 않습니다.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (! $this->validate($rules, $messages)) {
|
||||||
|
return redirect()->back()->with('errors', $this->validator->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
$mbIdx = session()->get('mb_idx');
|
||||||
|
$memberModel = model(MemberModel::class);
|
||||||
|
$member = $memberModel->find($mbIdx);
|
||||||
|
|
||||||
|
if (!$member || !password_verify($this->request->getPost('current_password'), $member->mb_passwd)) {
|
||||||
|
return redirect()->back()->with('error', '현재 비밀번호가 올바르지 않습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$memberModel->update($mbIdx, [
|
||||||
|
'mb_passwd' => password_hash($this->request->getPost('new_password'), PASSWORD_DEFAULT),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->to(mgmt_url('password-change'))->with('success', '비밀번호가 변경되었습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,105 +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')->findAll();
|
$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') ?? ''));
|
||||||
|
|
||||||
return view('admin/layout', [
|
$builder = $this->model->where('sa_lg_idx', $lgIdx);
|
||||||
'title' => '판매 대행소 관리',
|
if ($saKind !== '') {
|
||||||
'content' => view('admin/sales_agency/index', ['list' => $list]),
|
$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;
|
||||||
|
|
||||||
|
$queryForPager = [
|
||||||
|
'sa_kind' => $saKind,
|
||||||
|
'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_kind' => 'required|max_length[50]',
|
||||||
|
'sa_code' => 'required|max_length[50]',
|
||||||
'sa_name' => 'required|max_length[100]',
|
'sa_name' => 'required|max_length[100]',
|
||||||
'sa_biz_no' => 'permit_empty|max_length[20]',
|
|
||||||
'sa_rep_name' => 'permit_empty|max_length[50]',
|
|
||||||
'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_addr' => $this->request->getPost('sa_addr') ?? '',
|
|
||||||
'sa_state' => 1,
|
|
||||||
'sa_regdate' => date('Y-m-d H:i:s'),
|
'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');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
$item = $this->model->find($id);
|
$item = $this->model->find($id);
|
||||||
if (!$item || (int) $item->sa_lg_idx !== admin_effective_lg_idx()) {
|
if (! $item || ! $lgIdx || (int) $item->sa_lg_idx !== $lgIdx) {
|
||||||
return redirect()->to(site_url('admin/sales-agencies'))->with('error', '대행소를 찾을 수 없습니다.');
|
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_kind' => 'required|max_length[50]',
|
||||||
|
'sa_code' => 'required|max_length[50]',
|
||||||
'sa_name' => 'required|max_length[100]',
|
'sa_name' => 'required|max_length[100]',
|
||||||
'sa_state' => 'required|in_list[0,1]',
|
|
||||||
];
|
];
|
||||||
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');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
$item = $this->model->find($id);
|
$item = $this->model->find($id);
|
||||||
if (!$item || (int) $item->sa_lg_idx !== admin_effective_lg_idx()) {
|
if (! $item || ! $lgIdx || (int) $item->sa_lg_idx !== $lgIdx) {
|
||||||
return redirect()->to(site_url('admin/sales-agencies'))->with('error', '대행소를 찾을 수 없습니다.');
|
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', '삭제되었습니다.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
431
app/Controllers/Admin/SalesReport.php
Normal file
431
app/Controllers/Admin/SalesReport.php
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Controllers\BaseController;
|
||||||
|
use App\Models\BagSaleModel;
|
||||||
|
use App\Models\BagIssueModel;
|
||||||
|
use App\Models\BagReceivingModel;
|
||||||
|
use App\Models\BagInventoryModel;
|
||||||
|
|
||||||
|
class SalesReport extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* P5-01: 판매 대장 (일자별/기간별)
|
||||||
|
*/
|
||||||
|
public function salesLedger()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
||||||
|
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
||||||
|
$mode = $this->request->getGet('mode') ?? 'daily'; // daily or period
|
||||||
|
|
||||||
|
$saleModel = model(BagSaleModel::class);
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
if ($mode === 'daily') {
|
||||||
|
$result = $db->query("
|
||||||
|
SELECT bs_sale_date, bs_ds_name, bs_bag_code, bs_bag_name, bs_type,
|
||||||
|
SUM(ABS(bs_qty)) as total_qty,
|
||||||
|
SUM(bs_amount) as total_amount
|
||||||
|
FROM bag_sale
|
||||||
|
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ? AND bs_type IN('sale','return')
|
||||||
|
GROUP BY bs_sale_date, bs_ds_name, bs_bag_code, bs_bag_name, bs_type
|
||||||
|
ORDER BY bs_sale_date DESC, bs_ds_name, bs_bag_code
|
||||||
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||||
|
} else {
|
||||||
|
$result = $db->query("
|
||||||
|
SELECT bs_ds_name, bs_bag_code, bs_bag_name,
|
||||||
|
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
||||||
|
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount,
|
||||||
|
SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty,
|
||||||
|
SUM(CASE WHEN bs_type='return' THEN bs_amount ELSE 0 END) as return_amount
|
||||||
|
FROM bag_sale
|
||||||
|
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
|
||||||
|
GROUP BY bs_ds_name, bs_bag_code, bs_bag_name
|
||||||
|
ORDER BY bs_ds_name, bs_bag_code
|
||||||
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->renderWorkPage('판매 대장', 'admin/sales_report/sales_ledger', compact('result', 'startDate', 'endDate', 'mode'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P5-02: 일계표
|
||||||
|
*/
|
||||||
|
public function dailySummary()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$date = $this->request->getGet('date') ?? date('Y-m-d');
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
// 당일 판매
|
||||||
|
$daily = $db->query("
|
||||||
|
SELECT bs_bag_code, bs_bag_name,
|
||||||
|
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
||||||
|
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount
|
||||||
|
FROM bag_sale
|
||||||
|
WHERE bs_lg_idx = ? AND bs_sale_date = ?
|
||||||
|
GROUP BY bs_bag_code, bs_bag_name
|
||||||
|
ORDER BY bs_bag_code
|
||||||
|
", [$lgIdx, $date])->getResult();
|
||||||
|
|
||||||
|
// 당월 누계
|
||||||
|
$monthStart = date('Y-m-01', strtotime($date));
|
||||||
|
$monthly = $db->query("
|
||||||
|
SELECT bs_bag_code, bs_bag_name,
|
||||||
|
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
||||||
|
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount
|
||||||
|
FROM bag_sale
|
||||||
|
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
|
||||||
|
GROUP BY bs_bag_code, bs_bag_name
|
||||||
|
ORDER BY bs_bag_code
|
||||||
|
", [$lgIdx, $monthStart, $date])->getResult();
|
||||||
|
|
||||||
|
return $this->renderWorkPage('일계표', 'admin/sales_report/daily_summary', compact('daily', 'monthly', 'date'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P5-03: 기간별 판매현황
|
||||||
|
*/
|
||||||
|
public function periodSales()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
||||||
|
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
$result = $db->query("
|
||||||
|
SELECT bs_bag_code, bs_bag_name,
|
||||||
|
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
||||||
|
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount,
|
||||||
|
SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty,
|
||||||
|
SUM(CASE WHEN bs_type='return' THEN bs_amount ELSE 0 END) as return_amount
|
||||||
|
FROM bag_sale
|
||||||
|
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
|
||||||
|
GROUP BY bs_bag_code, bs_bag_name
|
||||||
|
ORDER BY bs_bag_code
|
||||||
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||||
|
|
||||||
|
return $this->renderWorkPage('기간별 판매현황', 'admin/sales_report/period_sales', compact('result', 'startDate', 'endDate'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P5-04: 년 판매 현황 (월별)
|
||||||
|
*/
|
||||||
|
public function yearlySales()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$year = $this->request->getGet('year') ?? date('Y');
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
$result = $db->query("
|
||||||
|
SELECT bs_bag_code, bs_bag_name,
|
||||||
|
SUM(CASE WHEN MONTH(bs_sale_date)=1 THEN ABS(bs_qty) ELSE 0 END) as m01,
|
||||||
|
SUM(CASE WHEN MONTH(bs_sale_date)=2 THEN ABS(bs_qty) ELSE 0 END) as m02,
|
||||||
|
SUM(CASE WHEN MONTH(bs_sale_date)=3 THEN ABS(bs_qty) ELSE 0 END) as m03,
|
||||||
|
SUM(CASE WHEN MONTH(bs_sale_date)=4 THEN ABS(bs_qty) ELSE 0 END) as m04,
|
||||||
|
SUM(CASE WHEN MONTH(bs_sale_date)=5 THEN ABS(bs_qty) ELSE 0 END) as m05,
|
||||||
|
SUM(CASE WHEN MONTH(bs_sale_date)=6 THEN ABS(bs_qty) ELSE 0 END) as m06,
|
||||||
|
SUM(CASE WHEN MONTH(bs_sale_date)=7 THEN ABS(bs_qty) ELSE 0 END) as m07,
|
||||||
|
SUM(CASE WHEN MONTH(bs_sale_date)=8 THEN ABS(bs_qty) ELSE 0 END) as m08,
|
||||||
|
SUM(CASE WHEN MONTH(bs_sale_date)=9 THEN ABS(bs_qty) ELSE 0 END) as m09,
|
||||||
|
SUM(CASE WHEN MONTH(bs_sale_date)=10 THEN ABS(bs_qty) ELSE 0 END) as m10,
|
||||||
|
SUM(CASE WHEN MONTH(bs_sale_date)=11 THEN ABS(bs_qty) ELSE 0 END) as m11,
|
||||||
|
SUM(CASE WHEN MONTH(bs_sale_date)=12 THEN ABS(bs_qty) ELSE 0 END) as m12,
|
||||||
|
SUM(ABS(bs_qty)) as total
|
||||||
|
FROM bag_sale
|
||||||
|
WHERE bs_lg_idx = ? AND YEAR(bs_sale_date) = ? AND bs_type = 'sale'
|
||||||
|
GROUP BY bs_bag_code, bs_bag_name
|
||||||
|
ORDER BY bs_bag_code
|
||||||
|
", [$lgIdx, $year])->getResult();
|
||||||
|
|
||||||
|
return $this->renderWorkPage('년 판매 현황', 'admin/sales_report/yearly_sales', compact('result', 'year'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P5-05: 지정판매소별 판매현황
|
||||||
|
*/
|
||||||
|
public function shopSales()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
||||||
|
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
$result = $db->query("
|
||||||
|
SELECT bs_ds_name,
|
||||||
|
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
||||||
|
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount,
|
||||||
|
SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty,
|
||||||
|
SUM(CASE WHEN bs_type='return' THEN ABS(bs_amount) ELSE 0 END) as return_amount
|
||||||
|
FROM bag_sale
|
||||||
|
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
|
||||||
|
GROUP BY bs_ds_name
|
||||||
|
ORDER BY bs_ds_name
|
||||||
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||||
|
|
||||||
|
return $this->renderWorkPage('지정판매소별 판매현황', 'admin/sales_report/shop_sales', compact('result', 'startDate', 'endDate'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P5-06: 홈택스 세금계산서 엑셀 내보내기
|
||||||
|
*/
|
||||||
|
public function hometaxExport()
|
||||||
|
{
|
||||||
|
helper(['admin', 'export']);
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
||||||
|
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
||||||
|
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$rows = $db->query("
|
||||||
|
SELECT bs.bs_sale_date, ds.ds_biz_no as buyer_biz_no, ds.ds_name as buyer_name,
|
||||||
|
bs.bs_bag_name, ABS(bs.bs_qty) as qty, bs.bs_unit_price, bs.bs_amount
|
||||||
|
FROM bag_sale bs
|
||||||
|
LEFT JOIN designated_shop ds ON bs.bs_ds_idx = ds.ds_idx
|
||||||
|
WHERE bs.bs_lg_idx = ? AND bs.bs_sale_date BETWEEN ? AND ? AND bs.bs_type = 'sale'
|
||||||
|
ORDER BY bs.bs_sale_date, ds.ds_name
|
||||||
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||||
|
|
||||||
|
// 지자체 정보 (공급자)
|
||||||
|
$lg = model(\App\Models\LocalGovernmentModel::class)->find($lgIdx);
|
||||||
|
$supplierBizNo = $lg->lg_biz_no ?? '';
|
||||||
|
$supplierName = $lg->lg_name ?? '';
|
||||||
|
|
||||||
|
$csvRows = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$amount = (int) $row->bs_amount;
|
||||||
|
$tax = (int) round($amount * 0.1);
|
||||||
|
$csvRows[] = [
|
||||||
|
str_replace('-', '', $row->bs_sale_date), // 작성일자 (YYYYMMDD)
|
||||||
|
$supplierBizNo, // 공급자사업자번호
|
||||||
|
$supplierName, // 공급자상호
|
||||||
|
$row->buyer_biz_no ?? '', // 공급받는자사업자번호
|
||||||
|
$row->buyer_name ?? '', // 공급받는자상호
|
||||||
|
$row->bs_bag_name, // 품목
|
||||||
|
(int) $row->qty, // 수량
|
||||||
|
(int) $row->bs_unit_price, // 단가
|
||||||
|
$amount, // 공급가액
|
||||||
|
$tax, // 세액
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export_csv(
|
||||||
|
'홈택스_세금계산서_' . date('Ymd') . '.csv',
|
||||||
|
['작성일자', '공급자사업자번호', '공급자상호', '공급받는자사업자번호', '공급받는자상호', '품목', '수량', '단가', '공급가액', '세액'],
|
||||||
|
$csvRows
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P5-08: 반품/파기 현황
|
||||||
|
*/
|
||||||
|
public function returns()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
||||||
|
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
$result = $db->query("
|
||||||
|
SELECT bs_sale_date, bs_ds_name, bs_bag_code, bs_bag_name, bs_type,
|
||||||
|
ABS(bs_qty) as qty, ABS(bs_amount) as amount
|
||||||
|
FROM bag_sale
|
||||||
|
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ? AND bs_type IN('return','cancel')
|
||||||
|
ORDER BY bs_sale_date DESC, bs_ds_name
|
||||||
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||||
|
|
||||||
|
return $this->renderWorkPage('반품/파기 현황', 'admin/sales_report/returns', compact('result', 'startDate', 'endDate'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P5-10: LOT 수불 조회
|
||||||
|
*/
|
||||||
|
public function lotFlow()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$lotNo = $this->request->getGet('lot_no') ?? '';
|
||||||
|
$order = null;
|
||||||
|
$items = [];
|
||||||
|
$receivings = [];
|
||||||
|
|
||||||
|
if ($lotNo !== '') {
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$order = $db->query("SELECT * FROM bag_order WHERE bo_lg_idx = ? AND bo_lot_no = ?", [$lgIdx, $lotNo])->getRow();
|
||||||
|
if ($order) {
|
||||||
|
$items = $db->query("SELECT * FROM bag_order_item WHERE boi_bo_idx = ? ORDER BY boi_bag_code", [(int) $order->bo_idx])->getResult();
|
||||||
|
$receivings = $db->query("SELECT * FROM bag_receiving WHERE br_bo_idx = ? ORDER BY br_receive_date", [(int) $order->bo_idx])->getResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->renderWorkPage('LOT 수불 조회', 'admin/sales_report/lot_flow', compact('lotNo', 'order', 'items', 'receivings'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P5-11: 기타 입출고 목록
|
||||||
|
*/
|
||||||
|
public function miscFlow()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
||||||
|
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
// bag_misc_flow 테이블이 존재하는지 확인
|
||||||
|
$tableExists = $db->query("SHOW TABLES LIKE 'bag_misc_flow'")->getNumRows() > 0;
|
||||||
|
$result = [];
|
||||||
|
if ($tableExists) {
|
||||||
|
$result = $db->query("
|
||||||
|
SELECT * FROM bag_misc_flow
|
||||||
|
WHERE bmf_lg_idx = ? AND bmf_date BETWEEN ? AND ?
|
||||||
|
ORDER BY bmf_date DESC, bmf_idx DESC
|
||||||
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 봉투 코드 목록
|
||||||
|
$kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
$bagCodes = $kindO ? model(\App\Models\CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true, $lgIdx) : [];
|
||||||
|
|
||||||
|
return $this->renderWorkPage('기타 입출고', 'admin/sales_report/misc_flow', compact('result', 'startDate', 'endDate', 'bagCodes', 'tableExists'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P5-11: 기타 입출고 등록 처리
|
||||||
|
*/
|
||||||
|
public function miscFlowStore()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (!$lgIdx) return redirect()->to(mgmt_url('reports/misc-flow'))->with('error', '지자체를 선택해 주세요.');
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'bmf_type' => 'required|in_list[in,out]',
|
||||||
|
'bmf_bag_code' => 'required|max_length[50]',
|
||||||
|
'bmf_qty' => 'required|is_natural_no_zero',
|
||||||
|
'bmf_date' => 'required|valid_date[Y-m-d]',
|
||||||
|
'bmf_reason' => 'required|max_length[200]',
|
||||||
|
];
|
||||||
|
if (! $this->validate($rules)) {
|
||||||
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
$bagCode = $this->request->getPost('bmf_bag_code');
|
||||||
|
$qty = (int) $this->request->getPost('bmf_qty');
|
||||||
|
$type = $this->request->getPost('bmf_type');
|
||||||
|
|
||||||
|
// 봉투명 조회
|
||||||
|
$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;
|
||||||
|
$bagName = $detail ? $detail->cd_name : '';
|
||||||
|
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$db->transStart();
|
||||||
|
|
||||||
|
$db->query("
|
||||||
|
INSERT INTO bag_misc_flow (bmf_lg_idx, bmf_type, bmf_bag_code, bmf_bag_name, bmf_qty, bmf_date, bmf_reason, bmf_regdate)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
", [$lgIdx, $type, $bagCode, $bagName, $qty, $this->request->getPost('bmf_date'), $this->request->getPost('bmf_reason'), date('Y-m-d H:i:s')]);
|
||||||
|
|
||||||
|
// 재고 조정
|
||||||
|
$delta = ($type === 'in') ? $qty : -$qty;
|
||||||
|
model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $bagName, $delta);
|
||||||
|
|
||||||
|
$db->transComplete();
|
||||||
|
|
||||||
|
return redirect()->to(mgmt_url('reports/misc-flow'))->with('success', '기타 입출고가 등록되었습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P5-07: 봉투 수불 현황
|
||||||
|
*/
|
||||||
|
public function supplyDemand()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
||||||
|
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
// 입고 합계
|
||||||
|
$receiving = $db->query("
|
||||||
|
SELECT br_bag_code, br_bag_name,
|
||||||
|
SUM(br_qty_sheet) as recv_qty
|
||||||
|
FROM bag_receiving
|
||||||
|
WHERE br_lg_idx = ? AND br_receive_date BETWEEN ? AND ?
|
||||||
|
GROUP BY br_bag_code, br_bag_name
|
||||||
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||||
|
|
||||||
|
// 판매 합계
|
||||||
|
$sales = $db->query("
|
||||||
|
SELECT bs_bag_code, bs_bag_name,
|
||||||
|
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
||||||
|
SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty
|
||||||
|
FROM bag_sale
|
||||||
|
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
|
||||||
|
GROUP BY bs_bag_code, bs_bag_name
|
||||||
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||||
|
|
||||||
|
// 불출 합계
|
||||||
|
$issues = $db->query("
|
||||||
|
SELECT bi2_bag_code, bi2_bag_name,
|
||||||
|
SUM(bi2_qty) as issue_qty
|
||||||
|
FROM bag_issue
|
||||||
|
WHERE bi2_lg_idx = ? AND bi2_issue_date BETWEEN ? AND ? AND bi2_status = 'normal'
|
||||||
|
GROUP BY bi2_bag_code, bi2_bag_name
|
||||||
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||||
|
|
||||||
|
// 현재 재고
|
||||||
|
$inventory = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->findAll();
|
||||||
|
|
||||||
|
return $this->renderWorkPage('봉투 수불 현황', 'admin/sales_report/supply_demand', compact('receiving', 'sales', 'issues', 'inventory', 'startDate', 'endDate'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,12 +9,12 @@ use Config\Roles;
|
|||||||
class SelectLocalGovernment extends BaseController
|
class SelectLocalGovernment extends BaseController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 지자체 선택 화면 (super admin 전용)
|
* 지자체 선택 화면 (super·본부 관리자)
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
if ((int) session()->get('mb_level') !== Roles::LEVEL_SUPER_ADMIN) {
|
if (! Roles::isSuperAdminEquivalent((int) session()->get('mb_level'))) {
|
||||||
return redirect()->to(site_url('admin'))->with('error', '지자체 선택은 super admin만 사용할 수 있습니다.');
|
return redirect()->to(site_url('admin'))->with('error', '지자체 선택은 상위 관리자만 사용할 수 있습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$list = model(LocalGovernmentModel::class)
|
$list = model(LocalGovernmentModel::class)
|
||||||
@@ -35,8 +35,8 @@ class SelectLocalGovernment extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function store()
|
public function store()
|
||||||
{
|
{
|
||||||
if ((int) session()->get('mb_level') !== Roles::LEVEL_SUPER_ADMIN) {
|
if (! Roles::isSuperAdminEquivalent((int) session()->get('mb_level'))) {
|
||||||
return redirect()->to(site_url('admin'))->with('error', '지자체 선택은 super admin만 사용할 수 있습니다.');
|
return redirect()->to(site_url('admin'))->with('error', '지자체 선택은 상위 관리자만 사용할 수 있습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$lgIdx = (int) $this->request->getPost('lg_idx');
|
$lgIdx = (int) $this->request->getPost('lg_idx');
|
||||||
|
|||||||
162
app/Controllers/Admin/ShopOrder.php
Normal file
162
app/Controllers/Admin/ShopOrder.php
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Controllers\BaseController;
|
||||||
|
use App\Models\ShopOrderModel;
|
||||||
|
use App\Models\ShopOrderItemModel;
|
||||||
|
use App\Models\DesignatedShopModel;
|
||||||
|
use App\Models\BagPriceModel;
|
||||||
|
use App\Models\PackagingUnitModel;
|
||||||
|
use App\Models\CodeKindModel;
|
||||||
|
use App\Models\CodeDetailModel;
|
||||||
|
|
||||||
|
class ShopOrder extends BaseController
|
||||||
|
{
|
||||||
|
private ShopOrderModel $orderModel;
|
||||||
|
private ShopOrderItemModel $itemModel;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->orderModel = model(ShopOrderModel::class);
|
||||||
|
$this->itemModel = model(ShopOrderItemModel::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder = $this->orderModel->where('so_lg_idx', $lgIdx);
|
||||||
|
$startDate = $this->request->getGet('start_date');
|
||||||
|
$endDate = $this->request->getGet('end_date');
|
||||||
|
if ($startDate) {
|
||||||
|
$builder->where('so_delivery_date >=', $startDate);
|
||||||
|
}
|
||||||
|
if ($endDate) {
|
||||||
|
$builder->where('so_delivery_date <=', $endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = $builder->orderBy('so_idx', 'DESC')->paginate(20);
|
||||||
|
$pager = $this->orderModel->pager;
|
||||||
|
|
||||||
|
return $this->renderWorkPage('주문 접수 관리', 'admin/shop_order/index', compact('list', 'startDate', 'endDate', 'pager'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
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();
|
||||||
|
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
||||||
|
|
||||||
|
return $this->renderWorkPage('주문 접수', 'admin/shop_order/create', compact('shops', 'bagCodes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'so_ds_idx' => 'required|is_natural_no_zero',
|
||||||
|
'so_delivery_date' => 'required|valid_date[Y-m-d]',
|
||||||
|
'so_payment_type' => 'required|in_list[이체,가상계좌]',
|
||||||
|
];
|
||||||
|
if (! $this->validate($rules)) {
|
||||||
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$db->transStart();
|
||||||
|
|
||||||
|
$dsIdx = (int) $this->request->getPost('so_ds_idx');
|
||||||
|
$shop = model(DesignatedShopModel::class)->find($dsIdx);
|
||||||
|
|
||||||
|
$this->orderModel->insert([
|
||||||
|
'so_lg_idx' => $lgIdx,
|
||||||
|
'so_ds_idx' => $dsIdx,
|
||||||
|
'so_ds_name' => $shop ? $shop->ds_name : '',
|
||||||
|
'so_order_date' => date('Y-m-d'),
|
||||||
|
'so_delivery_date' => $this->request->getPost('so_delivery_date'),
|
||||||
|
'so_payment_type' => $this->request->getPost('so_payment_type'),
|
||||||
|
'so_status' => 'normal',
|
||||||
|
'so_orderer_idx' => session()->get('mb_idx'),
|
||||||
|
'so_regdate' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
$soIdx = (int) $this->orderModel->getInsertID();
|
||||||
|
|
||||||
|
$bagCodes = $this->request->getPost('item_bag_code') ?? [];
|
||||||
|
$qtys = $this->request->getPost('item_qty') ?? [];
|
||||||
|
$totalQty = 0;
|
||||||
|
$totalAmt = 0;
|
||||||
|
|
||||||
|
foreach ($bagCodes as $i => $code) {
|
||||||
|
if (empty($code) || empty($qtys[$i])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$qty = (int) $qtys[$i];
|
||||||
|
|
||||||
|
$price = model(BagPriceModel::class)->where('bp_lg_idx', $lgIdx)->where('bp_bag_code', $code)->where('bp_state', 1)->first();
|
||||||
|
$unitPrice = $price ? (float) $price->bp_consumer : 0;
|
||||||
|
$amount = $unitPrice * $qty;
|
||||||
|
|
||||||
|
$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;
|
||||||
|
if ($unit && (int) $unit->pu_total_per_box > 0) {
|
||||||
|
$boxCount = intdiv($qty, (int) $unit->pu_total_per_box);
|
||||||
|
$remainder = $qty % (int) $unit->pu_total_per_box;
|
||||||
|
if ((int) $unit->pu_pack_per_sheet > 0) {
|
||||||
|
$packCount = intdiv($remainder, (int) $unit->pu_pack_per_sheet);
|
||||||
|
$sheetCount = $remainder % (int) $unit->pu_pack_per_sheet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
$detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $code, $lgIdx) : null;
|
||||||
|
|
||||||
|
$this->itemModel->insert([
|
||||||
|
'soi_so_idx' => $soIdx,
|
||||||
|
'soi_bag_code' => $code,
|
||||||
|
'soi_bag_name' => $detail ? $detail->cd_name : '',
|
||||||
|
'soi_unit_price' => $unitPrice,
|
||||||
|
'soi_qty' => $qty,
|
||||||
|
'soi_amount' => $amount,
|
||||||
|
'soi_box_count' => $boxCount,
|
||||||
|
'soi_pack_count' => $packCount,
|
||||||
|
'soi_sheet_count' => $sheetCount,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$totalQty += $qty;
|
||||||
|
$totalAmt += $amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->orderModel->update($soIdx, ['so_total_qty' => $totalQty, 'so_total_amount' => $totalAmt]);
|
||||||
|
$db->transComplete();
|
||||||
|
|
||||||
|
return redirect()->to(mgmt_url('shop-orders'))->with('success', '주문이 접수되었습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancel(int $id)
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$order = $this->orderModel->find($id);
|
||||||
|
if (! $order || (int) $order->so_lg_idx !== admin_effective_lg_idx()) {
|
||||||
|
return redirect()->to(mgmt_url('shop-orders'))->with('error', '주문을 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->orderModel->update($id, ['so_status' => 'cancelled']);
|
||||||
|
|
||||||
|
return redirect()->to(mgmt_url('shop-orders'))->with('success', '주문이 취소되었습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,8 @@ class User extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function index(): string
|
public function index(): string
|
||||||
{
|
{
|
||||||
$list = $this->memberModel->orderBy('mb_idx', 'DESC')->findAll();
|
$list = $this->memberModel->orderBy('mb_idx', 'DESC')->paginate(20);
|
||||||
|
$pager = $this->memberModel->pager;
|
||||||
$approvalMap = [];
|
$approvalMap = [];
|
||||||
try {
|
try {
|
||||||
$memberIds = array_map(static fn ($row) => (int) $row->mb_idx, $list);
|
$memberIds = array_map(static fn ($row) => (int) $row->mb_idx, $list);
|
||||||
@@ -56,6 +57,7 @@ class User extends BaseController
|
|||||||
'list' => $list,
|
'list' => $list,
|
||||||
'roles' => $this->roles,
|
'roles' => $this->roles,
|
||||||
'approvalMap' => $approvalMap,
|
'approvalMap' => $approvalMap,
|
||||||
|
'pager' => $pager,
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -119,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', [
|
||||||
@@ -175,9 +179,26 @@ 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 admin만 super admin(4) 부여 가능, 그 외는 1~3만 허용.
|
* super/본부만 4·5 부여 가능, 지자체 관리자는 1~3만.
|
||||||
*
|
*
|
||||||
* @return array<int,string>
|
* @return array<int,string>
|
||||||
*/
|
*/
|
||||||
@@ -185,10 +206,11 @@ class User extends BaseController
|
|||||||
{
|
{
|
||||||
$levelNames = $this->roles->levelNames;
|
$levelNames = $this->roles->levelNames;
|
||||||
$myLevel = (int) session()->get('mb_level');
|
$myLevel = (int) session()->get('mb_level');
|
||||||
if ($myLevel === Roles::LEVEL_SUPER_ADMIN) {
|
if (Roles::isSuperAdminEquivalent($myLevel)) {
|
||||||
return $levelNames;
|
return $levelNames;
|
||||||
}
|
}
|
||||||
unset($levelNames[Roles::LEVEL_SUPER_ADMIN]);
|
unset($levelNames[Roles::LEVEL_SUPER_ADMIN], $levelNames[Roles::LEVEL_HEADQUARTERS_ADMIN]);
|
||||||
|
|
||||||
return $levelNames;
|
return $levelNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
41
app/Controllers/Admin/WorkMovedToBag.php
Normal file
41
app/Controllers/Admin/WorkMovedToBag.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Libraries\TotpService;
|
||||||
use App\Models\LocalGovernmentModel;
|
use App\Models\LocalGovernmentModel;
|
||||||
use App\Models\MemberApprovalRequestModel;
|
use App\Models\MemberApprovalRequestModel;
|
||||||
use App\Models\MemberLogModel;
|
use App\Models\MemberLogModel;
|
||||||
use App\Models\MemberModel;
|
use App\Models\MemberModel;
|
||||||
|
use CodeIgniter\HTTP\RedirectResponse;
|
||||||
|
|
||||||
class Auth extends BaseController
|
class Auth extends BaseController
|
||||||
{
|
{
|
||||||
@@ -77,11 +79,33 @@ class Auth extends BaseController
|
|||||||
->with('error', '정지된 회원입니다.');
|
->with('error', '정지된 회원입니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! password_verify($password, $member->mb_passwd)) {
|
// P2-21: 로그인 잠금 체크 (5회 실패 시 30분 lock)
|
||||||
$this->insertMemberLog($logData, false, '비밀번호 불일치');
|
if (!empty($member->mb_locked_until) && strtotime($member->mb_locked_until) > time()) {
|
||||||
|
$remaining = ceil((strtotime($member->mb_locked_until) - time()) / 60);
|
||||||
|
$this->insertMemberLog($logData, false, '계정 잠금 상태');
|
||||||
return redirect()->back()
|
return redirect()->back()
|
||||||
->withInput()
|
->withInput()
|
||||||
->with('error', '아이디 또는 비밀번호가 올바르지 않습니다.');
|
->with('error', '로그인 시도 횟수 초과로 계정이 잠겼습니다. 약 ' . $remaining . '분 후 다시 시도해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! password_verify($password, $member->mb_passwd)) {
|
||||||
|
// 실패 횟수 증가
|
||||||
|
$failCount = ((int) ($member->mb_login_fail_count ?? 0)) + 1;
|
||||||
|
$updateData = ['mb_login_fail_count' => $failCount];
|
||||||
|
if ($failCount >= 5) {
|
||||||
|
$updateData['mb_locked_until'] = date('Y-m-d H:i:s', strtotime('+30 minutes'));
|
||||||
|
}
|
||||||
|
$memberModel->update($member->mb_idx, $updateData);
|
||||||
|
|
||||||
|
$this->insertMemberLog($logData, false, '비밀번호 불일치 (' . $failCount . '회)');
|
||||||
|
|
||||||
|
$msg = '아이디 또는 비밀번호가 올바르지 않습니다.';
|
||||||
|
if ($failCount >= 5) {
|
||||||
|
$msg .= ' 5회 연속 실패로 계정이 30분간 잠깁니다.';
|
||||||
|
} elseif ($failCount >= 3) {
|
||||||
|
$msg .= ' (실패 ' . $failCount . '/5회)';
|
||||||
|
}
|
||||||
|
return redirect()->back()->withInput()->with('error', $msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 승인 요청 상태 확인(공개 회원가입 사용자)
|
// 승인 요청 상태 확인(공개 회원가입 사용자)
|
||||||
@@ -101,33 +125,177 @@ class Auth extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 로그인 성공
|
if ($this->needsTotpStep($member)) {
|
||||||
$sessionData = [
|
$this->beginPending2faSession((int) $member->mb_idx);
|
||||||
'mb_idx' => $member->mb_idx,
|
$enabled = (int) ($member->mb_totp_enabled ?? 0) === 1;
|
||||||
'mb_id' => $member->mb_id,
|
if ($enabled) {
|
||||||
'mb_name' => $member->mb_name,
|
return redirect()->to(site_url('login/two-factor'));
|
||||||
'mb_level' => $member->mb_level,
|
}
|
||||||
'mb_lg_idx' => $member->mb_lg_idx ?? null,
|
session()->set('pending_totp_setup', true);
|
||||||
'logged_in' => true,
|
|
||||||
];
|
|
||||||
session()->set($sessionData);
|
|
||||||
|
|
||||||
$memberModel->update($member->mb_idx, [
|
return redirect()->to(site_url('login/totp-setup'));
|
||||||
'mb_latestdate' => date('Y-m-d H:i:s'),
|
}
|
||||||
|
|
||||||
|
return $this->completeLogin($member, $logData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function showTwoFactor()
|
||||||
|
{
|
||||||
|
if (session()->get('logged_in')) {
|
||||||
|
return redirect()->to('/');
|
||||||
|
}
|
||||||
|
$member = $this->ensurePending2faContext();
|
||||||
|
if ($member === null) {
|
||||||
|
return redirect()->to(site_url('login'))->with('error', '로그인 세션이 만료되었습니다. 다시 로그인해 주세요.');
|
||||||
|
}
|
||||||
|
if (session()->get('pending_totp_setup')) {
|
||||||
|
return redirect()->to(site_url('login/totp-setup'));
|
||||||
|
}
|
||||||
|
if ((int) ($member->mb_totp_enabled ?? 0) !== 1) {
|
||||||
|
return redirect()->to(site_url('login/totp-setup'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('auth/login_two_factor', [
|
||||||
|
'memberId' => $member->mb_id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->insertMemberLog($logData, true, '로그인 성공', $member->mb_idx);
|
|
||||||
|
|
||||||
// 지자체 관리자 → 관리자 대시보드로 이동
|
|
||||||
if ((int) $member->mb_level === \Config\Roles::LEVEL_LOCAL_ADMIN) {
|
|
||||||
return redirect()->to(site_url('admin'))->with('success', '로그인되었습니다.');
|
|
||||||
}
|
|
||||||
// super admin → 지자체 선택 페이지로 이동 (선택 후 관리자 페이지 사용)
|
|
||||||
if ((int) $member->mb_level === \Config\Roles::LEVEL_SUPER_ADMIN) {
|
|
||||||
return redirect()->to(site_url('admin/select-local-government'))->with('success', '로그인되었습니다.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->to(site_url('/'))->with('success', '로그인되었습니다.');
|
public function verifyTwoFactor()
|
||||||
|
{
|
||||||
|
if (session()->get('logged_in')) {
|
||||||
|
return redirect()->to('/');
|
||||||
|
}
|
||||||
|
$member = $this->ensurePending2faContext();
|
||||||
|
if ($member === null) {
|
||||||
|
return redirect()->to(site_url('login'))->with('error', '로그인 세션이 만료되었습니다. 다시 로그인해 주세요.');
|
||||||
|
}
|
||||||
|
if (session()->get('pending_totp_setup') || (int) ($member->mb_totp_enabled ?? 0) !== 1) {
|
||||||
|
return redirect()->to(site_url('login/totp-setup'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'totp_code' => 'required|exact_length[6]|numeric',
|
||||||
|
];
|
||||||
|
$messages = [
|
||||||
|
'totp_code' => [
|
||||||
|
'required' => '인증 코드 6자리를 입력해 주세요.',
|
||||||
|
'exact_length' => '인증 코드는 6자리 숫자입니다.',
|
||||||
|
'numeric' => '인증 코드는 숫자만 입력해 주세요.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
if (! $this->validate($rules, $messages)) {
|
||||||
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = (string) $this->request->getPost('totp_code');
|
||||||
|
helper('pii_encryption');
|
||||||
|
$secret = pii_decrypt((string) ($member->mb_totp_secret ?? ''));
|
||||||
|
if ($secret === '') {
|
||||||
|
$this->clearPending2faSession();
|
||||||
|
|
||||||
|
return redirect()->to(site_url('login'))->with('error', '2차 인증 설정이 올바르지 않습니다. 관리자에게 문의해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$totp = new TotpService();
|
||||||
|
if (! $totp->verify($secret, $code)) {
|
||||||
|
return $this->handleTotpFailure($member, $this->buildLogData($member->mb_id, (int) $member->mb_idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->completeLogin($member, $this->buildLogData($member->mb_id, (int) $member->mb_idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function showTotpSetup()
|
||||||
|
{
|
||||||
|
if (session()->get('logged_in')) {
|
||||||
|
return redirect()->to('/');
|
||||||
|
}
|
||||||
|
$member = $this->ensurePending2faContext();
|
||||||
|
if ($member === null) {
|
||||||
|
return redirect()->to(site_url('login'))->with('error', '로그인 세션이 만료되었습니다. 다시 로그인해 주세요.');
|
||||||
|
}
|
||||||
|
if (! session()->get('pending_totp_setup')) {
|
||||||
|
if ((int) ($member->mb_totp_enabled ?? 0) === 1) {
|
||||||
|
return redirect()->to(site_url('login/two-factor'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->to(site_url('login'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$totp = new TotpService();
|
||||||
|
$secret = session()->get('pending_totp_secret');
|
||||||
|
if (! is_string($secret) || $secret === '') {
|
||||||
|
$secret = $totp->createSecret();
|
||||||
|
session()->set('pending_totp_secret', $secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
$qrDataUri = null;
|
||||||
|
try {
|
||||||
|
$qrDataUri = $totp->getQrDataUri((string) $member->mb_id, $secret);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
$qrDataUri = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('auth/totp_setup', [
|
||||||
|
'memberId' => $member->mb_id,
|
||||||
|
'qrDataUri' => $qrDataUri,
|
||||||
|
'secret' => $secret,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function completeTotpSetup()
|
||||||
|
{
|
||||||
|
if (session()->get('logged_in')) {
|
||||||
|
return redirect()->to('/');
|
||||||
|
}
|
||||||
|
$member = $this->ensurePending2faContext();
|
||||||
|
if ($member === null) {
|
||||||
|
return redirect()->to(site_url('login'))->with('error', '로그인 세션이 만료되었습니다. 다시 로그인해 주세요.');
|
||||||
|
}
|
||||||
|
if (! session()->get('pending_totp_setup')) {
|
||||||
|
return redirect()->to(site_url('login/two-factor'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'totp_code' => 'required|exact_length[6]|numeric',
|
||||||
|
];
|
||||||
|
$messages = [
|
||||||
|
'totp_code' => [
|
||||||
|
'required' => '인증 코드 6자리를 입력해 주세요.',
|
||||||
|
'exact_length' => '인증 코드는 6자리 숫자입니다.',
|
||||||
|
'numeric' => '인증 코드는 숫자만 입력해 주세요.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
if (! $this->validate($rules, $messages)) {
|
||||||
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
$secret = session()->get('pending_totp_secret');
|
||||||
|
if (! is_string($secret) || $secret === '') {
|
||||||
|
return redirect()->to(site_url('login/totp-setup'))->with('error', '설정 정보가 없습니다. 페이지를 새로고침해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = (string) $this->request->getPost('totp_code');
|
||||||
|
$totp = new TotpService();
|
||||||
|
if (! $totp->verify($secret, $code)) {
|
||||||
|
return $this->handleTotpFailure($member, $this->buildLogData($member->mb_id, (int) $member->mb_idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
helper('pii_encryption');
|
||||||
|
model(MemberModel::class)->update((int) $member->mb_idx, [
|
||||||
|
'mb_totp_secret' => pii_encrypt($secret),
|
||||||
|
'mb_totp_enabled' => 1,
|
||||||
|
]);
|
||||||
|
session()->remove('pending_totp_setup');
|
||||||
|
session()->remove('pending_totp_secret');
|
||||||
|
|
||||||
|
$fresh = model(MemberModel::class)->find((int) $member->mb_idx);
|
||||||
|
if ($fresh === null) {
|
||||||
|
$this->clearPending2faSession();
|
||||||
|
|
||||||
|
return redirect()->to(site_url('login'))->with('error', '회원 정보를 다시 확인할 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->completeLogin($fresh, $this->buildLogData($fresh->mb_id, (int) $fresh->mb_idx));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function logout()
|
public function logout()
|
||||||
@@ -158,6 +326,7 @@ class Auth extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->clearPending2faSession();
|
||||||
session()->destroy();
|
session()->destroy();
|
||||||
|
|
||||||
return redirect()->to('login')->with('success', '로그아웃되었습니다.');
|
return redirect()->to('login')->with('success', '로그아웃되었습니다.');
|
||||||
@@ -274,6 +443,130 @@ class Auth extends BaseController
|
|||||||
return redirect()->to('login')->with('success', '회원가입이 완료되었습니다. 관리자 승인 후 로그인 가능합니다.');
|
return redirect()->to('login')->with('success', '회원가입이 완료되었습니다. 관리자 승인 후 로그인 가능합니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function needsTotpStep(object $member): bool
|
||||||
|
{
|
||||||
|
if (! config('Auth')->requireTotp) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return \Config\Roles::requiresTotp((int) $member->mb_level);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function beginPending2faSession(int $mbIdx): void
|
||||||
|
{
|
||||||
|
session()->set([
|
||||||
|
'pending_2fa' => true,
|
||||||
|
'pending_mb_idx' => $mbIdx,
|
||||||
|
'pending_2fa_started' => time(),
|
||||||
|
'totp_attempts' => 0,
|
||||||
|
]);
|
||||||
|
session()->remove('pending_totp_setup');
|
||||||
|
session()->remove('pending_totp_secret');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function clearPending2faSession(): void
|
||||||
|
{
|
||||||
|
session()->remove([
|
||||||
|
'pending_2fa',
|
||||||
|
'pending_mb_idx',
|
||||||
|
'pending_2fa_started',
|
||||||
|
'pending_totp_setup',
|
||||||
|
'pending_totp_secret',
|
||||||
|
'totp_attempts',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pending2faExpired(): bool
|
||||||
|
{
|
||||||
|
$started = (int) session()->get('pending_2fa_started');
|
||||||
|
if ($started <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$ttl = config('Auth')->pending2faTtlSeconds;
|
||||||
|
|
||||||
|
return (time() - $started) > $ttl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensurePending2faContext(): ?object
|
||||||
|
{
|
||||||
|
if (! session()->get('pending_2fa')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ($this->pending2faExpired()) {
|
||||||
|
$this->clearPending2faSession();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$mbIdx = (int) session()->get('pending_mb_idx');
|
||||||
|
if ($mbIdx <= 0) {
|
||||||
|
$this->clearPending2faSession();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$member = model(MemberModel::class)->find($mbIdx);
|
||||||
|
if ($member === null) {
|
||||||
|
$this->clearPending2faSession();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $member;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $logData
|
||||||
|
*/
|
||||||
|
private function handleTotpFailure(object $member, array $logData): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->insertMemberLog($logData, false, '2차 인증 실패', (int) $member->mb_idx);
|
||||||
|
$attempts = (int) session()->get('totp_attempts') + 1;
|
||||||
|
session()->set('totp_attempts', $attempts);
|
||||||
|
$max = config('Auth')->totpMaxAttempts;
|
||||||
|
if ($attempts >= $max) {
|
||||||
|
$this->clearPending2faSession();
|
||||||
|
|
||||||
|
return redirect()->to(site_url('login'))->with('error', "인증 코드가 {$max}회 틀려 세션이 종료되었습니다. 처음부터 로그인해 주세요.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()
|
||||||
|
->withInput()
|
||||||
|
->with('error', '인증 코드가 올바르지 않습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $logData
|
||||||
|
*/
|
||||||
|
private function completeLogin(object $member, array $logData): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->clearPending2faSession();
|
||||||
|
$sessionData = [
|
||||||
|
'mb_idx' => $member->mb_idx,
|
||||||
|
'mb_id' => $member->mb_id,
|
||||||
|
'mb_name' => $member->mb_name,
|
||||||
|
'mb_level' => $member->mb_level,
|
||||||
|
'mb_lg_idx' => $member->mb_lg_idx ?? null,
|
||||||
|
'logged_in' => true,
|
||||||
|
];
|
||||||
|
session()->set($sessionData);
|
||||||
|
|
||||||
|
model(MemberModel::class)->update($member->mb_idx, [
|
||||||
|
'mb_latestdate' => date('Y-m-d H:i:s'),
|
||||||
|
'mb_login_fail_count' => 0,
|
||||||
|
'mb_locked_until' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->insertMemberLog($logData, true, '로그인 성공', (int) $member->mb_idx);
|
||||||
|
|
||||||
|
if ((int) $member->mb_level === \Config\Roles::LEVEL_LOCAL_ADMIN) {
|
||||||
|
return redirect()->to(site_url('admin'))->with('success', '로그인되었습니다.');
|
||||||
|
}
|
||||||
|
if (\Config\Roles::isSuperAdminEquivalent((int) $member->mb_level)) {
|
||||||
|
return redirect()->to(site_url('admin/select-local-government'))->with('success', '로그인되었습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->to(site_url('/'))->with('success', '로그인되었습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
private function buildLogData(string $mbId, ?int $mbIdx): array
|
private function buildLogData(string $mbId, ?int $mbIdx): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
872
app/Controllers/Bag.php
Normal file
872
app/Controllers/Bag.php
Normal file
@@ -0,0 +1,872 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||||
|
use CodeIgniter\HTTP\RedirectResponse;
|
||||||
|
use App\Models\BagInventoryModel;
|
||||||
|
use App\Models\BagIssueModel;
|
||||||
|
use App\Models\BagOrderModel;
|
||||||
|
use App\Models\BagOrderItemModel;
|
||||||
|
use App\Models\BagPriceModel;
|
||||||
|
use App\Models\BagReceivingModel;
|
||||||
|
use App\Models\BagSaleModel;
|
||||||
|
use App\Models\CodeKindModel;
|
||||||
|
use App\Models\CodeDetailModel;
|
||||||
|
use App\Models\CompanyModel;
|
||||||
|
use App\Models\PackagingUnitModel;
|
||||||
|
use App\Models\SalesAgencyModel;
|
||||||
|
use App\Models\ShopOrderModel;
|
||||||
|
use App\Models\DesignatedShopModel;
|
||||||
|
use App\Models\LocalGovernmentModel;
|
||||||
|
use Config\Roles;
|
||||||
|
|
||||||
|
class Bag extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 로그인 사용자의 지자체 PK 반환 (미로그인/미지정 시 null)
|
||||||
|
*/
|
||||||
|
private function lgIdx(): ?int
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
return admin_effective_lg_idx();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function render(string $title, string $viewFile, array $data = []): string
|
||||||
|
{
|
||||||
|
return view('bag/layout/main', [
|
||||||
|
'title' => $title,
|
||||||
|
'content' => view($viewFile, $data),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// 기본정보관리 (단가·포장 단위 진입 허브)
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
public function basicInfo(): string
|
||||||
|
{
|
||||||
|
return $this->render('기본정보관리', 'bag/basic_info', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 봉투 단가 조회 (사이트) — 기간·봉투구분·봉투코드 필터, 적용기간 겹침, 페이징·인쇄 */
|
||||||
|
public function prices(): string|RedirectResponse
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
if ($this->request->is('post')) {
|
||||||
|
$post = $this->request->getPost();
|
||||||
|
$pick = 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;
|
||||||
|
};
|
||||||
|
session()->setFlashdata('bag_prices_filter', [
|
||||||
|
'start_y' => $pick($post, 'start_y'),
|
||||||
|
'start_m' => $pick($post, 'start_m'),
|
||||||
|
'start_d' => $pick($post, 'start_d'),
|
||||||
|
'end_y' => $pick($post, 'end_y'),
|
||||||
|
'end_m' => $pick($post, 'end_m'),
|
||||||
|
'end_d' => $pick($post, 'end_d'),
|
||||||
|
'bag_kind_e' => $pick($post, 'bag_kind_e'),
|
||||||
|
'bag_code' => $pick($post, 'bag_code'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->to(site_url('bag/prices'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
$bagPrices = [];
|
||||||
|
|
||||||
|
$get = $this->request->getGet();
|
||||||
|
$flash = session()->getFlashdata('bag_prices_filter');
|
||||||
|
|
||||||
|
$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;
|
||||||
|
};
|
||||||
|
|
||||||
|
$filterKeys = [
|
||||||
|
'start_y', 'start_m', 'start_d',
|
||||||
|
'end_y', 'end_m', 'end_d',
|
||||||
|
'bag_kind_e', 'bag_code',
|
||||||
|
'start_date', 'end_date',
|
||||||
|
];
|
||||||
|
$hasExplicitGetFilter = false;
|
||||||
|
foreach ($filterKeys as $fk) {
|
||||||
|
$v = $get[$fk] ?? null;
|
||||||
|
if ($v !== null && ! is_array($v) && trim((string) $v) !== '') {
|
||||||
|
$hasExplicitGetFilter = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$src = [];
|
||||||
|
if ($hasExplicitGetFilter) {
|
||||||
|
$src = $get;
|
||||||
|
} elseif (is_array($flash)) {
|
||||||
|
$src = $flash;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sy = $readSrc($src, 'start_y');
|
||||||
|
$sm = $readSrc($src, 'start_m');
|
||||||
|
$sd = $readSrc($src, 'start_d');
|
||||||
|
$ey = $readSrc($src, 'end_y');
|
||||||
|
$em = $readSrc($src, 'end_m');
|
||||||
|
$ed = $readSrc($src, '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) {
|
||||||
|
$g = $readSrc($src, 'start_date');
|
||||||
|
$startDate = ($g !== null && $g !== '') ? $g : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$endDate = null;
|
||||||
|
if ($ey !== null && $ey !== '' && $em !== null && $em !== '' && $ed !== null && $ed !== '') {
|
||||||
|
$endDate = parse_ymd_from_triple($ey, $em, $ed);
|
||||||
|
}
|
||||||
|
if ($endDate === null) {
|
||||||
|
$g = $readSrc($src, 'end_date');
|
||||||
|
$endDate = ($g !== null && $g !== '') ? $g : 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]];
|
||||||
|
} elseif ($sy !== null && $sm !== null && $sd !== null && $sy !== '' && $sm !== '' && $sd !== '') {
|
||||||
|
$iy = (int) $sy;
|
||||||
|
$im = (int) $sm;
|
||||||
|
$id = (int) $sd;
|
||||||
|
if ($iy >= 1000 && $iy <= 9999 && $im >= 1 && $im <= 12 && $id >= 1 && $id <= 31) {
|
||||||
|
$startParts = ['y' => (string) $iy, 'm' => $im, 'd' => $id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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]];
|
||||||
|
} elseif ($ey !== null && $em !== null && $ed !== null && $ey !== '' && $em !== '' && $ed !== '') {
|
||||||
|
$iy = (int) $ey;
|
||||||
|
$im = (int) $em;
|
||||||
|
$id = (int) $ed;
|
||||||
|
if ($iy >= 1000 && $iy <= 9999 && $im >= 1 && $im <= 12 && $id >= 1 && $id <= 31) {
|
||||||
|
$endParts = ['y' => (string) $iy, 'm' => $im, 'd' => $id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$dateYearMin = (int) date('Y') - 12;
|
||||||
|
$dateYearMax = (int) date('Y') + 2;
|
||||||
|
|
||||||
|
$bagKindE = $readSrc($src, 'bag_kind_e');
|
||||||
|
$bagCode = $readSrc($src, 'bag_code');
|
||||||
|
$pager = null;
|
||||||
|
$bagCodes = [];
|
||||||
|
$bagKindOpts = [];
|
||||||
|
$printLines = [];
|
||||||
|
$printLgName = '';
|
||||||
|
|
||||||
|
if ($lgIdx !== null) {
|
||||||
|
try {
|
||||||
|
$priceModel = model(BagPriceModel::class);
|
||||||
|
$builder = $priceModel->where('bp_lg_idx', $lgIdx);
|
||||||
|
|
||||||
|
if (($startDate !== null && $startDate !== '') || ($endDate !== null && $endDate !== '')) {
|
||||||
|
$qStart = ($startDate !== null && $startDate !== '') ? $startDate : $endDate;
|
||||||
|
$qEnd = ($endDate !== null && $endDate !== '') ? $endDate : $startDate;
|
||||||
|
if (strcmp((string) $qStart, (string) $qEnd) > 0) {
|
||||||
|
[$qStart, $qEnd] = [$qEnd, $qStart];
|
||||||
|
}
|
||||||
|
$builder->where('bp_start_date <=', $qEnd);
|
||||||
|
$builder->groupStart()
|
||||||
|
->where('bp_end_date IS NULL')
|
||||||
|
->orWhere('bp_end_date >=', $qStart)
|
||||||
|
->groupEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bagKindE !== null && $bagKindE !== '') {
|
||||||
|
$ek = model(CodeKindModel::class)->where('ck_code', 'E')->first();
|
||||||
|
if ($ek) {
|
||||||
|
$eDetail = model(CodeDetailModel::class)
|
||||||
|
->where('cd_ck_idx', (int) $ek->ck_idx)
|
||||||
|
->where('cd_code', $bagKindE)
|
||||||
|
->where('cd_state', 1)
|
||||||
|
->first();
|
||||||
|
if ($eDetail !== null) {
|
||||||
|
$builder->like('bp_bag_code', (string) $bagKindE, 'after');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bagCode !== null && $bagCode !== '') {
|
||||||
|
$ok = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
if ($ok) {
|
||||||
|
$oDetail = model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $ok->ck_idx, (string) $bagCode, $lgIdx);
|
||||||
|
if ($oDetail !== null) {
|
||||||
|
$builder->where('bp_bag_code', $bagCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$bagPrices = $builder->orderBy('bp_bag_code', 'ASC')->orderBy('bp_start_date', 'DESC')->paginate(20);
|
||||||
|
|
||||||
|
$queryForPager = [];
|
||||||
|
$tripleS = $sy !== null && $sy !== '' && $sm !== null && $sm !== '' && $sd !== null && $sd !== '';
|
||||||
|
$tripleE = $ey !== null && $ey !== '' && $em !== null && $em !== '' && $ed !== null && $ed !== '';
|
||||||
|
if ($tripleS) {
|
||||||
|
$queryForPager['start_y'] = $sy;
|
||||||
|
$queryForPager['start_m'] = $sm;
|
||||||
|
$queryForPager['start_d'] = $sd;
|
||||||
|
} else {
|
||||||
|
$legacyS = $readSrc($src, 'start_date');
|
||||||
|
if ($legacyS !== null) {
|
||||||
|
$queryForPager['start_date'] = $legacyS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($tripleE) {
|
||||||
|
$queryForPager['end_y'] = $ey;
|
||||||
|
$queryForPager['end_m'] = $em;
|
||||||
|
$queryForPager['end_d'] = $ed;
|
||||||
|
} else {
|
||||||
|
$legacyE = $readSrc($src, 'end_date');
|
||||||
|
if ($legacyE !== null) {
|
||||||
|
$queryForPager['end_date'] = $legacyE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($bagKindE !== null && $bagKindE !== '') {
|
||||||
|
$queryForPager['bag_kind_e'] = $bagKindE;
|
||||||
|
}
|
||||||
|
if ($bagCode !== null && $bagCode !== '') {
|
||||||
|
$queryForPager['bag_code'] = $bagCode;
|
||||||
|
}
|
||||||
|
$queryForPager = array_filter(
|
||||||
|
$queryForPager,
|
||||||
|
static fn ($v) => $v !== null && $v !== ''
|
||||||
|
);
|
||||||
|
$pagerPath = site_url('bag/prices');
|
||||||
|
if ($queryForPager !== []) {
|
||||||
|
$pagerPath .= '?' . http_build_query($queryForPager);
|
||||||
|
}
|
||||||
|
$priceModel->pager->setPath($pagerPath);
|
||||||
|
$pager = $priceModel->pager;
|
||||||
|
|
||||||
|
$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();
|
||||||
|
$bagKindOpts = $kindE
|
||||||
|
? model(CodeDetailModel::class)->getByKind((int) $kindE->ck_idx, true, null)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
$lgRow = model(LocalGovernmentModel::class)->find($lgIdx);
|
||||||
|
$printLgName = $lgRow !== null ? $lgRow->lg_name : '';
|
||||||
|
} catch (DatabaseException $e) {
|
||||||
|
log_message('error', '[prices] bag_price 조회 실패: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($startDate !== null && $startDate !== '') || ($endDate !== null && $endDate !== '')) {
|
||||||
|
$qs = ($startDate !== null && $startDate !== '') ? $startDate : $endDate;
|
||||||
|
$qe = ($endDate !== null && $endDate !== '') ? $endDate : $startDate;
|
||||||
|
if (strcmp((string) $qs, (string) $qe) > 0) {
|
||||||
|
[$qs, $qe] = [$qe, $qs];
|
||||||
|
}
|
||||||
|
$printLines[] = '조회기간(적용기간 겹침): ' . format_ymd_korean($qs) . ' ~ ' . format_ymd_korean($qe);
|
||||||
|
}
|
||||||
|
if ($bagKindE !== null && $bagKindE !== '') {
|
||||||
|
foreach ($bagKindOpts as $cd) {
|
||||||
|
if ((string) $cd->cd_code === (string) $bagKindE) {
|
||||||
|
$printLines[] = '봉투구분: ' . $cd->cd_name . ' (' . $bagKindE . ')';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($bagCode !== null && $bagCode !== '') {
|
||||||
|
$printLines[] = '봉투코드: ' . $bagCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
$viewData = [
|
||||||
|
'lgIdx' => $lgIdx,
|
||||||
|
'bagPrices' => $bagPrices,
|
||||||
|
'pager' => $pager,
|
||||||
|
'startDate' => $startDate,
|
||||||
|
'endDate' => $endDate,
|
||||||
|
'startParts' => $startParts,
|
||||||
|
'endParts' => $endParts,
|
||||||
|
'dateYearMin' => $dateYearMin,
|
||||||
|
'dateYearMax' => $dateYearMax,
|
||||||
|
'bag_kind_e' => $bagKindE,
|
||||||
|
'bag_code' => $bagCode,
|
||||||
|
'bag_codes' => $bagCodes,
|
||||||
|
'bag_kind_options' => $bagKindOpts,
|
||||||
|
'printExtraLines' => $printLines,
|
||||||
|
];
|
||||||
|
if ($printLgName !== '') {
|
||||||
|
$viewData['printLgName'] = $printLgName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('봉투 단가', 'bag/prices', $viewData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 포장 단위 조회 (사이트) */
|
||||||
|
public function packagingUnits(): string
|
||||||
|
{
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
$packagingUnits = [];
|
||||||
|
$dbDiag = null;
|
||||||
|
if ($lgIdx) {
|
||||||
|
try {
|
||||||
|
$packagingUnits = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->orderBy('pu_bag_code', 'ASC')->findAll();
|
||||||
|
} catch (DatabaseException $e) {
|
||||||
|
log_message('error', '[packagingUnits] packaging_unit 조회 실패: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->request->getGet('db_diag') === '1') {
|
||||||
|
$dbDiag = [
|
||||||
|
'lg_idx' => $lgIdx,
|
||||||
|
'db_name' => null,
|
||||||
|
'packaging_unit' => null,
|
||||||
|
'code_kind' => null,
|
||||||
|
'code_detail' => null,
|
||||||
|
'error' => null,
|
||||||
|
];
|
||||||
|
try {
|
||||||
|
$db = db_connect();
|
||||||
|
$dbDiag['db_name'] = $db->database;
|
||||||
|
$dbDiag['packaging_unit'] = (int) $db->table('packaging_unit')->where('pu_lg_idx', (int) $lgIdx)->countAllResults();
|
||||||
|
$dbDiag['code_kind'] = (int) $db->table('code_kind')->countAllResults();
|
||||||
|
$dbDiag['code_detail'] = (int) $db->table('code_detail')->countAllResults();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$dbDiag['error'] = $e->getMessage();
|
||||||
|
log_message('error', '[packagingUnits][db_diag] {type}: {message}', [
|
||||||
|
'type' => $e::class,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('포장 단위', 'bag/packaging_units', [
|
||||||
|
'packagingUnits' => $packagingUnits,
|
||||||
|
'dbDiag' => $dbDiag,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기본코드 종류·세부코드 조회 전용 (사이트 메뉴 기본코드관리)
|
||||||
|
*/
|
||||||
|
public function codeKinds(): string
|
||||||
|
{
|
||||||
|
$kindModel = model(CodeKindModel::class);
|
||||||
|
$detailModel = model(CodeDetailModel::class);
|
||||||
|
$kinds = [];
|
||||||
|
$countMap = [];
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
try {
|
||||||
|
$kinds = $kindModel->orderBy('ck_code', 'ASC')->findAll();
|
||||||
|
foreach ($kinds as $row) {
|
||||||
|
$countMap[$row->ck_idx] = (int) $detailModel->where('cd_ck_idx', $row->ck_idx)
|
||||||
|
->filterByTenantScope($lgIdx)
|
||||||
|
->countAllResults();
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
log_message('error', '[codeKinds] 실패: {type} {message} @ {file}:{line} / lg={lg}, user={user}, level={level}', [
|
||||||
|
'type' => $e::class,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'lg' => $lgIdx !== null ? (string) $lgIdx : 'null',
|
||||||
|
'user' => (string) (session()->get('mb_id') ?? ''),
|
||||||
|
'level' => (string) (session()->get('mb_level') ?? ''),
|
||||||
|
]);
|
||||||
|
session()->setFlashdata('error', '기본코드 조회 중 오류가 발생했습니다. 관리자에게 로그 확인을 요청해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$level = (int) session()->get('mb_level');
|
||||||
|
|
||||||
|
return $this->render('기본코드관리', 'bag/code_kinds', [
|
||||||
|
'codeKinds' => $kinds,
|
||||||
|
'countMap' => $countMap,
|
||||||
|
'canManageKinds' => Roles::canManageCodeKindMaster($level),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기본코드 세부 목록 (사이트 레이아웃). 등록·수정·삭제 폼은 /admin/code-details/* 유지.
|
||||||
|
*/
|
||||||
|
public function codeDetails(int $ckIdx)
|
||||||
|
{
|
||||||
|
$kindModel = model(CodeKindModel::class);
|
||||||
|
$detailModel = model(CodeDetailModel::class);
|
||||||
|
$kind = null;
|
||||||
|
try {
|
||||||
|
$kind = $kindModel->find($ckIdx);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
log_message('error', '[codeDetails] kind 조회 실패: {type} {message} @ {file}:{line} / ck={ck}, user={user}, level={level}', [
|
||||||
|
'type' => $e::class,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'ck' => (string) $ckIdx,
|
||||||
|
'user' => (string) (session()->get('mb_id') ?? ''),
|
||||||
|
'level' => (string) (session()->get('mb_level') ?? ''),
|
||||||
|
]);
|
||||||
|
return redirect()->to(site_url('bag/code-kinds'))->with('error', '세부코드 조회 중 오류가 발생했습니다. 관리자에게 로그 확인을 요청해 주세요.');
|
||||||
|
}
|
||||||
|
if ($kind === null) {
|
||||||
|
return redirect()->to(site_url('bag/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
try {
|
||||||
|
$list = $detailModel->where('cd_ck_idx', $ckIdx)
|
||||||
|
->filterByTenantScope($lgIdx)
|
||||||
|
->orderBy('cd_sort', 'ASC')
|
||||||
|
->orderBy('cd_idx', 'ASC')
|
||||||
|
->paginate(20);
|
||||||
|
$pager = $detailModel->pager;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
log_message('error', '[codeDetails] list 조회 실패: {type} {message} @ {file}:{line} / ck={ck}, lg={lg}, user={user}, level={level}', [
|
||||||
|
'type' => $e::class,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'ck' => (string) $ckIdx,
|
||||||
|
'lg' => $lgIdx !== null ? (string) $lgIdx : 'null',
|
||||||
|
'user' => (string) (session()->get('mb_id') ?? ''),
|
||||||
|
'level' => (string) (session()->get('mb_level') ?? ''),
|
||||||
|
]);
|
||||||
|
return redirect()->to(site_url('bag/code-kinds'))->with('error', '세부코드 조회 중 오류가 발생했습니다. 관리자에게 로그 확인을 요청해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
helper('admin');
|
||||||
|
$level = (int) session()->get('mb_level');
|
||||||
|
$adminLg = admin_effective_lg_idx();
|
||||||
|
$canManage = Roles::canManageCodeMaster($level);
|
||||||
|
$rowCanEdit = [];
|
||||||
|
foreach ($list as $row) {
|
||||||
|
$rowCanEdit[$row->cd_idx] = Roles::canEditCodeDetailRow($level, $row, $adminLg);
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = ($canManage ? '세부코드 관리' : '세부코드 조회') . ' — ' . $kind->ck_name . ' (' . $kind->ck_code . ')';
|
||||||
|
|
||||||
|
return $this->render($title, 'bag/code_details', [
|
||||||
|
'kind' => $kind,
|
||||||
|
'list' => $list,
|
||||||
|
'pager' => $pager,
|
||||||
|
'canManage' => $canManage,
|
||||||
|
'rowCanEdit' => $rowCanEdit,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// 발주 입고 관리
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
public function purchaseInbound(): string
|
||||||
|
{
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
$data = ['orders' => [], 'receivings' => [], 'startDate' => null, 'endDate' => null];
|
||||||
|
|
||||||
|
if ($lgIdx) {
|
||||||
|
$startDate = $this->request->getGet('start_date');
|
||||||
|
$endDate = $this->request->getGet('end_date');
|
||||||
|
$data['startDate'] = $startDate;
|
||||||
|
$data['endDate'] = $endDate;
|
||||||
|
|
||||||
|
// 발주 목록
|
||||||
|
$orderBuilder = model(BagOrderModel::class)->where('bo_lg_idx', $lgIdx);
|
||||||
|
if ($startDate) $orderBuilder->where('bo_order_date >=', $startDate);
|
||||||
|
if ($endDate) $orderBuilder->where('bo_order_date <=', $endDate);
|
||||||
|
$data['orders'] = $orderBuilder->orderBy('bo_order_date', 'DESC')->paginate(20, 'orders');
|
||||||
|
$data['orderPager'] = model(BagOrderModel::class)->pager;
|
||||||
|
|
||||||
|
// 발주별 품목 합계
|
||||||
|
$itemSummary = [];
|
||||||
|
foreach ($data['orders'] as $order) {
|
||||||
|
$items = model(BagOrderItemModel::class)->where('boi_bo_idx', $order->bo_idx)->findAll();
|
||||||
|
$totalQty = 0;
|
||||||
|
$totalAmt = 0;
|
||||||
|
foreach ($items as $it) {
|
||||||
|
$totalQty += (int) $it->boi_qty_sheet;
|
||||||
|
$totalAmt += (float) $it->boi_amount;
|
||||||
|
}
|
||||||
|
$itemSummary[$order->bo_idx] = ['qty' => $totalQty, 'amount' => $totalAmt, 'count' => count($items)];
|
||||||
|
}
|
||||||
|
$data['itemSummary'] = $itemSummary;
|
||||||
|
|
||||||
|
// 입고 목록
|
||||||
|
$recvBuilder = model(BagReceivingModel::class)->where('br_lg_idx', $lgIdx);
|
||||||
|
if ($startDate) $recvBuilder->where('br_receive_date >=', $startDate);
|
||||||
|
if ($endDate) $recvBuilder->where('br_receive_date <=', $endDate);
|
||||||
|
$data['receivings'] = $recvBuilder->orderBy('br_receive_date', 'DESC')->paginate(20, 'receivings');
|
||||||
|
$data['recvPager'] = model(BagReceivingModel::class)->pager;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('발주 입고 관리', 'bag/purchase_inbound', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// 불출 관리
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
public function issue(): string
|
||||||
|
{
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
$data = ['list' => [], 'startDate' => null, 'endDate' => null];
|
||||||
|
|
||||||
|
if ($lgIdx) {
|
||||||
|
$startDate = $this->request->getGet('start_date');
|
||||||
|
$endDate = $this->request->getGet('end_date');
|
||||||
|
$data['startDate'] = $startDate;
|
||||||
|
$data['endDate'] = $endDate;
|
||||||
|
|
||||||
|
$builder = model(BagIssueModel::class)->where('bi2_lg_idx', $lgIdx);
|
||||||
|
if ($startDate) $builder->where('bi2_issue_date >=', $startDate);
|
||||||
|
if ($endDate) $builder->where('bi2_issue_date <=', $endDate);
|
||||||
|
$data['list'] = $builder->orderBy('bi2_issue_date', 'DESC')->paginate(20);
|
||||||
|
$data['pager'] = model(BagIssueModel::class)->pager;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('불출 관리', 'bag/issue', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// 재고 관리
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
public function inventory(): string
|
||||||
|
{
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
$data = ['list' => []];
|
||||||
|
|
||||||
|
if ($lgIdx) {
|
||||||
|
$invModel = model(BagInventoryModel::class);
|
||||||
|
$data['list'] = $invModel->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->paginate(20);
|
||||||
|
$data['pager'] = $invModel->pager;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('재고 관리', 'bag/inventory', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// 판매 관리
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
public function sales(): string
|
||||||
|
{
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
$data = ['salesList' => [], 'orderList' => [], 'startDate' => null, 'endDate' => null];
|
||||||
|
|
||||||
|
if ($lgIdx) {
|
||||||
|
$startDate = $this->request->getGet('start_date');
|
||||||
|
$endDate = $this->request->getGet('end_date');
|
||||||
|
$data['startDate'] = $startDate;
|
||||||
|
$data['endDate'] = $endDate;
|
||||||
|
|
||||||
|
// 판매/반품
|
||||||
|
$saleBuilder = model(BagSaleModel::class)->where('bs_lg_idx', $lgIdx);
|
||||||
|
if ($startDate) $saleBuilder->where('bs_sale_date >=', $startDate);
|
||||||
|
if ($endDate) $saleBuilder->where('bs_sale_date <=', $endDate);
|
||||||
|
$data['salesList'] = $saleBuilder->orderBy('bs_sale_date', 'DESC')->paginate(20, 'sales');
|
||||||
|
$data['salesPager'] = model(BagSaleModel::class)->pager;
|
||||||
|
|
||||||
|
// 주문 접수
|
||||||
|
$orderBuilder = model(ShopOrderModel::class)->where('so_lg_idx', $lgIdx);
|
||||||
|
if ($startDate) $orderBuilder->where('so_delivery_date >=', $startDate);
|
||||||
|
if ($endDate) $orderBuilder->where('so_delivery_date <=', $endDate);
|
||||||
|
$data['orderList'] = $orderBuilder->orderBy('so_idx', 'DESC')->paginate(20, 'shoporders');
|
||||||
|
$data['orderPager'] = model(ShopOrderModel::class)->pager;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('판매 관리', 'bag/sales', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// 판매 현황
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
public function salesStats(): string
|
||||||
|
{
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
$data = ['result' => [], 'startDate' => null, 'endDate' => null];
|
||||||
|
|
||||||
|
if ($lgIdx) {
|
||||||
|
$startDate = $this->request->getGet('start_date');
|
||||||
|
$endDate = $this->request->getGet('end_date');
|
||||||
|
$data['startDate'] = $startDate;
|
||||||
|
$data['endDate'] = $endDate;
|
||||||
|
|
||||||
|
$builder = model(BagSaleModel::class)->where('bs_lg_idx', $lgIdx)->where('bs_type', 'sale');
|
||||||
|
if ($startDate) $builder->where('bs_sale_date >=', $startDate);
|
||||||
|
if ($endDate) $builder->where('bs_sale_date <=', $endDate);
|
||||||
|
$data['result'] = $builder->orderBy('bs_sale_date', 'DESC')->paginate(20);
|
||||||
|
$data['pager'] = model(BagSaleModel::class)->pager;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('판매 현황', 'bag/sales_stats', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// 봉투 수불 관리
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
public function flow(): string
|
||||||
|
{
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
$data = ['receiving' => [], 'sales' => [], 'issues' => [], 'inventory' => [], 'startDate' => null, 'endDate' => null];
|
||||||
|
|
||||||
|
if ($lgIdx) {
|
||||||
|
$startDate = $this->request->getGet('start_date');
|
||||||
|
$endDate = $this->request->getGet('end_date');
|
||||||
|
$data['startDate'] = $startDate;
|
||||||
|
$data['endDate'] = $endDate;
|
||||||
|
|
||||||
|
$data['inventory'] = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->findAll();
|
||||||
|
|
||||||
|
$recvBuilder = model(BagReceivingModel::class)->where('br_lg_idx', $lgIdx);
|
||||||
|
if ($startDate) $recvBuilder->where('br_receive_date >=', $startDate);
|
||||||
|
if ($endDate) $recvBuilder->where('br_receive_date <=', $endDate);
|
||||||
|
$data['receiving'] = $recvBuilder->findAll();
|
||||||
|
|
||||||
|
$saleBuilder = model(BagSaleModel::class)->where('bs_lg_idx', $lgIdx);
|
||||||
|
if ($startDate) $saleBuilder->where('bs_sale_date >=', $startDate);
|
||||||
|
if ($endDate) $saleBuilder->where('bs_sale_date <=', $endDate);
|
||||||
|
$data['sales'] = $saleBuilder->findAll();
|
||||||
|
|
||||||
|
$issueBuilder = model(BagIssueModel::class)->where('bi2_lg_idx', $lgIdx);
|
||||||
|
if ($startDate) $issueBuilder->where('bi2_issue_date >=', $startDate);
|
||||||
|
if ($endDate) $issueBuilder->where('bi2_issue_date <=', $endDate);
|
||||||
|
$data['issues'] = $issueBuilder->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('봉투 수불 관리', 'bag/flow', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// 통계 분석 관리
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
public function analytics(): string
|
||||||
|
{
|
||||||
|
return $this->render('통계 분석 관리', 'bag/analytics', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// 창 (프로그램 창 관리 - 추후)
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
public function window(): string
|
||||||
|
{
|
||||||
|
return $this->render('창', 'bag/window', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// 도움말
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
public function help(): string
|
||||||
|
{
|
||||||
|
return $this->render('도움말', 'bag/help', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// 재고 조정 (실사)
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
public function inventoryAdjust(): string
|
||||||
|
{
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
$inventory = $lgIdx ? model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code')->findAll() : [];
|
||||||
|
return $this->render('재고 조정', 'bag/inventory_adjust', compact('inventory'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inventoryAdjustStore()
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(site_url('bag/inventory'))->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'bag_code' => 'required|max_length[50]',
|
||||||
|
'adjust_type' => 'required|in_list[set,add,sub]',
|
||||||
|
'qty' => 'required|is_natural',
|
||||||
|
];
|
||||||
|
if (! $this->validate($rules)) {
|
||||||
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
$bagCode = $this->request->getPost('bag_code');
|
||||||
|
$type = $this->request->getPost('adjust_type');
|
||||||
|
$qty = (int) $this->request->getPost('qty');
|
||||||
|
|
||||||
|
$invModel = model(BagInventoryModel::class);
|
||||||
|
$existing = $invModel->where('bi_lg_idx', $lgIdx)->where('bi_bag_code', $bagCode)->first();
|
||||||
|
|
||||||
|
if ($type === 'set') {
|
||||||
|
if ($existing) {
|
||||||
|
$invModel->update($existing->bi_idx, ['bi_qty' => $qty, 'bi_updated_at' => date('Y-m-d H:i:s')]);
|
||||||
|
}
|
||||||
|
} elseif ($type === 'add') {
|
||||||
|
$bagName = $existing ? $existing->bi_bag_name : '';
|
||||||
|
$invModel->adjustQty($lgIdx, $bagCode, $bagName, $qty);
|
||||||
|
} elseif ($type === 'sub') {
|
||||||
|
$bagName = $existing ? $existing->bi_bag_name : '';
|
||||||
|
$invModel->adjustQty($lgIdx, $bagCode, $bagName, -$qty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->to(site_url('bag/inventory'))->with('success', '재고가 조정되었습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// CRUD — 사이트 레이아웃으로 등록/처리 폼 제공
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
// --- 불출 등록 ---
|
||||||
|
public function issueCreate(): string
|
||||||
|
{
|
||||||
|
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $this->lgIdx()) : [];
|
||||||
|
return $this->render('불출 처리', 'bag/create_bag_issue', compact('bagCodes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function issueStore()
|
||||||
|
{
|
||||||
|
$admin = new \App\Controllers\Admin\BagIssue();
|
||||||
|
$admin->initController($this->request, $this->response, service('logger'));
|
||||||
|
$result = $admin->store();
|
||||||
|
if ($result instanceof \CodeIgniter\HTTP\RedirectResponse) {
|
||||||
|
$to = (string) $result->getHeaderLine('Location');
|
||||||
|
$to = str_replace('/admin/bag-issues', '/bag/issue', $to);
|
||||||
|
return redirect()->to($to)->with('success', session()->getFlashdata('success'))->with('errors', session()->getFlashdata('errors'));
|
||||||
|
}
|
||||||
|
return redirect()->to(site_url('bag/issue'))->with('success', '불출 처리되었습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function issueCancel(int $id)
|
||||||
|
{
|
||||||
|
$admin = new \App\Controllers\Admin\BagIssue();
|
||||||
|
$admin->initController($this->request, $this->response, service('logger'));
|
||||||
|
$admin->cancel($id);
|
||||||
|
return redirect()->to(site_url('bag/issue'))->with('success', session()->getFlashdata('success') ?? '취소되었습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 발주 등록 ---
|
||||||
|
public function orderCreate(): string
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
$companies = $lgIdx
|
||||||
|
? model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->whereIn('cp_type', ['제작업체', 'manufacturer'])->where('cp_state', 1)->findAll()
|
||||||
|
: [];
|
||||||
|
$agencies = $lgIdx ? model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll() : [];
|
||||||
|
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
$bagCodes = $kind ? model(CodeDetailModel::class)->where('cd_ck_idx', $kind->ck_idx)->where('cd_state', 1)->orderBy('cd_sort')->findAll() : [];
|
||||||
|
return $this->render('발주 등록', 'bag/create_bag_order', compact('companies', 'agencies', 'bagCodes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function orderStore()
|
||||||
|
{
|
||||||
|
$admin = new \App\Controllers\Admin\BagOrder();
|
||||||
|
$admin->initController($this->request, $this->response, service('logger'));
|
||||||
|
$result = $admin->store();
|
||||||
|
if ($result instanceof \CodeIgniter\HTTP\RedirectResponse) {
|
||||||
|
return redirect()->to(site_url('bag/purchase-inbound'))->with('success', session()->getFlashdata('success'))->with('errors', session()->getFlashdata('errors'));
|
||||||
|
}
|
||||||
|
return redirect()->to(site_url('bag/purchase-inbound'))->with('success', '발주 등록되었습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function orderCancel(int $id)
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
if (!$lgIdx) {
|
||||||
|
return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '지자체를 확인할 수 없습니다.');
|
||||||
|
}
|
||||||
|
$orderModel = model(BagOrderModel::class);
|
||||||
|
$order = $orderModel->find($id);
|
||||||
|
if (!$order || (int) $order->bo_lg_idx !== $lgIdx) {
|
||||||
|
return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '발주를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
$before = (array) $order;
|
||||||
|
$orderModel->update($id, ['bo_status' => 'cancelled', 'bo_moddate' => date('Y-m-d H:i:s')]);
|
||||||
|
helper('audit');
|
||||||
|
audit_log('update', 'bag_order', $id, $before, ['bo_status' => 'cancelled']);
|
||||||
|
|
||||||
|
return redirect()->to(site_url('bag/purchase-inbound'))->with('success', '발주가 취소되었습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 입고 처리 ---
|
||||||
|
public function receivingCreate(): string
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
$orders = $lgIdx ? model(BagOrderModel::class)->where('bo_lg_idx', $lgIdx)->where('bo_status', 'normal')->orderBy('bo_order_date', 'DESC')->findAll() : [];
|
||||||
|
return $this->render('입고 처리', 'bag/create_bag_receiving', compact('orders'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function receivingStore()
|
||||||
|
{
|
||||||
|
$admin = new \App\Controllers\Admin\BagReceiving();
|
||||||
|
$admin->initController($this->request, $this->response, service('logger'));
|
||||||
|
$result = $admin->store();
|
||||||
|
if ($result instanceof \CodeIgniter\HTTP\RedirectResponse) {
|
||||||
|
return redirect()->to(site_url('bag/purchase-inbound'))->with('success', session()->getFlashdata('success'))->with('errors', session()->getFlashdata('errors'));
|
||||||
|
}
|
||||||
|
return redirect()->to(site_url('bag/purchase-inbound'))->with('success', '입고 처리되었습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 판매 등록 ---
|
||||||
|
public function saleCreate(): string
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
$shops = $lgIdx ? model(DesignatedShopModel::class)->where('ds_lg_idx', $lgIdx)->where('ds_state', 1)->findAll() : [];
|
||||||
|
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
||||||
|
return $this->render('판매 등록', 'bag/create_bag_sale', compact('shops', 'bagCodes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saleStore()
|
||||||
|
{
|
||||||
|
$admin = new \App\Controllers\Admin\BagSale();
|
||||||
|
$admin->initController($this->request, $this->response, service('logger'));
|
||||||
|
$result = $admin->store();
|
||||||
|
if ($result instanceof \CodeIgniter\HTTP\RedirectResponse) {
|
||||||
|
return redirect()->to(site_url('bag/sales'))->with('success', session()->getFlashdata('success'))->with('errors', session()->getFlashdata('errors'));
|
||||||
|
}
|
||||||
|
return redirect()->to(site_url('bag/sales'))->with('success', '판매 등록되었습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 주문 접수 ---
|
||||||
|
public function shopOrderCreate(): string
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
$shops = $lgIdx ? model(DesignatedShopModel::class)->where('ds_lg_idx', $lgIdx)->where('ds_state', 1)->findAll() : [];
|
||||||
|
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
||||||
|
return $this->render('주문 접수', 'bag/create_shop_order', compact('shops', 'bagCodes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function shopOrderStore()
|
||||||
|
{
|
||||||
|
$admin = new \App\Controllers\Admin\ShopOrder();
|
||||||
|
$admin->initController($this->request, $this->response, service('logger'));
|
||||||
|
$result = $admin->store();
|
||||||
|
if ($result instanceof \CodeIgniter\HTTP\RedirectResponse) {
|
||||||
|
return redirect()->to(site_url('bag/sales'))->with('success', session()->getFlashdata('success'))->with('errors', session()->getFlashdata('errors'));
|
||||||
|
}
|
||||||
|
return redirect()->to(site_url('bag/sales'))->with('success', '주문 접수되었습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,4 +42,31 @@ 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);
|
||||||
|
$uri = service('request')->getUri();
|
||||||
|
$seg1 = $uri->getSegment(1);
|
||||||
|
$seg2 = $uri->getSegment(2);
|
||||||
|
|
||||||
|
// 지정판매소 관리는 관리자 전용 기능으로, /bag 경로여도 관리자 레이아웃을 유지한다.
|
||||||
|
$forceAdminLayoutOnBag = ($seg1 === 'bag' && $seg2 === 'designated-shops');
|
||||||
|
if ($seg1 === 'bag' && ! $forceAdminLayoutOnBag) {
|
||||||
|
return view('bag/layout/main', [
|
||||||
|
'title' => $title,
|
||||||
|
'content' => $content,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('admin/layout', [
|
||||||
|
'title' => $title,
|
||||||
|
'content' => $content,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,6 +66,14 @@ class Home extends BaseController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /dashboard 와 동일 본문(호환 URL)
|
||||||
|
*/
|
||||||
|
public function dashboardBlend()
|
||||||
|
{
|
||||||
|
return $this->dashboard();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 재고 조회(수불) 화면 (목업)
|
* 재고 조회(수불) 화면 (목업)
|
||||||
*/
|
*/
|
||||||
@@ -104,4 +117,5 @@ class Home extends BaseController
|
|||||||
|
|
||||||
return '북구 (데모)';
|
return '북구 (데모)';
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use Config\Roles;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 관리자 전용 접근 필터.
|
* 관리자 전용 접근 필터.
|
||||||
* logged_in 이고 mb_level 이 SUPER_ADMIN(4) 또는 LOCAL_ADMIN(3) 일 때만 통과.
|
* logged_in 이고 mb_level 이 SUPER_ADMIN(4)·HEADQUARTERS_ADMIN(5)·LOCAL_ADMIN(3) 일 때만 통과.
|
||||||
*/
|
*/
|
||||||
class AdminAuthFilter implements FilterInterface
|
class AdminAuthFilter implements FilterInterface
|
||||||
{
|
{
|
||||||
@@ -22,15 +22,16 @@ class AdminAuthFilter implements FilterInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$level = (int) session()->get('mb_level');
|
$level = (int) session()->get('mb_level');
|
||||||
if ($level !== Roles::LEVEL_SUPER_ADMIN && $level !== Roles::LEVEL_LOCAL_ADMIN) {
|
$isAdminLevel = Roles::isSuperAdminEquivalent($level) || $level === Roles::LEVEL_LOCAL_ADMIN;
|
||||||
|
if (! $isAdminLevel) {
|
||||||
return redirect()->to(site_url('/'))->with('error', '관리자만 접근할 수 있습니다.');
|
return redirect()->to(site_url('/'))->with('error', '관리자만 접근할 수 있습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Super admin: 지자체 미선택 시 지자체 선택 페이지로 유도 (지자체 선택·지자체 CRUD는 미선택도 허용)
|
// Super/본부: 지자체 미선택 시 지자체 선택 페이지로 유도 (지자체 선택·지자체 CRUD는 미선택도 허용)
|
||||||
$uri = $request->getUri();
|
$uri = $request->getUri();
|
||||||
$seg2 = $uri->getSegment(2);
|
$seg2 = $uri->getSegment(2);
|
||||||
$allowedWithoutSelection = ['select-local-government', 'local-governments'];
|
$allowedWithoutSelection = ['select-local-government', 'local-governments'];
|
||||||
if ($level === Roles::LEVEL_SUPER_ADMIN && ! in_array($seg2, $allowedWithoutSelection, true)) {
|
if (Roles::isSuperAdminEquivalent($level) && ! in_array($seg2, $allowedWithoutSelection, true)) {
|
||||||
$selected = session()->get('admin_selected_lg_idx');
|
$selected = session()->get('admin_selected_lg_idx');
|
||||||
if ($selected === null || $selected === '') {
|
if ($selected === null || $selected === '') {
|
||||||
return redirect()->to(site_url('admin/select-local-government'))->with('error', '작업할 지자체를 먼저 선택해 주세요.');
|
return redirect()->to(site_url('admin/select-local-government'))->with('error', '작업할 지자체를 먼저 선택해 주세요.');
|
||||||
|
|||||||
29
app/Filters/LoginAuthFilter.php
Normal file
29
app/Filters/LoginAuthFilter.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,17 +6,20 @@ use Config\Roles;
|
|||||||
|
|
||||||
if (! function_exists('admin_effective_lg_idx')) {
|
if (! function_exists('admin_effective_lg_idx')) {
|
||||||
/**
|
/**
|
||||||
* 현재 로그인한 관리자가 작업 대상으로 사용하는 지자체 PK.
|
* 관리자 화면·사이트 메뉴·Bag 등에서 쓰는 작업 지자체 PK.
|
||||||
* Super admin → 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
|
||||||
{
|
{
|
||||||
$level = (int) session()->get('mb_level');
|
$level = (int) session()->get('mb_level');
|
||||||
if ($level === Roles::LEVEL_SUPER_ADMIN) {
|
if (Roles::isSuperAdminEquivalent($level)) {
|
||||||
$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,10 +27,27 @@ 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 기준, 평면 배열).
|
||||||
* 지자체 미선택(super admin)이면 빈 배열. 테이블/조회 실패 시에도 빈 배열.
|
* 지자체 미선택(super/본부)이면 빈 배열. 테이블/조회 실패 시에도 빈 배열.
|
||||||
*
|
*
|
||||||
* 하위 메뉴 포함 트리 구조가 필요하면 get_admin_nav_tree() 사용.
|
* 하위 메뉴 포함 트리 구조가 필요하면 get_admin_nav_tree() 사용.
|
||||||
*/
|
*/
|
||||||
@@ -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,319 @@ 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('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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
43
app/Helpers/audit_helper.php
Normal file
43
app/Helpers/audit_helper.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
if (! function_exists('audit_log')) {
|
||||||
|
/**
|
||||||
|
* CRUD 활동 로그 기록
|
||||||
|
*
|
||||||
|
* @param string $action 'create', 'update', 'delete'
|
||||||
|
* @param string $table 대상 테이블명
|
||||||
|
* @param int $recordId 대상 레코드 PK
|
||||||
|
* @param array|null $before 변경 전 데이터 (update/delete 시)
|
||||||
|
* @param array|null $after 변경 후 데이터 (create/update 시)
|
||||||
|
*/
|
||||||
|
function audit_log(string $action, string $table, int $recordId, ?array $before = null, ?array $after = null): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
// 테이블 존재 여부 확인 (없으면 skip)
|
||||||
|
if ($db->query("SHOW TABLES LIKE 'activity_log'")->getNumRows() === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mbIdx = session()->get('mb_idx');
|
||||||
|
$ip = service('request')->getIPAddress();
|
||||||
|
|
||||||
|
model(\App\Models\ActivityLogModel::class)->insert([
|
||||||
|
'al_mb_idx' => $mbIdx ? (int) $mbIdx : null,
|
||||||
|
'al_action' => $action,
|
||||||
|
'al_table' => $table,
|
||||||
|
'al_record_id' => $recordId,
|
||||||
|
'al_data_before' => $before !== null ? json_encode($before, JSON_UNESCAPED_UNICODE) : null,
|
||||||
|
'al_data_after' => $after !== null ? json_encode($after, JSON_UNESCAPED_UNICODE) : null,
|
||||||
|
'al_ip' => $ip,
|
||||||
|
'al_regdate' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// 로깅 실패 시 본 로직 방해하지 않음
|
||||||
|
log_message('error', 'audit_log failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
app/Helpers/export_helper.php
Normal file
69
app/Helpers/export_helper.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSV 엑셀 내보내기 헬퍼
|
||||||
|
*
|
||||||
|
* UTF-8 BOM 포함으로 한글 엑셀 호환성 보장
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (! function_exists('export_csv')) {
|
||||||
|
/**
|
||||||
|
* CSV 파일을 브라우저로 다운로드 전송
|
||||||
|
*
|
||||||
|
* @param string $filename 파일명 (확장자 포함, 예: 'export.csv')
|
||||||
|
* @param string[] $headers 컬럼 헤더 배열
|
||||||
|
* @param array $rows 데이터 행 배열 (각 행은 배열)
|
||||||
|
*/
|
||||||
|
function export_csv(string $filename, array $headers, array $rows): void
|
||||||
|
{
|
||||||
|
// 파일명에 .csv 확장자 보장
|
||||||
|
if (! str_ends_with($filename, '.csv')) {
|
||||||
|
$filename .= '.csv';
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = service('response');
|
||||||
|
$response->setHeader('Content-Type', 'text/csv; charset=UTF-8');
|
||||||
|
$response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
|
||||||
|
$response->setHeader('Pragma', 'no-cache');
|
||||||
|
$response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||||
|
|
||||||
|
// UTF-8 BOM (한글 엑셀 호환)
|
||||||
|
$output = "\xEF\xBB\xBF";
|
||||||
|
|
||||||
|
// 헤더 행
|
||||||
|
$output .= csv_encode_row($headers);
|
||||||
|
|
||||||
|
// 데이터 행
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$output .= csv_encode_row(array_values((array) $row));
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->setBody($output);
|
||||||
|
$response->send();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! function_exists('csv_encode_row')) {
|
||||||
|
/**
|
||||||
|
* 배열 한 행을 CSV 문자열로 변환
|
||||||
|
*
|
||||||
|
* @param array $fields
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function csv_encode_row(array $fields): string
|
||||||
|
{
|
||||||
|
$escaped = [];
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
$val = (string) ($field ?? '');
|
||||||
|
// 쌍따옴표 이스케이프 및 감싸기
|
||||||
|
if (str_contains($val, '"') || str_contains($val, ',') || str_contains($val, "\n") || str_contains($val, "\r")) {
|
||||||
|
$val = '"' . str_replace('"', '""', $val) . '"';
|
||||||
|
}
|
||||||
|
$escaped[] = $val;
|
||||||
|
}
|
||||||
|
return implode(',', $escaped) . "\r\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
app/Libraries/TotpService.php
Normal file
49
app/Libraries/TotpService.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Libraries;
|
||||||
|
|
||||||
|
use Config\Auth as AuthConfig;
|
||||||
|
use RobThree\Auth\Providers\Qr\QRServerProvider;
|
||||||
|
use RobThree\Auth\TwoFactorAuth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TOTP 생성·검증·QR (robthree/twofactorauth)
|
||||||
|
*/
|
||||||
|
class TotpService
|
||||||
|
{
|
||||||
|
private TwoFactorAuth $tfa;
|
||||||
|
|
||||||
|
public function __construct(?AuthConfig $authConfig = null)
|
||||||
|
{
|
||||||
|
$authConfig ??= config('Auth');
|
||||||
|
$this->tfa = new TwoFactorAuth(
|
||||||
|
new QRServerProvider(),
|
||||||
|
$authConfig->totpIssuer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createSecret(): string
|
||||||
|
{
|
||||||
|
return $this->tfa->createSecret();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verify(string $plainSecret, string $code): bool
|
||||||
|
{
|
||||||
|
$code = preg_replace('/\s+/', '', $code) ?? '';
|
||||||
|
if (strlen($code) !== 6 || ! ctype_digit($code)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->tfa->verifyCode($plainSecret, $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PNG data URI (외부 QR API 호출 — 네트워크 필요)
|
||||||
|
*/
|
||||||
|
public function getQrDataUri(string $accountLabel, string $secret): string
|
||||||
|
{
|
||||||
|
return $this->tfa->getQRCodeImageAsDataUri($accountLabel, $secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Models/ActivityLogModel.php
Normal file
25
app/Models/ActivityLogModel.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use CodeIgniter\Model;
|
||||||
|
|
||||||
|
class ActivityLogModel extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'activity_log';
|
||||||
|
protected $primaryKey = 'al_idx';
|
||||||
|
protected $returnType = 'object';
|
||||||
|
protected $useTimestamps = false;
|
||||||
|
protected $allowedFields = [
|
||||||
|
'al_mb_idx',
|
||||||
|
'al_action',
|
||||||
|
'al_table',
|
||||||
|
'al_record_id',
|
||||||
|
'al_data_before',
|
||||||
|
'al_data_after',
|
||||||
|
'al_ip',
|
||||||
|
'al_regdate',
|
||||||
|
];
|
||||||
|
}
|
||||||
38
app/Models/BagInventoryModel.php
Normal file
38
app/Models/BagInventoryModel.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use CodeIgniter\Model;
|
||||||
|
|
||||||
|
class BagInventoryModel extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'bag_inventory';
|
||||||
|
protected $primaryKey = 'bi_idx';
|
||||||
|
protected $returnType = 'object';
|
||||||
|
protected $useTimestamps = false;
|
||||||
|
protected $allowedFields = [
|
||||||
|
'bi_lg_idx', 'bi_bag_code', 'bi_bag_name', 'bi_qty', 'bi_updated_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 재고 증감 (upsert)
|
||||||
|
*/
|
||||||
|
public function adjustQty(int $lgIdx, string $bagCode, string $bagName, int $delta): void
|
||||||
|
{
|
||||||
|
$existing = $this->where('bi_lg_idx', $lgIdx)->where('bi_bag_code', $bagCode)->first();
|
||||||
|
if ($existing) {
|
||||||
|
$this->update($existing->bi_idx, [
|
||||||
|
'bi_qty' => max(0, (int) $existing->bi_qty + $delta),
|
||||||
|
'bi_updated_at' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$this->insert([
|
||||||
|
'bi_lg_idx' => $lgIdx,
|
||||||
|
'bi_bag_code' => $bagCode,
|
||||||
|
'bi_bag_name' => $bagName,
|
||||||
|
'bi_qty' => max(0, $delta),
|
||||||
|
'bi_updated_at'=> date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
app/Models/BagIssueModel.php
Normal file
18
app/Models/BagIssueModel.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use CodeIgniter\Model;
|
||||||
|
|
||||||
|
class BagIssueModel extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'bag_issue';
|
||||||
|
protected $primaryKey = 'bi2_idx';
|
||||||
|
protected $returnType = 'object';
|
||||||
|
protected $useTimestamps = false;
|
||||||
|
protected $allowedFields = [
|
||||||
|
'bi2_lg_idx', 'bi2_year', 'bi2_quarter', 'bi2_issue_type', 'bi2_issue_date',
|
||||||
|
'bi2_dest_type', 'bi2_dest_name', 'bi2_bag_code', 'bi2_bag_name',
|
||||||
|
'bi2_qty', 'bi2_status', 'bi2_regdate',
|
||||||
|
];
|
||||||
|
}
|
||||||
17
app/Models/BagOrderItemModel.php
Normal file
17
app/Models/BagOrderItemModel.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use CodeIgniter\Model;
|
||||||
|
|
||||||
|
class BagOrderItemModel extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'bag_order_item';
|
||||||
|
protected $primaryKey = 'boi_idx';
|
||||||
|
protected $returnType = 'object';
|
||||||
|
protected $useTimestamps = false;
|
||||||
|
protected $allowedFields = [
|
||||||
|
'boi_bo_idx', 'boi_bag_code', 'boi_bag_name',
|
||||||
|
'boi_unit_price', 'boi_qty_box', 'boi_qty_sheet', 'boi_amount',
|
||||||
|
];
|
||||||
|
}
|
||||||
19
app/Models/BagOrderModel.php
Normal file
19
app/Models/BagOrderModel.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use CodeIgniter\Model;
|
||||||
|
|
||||||
|
class BagOrderModel extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'bag_order';
|
||||||
|
protected $primaryKey = 'bo_idx';
|
||||||
|
protected $returnType = 'object';
|
||||||
|
protected $useTimestamps = false;
|
||||||
|
protected $allowedFields = [
|
||||||
|
'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_lot_no', 'bo_hash', 'bo_status', 'bo_orderer_idx',
|
||||||
|
'bo_regdate', 'bo_moddate',
|
||||||
|
];
|
||||||
|
}
|
||||||
18
app/Models/BagReceivingModel.php
Normal file
18
app/Models/BagReceivingModel.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use CodeIgniter\Model;
|
||||||
|
|
||||||
|
class BagReceivingModel extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'bag_receiving';
|
||||||
|
protected $primaryKey = 'br_idx';
|
||||||
|
protected $returnType = 'object';
|
||||||
|
protected $useTimestamps = false;
|
||||||
|
protected $allowedFields = [
|
||||||
|
'br_bo_idx', 'br_lg_idx', 'br_bag_code', 'br_bag_name',
|
||||||
|
'br_qty_box', 'br_qty_sheet', 'br_receive_date',
|
||||||
|
'br_receiver_idx', 'br_sender_name', 'br_type', 'br_regdate',
|
||||||
|
];
|
||||||
|
}
|
||||||
18
app/Models/BagSaleModel.php
Normal file
18
app/Models/BagSaleModel.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use CodeIgniter\Model;
|
||||||
|
|
||||||
|
class BagSaleModel extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'bag_sale';
|
||||||
|
protected $primaryKey = 'bs_idx';
|
||||||
|
protected $returnType = 'object';
|
||||||
|
protected $useTimestamps = false;
|
||||||
|
protected $allowedFields = [
|
||||||
|
'bs_lg_idx', 'bs_so_idx', 'bs_ds_idx', 'bs_ds_name', 'bs_sale_date',
|
||||||
|
'bs_bag_code', 'bs_bag_name', 'bs_qty', 'bs_unit_price', 'bs_amount',
|
||||||
|
'bs_type', 'bs_regdate',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class MemberModel extends Model
|
|||||||
protected $allowedFields = [
|
protected $allowedFields = [
|
||||||
'mb_id',
|
'mb_id',
|
||||||
'mb_passwd',
|
'mb_passwd',
|
||||||
|
'mb_totp_secret',
|
||||||
|
'mb_totp_enabled',
|
||||||
'mb_name',
|
'mb_name',
|
||||||
'mb_email',
|
'mb_email',
|
||||||
'mb_phone',
|
'mb_phone',
|
||||||
@@ -24,6 +26,8 @@ class MemberModel extends Model
|
|||||||
'mb_regdate',
|
'mb_regdate',
|
||||||
'mb_latestdate',
|
'mb_latestdate',
|
||||||
'mb_leavedate',
|
'mb_leavedate',
|
||||||
|
'mb_login_fail_count',
|
||||||
|
'mb_locked_until',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ class MenuModel extends Model
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 mb_level에 노출할 메뉴만 필터링 (mm_is_view=Y, mm_level에 해당 레벨 포함 또는 빈값).
|
* 특정 mb_level에 노출할 메뉴만 필터링 (mm_is_view=Y, mm_level에 해당 레벨 포함 또는 빈값).
|
||||||
* lg_idx 기준 해당 지자체 메뉴만 대상. super admin(4)은 mm_level 무관하게 해당 지자체 메뉴 전체 노출.
|
* lg_idx 기준 해당 지자체 메뉴만 대상. super/본부(4·5)는 mm_level 무관하게 해당 지자체 메뉴 전체 노출.
|
||||||
*/
|
*/
|
||||||
public function getVisibleByLevel(int $mtIdx, int $mbLevel, int $lgIdx): array
|
public function getVisibleByLevel(int $mtIdx, int $mbLevel, int $lgIdx): array
|
||||||
{
|
{
|
||||||
$all = $this->getAllByType($mtIdx, $lgIdx);
|
$all = $this->getAllByType($mtIdx, $lgIdx);
|
||||||
if ($mbLevel === \Config\Roles::LEVEL_SUPER_ADMIN) {
|
if (\Config\Roles::isSuperAdminEquivalent($mbLevel)) {
|
||||||
return array_values(array_filter($all, static fn ($row) => (string) $row->mm_is_view === 'Y'));
|
return array_values(array_filter($all, static fn ($row) => (string) $row->mm_is_view === 'Y'));
|
||||||
}
|
}
|
||||||
$levelStr = (string) $mbLevel;
|
$levelStr = (string) $mbLevel;
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
app/Models/ShopOrderItemModel.php
Normal file
17
app/Models/ShopOrderItemModel.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use CodeIgniter\Model;
|
||||||
|
|
||||||
|
class ShopOrderItemModel extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'shop_order_item';
|
||||||
|
protected $primaryKey = 'soi_idx';
|
||||||
|
protected $returnType = 'object';
|
||||||
|
protected $useTimestamps = false;
|
||||||
|
protected $allowedFields = [
|
||||||
|
'soi_so_idx', 'soi_bag_code', 'soi_bag_name', 'soi_unit_price',
|
||||||
|
'soi_qty', 'soi_amount', 'soi_box_count', 'soi_pack_count', 'soi_sheet_count',
|
||||||
|
];
|
||||||
|
}
|
||||||
18
app/Models/ShopOrderModel.php
Normal file
18
app/Models/ShopOrderModel.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use CodeIgniter\Model;
|
||||||
|
|
||||||
|
class ShopOrderModel extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'shop_order';
|
||||||
|
protected $primaryKey = 'so_idx';
|
||||||
|
protected $returnType = 'object';
|
||||||
|
protected $useTimestamps = false;
|
||||||
|
protected $allowedFields = [
|
||||||
|
'so_lg_idx', 'so_ds_idx', 'so_ds_name', 'so_order_date', 'so_delivery_date',
|
||||||
|
'so_payment_type', 'so_paid', 'so_received', 'so_total_qty', 'so_total_amount',
|
||||||
|
'so_status', 'so_orderer_idx', 'so_regdate',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
|
<?= view('components/print_header', ['printTitle' => '권한 승인 대기']) ?>
|
||||||
<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">
|
||||||
<span class="text-sm font-bold text-gray-700">권한 승인 대기</span>
|
<span class="text-sm font-bold text-gray-700">권한 승인 대기</span>
|
||||||
|
<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>
|
||||||
</section>
|
</section>
|
||||||
<div class="border border-gray-300 p-4 mt-2">
|
<div class="border border-gray-300 p-4 mt-2">
|
||||||
<form method="get" action="<?= base_url('admin/access/approvals') ?>" class="mb-4 flex flex-wrap items-center gap-2 text-sm">
|
<form method="get" action="<?= base_url('admin/access/approvals') ?>" class="mb-4 flex flex-wrap items-center gap-2 text-sm">
|
||||||
@@ -64,3 +68,4 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<?= view('components/print_header', ['printTitle' => '로그인 이력']) ?>
|
||||||
<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">
|
||||||
<div class="flex items-center gap-4 text-sm">
|
<div class="flex items-center gap-4 text-sm">
|
||||||
@@ -9,6 +10,7 @@
|
|||||||
<button type="submit" 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">조회</button>
|
<button type="submit" 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">조회</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<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>
|
||||||
</section>
|
</section>
|
||||||
<div class="border border-gray-300 overflow-auto mt-2">
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
@@ -35,3 +37,4 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||||
|
|||||||
38
app/Views/admin/bag_inventory/index.php
Normal file
38
app/Views/admin/bag_inventory/index.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?= 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">
|
||||||
|
<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>
|
||||||
|
</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>봉투코드</th>
|
||||||
|
<th>봉투명</th>
|
||||||
|
<th>현재재고(낱장)</th>
|
||||||
|
<th>최종갱신</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-right">
|
||||||
|
<?php foreach ($list as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center"><?= esc($row->bi_idx) ?></td>
|
||||||
|
<td class="text-center font-mono"><?= esc($row->bi_bag_code) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($row->bi_bag_name) ?></td>
|
||||||
|
<td class="font-bold"><?= number_format((int) $row->bi_qty) ?></td>
|
||||||
|
<td class="text-center"><?= esc($row->bi_updated_at) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($list)): ?>
|
||||||
|
<tr><td colspan="5" class="text-center text-gray-400 py-4">등록된 재고가 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||||
70
app/Views/admin/bag_issue/create.php
Normal file
70
app/Views/admin/bag_issue/create.php
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
|
<span class="text-sm font-bold text-gray-700">무료용 불출 처리</span>
|
||||||
|
</section>
|
||||||
|
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
||||||
|
<form action="<?= mgmt_url('bag-issues/store') ?>" method="POST" class="space-y-4">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 text-right" name="bi2_year" type="number" min="2000" max="2099" value="<?= esc(old('bi2_year', date('Y'))) ?>" required/>
|
||||||
|
</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="bi2_quarter" required>
|
||||||
|
<option value="">선택</option>
|
||||||
|
<option value="1" <?= old('bi2_quarter') === '1' ? 'selected' : '' ?>>1</option>
|
||||||
|
<option value="2" <?= old('bi2_quarter') === '2' ? 'selected' : '' ?>>2</option>
|
||||||
|
<option value="3" <?= old('bi2_quarter') === '3' ? 'selected' : '' ?>>3</option>
|
||||||
|
<option value="4" <?= old('bi2_quarter') === '4' ? 'selected' : '' ?>>4</option>
|
||||||
|
</select>
|
||||||
|
</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-44" name="bi2_issue_type" required>
|
||||||
|
<option value="">선택</option>
|
||||||
|
<option value="무료용" <?= old('bi2_issue_type') === '무료용' ? 'selected' : '' ?>>무료용</option>
|
||||||
|
<option value="공공용" <?= old('bi2_issue_type') === '공공용' ? 'selected' : '' ?>>공공용</option>
|
||||||
|
</select>
|
||||||
|
</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>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bi2_issue_date" type="date" value="<?= esc(old('bi2_issue_date', date('Y-m-d'))) ?>" required/>
|
||||||
|
</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-44" name="bi2_dest_type" type="text" placeholder="동사무소" value="<?= esc(old('bi2_dest_type')) ?>"/>
|
||||||
|
</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>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bi2_dest_name" type="text" value="<?= esc(old('bi2_dest_name')) ?>" required/>
|
||||||
|
</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-60" name="bi2_bag_code" required>
|
||||||
|
<option value="">선택</option>
|
||||||
|
<?php foreach ($bagCodes as $cd): ?>
|
||||||
|
<option value="<?= esc($cd->cd_code) ?>" <?= old('bi2_bag_code') === $cd->cd_code ? 'selected' : '' ?>>
|
||||||
|
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_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">수량 <span class="text-red-500">*</span></label>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 text-right" name="bi2_qty" type="number" min="0" value="<?= esc(old('bi2_qty', '0')) ?>" required/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
65
app/Views/admin/bag_issue/index.php
Normal file
65
app/Views/admin/bag_issue/index.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?= 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="<?= 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>
|
||||||
|
</section>
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200">
|
||||||
|
<form method="GET" action="<?= mgmt_url('bag-issues') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="text-sm text-gray-600">불출일</label>
|
||||||
|
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
|
<label class="text-sm text-gray-600">~</label>
|
||||||
|
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
|
<a href="<?= mgmt_url('bag-issues') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
|
||||||
|
</form>
|
||||||
|
</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>연도</th>
|
||||||
|
<th>분기</th>
|
||||||
|
<th>구분</th>
|
||||||
|
<th>불출일</th>
|
||||||
|
<th>불출처</th>
|
||||||
|
<th>봉투코드</th>
|
||||||
|
<th>봉투명</th>
|
||||||
|
<th>수량</th>
|
||||||
|
<th class="w-20">상태</th>
|
||||||
|
<th class="w-24">작업</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-right">
|
||||||
|
<?php foreach ($list as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center"><?= esc($row->bi2_idx) ?></td>
|
||||||
|
<td class="text-center"><?= esc($row->bi2_year) ?></td>
|
||||||
|
<td class="text-center"><?= esc($row->bi2_quarter) ?></td>
|
||||||
|
<td class="text-center"><?= esc($row->bi2_issue_type) ?></td>
|
||||||
|
<td class="text-center"><?= esc($row->bi2_issue_date) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($row->bi2_dest_name) ?></td>
|
||||||
|
<td class="text-center font-mono"><?= esc($row->bi2_bag_code) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($row->bi2_bag_name) ?></td>
|
||||||
|
<td><?= number_format((int) $row->bi2_qty) ?></td>
|
||||||
|
<td class="text-center"><?= esc($row->bi2_status) ?></td>
|
||||||
|
<td class="text-center">
|
||||||
|
<form action="<?= mgmt_url('bag-issues/cancel/' . (int) $row->bi2_idx) ?>" method="POST" class="inline" onsubmit="return confirm('취소하시겠습니까?');">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<button type="submit" class="text-orange-600 hover:underline text-sm">취소</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($list)): ?>
|
||||||
|
<tr><td colspan="11" class="text-center text-gray-400 py-4">등록된 불출이 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||||
83
app/Views/admin/bag_order/create.php
Normal file
83
app/Views/admin/bag_order/create.php
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
|
<span class="text-sm font-bold text-gray-700">발주 등록</span>
|
||||||
|
</section>
|
||||||
|
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-4xl">
|
||||||
|
<form action="<?= mgmt_url('bag-orders/store') ?>" method="POST" class="space-y-4">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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/>
|
||||||
|
</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-32 text-right" name="bo_fee_rate" type="number" step="0.01" value="<?= esc(old('bo_fee_rate', '0')) ?>"/>
|
||||||
|
<span class="text-sm text-gray-500">%</span>
|
||||||
|
</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_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_kind ?? '') ?>] <?= esc($ag->sa_code ?? '') ?> — <?= 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>
|
||||||
|
<tr>
|
||||||
|
<th class="w-16">순번</th>
|
||||||
|
<th>봉투</th>
|
||||||
|
<th class="w-32">박스수</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php for ($i = 0; $i < 3; $i++): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center"><?= $i + 1 ?></td>
|
||||||
|
<td>
|
||||||
|
<select class="border border-gray-300 rounded px-2 py-1 text-sm w-full" name="item_bag_code[]">
|
||||||
|
<option value="">선택</option>
|
||||||
|
<?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>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
102
app/Views/admin/bag_order/detail.php
Normal file
102
app/Views/admin/bag_order/detail.php
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<a href="<?= mgmt_url('bag-orders') ?>" class="text-blue-600 hover:underline text-sm">← 발주 목록</a>
|
||||||
|
<span class="text-gray-400">|</span>
|
||||||
|
<span class="text-sm font-bold text-gray-700">발주 상세 — <?= esc($order->bo_lot_no) ?></span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-4xl">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-b">
|
||||||
|
<th class="text-left py-2 pr-4 text-gray-600 w-28">UUID</th>
|
||||||
|
<td class="py-2 font-mono"><?= esc($order->bo_uuid) ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<th class="text-left py-2 pr-4 text-gray-600 w-28">버전</th>
|
||||||
|
<td class="py-2"><?= esc($order->bo_version) ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<th class="text-left py-2 pr-4 text-gray-600 w-28">발주일</th>
|
||||||
|
<td class="py-2"><?= esc($order->bo_order_date) ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<th class="text-left py-2 pr-4 text-gray-600 w-28">제작업체</th>
|
||||||
|
<td class="py-2"><?= esc($companyName ?? '') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<th class="text-left py-2 pr-4 text-gray-600 w-28">입고처</th>
|
||||||
|
<td class="py-2"><?= esc($agencyName ?? '') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<th class="text-left py-2 pr-4 text-gray-600 w-28">LOT번호</th>
|
||||||
|
<td class="py-2 font-mono"><?= esc($order->bo_lot_no) ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<th class="text-left py-2 pr-4 text-gray-600 w-28">수수료율</th>
|
||||||
|
<td class="py-2"><?= esc($order->bo_fee_rate) ?>%</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<th class="text-left py-2 pr-4 text-gray-600 w-28">상태</th>
|
||||||
|
<td class="py-2">
|
||||||
|
<?php
|
||||||
|
$statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제'];
|
||||||
|
echo esc($statusMap[$order->bo_status] ?? $order->bo_status);
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left py-2 pr-4 text-gray-600 w-28">해시</th>
|
||||||
|
<td class="py-2 font-mono text-xs"><?= esc($order->bo_hash) ?></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border border-gray-300 overflow-auto mt-4">
|
||||||
|
<table class="w-full data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>봉투코드</th>
|
||||||
|
<th>봉투명</th>
|
||||||
|
<th>단가</th>
|
||||||
|
<th>박스수</th>
|
||||||
|
<th>낱장수</th>
|
||||||
|
<th>금액</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-right">
|
||||||
|
<?php
|
||||||
|
$totalQtyBox = 0;
|
||||||
|
$totalQtySheet = 0;
|
||||||
|
$totalAmount = 0;
|
||||||
|
?>
|
||||||
|
<?php foreach ($items as $item): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center font-mono"><?= esc($item->boi_bag_code) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($item->boi_bag_name) ?></td>
|
||||||
|
<td><?= number_format((float) $item->boi_unit_price) ?></td>
|
||||||
|
<td><?= number_format((int) $item->boi_qty_box) ?></td>
|
||||||
|
<td><?= number_format((int) $item->boi_qty_sheet) ?></td>
|
||||||
|
<td><?= number_format((float) $item->boi_amount) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php
|
||||||
|
$totalQtyBox += (int) $item->boi_qty_box;
|
||||||
|
$totalQtySheet += (int) $item->boi_qty_sheet;
|
||||||
|
$totalAmount += (float) $item->boi_amount;
|
||||||
|
?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($items)): ?>
|
||||||
|
<tr><td colspan="6" class="text-center text-gray-400 py-4">등록된 품목이 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
<tfoot class="bg-gray-50 font-bold text-right">
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-center">합계</td>
|
||||||
|
<td><?= number_format($totalQtyBox) ?></td>
|
||||||
|
<td><?= number_format($totalQtySheet) ?></td>
|
||||||
|
<td><?= number_format($totalAmount) ?></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
81
app/Views/admin/bag_order/index.php
Normal file
81
app/Views/admin/bag_order/index.php
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?= 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">
|
||||||
|
<a href="<?= mgmt_url('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>
|
||||||
|
<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('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>
|
||||||
|
</section>
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200">
|
||||||
|
<form method="GET" action="<?= mgmt_url('bag-orders') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="text-sm text-gray-600">발주일</label>
|
||||||
|
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
|
<label class="text-sm text-gray-600">~</label>
|
||||||
|
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
|
<label class="text-sm text-gray-600">상태</label>
|
||||||
|
<select name="status" class="border border-gray-300 rounded px-2 py-1 text-sm">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="normal" <?= ($status ?? '') === 'normal' ? 'selected' : '' ?>>정상</option>
|
||||||
|
<option value="cancelled" <?= ($status ?? '') === 'cancelled' ? 'selected' : '' ?>>취소</option>
|
||||||
|
<option value="deleted" <?= ($status ?? '') === 'deleted' ? 'selected' : '' ?>>삭제</option>
|
||||||
|
</select>
|
||||||
|
<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">초기화</a>
|
||||||
|
</form>
|
||||||
|
</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>LOT번호</th>
|
||||||
|
<th>발주일</th>
|
||||||
|
<th>제작업체</th>
|
||||||
|
<th>입고처</th>
|
||||||
|
<th>품목수</th>
|
||||||
|
<th>총수량</th>
|
||||||
|
<th>총금액</th>
|
||||||
|
<th class="w-20">상태</th>
|
||||||
|
<th class="w-44">작업</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-right">
|
||||||
|
<?php foreach ($list as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center"><?= esc($row->bo_idx) ?></td>
|
||||||
|
<td class="text-center font-mono"><?= esc($row->bo_lot_no) ?></td>
|
||||||
|
<td class="text-center"><?= esc($row->bo_order_date) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($companyMap[$row->bo_company_idx] ?? '') ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($agencyMap[$row->bo_agency_idx] ?? '') ?></td>
|
||||||
|
<td><?= number_format((int) ($itemSummary[$row->bo_idx]['count'] ?? 0)) ?></td>
|
||||||
|
<td><?= number_format((int) ($itemSummary[$row->bo_idx]['qty'] ?? 0)) ?></td>
|
||||||
|
<td><?= number_format((int) ($itemSummary[$row->bo_idx]['amount'] ?? 0)) ?></td>
|
||||||
|
<td class="text-center">
|
||||||
|
<?php
|
||||||
|
$statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제'];
|
||||||
|
echo esc($statusMap[$row->bo_status] ?? $row->bo_status);
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="<?= mgmt_url('bag-orders/detail/' . (int) $row->bo_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">상세</a>
|
||||||
|
<form action="<?= mgmt_url('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="<?= mgmt_url('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>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($list)): ?>
|
||||||
|
<tr><td colspan="10" class="text-center text-gray-400 py-4">등록된 발주가 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">← 단가 목록</a>
|
<a href="<?= mgmt_url('bag-prices') ?>" class="text-blue-600 hover:underline text-sm">← 단가 목록</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>
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
|
<?= view('components/print_header', ['printTitle' => '봉투 단가 관리']) ?>
|
||||||
|
<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>
|
||||||
<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>
|
<div class="flex items-center gap-2 no-print">
|
||||||
|
<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('bag/prices') ?>" class="text-blue-600 hover:underline text-sm">단가 조회·검색</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>
|
||||||
</section>
|
</section>
|
||||||
<section class="p-2 bg-white border-b border-gray-200">
|
<p class="text-xs text-gray-500 mt-2 no-print">목록·등록·수정·삭제는 이 화면에서, <strong>기간·봉투별 조회·인쇄</strong>는 <a href="<?= base_url('bag/prices') ?>" class="text-blue-600 hover:underline">봉투 단가(조회)</a>에서 이용하세요.</p>
|
||||||
<form method="GET" action="<?= base_url('admin/bag-prices') ?>" class="flex flex-wrap items-center gap-2">
|
|
||||||
<label class="text-sm text-gray-600">적용시작일</label>
|
|
||||||
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
|
||||||
<label class="text-sm text-gray-600">~</label>
|
|
||||||
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
|
||||||
<a href="<?= base_url('admin/bag-prices') ?>" 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>
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
<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">
|
||||||
@@ -42,10 +43,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>
|
||||||
@@ -58,3 +59,4 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (isset($pager)): ?><div class="mt-3 no-print"><?= $pager->links() ?></div><?php endif; ?>
|
||||||
|
|||||||
53
app/Views/admin/bag_receiving/create.php
Normal file
53
app/Views/admin/bag_receiving/create.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
|
<span class="text-sm font-bold text-gray-700">입고 처리</span>
|
||||||
|
</section>
|
||||||
|
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
||||||
|
<form action="<?= mgmt_url('bag-receivings/store') ?>" method="POST" class="space-y-4">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
|
<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-72" name="br_bo_idx">
|
||||||
|
<option value="">선택</option>
|
||||||
|
<?php foreach ($orders as $od): ?>
|
||||||
|
<option value="<?= esc($od->bo_idx) ?>" <?= (int) old('br_bo_idx') === (int) $od->bo_idx ? 'selected' : '' ?>>
|
||||||
|
<?= esc($od->bo_lot_no) ?> (<?= esc($od->bo_order_date) ?>)
|
||||||
|
</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>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="br_bag_code" type="text" value="<?= esc(old('br_bag_code')) ?>"/>
|
||||||
|
</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>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 text-right" name="br_qty_box" type="number" min="0" value="<?= esc(old('br_qty_box', '0')) ?>" required/>
|
||||||
|
</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>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="br_receive_date" type="date" value="<?= esc(old('br_receive_date', date('Y-m-d'))) ?>" required/>
|
||||||
|
</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-44" name="br_sender_name" type="text" value="<?= esc(old('br_sender_name')) ?>"/>
|
||||||
|
</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-44" name="br_type">
|
||||||
|
<option value="batch" <?= old('br_type') === 'batch' ? 'selected' : '' ?>>batch</option>
|
||||||
|
<option value="scanner" <?= old('br_type') === 'scanner' ? 'selected' : '' ?>>scanner</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
54
app/Views/admin/bag_receiving/index.php
Normal file
54
app/Views/admin/bag_receiving/index.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?= 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="<?= 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>
|
||||||
|
</section>
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200">
|
||||||
|
<form method="GET" action="<?= mgmt_url('bag-receivings') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="text-sm text-gray-600">입고일</label>
|
||||||
|
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
|
<label class="text-sm text-gray-600">~</label>
|
||||||
|
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
|
<a href="<?= mgmt_url('bag-receivings') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
|
||||||
|
</form>
|
||||||
|
</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>봉투코드</th>
|
||||||
|
<th>봉투명</th>
|
||||||
|
<th>박스수</th>
|
||||||
|
<th>낱장수</th>
|
||||||
|
<th>입고일</th>
|
||||||
|
<th>구분</th>
|
||||||
|
<th>등록일</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-right">
|
||||||
|
<?php foreach ($list as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center"><?= esc($row->br_idx) ?></td>
|
||||||
|
<td class="text-center font-mono"><?= esc($row->br_bag_code) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($row->br_bag_name) ?></td>
|
||||||
|
<td><?= number_format((int) $row->br_qty_box) ?></td>
|
||||||
|
<td><?= number_format((int) $row->br_qty_sheet) ?></td>
|
||||||
|
<td class="text-center"><?= esc($row->br_receive_date) ?></td>
|
||||||
|
<td class="text-center"><?= esc($row->br_type) ?></td>
|
||||||
|
<td class="text-center"><?= esc($row->br_regdate) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($list)): ?>
|
||||||
|
<tr><td colspan="8" class="text-center text-gray-400 py-4">등록된 입고가 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||||
56
app/Views/admin/bag_sale/create.php
Normal file
56
app/Views/admin/bag_sale/create.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
|
<span class="text-sm font-bold text-gray-700">판매 등록</span>
|
||||||
|
</section>
|
||||||
|
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
||||||
|
<form action="<?= mgmt_url('bag-sales/store') ?>" method="POST" class="space-y-4">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
|
<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-60" name="bs_ds_idx" required>
|
||||||
|
<option value="">선택</option>
|
||||||
|
<?php foreach ($shops as $shop): ?>
|
||||||
|
<option value="<?= esc($shop->ds_idx) ?>" <?= (int) old('bs_ds_idx') === (int) $shop->ds_idx ? 'selected' : '' ?>>
|
||||||
|
<?= esc($shop->ds_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">봉투코드 <span class="text-red-500">*</span></label>
|
||||||
|
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bs_bag_code" required>
|
||||||
|
<option value="">선택</option>
|
||||||
|
<?php foreach ($bagCodes as $cd): ?>
|
||||||
|
<option value="<?= esc($cd->cd_code) ?>" <?= old('bs_bag_code') === $cd->cd_code ? 'selected' : '' ?>>
|
||||||
|
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_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">수량 <span class="text-red-500">*</span></label>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 text-right" name="bs_qty" type="number" min="0" value="<?= esc(old('bs_qty', '0')) ?>" required/>
|
||||||
|
</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>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bs_sale_date" type="date" value="<?= esc(old('bs_sale_date', date('Y-m-d'))) ?>" required/>
|
||||||
|
</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-44" name="bs_type" required>
|
||||||
|
<option value="">선택</option>
|
||||||
|
<option value="sale" <?= old('bs_type') === 'sale' ? 'selected' : '' ?>>판매</option>
|
||||||
|
<option value="return" <?= old('bs_type') === 'return' ? 'selected' : '' ?>>반품</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
69
app/Views/admin/bag_sale/index.php
Normal file
69
app/Views/admin/bag_sale/index.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?= 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">
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</section>
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200">
|
||||||
|
<form method="GET" action="<?= mgmt_url('bag-sales') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="text-sm text-gray-600">판매일</label>
|
||||||
|
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
|
<label class="text-sm text-gray-600">~</label>
|
||||||
|
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
|
<label class="text-sm text-gray-600">구분</label>
|
||||||
|
<select name="type" class="border border-gray-300 rounded px-2 py-1 text-sm">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="sale" <?= ($type ?? '') === 'sale' ? 'selected' : '' ?>>판매</option>
|
||||||
|
<option value="return" <?= ($type ?? '') === 'return' ? 'selected' : '' ?>>반품</option>
|
||||||
|
<option value="cancel" <?= ($type ?? '') === 'cancel' ? 'selected' : '' ?>>취소</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
|
<a href="<?= mgmt_url('bag-sales') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
|
||||||
|
</form>
|
||||||
|
</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>판매소</th>
|
||||||
|
<th>판매일</th>
|
||||||
|
<th>봉투코드</th>
|
||||||
|
<th>봉투명</th>
|
||||||
|
<th>수량</th>
|
||||||
|
<th>단가</th>
|
||||||
|
<th>금액</th>
|
||||||
|
<th class="w-20">구분</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-right">
|
||||||
|
<?php foreach ($list as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center"><?= esc($row->bs_idx) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($row->bs_ds_name) ?></td>
|
||||||
|
<td class="text-center"><?= esc($row->bs_sale_date) ?></td>
|
||||||
|
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
|
||||||
|
<td><?= number_format((int) $row->bs_qty) ?></td>
|
||||||
|
<td><?= number_format((int) $row->bs_unit_price) ?></td>
|
||||||
|
<td><?= number_format((int) $row->bs_amount) ?></td>
|
||||||
|
<td class="text-center">
|
||||||
|
<?php
|
||||||
|
$typeMap = ['sale' => '판매', 'return' => '반품', 'cancel' => '취소'];
|
||||||
|
echo esc($typeMap[$row->bs_type] ?? $row->bs_type);
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($list)): ?>
|
||||||
|
<tr><td colspan="9" class="text-center text-gray-400 py-4">등록된 판매/반품이 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||||
@@ -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">← <?= esc($kind->ck_name) ?></a>
|
<a href="<?= base_url('bag/code-details/' . (int) $kind->ck_idx) ?>" class="text-blue-600 hover:underline text-sm">← <?= 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>
|
||||||
|
|||||||
@@ -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">← <?= esc($kind->ck_name) ?></a>
|
<a href="<?= base_url('bag/code-details/' . (int) $kind->ck_idx) ?>" class="text-blue-600 hover:underline text-sm">← <?= 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>
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
<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">← 코드 종류</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>
|
|
||||||
<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>
|
|
||||||
</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>
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
<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>
|
|
||||||
<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>
|
|
||||||
</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>
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
<?= view('components/print_header', ['printTitle' => '업체 관리']) ?>
|
||||||
<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/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 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="<?= 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>
|
||||||
</section>
|
</section>
|
||||||
<div class="border border-gray-300 overflow-auto mt-2">
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
@@ -31,8 +35,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>
|
||||||
@@ -45,3 +49,4 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||||
|
|||||||
@@ -1,3 +1,127 @@
|
|||||||
<div class="border border-gray-300 p-4">
|
<?php $s = $stats ?? []; ?>
|
||||||
<p class="text-sm text-gray-600">관리자 메인 화면입니다. 상단 메뉴에서 기능을 선택하세요.</p>
|
|
||||||
|
<?php if (!($lgIdx ?? null)): ?>
|
||||||
|
<div class="border border-orange-300 bg-orange-50 p-4 text-sm text-orange-700">
|
||||||
|
작업할 지자체가 선택되지 않았습니다. 상단에서 지자체를 선택해 주세요.
|
||||||
</div>
|
</div>
|
||||||
|
<?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="border border-gray-300 p-4 bg-white">
|
||||||
|
<div class="text-xs text-gray-500">총 발주 건수</div>
|
||||||
|
<div class="text-2xl font-bold text-blue-700"><?= number_format($s['order_count'] ?? 0) ?></div>
|
||||||
|
<div class="text-xs text-gray-400 mt-1">금액: <?= number_format($s['order_amount'] ?? 0) ?>원</div>
|
||||||
|
</div>
|
||||||
|
<div class="border border-gray-300 p-4 bg-white">
|
||||||
|
<div class="text-xs text-gray-500">총 판매 건수</div>
|
||||||
|
<div class="text-2xl font-bold text-green-700"><?= number_format($s['sale_count'] ?? 0) ?></div>
|
||||||
|
<div class="text-xs text-gray-400 mt-1">금액: <?= number_format($s['sale_amount'] ?? 0) ?>원</div>
|
||||||
|
</div>
|
||||||
|
<div class="border border-gray-300 p-4 bg-white">
|
||||||
|
<div class="text-xs text-gray-500">재고 품목 수</div>
|
||||||
|
<div class="text-2xl font-bold text-purple-700"><?= number_format($s['inventory_count'] ?? 0) ?></div>
|
||||||
|
<div class="text-xs text-gray-400 mt-1">현재 재고가 있는 봉투 품목</div>
|
||||||
|
</div>
|
||||||
|
<div class="border border-gray-300 p-4 bg-white">
|
||||||
|
<div class="text-xs text-gray-500">이번 달 불출</div>
|
||||||
|
<div class="text-2xl font-bold text-orange-700"><?= number_format($s['issue_count_month'] ?? 0) ?></div>
|
||||||
|
<div class="text-xs text-gray-400 mt-1"><?= date('Y년 n월') ?> 무료용 불출</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 최근 내역 -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<!-- 최근 발주 -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between mb-1">
|
||||||
|
<h3 class="text-sm font-bold text-gray-700">최근 발주 5건</h3>
|
||||||
|
<a href="<?= base_url('bag/bag-orders') ?>" class="text-xs text-blue-600 hover:underline">전체보기</a>
|
||||||
|
</div>
|
||||||
|
<div class="border border-gray-300 overflow-auto">
|
||||||
|
<table class="w-full data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>LOT번호</th>
|
||||||
|
<th>발주일</th>
|
||||||
|
<th>상태</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-center">
|
||||||
|
<?php
|
||||||
|
$statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제'];
|
||||||
|
foreach (($s['recent_orders'] ?? []) as $order):
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td class="font-mono text-sm">
|
||||||
|
<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><?= esc($order->bo_order_date) ?></td>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
$stClass = match($order->bo_status) {
|
||||||
|
'cancelled' => 'text-red-600',
|
||||||
|
'deleted' => 'text-gray-400',
|
||||||
|
default => 'text-green-600',
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
<span class="<?= $stClass ?>"><?= esc($statusMap[$order->bo_status] ?? $order->bo_status) ?></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($s['recent_orders'])): ?>
|
||||||
|
<tr><td colspan="3" class="text-gray-400 py-3">발주 내역이 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 최근 판매 -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between mb-1">
|
||||||
|
<h3 class="text-sm font-bold text-gray-700">최근 판매 5건</h3>
|
||||||
|
<a href="<?= base_url('bag/bag-sales') ?>" class="text-xs text-blue-600 hover:underline">전체보기</a>
|
||||||
|
</div>
|
||||||
|
<div class="border border-gray-300 overflow-auto">
|
||||||
|
<table class="w-full data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>판매소</th>
|
||||||
|
<th>봉투명</th>
|
||||||
|
<th>수량</th>
|
||||||
|
<th>금액</th>
|
||||||
|
<th>구분</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-right">
|
||||||
|
<?php
|
||||||
|
$typeMap = ['sale' => '판매', 'return' => '반품', 'cancel' => '취소'];
|
||||||
|
foreach (($s['recent_sales'] ?? []) as $sale):
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-left pl-2"><?= esc($sale->bs_ds_name) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($sale->bs_bag_name) ?></td>
|
||||||
|
<td><?= number_format(abs((int) $sale->bs_qty)) ?></td>
|
||||||
|
<td><?= number_format((int) $sale->bs_amount) ?></td>
|
||||||
|
<td class="text-center"><?= esc($typeMap[$sale->bs_type] ?? $sale->bs_type) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($s['recent_sales'])): ?>
|
||||||
|
<tr><td colspan="5" class="text-center text-gray-400 py-3">판매 내역이 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -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 action="<?= mgmt_url('designated-shops/store') ?>" method="POST" class="space-y-4">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
<?php if (! empty($localGovs)): ?>
|
<?php if (! empty($localGovs)): ?>
|
||||||
@@ -95,7 +95,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-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>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ $v = fn ($key, $default = '') => old($key) !== null && old($key) !== '' ? old($k
|
|||||||
<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 action="<?= mgmt_url('designated-shops/update/' . (int) $shop->ds_idx) ?>" method="POST" class="space-y-4">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
<?php if ($currentLg !== null): ?>
|
<?php if ($currentLg !== null): ?>
|
||||||
@@ -99,7 +99,7 @@ $v = fn ($key, $default = '') => old($key) !== null && old($key) !== '' ? old($k
|
|||||||
|
|
||||||
<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>
|
||||||
|
|||||||
@@ -1,8 +1,36 @@
|
|||||||
|
<?= view('components/print_header', ['printTitle' => '지정판매소 목록']) ?>
|
||||||
<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/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 class="flex items-center gap-2">
|
||||||
|
<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>
|
||||||
|
<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/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>
|
||||||
|
</section>
|
||||||
|
<!-- P2-15: 다조건 검색 -->
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
||||||
|
<form method="GET" action="<?= mgmt_url('designated-shops') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
|
<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"/>
|
||||||
|
<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">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<?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 endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<label class="text-sm text-gray-600">상태</label>
|
||||||
|
<select name="ds_state" class="border border-gray-300 rounded px-2 py-1 text-sm">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="1" <?= ($dsState ?? '') === '1' ? 'selected' : '' ?>>정상</option>
|
||||||
|
<option value="2" <?= ($dsState ?? '') === '2' ? 'selected' : '' ?>>폐업</option>
|
||||||
|
<option value="3" <?= ($dsState ?? '') === '3' ? 'selected' : '' ?>>직권해지</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</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>
|
||||||
|
</form>
|
||||||
</section>
|
</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">
|
||||||
@@ -33,8 +61,8 @@
|
|||||||
<td class="text-center"><?= (int) $row->ds_state === 1 ? '정상' : ((int) $row->ds_state === 2 ? '폐업' : '직권해지') ?></td>
|
<td class="text-center"><?= (int) $row->ds_state === 1 ? '정상' : ((int) $row->ds_state === 2 ? '폐업' : '직권해지') ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->ds_regdate ?? '') ?></td>
|
<td class="text-left pl-2"><?= esc($row->ds_regdate ?? '') ?></td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a href="<?= base_url('admin/designated-shops/edit/' . (int) $row->ds_idx) ?>" class="text-blue-600 hover:underline text-sm">수정</a>
|
<a href="<?= mgmt_url('designated-shops/edit/' . (int) $row->ds_idx) ?>" class="text-blue-600 hover:underline text-sm">수정</a>
|
||||||
<form action="<?= base_url('admin/designated-shops/delete/' . (int) $row->ds_idx) ?>" method="POST" class="inline ml-1" onsubmit="return confirm('이 지정판매소를 삭제하시겠습니까?');">
|
<form action="<?= mgmt_url('designated-shops/delete/' . (int) $row->ds_idx) ?>" method="POST" class="inline ml-1" 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,4 +72,5 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||||
|
|
||||||
|
|||||||
56
app/Views/admin/designated_shop/map.php
Normal file
56
app/Views/admin/designated_shop/map.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?= 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>
|
||||||
|
<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>
|
||||||
|
</section>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<script src="//dapi.kakao.com/v2/maps/sdk.js?appkey=KAKAO_APP_KEY&libraries=services"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var mapContainer = document.getElementById('kakao-map');
|
||||||
|
if (typeof kakao === 'undefined' || typeof kakao.maps === 'undefined') {
|
||||||
|
mapContainer.innerHTML = '<div class="flex items-center justify-center h-full text-gray-400">카카오맵 API 키를 설정해 주세요.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mapOption = {
|
||||||
|
center: new kakao.maps.LatLng(35.8714, 128.6014), // 대구 기본 좌표
|
||||||
|
level: 8
|
||||||
|
};
|
||||||
|
var map = new kakao.maps.Map(mapContainer, mapOption);
|
||||||
|
|
||||||
|
var geocoder = new kakao.maps.services.Geocoder();
|
||||||
|
var shops = <?= json_encode(array_map(function($s) {
|
||||||
|
return ['name' => $s->ds_name, 'addr' => $s->ds_addr ?? '', 'rep' => $s->ds_rep_name ?? '', 'tel' => $s->ds_tel ?? ''];
|
||||||
|
}, $shops), JSON_UNESCAPED_UNICODE) ?>;
|
||||||
|
|
||||||
|
var bounds = new kakao.maps.LatLngBounds();
|
||||||
|
var markerCount = 0;
|
||||||
|
|
||||||
|
shops.forEach(function(shop) {
|
||||||
|
if (!shop.addr) return;
|
||||||
|
geocoder.addressSearch(shop.addr, function(result, status) {
|
||||||
|
if (status === kakao.maps.services.Status.OK) {
|
||||||
|
var coords = new kakao.maps.LatLng(result[0].y, result[0].x);
|
||||||
|
var marker = new kakao.maps.Marker({ map: map, position: coords });
|
||||||
|
var infoContent = '<div style="padding:5px;font-size:12px;min-width:150px;">' +
|
||||||
|
'<strong>' + shop.name + '</strong><br/>' +
|
||||||
|
(shop.rep ? '대표: ' + shop.rep + '<br/>' : '') +
|
||||||
|
(shop.tel ? 'TEL: ' + shop.tel + '<br/>' : '') +
|
||||||
|
'<span style="color:#888;">' + shop.addr + '</span></div>';
|
||||||
|
var infowindow = new kakao.maps.InfoWindow({ content: infoContent });
|
||||||
|
kakao.maps.event.addListener(marker, 'click', function() {
|
||||||
|
infowindow.open(map, marker);
|
||||||
|
});
|
||||||
|
bounds.extend(coords);
|
||||||
|
markerCount++;
|
||||||
|
if (markerCount > 0) map.setBounds(bounds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
80
app/Views/admin/designated_shop/status.php
Normal file
80
app/Views/admin/designated_shop/status.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?= 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="<?= 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>
|
||||||
|
|
||||||
|
<!-- 전체 현황 요약 -->
|
||||||
|
<div class="flex gap-4 mt-2 mb-2">
|
||||||
|
<div class="border border-gray-300 p-3 flex-1 text-center">
|
||||||
|
<div class="text-sm text-gray-500">활성 판매소</div>
|
||||||
|
<div class="text-2xl font-bold text-green-600"><?= number_format($totalActive) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="border border-gray-300 p-3 flex-1 text-center">
|
||||||
|
<div class="text-sm text-gray-500">비활성/취소 판매소</div>
|
||||||
|
<div class="text-2xl font-bold text-red-600"><?= number_format($totalInactive) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="border border-gray-300 p-3 flex-1 text-center">
|
||||||
|
<div class="text-sm text-gray-500">전체</div>
|
||||||
|
<div class="text-2xl font-bold text-gray-700"><?= number_format($totalActive + $totalInactive) ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
|
||||||
|
<!-- 연도별 신규등록 -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold text-gray-700 mb-1">연도별 신규등록 건수</h3>
|
||||||
|
<div class="border border-gray-300 overflow-auto">
|
||||||
|
<table class="w-full data-table">
|
||||||
|
<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-4">데이터가 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 연도별 취소/비활성 -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold text-gray-700 mb-1">연도별 취소/비활성 건수</h3>
|
||||||
|
<div class="border border-gray-300 overflow-auto">
|
||||||
|
<table class="w-full data-table">
|
||||||
|
<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-4">데이터가 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
<?= view('components/print_header', ['printTitle' => '무료용 대상자 관리']) ?>
|
||||||
<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/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 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="<?= 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>
|
||||||
</section>
|
</section>
|
||||||
<div class="border border-gray-300 overflow-auto mt-2">
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
@@ -33,8 +37,8 @@
|
|||||||
<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>
|
||||||
@@ -42,8 +46,14 @@
|
|||||||
</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="10" 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>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||||
|
|||||||
@@ -5,22 +5,24 @@ $n = $uriObj->getTotalSegments();
|
|||||||
$uri = $n >= 2 ? $uriObj->getSegment(2) : '';
|
$uri = $n >= 2 ? $uriObj->getSegment(2) : '';
|
||||||
$seg3 = $n >= 3 ? $uriObj->getSegment(3) : '';
|
$seg3 = $n >= 3 ? $uriObj->getSegment(3) : '';
|
||||||
$mbLevel = (int) session()->get('mb_level');
|
$mbLevel = (int) session()->get('mb_level');
|
||||||
$isSuperAdmin = ($mbLevel === \Config\Roles::LEVEL_SUPER_ADMIN);
|
$isSuperAdmin = \Config\Roles::isSuperAdminEquivalent($mbLevel);
|
||||||
$effectiveLgIdx = admin_effective_lg_idx();
|
$effectiveLgIdx = admin_effective_lg_idx();
|
||||||
$effectiveLgName = null;
|
$effectiveLgName = null;
|
||||||
if ($effectiveLgIdx) {
|
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&display=swap" rel="stylesheet"/>
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet"/>
|
||||||
<script>
|
<script>
|
||||||
@@ -70,37 +72,64 @@ tailwind.config = {
|
|||||||
.data-table tbody tr:hover td { background-color: #e6f7ff !important; }
|
.data-table tbody tr:hover td { background-color: #e6f7ff !important; }
|
||||||
.main-content-area { height: calc(100vh - 170px); overflow: auto; }
|
.main-content-area { height: calc(100vh - 170px); overflow: auto; }
|
||||||
body { overflow: hidden; }
|
body { overflow: hidden; }
|
||||||
|
@media print {
|
||||||
|
header, footer, .no-print, nav { display: none !important; }
|
||||||
|
.main-content-area { height: auto !important; overflow: visible !important; }
|
||||||
|
body { overflow: visible !important; }
|
||||||
|
.bg-title-bar { display: none !important; }
|
||||||
|
.bg-control-panel { break-inside: avoid; }
|
||||||
|
.print-header { display: block !important; }
|
||||||
|
}
|
||||||
</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">
|
|
||||||
<svg class="h-5 w-5" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<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"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<a href="<?= base_url('admin') ?>" class="text-base font-semibold text-gray-800 tracking-tight hover:text-blue-600">쓰레기봉투 물류시스템</a>
|
|
||||||
</div>
|
</div>
|
||||||
<nav class="hidden md:flex gap-5 text-sm font-medium text-gray-600">
|
<nav class="hidden md:flex gap-5 text-sm font-medium text-gray-600">
|
||||||
<?php if (! empty($adminNavTree)): ?>
|
<?php if (! empty($adminNavTree)): ?>
|
||||||
<?php foreach ($adminNavTree as $navItem): ?>
|
<?php foreach ($adminNavTree as $navItem): ?>
|
||||||
<?php $hasChildren = ! empty($navItem->children); ?>
|
<?php
|
||||||
|
$hasChildren = ! empty($navItem->children);
|
||||||
|
$parentLink = menu_link_preferred_href_path($navItem->mm_link ?? null, $currentPath);
|
||||||
|
$parentIsCurrent = $adminNavItemIsCurrent($navItem->mm_link ?? null);
|
||||||
|
if (! $parentIsCurrent && $hasChildren) {
|
||||||
|
foreach ($navItem->children as $ch) {
|
||||||
|
if ($adminNavItemIsCurrent($ch->mm_link ?? null)) {
|
||||||
|
$parentIsCurrent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
<div class="relative group">
|
<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' ?>"
|
<a class="<?= $parentIsCurrent ? '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) ?>">
|
href="<?= $parentLink !== '' ? base_url($parentLink) : '#' ?>">
|
||||||
<?= esc($navItem->mm_name) ?>
|
<?= esc($navItem->mm_name) ?>
|
||||||
</a>
|
</a>
|
||||||
<?php if ($hasChildren): ?>
|
<?php if ($hasChildren): ?>
|
||||||
<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">
|
<?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 = $adminNavItemIsCurrent($child->mm_link ?? null);
|
||||||
|
?>
|
||||||
|
<?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>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -115,19 +144,15 @@ body { overflow: hidden; }
|
|||||||
<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('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('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>
|
<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; ?>
|
<?php endif; ?>
|
||||||
<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('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>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
<?= view('components/header_user_tools', [
|
||||||
<div class="flex items-center gap-3">
|
'userNav' => $userNav,
|
||||||
<?php if ($effectiveLgName !== null): ?>
|
'effectiveLgName' => $effectiveLgName,
|
||||||
<span class="text-sm text-gray-600" title="현재 작업 지자체"><?= esc($effectiveLgName) ?></span>
|
'showSiteLink' => true,
|
||||||
<?php endif; ?>
|
'showAdminLink' => false,
|
||||||
<a href="<?= base_url('/') ?>" class="text-gray-500 hover:text-blue-600 text-sm">사이트</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="로그아웃">
|
|
||||||
<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> 종료
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</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 ?? '관리자') ?>
|
||||||
@@ -147,7 +172,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>
|
||||||
|
|||||||
46
app/Views/admin/local_government/edit.php
Normal file
46
app/Views/admin/local_government/edit.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
|
<span class="text-sm font-bold text-gray-700">지자체 수정</span>
|
||||||
|
</section>
|
||||||
|
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
||||||
|
<form action="<?= base_url('admin/local-governments/update/' . (int) $item->lg_idx) ?>" method="POST" class="space-y-4">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">코드</label>
|
||||||
|
<span class="text-sm font-mono font-bold"><?= esc($item->lg_code) ?></span>
|
||||||
|
</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>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="lg_name" type="text" value="<?= esc(old('lg_name', $item->lg_name)) ?>" required/>
|
||||||
|
</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>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="lg_sido" type="text" value="<?= esc(old('lg_sido', $item->lg_sido)) ?>" required/>
|
||||||
|
</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>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="lg_gugun" type="text" value="<?= esc(old('lg_gugun', $item->lg_gugun)) ?>" required/>
|
||||||
|
</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="lg_addr" type="text" value="<?= esc(old('lg_addr', $item->lg_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="lg_state" required>
|
||||||
|
<option value="1" <?= (int) old('lg_state', $item->lg_state) === 1 ? 'selected' : '' ?>>사용</option>
|
||||||
|
<option value="0" <?= (int) old('lg_state', $item->lg_state) === 0 ? 'selected' : '' ?>>미사용</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<a href="<?= base_url('admin/local-governments') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
|
<?= view('components/print_header', ['printTitle' => '지자체 목록']) ?>
|
||||||
<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">
|
||||||
|
<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/local-governments/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="<?= base_url('admin/local-governments/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>
|
||||||
</section>
|
</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">
|
||||||
@@ -15,6 +19,7 @@
|
|||||||
<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 class="text-right">
|
||||||
@@ -27,9 +32,17 @@
|
|||||||
<td class="text-left pl-2"><?= esc($row->lg_gugun) ?></td>
|
<td class="text-left pl-2"><?= esc($row->lg_gugun) ?></td>
|
||||||
<td class="text-center"><?= (int) $row->lg_state === 1 ? '사용' : '미사용' ?></td>
|
<td class="text-center"><?= (int) $row->lg_state === 1 ? '사용' : '미사용' ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->lg_regdate ?? '') ?></td>
|
<td class="text-left pl-2"><?= esc($row->lg_regdate ?? '') ?></td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="<?= base_url('admin/local-governments/edit/' . (int) $row->lg_idx) ?>" class="text-blue-600 hover:underline text-sm">수정</a>
|
||||||
|
<form action="<?= base_url('admin/local-governments/delete/' . (int) $row->lg_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>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -51,7 +51,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>
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -59,7 +59,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>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
<?= view('components/print_header', ['printTitle' => '담당자 관리']) ?>
|
||||||
<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/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 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="<?= 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>
|
||||||
</section>
|
</section>
|
||||||
<div class="border border-gray-300 overflow-auto mt-2">
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
@@ -31,8 +35,8 @@
|
|||||||
<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>
|
||||||
@@ -45,3 +49,4 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||||
|
|||||||
@@ -4,7 +4,29 @@ $list = $list ?? [];
|
|||||||
$mtIdx = (int) ($mtIdx ?? 0);
|
$mtIdx = (int) ($mtIdx ?? 0);
|
||||||
$mtCode = (string) ($mtCode ?? '');
|
$mtCode = (string) ($mtCode ?? '');
|
||||||
$levelNames = $levelNames ?? [];
|
$levelNames = $levelNames ?? [];
|
||||||
$superAdminLevel = \Config\Roles::LEVEL_SUPER_ADMIN;
|
$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">
|
||||||
@@ -24,6 +46,19 @@ $superAdminLevel = \Config\Roles::LEVEL_SUPER_ADMIN;
|
|||||||
</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>
|
||||||
@@ -49,6 +84,10 @@ $superAdminLevel = \Config\Roles::LEVEL_SUPER_ADMIN;
|
|||||||
</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>
|
||||||
@@ -68,15 +107,27 @@ $superAdminLevel = \Config\Roles::LEVEL_SUPER_ADMIN;
|
|||||||
└─
|
└─
|
||||||
<?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 === '') {
|
||||||
echo '전체';
|
echo '전체';
|
||||||
} else {
|
} else {
|
||||||
$levels = array_filter(explode(',', $row->mm_level), fn ($lv) => (int) trim($lv) !== $superAdminLevel);
|
$levels = array_filter(explode(',', $row->mm_level), fn ($lv) => ! \Config\Roles::isSuperAdminEquivalent((int) trim($lv)));
|
||||||
$labels = array_map(fn ($lv) => $levelNames[trim($lv)] ?? trim($lv), $levels);
|
$labels = array_map(fn ($lv) => $levelNames[trim($lv)] ?? trim($lv), $levels);
|
||||||
echo esc(implode(', ', $labels) ?: '전체');
|
echo esc(implode(', ', $labels) ?: '전체');
|
||||||
}
|
}
|
||||||
@@ -146,7 +197,7 @@ $superAdminLevel = \Config\Roles::LEVEL_SUPER_ADMIN;
|
|||||||
<span class="text-sm">전체</span>
|
<span class="text-sm">전체</span>
|
||||||
</label>
|
</label>
|
||||||
<?php foreach ($levelNames as $lv => $name): ?>
|
<?php foreach ($levelNames as $lv => $name): ?>
|
||||||
<?php if ((int) $lv === $superAdminLevel) { continue; } ?>
|
<?php if (\Config\Roles::isSuperAdminEquivalent((int) $lv)) { continue; } ?>
|
||||||
<label class="inline-flex items-center gap-1 mm-level-label">
|
<label class="inline-flex items-center gap-1 mm-level-label">
|
||||||
<input type="checkbox" name="mm_level[]" value="<?= (int) $lv ?>" class="mm-level-cb"/>
|
<input type="checkbox" name="mm_level[]" value="<?= (int) $lv ?>" class="mm-level-cb"/>
|
||||||
<span class="text-sm"><?= esc($name) ?></span>
|
<span class="text-sm"><?= esc($name) ?></span>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -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>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user