Files
superport_v2/doc/stock_approval_system_api_v4.md
2025-10-23 20:19:59 +09:00

46 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와 동일 값을 가진 path가 포함되며 각 항목은 is_deleted를 노출한다. include=group,menu 확장과 include_deleted=true 파라미터로 삭제 권한을 함께 조회할 수 있다.
  • 우편번호 검색 /api/v1/zipcodes는 부분 일치 검색(q, zipcode, road_name)과 단건 조회를 제공한다.

1. 공통 규칙

  • URI 규칙: 복수형 리소스 명 사용. 기본 경로 예) /api/v1/vendors.
  • 표준 응답 구조: 목록은 { items: [], page, page_size, total }, 단건은 { data: { ... } }.
  • 헬스 체크: GET /health{ status, build_version, error? } 구조를 반환하며 build_version 값은 config/default.toml[app].build_version에서 로딩된다. 원격 배포 시 script/deploy_remote.sh가 배포 아카이브 파일명에서 버전을 추출해 해당 값을 갱신한다.
  • 시간대: 모든 날짜·시간은 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": [...] } }.
  • CORS 정책: 서버는 config/default.toml[cors] 설정을 사용해 허용 오리진을 제어한다. allowed_origins가 비어 있으면 모든 오리진을 허용하고, 값에 http://localhost 또는 https://web.example.com:*처럼 포트 와일드카드(:*)를 포함하면 동적 포트 환경에서도 Access-Control-Allow-Origin이 요청 오리진과 동일하게 반환된다. 허용 오리진에 일치하지 않으면 400 BAD_REQUEST가 응답된다.

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
}
  • delta 값은 전일 대비 증감률(비율)로 반환되며 1.0은 100% 증가, -0.5는 50% 감소를 의미한다. 값이 계산되지 않는 KPI는 delta를 생략한다.

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

기본 정렬: 별도 sort 파라미터가 없으면 항상 id 오름차순으로 응답을 정렬한다. (order 기본값도 asc)

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/101
  • POST /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_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": {
    "requested_by_id": 7,
    "note": "입고 결재"
  }
}

응답은 생성된 트랜잭션 전체 정보를 반환하며, 라인·고객 식별자가 포함된다. transaction_noapproval.approval_no는 요청 시 생략하며, 서버가 각각 TRX-YYYYMMDDNNNN, APP-YYYYMMDDNNNN 패턴으로 생성한 값을 응답에서 확인한다. approval 블록은 결재 생성에 필요한 정보를 담으며 생략할 수 없다.

4.2 목록 조회

GET /stock-transactions?customer_id=301&include=lines,customers,approval

  • customer_id (optional, number): 지정한 고객이 연결된 트랜잭션만 반환한다. 다른 검색 파라미터와 조합 가능하며, include=customers 사용 시 선택 고객 정보가 응답에 유지된다.
{
  "items": [
    {
      "id": 9001,
      "transaction_no": "TRX-202511100001",
      "transaction_type": {
        "id": 1,
        "name": "입고"
      },
      "transaction_status": {
        "id": 1,
        "name": "초안"
      },
      "warehouse": {
        "id": 1,
        "warehouse_code": "WH-001",
        "warehouse_name": "1센터",
        "zipcode": {
          "zipcode": "06000",
          "road_name": "테헤란로"
        }
      },
      "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",
      "expected_return_date": "2025-09-30",
      "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": {
            "id": 301,
            "customer_code": "C001",
            "customer_name": "ABC물류"
          },
          "note": null
        }
      ],
      "approval": {
        "id": 5001,
        "approval_no": "APP-202511100001",
        "status": {
          "id": 1,
          "name": "대기",
          "is_blocking_next": true,
          "is_terminal": false
        },
        "current_step": {
          "id": 7001,
          "step_order": 1,
          "status": {
            "id": 1,
            "name": "대기",
            "is_blocking_next": true,
            "is_terminal": false
          },
          "approver": {
            "id": 21,
            "employee_no": "E2025002",
            "name": "박검토"
          },
          "assigned_at": "2025-09-18T06:05:00Z",
          "decided_at": null,
          "note": null
        },
        "requester": {
          "id": 7,
          "employee_no": "E2025001",
          "name": "김승인"
        },
        "requested_at": "2025-09-18T06:00:00Z",
        "decided_at": null,
        "note": "입고 결재",
        "template_name": "입고 결재 기본",
        "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": "TRX-202511100001",
    "transaction_type": {
      "id": 1,
      "name": "입고"
    },
    "transaction_status": {
      "id": 1,
      "name": "초안"
    },
    "warehouse": {
      "id": 1,
      "warehouse_code": "WH-001",
      "warehouse_name": "1센터",
      "zipcode": {
        "zipcode": "06000",
        "sido": "서울특별시",
        "road_name": "테헤란로"
      }
    },
    "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",
    "expected_return_date": "2025-09-30",
    "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": {
          "id": 301,
          "customer_code": "C001",
          "customer_name": "ABC물류"
        },
        "note": null
      }
    ],
    "approval": {
      "id": 5001,
      "approval_no": "APP-202511100001",
      "status": {
        "id": 1,
        "name": "대기",
        "color": "#F97316",
        "is_blocking_next": true,
        "is_terminal": false
      },
      "current_step": {
        "id": 7001,
        "step_order": 1,
        "approver": {
          "id": 21,
          "employee_no": "E2025002",
          "name": "박검토"
        },
        "status": {
          "id": 1,
          "name": "대기",
          "is_blocking_next": true,
          "is_terminal": false
        },
        "assigned_at": "2025-09-18T06:05:00Z",
        "decided_at": null,
        "note": null
      },
      "requester": {
        "id": 7,
        "employee_no": "E2025001",
        "name": "김승인"
      },
      "requested_at": "2025-09-18T06:00:00Z",
      "decided_at": null,
      "note": "입고 결재",
      "template_name": "입고 결재 기본",
      "steps": [
        {
          "id": 7001,
          "step_order": 1,
          "approver": {
            "id": 21,
            "employee_no": "E2025002",
            "name": "박검토"
          },
          "status": {
            "id": 1,
            "name": "대기",
            "is_blocking_next": true,
            "is_terminal": false
          },
          "assigned_at": "2025-09-18T06:05:00Z",
          "decided_at": null,
          "note": null
        }
      ],
      "histories": [
        {
          "id": 91001,
          "action": {
            "id": 1,
            "name": "상신"
          },
          "from_status": null,
          "to_status": {
            "id": 1,
            "name": "대기",
            "is_blocking_next": true,
            "is_terminal": false
          },
          "approver": {
            "id": 7,
            "employee_no": "E2025001",
            "name": "김승인"
          },
          "action_at": "2025-09-18T06:00:00Z",
          "note": null
        }
      ],
      "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_status_id": 1,
  "requested_by_id": 7,
  "note": "입고 결재"
}

응답:

{
  "data": {
    "approval": {
      "id": 5001,
      "approval_no": "APP-202511100001",
      "status": {
        "id": 1,
        "name": "대기",
        "is_blocking_next": true,
        "is_terminal": false
      },
      "current_step": null,
      "requester": {
        "id": 7,
        "employee_no": "E2025001",
        "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"
    }
  }
}
  • approval_no는 서버가 자동 발급하는 읽기 전용 필드로 APP-YYYYMMDDNNNN 형식을 따른다. 클라이언트는 필드를 전송하지 않으며, 중복 방지는 서버에서 처리된다.
  • 최초 생성 시 approval_status_id에는 대기 상태 ID를 전달하고, 서버는 동일 상태로 저장한다.
  • 단계나 이력이 존재하면 data.approval.steps, data.approval.histories가 함께 반환된다.

5.2 목록 조회

GET /approvals?include=steps,histories

{
  "items": [
    {
      "id": 5001,
      "approval_no": "APP-202511100001",
      "transaction": {
        "id": 9001,
        "transaction_no": "TRX-202511100001"
      },
      "status": {
        "id": 1,
        "name": "대기",
        "color": "#F97316",
        "is_blocking_next": true,
        "is_terminal": false
      },
      "current_step": {
        "id": 7001,
        "step_order": 1,
        "status": {
          "id": 1,
          "name": "대기",
          "is_blocking_next": true,
          "is_terminal": false
        },
        "approver": {
          "id": 21,
          "employee_no": "E2025002",
          "name": "박검토"
        },
        "assigned_at": "2025-09-18T06:05:00Z",
        "decided_at": null,
        "note": null
      },
      "requester": {
        "id": 7,
        "employee_no": "E2025001",
        "name": "김승인"
      },
      "requested_at": "2025-09-18T06:00:00Z",
      "decided_at": null,
      "note": "입고 결재",
      "is_active": true,
      "is_deleted": false,
      "created_at": "2025-09-18T06:00:00Z",
      "updated_at": "2025-09-18T06:05:00Z",
      "steps": [
        {
          "id": 7001,
          "step_order": 1,
          "status": {
            "id": 1,
            "name": "대기",
            "is_blocking_next": true,
            "is_terminal": false
          },
          "approver": {
            "id": 21,
            "employee_no": "E2025002",
            "name": "박검토"
          },
          "assigned_at": "2025-09-18T06:05:00Z",
          "decided_at": null,
          "note": null
        }
      ],
      "histories": [
        {
          "id": 91001,
          "action": {
            "id": 1,
            "name": "상신"
          },
          "from_status": null,
          "to_status": {
            "id": 1,
            "name": "대기",
            "is_blocking_next": true,
            "is_terminal": false
          },
          "approver": {
            "id": 7,
            "employee_no": "E2025001",
            "name": "김승인"
          },
          "action_at": "2025-09-18T06:00:00Z",
          "note": null
        }
      ]
    }
  ],
  "page": 1,
  "page_size": 50,
  "total": 1
}

5.3 단건 조회

GET /approvals/5001?include=steps,histories

{
  "data": {
    "id": 5001,
    "approval_no": "APP-202511100001",
    "transaction": {
      "id": 9001,
      "transaction_no": "TRX-202511100001"
    },
    "status": {
      "id": 1,
      "name": "대기",
      "color": "#F97316",
      "is_blocking_next": true,
      "is_terminal": false
    },
    "current_step": {
      "id": 7001,
      "step_order": 1,
      "status": {
        "id": 1,
        "name": "대기",
        "is_blocking_next": true,
        "is_terminal": false
      },
      "approver": {
        "id": 21,
        "employee_no": "E2025002",
        "name": "박검토"
      },
      "assigned_at": "2025-09-18T06:05:00Z",
      "decided_at": null,
      "note": null
    },
    "requester": {
      "id": 7,
      "employee_no": "E2025001",
      "name": "김승인"
    },
    "requested_at": "2025-09-18T06:00:00Z",
    "decided_at": null,
    "note": "입고 결재",
    "steps": [
      {
        "id": 7001,
        "step_order": 1,
        "status": {
          "id": 1,
          "name": "대기",
          "is_blocking_next": true,
          "is_terminal": false
        },
        "approver": {
          "id": 21,
          "employee_no": "E2025002",
          "name": "박검토"
        },
        "assigned_at": "2025-09-18T06:05:00Z",
        "decided_at": null,
        "note": null
      }
    ],
    "histories": [
      {
        "id": 91001,
        "action": {
          "id": 1,
          "name": "상신"
        },
        "from_status": null,
        "to_status": {
          "id": 1,
          "name": "대기",
          "is_blocking_next": true,
          "is_terminal": false
        },
        "approver": {
          "id": 7,
          "employee_no": "E2025001",
          "name": "김승인"
        },
        "action_at": "2025-09-18T06:00:00Z",
        "note": null
      }
    ],
    "is_active": true,
    "is_deleted": false,
    "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,
        "status_id": 1,
        "step_status_id": 1,
        "status": {
          "id": 1,
          "name": "대기",
          "is_blocking_next": true,
          "is_terminal": false
        },
        "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,
        "status_id": 1,
        "step_status_id": 1,
        "status": {
          "id": 1,
          "name": "대기",
          "is_blocking_next": true,
          "is_terminal": false
        },
        "assigned_at": "2025-09-18T06:05:00Z",
        "decided_at": null,
        "note": "재무 확인",
        "is_active": true
      }
    ],
    "approval": {
      "id": 5001,
      "transaction_no": "TRX-202511100001",
      "status": {
        "id": 1,
        "name": "대기",
        "is_blocking_next": true,
        "is_terminal": false
      },
      "current_step": {
        "id": 7001,
        "step_order": 1
      },
      "template_name": "입고 결재 기본",
      "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,
        "status_id": 1,
        "step_status_id": 1,
        "status": {
          "id": 1,
          "name": "대기",
          "is_blocking_next": true,
          "is_terminal": false
        },
        "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,
        "status_id": 1,
        "step_status_id": 1,
        "status": {
          "id": 1,
          "name": "대기",
          "is_blocking_next": true,
          "is_terminal": false
        },
        "assigned_at": "2025-09-18T06:05:00Z",
        "decided_at": null,
        "note": "재무 확인",
        "is_active": true
      }
    ],
    "approval": {
      "id": 5001,
      "transaction_no": "TRX-202511100001",
      "status": {
        "id": 1,
        "name": "대기",
        "is_blocking_next": true,
        "is_terminal": false
      },
      "current_step": {
        "id": 7001,
        "step_order": 1
      },
      "template_name": "입고 결재 기본",
      "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,
      "transaction_no": "TRX-202511100001",
      "status": {
        "id": 2,
        "name": "진행중",
        "is_blocking_next": true,
        "is_terminal": false
      },
      "current_step": {
        "id": 7002,
        "step_order": 2,
        "approver": {
          "id": 34,
          "employee_no": "E2025003",
          "name": "최검토"
        },
        "status": {
          "id": 3,
          "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,
          "action": {
            "id": 1,
            "name": "승인"
          },
          "action_at": "2025-09-18T08:05:00Z",
          "note": "승인합니다.",
          "from_status": {
            "id": 1,
            "name": "대기",
            "is_blocking_next": true,
            "is_terminal": false
          },
          "to_status": {
            "id": 2,
            "name": "진행중",
            "is_blocking_next": true,
            "is_terminal": false
          }
        }
      ]
    },
    "step": {
      "id": 7001,
      "approval_id": 5001,
      "step_order": 1,
      "approver_id": 21,
      "status_id": 2,
      "step_status_id": 2,
      "status": {
        "id": 2,
        "name": "진행중",
        "is_blocking_next": true,
        "is_terminal": false
      },
      "assigned_at": "2025-09-18T06:05:00Z",
      "decided_at": "2025-09-18T08:05:00Z",
      "note": "승인합니다."
    },
    "next_step": {
      "id": 7002,
      "step_order": 2,
      "approver": {
        "id": 34,
        "employee_no": "E2025003",
        "name": "최검토"
      },
      "status": {
        "id": 3,
        "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,
      "action": {
        "id": 1,
        "name": "승인"
      },
      "note": "승인합니다.",
      "action_at": "2025-09-18T08:05:00Z"
    }
  }
}

응답에는 전후 상태(from_status, to_status), 차기 단계 정보가 포함되며, approval_histories에 기록된다.

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": "보류 처리"
}

응답은 data.approval 구조로 최신 요약을 반환한다.

{
  "data": {
    "approval": {
      "id": 5001,
      "approval_no": "APP-202511100001",
      "status": {
        "id": 2,
        "name": "진행중",
        "is_blocking_next": true,
        "is_terminal": false
      },
      "current_step": {
        "id": 7002,
        "step_order": 2
      },
      "requester": {
        "id": 7,
        "employee_no": "E2025001",
        "name": "김승인"
      },
      "requested_at": "2025-09-18T06:00:00Z",
      "decided_at": null,
      "note": "보류 처리",
      "template_name": "입고 결재 기본",
      "updated_at": "2025-09-18T08:10:00Z"
    }
  }
}
  • DELETE /approvals/5001
  • POST /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,
      "action": {
        "id": 3,
        "name": "보류"
      },
      "action_at": "2025-09-18T08:05:00Z",
      "note": "보류 코멘트",
      "approver": {
        "id": 21,
        "employee_no": "E2025002",
        "employee_name": "박검토"
      },
      "from_status": {
        "id": 1,
        "name": "대기",
        "is_blocking_next": true,
        "is_terminal": false
      },
      "to_status": {
        "id": 2,
        "name": "진행중",
        "is_blocking_next": true,
        "is_terminal": false
      },
      "approval": {
        "id": 5001,
        "approval_no": "APP-202511100001",
        "status": {
          "id": 2,
          "name": "진행중",
          "is_blocking_next": true,
          "is_terminal": false
        }
      },
      "step": {
        "id": 7001,
        "approval_id": 5001,
        "step_order": 1,
        "approver": {
          "id": 21,
          "employee_no": "E2025002",
          "name": "박검토"
        },
        "status": {
          "id": 2,
          "name": "진행중",
          "is_blocking_next": true,
          "is_terminal": false
        }
      }
    }
  ],
  "page": 1,
  "page_size": 50,
  "total": 2
}

5.10 단계 개별 CRUD

  • GET /approval-steps?approval_id=5001&include=approval,approver,status{ items: [], page, page_size, total } 형태로 반환하며, 각 항목은 approval, approver, status 서브 오브젝트를 선택적으로 포함한다.
  • GET /approval-steps/7001?include=approval,approver,status{ data: { ... } }.
  • POST /approval-steps → 단일 단계를 생성하고 { data: { ... } } 형태로 생성된 요약을 반환한다. status_id(구 버전 호환용 step_status_id)를 생략하면 자동으로 대기 상태가 지정된다.
  • PATCH /approval-steps/{id} → 갱신된 단계 요약을 반환한다.
  • DELETE /approval-steps/{id}{ data: { id, deleted_at } }.
  • POST /approval-steps/{id}/restore{ data: { id, restored_at } }.

주요 필터 및 확장 파라미터:

  • approval_id, approval_step_id, approver_id, approval_action_id, status_id
  • q(결재번호·승인자 검색), action_from, action_to (ISO8601)
  • sort=action_at|created_at|updated_at, order=asc|desc
  • include 기본값은 approver,approval_action,from_status,to_status; approval, step, status 토큰으로 확장

GET /approval-histories/91001?include=approval,step

{
  "data": {
    "id": 91001,
    "approval_id": 5001,
    "approval_step_id": 7001,
    "action": {
      "id": 3,
      "name": "보류"
    },
    "action_at": "2025-09-18T08:05:00Z",
    "note": "보류 코멘트",
    "approver": {
      "id": 21,
      "employee_no": "E2025002",
      "employee_name": "박검토"
    },
    "from_status": null,
    "to_status": {
      "id": 2,
      "name": "진행중",
      "is_blocking_next": true,
      "is_terminal": false
    },
    "approval": {
      "id": 5001,
      "approval_no": "APP-202511100001",
      "status": {
        "id": 2,
        "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": "박검토"
      },
      "status": {
        "id": 2,
        "name": "진행중",
        "is_blocking_next": true,
        "is_terminal": false
      }
    }
  }
}

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

  • 공통 쿼리 파라미터: from, to, format=xlsx|pdf, transaction_status_id, approval_status_id, requested_by_id. 필요 시 delivery=metadata를 전달하면 스트리밍 대신 다운로드 메타데이터(JSON)를 반환한다.
  • 기본 응답(delivery=stream 또는 파라미터 생략)은 Content-Type을 포맷에 맞춰 설정하고 Content-Disposition: attachment; filename="<파일명>" 헤더를 포함한 바이트 스트림이다.
  • delivery=metadata 응답 예: GET /reports/transactions/export?from=2025-09-01&to=2025-09-30&transaction_status_id=2&delivery=metadata
    {
      "data": {
        "download_url": "/api/v1/reports/transactions/export?from=2025-09-01&to=2025-09-30&transaction_status_id=2&format=xlsx",
        "filename": "transactions_export_20250930120000.xlsx",
        "mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "expires_at": "2025-09-30T12:15:00Z"
      }
    }
    
  • 감사 로그와 권한 검증은 모든 Export 호출에 공통 적용한다.

7.1 트랜잭션 Export

GET /reports/transactions/export?from=2025-09-01&to=2025-09-30&transaction_status_id=2&warehouse_id=1&requested_by_id=7&format=xlsx

  • from, toyyyy-MM-dd 형식으로 요청하며, 날짜는 트랜잭션 발생일 기준이다.
  • 응답 열 구성: Transaction No, Transaction Date, Transaction Type, Status, Warehouse, Created By, Approval No, Approval Status.
  • approval_status_id, requested_by_id, transaction_status_id로 필터링이 가능하다.

7.2 결재 Export

GET /reports/approvals/export?approval_status_id=1&requested_by_id=7&from=2025-09-01T00:00:00Z&to=2025-09-30T23:59:59Z&format=pdf

  • from, to는 ISO8601 UTC 문자열이며 requested_at 기준으로 필터링한다.
  • 응답 열 구성: Approval No, Approval Status, Transaction No, Requested By, Requested At, Decided At, Current Step Order, Current Step Approver.

8. 인증 및 대시보드 API

  • POST /auth/login
    {
      "identifier": "user@example.com",
      "password": "Sup3rS3cret!",
      "remember_me": true
    }
    
    응답:
    {
      "data": {
        "access_token": "<jwt>",
        "refresh_token": "<jwt>",
        "expires_at": "2025-09-18T09:00:00Z",
        "user": {
          "id": 7,
          "name": "김승인",
          "employee_no": "E2025001",
          "email": "approver@example.com",
          "primary_group": {
            "id": 3,
            "name": "물류팀"
          }
        },
        "permissions": [
          {
            "resource": "/dashboard",
            "actions": ["read"]
          },
          {
            "resource": "/approvals",
            "actions": ["read", "update"]
          }
        ]
      }
    }
    
  • POST /auth/refresh
    {
      "refresh_token": "<jwt>"
    }
    
    응답은 로그인과 동일한 스키마를 따른다.
  • 인증 실패 시 401, 잠금·권한 오류 시 403을 반환하며 { "error": { "code": 401, "message": "...", "details": [...] } } 형식을 유지한다.
  • 토큰/계정 상태별 메시지 매핑
    • 잘못된 자격 증명: invalid credentials
    • 비활성 계정 접근: account is inactive
    • 만료된 토큰: token expired
    • 재사용·서명 오류: invalid token

8.1 대시보드 요약

GET /dashboard/summary

{
  "data": {
    "generated_at": "2025-09-18T08:00:00Z",
    "kpis": [
      {
        "key": "inbound_today",
        "label": "오늘 입고",
        "value": 12,
        "trend_label": "어제 대비",
        "delta": 0.2
      },
      {
        "key": "pending_approvals",
        "label": "대기 결재",
        "value": 5
      }
    ],
    "recent_transactions": [
      {
        "transaction_no": "TRX-202511100001",
        "transaction_date": "2025-09-18",
        "transaction_type": "입고",
        "status_name": "상신",
        "created_by": "김승인"
      }
    ],
    "pending_approvals": [
      {
        "approval_id": 5005,
        "approval_no": "APP-202511100005",
        "title": "출고 결재",
        "step_summary": "2단계/3단계 진행중",
        "requested_at": "2025-09-17T03:00:00Z"
      }
    ]
  }
}
  • pending_approvals[].approval_id는 결재 상세 조회(GET /approvals/{id})에 사용되는 approvals.id 값을 그대로 노출한다.

9. 구현 참고

  • FK 요약 정보는 기본 응답에 포함하며, 상세 정보가 필요하면 include 파라미터를 활용해 확장한다.
  • 배열 기반 다건 작업은 전체를 트랜잭션 처리해야 한다. 실패 시 롤백하고 부분 처리 결과를 반환하지 않는다.
  • is_active 변경은 권한·결재 등의 즉시성 요구를 고려하여 관련 캐시를 무효화한다.
  • 결재 단계 상태 전이는 approval_statuses.is_blocking_next 규칙을 준수해야 하며, 반려(is_terminal=true) 상태 시 결재를 종료한다.