환경 초기화 및 벤더 리포지토리 스켈레톤 도입

This commit is contained in:
JiWoong Sul
2025-09-22 17:38:51 +09:00
commit 5c9de2594a
171 changed files with 13304 additions and 0 deletions

View File

@@ -0,0 +1,677 @@
# 간단 입·출고 + 결재 시스템 설계서 (최종 v4)
**버전:** 2025-09-18 16:22:30Z (UTC)
**요약:** 벤더 ↔ 창고 ↔ 고객사 간 물품 이동(입고/출고)을 관리하는 최소구성 시스템.
- 트랜잭션당 1개의 결재(1:1), **승인자 순서 기반의 순차 결재** 지원.
- **다음 승인자로 넘어가면 안 되는 상태**를 `approval_statuses.is_blocking_next`로 제어.
- 모든 테이블(타입/코드 테이블 포함)에 **공통 컬럼** 적용: `is_active`, `is_deleted`, `created_at`, `updated_at`.
- **벤더는 트랜잭션 헤더에 연결하지 않음**(벤더는 제품을 통해서만 추적).
- **customer_roles 제거**: 트랜잭션-고객은 역할 없이 다수 연결만 허용.
- 타입값은 **별도 테이블**로 분리하며 `*_code`/정렬순서 미사용, ID 기반 참조.
- 메뉴 접근은 `groups``group_menu_permissions`를 통해 제어되며, 모든 직원은 정확히 하나의 그룹에 속함.
---
## 0) 핵심 비즈니스 규칙
- 제품 1개는 반드시 1개의 벤더에 소속 (`products.vendor_id` 필수).
- **트랜잭션 1건당 결재 1건**(1:1, 소프트삭제 제외).
- 결재는 **승인자 순서(`approval_steps.step_order`)대로**만 진행.
- 각 단계 상태가 **blocking**이면 다음 단계로 이동 불가.
- 트랜잭션에는 **여러 고객사**를 연결할 수 있음(역할 없음).
- 모든 직원은 **그룹**에 속하며(`employees.group_id`), 그룹-메뉴 권한(`group_menu_permissions`)으로 메뉴별 CRUD 가능 여부가 결정됨.
- 고객사는 **유형**을 `is_partner`/`is_general` 플래그로 구분하며 둘 중 하나 이상이 true여야 함(기본: 일반 true, 파트너 false).
- 반복되는 결재 라인은 **결재 템플릿**으로 저장 후 호출하여 재사용 가능.
- 모든 삭제는 **소프트 삭제**(`is_deleted=true`)이며, 삭제 시 `is_active=false`로 내림.
---
## 1) 개념 ERD
```mermaid
erDiagram
vendors ||--o{ products : supplies
uoms ||--o{ products : measured_in
warehouses ||--o{ stock_transactions : occurs_in
transaction_types ||--o{ stock_transactions : typed_as
transaction_statuses ||--o{ stock_transactions : has_status
stock_transactions ||--o{ transaction_lines : has
products ||--o{ transaction_lines : item
stock_transactions ||--o{ transaction_customers : serves
customers ||--o{ transaction_customers : party
stock_transactions ||--|| approvals : has_one
approval_statuses ||--o{ approvals : overall_status
approvals ||--o{ approval_steps : has_sequence
approval_statuses ||--o{ approval_steps : step_status
approval_steps ||--o{ approval_histories : logs
approval_actions ||--o{ approval_histories : acted_as
approval_templates ||--o{ approval_template_steps : has_sequence
employees ||--o{ approvals : requested_by
employees ||--o{ approval_steps : assigned_to
employees ||--o{ approval_histories : actor
employees ||--o{ stock_transactions : created_by
employees ||--o{ approval_templates : authored
employees ||--o{ approval_template_steps : template_approver
groups ||--o{ employees : members
groups ||--o{ group_menu_permissions : controls
menus ||--o{ group_menu_permissions : target
zipcodes ||--o{ warehouses : located
zipcodes ||--o{ customers : addressed
```
---
## 2) 공통 컬럼 (모든 테이블 공통 적용)
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| note | 비고 | text | - | - | N | N | N | - |
| is_active | 사용여부 | boolean | - | true | Y | N | N | - |
| is_deleted | 삭제여부(소프트) | boolean | - | false | Y | N | N | - |
| created_at | 생성일시 | timestamp | - | now() | Y | N | N | - |
| updated_at | 변경일시 | timestamp | - | now() | Y | N | N | - |
> 모든 테이블(타입/코드 테이블 포함)에 위 4개 컬럼을 **명시적으로 포함**.
> `note`는 테이블별 메모/추가 설명을 저장하는 자유 텍스트 필드.
> `updated_at`은 UPDATE 시 자동 갱신(트리거/생성 컬럼 권장). 삭제 시 `is_deleted=true`, `is_active=false` 처리.
---
## 3) 테이블 정의
### 3.1 `vendors` (벤더)
| 영문테이블명 | 한글테이블명 |
|---|---|
| vendors | 벤더 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 벤더ID | bigint | - | identity | Y | Y | Y | - |
| vendor_code | 벤더코드 | varchar | 30 | - | Y | (부분유니크: is_deleted=false) | N | - |
| vendor_name | 벤더명 | varchar | 100 | - | Y | | | |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
---
### 3.2 `warehouses` (창고)
| 영문테이블명 | 한글테이블명 |
|---|---|
| warehouses | 창고 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 창고ID | bigint | - | identity | Y | Y | Y | - |
| warehouse_code | 창고코드 | varchar | 30 | - | Y | (부분유니크: is_deleted=false) | N | - |
| warehouse_name | 창고명 | varchar | 100 | - | Y | | | |
| zipcode | 우편번호 | varchar | 5 | - | N | | | zipcodes.zipcode |
| address_detail | 상세주소 | varchar | 200 | - | N | | | - |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
---
### 3.3 `customers` (고객사)
| 영문테이블명 | 한글테이블명 |
|---|---|
| customers | 고객사 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 고객사ID | bigint | - | identity | Y | Y | Y | - |
| customer_code | 고객사코드 | varchar | 30 | - | Y | (부분유니크: is_deleted=false) | N | - |
| customer_name | 고객사명 | varchar | 100 | - | Y | | | |
| is_partner | 파트너여부 | boolean | - | false | Y | | | - |
| is_general | 일반여부 | boolean | - | true | Y | | | - |
| email | 이메일 | varchar | 100 | - | N | | | - |
| mobile_no | 모바일번호 | varchar | 20 | - | N | | | - |
| zipcode | 우편번호 | varchar | 5 | - | N | | | zipcodes.zipcode |
| address_detail | 상세주소 | varchar | 200 | - | N | | | - |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
---
### 3.4 `employees` (사원)
| 영문테이블명 | 한글테이블명 |
|---|---|
| employees | 사원 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 사원ID | bigint | - | identity | Y | Y | Y | - |
| employee_no | 사번 | varchar | 30 | - | Y | (부분유니크: is_deleted=false) | N | - |
| employee_name | 성명 | varchar | 100 | - | Y | | | |
| email | 이메일 | varchar | 100 | - | N | Y | | |
| mobile_no | 모바일번호 | varchar | 20 | - | N | | | |
| group_id | 그룹ID | bigint | - | - | Y | | | groups.id |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
---
### 3.5 `zipcodes` (우편번호)
| 영문테이블명 | 한글테이블명 |
|---|---|
| zipcodes | 우편번호 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| zipcode | 우편번호 | varchar | 5 | - | Y | Y | Y | - |
| sido | 시도 | varchar | 50 | - | Y | | | - |
| sido_eng | 시도영문 | varchar | 100 | - | N | | | - |
| sigungu | 시군구 | varchar | 100 | - | Y | | | - |
| sigungu_eng | 시군구영문 | varchar | 100 | - | N | | | - |
| eupmyeon | 읍면 | varchar | 100 | - | N | | | - |
| eupmyeon_eng | 읍면영문 | varchar | 100 | - | N | | | - |
| road_code | 도로명코드 | varchar | 12 | - | Y | | | - |
| road_name | 도로명 | varchar | 200 | - | Y | | | - |
| road_name_eng | 도로명영문 | varchar | 200 | - | N | | | - |
| underground_flag | 지하여부 | varchar | 1 | 'N' | Y | | | - |
| building_main_no | 건물번호본번 | integer | - | 0 | Y | | | - |
| building_sub_no | 건물번호부번 | integer | - | 0 | N | | | - |
| building_mgmt_no | 건물관리번호 | varchar | 25 | - | Y | | | - |
| bulk_receiver | 다량배달처명 | varchar | 200 | - | N | | | - |
| sigungu_building_name | 시군구용건물명 | varchar | 200 | - | N | | | - |
| legal_dong_code | 법정동코드 | varchar | 10 | - | Y | | | - |
| legal_dong_name | 법정동명 | varchar | 100 | - | Y | | | - |
| ri_name | 리명 | varchar | 100 | - | N | | | - |
| admin_dong_name | 행정동명 | varchar | 100 | - | N | | | - |
| mountain_flag | 산여부 | varchar | 1 | 'N' | Y | | | - |
| land_main_no | 지번본번 | integer | - | 0 | Y | | | - |
| town_serial_no | 읍면동일련번호 | integer | - | 0 | N | | | - |
| land_sub_no | 지번부번 | integer | - | 0 | N | | | - |
| old_zipcode | 구우편번호 | varchar | 6 | - | N | | | - |
| zipcode_serial_no | 우편번호일련번호 | integer | - | 0 | Y | | | - |
| search_text | 검색텍스트 | text | - | - | N | | | - |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
> 도로명 주소 데이터와 매핑되는 5자리 우편번호 기준. `zipcode`가 PK이며 외부 데이터 동기화를 위한 `zipcode_serial_no`를 포함.
---
### 3.6 `menus` (메뉴)
| 영문테이블명 | 한글테이블명 |
|---|---|
| menus | 메뉴 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 메뉴ID | bigint | - | identity | Y | Y | Y | - |
| menu_code | 메뉴코드 | varchar | 50 | - | Y | (부분유니크: is_deleted=false) | N | - |
| menu_name | 메뉴명 | varchar | 100 | - | Y | | | |
| parent_menu_id | 상위메뉴ID | bigint | - | - | N | | | menus.id |
| route_path | 경로 | varchar | 255 | - | N | | | - |
| display_order | 표시순서 | integer | - | 0 | Y | | | - |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
> 메뉴는 계층 구조를 지원하며 `parent_menu_id`가 NULL이면 1차 메뉴로 간주.
---
### 3.8 `groups` (그룹)
| 영문테이블명 | 한글테이블명 |
|---|---|
| groups | 그룹 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 그룹ID | bigint | - | identity | Y | Y | Y | - |
| group_name | 그룹명 | varchar | 100 | - | Y | (부분유니크: is_deleted=false) | N | - |
| group_description | 그룹설명 | varchar | 255 | - | N | | | - |
| is_default | 기본그룹여부 | boolean | - | false | Y | | | |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
> `group_menu_permissions`를 통해 각 그룹별 메뉴 CRUD 권한을 정의하며, 사원은 `employees.group_id`로 그룹에 연결됨.
---
### 3.9 `group_menu_permissions` (그룹_메뉴_권한)
| 영문테이블명 | 한글테이블명 |
|---|---|
| group_menu_permissions | 그룹_메뉴_권한 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 메뉴그룹권한ID | bigint | - | identity | Y | Y | Y | - |
| group_id | 그룹ID | bigint | - | - | Y | (복합유니크: group_id, menu_id, is_deleted) | N | groups.id |
| menu_id | 메뉴ID | bigint | - | - | Y | (복합유니크: group_id, menu_id, is_deleted) | N | menus.id |
| can_create | 생성권한 | boolean | - | false | Y | | | - |
| can_read | 조회권한 | boolean | - | true | Y | | | - |
| can_update | 수정권한 | boolean | - | false | Y | | | - |
| can_delete | 삭제권한 | boolean | - | false | Y | | | - |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
> 각 메뉴에 대한 CRUD 권한을 그룹 단위로 정의하며, 권한 미설정 시 기본적으로 조회만 허용.
---
### 3.10 `uoms` (단위) — 타입 테이블
| 영문테이블명 | 한글테이블명 |
|---|---|
| uoms | 단위 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 단위ID | bigint | - | identity | Y | Y | Y | - |
| uom_name | 단위명 | varchar | 100 | - | Y | (선택) 부분유니크 | N | - |
| is_default | 기본여부 | boolean | - | false | Y | | | |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
> 예시 값: `EA`(기본 단위), `BOX`, `KG`, `LITER` 등.
---
### 3.11 `products` (제품)
| 영문테이블명 | 한글테이블명 |
|---|---|
| products | 제품 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 제품ID | bigint | - | identity | Y | Y | Y | - |
| product_code | 제품코드 | varchar | 30 | - | Y | (부분유니크: is_deleted=false) | N | - |
| product_name | 제품명 | varchar | 100 | - | Y | | | |
| vendor_id | 벤더ID | bigint | - | - | Y | | | vendors.id |
| uom_id | 단위ID | bigint | - | - | Y | | | uoms.id |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
---
### 3.12 `transaction_types` (입출고_유형) — 타입 테이블
| 영문테이블명 | 한글테이블명 |
|---|---|
| transaction_types | 입출고_유형 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 유형ID | bigint | - | identity | Y | Y | Y | - |
| type_name | 유형명 | varchar | 100 | - | Y | (선택) 부분유니크 | N | - |
| is_default | 기본여부 | boolean | - | false | Y | | | |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
> 예시 값: `입고`(is_default=true), `출고`.
---
### 3.13 `transaction_statuses` (트랜잭션_상태) — 타입 테이블
| 영문테이블명 | 한글테이블명 |
|---|---|
| transaction_statuses | 트랜잭션_상태 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 상태ID | bigint | - | identity | Y | Y | Y | - |
| status_name | 상태명 | varchar | 100 | - | Y | (선택) 부분유니크 | N | - |
| is_default | 기본여부 | boolean | - | false | Y | | | |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
> 예시 값: `정상`(is_default), `반품`, `폐기`.
---
### 3.14 `stock_transactions` (입출고_트랜잭션)
| 영문테이블명 | 한글테이블명 |
|---|---|
| stock_transactions | 입출고_트랜잭션 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 트랜잭션ID | bigint | - | identity | Y | Y | Y | - |
| transaction_no | 트랜잭션번호 | varchar | 30 | - | Y | (부분유니크: is_deleted=false) | N | - |
| transaction_type_id | 입출고유형ID | bigint | - | - | Y | | | transaction_types.id |
| transaction_status_id | 트랜잭션상태ID | bigint | - | - | Y | | | transaction_statuses.id |
| warehouse_id | 창고ID | bigint | - | - | Y | | | warehouses.id |
| transaction_date | 처리일자 | date | - | current_date | Y | | | - |
| created_by_id | 작성자ID | bigint | - | - | N | | | employees.id |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
> 주의: **벤더ID 없음**. 벤더 정보는 라인의 `product_id`가 가리키는 `products.vendor_id`로 파생.
---
### 3.15 `transaction_lines` (트랜잭션_라인)
| 영문테이블명 | 한글테이블명 |
|---|---|
| transaction_lines | 트랜잭션_라인 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 라인ID | bigint | - | identity | Y | Y | Y | - |
| transaction_id | 트랜잭션ID | bigint | - | - | Y | | | stock_transactions.id |
| line_no | 라인번호 | integer | - | 1 | Y | (복합유니크: transaction_id, line_no, is_deleted) | N | - |
| product_id | 제품ID | bigint | - | - | Y | | | products.id |
| quantity | 수량 | numeric | 20,6 | 0 | Y | | | - |
| unit_price | 단가 | numeric | 20,6 | 0 | N | | | - |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
---
### 3.16 `transaction_customers` (트랜잭션_고객사)
| 영문테이블명 | 한글테이블명 |
|---|---|
| transaction_customers | 트랜잭션_고객사 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 키 | bigint | - | identity | Y | Y | Y | - |
| transaction_id | 트랜잭션ID | bigint | - | - | Y | | | stock_transactions.id |
| customer_id | 고객사ID | bigint | - | - | Y | (복합유니크: transaction_id, customer_id, is_deleted) | N | customers.id |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
---
### 3.17 `approval_statuses` (결재_상태) — 타입 테이블
| 영문테이블명 | 한글테이블명 |
|---|---|
| approval_statuses | 결재_상태 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 상태ID | bigint | - | identity | Y | Y | Y | - |
| status_name | 상태명 | varchar | 100 | - | Y | (선택) 부분유니크 | N | - |
| is_default | 기본여부 | boolean | - | false | Y | | | |
| is_blocking_next | 차기이동차단 | boolean | - | true | Y | | | |
| is_terminal | 종결여부 | boolean | - | false | Y | | | |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
> 예시 값:
> - `대기`(`is_default=true`, `is_blocking_next=true`, `is_terminal=false`)
> - `진행중`(`is_blocking_next=true`, `is_terminal=false`)
> - `보류`(`is_blocking_next=true`, `is_terminal=false`)
> - `승인`(`is_blocking_next=false`, `is_terminal=false`)
> - `반려`(`is_blocking_next=true`, `is_terminal=true`).
---
### 3.18 `approval_actions` (결재_행위) — 타입 테이블
| 영문테이블명 | 한글테이블명 |
|---|---|
| approval_actions | 결재_행위 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 행위ID | bigint | - | identity | Y | Y | Y | - |
| action_name | 행위명 | varchar | 100 | - | Y | (선택) 부분유니크 | N | - |
| is_default | 기본여부 | boolean | - | false | Y | | | |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
> 예시 값: `approve`(승인), `reject`(반려), `comment`(코멘트).
---
### 3.19 `approvals` (결재)
| 영문테이블명 | 한글테이블명 |
|---|---|
| approvals | 결재 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 결재ID | bigint | - | identity | Y | Y | Y | - |
| transaction_id | 트랜잭션ID | bigint | - | - | Y | (부분유니크: is_deleted=false) | N | stock_transactions.id |
| approval_no | 결재번호 | varchar | 30 | - | Y | (부분유니크: is_deleted=false) | N | - |
| approval_status_id | 전체결재상태ID | bigint | - | - | Y | | | approval_statuses.id |
| current_step_id | 현재단계ID | bigint | - | - | N | | | approval_steps.id |
| requested_by_id | 상신자ID | bigint | - | - | Y | | | employees.id |
| requested_at | 상신일시 | timestamp | - | now() | Y | | | - |
| decided_at | 최종결정일시 | timestamp | - | - | N | | | - |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
---
### 3.20 `approval_steps` (결재_단계)
| 영문테이블명 | 한글테이블명 |
|---|---|
| approval_steps | 결재_단계 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 단계ID | bigint | - | identity | Y | Y | Y | - |
| approval_id | 결재ID | bigint | - | - | Y | | | approvals.id |
| step_order | 단계순서 | integer | - | 1 | Y | (복합유니크: approval_id, step_order, is_deleted) | N | - |
| approver_id | 승인자ID | bigint | - | - | Y | | | employees.id |
| step_status_id | 단계상태ID | bigint | - | - | Y | | | approval_statuses.id |
| assigned_at | 배정일시 | timestamp | - | now() | Y | | | - |
| decided_at | 결정일시 | timestamp | - | - | N | | | - |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
---
### 3.21 `approval_histories` (결재_승인이력)
| 영문테이블명 | 한글테이블명 |
|---|---|
| approval_histories | 결재_승인이력 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 이력ID | bigint | - | identity | Y | Y | Y | - |
| approval_id | 결재ID | bigint | - | - | Y | | | approvals.id |
| approval_step_id | 결재단계ID | bigint | - | - | Y | | | approval_steps.id |
| approver_id | 승인자ID | bigint | - | - | Y | | | employees.id |
| approval_action_id | 결재행위ID | bigint | - | - | Y | | | approval_actions.id |
| from_status_id | 변경전상태ID | bigint | - | - | N | | | approval_statuses.id |
| to_status_id | 변경후상태ID | bigint | - | - | Y | | | approval_statuses.id |
| action_at | 작업일시 | timestamp | - | now() | Y | | | - |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
---
### 3.22 `approval_templates` (결재_템플릿)
| 영문테이블명 | 한글테이블명 |
|---|---|
| approval_templates | 결재_템플릿 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 템플릿ID | bigint | - | identity | Y | Y | Y | - |
| template_code | 템플릿코드 | varchar | 30 | - | Y | (부분유니크: is_deleted=false) | N | - |
| template_name | 템플릿명 | varchar | 100 | - | Y | | | |
| description | 설명 | varchar | 255 | - | N | | | - |
| created_by_id | 작성자ID | bigint | - | - | Y | | | employees.id |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
---
### 3.23 `approval_template_steps` (결재_템플릿_단계)
| 영문테이블명 | 한글테이블명 |
|---|---|
| approval_template_steps | 결재_템플릿_단계 |
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|---|---|---|---|---|---|---|---|---|
| id | 템플릿단계ID | bigint | - | identity | Y | Y | Y | - |
| template_id | 템플릿ID | bigint | - | - | Y | (복합유니크: template_id, step_order, is_deleted) | N | approval_templates.id |
| step_order | 단계순서 | integer | - | 1 | Y | (복합유니크: template_id, step_order, is_deleted) | N | - |
| approver_id | 승인자ID | bigint | - | - | Y | | | employees.id |
| note | 비고 | text | - | - | N | | | - |
| is_active | 사용여부 | boolean | - | true | Y | | | |
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
> 템플릿 단계는 실제 결재 단계 생성 시 그대로 복제되며, `step_order` 순서대로 승인자가 배치됨.
---
## 4) FK 관계 (source → target)
- `menus.parent_menu_id``menus.id`
- `employees.group_id``groups.id`
- `group_menu_permissions.group_id``groups.id`
- `group_menu_permissions.menu_id``menus.id`
- `warehouses.zipcode``zipcodes.zipcode`
- `customers.zipcode``zipcodes.zipcode`
- `products.vendor_id``vendors.id`
- `products.uom_id``uoms.id`
- `stock_transactions.warehouse_id``warehouses.id`
- `stock_transactions.created_by_id``employees.id`
- `stock_transactions.transaction_type_id``transaction_types.id`
- `stock_transactions.transaction_status_id``transaction_statuses.id`
- `transaction_lines.transaction_id``stock_transactions.id`
- `transaction_lines.product_id``products.id`
- `transaction_customers.transaction_id``stock_transactions.id`
- `transaction_customers.customer_id``customers.id`
- `approvals.transaction_id``stock_transactions.id`
- `approvals.approval_status_id``approval_statuses.id`
- `approvals.current_step_id``approval_steps.id`
- `approvals.requested_by_id``employees.id`
- `approval_steps.approval_id``approvals.id`
- `approval_steps.approver_id``employees.id`
- `approval_steps.step_status_id``approval_statuses.id`
- `approval_histories.approval_id``approvals.id`
- `approval_histories.approval_step_id``approval_steps.id`
- `approval_histories.approver_id``employees.id`
- `approval_histories.approval_action_id``approval_actions.id`
- `approval_histories.from_status_id``approval_statuses.id`
- `approval_histories.to_status_id``approval_statuses.id`
- `approval_templates.created_by_id``employees.id`
- `approval_template_steps.template_id``approval_templates.id`
- `approval_template_steps.approver_id``employees.id`
---
## 5) 비즈니스/검증 규칙
- 제품 등록 시 `vendor_id` **필수**.
- 입고(`transaction_type_id`=입고) 트랜잭션의 공급자 정보는 **라인 제품의 벤더**로만 해석. (헤더에 벤더 금지)
- 출고 트랜잭션은 `transaction_customers` **최소 1건** 필요.
- 결재는 **트랜잭션당 1건**(미삭제 기준)만 허용.
- 단계 전이는 **현재 단계**에서만 수행 가능. blocking 상태에서는 차기 이동 불가.
- 수량/단가 음수 금지(CHECK).
- 그룹이 비활성(`is_active=false`) 또는 삭제되면 해당 그룹 권한/구성원은 즉시 무효 처리.
- 사원의 소속 그룹(`employees.group_id`)에서 해당 메뉴에 대한 `can_create|can_update|can_delete` 중 하나라도 true이면 그 동작을 수행할 수 있음.
---
## 6) 인덱스/유니크 권장
- 부분 유니크(또는 복합 유니크)로 소프트 삭제와 공존:
- `vendors(vendor_code)`, `warehouses(warehouse_code)`, `customers(customer_code)`, `employees(employee_no)`, `menus(menu_code)`, `groups(group_name)`, `zipcodes(zipcode)`, `products(product_code)`, `stock_transactions(transaction_no)`, `approvals(approval_no)`
- `group_menu_permissions(group_id, menu_id, is_deleted)`
- `approvals(transaction_id)` — 미삭제 조건에서 1:1 보장
- `transaction_lines(transaction_id, line_no, is_deleted)`
- `transaction_customers(transaction_id, customer_id, is_deleted)`
- FK 및 조회 인덱스: 모든 `*_id`, `updated_at`, `is_deleted`, `is_active`.
---
## 7) 에러 규격(예시)
- `400 BAD_REQUEST` — 필수 필드 누락, 형식 오류
- `409 CONFLICT` — 유니크 충돌(코드/번호/조합), **현재 단계 아님**
- `422 UNPROCESSABLE_ENTITY` — 비즈니스 규칙 위반(출고인데 고객 없음, blocking 상태에서 이동 등)
- `404 NOT_FOUND` — 리소스 없음 또는 삭제됨(`deleted=false` 기본 필터로 미노출)
---
## 8) 마이그레이션 가이드(요약)
1) `stock_transactions`에서 `vendor_id` 드롭.
2) `customer_roles` 테이블 및 관련 컬럼 드롭.
3) 모든 타입/코드 테이블에 공통 컬럼 4종 추가(미존재 시).
4) 부분 유니크 인덱스(`WHERE is_deleted=false`) 또는 `(컬럼, is_deleted)` 복합 유니크 구성.
5) 기존 결재 이력은 `approval_step_id` 매핑(없으면 1단계로 귀속).
6) `approval_statuses``is_blocking_next`, `is_terminal` 값 시드.
7) `menus`, `groups`, `group_menu_permissions` 신규 생성 및 기존 관리자 권한/사원-그룹 매핑을 `employees.group_id`로 이관.
8) `zipcodes` 테이블 생성 및 도로명 주소 기준 데이터 적재.
9) 모든 테이블에 `note`(text) 컬럼 추가 및 필요한 경우 기본값 NULL 유지.
---
## 9) 초기 시드 값(예시)
- `transaction_types`: [입고, 출고] (`is_default`: 입고)
- `transaction_statuses`: [초안, 상신, 승인, 반려, 완료] (`is_default`: 초안)
- `approval_statuses`: [대기(pending, default, blocking), 진행중(in_progress, blocking), 보류(on_hold, blocking), 승인(approved, !blocking), 반려(rejected, blocking+terminal)]
- `approval_actions`: [승인(approve), 반려(reject), 코멘트(comment)]
- `uoms`: [EA(기본), BOX, KG ...]
- `menus`: [대시보드, 입출고 관리, 결재 관리, 레포트 등] — 상위/하위 메뉴 구조 포함
- `groups`: [전사 관리자(기본), 창고 관리자, 결재 담당자]
- `group_menu_permissions`: 기본 그룹별 메뉴 권한(CRUD 플래그); 전사 관리자는 모든 메뉴 `can_*`=true, 역할별로 세분화 설정
- `zipcodes`: 행정안전부 도로명 주소 DB(5자리) 최신본을 기준으로 일괄 적재
---
## 10) 구현 팁
- `updated_at` 자동 갱신 트리거, 소프트 삭제 처리 트리거 권장.
- 낙관적 잠금(선택): `version`(int) + ETag.
- 병렬 결재 확장(선택): `approval_steps``group_no`, `approval_mode(all|any)` 도입.