feat(inventory): 재고 현황 요약/상세 플로우를 릴리스
- lib/features/inventory/summary 계층과 warehouse select 위젯을 추가해 목록/상세, 자동 새로고침, 필터, 상세 시트를 구현 - PermissionBootstrapper, scope 파서, 라우트 가드로 inventory.view 기반 권한 부여와 메뉴 노출을 통합(lib/core, lib/main.dart 등) - Inventory Summary API/QA/Audit 문서와 PR 템플릿, CHANGELOG를 신규 스펙과 검증 커맨드로 업데이트 - DTO 직렬화 의존성을 추가하고 Golden·Widget·단위 테스트를 작성했으며 flutter analyze / flutter test --coverage를 통과
This commit is contained in:
@@ -132,7 +132,7 @@ zipcodes ||--o{ customers : addressed
|
||||
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||
|
||||
> API 기본 응답(`GET /approval/templates`, `GET /approval/templates/{id}`)은 작성자 요약(`created_by { id, employee_id, name }`)을 항상 포함하며, `include=created_by` 없이도 반환된다.
|
||||
> API 기본 응답(`GET /approval-templates`, `GET /approval-templates/{id}`)은 작성자 요약(`created_by { id, employee_id, name }`)을 항상 포함하며, `include=created_by` 없이도 반환된다.
|
||||
|
||||
---
|
||||
|
||||
@@ -562,6 +562,7 @@ zipcodes ||--o{ customers : addressed
|
||||
| approval_step_id | 결재단계ID | bigint | - | - | Y | | | approval_steps.id |
|
||||
| approver_id | 승인자ID | bigint | - | - | Y | | | users.id |
|
||||
| approval_action_id | 결재행위ID | bigint | - | - | Y | | | approval_actions.id |
|
||||
| action_code | 행위코드 | varchar | 30 | - | Y | | | - |
|
||||
| from_status_id | 변경전상태ID | bigint | - | - | N | | | approval_statuses.id |
|
||||
| to_status_id | 변경후상태ID | bigint | - | - | Y | | | approval_statuses.id |
|
||||
| action_at | 작업일시 | timestamp | - | now() | Y | | | - |
|
||||
@@ -573,6 +574,8 @@ zipcodes ||--o{ customers : addressed
|
||||
|
||||
---
|
||||
|
||||
- `action_code`는 `submit`, `approve`, `reject`, `comment`, `recall`, `resubmit` 등 표준 문자열을 저장해 참조 행위 레코드가 없어도 이력 복원이 가능하도록 한다.
|
||||
|
||||
### 3.22 `approval_templates` (결재_템플릿)
|
||||
| 영문테이블명 | 한글테이블명 |
|
||||
|---|---|
|
||||
@@ -614,6 +617,61 @@ zipcodes ||--o{ customers : addressed
|
||||
|
||||
---
|
||||
|
||||
### 3.24 `inventory_balance_events_view` (재고_이벤트_뷰)
|
||||
| 영문테이블명 | 한글테이블명 |
|
||||
|---|---|
|
||||
| inventory_balance_events_view | 재고_이벤트_뷰 |
|
||||
|
||||
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| event_id | 이벤트ID | bigint | - | - | Y | Y | Y | transaction_lines.id |
|
||||
| transaction_id | 트랜잭션ID | bigint | - | - | Y | | | stock_transactions.id |
|
||||
| transaction_line_id | 트랜잭션라인ID | bigint | - | - | Y | | | transaction_lines.id |
|
||||
| transaction_no | 전표번호 | varchar | 40 | - | Y | | | - |
|
||||
| product_id | 제품ID | bigint | - | - | Y | | | products.id |
|
||||
| warehouse_id | 창고ID | bigint | - | - | Y | | | warehouses.id |
|
||||
| transaction_type_id | 트랜잭션타입ID | bigint | - | - | Y | | | transaction_types.id |
|
||||
| transaction_status_id | 트랜잭션상태ID | bigint | - | - | Y | | | transaction_statuses.id |
|
||||
| delta_quantity | 증감수량 | numeric | 20,6 | 0 | Y | | | - |
|
||||
| event_kind | 이벤트종류 | varchar | 30 | - | Y | | | - |
|
||||
| counterparty_name | 거래처요약 | varchar | 150 | - | N | | | - |
|
||||
| event_occurred_at | 이벤트일시 | timestamp | - | - | Y | | | - |
|
||||
| captured_at | 집계일시 | timestamp | - | now() | Y | | | - |
|
||||
|
||||
> 입고/출고/대여 라인을 `transaction_lines`에서 펼친 뷰다. `delta_quantity`는 입고/반납=양수, 출고/대여=음수 규칙을 따른다. `event_kind`는 `receipt`, `issue`, `rental_out`, `rental_return` 등 표준 문자열로 저장한다. 최신 변동 정렬을 위해 `event_occurred_at DESC`, `event_id DESC` 복합 인덱스를 생성하며, 뷰는 마테리얼라이즈드 형태로 5분마다 리프레시된다.
|
||||
|
||||
---
|
||||
|
||||
### 3.25 `inventory_balance_snapshots` (재고_집계_뷰)
|
||||
| 영문테이블명 | 한글테이블명 |
|
||||
|---|---|
|
||||
| inventory_balance_snapshots | 재고_집계_뷰 |
|
||||
|
||||
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| product_id | 제품ID | bigint | - | - | Y | Y | Y | products.id |
|
||||
| product_code | 제품코드 | varchar | 30 | - | Y | | | - |
|
||||
| product_name | 제품명 | varchar | 100 | - | Y | | | - |
|
||||
| vendor_id | 벤더ID | bigint | - | - | Y | | | vendors.id |
|
||||
| vendor_name | 벤더명 | varchar | 100 | - | Y | | | - |
|
||||
| total_quantity | 총재고수량 | numeric | 20,6 | 0 | Y | | | - |
|
||||
| warehouse_balances | 창고별요약 | jsonb | - | '[]'::jsonb | Y | | | - |
|
||||
| recent_event_id | 최근이벤트ID | bigint | - | - | N | | | inventory_balance_events_view.event_id |
|
||||
| recent_event_kind | 최근이벤트종류 | varchar | 30 | - | N | | | - |
|
||||
| recent_event_delta | 최근이벤트증감 | numeric | 20,6 | 0 | N | | | - |
|
||||
| recent_event_counterparty | 최근거래처 | varchar | 150 | - | N | | | - |
|
||||
| recent_event_warehouse_id | 최근창고ID | bigint | - | - | N | | | warehouses.id |
|
||||
| recent_event_warehouse_name | 최근창고명 | varchar | 100 | - | N | | | - |
|
||||
| recent_event_transaction_id | 최근전표ID | bigint | - | - | N | | | stock_transactions.id |
|
||||
| recent_event_transaction_no | 최근전표번호 | varchar | 40 | - | N | | | - |
|
||||
| recent_event_at | 최근이벤트일시 | timestamp | - | - | N | | | - |
|
||||
| updated_at | 변경일시 | timestamp | - | now() | Y | | | - |
|
||||
| refreshed_at | 리프레시일시 | timestamp | - | now() | Y | | | - |
|
||||
|
||||
> `inventory_balance_events_view`를 제품 단위로 집계한 마테뷰. `warehouse_balances`는 `[ { "warehouse_id": 1, "warehouse_code": "WH-001", "warehouse_name": "1센터", "quantity": 80 } ]` 형태의 배열 JSON을 저장한다. `recent_*` 필드는 최신 이벤트 스냅샷을 캐싱해 `/api/v1/inventory/summary` 응답과 동일 구조를 제공하며, `refreshed_at`은 뷰가 새로 고쳐진 시각(UTC)을 그대로 보존한다. `updated_at` 컬럼으로 증분 조회가 가능하도록 `updated_at DESC` 인덱스를 생성한다.
|
||||
|
||||
---
|
||||
|
||||
## 4) FK 관계 (source → target)
|
||||
- `menus.parent_menu_id` → `menus.id`
|
||||
- `users.group_id` → `groups.id`
|
||||
@@ -657,6 +715,7 @@ zipcodes ||--o{ customers : addressed
|
||||
- 수량/단가 음수 금지(CHECK).
|
||||
- 그룹이 비활성(`is_active=false`) 또는 삭제되면 해당 그룹 권한/구성원은 즉시 무효 처리.
|
||||
- 사용자의 소속 그룹(`users.group_id`)에서 해당 메뉴에 대한 `can_create|can_update|can_delete` 중 하나라도 true이면 그 동작을 수행할 수 있음.
|
||||
- 재고 현황 조회 API는 읽기 전용 권한 스코프 `inventory.view`를 요구하며, 스코프가 없는 사용자는 `/api/v1/inventory/**` 경로에서 403(`INVENTORY_SCOPE_REQUIRED`)을 받는다.
|
||||
- `users.employee_id`는 앞뒤 공백을 제거한 뒤 대소문자 구분 없이 중복 검증하며, 저장 시 대문자로 정규화한다.
|
||||
- 자기 정보 수정(`PATCH /users/me`)에서는 `phone`, `email`, `password`만 변경할 수 있고, `password` 변경 시 기존 비밀번호 검증이 필수다.
|
||||
- 비밀번호 재설정(관리자)은 8자 영문 대소문자+숫자 조합을 생성하고 이메일 발송 큐에 푸시한 뒤 `force_password_change=true`, `password_updated_at=now()`로 기록한다.
|
||||
@@ -672,6 +731,7 @@ zipcodes ||--o{ customers : addressed
|
||||
- `transaction_lines(transaction_id, line_no, is_deleted)`
|
||||
- `transaction_customers(transaction_id, customer_id, is_deleted)`
|
||||
- FK 및 조회 인덱스: 모든 `*_id`, `updated_at`, `is_deleted`, `is_active`.
|
||||
- 재고 집계 마테뷰 인덱스: `inventory_balance_events_view(event_occurred_at DESC, event_id DESC)`, `inventory_balance_snapshots(updated_at DESC)` 및 필요 시 `inventory_balance_snapshots`의 `warehouse_balances` JSONB GIN 인덱스.
|
||||
|
||||
---
|
||||
|
||||
@@ -714,4 +774,5 @@ zipcodes ||--o{ customers : addressed
|
||||
- `updated_at` 자동 갱신 트리거, 소프트 삭제 처리 트리거 권장.
|
||||
- 낙관적 잠금(선택): `version`(int) + ETag.
|
||||
- 병렬 결재 확장(선택): `approval_steps`에 `group_no`, `approval_mode(all|any)` 도입.
|
||||
- 감사 로그(`approval_audits`) 적재 시 `approval.audit.recorded` 이벤트를 Kafka(토픽 예: `approval_audit_events`)와 WebSocket 브로드캐스트로 발행한다. 이벤트 구성은 `{ event, version, emitted_at, request_id, audit_id, summary }` JSON으로 정의하며, 운영 환경별 엔드포인트는 `event_bus.kafka.*`, `event_bus.websocket.*` 설정으로 분리한다.
|
||||
- `/health` 응답의 `build_version`은 `config/default.toml`의 `[app].build_version`을 사용하며, `script/deploy_remote.sh`가 배포 아카이브 파일명에서 버전을 추출해 값을 주입한다.
|
||||
|
||||
Reference in New Issue
Block a user