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

@@ -1,56 +1,52 @@
# 백엔드 수정 요청서 (Master/Transaction API 확장)
# 백엔드 수정 요청서 (2024-08-XX 갱신)
## 1. 배경
- 프론트엔드에서 인벤토리/승인 플로우를 실데이터에 맞춰 구현하기 위해서는 백엔드가 스펙(`stock_approval_system_api_v4.md`)상의 모든 마스터와 재고 트랜잭션 API를 제공해야 한다.
- 현 시점에는 `vendors`, `uoms`, `transaction_types`, `transaction_statuses`, `approval_statuses`, `approval_actions`, `warehouses` 엔드포인트까지만 구현되어 있으며, Flutter 화면은 아직 mock 데이터를 사용 중이다.
- 백엔드 코드를 직접 수정할 수 없는 상황이므로, 필요한 변경 사항을 명확히 정리해 전담 팀/담당자에게 전달한다.
- Flutter 프론트엔드(`superport_v2`)가 최신 백엔드(`superport_api_v2`)와 실연동을 준비하면서, 일부 엔드포인트가 미구현이거나 응답 스키마가 불완전해 화면 기능을 마무리하기 어렵다.
- 프론트는 Clean Architecture 기반으로 이미 도메인/레포지토리 계층을 구성했으며, UI 스펙에 맞춰 API 계약을 준수해야 한다.
- 본 문서는 백엔드 측 변경·추가 개발이 필요한 항목을 정리해 담당자에게 전달하기 위한 요청서이다.
## 2. 요청 범위
### 2.1 기본 경로 정렬
- 모든 REST 엔드포인트는 `/api/v1` prefix 하위로 노출되어야 하며, 기존 구현(vendors/uoms/transaction-types/transaction-statuses/approval-statuses/approval-actions/warehouses)도 동일한 경로를 유지해야 한다.
- OpenAPI/스펙 문서에는 버전 프리픽스가 명확히 표기되어야 하며, 변경 시 프론트엔드가 사용하는 베이스 URL(`Environment.baseUrl`)과 일치하도록 공지한다.
## 2. 주요 이슈 요약
- 보고서 다운로드 화면이 호출하는 `/api/v1/reports/**` 엔드포인트가 존재하지 않는다.
- 결재 단계(`approval-steps`) 관련 API가 단계 CRUD 조회/생성 응답을 돌려주지 않아 프론트 단계 관리 화면을 구현할 수 없다.
- 결재 단계 액션(`POST /approval-steps/{id}/actions`)과 승인 단계 일괄 등록/수정(`POST/PATCH /approvals/{id}/steps`)이 204만 반환해 프론트가 최신 결재 정보를 다시 받지 못한다.
- 그룹-메뉴 권한 목록이 메뉴 `route_path` 정보를 제공하지 않아 권한 매니저가 라우트별 권한을 구성할 수 없다.
### 2.2 마스터 데이터 API 확대
- 대상 테이블: `customers`, `products`, `employees`, `groups`, `menus`, `group_menu_permissions`, `approval_templates`, `approval_steps`(정의), `zipcodes` 검색용 API 등
- 요구 사항
- `/api/v1/<resource>` 패턴으로 목록/상세/생성/수정/삭제/복구 CRUD 일관성 유지
- 목록 API는 검색(q), 활성/비활성 필터, soft-delete 필터, 정렬(sort/order), 페이지네이션(page/page_size) 지원
- 관계형 데이터는 `find_also_related` 패턴으로 DTO에 포함 (예: 고객→zipcode, 그룹→permissions, 직원→group)
- 프론트엔드 Remote Repository(`lib/features/masters/**/data/repositories`)와 엔티티 스키마를 맞추기 위해 스펙 필드명 그대로 응답
## 3. 상세 요청
### 2.3 결재(Approval) 도메인 확장
- 리소스: `/approvals`, `/approval-steps`, `/approval-histories`, `/approval-templates`
- 요구 사항
- 리스트/상세 API는 `include=steps,histories` 등 프론트가 사용하는 확장 파라미터를 지원해야 한다.
- 단계 배정(`POST /approvals/{id}/steps`), 단계 재배치(`PATCH /approvals/{id}/steps`), 단계 액션 수행(`POST /approval-steps/{id}/actions`)을 스펙대로 구현
- 승인 가능 여부 조회(`GET /approvals/{id}/can-proceed`) 및 복구(`/approvals/{id}/restore`) 포함
- 응답에는 Domain DTO(`ApprovalDto`, `ApprovalActionDto` 등)에서 필요로 하는 필드가 누락되지 않도록 검증
### 3.1 보고서 Export API 구현
- 엔드포인트:
- `GET /api/v1/reports/transactions/export`
- `GET /api/v1/reports/approvals/export`
- 요구 사항:
- 쿼리 파라미터: `from`, `to`(ISO 8601), `format`(xlsx|pdf), `type_id`, `status_id`, `warehouse_id` 등 프론트에서 사용하는 필터 수용.
- 응답:
- 파일 다운로드(바이트 스트림) 또는 `data.download_url`, `data.filename`, `data.mime_type`, `data.expires_at`을 포함한 JSON.
- 인증/권한 정책 확정 후 문서화.
### 2.4 재고 트랜잭션 API 설계 및 구현
- 리소스: `stock_transactions` (입고/출고/대여), `transaction_lines`, `transaction_approvals` 등 스펙 정의 테이블
- 요구 사항
- 목록 필터: 상태, 창고, 고객/거래처, 기간(처리일/반납예정일 등), 포함(include=lines, approval_history 등)
- 상세 응답: 헤더 정보 + 라인아이템 + 승인 이력/로그 전달
- 상태 전이/승인 플로우 API (`submit`, `approve`, `reject`, `cancel`)와 재고 처리 결과 반영
- soft-delete 및 복구 정책 정의 (필요 시 논의)
- SeaORM 트랜잭션을 이용해 헤더/라인/로그 동시 저장
### 3.2 결재 단계/행위 API 정합성 보강
- `GET /api/v1/approval-steps` 및 단건/생성/수정/삭제/복구 API 구현이 필요하다. (프론트 `ApprovalStepRepository`가 CRUD 전체를 호출)
- `POST /api/v1/approval-steps/{id}/actions`는 204 대신 갱신된 결재 본문 혹은 최소한 단계와 상태 변화를 반환해야 한다.
- `POST|PATCH /api/v1/approvals/{id}/steps` 역시 204가 아닌
- 변경된 `approval` 요약, 혹은
- 새로 구성된 단계 리스트
를 포함한 JSON 응답을 제공해 프론트가 재조회 없이 상태를 갱신할 수 있도록 해 달라.
- 액션/단계 요청 본문은 `stock_approval_system_api_v4.md` 스펙과 동일하게 유지.
### 2.5 공통 고려사항
- DTO/응답 구조는 `stock_approval_system_api_v4.md`와 동기화하고, 변경 시 문서도 업데이트
- `script/run_api_tests.sh`에 각 리소스의 CRUD 및 상태 전이 스텝을 추가해 회귀 테스트 가능하도록 보완
- 샘플 데이터(`migration/002_sample_data.sql`)는 필수 참조 데이터만 유지하고, 대량 더미는 옵션 플래그로 분리
### 3.3 그룹-메뉴 권한 응답 확장
- `GET /api/v1/group-menu-permissions` 및 단건 응답의 `menu` 객체에 다음 필드를 추가:
- `route_path` (launched 메뉴일 경우 실제 라우트 경로)
- 필요 시 `menu_code` 그대로 유지.
- 선택적으로 `include=group` 파라미터를 지원해 그룹 요약을 함께 반환하면 Front 권한 동기화 시 재조회가 줄어든다.
## 3. 선행 작업 및 의존성
- 데이터 모델 검증: 스펙과 현재 DB 스키마 일치 여부 확인, 필요한 경우 추가 마이그레이션 작성
- 인증/권한: 그룹-메뉴 권한 매핑을 API 보호 미들웨어에 적용 (추후 프론트엔드 권한 제어와 연동)
- 로깅/관측성: 주요 재고 트랜잭션 이벤트를 tracing 로그로 남겨 운영 대응
### 3.4 응답/에러 문서화
- 위 변경 사항이 반영되면 `stock_approval_system_api_v4.md`를 업데이트하고, 각 엔드포인트 예제 응답을 최신 상태로 반영한다.
- 회귀 테스트(`cargo test` + 통합 시나리오 스크립트)가 변경된 계약을 검증하도록 보강한다.
## 4. 수용 기준 (Acceptance Criteria)
- 모든 신규/확장된 엔드포인트에 대해 Actix 라우트, 도메인 DTO, SeaORM 리포지토리, 에러 매핑이 완비되어야 한다.
- `cargo check`, `cargo test`, `script/run_api_tests.sh`가 통과해야 하며, 샘플 DB로 기본 CRUD 시나리오가 동작할 것.
- README `Next Steps` 섹션 업데이트와 변경된 API 스펙 커밋이 포함되어야 한다.
## 4. 수용 기준
- 상기 엔드포인트가 모두 구현되고, 요청/응답이 문서와 일치해야 한다.
- 레거시 응답(204)에서 JSON 반환으로 변경될 경우, 클라이언트가 기대하는 키(`data.approval`, `data.steps` 등)를 포함해야 한다.
- `cargo fmt`, `cargo check`, `cargo test` 및 기존 CI 파이프라인이 통과한다.
## 5. 후속 조치
- 본 문서 확인 후 백엔드 담당자가 작업 범위/일정을 산출
- 작업 완료 시 프론트엔드 팀에 API mock 제거 및 실연동 착수 일정 공유
- 필요 시 추가 논의 사항을 본 문서 하단에 코멘트 형태로 기록
- 백엔드 담당자가 일정/우선순위를 산출해 프론트 팀과 공유.
- 구현 완료 후 샌드박스 환경에서 API 계약 검증 → 프론트엔드 실연동 착수.

View File

@@ -114,3 +114,18 @@
- [x] 그룹-메뉴 권한 복구 미구현(4건)은 `/group-menu-permissions/{id}/restore` 엔드포인트 공개 후 프론트 통합 테스트에 포함시킨다. (2025-10-19) 동일 문서 2.2절에 복구 API 요구사항을 명시하고 테스트 시나리오를 정리했다.
- [x] 프론트단에서는 `ApiErrorMapper``Failure` 파서를 보강해 403/409/422 응답 메시지를 토스트·다이얼로그에 그대로 노출하고, 재시도 시 가이드 문구를 제공한다.
- [x] 백엔드 수정 전까지 승인/취소 버튼에는 기능 플래그를 적용해 운영 환경에서 잘못된 전이 요청이 발생하지 않도록 보호한다. (2025-10-19) `FEATURE_STOCK_TRANSITIONS_ENABLED` 플래그를 추가하고 입·출·대여 화면에서 버튼을 비활성화하며 안내 배지를 노출하도록 조정했다.
## 8. 재고 생성 결재 정보 수집 계획 (2024-08-XX 업데이트)
1. **신규 입력 필드 구성**
- 입고/출고/대여 등록 모달에 “결재 정보” 섹션을 추가하고 `거래번호`, `결재번호`, `결재 메모`, `결재 요청자` 필드를 배치한다.
- 거래번호는 수동 입력 + “번호 자동 생성” 버튼을 제공하고, 후자는 시퀀스 API(백엔드 지원 필요)와 연동한다.
- 결재 요청자는 기존 작성자 자동완성 컴포넌트를 재사용해 `requested_by_id`로 매핑한다.
2. **컨트롤러/검증 로직**
- `StockTransactionCreateInput``StockTransactionApprovalInput`을 추가해 `approval_no`, `approval_status_id`, `requested_by_id`, `note`를 묶어서 전송한다.
- 검증 단계에서 거래번호/결재번호 누락 여부를 체크하고, 승인 상태는 Lookup(`fetchApprovalStatuses`)에서 “대기” ID를 로딩해 기본값으로 사용한다.
3. **사용자 경험 보완**
- 결재 템플릿 선택 시 템플릿에서 결재번호 규칙·승인자를 추천하고, 수동 변경 시 경고 메시지를 노출한다.
- 저장 직전 `/approvals` 간단 조회 또는 별도 중복 체크 API로 결재번호·거래번호 중복을 사전 확인한다.
4. **후속 일정**
- 1차 목표는 필수 필드 수집과 API 호출 연계이며, 템플릿 적용/번호 시퀀스 API는 백엔드 명세 확정 이후 2차 작업으로 분리한다.
- 컨트롤러 단위 테스트·위젯 테스트에 승인 정보 입력 시나리오를 추가하고 QA 문서에도 신규 체크리스트를 반영한다.

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 타임스탬프 범위 필터다.
---