Compare commits
10 Commits
da132f0e51
...
d36217920f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d36217920f | ||
|
|
0a982aae96 | ||
|
|
65d8076721 | ||
|
|
a0103eb95d | ||
|
|
466f6fe085 | ||
|
|
2e3b43554c | ||
|
|
f451f0ff3b | ||
|
|
6e8bd84182 | ||
|
|
d9d3ef46c1 | ||
|
|
c2840a9e34 |
738
README.md
@@ -1,8 +1,8 @@
|
||||
# 종량제 — 쓰레기봉투 물류시스템 (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/)** 기반입니다.
|
||||
|
||||
**저장소:** [wixon-associates/jongryangje](https://github.com/wixon-associates/jongryangje)
|
||||
@@ -18,77 +18,9 @@
|
||||
| Language | PHP 8.2+ (strict types) |
|
||||
| Database | MySQL / MariaDB (MySQLi) |
|
||||
| 의존성 관리 | Composer 2.x |
|
||||
| 테스트 | PHPUnit 10.x |
|
||||
| E2E 테스트 | Playwright (Chromium) |
|
||||
| 세션 | 파일 기반 (`writable/session/`) |
|
||||
|
||||
---
|
||||
|
||||
## 요구 사항
|
||||
|
||||
- **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
|
||||
```
|
||||
| 프론트엔드 | Tailwind CSS (CDN), Vanilla JS |
|
||||
|
||||
---
|
||||
|
||||
@@ -96,21 +28,119 @@ vendor/bin/phpunit
|
||||
|
||||
```
|
||||
app/
|
||||
├── Config/ # Routes, Database, Roles, Filters, Session 등
|
||||
├── Controllers/ # Home, Auth, Admin/* (8개 관리자 컨트롤러)
|
||||
├── Models/ # 7개 모델 (Member, LocalGovernment, DesignatedShop 등)
|
||||
├── Views/ # admin/, auth/, bag/, home/ 템플릿
|
||||
├── Config/ # Routes, Database, Roles, Filters, Session 등 (45개)
|
||||
├── Controllers/ # 28개 컨트롤러
|
||||
│ ├── Auth.php # 로그인/로그아웃/회원가입
|
||||
│ ├── Bag.php # 사이트 메뉴 페이지 (10개 메뉴)
|
||||
│ ├── 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 (관리자 접근 제어)
|
||||
├── Helpers/ # admin_helper, pii_encryption_helper
|
||||
└── Database/ # Migrations, Seeds
|
||||
public/ # 웹 루트
|
||||
writable/database/ # SQL 초기화 스크립트
|
||||
tests/ # unit/, database/, session/
|
||||
writable/database/ # SQL 초기화/시드 스크립트 (21개)
|
||||
e2e/ # Playwright E2E 테스트 (84개 테스트)
|
||||
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)
|
||||
|
||||
| Level | 역할 | 설명 |
|
||||
@@ -120,295 +150,329 @@ assets/ # 기획 문서 (엑셀)
|
||||
| 2 | 지정판매소 | 봉투 판매/재고 관리 |
|
||||
| 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 + 지자체 선택 여부 검증
|
||||
|
||||
## 멀티테넌시
|
||||
|
||||
- `local_government.lg_idx` 가 테넌트 루트
|
||||
- 관리자 필터에서 `session('admin_lg_idx')` 기반 테넌트 분리
|
||||
- `local_government.lg_idx`가 테넌트 루트
|
||||
- 관리자 필터에서 `admin_effective_lg_idx()` 기반 테넌트 분리
|
||||
- Super Admin은 `/admin/select-local-government`에서 작업 지자체 선택
|
||||
- 지자체관리자는 소속 `mb_lg_idx` 자동 적용
|
||||
|
||||
---
|
||||
|
||||
## 주요 URL
|
||||
## 라우트 구조
|
||||
|
||||
| 경로 | 설명 | 인증 |
|
||||
### 공개 페이지
|
||||
|
||||
| 경로 | 설명 |
|
||||
|------|------|
|
||||
| `/` | 홈 (비로그인: 환영, 로그인: 대시보드) |
|
||||
| `/login`, `/logout` | 로그인/로그아웃 |
|
||||
| `/register` | 회원가입 (역할 승인 플로우) |
|
||||
| `/dashboard/*` | 대시보드 시안 (classic/modern/dense/charts) |
|
||||
|
||||
### 사이트 메뉴 (`/bag/*`)
|
||||
|
||||
| 경로 | 설명 | 기능 |
|
||||
|------|------|------|
|
||||
| `/` | 홈 (비로그인 시 환영 화면) | 공개 |
|
||||
| `/login`, `/logout` | 로그인·로그아웃 | 공개 |
|
||||
| `/register` | 회원가입 (역할 승인 플로우 연동) | 공개 |
|
||||
| `/dashboard` | 로그인 후 메인 대시보드 | 인증 |
|
||||
| `/dashboard/classic-mock` 등 | UI 시안용 라우트 | 인증 |
|
||||
| `/bag/*` | 봉투 관련 페이지 (목업) | 인증 |
|
||||
| `/admin` | 관리자 대시보드 | 관리자 (Lv.3+) |
|
||||
| `/admin/users` | 회원 관리 (CRUD) | 관리자 |
|
||||
| `/admin/access/login-history` | 로그인 이력 조회 | 관리자 |
|
||||
| `/admin/access/approvals` | 회원가입 역할 승인 대기 처리 | 관리자 |
|
||||
| `/admin/roles` | 역할 목록 조회 | 관리자 |
|
||||
| `/admin/menus` | 메뉴 관리 (트리 구조 CRUD) | 관리자 |
|
||||
| `/admin/local-governments` | 지자체 관리 | 관리자 |
|
||||
| `/admin/designated-shops` | 지정판매소 관리 (CRUD) | 관리자 |
|
||||
| `/admin/select-local-government` | 작업 지자체 선택 | Super Admin |
|
||||
| `/bag/basic-info` | 기본정보관리 | 코드/단가/포장단위 조회 + 관리 링크 |
|
||||
| `/bag/purchase-inbound` | 발주 입고 관리 | 발주/입고 목록 + 등록 버튼 |
|
||||
| `/bag/issue` | 불출 관리 | 불출 목록 + 처리/취소 |
|
||||
| `/bag/inventory` | 재고 관리 | 봉투별 현재 재고 조회 |
|
||||
| `/bag/sales` | 판매 관리 | 주문/판매/반품 + 등록 |
|
||||
| `/bag/sales-stats` | 판매 현황 | 기간별 판매 데이터 |
|
||||
| `/bag/flow` | 봉투 수불 관리 | 봉투코드별 입출고 수불 요약 |
|
||||
| `/bag/analytics` | 통계 분석 관리 | Phase 6 예정 |
|
||||
| `/bag/window` | 창 | Phase 6 예정 |
|
||||
| `/bag/help` | 도움말 | 시스템 안내 |
|
||||
|
||||
정확한 라우트는 `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) |
|
||||
| `/admin/password-change` | 비밀번호 변경 |
|
||||
| `/admin/designated-shops/*` | 지정판매소 관리 (CRUD) |
|
||||
|
||||
**기본정보관리 (Phase 2)**
|
||||
|
||||
| 경로 | 기능 |
|
||||
|------|------|
|
||||
| `/admin/code-kinds/*` | 기본코드 종류 (CRUD) |
|
||||
| `/admin/code-details/*` | 세부코드 (CRUD) |
|
||||
| `/admin/bag-prices/*` | 봉투 단가 (CRUD + 이력) |
|
||||
| `/admin/packaging-units/*` | 포장 단위 (CRUD + 이력) |
|
||||
| `/admin/sales-agencies/*` | 판매 대행소 (CRUD) |
|
||||
| `/admin/managers/*` | 담당자 (CRUD) |
|
||||
| `/admin/companies/*` | 업체 (CRUD) |
|
||||
| `/admin/free-recipients/*` | 무료용 대상자 (CRUD) |
|
||||
|
||||
**발주/입고/재고 (Phase 3)**
|
||||
|
||||
| 경로 | 기능 |
|
||||
|------|------|
|
||||
| `/admin/bag-orders/*` | 발주 관리 (등록/상세/취소/삭제) |
|
||||
| `/admin/bag-receivings/*` | 입고 관리 (등록, 재고 자동 반영) |
|
||||
| `/admin/bag-inventory` | 재고 현황 조회 |
|
||||
|
||||
**판매/주문/불출 (Phase 4)**
|
||||
|
||||
| 경로 | 기능 |
|
||||
|------|------|
|
||||
| `/admin/shop-orders/*` | 주문 접수 (등록/취소) |
|
||||
| `/admin/bag-sales/*` | 판매/반품 (등록) |
|
||||
| `/admin/bag-issues/*` | 무료용 불출 (등록/취소, 재고 연동) |
|
||||
|
||||
**리포트 (Phase 5)**
|
||||
|
||||
| 경로 | 기능 |
|
||||
|------|------|
|
||||
| `/admin/reports/sales-ledger` | 판매 대장 (일자별/기간별) |
|
||||
| `/admin/reports/daily-summary` | 일계표 (일계 + 월간 누계) |
|
||||
| `/admin/reports/period-sales` | 기간별 판매현황 |
|
||||
| `/admin/reports/supply-demand` | 봉투 수불 현황 |
|
||||
|
||||
---
|
||||
|
||||
## DB 테이블
|
||||
## 모델 (25개)
|
||||
|
||||
| 테이블 | 용도 |
|
||||
|--------|------|
|
||||
| `member` | 회원 (mb_id, mb_level, mb_state, PII 암호화) |
|
||||
| `member_log` | 로그인/로그아웃 감사 로그 (IP, User-Agent) |
|
||||
| `member_approval_request` | 회원가입 승인 요청 (pending/approved/rejected) |
|
||||
| `local_government` | 지자체 (테넌트 루트) |
|
||||
| `designated_shop` | 지정판매소 (지자체별 관리) |
|
||||
| `menu_type` | 메뉴 유형 (admin, site) |
|
||||
| `menu` | 메뉴 항목 (트리 구조, 역할별 노출) |
|
||||
| 모델 | 테이블 | 용도 |
|
||||
|------|--------|------|
|
||||
| MemberModel | member | 회원 계정 |
|
||||
| MemberLogModel | member_log | 로그인 이력 |
|
||||
| MemberApprovalRequestModel | member_approval_request | 승인 요청 |
|
||||
| LocalGovernmentModel | local_government | 지자체 |
|
||||
| DesignatedShopModel | designated_shop | 지정판매소 |
|
||||
| MenuModel | 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로 관리자 접근 제어 |
|
||||
| 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` | 실제형 시범 데이터 |
|
||||
|
||||
### 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_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-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종의 코드 체계:
|
||||
A~T 총 20종의 코드 체계 (`code_kind` + `code_detail`):
|
||||
|
||||
| 코드 | 코드명 | 코드 | 코드명 |
|
||||
|------|--------|------|--------|
|
||||
| A | 도/특별시/광역시 구분 | N | 동판종류 |
|
||||
| B | 특별시/광역시/시/군코드 | O | 봉투명 |
|
||||
| C | 구코드 | P | 작업권한 |
|
||||
| D | 동코드 | Q | 예산과목 |
|
||||
| E | 봉투구분 (일반/공공/무료/공동주택/재사용/음식물) | R | 은행목록 |
|
||||
| F | 봉투재질 (고밀도/PP마대/스티커/용기) | S | 소속 |
|
||||
| G | 용량별 (2L~120L, 1000원~10000원) | T | 직위 |
|
||||
| H | 무상지급 대상 | U | 배달 |
|
||||
| I | 판매형태 (무상/일반/관내/교환) | V | 구역 |
|
||||
| J | 반품형태 | W | 봉투명(약어) |
|
||||
| K | 반품사유 | X | 봉투구분(대분) |
|
||||
| L | 지정판매소 변경사유 | Y | 분기 |
|
||||
| M | 수불구분 | | |
|
||||
| A | 도/특별시/광역시 구분 | K | 반품사유 |
|
||||
| B | 특별시/광역시/시/군코드 | L | 지정판매소 변경사유 |
|
||||
| C | 구코드 | M | 수불구분 |
|
||||
| D | 동코드 | N | 동판종류 |
|
||||
| E | 봉투구분 | O | 봉투명 (상세 봉투코드) |
|
||||
| F | 봉투재질 | P | 작업권한 |
|
||||
| G | 용량별 | Q | 예산과목 |
|
||||
| H | 무상지급 대상 | R | 은행목록 |
|
||||
| I | 판매형태 | S | 소속 |
|
||||
| J | 반품형태 | T | 직위 |
|
||||
|
||||
---
|
||||
|
||||
## 개발 현황
|
||||
## 개발 진행 현황
|
||||
|
||||
### 웹 기능목록 (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 기록 |
|
||||
| 2 | 공통 | 개인정보 비식별화 | **완료** | `pii_encryption_helper` (ENC: prefix, AES 암호화) |
|
||||
| 3 | 공통 | 로그인 | **부분** | 로그인/세션/역할별 리다이렉트 완료. 2차 인증 미구현, 5회 실패 lock 미구현 |
|
||||
| 4 | 관리자단 | 사용자 권한 관리 | **완료** | 4단계 RBAC, Config 기반 |
|
||||
| 5 | 관리자단 | 사용자 관리 | **완료** | Full CRUD + soft delete + PII 암복호화 |
|
||||
| 6 | 관리자단 | 사용자 로그인 이력 확인 | **완료** | 기간 지정 조회 구현 |
|
||||
| 7 | 관리자단 | 사용자 권한 승인 | **완료** | 승인/거절 + 사유 입력 + 트랜잭션 처리 |
|
||||
| 8 | 관리자단 | 메뉴 관리 | **완료** | 트리 구조 CRUD, 지자체별 메뉴 복사 |
|
||||
| 9 | 관리자단 | 메뉴 별 권한 설정 | **완료** | `mm_level` 필드로 역할별 노출 제어 |
|
||||
| 25 | 기본정보관리 | 지정판매소 관리 (리스트/상세) | **완료** | 지자체별 필터링, 판매소 정보 표시 |
|
||||
| 26 | 기본정보관리 | 지정판매소 등록/수정/삭제 | **완료** | Full CRUD, 판매소번호 자동생성 |
|
||||
| 29 | 기본정보관리 | PASSWORD 변경 | **미구현** | |
|
||||
|
||||
#### 미구현 — 기본정보관리 (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 수불
|
||||
통계 분석 관리 ─ 전년대비, 월별/계절별 추이 분석
|
||||
```
|
||||
- 지정판매소 다조건 조회 + 엑셀 + 인쇄 + 바코드 출력
|
||||
- 지정판매소 지도 표시 / 현황 (신규/취소)
|
||||
- 카카오 주소 검색 API 연동
|
||||
- 년 판매 현황 (월별/분기별)
|
||||
- 지정판매소별 판매현황
|
||||
- 홈택스 세금계산서 엑셀 생성
|
||||
- 반품/파기 현황, LOT 수불 조회
|
||||
- 바코드 스캐너 연동 (Electron + serialport)
|
||||
- 실사 선별/등록/조회
|
||||
- 페이지네이션/엑셀/인쇄 공통 컴포넌트
|
||||
- CRUD 로깅 (전체 데이터 변경 이력)
|
||||
- 2차 인증 적용
|
||||
- 대시보드 실 데이터 연동
|
||||
- 모바일앱 (15개 기능)
|
||||
|
||||
---
|
||||
|
||||
## 개발 진행 요약
|
||||
## SQL 스크립트 목록 (writable/database/)
|
||||
|
||||
| 구분 | 항목 수 | 완료 | 부분완료 | 미구현 |
|
||||
|------|---------|------|---------|--------|
|
||||
| 공통 | 3 | 2 | 1 | 0 |
|
||||
| 관리자단 | 6 | 6 | 0 | 0 |
|
||||
| 기본정보관리 | 20 | 2 | 0 | 18 |
|
||||
| 발주 입고 관리 | 9 | 0 | 0 | 9 |
|
||||
| 불출 관리 | 3 | 0 | 0 | 3 |
|
||||
| 재고/실사 관리 | 3 | 0 | 0 | 3 |
|
||||
| 주문/판매 관리 | 7 | 0 | 0 | 7 |
|
||||
| 판매 현황 | 6 | 0 | 0 | 6 |
|
||||
| 봉투 수불 관리 | 5 | 0 | 0 | 5 |
|
||||
| 봉투 스캔 | 1 | 0 | 0 | 1 |
|
||||
| **웹 합계** | **63** | **10** | **1** | **52** |
|
||||
| 모바일앱 | 15 | 0 | 0 | 15 |
|
||||
| **전체 합계** | **78** | **10** | **1** | **67** |
|
||||
|
||||
> **현재 진척율: 약 13% (웹 기준 약 16%)**
|
||||
|
||||
---
|
||||
|
||||
## 향후 개발 로드맵
|
||||
|
||||
### Phase 2 — 기본정보관리 완성
|
||||
|
||||
- [ ] 기본코드 종류/세부코드 관리 (CRUD)
|
||||
- [ ] 지자체별 봉투 단가 관리 + 이력 관리
|
||||
- [ ] 포장 단위 관리 (박스/팩/낱장 체계)
|
||||
- [ ] 판매 대행소 관리 + 지자체 연결
|
||||
- [ ] 담당자 관리, 업체 관리 (협회/제작업체/회수업체)
|
||||
- [ ] 무료용 대상자 관리
|
||||
- [ ] 지정판매소 다조건 조회 + 엑셀 저장 + 인쇄 + 바코드 출력
|
||||
- [ ] 지정판매소 현황 (신규/취소) + 지도 표시
|
||||
- [ ] 지자체 수정/삭제 기능
|
||||
- [ ] 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 로깅 (전체 데이터 변경 이력)
|
||||
- [ ] 대시보드 실 데이터 연동
|
||||
| 파일 | 용도 |
|
||||
|------|------|
|
||||
| `init_jongryangje_dev.sql` | DB/사용자 생성 |
|
||||
| `login_tables.sql` | member, member_log, local_government, designated_shop |
|
||||
| `member_approval_request_add.sql` | 승인 요청 테이블 |
|
||||
| `member_login_lock_add.sql` | 로그인 실패 잠금 컬럼 |
|
||||
| `menu_tables.sql` | menu_type, menu + admin/site 시드 |
|
||||
| `menu_type_add_site.sql` | 사이트 메뉴 타입 추가 |
|
||||
| `menu_add_lg_idx.sql` | 메뉴에 지자체 컬럼 추가 |
|
||||
| `menu_site_seed_from_csv.sql` | 사이트 네비게이션 시드 |
|
||||
| `local_government_init_daegu.sql` | 대구 8개 구군 지자체 |
|
||||
| `code_master_init_daegu.sql` | 기본코드 20종 + 세부코드 |
|
||||
| `bag_price_tables.sql` | bag_price, bag_price_history |
|
||||
| `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 |
|
||||
| `sales_tables.sql` | bag_sale, bag_receiving, bag_inventory, bag_issue, shop_order, shop_order_item |
|
||||
| `seed_test_accounts.sql` | 테스터 계정 4개 |
|
||||
| `seed_realistic_data.sql` | 실제형 시범 데이터 (대구 남구청 기준) |
|
||||
| `fix_double_encoding.sql` | UTF-8 이중인코딩 수정 |
|
||||
|
||||
@@ -14,6 +14,18 @@ $routes->get('dashboard/charts', 'Home::dashboardCharts');
|
||||
$routes->get('bag/inventory-inquiry', 'Home::inventoryInquiry');
|
||||
$routes->get('bag/waste-suibal-enterprise', 'Home::wasteSuibalEnterprise');
|
||||
|
||||
// 사이트 메뉴 (/bag/*)
|
||||
$routes->get('bag/basic-info', 'Bag::basicInfo');
|
||||
$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');
|
||||
|
||||
// Auth
|
||||
$routes->get('login', 'Auth::showLoginForm');
|
||||
$routes->post('login', 'Auth::login');
|
||||
@@ -48,6 +60,13 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo
|
||||
$routes->get('local-governments', 'Admin\LocalGovernment::index');
|
||||
$routes->get('local-governments/create', 'Admin\LocalGovernment::create');
|
||||
$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-20)
|
||||
$routes->get('password-change', 'Admin\PasswordChange::index');
|
||||
$routes->post('password-change', 'Admin\PasswordChange::update');
|
||||
|
||||
// 기본코드 종류 관리 (P2-01)
|
||||
$routes->get('code-kinds', 'Admin\CodeKind::index');
|
||||
@@ -74,6 +93,39 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo
|
||||
$routes->post('bag-prices/delete/(:num)', 'Admin\BagPrice::delete/$1');
|
||||
$routes->get('bag-prices/history/(:num)', 'Admin\BagPrice::history/$1');
|
||||
|
||||
// 발주 관리 (P3-01~05)
|
||||
$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');
|
||||
|
||||
// 입고 관리 (P3-06~09)
|
||||
$routes->get('bag-receivings', 'Admin\BagReceiving::index');
|
||||
$routes->get('bag-receivings/create', 'Admin\BagReceiving::create');
|
||||
$routes->post('bag-receivings/store', 'Admin\BagReceiving::store');
|
||||
|
||||
// 재고 현황 (P3-10)
|
||||
$routes->get('bag-inventory', 'Admin\BagInventory::index');
|
||||
|
||||
// 주문 접수 관리 (P4-01~03)
|
||||
$routes->get('shop-orders', 'Admin\ShopOrder::index');
|
||||
$routes->get('shop-orders/create', 'Admin\ShopOrder::create');
|
||||
$routes->post('shop-orders/store', 'Admin\ShopOrder::store');
|
||||
$routes->post('shop-orders/cancel/(:num)', 'Admin\ShopOrder::cancel/$1');
|
||||
|
||||
// 판매/반품 관리 (P4-04~07)
|
||||
$routes->get('bag-sales', 'Admin\BagSale::index');
|
||||
$routes->get('bag-sales/create', 'Admin\BagSale::create');
|
||||
$routes->post('bag-sales/store', 'Admin\BagSale::store');
|
||||
|
||||
// 무료용 불출 관리 (P4-08~10)
|
||||
$routes->get('bag-issues', 'Admin\BagIssue::index');
|
||||
$routes->get('bag-issues/create', 'Admin\BagIssue::create');
|
||||
$routes->post('bag-issues/store', 'Admin\BagIssue::store');
|
||||
$routes->post('bag-issues/cancel/(:num)', 'Admin\BagIssue::cancel/$1');
|
||||
|
||||
// 포장 단위 관리 (P2-05/06)
|
||||
$routes->get('packaging-units', 'Admin\PackagingUnit::index');
|
||||
$routes->get('packaging-units/create', 'Admin\PackagingUnit::create');
|
||||
@@ -83,6 +135,12 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo
|
||||
$routes->post('packaging-units/delete/(:num)', 'Admin\PackagingUnit::delete/$1');
|
||||
$routes->get('packaging-units/history/(:num)', 'Admin\PackagingUnit::history/$1');
|
||||
|
||||
// 현황/리포트 (Phase 5)
|
||||
$routes->get('reports/sales-ledger', 'Admin\SalesReport::salesLedger');
|
||||
$routes->get('reports/daily-summary', 'Admin\SalesReport::dailySummary');
|
||||
$routes->get('reports/period-sales', 'Admin\SalesReport::periodSales');
|
||||
$routes->get('reports/supply-demand', 'Admin\SalesReport::supplyDemand');
|
||||
|
||||
// 판매 대행소 관리 (P2-07/08)
|
||||
$routes->get('sales-agencies', 'Admin\SalesAgency::index');
|
||||
$routes->get('sales-agencies/create', 'Admin\SalesAgency::create');
|
||||
|
||||
23
app/Controllers/Admin/BagInventory.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
|
||||
$list = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '재고 현황',
|
||||
'content' => view('admin/bag_inventory/index', ['list' => $list]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
122
app/Controllers/Admin/BagIssue.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?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(site_url('admin'))->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')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '무료용 불출 관리',
|
||||
'content' => view('admin/bag_issue/index', compact('list', 'startDate', 'endDate')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
helper('admin');
|
||||
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true) : [];
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '무료용 불출 처리',
|
||||
'content' => view('admin/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)->where('cd_ck_idx', $kindO->ck_idx)->where('cd_code', $bagCode)->first() : null;
|
||||
$bagName = $detail ? $detail->cd_name : '';
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$db->transStart();
|
||||
|
||||
$this->issueModel->insert([
|
||||
'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'),
|
||||
]);
|
||||
|
||||
// 재고 감산
|
||||
model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $bagName, -$qty);
|
||||
|
||||
$db->transComplete();
|
||||
|
||||
return redirect()->to(site_url('admin/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(site_url('admin/bag-issues'))->with('error', '불출 내역을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$db->transStart();
|
||||
|
||||
$this->issueModel->update($id, ['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(site_url('admin/bag-issues'))->with('success', '불출이 취소되었습니다.');
|
||||
}
|
||||
}
|
||||
221
app/Controllers/Admin/BagOrder.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<?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;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
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(site_url('admin'))->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();
|
||||
|
||||
// 발주별 품목 합계
|
||||
$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)->findAll() as $a) $agencyMap[$a->sa_idx] = $a->sa_name;
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '발주 현황',
|
||||
'content' => view('admin/bag_order/index', compact('list', 'itemSummary', 'companyMap', 'agencyMap', 'startDate', 'endDate', 'status')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) return redirect()->to(site_url('admin/bag-orders'))->with('error', '지자체를 선택해 주세요.');
|
||||
|
||||
// 봉투 종류 + 단가 + 포장단위
|
||||
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true) : [];
|
||||
$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)->where('sa_state', 1)->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '발주 등록',
|
||||
'content' => view('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();
|
||||
|
||||
// 품목 저장
|
||||
$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)->where('cd_ck_idx', $kindO->ck_idx)->where('cd_code', $code)->first() : 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(site_url('admin/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(site_url('admin/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);
|
||||
$agencyName = $a ? $a->sa_name : '';
|
||||
}
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '발주 상세 — ' . $order->bo_lot_no,
|
||||
'content' => view('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(site_url('admin/bag-orders'))->with('error', '발주를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
$this->orderModel->update($id, ['bo_status' => 'cancelled', 'bo_moddate' => date('Y-m-d H:i:s')]);
|
||||
return redirect()->to(site_url('admin/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(site_url('admin/bag-orders'))->with('error', '발주를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
$this->orderModel->update($id, ['bo_status' => 'deleted', 'bo_moddate' => date('Y-m-d H:i:s')]);
|
||||
return redirect()->to(site_url('admin/bag-orders'))->with('success', '발주가 삭제 처리되었습니다.');
|
||||
}
|
||||
}
|
||||
109
app/Controllers/Admin/BagReceiving.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?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(site_url('admin'))->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')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '입고 현황',
|
||||
'content' => view('admin/bag_receiving/index', compact('list', 'startDate', 'endDate')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) return redirect()->to(site_url('admin/bag-receivings'))->with('error', '지자체를 선택해 주세요.');
|
||||
|
||||
// 미입고 발주 목록
|
||||
$orders = model(BagOrderModel::class)->where('bo_lg_idx', $lgIdx)->where('bo_status', 'normal')->orderBy('bo_order_date', 'DESC')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '입고 처리',
|
||||
'content' => view('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)->where('cd_ck_idx', $kindO->ck_idx)->where('cd_code', $bagCode)->first() : 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(site_url('admin/bag-receivings'))->with('success', '입고 처리되었습니다. (' . $bagName . ' ' . $qtyBox . '박스)');
|
||||
}
|
||||
}
|
||||
114
app/Controllers/Admin/BagSale.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?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(site_url('admin'))->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();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '판매/반품 관리',
|
||||
'content' => view('admin/bag_sale/index', compact('list', 'startDate', 'endDate', 'type')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) return redirect()->to(site_url('admin/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) : [];
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '판매 등록',
|
||||
'content' => view('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)->where('cd_ck_idx', $kindO->ck_idx)->where('cd_code', $bagCode)->first() : 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();
|
||||
|
||||
$this->saleModel->insert([
|
||||
'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'),
|
||||
]);
|
||||
|
||||
// 재고 감산(판매) / 가산(반품)
|
||||
model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $detail ? $detail->cd_name : '', -$actualQty);
|
||||
|
||||
$db->transComplete();
|
||||
|
||||
$msg = ($type === 'sale') ? '판매 처리되었습니다.' : '반품 처리되었습니다.';
|
||||
return redirect()->to(site_url('admin/bag-sales'))->with('success', $msg);
|
||||
}
|
||||
}
|
||||
@@ -95,5 +95,89 @@ class LocalGovernment extends BaseController
|
||||
return redirect()->to(site_url('admin/local-governments'))
|
||||
->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', '지자체가 비활성화되었습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
{
|
||||
return view('admin/layout', [
|
||||
'title' => '비밀번호 변경',
|
||||
'content' => view('admin/password_change/index'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$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(site_url('admin/password-change'))->with('success', '비밀번호가 변경되었습니다.');
|
||||
}
|
||||
}
|
||||
180
app/Controllers/Admin/SalesReport.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?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(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
|
||||
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
||||
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
||||
$mode = $this->request->getGet('mode') ?? 'daily'; // daily or period
|
||||
|
||||
$saleModel = model(BagSaleModel::class);
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
if ($mode === 'daily') {
|
||||
$result = $db->query("
|
||||
SELECT bs_sale_date, bs_ds_name, bs_bag_code, bs_bag_name, bs_type,
|
||||
SUM(ABS(bs_qty)) as total_qty,
|
||||
SUM(bs_amount) as total_amount
|
||||
FROM bag_sale
|
||||
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ? AND bs_type IN('sale','return')
|
||||
GROUP BY bs_sale_date, bs_ds_name, bs_bag_code, bs_bag_name, bs_type
|
||||
ORDER BY bs_sale_date DESC, bs_ds_name, bs_bag_code
|
||||
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||
} else {
|
||||
$result = $db->query("
|
||||
SELECT bs_ds_name, bs_bag_code, bs_bag_name,
|
||||
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
||||
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount,
|
||||
SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty,
|
||||
SUM(CASE WHEN bs_type='return' THEN bs_amount ELSE 0 END) as return_amount
|
||||
FROM bag_sale
|
||||
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
|
||||
GROUP BY bs_ds_name, bs_bag_code, bs_bag_name
|
||||
ORDER BY bs_ds_name, bs_bag_code
|
||||
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||
}
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '판매 대장',
|
||||
'content' => view('admin/sales_report/sales_ledger', compact('result', 'startDate', 'endDate', 'mode')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* P5-02: 일계표
|
||||
*/
|
||||
public function dailySummary()
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
|
||||
$date = $this->request->getGet('date') ?? date('Y-m-d');
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
// 당일 판매
|
||||
$daily = $db->query("
|
||||
SELECT bs_bag_code, bs_bag_name,
|
||||
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
||||
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount
|
||||
FROM bag_sale
|
||||
WHERE bs_lg_idx = ? AND bs_sale_date = ?
|
||||
GROUP BY bs_bag_code, bs_bag_name
|
||||
ORDER BY bs_bag_code
|
||||
", [$lgIdx, $date])->getResult();
|
||||
|
||||
// 당월 누계
|
||||
$monthStart = date('Y-m-01', strtotime($date));
|
||||
$monthly = $db->query("
|
||||
SELECT bs_bag_code, bs_bag_name,
|
||||
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
||||
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount
|
||||
FROM bag_sale
|
||||
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
|
||||
GROUP BY bs_bag_code, bs_bag_name
|
||||
ORDER BY bs_bag_code
|
||||
", [$lgIdx, $monthStart, $date])->getResult();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '일계표',
|
||||
'content' => view('admin/sales_report/daily_summary', compact('daily', 'monthly', 'date')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* P5-03: 기간별 판매현황
|
||||
*/
|
||||
public function periodSales()
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
|
||||
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
||||
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$result = $db->query("
|
||||
SELECT bs_bag_code, bs_bag_name,
|
||||
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
||||
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount,
|
||||
SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty,
|
||||
SUM(CASE WHEN bs_type='return' THEN bs_amount ELSE 0 END) as return_amount
|
||||
FROM bag_sale
|
||||
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
|
||||
GROUP BY bs_bag_code, bs_bag_name
|
||||
ORDER BY bs_bag_code
|
||||
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '기간별 판매현황',
|
||||
'content' => view('admin/sales_report/period_sales', compact('result', 'startDate', 'endDate')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* P5-07: 봉투 수불 현황
|
||||
*/
|
||||
public function supplyDemand()
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
||||
|
||||
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
||||
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
// 입고 합계
|
||||
$receiving = $db->query("
|
||||
SELECT br_bag_code, br_bag_name,
|
||||
SUM(br_qty_sheet) as recv_qty
|
||||
FROM bag_receiving
|
||||
WHERE br_lg_idx = ? AND br_receive_date BETWEEN ? AND ?
|
||||
GROUP BY br_bag_code, br_bag_name
|
||||
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||
|
||||
// 판매 합계
|
||||
$sales = $db->query("
|
||||
SELECT bs_bag_code, bs_bag_name,
|
||||
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
||||
SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty
|
||||
FROM bag_sale
|
||||
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
|
||||
GROUP BY bs_bag_code, bs_bag_name
|
||||
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||
|
||||
// 불출 합계
|
||||
$issues = $db->query("
|
||||
SELECT bi2_bag_code, bi2_bag_name,
|
||||
SUM(bi2_qty) as issue_qty
|
||||
FROM bag_issue
|
||||
WHERE bi2_lg_idx = ? AND bi2_issue_date BETWEEN ? AND ? AND bi2_status = 'normal'
|
||||
GROUP BY bi2_bag_code, bi2_bag_name
|
||||
", [$lgIdx, $startDate, $endDate])->getResult();
|
||||
|
||||
// 현재 재고
|
||||
$inventory = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '봉투 수불 현황',
|
||||
'content' => view('admin/sales_report/supply_demand', compact('receiving', 'sales', 'issues', 'inventory', 'startDate', 'endDate')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
153
app/Controllers/Admin/ShopOrder.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?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(site_url('admin'))->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')->findAll();
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '주문 접수 관리',
|
||||
'content' => view('admin/shop_order/index', compact('list', 'startDate', 'endDate')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (!$lgIdx) return redirect()->to(site_url('admin/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) : [];
|
||||
|
||||
return view('admin/layout', [
|
||||
'title' => '주문 접수',
|
||||
'content' => view('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)->where('cd_ck_idx', $kindO->ck_idx)->where('cd_code', $code)->first() : 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(site_url('admin/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(site_url('admin/shop-orders'))->with('error', '주문을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
$this->orderModel->update($id, ['so_status' => 'cancelled']);
|
||||
return redirect()->to(site_url('admin/shop-orders'))->with('success', '주문이 취소되었습니다.');
|
||||
}
|
||||
}
|
||||
@@ -77,11 +77,33 @@ class Auth extends BaseController
|
||||
->with('error', '정지된 회원입니다.');
|
||||
}
|
||||
|
||||
if (! password_verify($password, $member->mb_passwd)) {
|
||||
$this->insertMemberLog($logData, false, '비밀번호 불일치');
|
||||
// P2-21: 로그인 잠금 체크 (5회 실패 시 30분 lock)
|
||||
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()
|
||||
->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);
|
||||
}
|
||||
|
||||
// 승인 요청 상태 확인(공개 회원가입 사용자)
|
||||
@@ -114,6 +136,8 @@ class Auth extends BaseController
|
||||
|
||||
$memberModel->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, '로그인 성공', $member->mb_idx);
|
||||
|
||||
250
app/Controllers/Bag.php
Normal file
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
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;
|
||||
|
||||
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
|
||||
{
|
||||
$lgIdx = $this->lgIdx();
|
||||
$data = [];
|
||||
|
||||
if ($lgIdx) {
|
||||
$data['codeKinds'] = model(CodeKindModel::class)->orderBy('ck_code', 'ASC')->findAll();
|
||||
$data['bagPrices'] = model(BagPriceModel::class)->where('bp_lg_idx', $lgIdx)->orderBy('bp_bag_code', 'ASC')->findAll();
|
||||
$data['packagingUnits'] = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->orderBy('pu_bag_code', 'ASC')->findAll();
|
||||
}
|
||||
|
||||
return $this->render('기본정보관리', 'bag/basic_info', $data);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 발주 입고 관리
|
||||
// ──────────────────────────────────────────────
|
||||
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')->findAll();
|
||||
|
||||
// 발주별 품목 합계
|
||||
$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')->findAll();
|
||||
}
|
||||
|
||||
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')->findAll();
|
||||
}
|
||||
|
||||
return $this->render('불출 관리', 'bag/issue', $data);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 재고 관리
|
||||
// ──────────────────────────────────────────────
|
||||
public function inventory(): string
|
||||
{
|
||||
$lgIdx = $this->lgIdx();
|
||||
$data = ['list' => []];
|
||||
|
||||
if ($lgIdx) {
|
||||
$data['list'] = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code', 'ASC')->findAll();
|
||||
}
|
||||
|
||||
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')->findAll();
|
||||
|
||||
// 주문 접수
|
||||
$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')->findAll();
|
||||
}
|
||||
|
||||
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')->findAll();
|
||||
}
|
||||
|
||||
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', []);
|
||||
}
|
||||
}
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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',
|
||||
];
|
||||
}
|
||||
@@ -24,6 +24,8 @@ class MemberModel extends Model
|
||||
'mb_regdate',
|
||||
'mb_latestdate',
|
||||
'mb_leavedate',
|
||||
'mb_login_fail_count',
|
||||
'mb_locked_until',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
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
@@ -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',
|
||||
];
|
||||
}
|
||||
30
app/Views/admin/bag_inventory/index.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<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 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>
|
||||
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="<?= base_url('admin/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="<?= base_url('admin/bag-issues') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
60
app/Views/admin/bag_issue/index.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<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/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>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/bag-issues') ?>" class="flex flex-wrap items-center gap-2">
|
||||
<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-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="<?= base_url('admin/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>
|
||||
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="<?= base_url('admin/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_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="<?= base_url('admin/bag-orders') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
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="<?= base_url('admin/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>
|
||||
75
app/Views/admin/bag_order/index.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<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/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>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/bag-orders') ?>" class="flex flex-wrap items-center gap-2">
|
||||
<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="<?= base_url('admin/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="<?= base_url('admin/bag-orders/detail/' . (int) $row->bo_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">상세</a>
|
||||
<form action="<?= base_url('admin/bag-orders/cancel/' . (int) $row->bo_idx) ?>" method="POST" class="inline" onsubmit="return confirm('취소하시겠습니까?');">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="text-orange-600 hover:underline text-sm mr-1">취소</button>
|
||||
</form>
|
||||
<form action="<?= base_url('admin/bag-orders/delete/' . (int) $row->bo_idx) ?>" method="POST" class="inline" onsubmit="return confirm('삭제하시겠습니까?');">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?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>
|
||||
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="<?= base_url('admin/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="<?= base_url('admin/bag-receivings') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
49
app/Views/admin/bag_receiving/index.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<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/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>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/bag-receivings') ?>" class="flex flex-wrap items-center gap-2">
|
||||
<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-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>
|
||||
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="<?= base_url('admin/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="<?= base_url('admin/bag-sales') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
63
app/Views/admin/bag_sale/index.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<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/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>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/bag-sales') ?>" class="flex flex-wrap items-center gap-2">
|
||||
<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="<?= base_url('admin/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>
|
||||
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>
|
||||
@@ -15,6 +15,7 @@
|
||||
<th>구/군</th>
|
||||
<th>상태</th>
|
||||
<th>등록일</th>
|
||||
<th class="w-28">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
@@ -27,6 +28,13 @@
|
||||
<td class="text-left pl-2"><?= esc($row->lg_gugun) ?></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-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>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
28
app/Views/admin/password_change/index.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<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-md">
|
||||
<form action="<?= base_url('admin/password-change') ?>" 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-32">현재 비밀번호 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="current_password" type="password" required autocomplete="current-password"/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-32">새 비밀번호 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="new_password" type="password" required autocomplete="new-password"/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-32">비밀번호 확인 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="new_password_confirm" type="password" required autocomplete="new-password"/>
|
||||
</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') ?>" 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/sales_report/daily_summary.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<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>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/reports/daily-summary') ?>" class="flex flex-wrap items-center gap-2">
|
||||
<label class="text-sm text-gray-600">조회일</label>
|
||||
<input type="date" name="date" value="<?= esc($date ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="flex gap-4 mt-2">
|
||||
<!-- 당일 -->
|
||||
<div class="flex-1 border border-gray-300 overflow-auto">
|
||||
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
|
||||
<span class="text-sm font-bold text-gray-700">당일 (<?= esc($date ?? '') ?>)</span>
|
||||
</div>
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>봉투코드</th>
|
||||
<th>봉투명</th>
|
||||
<th>판매수량</th>
|
||||
<th>판매금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php
|
||||
$dailySaleQtyTotal = 0;
|
||||
$dailySaleAmountTotal = 0;
|
||||
?>
|
||||
<?php foreach ($daily as $row): ?>
|
||||
<?php
|
||||
$dailySaleQtyTotal += (int) $row->sale_qty;
|
||||
$dailySaleAmountTotal += (int) $row->sale_amount;
|
||||
?>
|
||||
<tr>
|
||||
<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->sale_qty) ?></td>
|
||||
<td><?= number_format((int) $row->sale_amount) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($daily)): ?>
|
||||
<tr><td colspan="4" 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="2" class="text-center">합계</td>
|
||||
<td><?= number_format($dailySaleQtyTotal) ?></td>
|
||||
<td><?= number_format($dailySaleAmountTotal) ?></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 당월 누계 -->
|
||||
<div class="flex-1 border border-gray-300 overflow-auto">
|
||||
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
|
||||
<span class="text-sm font-bold text-gray-700">당월 누계 (<?= esc($monthStart ?? '') ?> ~ <?= esc($date ?? '') ?>)</span>
|
||||
</div>
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>봉투코드</th>
|
||||
<th>봉투명</th>
|
||||
<th>판매수량</th>
|
||||
<th>판매금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php
|
||||
$monthlySaleQtyTotal = 0;
|
||||
$monthlySaleAmountTotal = 0;
|
||||
?>
|
||||
<?php foreach ($monthly as $row): ?>
|
||||
<?php
|
||||
$monthlySaleQtyTotal += (int) $row->sale_qty;
|
||||
$monthlySaleAmountTotal += (int) $row->sale_amount;
|
||||
?>
|
||||
<tr>
|
||||
<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->sale_qty) ?></td>
|
||||
<td><?= number_format((int) $row->sale_amount) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($monthly)): ?>
|
||||
<tr><td colspan="4" 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="2" class="text-center">합계</td>
|
||||
<td><?= number_format($monthlySaleQtyTotal) ?></td>
|
||||
<td><?= number_format($monthlySaleAmountTotal) ?></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
69
app/Views/admin/sales_report/period_sales.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<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>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/reports/period-sales') ?>" class="flex flex-wrap items-center gap-2">
|
||||
<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>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>봉투코드</th>
|
||||
<th>봉투명</th>
|
||||
<th>판매수량</th>
|
||||
<th>판매금액</th>
|
||||
<th>반품수량</th>
|
||||
<th>반품금액</th>
|
||||
<th>합계수량</th>
|
||||
<th>합계금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php
|
||||
$grandSaleQty = 0;
|
||||
$grandSaleAmount = 0;
|
||||
$grandReturnQty = 0;
|
||||
$grandReturnAmount = 0;
|
||||
?>
|
||||
<?php foreach ($result as $row): ?>
|
||||
<?php
|
||||
$grandSaleQty += (int) $row->sale_qty;
|
||||
$grandSaleAmount += (int) $row->sale_amount;
|
||||
$grandReturnQty += (int) $row->return_qty;
|
||||
$grandReturnAmount += (int) $row->return_amount;
|
||||
?>
|
||||
<tr>
|
||||
<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->sale_qty) ?></td>
|
||||
<td><?= number_format((int) $row->sale_amount) ?></td>
|
||||
<td><?= number_format((int) $row->return_qty) ?></td>
|
||||
<td><?= number_format((int) $row->return_amount) ?></td>
|
||||
<td class="font-bold"><?= number_format((int) $row->sale_qty - (int) $row->return_qty) ?></td>
|
||||
<td class="font-bold"><?= number_format((int) $row->sale_amount - (int) $row->return_amount) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($result)): ?>
|
||||
<tr><td colspan="8" 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="2" class="text-center">합계</td>
|
||||
<td><?= number_format($grandSaleQty) ?></td>
|
||||
<td><?= number_format($grandSaleAmount) ?></td>
|
||||
<td><?= number_format($grandReturnQty) ?></td>
|
||||
<td><?= number_format($grandReturnAmount) ?></td>
|
||||
<td><?= number_format($grandSaleQty - $grandReturnQty) ?></td>
|
||||
<td><?= number_format($grandSaleAmount - $grandReturnAmount) ?></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
93
app/Views/admin/sales_report/sales_ledger.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<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>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/reports/sales-ledger') ?>" class="flex flex-wrap items-center gap-2">
|
||||
<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="mode" class="border border-gray-300 rounded px-2 py-1 text-sm">
|
||||
<option value="daily" <?= ($mode ?? '') === 'daily' ? 'selected' : '' ?>>일자별</option>
|
||||
<option value="period" <?= ($mode ?? '') === 'period' ? 'selected' : '' ?>>기간별</option>
|
||||
</select>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<?php if (($mode ?? 'daily') === 'daily'): ?>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>판매일</th>
|
||||
<th>판매소</th>
|
||||
<th>봉투코드</th>
|
||||
<th>봉투명</th>
|
||||
<th>구분</th>
|
||||
<th>수량</th>
|
||||
<th>금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php foreach ($result as $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= esc($row->bs_sale_date) ?></td>
|
||||
<td class="text-left pl-2"><?= esc($row->bs_ds_name) ?></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 class="text-center">
|
||||
<?php
|
||||
$typeMap = ['sale' => '판매', 'return' => '반품'];
|
||||
echo esc($typeMap[$row->bs_type] ?? $row->bs_type);
|
||||
?>
|
||||
</td>
|
||||
<td><?= number_format((int) $row->total_qty) ?></td>
|
||||
<td><?= number_format((int) $row->total_amount) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($result)): ?>
|
||||
<tr><td colspan="7" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>판매소</th>
|
||||
<th>봉투코드</th>
|
||||
<th>봉투명</th>
|
||||
<th>판매수량</th>
|
||||
<th>판매금액</th>
|
||||
<th>반품수량</th>
|
||||
<th>반품금액</th>
|
||||
<th>계(수량)</th>
|
||||
<th>계(금액)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php foreach ($result as $row): ?>
|
||||
<tr>
|
||||
<td class="text-left pl-2"><?= esc($row->bs_ds_name) ?></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->sale_qty) ?></td>
|
||||
<td><?= number_format((int) $row->sale_amount) ?></td>
|
||||
<td><?= number_format((int) $row->return_qty) ?></td>
|
||||
<td><?= number_format((int) $row->return_amount) ?></td>
|
||||
<td class="font-bold"><?= number_format((int) $row->sale_qty - (int) $row->return_qty) ?></td>
|
||||
<td class="font-bold"><?= number_format((int) $row->sale_amount - (int) $row->return_amount) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($result)): ?>
|
||||
<tr><td colspan="9" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
130
app/Views/admin/sales_report/supply_demand.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<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>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/reports/supply-demand') ?>" class="flex flex-wrap items-center gap-2">
|
||||
<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>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mt-2">
|
||||
<!-- 현재 재고 -->
|
||||
<div class="border border-gray-300 rounded overflow-auto">
|
||||
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
|
||||
<span class="text-sm font-bold text-gray-700">현재 재고</span>
|
||||
</div>
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>봉투코드</th>
|
||||
<th>봉투명</th>
|
||||
<th>재고수량</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php foreach ($inventory as $row): ?>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($inventory)): ?>
|
||||
<tr><td colspan="3" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 기간 입고 -->
|
||||
<div class="border border-gray-300 rounded overflow-auto">
|
||||
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
|
||||
<span class="text-sm font-bold text-gray-700">기간 입고</span>
|
||||
</div>
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>봉투코드</th>
|
||||
<th>봉투명</th>
|
||||
<th>입고수량</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php foreach ($receiving as $row): ?>
|
||||
<tr>
|
||||
<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->recv_qty) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($receiving)): ?>
|
||||
<tr><td colspan="3" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 기간 판매 -->
|
||||
<div class="border border-gray-300 rounded overflow-auto">
|
||||
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
|
||||
<span class="text-sm font-bold text-gray-700">기간 판매</span>
|
||||
</div>
|
||||
<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 foreach ($sales as $row): ?>
|
||||
<tr>
|
||||
<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->sale_qty) ?></td>
|
||||
<td><?= number_format((int) $row->return_qty) ?></td>
|
||||
<td class="font-bold"><?= number_format((int) $row->sale_qty - (int) $row->return_qty) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($sales)): ?>
|
||||
<tr><td colspan="5" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 기간 불출 -->
|
||||
<div class="border border-gray-300 rounded overflow-auto">
|
||||
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
|
||||
<span class="text-sm font-bold text-gray-700">기간 불출</span>
|
||||
</div>
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>봉투코드</th>
|
||||
<th>봉투명</th>
|
||||
<th>불출수량</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php foreach ($issues as $row): ?>
|
||||
<tr>
|
||||
<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->issue_qty) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($issues)): ?>
|
||||
<tr><td colspan="3" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
74
app/Views/admin/shop_order/create.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<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="<?= base_url('admin/shop-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>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="so_ds_idx" required>
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($shops as $shop): ?>
|
||||
<option value="<?= esc($shop->ds_idx) ?>" <?= (int) old('so_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>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="so_delivery_date" type="date" value="<?= esc(old('so_delivery_date', date('Y-m-d', strtotime('+1 day')))) ?>" 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="so_payment_type" required>
|
||||
<option value="">선택</option>
|
||||
<option value="이체" <?= old('so_payment_type') === '이체' ? 'selected' : '' ?>>이체</option>
|
||||
<option value="가상계좌" <?= old('so_payment_type') === '가상계좌' ? 'selected' : '' ?>>가상계좌</option>
|
||||
</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[]" 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="<?= base_url('admin/shop-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>
|
||||
75
app/Views/admin/shop_order/index.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<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/shop-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>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= base_url('admin/shop-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"/>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||
<a href="<?= base_url('admin/shop-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>판매소</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->so_idx) ?></td>
|
||||
<td class="text-left pl-2"><?= esc($row->so_ds_name) ?></td>
|
||||
<td class="text-center"><?= esc($row->so_order_date) ?></td>
|
||||
<td class="text-center"><?= esc($row->so_delivery_date) ?></td>
|
||||
<td class="text-center"><?= esc($row->so_payment_type) ?></td>
|
||||
<td class="text-center">
|
||||
<?php
|
||||
$paidMap = ['0' => '미입금', '1' => '입금'];
|
||||
echo esc($paidMap[$row->so_paid] ?? $row->so_paid);
|
||||
?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<?php
|
||||
$receivedMap = ['0' => '미수령', '1' => '수령'];
|
||||
echo esc($receivedMap[$row->so_received] ?? $row->so_received);
|
||||
?>
|
||||
</td>
|
||||
<td><?= number_format((int) $row->so_total_qty) ?></td>
|
||||
<td><?= number_format((int) $row->so_total_amount) ?></td>
|
||||
<td class="text-center">
|
||||
<?php
|
||||
$statusMap = ['normal' => '정상', 'cancelled' => '취소'];
|
||||
echo esc($statusMap[$row->so_status] ?? $row->so_status);
|
||||
?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<form action="<?= base_url('admin/shop-orders/cancel/' . (int) $row->so_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>
|
||||
9
app/Views/bag/analytics.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<div class="flex items-center justify-center h-full text-gray-400">
|
||||
<div class="text-center">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"/>
|
||||
</svg>
|
||||
<p class="text-lg font-medium">통계 분석 관리</p>
|
||||
<p class="text-sm mt-1">Phase 6에서 구현 예정입니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
92
app/Views/bag/basic_info.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<div class="space-y-6">
|
||||
<!-- 기본코드 종류 -->
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-2 border-b pb-1">
|
||||
<h3 class="text-base font-bold text-gray-700">기본코드 종류</h3>
|
||||
<a href="<?= base_url('admin/code-kinds') ?>" class="text-blue-600 hover:underline text-sm">관리 →</a>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead><tr>
|
||||
<th class="w-16">번호</th><th>코드</th><th>코드명</th><th>상태</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php if (! empty($codeKinds)): ?>
|
||||
<?php foreach ($codeKinds as $i => $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $i + 1 ?></td>
|
||||
<td class="text-center"><?= esc($row->ck_code) ?></td>
|
||||
<td><?= esc($row->ck_name) ?></td>
|
||||
<td class="text-center"><?= ($row->ck_status ?? 'active') === 'active' ? '사용' : '미사용' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="4" class="text-center text-gray-400 py-4">등록된 코드 종류가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- 봉투 단가 -->
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-2 border-b pb-1">
|
||||
<h3 class="text-base font-bold text-gray-700">봉투 단가</h3>
|
||||
<a href="<?= base_url('admin/bag-prices') ?>" class="text-blue-600 hover:underline text-sm">관리 →</a>
|
||||
</div>
|
||||
<table class="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>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php if (! empty($bagPrices)): ?>
|
||||
<?php foreach ($bagPrices as $i => $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $i + 1 ?></td>
|
||||
<td class="text-center"><?= esc($row->bp_bag_code) ?></td>
|
||||
<td><?= esc($row->bp_bag_name ?? '') ?></td>
|
||||
<td class="text-right"><?= number_format((float)($row->bp_order_price ?? 0)) ?></td>
|
||||
<td class="text-right"><?= number_format((float)($row->bp_wholesale_price ?? 0)) ?></td>
|
||||
<td class="text-right"><?= number_format((float)($row->bp_consumer_price ?? 0)) ?></td>
|
||||
<td class="text-center"><?= esc($row->bp_start_date ?? '') ?></td>
|
||||
<td class="text-center"><?= ($row->bp_end_date ?? '') ?: '현재' ?></td>
|
||||
<td class="text-center"><?= ($row->bp_status ?? 'active') === 'active' ? '사용' : '만료' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="9" class="text-center text-gray-400 py-4">등록된 단가 정보가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- 포장 단위 -->
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-2 border-b pb-1">
|
||||
<h3 class="text-base font-bold text-gray-700">포장 단위</h3>
|
||||
<a href="<?= base_url('admin/packaging-units') ?>" class="text-blue-600 hover:underline text-sm">관리 →</a>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead><tr>
|
||||
<th class="w-16">번호</th><th>봉투코드</th><th>봉투명</th><th>박스당 팩 수</th><th>팩당 낱장 수</th><th>1박스 총 낱장</th><th>적용시작</th><th>적용종료</th><th>상태</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php if (! empty($packagingUnits)): ?>
|
||||
<?php foreach ($packagingUnits as $i => $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $i + 1 ?></td>
|
||||
<td class="text-center"><?= esc($row->pu_bag_code) ?></td>
|
||||
<td><?= esc($row->pu_bag_name ?? '') ?></td>
|
||||
<td class="text-right"><?= number_format((int)($row->pu_packs_per_box ?? 0)) ?></td>
|
||||
<td class="text-right"><?= number_format((int)($row->pu_sheets_per_pack ?? 0)) ?></td>
|
||||
<td class="text-right"><?= number_format((int)($row->pu_packs_per_box ?? 0) * (int)($row->pu_sheets_per_pack ?? 0)) ?></td>
|
||||
<td class="text-center"><?= esc($row->pu_start_date ?? '') ?></td>
|
||||
<td class="text-center"><?= ($row->pu_end_date ?? '') ?: '현재' ?></td>
|
||||
<td class="text-center"><?= ($row->pu_status ?? 'active') === 'active' ? '사용' : '만료' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="9" class="text-center text-gray-400 py-4">등록된 포장 단위가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
88
app/Views/bag/flow.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<div class="space-y-1">
|
||||
<form method="get" class="flex items-center gap-3 text-sm mb-3">
|
||||
<label class="font-bold text-gray-700">조회기간</label>
|
||||
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||
<span>~</span>
|
||||
<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.5 rounded-sm text-sm">조회</button>
|
||||
<a href="<?= base_url('bag/flow') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
|
||||
</form>
|
||||
|
||||
<!-- 수불 요약 -->
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">봉투코드</th>
|
||||
<th rowspan="2">봉투명</th>
|
||||
<th rowspan="2">현재재고</th>
|
||||
<th colspan="2">입고</th>
|
||||
<th colspan="2">출고</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>입고수량</th><th>반품수량</th>
|
||||
<th>판매수량</th><th>불출수량</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
// 봉투코드별 수불 집계
|
||||
$summary = [];
|
||||
// 재고
|
||||
foreach ($inventory as $inv) {
|
||||
$code = $inv->bi_bag_code ?? '';
|
||||
if (! isset($summary[$code])) {
|
||||
$summary[$code] = ['name' => $inv->bi_bag_name ?? '', 'stock' => 0, 'recv' => 0, 'return' => 0, 'sale' => 0, 'issue' => 0];
|
||||
}
|
||||
$summary[$code]['stock'] += (int)($inv->bi_qty_sheet ?? 0);
|
||||
}
|
||||
// 입고
|
||||
foreach ($receiving as $r) {
|
||||
$code = $r->br_bag_code ?? '';
|
||||
if (! isset($summary[$code])) {
|
||||
$summary[$code] = ['name' => $r->br_bag_name ?? '', 'stock' => 0, 'recv' => 0, 'return' => 0, 'sale' => 0, 'issue' => 0];
|
||||
}
|
||||
$summary[$code]['recv'] += (int)($r->br_qty_sheet ?? 0);
|
||||
}
|
||||
// 판매/반품
|
||||
foreach ($sales as $s) {
|
||||
$code = $s->bs_bag_code ?? '';
|
||||
if (! isset($summary[$code])) {
|
||||
$summary[$code] = ['name' => $s->bs_bag_name ?? '', 'stock' => 0, 'recv' => 0, 'return' => 0, 'sale' => 0, 'issue' => 0];
|
||||
}
|
||||
$type = $s->bs_type ?? 'sale';
|
||||
if ($type === 'return') {
|
||||
$summary[$code]['return'] += (int)($s->bs_qty ?? 0);
|
||||
} else {
|
||||
$summary[$code]['sale'] += (int)($s->bs_qty ?? 0);
|
||||
}
|
||||
}
|
||||
// 불출
|
||||
foreach ($issues as $iss) {
|
||||
$code = $iss->bi2_bag_code ?? '';
|
||||
if (! isset($summary[$code])) {
|
||||
$summary[$code] = ['name' => $iss->bi2_bag_name ?? '', 'stock' => 0, 'recv' => 0, 'return' => 0, 'sale' => 0, 'issue' => 0];
|
||||
}
|
||||
if (($iss->bi2_status ?? 'normal') === 'normal') {
|
||||
$summary[$code]['issue'] += (int)($iss->bi2_qty ?? 0);
|
||||
}
|
||||
}
|
||||
ksort($summary);
|
||||
?>
|
||||
<?php if (! empty($summary)): ?>
|
||||
<?php $idx = 0; foreach ($summary as $code => $s): $idx++; ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= esc($code) ?></td>
|
||||
<td><?= esc($s['name']) ?></td>
|
||||
<td class="text-right"><?= number_format($s['stock']) ?></td>
|
||||
<td class="text-right"><?= number_format($s['recv']) ?></td>
|
||||
<td class="text-right"><?= number_format($s['return']) ?></td>
|
||||
<td class="text-right"><?= number_format($s['sale']) ?></td>
|
||||
<td class="text-right"><?= number_format($s['issue']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="7" class="text-center text-gray-400 py-4">수불 데이터가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
31
app/Views/bag/help.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<div class="max-w-3xl mx-auto py-4">
|
||||
<h2 class="text-lg font-bold text-gray-700 mb-4">도움말</h2>
|
||||
|
||||
<div class="space-y-4 text-sm text-gray-600">
|
||||
<section>
|
||||
<h3 class="font-bold text-gray-700 mb-1">시스템 개요</h3>
|
||||
<p>쓰레기봉투 물류시스템은 지자체 종량제 쓰레기봉투의 발주, 입고, 재고, 판매, 불출 등 전체 물류 프로세스를 관리합니다.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="font-bold text-gray-700 mb-1">메뉴 안내</h3>
|
||||
<table class="data-table">
|
||||
<thead><tr><th>메뉴</th><th>설명</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="font-medium">기본정보관리</td><td>코드 종류, 봉투 단가, 포장 단위 등 기본 데이터 조회</td></tr>
|
||||
<tr><td class="font-medium">발주 입고 관리</td><td>봉투 발주 현황 및 입고 현황 조회</td></tr>
|
||||
<tr><td class="font-medium">불출 관리</td><td>무료용 봉투 불출 내역 조회</td></tr>
|
||||
<tr><td class="font-medium">재고 관리</td><td>봉투별 현재 재고(낱장) 조회</td></tr>
|
||||
<tr><td class="font-medium">판매 관리</td><td>주문 접수, 판매/반품 내역 조회</td></tr>
|
||||
<tr><td class="font-medium">판매 현황</td><td>기간별 판매 데이터 조회</td></tr>
|
||||
<tr><td class="font-medium">봉투 수불 관리</td><td>봉투코드별 입출고 수불 요약 조회</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="font-bold text-gray-700 mb-1">문의</h3>
|
||||
<p>시스템 사용 중 문의사항은 소속 지자체 담당자에게 연락하시기 바랍니다.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
20
app/Views/bag/inventory.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<table class="data-table">
|
||||
<thead><tr>
|
||||
<th class="w-16">번호</th><th>봉투코드</th><th>봉투명</th><th>현재재고(낱장)</th><th>최종갱신</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php if (! empty($list)): ?>
|
||||
<?php foreach ($list as $i => $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $i + 1 ?></td>
|
||||
<td class="text-center"><?= esc($row->bi_bag_code ?? '') ?></td>
|
||||
<td><?= esc($row->bi_bag_name ?? '') ?></td>
|
||||
<td class="text-right"><?= number_format((int)($row->bi_qty_sheet ?? 0)) ?></td>
|
||||
<td class="text-center"><?= esc($row->bi_updated_at ?? $row->updated_at ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="5" class="text-center text-gray-400 py-4">재고 데이터가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
54
app/Views/bag/issue.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<form method="get" class="flex items-center gap-3 text-sm">
|
||||
<label class="font-bold text-gray-700">불출일</label>
|
||||
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||
<span>~</span>
|
||||
<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.5 rounded-sm text-sm">조회</button>
|
||||
<a href="<?= base_url('bag/issue') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
|
||||
</form>
|
||||
<a href="<?= base_url('admin/bag-issues/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">불출 처리</a>
|
||||
</div>
|
||||
|
||||
<table class="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>상태</th><th>작업</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php if (! empty($list)): ?>
|
||||
<?php foreach ($list as $i => $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $i + 1 ?></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><?= esc($row->bi2_dest_name ?? '') ?></td>
|
||||
<td class="text-center"><?= esc($row->bi2_bag_code ?? '') ?></td>
|
||||
<td><?= esc($row->bi2_bag_name ?? '') ?></td>
|
||||
<td class="text-right"><?= number_format((int)($row->bi2_qty ?? 0)) ?></td>
|
||||
<td class="text-center">
|
||||
<?php
|
||||
$st = $row->bi2_status ?? 'normal';
|
||||
echo match($st) { 'normal' => '정상', 'cancelled' => '<span class="text-orange-600">취소</span>', default => esc($st) };
|
||||
?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<?php if (($row->bi2_status ?? '') === 'normal'): ?>
|
||||
<form method="post" action="<?= base_url('admin/bag-issues/cancel/' . $row->bi2_idx) ?>" class="inline" onsubmit="return confirm('취소하시겠습니까?')">
|
||||
<?= csrf_field() ?>
|
||||
<button class="text-orange-600 hover:underline text-xs">취소</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="11" class="text-center text-gray-400 py-4">등록된 불출이 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
123
app/Views/bag/layout/main.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
helper('admin');
|
||||
$siteNavTree = get_site_nav_tree();
|
||||
$uriObj = service('request')->getUri();
|
||||
$currentPath = trim((string) $uriObj->getPath(), '/');
|
||||
if (str_starts_with($currentPath, 'index.php/')) {
|
||||
$currentPath = substr($currentPath, strlen('index.php/'));
|
||||
}
|
||||
$mbLevel = (int) session()->get('mb_level');
|
||||
$isAdmin = ($mbLevel === \Config\Roles::LEVEL_SUPER_ADMIN || $mbLevel === \Config\Roles::LEVEL_LOCAL_ADMIN);
|
||||
$effectiveLgIdx = admin_effective_lg_idx();
|
||||
$effectiveLgName = null;
|
||||
if ($effectiveLgIdx) {
|
||||
$lgRow = model(\App\Models\LocalGovernmentModel::class)->find($effectiveLgIdx);
|
||||
$effectiveLgName = $lgRow ? $lgRow->lg_name : null;
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title><?= esc($title ?? '쓰레기봉투 물류시스템') ?></title>
|
||||
<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"/>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: { sans: ['"Malgun Gothic"', '"Noto Sans KR"', 'sans-serif'] },
|
||||
colors: {
|
||||
'system-header': '#ffffff',
|
||||
'title-bar': '#2c3e50',
|
||||
'control-panel': '#f8f9fa',
|
||||
'btn-search': '#1c4e80',
|
||||
'btn-excel-border': '#28a745',
|
||||
'btn-excel-text': '#28a745',
|
||||
'btn-print-border': '#ced4da',
|
||||
'btn-exit': '#d9534f',
|
||||
},
|
||||
fontSize: { 'xxs': '0.65rem' }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style data-purpose="table-layout">
|
||||
.data-table { width: 100%; border-collapse: collapse; font-family: 'Malgun Gothic', 'Noto Sans KR', sans-serif; }
|
||||
.data-table th, .data-table td { border: 1px solid #ccc; padding: 4px 8px; white-space: nowrap; font-size: 13px; }
|
||||
.data-table th { background-color: #e9ecef; text-align: center; vertical-align: middle; font-weight: bold; color: #333; }
|
||||
.data-table tbody tr:nth-child(even) td { background-color: #f9f9f9; }
|
||||
.data-table tbody tr:hover td { background-color: #e6f7ff !important; }
|
||||
.main-content-area { height: calc(100vh - 130px); overflow: auto; }
|
||||
body { overflow: hidden; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased select-none">
|
||||
<!-- BEGIN: Top Navigation -->
|
||||
<header class="bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0 z-20">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<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() ?>" 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">
|
||||
<?php if (! empty($siteNavTree)): ?>
|
||||
<?php foreach ($siteNavTree as $navItem): ?>
|
||||
<?php $isActive = ($currentPath === trim((string) $navItem->mm_link, '/')); ?>
|
||||
<div class="relative group">
|
||||
<a class="<?= $isActive ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>"
|
||||
href="<?= base_url($navItem->mm_link) ?>">
|
||||
<?= esc($navItem->mm_name) ?>
|
||||
</a>
|
||||
<?php if (! empty($navItem->children)): ?>
|
||||
<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 foreach ($navItem->children as $child): ?>
|
||||
<a href="<?= base_url($child->mm_link) ?>"
|
||||
class="block px-3 py-1.5 text-sm text-gray-700 hover:bg-blue-50 whitespace-nowrap">
|
||||
<?= esc($child->mm_name) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<div class="flex items-center gap-2">
|
||||
<?php if ($effectiveLgName !== null): ?>
|
||||
<span class="text-sm text-gray-600" title="현재 작업 지자체"><?= esc($effectiveLgName) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($isAdmin): ?>
|
||||
<a href="<?= base_url('admin') ?>" class="text-gray-500 hover:text-blue-600 text-sm">관리자</a>
|
||||
<?php endif; ?>
|
||||
<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>
|
||||
<!-- END: Top Navigation -->
|
||||
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
|
||||
<?= esc($title ?? '') ?>
|
||||
</div>
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="mx-4 mt-2 p-3 rounded-lg bg-green-50 text-green-700 text-sm" role="alert"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (session()->getFlashdata('error')): ?>
|
||||
<div class="mx-4 mt-2 p-3 rounded-lg bg-red-50 text-red-700 text-sm" role="alert"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||
<?php endif; ?>
|
||||
<main class="main-content-area flex-grow bg-white p-4">
|
||||
<?= $content ?>
|
||||
</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">
|
||||
<span>쓰레기봉투 물류시스템</span>
|
||||
<span><?= date('Y.m.d (D) g:i:sA') ?></span>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
86
app/Views/bag/purchase_inbound.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<div class="space-y-1">
|
||||
<!-- 필터 -->
|
||||
<form method="get" class="flex items-center gap-3 text-sm mb-3">
|
||||
<label class="font-bold text-gray-700">조회기간</label>
|
||||
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||
<span>~</span>
|
||||
<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.5 rounded-sm text-sm">조회</button>
|
||||
<a href="<?= base_url('bag/purchase-inbound') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
|
||||
</form>
|
||||
|
||||
<!-- 발주 현황 -->
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-2 border-b pb-1">
|
||||
<h3 class="text-base font-bold text-gray-700">발주 현황</h3>
|
||||
<a href="<?= base_url('admin/bag-orders/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">발주 등록</a>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead><tr>
|
||||
<th class="w-16">번호</th><th>LOT번호</th><th>발주일</th><th>품목수</th><th>총수량(낱장)</th><th>총금액</th><th>상태</th><th>작업</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php if (! empty($orders)): ?>
|
||||
<?php foreach ($orders as $i => $row): ?>
|
||||
<?php $summary = $itemSummary[$row->bo_idx] ?? ['qty' => 0, 'amount' => 0, 'count' => 0]; ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $i + 1 ?></td>
|
||||
<td class="text-center"><?= esc($row->bo_lot_no ?? '') ?></td>
|
||||
<td class="text-center"><?= esc($row->bo_order_date ?? '') ?></td>
|
||||
<td class="text-right"><?= number_format($summary['count']) ?></td>
|
||||
<td class="text-right"><?= number_format($summary['qty']) ?></td>
|
||||
<td class="text-right"><?= number_format($summary['amount']) ?></td>
|
||||
<td class="text-center">
|
||||
<?php
|
||||
$st = $row->bo_status ?? 'normal';
|
||||
echo match($st) { 'normal' => '정상', 'cancelled' => '<span class="text-orange-600">취소</span>', 'deleted' => '<span class="text-red-600">삭제</span>', default => esc($st) };
|
||||
?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="<?= base_url('admin/bag-orders/detail/' . $row->bo_idx) ?>" class="text-blue-600 hover:underline text-xs">상세</a>
|
||||
<?php if (($row->bo_status ?? '') === 'normal'): ?>
|
||||
<form method="post" action="<?= base_url('admin/bag-orders/cancel/' . $row->bo_idx) ?>" class="inline" onsubmit="return confirm('취소하시겠습니까?')">
|
||||
<?= csrf_field() ?>
|
||||
<button class="text-orange-600 hover:underline text-xs ml-1">취소</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="8" class="text-center text-gray-400 py-4">등록된 발주가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- 입고 현황 -->
|
||||
<section class="mt-4">
|
||||
<div class="flex items-center justify-between mb-2 border-b pb-1">
|
||||
<h3 class="text-base font-bold text-gray-700">입고 현황</h3>
|
||||
<a href="<?= base_url('admin/bag-receivings/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">입고 처리</a>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead><tr>
|
||||
<th class="w-16">번호</th><th>봉투코드</th><th>봉투명</th><th>박스수</th><th>낱장수</th><th>입고일</th><th>구분</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php if (! empty($receivings)): ?>
|
||||
<?php foreach ($receivings as $i => $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $i + 1 ?></td>
|
||||
<td class="text-center"><?= esc($row->br_bag_code ?? '') ?></td>
|
||||
<td><?= esc($row->br_bag_name ?? '') ?></td>
|
||||
<td class="text-right"><?= number_format((int)($row->br_qty_box ?? 0)) ?></td>
|
||||
<td class="text-right"><?= number_format((int)($row->br_qty_sheet ?? 0)) ?></td>
|
||||
<td class="text-center"><?= esc($row->br_receive_date ?? '') ?></td>
|
||||
<td class="text-center"><?= esc($row->br_type ?? '정상입고') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="7" class="text-center text-gray-400 py-4">등록된 입고가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
82
app/Views/bag/sales.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<div class="space-y-1">
|
||||
<form method="get" class="flex items-center gap-3 text-sm mb-3">
|
||||
<label class="font-bold text-gray-700">조회기간</label>
|
||||
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||
<span>~</span>
|
||||
<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.5 rounded-sm text-sm">조회</button>
|
||||
<a href="<?= base_url('bag/sales') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
|
||||
</form>
|
||||
|
||||
<!-- 주문 접수 -->
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-2 border-b pb-1">
|
||||
<h3 class="text-base font-bold text-gray-700">주문 접수</h3>
|
||||
<a href="<?= base_url('admin/shop-orders/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">주문 등록</a>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead><tr>
|
||||
<th class="w-16">번호</th><th>판매소</th><th>접수일</th><th>배달일</th><th>수량</th><th>금액</th><th>상태</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php if (! empty($orderList)): ?>
|
||||
<?php foreach ($orderList as $i => $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $i + 1 ?></td>
|
||||
<td><?= esc($row->so_shop_name ?? '') ?></td>
|
||||
<td class="text-center"><?= esc($row->so_order_date ?? '') ?></td>
|
||||
<td class="text-center"><?= esc($row->so_delivery_date ?? '') ?></td>
|
||||
<td class="text-right"><?= number_format((int)($row->so_qty ?? 0)) ?></td>
|
||||
<td class="text-right"><?= number_format((float)($row->so_amount ?? 0)) ?></td>
|
||||
<td class="text-center">
|
||||
<?php
|
||||
$st = $row->so_status ?? 'normal';
|
||||
echo match($st) { 'normal' => '정상', 'cancelled' => '<span class="text-orange-600">취소</span>', default => esc($st) };
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="7" class="text-center text-gray-400 py-4">등록된 주문이 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- 판매/반품 -->
|
||||
<section class="mt-4">
|
||||
<div class="flex items-center justify-between mb-2 border-b pb-1">
|
||||
<h3 class="text-base font-bold text-gray-700">판매/반품</h3>
|
||||
<a href="<?= base_url('admin/bag-sales/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">판매 등록</a>
|
||||
</div>
|
||||
<table class="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>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php if (! empty($salesList)): ?>
|
||||
<?php foreach ($salesList as $i => $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $i + 1 ?></td>
|
||||
<td><?= esc($row->bs_shop_name ?? '') ?></td>
|
||||
<td class="text-center"><?= esc($row->bs_sale_date ?? '') ?></td>
|
||||
<td class="text-center"><?= esc($row->bs_bag_code ?? '') ?></td>
|
||||
<td><?= esc($row->bs_bag_name ?? '') ?></td>
|
||||
<td class="text-right"><?= number_format((int)($row->bs_qty ?? 0)) ?></td>
|
||||
<td class="text-right"><?= number_format((float)($row->bs_unit_price ?? 0)) ?></td>
|
||||
<td class="text-right"><?= number_format((float)($row->bs_amount ?? 0)) ?></td>
|
||||
<td class="text-center">
|
||||
<?php
|
||||
$t = $row->bs_type ?? 'sale';
|
||||
echo match($t) { 'sale' => '판매', 'return' => '<span class="text-blue-600">반품</span>', 'cancel' => '<span class="text-red-600">취소</span>', default => esc($t) };
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="9" class="text-center text-gray-400 py-4">등록된 판매/반품이 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
34
app/Views/bag/sales_stats.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<div class="space-y-1">
|
||||
<form method="get" class="flex items-center gap-3 text-sm mb-3">
|
||||
<label class="font-bold text-gray-700">조회기간</label>
|
||||
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||
<span>~</span>
|
||||
<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.5 rounded-sm text-sm">조회</button>
|
||||
<a href="<?= base_url('bag/sales-stats') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
|
||||
</form>
|
||||
|
||||
<table class="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>
|
||||
<?php if (! empty($result)): ?>
|
||||
<?php foreach ($result as $i => $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $i + 1 ?></td>
|
||||
<td><?= esc($row->bs_shop_name ?? '') ?></td>
|
||||
<td class="text-center"><?= esc($row->bs_sale_date ?? '') ?></td>
|
||||
<td class="text-center"><?= esc($row->bs_bag_code ?? '') ?></td>
|
||||
<td><?= esc($row->bs_bag_name ?? '') ?></td>
|
||||
<td class="text-right"><?= number_format((int)($row->bs_qty ?? 0)) ?></td>
|
||||
<td class="text-right"><?= number_format((float)($row->bs_unit_price ?? 0)) ?></td>
|
||||
<td class="text-right"><?= number_format((float)($row->bs_amount ?? 0)) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="8" class="text-center text-gray-400 py-4">판매 데이터가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
9
app/Views/bag/window.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<div class="flex items-center justify-center h-full text-gray-400">
|
||||
<div class="text-center">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 8.25V18a2.25 2.25 0 002.25 2.25h13.5A2.25 2.25 0 0021 18V8.25m-18 0V6a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 6v2.25m-18 0h18M5.25 6h.008v.008H5.25V6zM7.5 6h.008v.008H7.5V6zm2.25 0h.008v.008H9.75V6z"/>
|
||||
</svg>
|
||||
<p class="text-lg font-medium">창 관리</p>
|
||||
<p class="text-sm mt-1">Phase 6에서 구현 예정입니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@ test.describe('관리자 패널 — Super Admin', () => {
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// 선택 후 관리자 대시보드로 이동
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
|
||||
await page.goto('/admin');
|
||||
await expect(page).not.toHaveURL(/\/select-local-government/);
|
||||
});
|
||||
@@ -74,7 +74,7 @@ test.describe('관리자 패널 — Super Admin', () => {
|
||||
const radio = page.locator('input[name="lg_idx"]').first();
|
||||
await radio.check();
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
|
||||
|
||||
await page.goto('/admin/local-governments');
|
||||
await expect(page).toHaveURL(/\/admin\/local-governments/);
|
||||
|
||||
@@ -49,7 +49,7 @@ test.describe('인증 시스템', () => {
|
||||
test('로그아웃', async ({ page }) => {
|
||||
await login(page, 'local');
|
||||
await page.goto('/logout');
|
||||
await page.waitForURL(/\/login/, { timeout: 10000 });
|
||||
await page.waitForURL(/\/login/, { timeout: 30000 });
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
// 로그아웃 후 관리자 접근 불가 확인
|
||||
await page.goto('/admin');
|
||||
|
||||
@@ -6,7 +6,7 @@ async function loginAsAdmin(page) {
|
||||
await login(page, 'admin');
|
||||
await page.locator('input[name="lg_idx"]').first().check();
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
|
||||
}
|
||||
|
||||
test.describe('P2-03/04: 봉투 단가 관리', () => {
|
||||
|
||||
80
e2e/bag-site.spec.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { login } = require('./helpers/auth');
|
||||
|
||||
test.describe('사이트 메뉴 (/bag/*) 페이지 접근', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page, 'local');
|
||||
});
|
||||
|
||||
test('기본정보관리', async ({ page }) => {
|
||||
await page.goto('/bag/basic-info');
|
||||
await expect(page).toHaveURL(/\/bag\/basic-info/);
|
||||
await expect(page.locator('text=기본코드 종류')).toBeVisible();
|
||||
});
|
||||
|
||||
test('발주 입고 관리', async ({ page }) => {
|
||||
await page.goto('/bag/purchase-inbound');
|
||||
await expect(page).toHaveURL(/\/bag\/purchase-inbound/);
|
||||
await expect(page.locator('text=발주 현황')).toBeVisible();
|
||||
});
|
||||
|
||||
test('불출 관리', async ({ page }) => {
|
||||
await page.goto('/bag/issue');
|
||||
await expect(page).toHaveURL(/\/bag\/issue/);
|
||||
await expect(page.locator('th:has-text("불출일")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('재고 관리', async ({ page }) => {
|
||||
await page.goto('/bag/inventory');
|
||||
await expect(page).toHaveURL(/\/bag\/inventory/);
|
||||
await expect(page.locator('th:has-text("현재재고")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('판매 관리', async ({ page }) => {
|
||||
await page.goto('/bag/sales');
|
||||
await expect(page).toHaveURL(/\/bag\/sales/);
|
||||
await expect(page.locator('text=주문 접수')).toBeVisible();
|
||||
});
|
||||
|
||||
test('판매 현황', async ({ page }) => {
|
||||
await page.goto('/bag/sales-stats');
|
||||
await expect(page).toHaveURL(/\/bag\/sales-stats/);
|
||||
await expect(page.locator('th:has-text("봉투코드")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('봉투 수불 관리', async ({ page }) => {
|
||||
await page.goto('/bag/flow');
|
||||
await expect(page).toHaveURL(/\/bag\/flow/);
|
||||
await expect(page.locator('th:has-text("현재재고")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('통계 분석 관리', async ({ page }) => {
|
||||
await page.goto('/bag/analytics');
|
||||
await expect(page).toHaveURL(/\/bag\/analytics/);
|
||||
await expect(page.locator('main >> text=Phase 6에서 구현 예정')).toBeVisible();
|
||||
});
|
||||
|
||||
test('창', async ({ page }) => {
|
||||
await page.goto('/bag/window');
|
||||
await expect(page).toHaveURL(/\/bag\/window/);
|
||||
await expect(page.locator('text=창 관리')).toBeVisible();
|
||||
});
|
||||
|
||||
test('도움말', async ({ page }) => {
|
||||
await page.goto('/bag/help');
|
||||
await expect(page).toHaveURL(/\/bag\/help/);
|
||||
await expect(page.locator('text=시스템 개요')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('홈페이지 네비게이션 메뉴 링크', () => {
|
||||
test('메뉴 클릭으로 각 페이지 이동', async ({ page }) => {
|
||||
await login(page, 'local');
|
||||
await page.goto('/');
|
||||
|
||||
// 발주 입고 관리 메뉴 클릭
|
||||
await page.click('a:has-text("발주 입고 관리")');
|
||||
await expect(page).toHaveURL(/\/bag\/purchase-inbound/);
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@ async function loginAsAdmin(page) {
|
||||
const radio = page.locator('input[name="lg_idx"]').first();
|
||||
await radio.check();
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
|
||||
}
|
||||
|
||||
test.describe('P2-01: 기본코드 종류 관리', () => {
|
||||
|
||||
104
e2e/helpers/db-seed-realistic.js
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 실제와 같은 시범 데이터 시딩 스크립트
|
||||
* 유저(member) 테이블 제외 — 모든 업무 테이블에 대구 남구청 기준 데이터 삽입
|
||||
*
|
||||
* 실행: node e2e/helpers/db-seed-realistic.js
|
||||
*/
|
||||
const mysql = require('mysql2/promise');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// .env 파일에서 DB 설정 읽기
|
||||
function loadEnv() {
|
||||
const envPath = path.join(__dirname, '../../.env');
|
||||
const lines = fs.readFileSync(envPath, 'utf8').split('\n');
|
||||
const cfg = {};
|
||||
for (const line of lines) {
|
||||
const m = line.match(/^database\.default\.(\w+)\s*=\s*(.+)$/);
|
||||
if (m) cfg[m[1]] = m[2].trim();
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const env = loadEnv();
|
||||
const conn = await mysql.createConnection({
|
||||
host: env.hostname,
|
||||
port: parseInt(env.port || '3306'),
|
||||
user: env.username,
|
||||
password: env.password,
|
||||
database: env.database,
|
||||
multipleStatements: true,
|
||||
});
|
||||
|
||||
console.log('DB 연결 성공:', env.hostname);
|
||||
|
||||
// 남구청 lg_idx 가져오기
|
||||
const [[lgRow]] = await conn.query("SELECT lg_idx FROM local_government WHERE lg_code = '110204' LIMIT 1");
|
||||
if (!lgRow) {
|
||||
console.error('남구청(110204) 데이터가 없습니다. local_government_init_daegu.sql을 먼저 실행하세요.');
|
||||
process.exit(1);
|
||||
}
|
||||
const LG = lgRow.lg_idx;
|
||||
console.log(`남구청 lg_idx = ${LG}`);
|
||||
|
||||
// 기존 시드 데이터 정리 (idempotent)
|
||||
console.log('기존 시드 데이터 정리...');
|
||||
await conn.query(`DELETE FROM bag_issue WHERE bi2_lg_idx = ?`, [LG]);
|
||||
await conn.query(`DELETE FROM bag_sale WHERE bs_lg_idx = ?`, [LG]);
|
||||
await conn.query(`DELETE FROM shop_order_item WHERE soi_so_idx IN (SELECT so_idx FROM shop_order WHERE so_lg_idx = ?)`, [LG]);
|
||||
await conn.query(`DELETE FROM shop_order WHERE so_lg_idx = ?`, [LG]);
|
||||
await conn.query(`DELETE FROM bag_inventory WHERE bi_lg_idx = ?`, [LG]);
|
||||
await conn.query(`DELETE FROM bag_receiving WHERE br_lg_idx = ?`, [LG]);
|
||||
await conn.query(`DELETE FROM bag_order_item WHERE boi_bo_idx IN (SELECT bo_idx FROM bag_order WHERE bo_lg_idx = ?)`, [LG]);
|
||||
await conn.query(`DELETE FROM bag_order WHERE bo_lg_idx = ?`, [LG]);
|
||||
await conn.query(`DELETE FROM free_recipient WHERE fr_lg_idx = ?`, [LG]);
|
||||
await conn.query(`DELETE FROM manager WHERE mg_lg_idx = ?`, [LG]);
|
||||
await conn.query(`DELETE FROM company WHERE cp_lg_idx = ?`, [LG]);
|
||||
await conn.query(`DELETE FROM sales_agency WHERE sa_lg_idx = ?`, [LG]);
|
||||
await conn.query(`DELETE FROM bag_price WHERE bp_lg_idx = ?`, [LG]);
|
||||
await conn.query(`DELETE FROM packaging_unit WHERE pu_lg_idx = ?`, [LG]);
|
||||
await conn.query(`DELETE FROM designated_shop WHERE ds_lg_idx = ?`, [LG]);
|
||||
|
||||
// SQL 파일 읽어서 실행 (변수 치환)
|
||||
let sql = fs.readFileSync(path.join(__dirname, '../../writable/database/seed_realistic_data.sql'), 'utf8');
|
||||
|
||||
// SET @LG_IDX 문을 실제 값으로 치환
|
||||
sql = sql.replace(/SET @LG_IDX = \(SELECT.*?\);/s, `SET @LG_IDX = ${LG};`);
|
||||
|
||||
await conn.query(sql);
|
||||
console.log('시드 데이터 삽입 완료!');
|
||||
|
||||
// 확인
|
||||
const tables = ['designated_shop', 'sales_agency', 'company', 'manager', 'free_recipient', 'bag_price', 'packaging_unit', 'bag_order', 'bag_order_item', 'bag_receiving', 'bag_inventory', 'shop_order', 'bag_sale', 'bag_issue'];
|
||||
for (const t of tables) {
|
||||
let where = '';
|
||||
if (t === 'bag_order_item') where = `WHERE boi_bo_idx IN (SELECT bo_idx FROM bag_order WHERE bo_lg_idx = ${LG})`;
|
||||
else if (t === 'shop_order_item') where = `WHERE soi_so_idx IN (SELECT so_idx FROM shop_order WHERE so_lg_idx = ${LG})`;
|
||||
else {
|
||||
const prefix = t.split('_').map(w => w[0]).join('');
|
||||
// Try common patterns
|
||||
const col = t === 'designated_shop' ? 'ds_lg_idx' :
|
||||
t === 'sales_agency' ? 'sa_lg_idx' :
|
||||
t === 'company' ? 'cp_lg_idx' :
|
||||
t === 'manager' ? 'mg_lg_idx' :
|
||||
t === 'free_recipient' ? 'fr_lg_idx' :
|
||||
t === 'bag_price' ? 'bp_lg_idx' :
|
||||
t === 'packaging_unit' ? 'pu_lg_idx' :
|
||||
t === 'bag_order' ? 'bo_lg_idx' :
|
||||
t === 'bag_receiving' ? 'br_lg_idx' :
|
||||
t === 'bag_inventory' ? 'bi_lg_idx' :
|
||||
t === 'shop_order' ? 'so_lg_idx' :
|
||||
t === 'bag_sale' ? 'bs_lg_idx' :
|
||||
t === 'bag_issue' ? 'bi2_lg_idx' : null;
|
||||
if (col) where = `WHERE ${col} = ${LG}`;
|
||||
}
|
||||
const [[{ cnt }]] = await conn.query(`SELECT COUNT(*) as cnt FROM ${t} ${where}`);
|
||||
console.log(` ${t}: ${cnt}건`);
|
||||
}
|
||||
|
||||
await conn.end();
|
||||
console.log('\n완료!');
|
||||
}
|
||||
|
||||
main().catch(e => { console.error(e); process.exit(1); });
|
||||
68
e2e/helpers/screenshots-phase2-5.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const BASE_URL = 'http://localhost:8045';
|
||||
const DIR = '/mnt/c/project/jongryangje/screenshots';
|
||||
|
||||
async function run() {
|
||||
if (!fs.existsSync(DIR)) fs.mkdirSync(DIR, { recursive: true });
|
||||
const browser = await chromium.launch();
|
||||
const page = await (await browser.newContext({ viewport: { width: 1440, height: 900 }, locale: 'ko-KR' })).newPage();
|
||||
|
||||
// Super Admin 로그인 + 지자체 선택
|
||||
await page.goto(`${BASE_URL}/login`);
|
||||
await page.fill('input[name="login_id"]', 'tester_admin');
|
||||
await page.fill('input[name="password"]', 'test1234!');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(url => !url.pathname.includes('/login'), { timeout: 30000 });
|
||||
await page.locator('input[name="lg_idx"]').first().check();
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
|
||||
|
||||
const pages = [
|
||||
// Phase 2
|
||||
['30_bag_price_create', '/admin/bag-prices/create'],
|
||||
['31_packaging_unit', '/admin/packaging-units'],
|
||||
['32_sales_agency', '/admin/sales-agencies'],
|
||||
['33_sales_agency_create', '/admin/sales-agencies/create'],
|
||||
['34_manager', '/admin/managers'],
|
||||
['35_manager_create', '/admin/managers/create'],
|
||||
['36_company', '/admin/companies'],
|
||||
['37_company_create', '/admin/companies/create'],
|
||||
['38_free_recipient', '/admin/free-recipients'],
|
||||
['39_free_recipient_create', '/admin/free-recipients/create'],
|
||||
['40_lg_edit', '/admin/local-governments/edit/1'],
|
||||
['41_password_change', '/admin/password-change'],
|
||||
// Phase 3
|
||||
['42_bag_orders', '/admin/bag-orders'],
|
||||
['43_bag_order_create', '/admin/bag-orders/create'],
|
||||
['44_bag_receivings', '/admin/bag-receivings'],
|
||||
['45_bag_receiving_create', '/admin/bag-receivings/create'],
|
||||
['46_bag_inventory', '/admin/bag-inventory'],
|
||||
// Phase 4
|
||||
['47_shop_orders', '/admin/shop-orders'],
|
||||
['48_shop_order_create', '/admin/shop-orders/create'],
|
||||
['49_bag_sales', '/admin/bag-sales'],
|
||||
['50_bag_sale_create', '/admin/bag-sales/create'],
|
||||
['51_bag_issues', '/admin/bag-issues'],
|
||||
['52_bag_issue_create', '/admin/bag-issues/create'],
|
||||
// Phase 5
|
||||
['53_report_sales_ledger', '/admin/reports/sales-ledger'],
|
||||
['54_report_daily_summary', '/admin/reports/daily-summary'],
|
||||
['55_report_period_sales', '/admin/reports/period-sales'],
|
||||
['56_report_supply_demand', '/admin/reports/supply-demand'],
|
||||
];
|
||||
|
||||
for (const [name, url] of pages) {
|
||||
await page.goto(`${BASE_URL}${url}`);
|
||||
await page.waitForTimeout(800);
|
||||
await page.screenshot({ path: path.join(DIR, `${name}.png`), fullPage: true });
|
||||
console.log(` ✓ ${name}`);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
console.log(`\n${pages.length}개 스크린샷 완료`);
|
||||
}
|
||||
|
||||
run().catch(err => { console.error('실패:', err.message); process.exit(1); });
|
||||
@@ -20,7 +20,7 @@ async function login(page, account) {
|
||||
await page.fill('input[name="login_id"]', account.id);
|
||||
await page.fill('input[name="password"]', account.password);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(url => !url.pathname.includes('/login'), { timeout: 15000 });
|
||||
await page.waitForURL(url => !url.pathname.includes('/login'), { timeout: 30000 });
|
||||
}
|
||||
|
||||
async function screenshot(page, name, url) {
|
||||
@@ -52,7 +52,7 @@ async function run() {
|
||||
const radio = page.locator('input[name="lg_idx"]').first();
|
||||
await radio.check();
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
|
||||
|
||||
console.log('\n=== 관리자 패널 (Super Admin) ===');
|
||||
await screenshot(page, '05_admin_dashboard', `${BASE_URL}/admin`);
|
||||
|
||||
@@ -6,7 +6,7 @@ async function loginAsAdmin(page) {
|
||||
await login(page, 'admin');
|
||||
await page.locator('input[name="lg_idx"]').first().check();
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
|
||||
}
|
||||
|
||||
test.describe('P2-05/06: 포장 단위 관리', () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ async function loginAsAdmin(page) {
|
||||
await login(page, 'admin');
|
||||
await page.locator('input[name="lg_idx"]').first().check();
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 });
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
|
||||
}
|
||||
|
||||
async function loginAsLocal(page) {
|
||||
|
||||
54
e2e/phase2-extra.spec.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { login } = require('./helpers/auth');
|
||||
|
||||
async function loginAsAdmin(page) {
|
||||
await login(page, 'admin');
|
||||
await page.locator('input[name="lg_idx"]').first().check();
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
|
||||
}
|
||||
|
||||
test.describe('P2-19: 지자체 수정/삭제', () => {
|
||||
test('지자체 수정 폼 표시', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/local-governments/edit/1');
|
||||
await expect(page.locator('input[name="lg_name"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('지자체 목록에 수정/비활성 버튼 존재', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/local-governments');
|
||||
await expect(page.locator('a:has-text("수정")').first()).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('P2-20: 비밀번호 변경', () => {
|
||||
test('비밀번호 변경 폼 표시', async ({ page }) => {
|
||||
await login(page, 'local');
|
||||
await page.goto('/admin/password-change');
|
||||
await expect(page.locator('input[name="current_password"]')).toBeVisible();
|
||||
await expect(page.locator('input[name="new_password"]')).toBeVisible();
|
||||
await expect(page.locator('input[name="new_password_confirm"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('P2-21: 로그인 실패 lock', () => {
|
||||
test('잘못된 비밀번호 시 실패 카운트 메시지', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
// 3회 연속 실패하면 (실패 N/5회) 표시
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="login_id"]', 'tester_user');
|
||||
await page.fill('input[name="password"]', 'wrong_password');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(/\/login/, { timeout: 30000 });
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
// 3회 이후 실패 카운트 표시 확인
|
||||
await expect(page.locator('[role="alert"]').first()).toBeVisible({ timeout: 5000 });
|
||||
const alertText = await page.locator('[role="alert"]').first().textContent();
|
||||
// 실패 카운트 또는 잠금 메시지 확인
|
||||
expect(alertText?.includes('실패') || alertText?.includes('잠겼')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
64
e2e/phase3-order.spec.js
Normal file
@@ -0,0 +1,64 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { login } = require('./helpers/auth');
|
||||
|
||||
async function loginAsAdmin(page) {
|
||||
await login(page, 'admin');
|
||||
await page.locator('input[name="lg_idx"]').first().check();
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
|
||||
}
|
||||
|
||||
test.describe('P3: 발주 관리', () => {
|
||||
test('발주 현황 접근', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/bag-orders');
|
||||
await expect(page).toHaveURL(/\/admin\/bag-orders/);
|
||||
});
|
||||
|
||||
test('발주 등록 폼', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/bag-orders/create');
|
||||
await expect(page.locator('input[name="bo_order_date"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('기간 필터 조회', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/bag-orders?start_date=2026-01-01&end_date=2026-12-31');
|
||||
await expect(page).toHaveURL(/start_date/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('P3: 입고 관리', () => {
|
||||
test('입고 현황 접근', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/bag-receivings');
|
||||
await expect(page).toHaveURL(/\/admin\/bag-receivings/);
|
||||
});
|
||||
|
||||
test('입고 처리 폼', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/bag-receivings/create');
|
||||
await expect(page.locator('select[name="br_bo_idx"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('P3: 재고 현황', () => {
|
||||
test('재고 현황 접근', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/bag-inventory');
|
||||
await expect(page).toHaveURL(/\/admin\/bag-inventory/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('P3: 지자체관리자 접근', () => {
|
||||
test('발주/입고/재고 접근 가능', async ({ page }) => {
|
||||
await login(page, 'local');
|
||||
await page.goto('/admin/bag-orders');
|
||||
await expect(page).toHaveURL(/\/admin\/bag-orders/);
|
||||
await page.goto('/admin/bag-receivings');
|
||||
await expect(page).toHaveURL(/\/admin\/bag-receivings/);
|
||||
await page.goto('/admin/bag-inventory');
|
||||
await expect(page).toHaveURL(/\/admin\/bag-inventory/);
|
||||
});
|
||||
});
|
||||
67
e2e/phase4-sales.spec.js
Normal file
@@ -0,0 +1,67 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { login } = require('./helpers/auth');
|
||||
|
||||
async function loginAsAdmin(page) {
|
||||
await login(page, 'admin');
|
||||
await page.locator('input[name="lg_idx"]').first().check();
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
|
||||
}
|
||||
|
||||
test.describe('P4: 주문 접수 관리', () => {
|
||||
test('주문 목록 접근', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/shop-orders');
|
||||
await expect(page).toHaveURL(/\/admin\/shop-orders/);
|
||||
});
|
||||
test('주문 접수 폼', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/shop-orders/create');
|
||||
await expect(page.locator('select[name="so_ds_idx"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('P4: 판매/반품 관리', () => {
|
||||
test('판매 목록 접근', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/bag-sales');
|
||||
await expect(page).toHaveURL(/\/admin\/bag-sales/);
|
||||
});
|
||||
test('판매 등록 폼', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/bag-sales/create');
|
||||
await expect(page.locator('select[name="bs_ds_idx"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('P4: 무료용 불출 관리', () => {
|
||||
test('불출 목록 접근', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/bag-issues');
|
||||
await expect(page).toHaveURL(/\/admin\/bag-issues/);
|
||||
});
|
||||
test('불출 처리 폼', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/bag-issues/create');
|
||||
await expect(page.locator('select[name="bi2_bag_code"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('P4: 지자체관리자 접근', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page, 'local');
|
||||
});
|
||||
test('주문 접수 접근 가능', async ({ page }) => {
|
||||
await page.goto('/admin/shop-orders');
|
||||
await expect(page).toHaveURL(/\/admin\/shop-orders/);
|
||||
});
|
||||
test('판매 접근 가능', async ({ page }) => {
|
||||
await page.goto('/admin/bag-sales');
|
||||
await expect(page).toHaveURL(/\/admin\/bag-sales/);
|
||||
});
|
||||
test('불출 접근 가능', async ({ page }) => {
|
||||
await page.goto('/admin/bag-issues');
|
||||
await expect(page).toHaveURL(/\/admin\/bag-issues/);
|
||||
});
|
||||
});
|
||||
57
e2e/phase5-reports.spec.js
Normal file
@@ -0,0 +1,57 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { login } = require('./helpers/auth');
|
||||
|
||||
async function loginAsAdmin(page) {
|
||||
await login(page, 'admin');
|
||||
await page.locator('input[name="lg_idx"]').first().check();
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 30000 });
|
||||
}
|
||||
|
||||
test.describe('P5: 판매 대장', () => {
|
||||
test('일자별 판매 대장 접근', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/reports/sales-ledger');
|
||||
await expect(page).toHaveURL(/\/admin\/reports\/sales-ledger/);
|
||||
});
|
||||
test('기간별 판매 대장', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/reports/sales-ledger?mode=period&start_date=2026-01-01&end_date=2026-12-31');
|
||||
await expect(page).toHaveURL(/mode=period/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('P5: 일계표', () => {
|
||||
test('일계표 접근', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/reports/daily-summary');
|
||||
await expect(page).toHaveURL(/\/admin\/reports\/daily-summary/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('P5: 기간별 판매현황', () => {
|
||||
test('기간별 판매현황 접근', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/reports/period-sales');
|
||||
await expect(page).toHaveURL(/\/admin\/reports\/period-sales/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('P5: 봉투 수불 현황', () => {
|
||||
test('수불 현황 접근', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/reports/supply-demand');
|
||||
await expect(page).toHaveURL(/\/admin\/reports\/supply-demand/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('P5: 지자체관리자 접근', () => {
|
||||
test('리포트 접근 가능', async ({ page }) => {
|
||||
await login(page, 'local');
|
||||
await page.goto('/admin/reports/sales-ledger');
|
||||
await expect(page).toHaveURL(/\/admin\/reports\/sales-ledger/);
|
||||
await page.goto('/admin/reports/daily-summary');
|
||||
await expect(page).toHaveURL(/\/admin\/reports\/daily-summary/);
|
||||
});
|
||||
});
|
||||
95
jobs.md
@@ -39,70 +39,70 @@
|
||||
| P2-04 | 지자체별 봉투 단가 조회 (기간별) | 중간 | P2-03 | **완료** |
|
||||
| P2-05 | 포장 단위 관리 (박스/팩/낱장) | 높음 | P2-01 | **완료** |
|
||||
| P2-06 | 포장 단위 조회 (기간별) | 중간 | P2-05 | **완료** |
|
||||
| P2-07 | 판매 대행소 관리 + 지자체 연결 | 중간 | — | 대기 |
|
||||
| P2-08 | 판매 대행소 조회 | 낮음 | P2-07 | 대기 |
|
||||
| P2-09 | 담당자 관리 (소속별 CRUD) | 중간 | — | 대기 |
|
||||
| P2-10 | 담당자 조회 / 인쇄 | 낮음 | P2-09 | 대기 |
|
||||
| P2-11 | 업체 관리 (협회/제작업체/회수업체) | 중간 | — | 대기 |
|
||||
| P2-12 | 업체 조회 / 인쇄 | 낮음 | P2-11 | 대기 |
|
||||
| P2-13 | 무료용 대상자 관리 (CRUD) | 중간 | — | 대기 |
|
||||
| P2-14 | 무료용 대상자 조회 / 인쇄 | 낮음 | P2-13 | 대기 |
|
||||
| P2-07 | 판매 대행소 관리 + 지자체 연결 | 중간 | — | **완료** |
|
||||
| P2-08 | 판매 대행소 조회 | 낮음 | P2-07 | **완료** |
|
||||
| P2-09 | 담당자 관리 (소속별 CRUD) | 중간 | — | **완료** |
|
||||
| P2-10 | 담당자 조회 / 인쇄 | 낮음 | P2-09 | **완료** |
|
||||
| P2-11 | 업체 관리 (협회/제작업체/회수업체) | 중간 | — | **완료** |
|
||||
| P2-12 | 업체 조회 / 인쇄 | 낮음 | P2-11 | **완료** |
|
||||
| P2-13 | 무료용 대상자 관리 (CRUD) | 중간 | — | **완료** |
|
||||
| P2-14 | 무료용 대상자 조회 / 인쇄 | 낮음 | P2-13 | **완료** |
|
||||
| P2-15 | 지정판매소 다조건 조회 + 엑셀 + 인쇄 | 중간 | — | 대기 |
|
||||
| P2-16 | 지정판매소 바코드 출력 | 낮음 | P2-15 | 대기 |
|
||||
| P2-17 | 지정판매소 지도 표시 | 낮음 | — | 대기 |
|
||||
| P2-18 | 지정판매소 현황 (신규/취소) | 낮음 | — | 대기 |
|
||||
| P2-19 | 지자체 수정/삭제 기능 | 중간 | — | 대기 |
|
||||
| P2-20 | PASSWORD 변경 기능 | 중간 | — | 대기 |
|
||||
| P2-21 | 로그인 5회 실패 lock | 중간 | — | 대기 |
|
||||
| P2-22 | 카카오 주소 검색 API 연동 | 중간 | — | 대기 |
|
||||
| P2-19 | 지자체 수정/삭제 기능 | 중간 | — | **완료** |
|
||||
| P2-20 | PASSWORD 변경 기능 | 중간 | — | **완료** |
|
||||
| P2-21 | 로그인 5회 실패 lock | 중간 | — | **완료** |
|
||||
| P2-22 | 카카오 주소 검색 API 연동 | 중간 | — | Phase 3+ |
|
||||
|
||||
### Phase 3 — 발주/입고/재고
|
||||
|
||||
| ID | 작업 | 우선순위 | 의존성 | 상태 |
|
||||
|----|------|---------|--------|------|
|
||||
| P3-01 | 발주 등록 (UUID + SHA-256 + 블록) | 높음 | P2-03, P2-05 | 대기 |
|
||||
| P3-02 | LOT 번호 생성 + PDF417 바코드 | 높음 | P3-01 | 대기 |
|
||||
| P3-03 | 발주 변경 (버전 관리, 해시 체인) | 높음 | P3-01 | 대기 |
|
||||
| P3-04 | 발주 삭제 (상태 변경) | 중간 | P3-01 | 대기 |
|
||||
| P3-05 | 발주 현황 리포트 | 중간 | P3-01 | 대기 |
|
||||
| P3-06 | 발주 입고 - 바코드 스캐너 연동 | 높음 | P3-02 | 대기 |
|
||||
| P3-07 | 발주 입고 - 스캐너 입고 처리 | 높음 | P3-06 | 대기 |
|
||||
| P3-08 | 일괄 입고 (LOT 단위) | 중간 | P3-01 | 대기 |
|
||||
| P3-09 | 입고 현황 리포트 | 중간 | P3-07 | 대기 |
|
||||
| P3-10 | 재고 조회 + 결재란 인쇄 | 높음 | P3-07 | 대기 |
|
||||
| P3-11 | 실사 선별 / 등록 / 조회 | 중간 | P3-10 | 대기 |
|
||||
| P3-01 | 발주 등록 (UUID + SHA-256 + LOT) | 높음 | P2-03, P2-05 | **완료** |
|
||||
| P3-02 | LOT 번호 생성 | 높음 | P3-01 | **완료** |
|
||||
| P3-03 | 발주 취소 (상태 변경) | 높음 | P3-01 | **완료** |
|
||||
| P3-04 | 발주 삭제 (상태 변경) | 중간 | P3-01 | **완료** |
|
||||
| P3-05 | 발주 현황 (기간/상태 필터) | 중간 | P3-01 | **완료** |
|
||||
| P3-06 | 입고 처리 (일괄) | 높음 | P3-01 | **완료** |
|
||||
| P3-07 | 입고 현황 (기간 필터) | 중간 | P3-06 | **완료** |
|
||||
| P3-08 | 재고 현황 조회 | 높음 | P3-06 | **완료** |
|
||||
| P3-09 | 바코드 스캐너 연동 | 중간 | P3-06 | Phase 6+ |
|
||||
| P3-10 | PDF417 바코드 생성 | 중간 | P3-01 | Phase 6+ |
|
||||
| P3-11 | 실사 선별 / 등록 / 조회 | 중간 | P3-08 | Phase 6+ |
|
||||
|
||||
### Phase 4 — 주문/판매/불출
|
||||
|
||||
| ID | 작업 | 우선순위 | 의존성 | 상태 |
|
||||
|----|------|---------|--------|------|
|
||||
| P4-01 | 전화 주문 접수 (자동완성 검색) | 높음 | P2-05, P3-10 | 대기 |
|
||||
| P4-02 | 주문 접수 관리 메인 화면 | 높음 | P4-01 | 대기 |
|
||||
| P4-03 | 주문 수정 / 취소 | 중간 | P4-01 | 대기 |
|
||||
| P4-04 | 지정판매소 판매 (바코드 스캔) | 높음 | P3-07, P4-01 | 대기 |
|
||||
| P4-05 | 지정판매소 판매 취소 | 중간 | P4-04 | 대기 |
|
||||
| P4-06 | 지정판매소 반품 | 중간 | P4-04 | 대기 |
|
||||
| P4-07 | 지정판매소 반품 취소 | 낮음 | P4-06 | 대기 |
|
||||
| P4-08 | 무료용 불출 현황 | 중간 | P3-10 | 대기 |
|
||||
| P4-09 | 무료용 불출 처리 (바코드 스캔) | 높음 | P3-10 | 대기 |
|
||||
| P4-10 | 무료용 불출 취소 | 중간 | P4-09 | 대기 |
|
||||
| P4-01 | 주문 접수 (판매소 선택, 품목) | 높음 | P2-05, P3-08 | **완료** |
|
||||
| P4-02 | 주문 접수 관리 (목록/필터) | 높음 | P4-01 | **완료** |
|
||||
| P4-03 | 주문 취소 | 중간 | P4-01 | **완료** |
|
||||
| P4-04 | 지정판매소 판매 | 높음 | P3-08 | **완료** |
|
||||
| P4-05 | 지정판매소 반품 | 중간 | P4-04 | **완료** |
|
||||
| P4-06 | 판매/반품 목록 (기간/유형 필터) | 중간 | P4-04 | **완료** |
|
||||
| P4-07 | 바코드 스캔 판매 | 낮음 | P4-04 | Phase 6+ |
|
||||
| P4-08 | 무료용 불출 현황 (기간 필터) | 중간 | P3-08 | **완료** |
|
||||
| P4-09 | 무료용 불출 처리 + 재고 감산 | 높음 | P3-08 | **완료** |
|
||||
| P4-10 | 무료용 불출 취소 + 재고 복원 | 중간 | P4-09 | **완료** |
|
||||
|
||||
### Phase 5 — 현황/리포트/수불
|
||||
|
||||
| ID | 작업 | 우선순위 | 의존성 | 상태 |
|
||||
|----|------|---------|--------|------|
|
||||
| P5-01 | 판매 대장 (일자별/기간별) | 높음 | P4-04 | 대기 |
|
||||
| P5-02 | 일계표 (일계 + 월간 누계) | 높음 | P4-04 | 대기 |
|
||||
| P5-03 | 기간별 판매현황 | 중간 | P4-04 | 대기 |
|
||||
| P5-04 | 년 판매 현황 (월별/분기별) | 중간 | P4-04 | 대기 |
|
||||
| P5-05 | 지정판매소별 판매현황 (수량/금액) | 중간 | P4-04 | 대기 |
|
||||
| P5-06 | 홈택스 세금계산서 엑셀 생성 | 높음 | P4-04 | 대기 |
|
||||
| P5-07 | 봉투 수불 현황 (전일재고/입고/출고/잔량) | 높음 | P3-10, P4-04 | 대기 |
|
||||
| P5-08 | 반품/파기 현황 | 중간 | P4-06 | 대기 |
|
||||
| P5-09 | 봉투 수급 계획 | 낮음 | P5-07 | 대기 |
|
||||
| P5-10 | LOT 수불 조회 | 중간 | P3-02 | 대기 |
|
||||
| P5-11 | 기타 입출고 | 낮음 | P3-10 | 대기 |
|
||||
| P5-12 | 봉투 스캔 현황 (횟수/위치) | 낮음 | — | 대기 |
|
||||
| P5-01 | 판매 대장 (일자별/기간별) | 높음 | P4-04 | **완료** |
|
||||
| P5-02 | 일계표 (일계 + 월간 누계) | 높음 | P4-04 | **완료** |
|
||||
| P5-03 | 기간별 판매현황 | 중간 | P4-04 | **완료** |
|
||||
| P5-04 | 년 판매 현황 (월별/분기별) | 중간 | P4-04 | Phase 6+ |
|
||||
| P5-05 | 지정판매소별 판매현황 (수량/금액) | 중간 | P4-04 | Phase 6+ |
|
||||
| P5-06 | 홈택스 세금계산서 엑셀 생성 | 높음 | P4-04 | Phase 6+ |
|
||||
| P5-07 | 봉투 수불 현황 (재고/입고/판매/불출) | 높음 | P3-08, P4-04 | **완료** |
|
||||
| P5-08 | 반품/파기 현황 | 중간 | P4-06 | Phase 6+ |
|
||||
| P5-09 | 봉투 수급 계획 | 낮음 | P5-07 | Phase 6+ |
|
||||
| P5-10 | LOT 수불 조회 | 중간 | P3-02 | Phase 6+ |
|
||||
| P5-11 | 기타 입출고 | 낮음 | P3-08 | Phase 6+ |
|
||||
| P5-12 | 봉투 스캔 현황 (횟수/위치) | 낮음 | — | Phase 6+ |
|
||||
|
||||
### Phase 6 — 모바일앱
|
||||
|
||||
@@ -154,6 +154,11 @@
|
||||
|
||||
### 2026-03-25
|
||||
|
||||
- **P5** Phase 5 판매대장/일계표/기간별현황/수불현황 리포트 (`f451f0f`)
|
||||
- **P4** Phase 4 주문접수/판매/반품/불출 관리 (`6e8bd84`)
|
||||
- **P3** Phase 3 발주/입고/재고 관리 (`d9d3ef4`)
|
||||
- **P2-19~21** 지자체 수정/삭제, 비밀번호 변경, 로그인 lock (`c2840a9`)
|
||||
- **P2-07~14** 판매대행소/담당자/업체/무료대상자 CRUD (`da132f0`)
|
||||
- **P2-05/06** 포장 단위 관리 CRUD + 이력 + 기간별 조회 (`acc9e47`)
|
||||
- **P2-03/04** 봉투 단가 관리 CRUD + 이력 + 기간별 조회 (`6949227`)
|
||||
- **P2-01/02** 기본코드 종류/세부코드 관리 CRUD (`41442c2`)
|
||||
|
||||
@@ -8,7 +8,7 @@ module.exports = defineConfig({
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: [['html', { open: 'never' }], ['list']],
|
||||
timeout: 30000,
|
||||
timeout: 60000,
|
||||
|
||||
use: {
|
||||
baseURL: 'http://localhost:8045',
|
||||
|
||||
BIN
screenshots/30_bag_price_create.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
screenshots/31_packaging_unit.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
screenshots/32_sales_agency.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
screenshots/33_sales_agency_create.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
screenshots/34_manager.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
screenshots/35_manager_create.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
screenshots/36_company.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
screenshots/37_company_create.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
screenshots/38_free_recipient.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
screenshots/39_free_recipient_create.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
screenshots/40_lg_edit.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
screenshots/41_password_change.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
screenshots/42_bag_orders.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
screenshots/43_bag_order_create.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
screenshots/44_bag_receivings.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
screenshots/45_bag_receiving_create.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
screenshots/46_bag_inventory.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
screenshots/47_shop_orders.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
screenshots/48_shop_order_create.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
screenshots/49_bag_sales.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
screenshots/50_bag_sale_create.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
screenshots/51_bag_issues.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
screenshots/52_bag_issue_create.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
screenshots/53_report_sales_ledger.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
screenshots/54_report_daily_summary.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
screenshots/55_report_period_sales.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
screenshots/56_report_supply_demand.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
7
writable/database/member_login_lock_add.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- ============================================
|
||||
-- 로그인 실패 잠금 컬럼 추가 (P2-21)
|
||||
-- ============================================
|
||||
|
||||
ALTER TABLE `member`
|
||||
ADD COLUMN `mb_login_fail_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '연속 로그인 실패 횟수' AFTER `mb_leavedate`,
|
||||
ADD COLUMN `mb_locked_until` DATETIME NULL DEFAULT NULL COMMENT '잠금 해제 시각' AFTER `mb_login_fail_count`;
|
||||
73
writable/database/order_tables.sql
Normal file
@@ -0,0 +1,73 @@
|
||||
-- ============================================
|
||||
-- 발주/입고 관리 테이블 (Phase 3)
|
||||
-- ============================================
|
||||
|
||||
-- 발주 (P3-01~04)
|
||||
CREATE TABLE IF NOT EXISTS `bag_order` (
|
||||
`bo_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`bo_uuid` CHAR(36) NOT NULL COMMENT 'UUID v4',
|
||||
`bo_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '발주 버전',
|
||||
`bo_lg_idx` INT UNSIGNED NOT NULL COMMENT '지자체 FK',
|
||||
`bo_gugun_code` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '구/군 코드',
|
||||
`bo_dong_code` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '동 코드',
|
||||
`bo_company_idx` INT UNSIGNED NULL COMMENT '제작업체 FK (company)',
|
||||
`bo_agency_idx` INT UNSIGNED NULL COMMENT '입고처(대행소) FK (sales_agency)',
|
||||
`bo_fee_rate` DECIMAL(5,2) NOT NULL DEFAULT 0 COMMENT '수수료율(%)',
|
||||
`bo_order_date` DATE NOT NULL COMMENT '발주일',
|
||||
`bo_lot_no` VARCHAR(50) NOT NULL DEFAULT '' COMMENT 'LOT 번호',
|
||||
`bo_hash` VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'SHA-256 해시',
|
||||
`bo_status` VARCHAR(10) NOT NULL DEFAULT 'normal' COMMENT 'normal/cancelled/deleted',
|
||||
`bo_orderer_idx` INT UNSIGNED NULL COMMENT '발주자 mb_idx',
|
||||
`bo_regdate` DATETIME NOT NULL,
|
||||
`bo_moddate` DATETIME NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`bo_idx`),
|
||||
UNIQUE KEY `uk_bo_uuid_ver` (`bo_uuid`, `bo_version`),
|
||||
KEY `idx_bo_lg_idx` (`bo_lg_idx`),
|
||||
KEY `idx_bo_status` (`bo_status`),
|
||||
KEY `idx_bo_order_date` (`bo_order_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='발주';
|
||||
|
||||
-- 발주 상세 (봉투 종류별 수량)
|
||||
CREATE TABLE IF NOT EXISTS `bag_order_item` (
|
||||
`boi_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`boi_bo_idx` INT UNSIGNED NOT NULL COMMENT 'bag_order FK',
|
||||
`boi_bag_code` VARCHAR(50) NOT NULL COMMENT '봉투코드(code_detail O)',
|
||||
`boi_bag_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`boi_unit_price` DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '발주 단가',
|
||||
`boi_qty_box` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '발주 박스 수',
|
||||
`boi_qty_sheet` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '낱장 환산 수량',
|
||||
`boi_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '금액(단가*낱장수)',
|
||||
PRIMARY KEY (`boi_idx`),
|
||||
KEY `idx_boi_bo_idx` (`boi_bo_idx`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='발주 상세';
|
||||
|
||||
-- 입고 (P3-06~09)
|
||||
CREATE TABLE IF NOT EXISTS `bag_receiving` (
|
||||
`br_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`br_bo_idx` INT UNSIGNED NOT NULL COMMENT 'bag_order FK',
|
||||
`br_lg_idx` INT UNSIGNED NOT NULL,
|
||||
`br_bag_code` VARCHAR(50) NOT NULL,
|
||||
`br_bag_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`br_qty_box` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '입고 박스 수',
|
||||
`br_qty_sheet` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '입고 낱장 수',
|
||||
`br_receive_date` DATE NOT NULL,
|
||||
`br_receiver_idx` INT UNSIGNED NULL COMMENT '인수자 mb_idx',
|
||||
`br_sender_name` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '인계자명',
|
||||
`br_type` VARCHAR(20) NOT NULL DEFAULT 'scanner' COMMENT 'scanner/batch',
|
||||
`br_regdate` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`br_idx`),
|
||||
KEY `idx_br_bo_idx` (`br_bo_idx`),
|
||||
KEY `idx_br_lg_idx` (`br_lg_idx`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='발주 입고';
|
||||
|
||||
-- 재고 (P3-10)
|
||||
CREATE TABLE IF NOT EXISTS `bag_inventory` (
|
||||
`bi_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`bi_lg_idx` INT UNSIGNED NOT NULL,
|
||||
`bi_bag_code` VARCHAR(50) NOT NULL,
|
||||
`bi_bag_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`bi_qty` INT NOT NULL DEFAULT 0 COMMENT '현재 재고(낱장)',
|
||||
`bi_updated_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`bi_idx`),
|
||||
UNIQUE KEY `uk_bi_lg_bag` (`bi_lg_idx`, `bi_bag_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='봉투 재고';
|
||||
81
writable/database/sales_tables.sql
Normal file
@@ -0,0 +1,81 @@
|
||||
-- ============================================
|
||||
-- 주문/판매/불출 관리 테이블 (Phase 4)
|
||||
-- ============================================
|
||||
|
||||
-- 주문 접수 (P4-01~03)
|
||||
CREATE TABLE IF NOT EXISTS `shop_order` (
|
||||
`so_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`so_lg_idx` INT UNSIGNED NOT NULL,
|
||||
`so_ds_idx` INT UNSIGNED NULL COMMENT '지정판매소 FK',
|
||||
`so_ds_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '판매소명(스냅샷)',
|
||||
`so_order_date` DATE NOT NULL COMMENT '접수일',
|
||||
`so_delivery_date` DATE NULL COMMENT '배달일',
|
||||
`so_payment_type` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '이체/가상계좌',
|
||||
`so_paid` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '입금여부 1=예',
|
||||
`so_received` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '수령여부 1=예',
|
||||
`so_total_qty` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`so_total_amount` DECIMAL(14,2) NOT NULL DEFAULT 0,
|
||||
`so_status` VARCHAR(10) NOT NULL DEFAULT 'normal' COMMENT 'normal/cancelled',
|
||||
`so_orderer_idx` INT UNSIGNED NULL,
|
||||
`so_regdate` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`so_idx`),
|
||||
KEY `idx_so_lg_idx` (`so_lg_idx`),
|
||||
KEY `idx_so_ds_idx` (`so_ds_idx`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='주문 접수';
|
||||
|
||||
-- 주문 상세
|
||||
CREATE TABLE IF NOT EXISTS `shop_order_item` (
|
||||
`soi_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`soi_so_idx` INT UNSIGNED NOT NULL,
|
||||
`soi_bag_code` VARCHAR(50) NOT NULL,
|
||||
`soi_bag_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`soi_unit_price` DECIMAL(12,2) NOT NULL DEFAULT 0,
|
||||
`soi_qty` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '접수량(낱장)',
|
||||
`soi_amount` DECIMAL(14,2) NOT NULL DEFAULT 0,
|
||||
`soi_box_count` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`soi_pack_count` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`soi_sheet_count` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`soi_idx`),
|
||||
KEY `idx_soi_so_idx` (`soi_so_idx`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='주문 상세';
|
||||
|
||||
-- 판매 (P4-04~07)
|
||||
CREATE TABLE IF NOT EXISTS `bag_sale` (
|
||||
`bs_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`bs_lg_idx` INT UNSIGNED NOT NULL,
|
||||
`bs_so_idx` INT UNSIGNED NULL COMMENT '주문 FK (NULL=직접판매)',
|
||||
`bs_ds_idx` INT UNSIGNED NULL COMMENT '지정판매소 FK',
|
||||
`bs_ds_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`bs_sale_date` DATE NOT NULL,
|
||||
`bs_bag_code` VARCHAR(50) NOT NULL,
|
||||
`bs_bag_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`bs_qty` INT NOT NULL DEFAULT 0 COMMENT '판매수량(낱장, 음수=반품)',
|
||||
`bs_unit_price` DECIMAL(12,2) NOT NULL DEFAULT 0,
|
||||
`bs_amount` DECIMAL(14,2) NOT NULL DEFAULT 0,
|
||||
`bs_type` VARCHAR(20) NOT NULL DEFAULT 'sale' COMMENT 'sale/return/cancel',
|
||||
`bs_regdate` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`bs_idx`),
|
||||
KEY `idx_bs_lg_idx` (`bs_lg_idx`),
|
||||
KEY `idx_bs_ds_idx` (`bs_ds_idx`),
|
||||
KEY `idx_bs_sale_date` (`bs_sale_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='판매/반품';
|
||||
|
||||
-- 불출 (P4-08~10)
|
||||
CREATE TABLE IF NOT EXISTS `bag_issue` (
|
||||
`bi2_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`bi2_lg_idx` INT UNSIGNED NOT NULL,
|
||||
`bi2_year` YEAR NOT NULL,
|
||||
`bi2_quarter` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '분기(1~4)',
|
||||
`bi2_issue_type` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '무료용/공공용',
|
||||
`bi2_issue_date` DATE NOT NULL,
|
||||
`bi2_dest_type` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '불출처 구분(동사무소 등)',
|
||||
`bi2_dest_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '불출처명',
|
||||
`bi2_bag_code` VARCHAR(50) NOT NULL,
|
||||
`bi2_bag_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`bi2_qty` INT NOT NULL DEFAULT 0 COMMENT '불출수량(낱장, 음수=취소)',
|
||||
`bi2_status` VARCHAR(10) NOT NULL DEFAULT 'normal' COMMENT 'normal/cancelled',
|
||||
`bi2_regdate` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`bi2_idx`),
|
||||
KEY `idx_bi2_lg_idx` (`bi2_lg_idx`),
|
||||
KEY `idx_bi2_date` (`bi2_issue_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='무료용 불출';
|
||||
262
writable/database/seed_realistic_data.sql
Normal file
@@ -0,0 +1,262 @@
|
||||
-- ================================================================
|
||||
-- 실제와 같은 시범 데이터 (대구 남구청 기준)
|
||||
-- 유저(member) 테이블 제외
|
||||
-- 실행 전: 기존 시드 데이터(local_government, code_kind 등) 적용 완료 필요
|
||||
-- ================================================================
|
||||
|
||||
-- lg_idx는 남구청 기준 (local_government_init_daegu.sql 기준 4번째 = lg_idx=4)
|
||||
-- 실제 lg_idx 값은 환경에 따라 다를 수 있으므로 변수 사용
|
||||
SET @LG_IDX = (SELECT lg_idx FROM local_government WHERE lg_code = '110204' LIMIT 1);
|
||||
|
||||
-- ================================================================
|
||||
-- 1. 지정판매소 (designated_shop) — 남구 실제 편의점/슈퍼 기반
|
||||
-- ================================================================
|
||||
INSERT INTO `designated_shop` (`ds_lg_idx`, `ds_shop_no`, `ds_name`, `ds_biz_no`, `ds_rep_name`, `ds_va_number`, `ds_zip`, `ds_addr`, `ds_addr_jibun`, `ds_tel`, `ds_rep_phone`, `ds_email`, `ds_gugun_code`, `ds_designated_at`, `ds_state`, `ds_regdate`) VALUES
|
||||
(@LG_IDX, 'DS-2024-001', 'CU 대명점', '502-12-34567', '김민수', 'VA001', '42472', '대구 남구 대명로 112', '대명동 123-4', '053-621-0001', '010-1111-0001', '', '110204', '2023-03-15', 1, NOW()),
|
||||
(@LG_IDX, 'DS-2024-002', 'GS25 앞산점', '502-23-45678', '박영희', 'VA002', '42473', '대구 남구 앞산순환로 55', '대명동 456-7', '053-621-0002', '010-1111-0002', '', '110204', '2023-05-20', 1, NOW()),
|
||||
(@LG_IDX, 'DS-2024-003', '세븐일레븐 남구청점', '502-34-56789', '이정훈', 'VA003', '42474', '대구 남구 대명로 215', '대명동 100-1', '053-621-0003', '010-1111-0003', '', '110204', '2023-07-01', 1, NOW()),
|
||||
(@LG_IDX, 'DS-2024-004', '봉덕슈퍼', '502-45-67890', '최순자', 'VA004', '42475', '대구 남구 봉덕로 88', '봉덕동 234-5', '053-621-0004', '010-1111-0004', '', '110204', '2022-01-10', 1, NOW()),
|
||||
(@LG_IDX, 'DS-2024-005', 'CU 이천점', '502-56-78901', '정대환', 'VA005', '42476', '대구 남구 이천로 40', '이천동 567-8', '053-621-0005', '010-1111-0005', '', '110204', '2024-01-15', 1, NOW()),
|
||||
(@LG_IDX, 'DS-2024-006', '미니스톱 대명역점', '502-67-89012', '한미영', 'VA006', '42477', '대구 남구 대명로 300', '대명동 789-0', '053-621-0006', '010-1111-0006', '', '110204', '2024-03-01', 1, NOW()),
|
||||
(@LG_IDX, 'DS-2024-007', 'GS25 남산점', '502-78-90123', '윤석진', 'VA007', '42478', '대구 남구 남산로 22', '대명동 111-2', '053-621-0007', '010-1111-0007', '', '110204', '2024-06-01', 1, NOW()),
|
||||
(@LG_IDX, 'DS-2024-008', '대구남구마트', '502-89-01234', '강은주', 'VA008', '42479', '대구 남구 두류로 150', '대명동 333-4', '053-621-0008', '010-1111-0008', '', '110204', '2022-09-01', 1, NOW());
|
||||
|
||||
-- ================================================================
|
||||
-- 2. 판매 대행소 (sales_agency)
|
||||
-- ================================================================
|
||||
INSERT INTO `sales_agency` (`sa_lg_idx`, `sa_name`, `sa_biz_no`, `sa_rep_name`, `sa_tel`, `sa_addr`, `sa_state`, `sa_regdate`) VALUES
|
||||
(@LG_IDX, '남구청소과', '502-82-00001', '김철수', '053-664-2111', '대구 남구 대명로 215', 1, NOW()),
|
||||
(@LG_IDX, '대구환경공단 남구', '502-82-00002', '이영수', '053-664-3000', '대구 남구 앞산순환로 100', 1, NOW()),
|
||||
(@LG_IDX, '남구봉투판매협회', '502-82-00003', '박지영', '053-664-4000', '대구 남구 봉덕로 50', 1, NOW());
|
||||
|
||||
-- ================================================================
|
||||
-- 3. 업체 (company) — 제작/협회/회수
|
||||
-- ================================================================
|
||||
INSERT INTO `company` (`cp_lg_idx`, `cp_type`, `cp_name`, `cp_biz_no`, `cp_rep_name`, `cp_tel`, `cp_addr`, `cp_state`, `cp_regdate`) VALUES
|
||||
(@LG_IDX, 'manufacturer', '(주)대구폴리머', '504-81-11111', '김제작', '053-555-1001', '대구 달서구 성서공단로 100', 1, NOW()),
|
||||
(@LG_IDX, 'manufacturer', '(주)영남포장', '504-81-22222', '박제조', '053-555-2002', '대구 달성군 화원읍 공단1로 50', 1, NOW()),
|
||||
(@LG_IDX, 'association', '대구종량제봉투협회', '504-82-33333', '이협회', '053-555-3003', '대구 중구 국채보상로 500', 1, NOW()),
|
||||
(@LG_IDX, 'collector', '남구자원회수센터', '502-81-44444', '최회수', '053-555-4004', '대구 남구 이천로 200', 1, NOW());
|
||||
|
||||
-- ================================================================
|
||||
-- 4. 담당자 (manager)
|
||||
-- ================================================================
|
||||
INSERT INTO `manager` (`mg_lg_idx`, `mg_name`, `mg_dept_code`, `mg_position_code`, `mg_tel`, `mg_phone`, `mg_email`, `mg_state`, `mg_regdate`) VALUES
|
||||
(@LG_IDX, '김청소', '1', '6', '053-664-2111', '010-2222-1111', '', 1, NOW()),
|
||||
(@LG_IDX, '이환경', '1', '5', '053-664-2112', '010-2222-2222', '', 1, NOW()),
|
||||
(@LG_IDX, '박봉투', '3', '1', '053-664-2113', '010-2222-3333', '', 1, NOW()),
|
||||
(@LG_IDX, '최관리', '2', '2', '053-664-2114', '010-2222-4444', '', 1, NOW()),
|
||||
(@LG_IDX, '정수불', '1', '3', '053-664-2115', '010-2222-5555', '', 1, NOW());
|
||||
|
||||
-- ================================================================
|
||||
-- 5. 무료용 대상자 (free_recipient)
|
||||
-- ================================================================
|
||||
INSERT INTO `free_recipient` (`fr_lg_idx`, `fr_type_code`, `fr_name`, `fr_phone`, `fr_addr`, `fr_dong_code`, `fr_note`, `fr_end_date`, `fr_state`, `fr_regdate`) VALUES
|
||||
(@LG_IDX, '10', '김복지', '010-3333-1111', '대구 남구 대명로 50-1', '11020401', '생보자 20L 30매/월', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10', '이수급', '010-3333-2222', '대구 남구 봉덕로 33-2', '11020402', '생보자 20L 30매/월', NULL, 1, NOW()),
|
||||
(@LG_IDX, '1', '남구보호센터', '053-664-8000', '대구 남구 앞산순환로 200', '11020403', '시설보호 50L 100매/월', NULL, 1, NOW()),
|
||||
(@LG_IDX, '3', '박유공', '010-3333-3333', '대구 남구 이천로 80', '11020404', '무공수훈자 20L 20매/월', '2026-12-31', 1, NOW()),
|
||||
(@LG_IDX, '4', '대명노인복지관', '053-664-9000', '대구 남구 대명로 150', '11020405', '복지시설 50L 200매/월', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10', '최기초', '010-3333-4444', '대구 남구 남산로 15', '11020401', '생보자 10L 30매/월', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10', '정보호', '010-3333-5555', '대구 남구 두류로 77', '11020402', '생보자 20L 30매/월', NULL, 1, NOW()),
|
||||
(@LG_IDX, '4', '봉덕종합사회복지관', '053-664-7000', '대구 남구 봉덕로 120', '11020403', '복지시설 30L 150매/월', NULL, 1, NOW());
|
||||
|
||||
-- ================================================================
|
||||
-- 6. 봉투 단가 (bag_price) — 남구 주요 봉투 현행 단가
|
||||
-- ================================================================
|
||||
INSERT INTO `bag_price` (`bp_lg_idx`, `bp_bag_code`, `bp_bag_name`, `bp_order_price`, `bp_wholesale`, `bp_consumer`, `bp_start_date`, `bp_end_date`, `bp_state`, `bp_regdate`) VALUES
|
||||
(@LG_IDX, '10112', '일반용 3L', 25.00, 60.00, 80.00, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10122', '일반용 5L', 35.00, 100.00, 130.00, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10132', '일반용 10L', 60.00, 190.00, 250.00, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10152', '일반용 20L', 100.00, 370.00, 490.00, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10162', '일반용 30L', 140.00, 540.00, 710.00, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10172', '일반용 50L', 210.00, 860.00, 1130.00, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10192', '일반용 75L', 300.00, 1280.00, 1690.00, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10202', '일반용 100L', 380.00, 1700.00, 2250.00, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '60102', '음식물 2L', 18.00, 50.00, 60.00, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '60132', '음식물 10L', 70.00, 210.00, 280.00, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '40152', '공동주택용 20L', 90.00, 350.00, 460.00, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '20172', '공공용 50L', 180.00, 700.00, 0.00, '2025-01-01', NULL, 1, NOW());
|
||||
|
||||
-- 이전 단가 (2024년, 만료)
|
||||
INSERT INTO `bag_price` (`bp_lg_idx`, `bp_bag_code`, `bp_bag_name`, `bp_order_price`, `bp_wholesale`, `bp_consumer`, `bp_start_date`, `bp_end_date`, `bp_state`, `bp_regdate`) VALUES
|
||||
(@LG_IDX, '10152', '일반용 20L', 95.00, 350.00, 460.00, '2024-01-01', '2024-12-31', 0, '2024-01-01'),
|
||||
(@LG_IDX, '10172', '일반용 50L', 200.00, 820.00, 1080.00, '2024-01-01', '2024-12-31', 0, '2024-01-01');
|
||||
|
||||
-- ================================================================
|
||||
-- 7. 포장 단위 (packaging_unit) — 주요 봉투
|
||||
-- ================================================================
|
||||
INSERT INTO `packaging_unit` (`pu_lg_idx`, `pu_bag_code`, `pu_bag_name`, `pu_box_per_pack`, `pu_pack_per_sheet`, `pu_total_per_box`, `pu_start_date`, `pu_end_date`, `pu_state`, `pu_regdate`) VALUES
|
||||
(@LG_IDX, '10112', '일반용 3L', 20, 50, 1000, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10122', '일반용 5L', 20, 50, 1000, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10132', '일반용 10L', 10, 50, 500, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10152', '일반용 20L', 10, 50, 500, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10162', '일반용 30L', 10, 30, 300, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10172', '일반용 50L', 10, 20, 200, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10192', '일반용 75L', 5, 20, 100, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '10202', '일반용 100L', 5, 10, 50, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '60102', '음식물 2L', 20, 50, 1000, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '60132', '음식물 10L', 10, 50, 500, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '40152', '공동주택용 20L', 10, 50, 500, '2025-01-01', NULL, 1, NOW()),
|
||||
(@LG_IDX, '20172', '공공용 50L', 10, 20, 200, '2025-01-01', NULL, 1, NOW());
|
||||
|
||||
-- ================================================================
|
||||
-- 8. 발주 (bag_order + bag_order_item) — 2025~2026 분기별 발주
|
||||
-- ================================================================
|
||||
SET @CO1 = (SELECT cp_idx FROM company WHERE cp_lg_idx = @LG_IDX AND cp_type = 'manufacturer' LIMIT 1);
|
||||
SET @AG1 = (SELECT sa_idx FROM sales_agency WHERE sa_lg_idx = @LG_IDX LIMIT 1);
|
||||
|
||||
-- 2025 Q1 발주
|
||||
INSERT INTO `bag_order` (`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_regdate`) VALUES
|
||||
(UUID(), 1, @LG_IDX, '110204', '', @CO1, @AG1, 3.50, '2025-01-15', 'LOT-2025-Q1-001', SHA2(UUID(), 256), 'normal', '2025-01-15 09:00:00');
|
||||
SET @BO1 = LAST_INSERT_ID();
|
||||
INSERT INTO `bag_order_item` (`boi_bo_idx`, `boi_bag_code`, `boi_bag_name`, `boi_unit_price`, `boi_qty_box`, `boi_qty_sheet`, `boi_amount`) VALUES
|
||||
(@BO1, '10152', '일반용 20L', 100.00, 200, 100000, 10000000.00),
|
||||
(@BO1, '10172', '일반용 50L', 210.00, 80, 16000, 3360000.00),
|
||||
(@BO1, '10132', '일반용 10L', 60.00, 100, 50000, 3000000.00),
|
||||
(@BO1, '60102', '음식물 2L', 18.00, 150, 150000, 2700000.00);
|
||||
|
||||
-- 2025 Q2 발주
|
||||
INSERT INTO `bag_order` (`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_regdate`) VALUES
|
||||
(UUID(), 1, @LG_IDX, '110204', '', @CO1, @AG1, 3.50, '2025-04-10', 'LOT-2025-Q2-001', SHA2(UUID(), 256), 'normal', '2025-04-10 09:00:00');
|
||||
SET @BO2 = LAST_INSERT_ID();
|
||||
INSERT INTO `bag_order_item` (`boi_bo_idx`, `boi_bag_code`, `boi_bag_name`, `boi_unit_price`, `boi_qty_box`, `boi_qty_sheet`, `boi_amount`) VALUES
|
||||
(@BO2, '10152', '일반용 20L', 100.00, 250, 125000, 12500000.00),
|
||||
(@BO2, '10172', '일반용 50L', 210.00, 100, 20000, 4200000.00),
|
||||
(@BO2, '10162', '일반용 30L', 140.00, 60, 18000, 2520000.00);
|
||||
|
||||
-- 2025 Q3 발주
|
||||
INSERT INTO `bag_order` (`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_regdate`) VALUES
|
||||
(UUID(), 1, @LG_IDX, '110204', '', @CO1, @AG1, 3.50, '2025-07-15', 'LOT-2025-Q3-001', SHA2(UUID(), 256), 'normal', '2025-07-15 09:00:00');
|
||||
SET @BO3 = LAST_INSERT_ID();
|
||||
INSERT INTO `bag_order_item` (`boi_bo_idx`, `boi_bag_code`, `boi_bag_name`, `boi_unit_price`, `boi_qty_box`, `boi_qty_sheet`, `boi_amount`) VALUES
|
||||
(@BO3, '10152', '일반용 20L', 100.00, 220, 110000, 11000000.00),
|
||||
(@BO3, '10132', '일반용 10L', 60.00, 120, 60000, 3600000.00),
|
||||
(@BO3, '60132', '음식물 10L', 70.00, 80, 40000, 2800000.00);
|
||||
|
||||
-- 2026 Q1 발주 (최근)
|
||||
INSERT INTO `bag_order` (`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_regdate`) VALUES
|
||||
(UUID(), 1, @LG_IDX, '110204', '', @CO1, @AG1, 3.50, '2026-01-20', 'LOT-2026-Q1-001', SHA2(UUID(), 256), 'normal', '2026-01-20 09:00:00');
|
||||
SET @BO4 = LAST_INSERT_ID();
|
||||
INSERT INTO `bag_order_item` (`boi_bo_idx`, `boi_bag_code`, `boi_bag_name`, `boi_unit_price`, `boi_qty_box`, `boi_qty_sheet`, `boi_amount`) VALUES
|
||||
(@BO4, '10152', '일반용 20L', 100.00, 300, 150000, 15000000.00),
|
||||
(@BO4, '10172', '일반용 50L', 210.00, 120, 24000, 5040000.00),
|
||||
(@BO4, '10132', '일반용 10L', 60.00, 150, 75000, 4500000.00),
|
||||
(@BO4, '60102', '음식물 2L', 18.00, 200, 200000, 3600000.00),
|
||||
(@BO4, '10162', '일반용 30L', 140.00, 50, 15000, 2100000.00);
|
||||
|
||||
-- 취소된 발주 1건
|
||||
INSERT INTO `bag_order` (`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_regdate`) VALUES
|
||||
(UUID(), 1, @LG_IDX, '110204', '', @CO1, @AG1, 3.50, '2025-10-05', 'LOT-2025-Q4-CANCEL', SHA2(UUID(), 256), 'cancelled', '2025-10-05 09:00:00');
|
||||
|
||||
-- ================================================================
|
||||
-- 9. 입고 (bag_receiving) — 발주에 대응하는 입고
|
||||
-- ================================================================
|
||||
INSERT INTO `bag_receiving` (`br_bo_idx`, `br_lg_idx`, `br_bag_code`, `br_bag_name`, `br_qty_box`, `br_qty_sheet`, `br_receive_date`, `br_sender_name`, `br_type`, `br_regdate`) VALUES
|
||||
(@BO1, @LG_IDX, '10152', '일반용 20L', 200, 100000, '2025-02-01', '(주)대구폴리머', 'scanner', '2025-02-01 10:00:00'),
|
||||
(@BO1, @LG_IDX, '10172', '일반용 50L', 80, 16000, '2025-02-01', '(주)대구폴리머', 'scanner', '2025-02-01 10:30:00'),
|
||||
(@BO1, @LG_IDX, '10132', '일반용 10L', 100, 50000, '2025-02-03', '(주)대구폴리머', 'scanner', '2025-02-03 09:00:00'),
|
||||
(@BO1, @LG_IDX, '60102', '음식물 2L', 150, 150000, '2025-02-03', '(주)대구폴리머', 'scanner', '2025-02-03 09:30:00'),
|
||||
(@BO2, @LG_IDX, '10152', '일반용 20L', 250, 125000, '2025-04-25', '(주)대구폴리머', 'scanner', '2025-04-25 10:00:00'),
|
||||
(@BO2, @LG_IDX, '10172', '일반용 50L', 100, 20000, '2025-04-25', '(주)대구폴리머', 'scanner', '2025-04-25 10:30:00'),
|
||||
(@BO2, @LG_IDX, '10162', '일반용 30L', 60, 18000, '2025-04-28', '(주)영남포장', 'scanner', '2025-04-28 09:00:00'),
|
||||
(@BO3, @LG_IDX, '10152', '일반용 20L', 220, 110000, '2025-08-01', '(주)대구폴리머', 'scanner', '2025-08-01 10:00:00'),
|
||||
(@BO3, @LG_IDX, '10132', '일반용 10L', 120, 60000, '2025-08-01', '(주)대구폴리머', 'scanner', '2025-08-01 10:30:00'),
|
||||
(@BO3, @LG_IDX, '60132', '음식물 10L', 80, 40000, '2025-08-05', '(주)영남포장', 'scanner', '2025-08-05 09:00:00'),
|
||||
(@BO4, @LG_IDX, '10152', '일반용 20L', 300, 150000, '2026-02-10', '(주)대구폴리머', 'scanner', '2026-02-10 10:00:00'),
|
||||
(@BO4, @LG_IDX, '10172', '일반용 50L', 120, 24000, '2026-02-10', '(주)대구폴리머', 'scanner', '2026-02-10 10:30:00'),
|
||||
(@BO4, @LG_IDX, '10132', '일반용 10L', 150, 75000, '2026-02-12', '(주)대구폴리머', 'scanner', '2026-02-12 09:00:00'),
|
||||
(@BO4, @LG_IDX, '60102', '음식물 2L', 200, 200000, '2026-02-12', '(주)대구폴리머', 'scanner', '2026-02-12 09:30:00'),
|
||||
(@BO4, @LG_IDX, '10162', '일반용 30L', 50, 15000, '2026-02-15', '(주)영남포장', 'scanner', '2026-02-15 09:00:00');
|
||||
|
||||
-- ================================================================
|
||||
-- 10. 재고 (bag_inventory) — 현재 재고 현황
|
||||
-- ================================================================
|
||||
INSERT INTO `bag_inventory` (`bi_lg_idx`, `bi_bag_code`, `bi_bag_name`, `bi_qty`, `bi_updated_at`) VALUES
|
||||
(@LG_IDX, '10112', '일반용 3L', 45200, NOW()),
|
||||
(@LG_IDX, '10122', '일반용 5L', 38500, NOW()),
|
||||
(@LG_IDX, '10132', '일반용 10L', 72300, NOW()),
|
||||
(@LG_IDX, '10152', '일반용 20L', 185400, NOW()),
|
||||
(@LG_IDX, '10162', '일반용 30L', 21800, NOW()),
|
||||
(@LG_IDX, '10172', '일반용 50L', 28600, NOW()),
|
||||
(@LG_IDX, '10192', '일반용 75L', 5200, NOW()),
|
||||
(@LG_IDX, '10202', '일반용 100L', 2100, NOW()),
|
||||
(@LG_IDX, '20172', '공공용 50L', 8400, NOW()),
|
||||
(@LG_IDX, '40152', '공동주택용 20L', 32100, NOW()),
|
||||
(@LG_IDX, '60102', '음식물 2L', 128700, NOW()),
|
||||
(@LG_IDX, '60132', '음식물 10L', 24500, NOW());
|
||||
|
||||
-- ================================================================
|
||||
-- 11. 주문 접수 (shop_order + shop_order_item)
|
||||
-- ================================================================
|
||||
SET @DS1 = (SELECT ds_idx FROM designated_shop WHERE ds_lg_idx = @LG_IDX AND ds_shop_no = 'DS-2024-001' LIMIT 1);
|
||||
SET @DS2 = (SELECT ds_idx FROM designated_shop WHERE ds_lg_idx = @LG_IDX AND ds_shop_no = 'DS-2024-002' LIMIT 1);
|
||||
SET @DS3 = (SELECT ds_idx FROM designated_shop WHERE ds_lg_idx = @LG_IDX AND ds_shop_no = 'DS-2024-003' LIMIT 1);
|
||||
SET @DS4 = (SELECT ds_idx FROM designated_shop WHERE ds_lg_idx = @LG_IDX AND ds_shop_no = 'DS-2024-004' LIMIT 1);
|
||||
|
||||
INSERT INTO `shop_order` (`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_regdate`) VALUES
|
||||
(@LG_IDX, @DS1, 'CU 대명점', '2026-03-01', '2026-03-05', '후불', 1, 1, 500, 245000, 'normal', '2026-03-01 09:00:00'),
|
||||
(@LG_IDX, @DS2, 'GS25 앞산점', '2026-03-03', '2026-03-07', '후불', 1, 1, 300, 147000, 'normal', '2026-03-03 10:00:00'),
|
||||
(@LG_IDX, @DS3, '세븐일레븐 남구청점', '2026-03-10', '2026-03-14', '선불', 1, 1, 800, 392000, 'normal', '2026-03-10 09:30:00'),
|
||||
(@LG_IDX, @DS4, '봉덕슈퍼', '2026-03-15', '2026-03-18', '후불', 0, 0, 200, 98000, 'normal', '2026-03-15 11:00:00'),
|
||||
(@LG_IDX, @DS1, 'CU 대명점', '2026-03-20', '2026-03-24', '후불', 0, 0, 600, 294000, 'normal', '2026-03-20 09:00:00'),
|
||||
(@LG_IDX, @DS2, 'GS25 앞산점', '2026-02-15', '2026-02-18', '후불', 1, 1, 400, 196000, 'normal', '2026-02-15 10:00:00'),
|
||||
(@LG_IDX, @DS3, '세븐일레븐 남구청점', '2026-02-20', NULL, '선불', 0, 0, 100, 49000, 'cancelled', '2026-02-20 14:00:00');
|
||||
|
||||
-- 주문 품목
|
||||
SET @SO1 = (SELECT so_idx FROM shop_order WHERE so_lg_idx = @LG_IDX ORDER BY so_idx ASC LIMIT 1);
|
||||
INSERT INTO `shop_order_item` (`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`) VALUES
|
||||
(@SO1, '10152', '일반용 20L', 490.00, 300, 147000, 0, 6, 300),
|
||||
(@SO1, '10132', '일반용 10L', 250.00, 200, 50000, 0, 4, 200);
|
||||
SET @SO2 = @SO1 + 1;
|
||||
INSERT INTO `shop_order_item` (`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`) VALUES
|
||||
(@SO2, '10152', '일반용 20L', 490.00, 300, 147000, 0, 6, 300);
|
||||
|
||||
-- ================================================================
|
||||
-- 12. 판매/반품 (bag_sale) — 2026년 1~3월 판매 데이터
|
||||
-- ================================================================
|
||||
INSERT INTO `bag_sale` (`bs_lg_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`) VALUES
|
||||
-- 3월
|
||||
(@LG_IDX, @DS1, 'CU 대명점', '2026-03-02', '10152', '일반용 20L', 150, 490, 73500, 'sale', '2026-03-02 10:00:00'),
|
||||
(@LG_IDX, @DS1, 'CU 대명점', '2026-03-02', '10132', '일반용 10L', 100, 250, 25000, 'sale', '2026-03-02 10:05:00'),
|
||||
(@LG_IDX, @DS2, 'GS25 앞산점', '2026-03-05', '10152', '일반용 20L', 200, 490, 98000, 'sale', '2026-03-05 11:00:00'),
|
||||
(@LG_IDX, @DS3, '세븐일레븐 남구청점', '2026-03-08', '10172', '일반용 50L', 80, 1130, 90400, 'sale', '2026-03-08 09:30:00'),
|
||||
(@LG_IDX, @DS3, '세븐일레븐 남구청점', '2026-03-08', '10152', '일반용 20L', 300, 490, 147000, 'sale', '2026-03-08 09:35:00'),
|
||||
(@LG_IDX, @DS4, '봉덕슈퍼', '2026-03-12', '10132', '일반용 10L', 200, 250, 50000, 'sale', '2026-03-12 10:00:00'),
|
||||
(@LG_IDX, @DS4, '봉덕슈퍼', '2026-03-12', '60102', '음식물 2L', 300, 60, 18000, 'sale', '2026-03-12 10:05:00'),
|
||||
(@LG_IDX, @DS1, 'CU 대명점', '2026-03-18', '10152', '일반용 20L', 250, 490, 122500, 'sale', '2026-03-18 10:00:00'),
|
||||
(@LG_IDX, @DS2, 'GS25 앞산점', '2026-03-22', '10162', '일반용 30L', 100, 710, 71000, 'sale', '2026-03-22 11:30:00'),
|
||||
(@LG_IDX, @DS3, '세븐일레븐 남구청점', '2026-03-25', '10152', '일반용 20L', 400, 490, 196000, 'sale', '2026-03-25 09:00:00'),
|
||||
-- 2월
|
||||
(@LG_IDX, @DS1, 'CU 대명점', '2026-02-05', '10152', '일반용 20L', 180, 490, 88200, 'sale', '2026-02-05 10:00:00'),
|
||||
(@LG_IDX, @DS2, 'GS25 앞산점', '2026-02-10', '10152', '일반용 20L', 220, 490, 107800, 'sale', '2026-02-10 11:00:00'),
|
||||
(@LG_IDX, @DS3, '세븐일레븐 남구청점', '2026-02-15', '10172', '일반용 50L', 60, 1130, 67800, 'sale', '2026-02-15 09:30:00'),
|
||||
(@LG_IDX, @DS4, '봉덕슈퍼', '2026-02-20', '60102', '음식물 2L', 500, 60, 30000, 'sale', '2026-02-20 10:00:00'),
|
||||
(@LG_IDX, @DS1, 'CU 대명점', '2026-02-25', '10132', '일반용 10L', 300, 250, 75000, 'sale', '2026-02-25 10:30:00'),
|
||||
-- 1월
|
||||
(@LG_IDX, @DS1, 'CU 대명점', '2026-01-08', '10152', '일반용 20L', 200, 490, 98000, 'sale', '2026-01-08 10:00:00'),
|
||||
(@LG_IDX, @DS2, 'GS25 앞산점', '2026-01-15', '10172', '일반용 50L', 50, 1130, 56500, 'sale', '2026-01-15 11:00:00'),
|
||||
(@LG_IDX, @DS3, '세븐일레븐 남구청점', '2026-01-22', '10152', '일반용 20L', 350, 490, 171500, 'sale', '2026-01-22 09:30:00'),
|
||||
(@LG_IDX, @DS4, '봉덕슈퍼', '2026-01-28', '10132', '일반용 10L', 150, 250, 37500, 'sale', '2026-01-28 10:00:00'),
|
||||
-- 반품 2건
|
||||
(@LG_IDX, @DS2, 'GS25 앞산점', '2026-03-10', '10152', '일반용 20L', 50, 490, 24500, 'return', '2026-03-10 14:00:00'),
|
||||
(@LG_IDX, @DS4, '봉덕슈퍼', '2026-02-28', '10132', '일반용 10L', 30, 250, 7500, 'return', '2026-02-28 15:00:00');
|
||||
|
||||
-- ================================================================
|
||||
-- 13. 무료용 불출 (bag_issue)
|
||||
-- ================================================================
|
||||
INSERT INTO `bag_issue` (`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`) VALUES
|
||||
(@LG_IDX, 2026, 1, '무료불출', '2026-01-10', '생보자', '김복지', '10152', '일반용 20L', 30, 'normal', '2026-01-10 09:00:00'),
|
||||
(@LG_IDX, 2026, 1, '무료불출', '2026-01-10', '생보자', '이수급', '10152', '일반용 20L', 30, 'normal', '2026-01-10 09:05:00'),
|
||||
(@LG_IDX, 2026, 1, '무료불출', '2026-01-10', '시설', '남구보호센터', '10172', '일반용 50L', 100, 'normal', '2026-01-10 09:10:00'),
|
||||
(@LG_IDX, 2026, 1, '무료불출', '2026-01-10', '수훈자', '박유공', '10152', '일반용 20L', 20, 'normal', '2026-01-10 09:15:00'),
|
||||
(@LG_IDX, 2026, 1, '무료불출', '2026-01-10', '시설', '대명노인복지관', '10172', '일반용 50L', 200, 'normal', '2026-01-10 09:20:00'),
|
||||
(@LG_IDX, 2026, 1, '무료불출', '2026-02-10', '생보자', '김복지', '10152', '일반용 20L', 30, 'normal', '2026-02-10 09:00:00'),
|
||||
(@LG_IDX, 2026, 1, '무료불출', '2026-02-10', '생보자', '이수급', '10152', '일반용 20L', 30, 'normal', '2026-02-10 09:05:00'),
|
||||
(@LG_IDX, 2026, 1, '무료불출', '2026-02-10', '생보자', '최기초', '10132', '일반용 10L', 30, 'normal', '2026-02-10 09:10:00'),
|
||||
(@LG_IDX, 2026, 1, '무료불출', '2026-02-10', '시설', '봉덕종합사회복지관','10162','일반용 30L', 150, 'normal', '2026-02-10 09:15:00'),
|
||||
(@LG_IDX, 2026, 1, '무료불출', '2026-03-10', '생보자', '김복지', '10152', '일반용 20L', 30, 'normal', '2026-03-10 09:00:00'),
|
||||
(@LG_IDX, 2026, 1, '무료불출', '2026-03-10', '생보자', '정보호', '10152', '일반용 20L', 30, 'normal', '2026-03-10 09:05:00'),
|
||||
(@LG_IDX, 2026, 1, '무료불출', '2026-03-10', '시설', '남구보호센터', '10172', '일반용 50L', 100, 'normal', '2026-03-10 09:10:00'),
|
||||
-- 취소 1건
|
||||
(@LG_IDX, 2026, 1, '무료불출', '2026-03-05', '생보자', '이수급', '10152', '일반용 20L', 30, 'cancelled', '2026-03-05 09:00:00');
|
||||