API v4 계약 반영하고 보고서·입출고 화면 실연동 강화

This commit is contained in:
JiWoong Sul
2025-10-16 14:57:07 +09:00
parent 7e0f7b1c55
commit d5c99627db
34 changed files with 1767 additions and 327 deletions

View File

@@ -6,6 +6,14 @@
---
## 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: { ... } }`.
@@ -206,6 +214,7 @@
"id": 301,
"customer_code": "C001",
"customer_name": "ABC물류",
"contact_name": "박담당",
"is_partner": true,
"is_general": false,
"email": "contact@abc.com",
@@ -229,6 +238,8 @@
}
```
> `contact_name`은 고객사 담당자 실명. 선택 입력이며 미입력 시 `null`.
`GET /employees?page=1`
```json
{
@@ -270,7 +281,8 @@
"menu": {
"id": 12,
"menu_code": "STOCK_MGMT",
"menu_name": "입출고 관리"
"menu_name": "입출고 관리",
"route_path": "/inventory/transactions"
},
"can_create": true,
"can_read": true,
@@ -418,10 +430,16 @@
"unit_price": 0
}
],
"customers": []
"customers": [],
"approval": {
"approval_no": "APP-2025-0001",
"requested_by_id": 7,
"note": "입고 결재"
}
}
```
응답은 생성된 트랜잭션 전체 정보를 반환하며, 라인·고객 식별자가 포함된다.
응답은 생성된 트랜잭션 전체 정보를 반환하며, 라인·고객 식별자가 포함된다. `approval`
블록은 결재 생성에 필요한 정보를 담으며 생략할 수 없다.
### 4.2 목록 조회
`GET /stock-transactions?include=lines,customers,approval`
@@ -476,8 +494,53 @@
"note": null
}
],
"customers": [],
"approval": 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,
@@ -542,8 +605,53 @@
"note": null
}
],
"customers": [],
"approval": 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"
}
}
}
```
@@ -632,15 +740,56 @@
### 4.7 상태 전이 권장 API
- `POST /stock-transactions/9001/submit`
- `POST /stock-transactions/9001/complete`
```json
{
"id": 9001,
"note": "승인 요청"
}
```
응답은 `{ "data": { "id": 9001, "transaction_status": { ... }, "updated_at": "..." } }` 형태.
- `POST /stock-transactions/9001/complete`
```json
{
"id": 9001,
"note": "처리 완료"
}
```
- `POST /stock-transactions/9001/approve`
```json
{
"id": 9001,
"note": "최종 승인"
}
```
- `POST /stock-transactions/9001/reject`
```json
{
"id": 9001,
"note": "재작업 필요"
}
```
- `POST /stock-transactions/9001/cancel`
```json
{
"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`
```json
@@ -671,7 +820,24 @@
"status_name": "대기",
"is_blocking_next": true
},
"current_step": null,
"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",
@@ -683,7 +849,26 @@
"is_active": true,
"created_at": "2025-09-18T06:00:00Z",
"updated_at": "2025-09-18T06:00:00Z",
"steps": [],
"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": []
}
],
@@ -710,7 +895,24 @@
"is_blocking_next": true,
"is_terminal": false
},
"current_step": null,
"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",
@@ -764,6 +966,52 @@
]
}
```
응답:
```json
{
"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`
@@ -784,6 +1032,52 @@
]
}
```
응답:
```json
{
"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`
@@ -794,7 +1088,104 @@
"note": "승인합니다."
}
```
응답:
```json
{
"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`
@@ -820,6 +1211,126 @@
- `DELETE /approvals/5001`
- `POST /approvals/5001/restore`
### 5.9 결재 이력 조회
`GET /approval-histories?approval_id=5001&include=approval,step,approver`
```json
{
"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_id`
- `include=approval,approver,step_status`
`GET /approval-histories/91001?include=approval,step`
```json
{
"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
@@ -940,11 +1451,20 @@
---
## 7. 보고서 API (선택)
- `GET /reports/transactions/export?from=2025-09-01&to=2025-09-30&type_id=2&warehouse_id=1&format=xlsx`
- `GET /reports/approvals/export?status_id=1&format=pdf`
## 7. 보고서 Export
- `format=xlsx|pdf` 파라미터를 지원한다. 현재는 `format=xlsx`만 성공하며, `format=pdf`를 지정하면 400 Bad Request가 반환된다.
- 응답은 즉시 다운로드 스트림으로 전달되며 `Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet``Content-Disposition: attachment` 헤더가 포함된다. 프론트엔드는 받은 바이트 스트림을 그대로 파일로 저장해야 한다.
응답은 파일 다운로드 링크 또는 스트림. 요청 파라미터에는 대상 리소스의 PK를 포함한다.
### 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 타임스탬프 범위 필터다.
---