# 간단 입·출고 + 결재 시스템 설계서 (최종 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). - 고객사는 담당자 이름(`contact_name`)을 별도 관리하며 고객 응답에 항상 포함. - 반복되는 결재 라인은 **결재 템플릿**으로 저장 후 호출하여 재사용 가능. - 모든 삭제는 **소프트 삭제**(`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 | | | | > `zipcodes` 테이블이 우편번호 마스터를 보유하며, 참조 측 테이블은 `zipcode_id` → `zipcodes.id` FK만 저장한다. 응답 시에는 FK를 통해 조회한 우편번호 요약 정보를 포함한다. --- ### 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_id | 우편번호ID | bigint | - | - | N | | | zipcodes.id | | 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 | | | | | contact_name | 담당자명 | varchar | 100 | - | N | | | - | | is_partner | 파트너여부 | boolean | - | false | Y | | | - | | is_general | 일반여부 | boolean | - | true | Y | | | - | | email | 이메일 | varchar | 100 | - | N | | | - | | mobile_no | 모바일번호 | varchar | 20 | - | N | | | - | | zipcode_id | 우편번호ID | bigint | - | - | N | | | zipcodes.id | | 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 | | | | > `contact_name`은 고객사의 대표 연락 창구로 사용하는 담당자 실명. 필수는 아니며 미입력 시 `null` 저장. > 고객/창고 모두 `zipcode_id`를 통해 `zipcodes.id`와 연결하며, API 응답은 FK가 가리키는 `zipcodes` 행에서 필요한 우편번호 메타 정보를 추출해 제공한다. --- ### 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 | |---|---|---|---|---|---|---|---|---| | id | 우편번호ID | bigint | - | identity | Y | Y | Y | - | | zipcode | 우편번호 | varchar | 5 | - | Y | | N | - | | 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자리 우편번호 기준. `id`는 내부용 서러겟 PK이며, `zipcode`는 동일 코드가 여러 주소 행에 등장할 수 있다. --- ### 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`로 파생. > 번호 발급: 서버가 `TRX-YYYYMMDDNNNN` 형식으로 `transaction_no`를 생성하며 클라이언트 입력을 허용하지 않는다. > 목록 조회는 `customer_id` 쿼리 파라미터를 지원해 특정 고객이 연결된 트랜잭션만 필터링할 수 있다. (2024-10 갱신) --- ### 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 | | | | > 번호 발급: 서버가 `APP-YYYYMMDDNNNN` 형식으로 `approval_no`를 생성하며 클라이언트 입력을 허용하지 않는다. --- ### 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` - `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)`, `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)` 도입. - `/health` 응답의 `build_version`은 `config/default.toml`의 `[app].build_version`을 사용하며, `script/deploy_remote.sh`가 배포 아카이브 파일명에서 버전을 추출해 값을 주입한다.