37 KiB
간단 입·출고 + 결재 시스템 API 규격 (v4)
기준 버전: 2025-09-18 16:22:30Z (UTC)
본 문서는 stock_approval_system_spec_full_v4.md의 데이터 모델과 비즈니스 규칙을 기반으로 한 REST API 구성을 정의한다. 기본 CRUD를 제공하며, 목록·상세 조회 시 FK로 연결된 주요 엔터티 정보를 함께 반환한다. 모든 엔드포인트는 소프트 삭제 컬럼(is_deleted)을 노출하지 않는다.
0. 구현 현황 요약 (2025-09-18 기준)
- 마스터 데이터:
/vendors,/uoms,/transaction-types,/transaction-statuses,/approval-statuses,/approval-actions,/warehouses,/customers,/products,/employees,/groups,/menus,/group-menu-permissions,/zipcodes - 각 자원은
/api/v1/<resource>패턴을 따르며, 목록 필터·페이지네이션·include확장을 지원한다. - 그룹 권한은
/api/v1/group-menu-permissions와/api/v1/groups/{id}/permissions일괄 갱신 엔드포인트로 관리한다.group-menu-permissions응답의menu객체에는route_path가 포함되며, FE는 이 값을 사용해 라우트별 권한 매핑을 완성해야 한다.include=group쿼리를 추가하면 그룹 요약이 함께 반환돼 권한 매트릭스를 단일 호출로 구축할 수 있다. - 우편번호 검색
/api/v1/zipcodes는 부분 일치 검색(q,zipcode,road_name)과 단건 조회를 제공한다.
1. 공통 규칙
- URI 규칙: 복수형 리소스 명 사용. 기본 경로 예)
/api/v1/vendors. - 표준 응답 구조: 목록은
{ items: [], page, page_size, total }, 단건은{ data: { ... } }. - 시간대: 모든 날짜·시간은 ISO8601 UTC 문자열.
- 소프트 삭제:
DELETE /{res}/{id}호출 시 서버는is_deleted=true,is_active=false로 처리하고 응답 바디는{ data: { id, deleted_at } }형식을 사용. - 복구:
POST /{res}/{id}/restore. - 공통 컬럼:
note,is_active,created_at,updated_at는 요청·응답에 필요 시 노출하되is_deleted는 절대 노출하지 않는다. - 기본 필터: 목록 조회 시 기본 쿼리
active=true,deleted=false.deleted파라미터가true일 때에만 삭제된 항목을 반환. - 증분 조회:
updated_since=ISO8601. - 정렬:
sort(기본updated_at),order=asc|desc(기본 desc). - 검색:
q파라미터로 코드/명칭 부분 일치. 필요한 경우 컬럼별 필터 지원. - Include 확장:
include쿼리로 추가 데이터(lines,customers,approval,steps,histories,permissions,employees등) 선택 가능. 포함 대상은 FK 요약 정보를 이미 반환하므로include는 상세 컬렉션을 불러올 때 사용. - 배열 입력: 트랜잭션 라인, 트랜잭션 고객, 결재 단계, 그룹 메뉴 권한 등 다건 작업은 항상 배열(
[]) 기반으로 요청한다. - Primary Key 규칙: Create 요청 바디에는 PK를 포함하지 않는다. Create 응답 및 나머지 모든 요청·응답에는 PK가 포함돼야 한다(경로에 이미 포함된 경우라도 바디 내
id를 명시). - 에러 규격:
400 BAD_REQUEST— 검증 오류, 필수값 누락.404 NOT_FOUND— 리소스 없음 또는 삭제됨.409 CONFLICT— 유니크 제약, 결재 단계 상태 충돌.422 UNPROCESSABLE_ENTITY— 비즈니스 규칙 위반(출고 고객 누락, blocking 상태 전이 등).- 에러 응답 예:
{ "error": { "code": 422, "message": "출고 트랜잭션에는 고객이 최소 1건 필요합니다.", "details": [...] } }.
2. 타입(룩업) API
대상: /uoms, /transaction-types, /transaction-statuses, /approval-statuses, /approval-actions
2.1 목록 조회
GET /{type}?page=1&page_size=50&active=true
{
"items": [
{
"id": 1,
"name": "EA",
"is_default": true,
"is_active": true,
"note": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-02-01T03:00:00Z"
}
],
"page": 1,
"page_size": 50,
"total": 1
}
2.2 단건 조회
GET /{type}/{id}
{
"data": {
"id": 3,
"name": "반려",
"is_default": false,
"is_blocking_next": true,
"is_terminal": true,
"is_active": true,
"note": "최종 거절",
"created_at": "2025-01-10T09:00:00Z",
"updated_at": "2025-02-01T10:00:00Z"
}
}
2.3 생성
POST /{type}
{
"name": "진행중",
"is_default": false,
"is_blocking_next": true,
"is_terminal": false,
"is_active": true,
"note": null
}
응답:
{
"data": {
"id": 4,
"name": "진행중",
"is_default": false,
"is_blocking_next": true,
"is_terminal": false,
"is_active": true,
"note": null,
"created_at": "2025-03-01T00:00:00Z",
"updated_at": "2025-03-01T00:00:00Z"
}
}
2.4 수정
PATCH /{type}/{id}
{
"id": 4,
"is_blocking_next": false,
"note": "임시 승인 허용"
}
2.5 삭제 & 복구
DELETE /{type}/{id}→{ "data": { "id": 4, "deleted_at": "2025-03-05T09:00:00Z" } }POST /{type}/{id}/restore→{ "data": { "id": 4, "restored_at": "2025-03-06T01:00:00Z" } }
approval-statuses는 추가 속성(is_blocking_next,is_terminal)을 사용하며, 다른 타입 테이블은name,is_default,is_active,note중심으로 작동한다.
3. 마스터 데이터 API
리소스: /vendors, /warehouses, /customers, /employees, /products, /menus, /groups, /zipcodes
3.1 목록 조회
GET /vendors?page=1&q=한빛
{
"items": [
{
"id": 10,
"vendor_code": "V001",
"vendor_name": "한빛상사",
"note": "서울/경기 공급처",
"is_active": true,
"created_at": "2025-01-01T12:00:00Z",
"updated_at": "2025-01-03T09:00:00Z"
}
],
"page": 1,
"page_size": 50,
"total": 1
}
GET /products?page=1&include=vendor
{
"items": [
{
"id": 101,
"product_code": "P100",
"product_name": "샘플",
"vendor": {
"id": 10,
"vendor_code": "V001",
"vendor_name": "한빛상사"
},
"uom": {
"id": 1,
"uom_name": "EA",
"is_default": true
},
"note": "출고 우선 재고",
"is_active": true,
"created_at": "2025-02-01T12:00:00Z",
"updated_at": "2025-02-03T09:00:00Z"
}
],
"page": 1,
"page_size": 50,
"total": 1
}
GET /warehouses?page=1
{
"items": [
{
"id": 20,
"warehouse_code": "WH-001",
"warehouse_name": "1센터",
"zipcode": {
"zipcode": "06000",
"sido": "서울특별시",
"sigungu": "강남구",
"road_name": "테헤란로"
},
"address_detail": "강남파이낸스센터 10층",
"is_active": true,
"created_at": "2025-01-05T08:00:00Z",
"updated_at": "2025-01-10T09:30:00Z"
}
],
"page": 1,
"page_size": 50,
"total": 1
}
GET /customers?page=1
{
"items": [
{
"id": 301,
"customer_code": "C001",
"customer_name": "ABC물류",
"contact_name": "박담당",
"is_partner": true,
"is_general": false,
"email": "contact@abc.com",
"mobile_no": "010-1234-5678",
"zipcode": {
"zipcode": "06000",
"sido": "서울특별시",
"sigungu": "강남구",
"road_name": "테헤란로"
},
"address_detail": "10층",
"note": "VIP 고객",
"is_active": true,
"created_at": "2025-01-15T11:00:00Z",
"updated_at": "2025-01-20T08:10:00Z"
}
],
"page": 1,
"page_size": 50,
"total": 1
}
contact_name은 고객사 담당자 실명. 선택 입력이며 미입력 시null.
GET /employees?page=1
{
"items": [
{
"id": 7,
"employee_no": "E2025001",
"employee_name": "김승인",
"email": "approver@example.com",
"mobile_no": "010-2222-1111",
"group": {
"id": 2,
"group_name": "창고 관리자"
},
"is_active": true,
"created_at": "2025-01-02T09:00:00Z",
"updated_at": "2025-01-10T11:00:00Z"
}
],
"page": 1,
"page_size": 50,
"total": 1
}
GET /groups?include=permissions,employees
{
"items": [
{
"id": 2,
"group_name": "창고 관리자",
"group_description": "창고 및 재고 관리",
"is_default": false,
"is_active": true,
"permissions": [
{
"id": 8,
"menu": {
"id": 12,
"menu_code": "STOCK_MGMT",
"menu_name": "입출고 관리",
"route_path": "/inventory/transactions"
},
"can_create": true,
"can_read": true,
"can_update": true,
"can_delete": false
}
],
"employees": [
{
"id": 7,
"employee_no": "E2025001",
"employee_name": "김승인"
}
],
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-15T00:00:00Z"
}
],
"page": 1,
"page_size": 50,
"total": 1
}
3.2 단건 조회
GET /products/101
{
"data": {
"id": 101,
"product_code": "P100",
"product_name": "샘플",
"vendor": {
"id": 10,
"vendor_code": "V001",
"vendor_name": "한빛상사",
"note": "서울/경기 공급처"
},
"uom": {
"id": 1,
"uom_name": "EA",
"is_default": true
},
"note": "출고 우선 재고",
"is_active": true,
"created_at": "2025-02-01T12:00:00Z",
"updated_at": "2025-02-03T09:00:00Z"
}
}
3.3 생성
POST /vendors
{
"vendor_code": "V002",
"vendor_name": "미래상사",
"note": "부산 공급처",
"is_active": true
}
응답:
{
"data": {
"id": 11,
"vendor_code": "V002",
"vendor_name": "미래상사",
"note": "부산 공급처",
"is_active": true,
"created_at": "2025-03-01T00:00:00Z",
"updated_at": "2025-03-01T00:00:00Z"
}
}
3.4 수정
PATCH /products/101
{
"id": 101,
"product_name": "샘플 A",
"note": "재고 우선순위 변경"
}
3.5 삭제 & 복구
DELETE /products/101POST /products/101/restore
3.6 그룹 메뉴 권한 일괄 갱신
POST /groups/2/permissions
{
"id": 2,
"entries": [
{
"menu_id": 12,
"can_create": true,
"can_read": true,
"can_update": true,
"can_delete": false
},
{
"menu_id": 13,
"can_create": false,
"can_read": true,
"can_update": false,
"can_delete": false
}
]
}
응답은 갱신된 권한 목록을 반환.
zipcodes는 대량 데이터 특성상GET /zipcodes?zipcode=06000&road_name=세종대로형태로 조회하며, 응답 항목에는zipcode,sido,sigungu,road_name,building_main_no등 주소 구성 요소가 포함된다.
4. 트랜잭션 API
리소스: /stock-transactions, 보조 리소스: /transaction-lines, /transaction-customers
4.1 생성 (헤더 + 라인 + 고객 다건)
POST /stock-transactions
{
"transaction_no": "TXN-2025-0001",
"transaction_type_id": 1,
"transaction_status_id": 1,
"warehouse_id": 1,
"transaction_date": "2025-09-18",
"created_by_id": 7,
"note": "창고 입고",
"lines": [
{
"line_no": 1,
"product_id": 101,
"quantity": 50,
"unit_price": 1200
},
{
"line_no": 2,
"product_id": 102,
"quantity": 20,
"unit_price": 0
}
],
"customers": [],
"approval": {
"approval_no": "APP-2025-0001",
"requested_by_id": 7,
"note": "입고 결재"
}
}
응답은 생성된 트랜잭션 전체 정보를 반환하며, 라인·고객 식별자가 포함된다. approval
블록은 결재 생성에 필요한 정보를 담으며 생략할 수 없다.
4.2 목록 조회
GET /stock-transactions?include=lines,customers,approval
{
"items": [
{
"id": 9001,
"transaction_no": "TXN-2025-0001",
"transaction_type": {
"id": 1,
"type_name": "입고"
},
"transaction_status": {
"id": 1,
"status_name": "초안"
},
"warehouse": {
"id": 1,
"warehouse_code": "WH-001",
"warehouse_name": "1센터"
},
"transaction_date": "2025-09-18",
"created_by": {
"id": 7,
"employee_no": "E2025001",
"employee_name": "김승인"
},
"note": "창고 입고",
"is_active": true,
"created_at": "2025-09-18T05:00:00Z",
"updated_at": "2025-09-18T05:00:00Z",
"lines": [
{
"id": 12001,
"line_no": 1,
"product": {
"id": 101,
"product_code": "P100",
"product_name": "샘플",
"vendor": {
"id": 10,
"vendor_name": "한빛상사"
},
"uom": {
"id": 1,
"uom_name": "EA"
}
},
"quantity": 50,
"unit_price": 1200,
"note": null
}
],
"customers": [
{
"id": 301,
"customer_code": "C001",
"customer_name": "ABC물류",
"contact_name": "박담당"
}
],
"approval": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
"current_step": {
"id": 7001,
"step_order": 1,
"approver": {
"id": 21,
"employee_no": "E2025002",
"employee_name": "박검토"
},
"step_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
"assigned_at": "2025-09-18T06:05:00Z",
"decided_at": null,
"note": null
},
"requested_by": {
"id": 7,
"employee_no": "E2025001",
"employee_name": "김승인"
},
"requested_at": "2025-09-18T06:00:00Z",
"decided_at": null,
"note": "입고 결재",
"is_active": true,
"created_at": "2025-09-18T06:00:00Z",
"updated_at": "2025-09-18T06:05:00Z"
}
}
],
"page": 1,
"page_size": 50,
"total": 1
}
4.3 단건 조회
GET /stock-transactions/9001?include=lines,customers,approval,approval.steps
{
"data": {
"id": 9001,
"transaction_no": "TXN-2025-0001",
"transaction_type": {
"id": 1,
"type_name": "입고"
},
"transaction_status": {
"id": 1,
"status_name": "초안"
},
"warehouse": {
"id": 1,
"warehouse_code": "WH-001",
"warehouse_name": "1센터",
"zipcode": {
"zipcode": "06000",
"sido": "서울특별시"
}
},
"transaction_date": "2025-09-18",
"created_by": {
"id": 7,
"employee_no": "E2025001",
"employee_name": "김승인"
},
"note": "창고 입고",
"is_active": true,
"created_at": "2025-09-18T05:00:00Z",
"updated_at": "2025-09-18T05:00:00Z",
"lines": [
{
"id": 12001,
"line_no": 1,
"product": {
"id": 101,
"product_code": "P100",
"product_name": "샘플",
"vendor": {
"id": 10,
"vendor_name": "한빛상사"
},
"uom": {
"id": 1,
"uom_name": "EA"
}
},
"quantity": 50,
"unit_price": 1200,
"note": null
}
],
"customers": [
{
"id": 301,
"customer_code": "C001",
"customer_name": "ABC물류",
"contact_name": "박담당"
}
],
"approval": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
"current_step": {
"id": 7001,
"step_order": 1,
"approver": {
"id": 21,
"employee_no": "E2025002",
"employee_name": "박검토"
},
"step_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
"assigned_at": "2025-09-18T06:05:00Z",
"decided_at": null,
"note": null
},
"requested_by": {
"id": 7,
"employee_no": "E2025001",
"employee_name": "김승인"
},
"requested_at": "2025-09-18T06:00:00Z",
"decided_at": null,
"note": "입고 결재",
"is_active": true,
"created_at": "2025-09-18T06:00:00Z",
"updated_at": "2025-09-18T06:05:00Z"
}
}
}
4.4 헤더 수정
PATCH /stock-transactions/9001
{
"id": 9001,
"transaction_status_id": 2,
"note": "상신 준비"
}
4.5 라인 다건 추가/수정/삭제
- 추가:
POST /stock-transactions/9001/lines
{
"id": 9001,
"lines": [
{
"line_no": 2,
"product_id": 102,
"quantity": 20,
"unit_price": 900
}
]
}
- 일괄 수정:
PATCH /stock-transactions/9001/lines
{
"id": 9001,
"lines": [
{
"id": 12001,
"line_no": 1,
"quantity": 60,
"note": "추가 입고"
},
{
"id": 12002,
"line_no": 2,
"unit_price": 950
}
]
}
- 삭제:
DELETE /transaction-lines/12002 - 복구:
POST /transaction-lines/12002/restore
4.6 고객 연결 다건 관리
- 추가:
POST /stock-transactions/9100/customers
{
"id": 9100,
"customers": [
{
"customer_id": 301,
"note": "1차 납품"
},
{
"customer_id": 302,
"note": "2차 납품"
}
]
}
- 수정:
PATCH /stock-transactions/9100/customers
{
"id": 9100,
"customers": [
{
"id": 33001,
"note": "수량 조정"
}
]
}
- 삭제:
DELETE /transaction-customers/33001
4.7 상태 전이 권장 API
POST /stock-transactions/9001/submit
{
"id": 9001,
"note": "승인 요청"
}
POST /stock-transactions/9001/complete
{
"id": 9001,
"note": "처리 완료"
}
POST /stock-transactions/9001/approve
{
"id": 9001,
"note": "최종 승인"
}
POST /stock-transactions/9001/reject
{
"id": 9001,
"note": "재작업 필요"
}
POST /stock-transactions/9001/cancel
{
"id": 9001,
"note": "상신 취소"
}
모든 액션은 { "data": { "id": 9001, "transaction_status": { ... }, "updated_at": "..." } } 구조를 반환한다. submit은 초안 상태의 트랜잭션을 상신 상태로, 결재 현재 단계를 진행중으로 전환한다. approve는 결재 상태가 이미 승인(approval_status_id = 승인)으로 확정된 건을 재고 상태 승인으로 승격한다. reject는 상신/승인 상태의 건을 반려 상태로 내리고 결재 레코드도 반려로 남긴다. cancel은 상신된 건을 다시 초안 상태(또는 취소 상태가 존재할 경우 해당 상태)로 되돌리며, 결재 단계와 상태를 초기화한다. complete 는 결재 상태가 승인된 건에 한해 완료 상태로 변경한다.
5. 결재 API
리소스: /approvals, 보조 리소스: /approval-steps, /approval-histories
- 단계 상태가 바뀔 때마다
approvals.current_step_id는 차기 단계의 ID로 갱신되고, 전체 결재 상태(approval_status_id) 역시 해당 단계 상태로 업데이트된다. - 템플릿에서 복제된 단계는 모두
대기상태로 저장되며 템플릿이 이후 수정돼도 기존 결재에는 반영되지 않는다. GET /approvals/{id}/can-proceed는 현재 단계의 상태에 매핑된is_blocking_next값이false일 때true를 반환한다.
5.1 결재 생성
POST /approvals
{
"transaction_id": 9001,
"approval_no": "APP-2025-0001",
"approval_status_id": 1,
"requested_by_id": 7,
"note": "입고 결재"
}
응답에는 id와 현재 단계 정보가 포함된다.
5.2 목록 조회
GET /approvals?include=steps,histories
{
"items": [
{
"id": 5001,
"approval_no": "APP-2025-0001",
"transaction": {
"id": 9001,
"transaction_no": "TXN-2025-0001"
},
"approval_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true
},
"current_step": {
"id": 7001,
"step_order": 1,
"approver": {
"id": 21,
"employee_no": "E2025002",
"employee_name": "박검토"
},
"step_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
"assigned_at": "2025-09-18T06:05:00Z",
"decided_at": null,
"note": null
},
"requested_by": {
"id": 7,
"employee_no": "E2025001",
"employee_name": "김승인"
},
"requested_at": "2025-09-18T06:00:00Z",
"decided_at": null,
"note": "입고 결재",
"is_active": true,
"created_at": "2025-09-18T06:00:00Z",
"updated_at": "2025-09-18T06:00:00Z",
"steps": [
{
"id": 7001,
"step_order": 1,
"approver": {
"id": 21,
"employee_no": "E2025002",
"employee_name": "박검토"
},
"step_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
"assigned_at": "2025-09-18T06:05:00Z",
"decided_at": null,
"note": null
}
],
"histories": []
}
],
"page": 1,
"page_size": 50,
"total": 1
}
5.3 단건 조회
GET /approvals/5001?include=steps,histories
{
"data": {
"id": 5001,
"approval_no": "APP-2025-0001",
"transaction": {
"id": 9001,
"transaction_no": "TXN-2025-0001"
},
"approval_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
"current_step": {
"id": 7001,
"step_order": 1,
"approver": {
"id": 21,
"employee_no": "E2025002",
"employee_name": "박검토"
},
"step_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
"assigned_at": "2025-09-18T06:05:00Z",
"decided_at": null,
"note": null
},
"requested_by": {
"id": 7,
"employee_no": "E2025001",
"employee_name": "김승인"
},
"requested_at": "2025-09-18T06:00:00Z",
"decided_at": null,
"note": "입고 결재",
"steps": [
{
"id": 7001,
"step_order": 1,
"approver": {
"id": 21,
"employee_no": "E2025002",
"employee_name": "박검토"
},
"step_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true
},
"assigned_at": "2025-09-18T06:05:00Z",
"decided_at": null,
"note": null
}
],
"histories": [],
"created_at": "2025-09-18T06:00:00Z",
"updated_at": "2025-09-18T06:05:00Z"
}
}
5.4 단계 구성 (배치 생성)
POST /approvals/5001/steps
{
"id": 5001,
"steps": [
{
"step_order": 1,
"approver_id": 21,
"note": null
},
{
"step_order": 2,
"approver_id": 34,
"note": "재무 확인"
}
]
}
응답:
{
"data": {
"approval_id": 5001,
"steps": [
{
"id": 7001,
"approval_id": 5001,
"step_order": 1,
"approver_id": 21,
"step_status_id": 1,
"assigned_at": "2025-09-18T06:05:00Z",
"decided_at": null,
"note": null,
"is_active": true
},
{
"id": 7002,
"approval_id": 5001,
"step_order": 2,
"approver_id": 34,
"step_status_id": 1,
"assigned_at": "2025-09-18T06:05:00Z",
"decided_at": null,
"note": "재무 확인",
"is_active": true
}
],
"approval": {
"id": 5001,
"approval_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
"current_step": {
"id": 7001,
"step_order": 1
},
"updated_at": "2025-09-18T06:05:00Z"
}
}
}
5.5 단계 일괄 수정/재배치
PATCH /approvals/5001/steps
{
"id": 5001,
"steps": [
{
"id": 7001,
"step_order": 1,
"note": "서류 확인 중"
},
{
"id": 7002,
"step_order": 2,
"approver_id": 35
}
]
}
응답:
{
"data": {
"approval_id": 5001,
"steps": [
{
"id": 7001,
"approval_id": 5001,
"step_order": 1,
"approver_id": 21,
"step_status_id": 1,
"assigned_at": "2025-09-18T06:05:00Z",
"decided_at": null,
"note": "서류 확인 중",
"is_active": true
},
{
"id": 7002,
"approval_id": 5001,
"step_order": 2,
"approver_id": 35,
"step_status_id": 1,
"assigned_at": "2025-09-18T06:05:00Z",
"decided_at": null,
"note": "재무 확인",
"is_active": true
}
],
"approval": {
"id": 5001,
"approval_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
"current_step": {
"id": 7001,
"step_order": 1
},
"updated_at": "2025-09-18T06:10:00Z"
}
}
}
5.6 단계 행위
POST /approval-steps/7001/actions
{
"id": 7001,
"approval_action_id": 1,
"note": "승인합니다."
}
응답:
{
"data": {
"approval": {
"id": 5001,
"approval_status": {
"id": 2,
"status_name": "진행중",
"is_blocking_next": true,
"is_terminal": false
},
"current_step": {
"id": 7002,
"step_order": 2,
"approver": {
"id": 34,
"employee_no": "E2025003",
"employee_name": "최검토"
},
"step_status": {
"id": 3,
"status_name": "진행중",
"is_blocking_next": true,
"is_terminal": false
},
"assigned_at": "2025-09-18T08:05:00Z",
"decided_at": null,
"note": "재무 확인"
},
"updated_at": "2025-09-18T08:05:00Z",
"histories": [
{
"id": 91001,
"approval_action_id": 1,
"action_at": "2025-09-18T08:05:00Z",
"note": "승인합니다.",
"from_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
"to_status": {
"id": 2,
"status_name": "진행중",
"is_blocking_next": true,
"is_terminal": false
}
}
]
},
"step": {
"id": 7001,
"approval_id": 5001,
"step_order": 1,
"approver_id": 21,
"step_status_id": 2,
"assigned_at": "2025-09-18T06:05:00Z",
"decided_at": "2025-09-18T08:05:00Z",
"note": "승인합니다.",
"step_status": {
"id": 2,
"status_name": "진행중",
"is_blocking_next": true,
"is_terminal": false
}
},
"next_step": {
"id": 7002,
"step_order": 2,
"approver": {
"id": 34,
"employee_no": "E2025003",
"employee_name": "최검토"
},
"step_status": {
"id": 3,
"status_name": "진행중",
"is_blocking_next": true,
"is_terminal": false
},
"assigned_at": "2025-09-18T08:05:00Z",
"decided_at": null,
"note": "재무 확인"
},
"history": {
"id": 91001,
"approval_step_id": 7001,
"approval_action_id": 1,
"note": "승인합니다.",
"action_at": "2025-09-18T08:05:00Z"
}
}
}
응답에는 전후 상태(from_status, to_status), 차기 단계 정보가 포함되며, approval_histories에 기록된다.
프론트엔드는 204 응답이 아닌 위의 { "data": { ... } } 본문을 소비해 화면 상태를 즉시 갱신해야 한다.
5.7 결재 상태 확인
GET /approvals/5001/can-proceed
{
"data": {
"id": 5001,
"can_proceed": true,
"reason": null
}
}
5.8 결재 수정·삭제·복구
PATCH /approvals/5001
{
"id": 5001,
"approval_status_id": 2,
"note": "보류 처리"
}
DELETE /approvals/5001POST /approvals/5001/restore
5.9 결재 이력 조회
GET /approval-histories?approval_id=5001&include=approval,step,approver
{
"items": [
{
"id": 91001,
"approval_id": 5001,
"approval_step_id": 7001,
"approval_action_id": 3,
"action_at": "2025-09-18T08:05:00Z",
"note": "보류 코멘트",
"approver": {
"id": 21,
"employee_no": "E2025002",
"employee_name": "박검토"
},
"from_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
"to_status": {
"id": 2,
"status_name": "진행중",
"is_blocking_next": true,
"is_terminal": false
},
"approval": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_status": {
"id": 2,
"status_name": "진행중",
"is_blocking_next": true,
"is_terminal": false
}
},
"step": {
"id": 7001,
"approval_id": 5001,
"step_order": 1,
"approver": {
"id": 21,
"employee_no": "E2025002",
"employee_name": "박검토"
}
}
}
],
"page": 1,
"page_size": 50,
"total": 2
}
5.10 단계 개별 CRUD
GET /approval-steps?approval_id=5001&include=approval,approver,step_status→{ items: [], page, page_size, total }형태로 반환하며, 각 항목은 요청한include토큰에 따라 관련 결재/결재자/상태 요약을 포함한다.GET /approval-steps/7001?include=approval,approver,step_status→{ data: { ... } }.POST /approval-steps→ 단일 단계를 생성하고{ data: { step, approval } }형태로 생성된 요약과 결재 상태를 반환한다.step_status_id를 생략하면 자동으로대기상태가 지정된다.POST /approval-steps/batch→ 여러 단계를 한 번에 생성·재정렬하며, 응답은{ data: { approval, steps, histories } }구조로 최신 결재 상태와 정렬된 단계 목록, 신규 이력 요약을 포함한다.PATCH /approval-steps/batch→ 다건 단계 수정/비활성화를 처리하고{ data: { approval, steps, histories } }본문으로 변경 결과를 반환한다.PATCH /approval-steps/{id}→ 갱신된 단계 요약과 함께{ data: { step, approval } }를 반환한다.DELETE /approval-steps/{id}→{ data: { id, deleted_at } }.POST /approval-steps/{id}/restore→{ data: { id, restored_at } }.
주요 필터 및 확장 파라미터(approval-steps):
approval_id,approver_id,step_status_idinclude=approval,approver,step_status
GET /approval-histories/91001?include=approval,step
{
"data": {
"id": 91001,
"approval_id": 5001,
"approval_step_id": 7001,
"approval_action_id": 3,
"action_at": "2025-09-18T08:05:00Z",
"note": "보류 코멘트",
"approver": {
"id": 21,
"employee_no": "E2025002",
"employee_name": "박검토"
},
"from_status": null,
"to_status": {
"id": 2,
"status_name": "진행중",
"is_blocking_next": true,
"is_terminal": false
},
"approval": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_status": {
"id": 2,
"status_name": "진행중",
"is_blocking_next": true,
"is_terminal": false
}
},
"step": {
"id": 7001,
"approval_id": 5001,
"step_order": 1,
"approver": {
"id": 21,
"employee_no": "E2025002",
"employee_name": "박검토"
}
}
}
}
approval-histories 엔드포인트는 approval_id, approval_step_id, approver_id, approval_action_id, action_from, action_to, sort=action_at|created_at|updated_at, order=asc|desc 파라미터와 include=approval,step,approver,from_status,to_status 확장을 지원한다.
6. 결재 템플릿 API
리소스: /approval-templates
6.1 목록 조회
GET /approval-templates?page=1
{
"items": [
{
"id": 3001,
"template_code": "AP_INBOUND",
"template_name": "입고 결재 기본",
"description": "입고 결재 2단계",
"created_by": {
"id": 7,
"employee_no": "E2025001",
"employee_name": "김승인"
},
"is_active": true,
"created_at": "2025-01-20T00:00:00Z",
"updated_at": "2025-01-25T00:00:00Z"
}
],
"page": 1,
"page_size": 50,
"total": 1
}
6.2 단건 조회
GET /approval-templates/3001?include=steps
{
"data": {
"id": 3001,
"template_code": "AP_INBOUND",
"template_name": "입고 결재 기본",
"description": "입고 결재 2단계",
"created_by": {
"id": 7,
"employee_no": "E2025001",
"employee_name": "김승인"
},
"steps": [
{
"id": 9101,
"step_order": 1,
"approver": {
"id": 21,
"employee_no": "E2025002",
"employee_name": "박검토"
},
"note": null
}
],
"is_active": true,
"created_at": "2025-01-20T00:00:00Z",
"updated_at": "2025-01-25T00:00:00Z"
}
}
6.3 생성·수정
POST /approval-templates
{
"template_code": "AP_OUTBOUND",
"template_name": "출고 결재 기본",
"description": "출고 결재 3단계",
"created_by_id": 7,
"note": "표준 출고"
}
POST /approval-templates/3002/steps
{
"id": 3002,
"steps": [
{
"step_order": 1,
"approver_id": 34
},
{
"step_order": 2,
"approver_id": 55
}
]
}
PATCH /approval-templates/3002
{
"id": 3002,
"template_name": "출고 결재 확장",
"note": "정기 출고용"
}
PATCH /approval-templates/3002/steps
{
"id": 3002,
"steps": [
{
"id": 9105,
"step_order": 1,
"approver_id": 36
}
]
}
- 삭제/복구:
DELETE /approval-templates/{id},POST /approval-templates/{id}/restore
7. 보고서 Export
format=xlsx|pdf파라미터를 지원한다. 현재는format=xlsx만 성공하며,format=pdf를 지정하면 400 Bad Request가 반환된다.- 응답은 즉시 다운로드 스트림으로 전달되며
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet와Content-Disposition: attachment헤더가 포함된다. 프론트엔드는 받은 바이트 스트림을 그대로 파일로 저장해야 한다.
7.1 트랜잭션 Export
GET /api/v1/reports/transactions/export?from=2025-09-01&to=2025-09-30&transaction_status_id=2&approval_status_id=3&requested_by_id=7&format=xlsx
- 지원 쿼리:
from,to,transaction_status_id,approval_status_id,requested_by_id,format. - 열 구성:
Transaction No,Transaction Date,Transaction Type,Status,Warehouse,Created By,Approval No,Approval Status.
7.2 결재 Export
GET /api/v1/reports/approvals/export?from=2025-09-01T00:00:00Z&to=2025-09-30T23:59:59Z&transaction_status_id=1&approval_status_id=1&requested_by_id=7&format=xlsx
- 지원 쿼리:
from,to,transaction_status_id,approval_status_id,requested_by_id,format. - 열 구성:
Approval No,Approval Status,Transaction No,Requested By,Requested At,Decided At,Current Step Order,Current Step Approver. from,to파라미터는requested_at기준 UTC 타임스탬프 범위 필터다.
8. 구현 참고
- FK 요약 정보는 기본 응답에 포함하며, 상세 정보가 필요하면
include파라미터를 활용해 확장한다. - 배열 기반 다건 작업은 전체를 트랜잭션 처리해야 한다. 실패 시 롤백하고 부분 처리 결과를 반환하지 않는다.
is_active변경은 권한·결재 등의 즉시성 요구를 고려하여 관련 캐시를 무효화한다.- 결재 단계 상태 전이는
approval_statuses.is_blocking_next규칙을 준수해야 하며, 반려(is_terminal=true) 상태 시 결재를 종료한다.