정합성 문서 및 결재 입력 테스트 갱신

This commit is contained in:
JiWoong Sul
2025-10-17 16:09:57 +09:00
parent 7522f46693
commit b3da3a5c60
5 changed files with 544 additions and 162 deletions

View File

@@ -453,16 +453,20 @@
"transaction_no": "TXN-2025-0001",
"transaction_type": {
"id": 1,
"type_name": "입고"
"name": "입고"
},
"transaction_status": {
"id": 1,
"status_name": "초안"
"name": "초안"
},
"warehouse": {
"id": 1,
"warehouse_code": "WH-001",
"warehouse_name": "1센터"
"warehouse_name": "1센터",
"zipcode": {
"zipcode": "06000",
"road_name": "테헤란로"
}
},
"transaction_date": "2025-09-18",
"created_by": {
@@ -474,6 +478,7 @@
"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,
@@ -499,46 +504,50 @@
"customers": [
{
"id": 301,
"customer_code": "C001",
"customer_name": "ABC물류",
"contact_name": "박담당"
"customer": {
"id": 301,
"customer_code": "C001",
"customer_name": "ABC물류"
},
"note": null
}
],
"approval": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_status": {
"status": {
"id": 1,
"status_name": "대기",
"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",
"employee_name": "박검토"
},
"step_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true,
"is_terminal": false
"name": "박검토"
},
"assigned_at": "2025-09-18T06:05:00Z",
"decided_at": null,
"note": null
},
"requested_by": {
"requester": {
"id": 7,
"employee_no": "E2025001",
"employee_name": "김승인"
"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"
@@ -560,11 +569,11 @@
"transaction_no": "TXN-2025-0001",
"transaction_type": {
"id": 1,
"type_name": "입고"
"name": "입고"
},
"transaction_status": {
"id": 1,
"status_name": "초안"
"name": "초안"
},
"warehouse": {
"id": 1,
@@ -572,7 +581,8 @@
"warehouse_name": "1센터",
"zipcode": {
"zipcode": "06000",
"sido": "서울특별시"
"sido": "서울특별시",
"road_name": "테헤란로"
}
},
"transaction_date": "2025-09-18",
@@ -585,6 +595,7 @@
"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,
@@ -610,17 +621,21 @@
"customers": [
{
"id": 301,
"customer_code": "C001",
"customer_name": "ABC물류",
"contact_name": "박담당"
"customer": {
"id": 301,
"customer_code": "C001",
"customer_name": "ABC물류"
},
"note": null
}
],
"approval": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_status": {
"status": {
"id": 1,
"status_name": "대기",
"name": "대기",
"color": "#F97316",
"is_blocking_next": true,
"is_terminal": false
},
@@ -630,11 +645,11 @@
"approver": {
"id": 21,
"employee_no": "E2025002",
"employee_name": "박검토"
"name": "박검토"
},
"step_status": {
"status": {
"id": 1,
"status_name": "대기",
"name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
@@ -642,14 +657,58 @@
"decided_at": null,
"note": null
},
"requested_by": {
"requester": {
"id": 7,
"employee_no": "E2025001",
"employee_name": "김승인"
"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"
@@ -810,17 +869,17 @@
"approval": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_status": {
"status": {
"id": 1,
"status_name": "대기",
"name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
"current_step": null,
"requested_by": {
"requester": {
"id": 7,
"employee_no": "E2025001",
"employee_name": "김승인"
"name": "김승인"
},
"requested_at": "2025-09-18T06:00:00Z",
"decided_at": null,
@@ -848,61 +907,86 @@
"id": 9001,
"transaction_no": "TXN-2025-0001"
},
"approval_status": {
"status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true
"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",
"employee_name": "박검토"
},
"step_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true,
"is_terminal": false
"name": "박검토"
},
"assigned_at": "2025-09-18T06:05:00Z",
"decided_at": null,
"note": null
},
"requested_by": {
"requester": {
"id": 7,
"employee_no": "E2025001",
"employee_name": "김승인"
"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: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",
"employee_name": "박검토"
},
"step_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true,
"is_terminal": false
"name": "박검토"
},
"assigned_at": "2025-09-18T06:05:00Z",
"decided_at": null,
"note": null
}
],
"histories": []
"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,
@@ -922,34 +1006,35 @@
"id": 9001,
"transaction_no": "TXN-2025-0001"
},
"approval_status": {
"status": {
"id": 1,
"status_name": "대기",
"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",
"employee_name": "박검토"
},
"step_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true,
"is_terminal": false
"name": "박검토"
},
"assigned_at": "2025-09-18T06:05:00Z",
"decided_at": null,
"note": null
},
"requested_by": {
"requester": {
"id": 7,
"employee_no": "E2025001",
"employee_name": "김승인"
"name": "김승인"
},
"requested_at": "2025-09-18T06:00:00Z",
"decided_at": null,
@@ -958,22 +1043,47 @@
{
"id": 7001,
"step_order": 1,
"status": {
"id": 1,
"name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
"approver": {
"id": 21,
"employee_no": "E2025002",
"employee_name": "박검토"
},
"step_status": {
"id": 1,
"status_name": "대기",
"is_blocking_next": true
"name": "박검토"
},
"assigned_at": "2025-09-18T06:05:00Z",
"decided_at": null,
"note": null
}
],
"histories": [],
"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"
}
@@ -1011,6 +1121,12 @@
"step_order": 1,
"approver_id": 21,
"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,
@@ -1022,6 +1138,12 @@
"step_order": 2,
"approver_id": 34,
"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": "재무 확인",
@@ -1030,9 +1152,9 @@
],
"approval": {
"id": 5001,
"approval_status": {
"status": {
"id": 1,
"status_name": "대기",
"name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
@@ -1040,6 +1162,7 @@
"id": 7001,
"step_order": 1
},
"template_name": "입고 결재 기본",
"updated_at": "2025-09-18T06:05:00Z"
}
}
@@ -1077,6 +1200,12 @@
"step_order": 1,
"approver_id": 21,
"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": "서류 확인 중",
@@ -1088,6 +1217,12 @@
"step_order": 2,
"approver_id": 35,
"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": "재무 확인",
@@ -1096,9 +1231,9 @@
],
"approval": {
"id": 5001,
"approval_status": {
"status": {
"id": 1,
"status_name": "대기",
"name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
@@ -1106,6 +1241,7 @@
"id": 7001,
"step_order": 1
},
"template_name": "입고 결재 기본",
"updated_at": "2025-09-18T06:10:00Z"
}
}
@@ -1127,9 +1263,9 @@
"data": {
"approval": {
"id": 5001,
"approval_status": {
"status": {
"id": 2,
"status_name": "진행중",
"name": "진행중",
"is_blocking_next": true,
"is_terminal": false
},
@@ -1139,11 +1275,11 @@
"approver": {
"id": 34,
"employee_no": "E2025003",
"employee_name": "최검토"
"name": "최검토"
},
"step_status": {
"status": {
"id": 3,
"status_name": "진행중",
"name": "진행중",
"is_blocking_next": true,
"is_terminal": false
},
@@ -1155,18 +1291,21 @@
"histories": [
{
"id": 91001,
"approval_action_id": 1,
"action": {
"id": 1,
"name": "승인"
},
"action_at": "2025-09-18T08:05:00Z",
"note": "승인합니다.",
"from_status": {
"id": 1,
"status_name": "대기",
"name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
"to_status": {
"id": 2,
"status_name": "진행중",
"name": "진행중",
"is_blocking_next": true,
"is_terminal": false
}
@@ -1179,15 +1318,15 @@
"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": {
"status": {
"id": 2,
"status_name": "진행중",
"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,
@@ -1195,11 +1334,11 @@
"approver": {
"id": 34,
"employee_no": "E2025003",
"employee_name": "최검토"
"name": "최검토"
},
"step_status": {
"status": {
"id": 3,
"status_name": "진행중",
"name": "진행중",
"is_blocking_next": true,
"is_terminal": false
},
@@ -1210,7 +1349,10 @@
"history": {
"id": 91001,
"approval_step_id": 7001,
"approval_action_id": 1,
"action": {
"id": 1,
"name": "승인"
},
"note": "승인합니다.",
"action_at": "2025-09-18T08:05:00Z"
}
@@ -1247,9 +1389,9 @@
"approval": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_status": {
"status": {
"id": 2,
"status_name": "진행중",
"name": "진행중",
"is_blocking_next": true,
"is_terminal": false
},
@@ -1257,14 +1399,15 @@
"id": 7002,
"step_order": 2
},
"requested_by": {
"requester": {
"id": 7,
"employee_no": "E2025001",
"employee_name": "김승인"
"name": "김승인"
},
"requested_at": "2025-09-18T06:00:00Z",
"decided_at": null,
"note": "보류 처리",
"template_name": "입고 결재 기본",
"updated_at": "2025-09-18T08:10:00Z"
}
}
@@ -1282,7 +1425,10 @@
"id": 91001,
"approval_id": 5001,
"approval_step_id": 7001,
"approval_action_id": 3,
"action": {
"id": 3,
"name": "보류"
},
"action_at": "2025-09-18T08:05:00Z",
"note": "보류 코멘트",
"approver": {
@@ -1292,22 +1438,22 @@
},
"from_status": {
"id": 1,
"status_name": "대기",
"name": "대기",
"is_blocking_next": true,
"is_terminal": false
},
"to_status": {
"id": 2,
"status_name": "진행중",
"name": "진행중",
"is_blocking_next": true,
"is_terminal": false
},
"approval": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_status": {
"status": {
"id": 2,
"status_name": "진행중",
"name": "진행중",
"is_blocking_next": true,
"is_terminal": false
}
@@ -1319,7 +1465,13 @@
"approver": {
"id": 21,
"employee_no": "E2025002",
"employee_name": "박검토"
"name": "박검토"
},
"status": {
"id": 2,
"name": "진행중",
"is_blocking_next": true,
"is_terminal": false
}
}
}
@@ -1331,8 +1483,8 @@
```
### 5.10 단계 개별 CRUD
- `GET /approval-steps?approval_id=5001&include=approver,step_status``{ items: [], page, page_size, total }` 형태로 반환하며, 각 항목은 `approval`, `approver`, `step_status` 서브 오브젝트를 선택적으로 포함한다.
- `GET /approval-steps/7001?include=approval,approver,step_status``{ data: { ... } }`.
- `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: { ... } }` 형태로 생성된 요약을 반환한다. `step_status_id`를 생략하면 자동으로 `대기` 상태가 지정된다.
- `PATCH /approval-steps/{id}` → 갱신된 단계 요약을 반환한다.
- `DELETE /approval-steps/{id}``{ data: { id, deleted_at } }`.
@@ -1352,7 +1504,10 @@
"id": 91001,
"approval_id": 5001,
"approval_step_id": 7001,
"approval_action_id": 3,
"action": {
"id": 3,
"name": "보류"
},
"action_at": "2025-09-18T08:05:00Z",
"note": "보류 코멘트",
"approver": {
@@ -1363,16 +1518,16 @@
"from_status": null,
"to_status": {
"id": 2,
"status_name": "진행중",
"name": "진행중",
"is_blocking_next": true,
"is_terminal": false
},
"approval": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_status": {
"status": {
"id": 2,
"status_name": "진행중",
"name": "진행중",
"is_blocking_next": true,
"is_terminal": false
}
@@ -1385,6 +1540,12 @@
"id": 21,
"employee_no": "E2025002",
"employee_name": "박검토"
},
"status": {
"id": 2,
"name": "진행중",
"is_blocking_next": true,
"is_terminal": false
}
}
}
@@ -1512,36 +1673,132 @@
---
## 7. 보고서 Export
- `format=xlsx|pdf` 파라미터를 지원한다. 현재 구현은 XLSX 다운로드만 제공하며, `format=pdf` 요청 시 400 Bad Request를 반환한다.
- `delivery=stream|metadata`(기본값 `stream`) 파라미터를 지원한다.
- `delivery=metadata` 요청 시 다운로드 메타데이터를 반환한다.
`GET /reports/transactions/export?from=2025-09-01&to=2025-09-30&transaction_status_id=2&delivery=metadata`
```json
{
"data": {
"download_url": "/api/v1/reports/transactions/export?from=2025-09-01&to=2025-09-30&transaction_status_id=2&delivery=stream&exported_at=2025-09-30T12:00:00Z",
"filename": "transactions_export_20250930120000.xlsx",
"mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"expires_at": "2025-09-30T12:15:00Z"
}
- 공통 쿼리 파라미터: `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`
```json
{
"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"
}
```
`download_url`에는 `delivery=stream`과 `exported_at` 값이 포함되며, 해당 URL을 그대로 호출하면 동일한 파일명을 유지한 스트리밍 응답을 받을 수 있다.
- `delivery=stream` 요청(기본값)은 `Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`, `Content-Disposition: attachment` 헤더를 포함한 바이트 스트림을 반환한다.
}
```
- 감사 로그와 권한 검증은 모든 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`
- 열 구성: `Transaction No`, `Transaction Date`, `Transaction Type`, `Status`, `Warehouse`, `Created By`, `Approval No`, `Approval Status`
- `approval_status_id`, `requested_by_id` 파라미터로 결재 상태·요청자 기준 필터링이 가능하다.
- `from`, `to` `yyyy-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=xlsx`
- 열 구성: `Approval No`, `Approval Status`, `Transaction No`, `Requested By`, `Requested At`, `Decided At`, `Current Step Order`, `Current Step Approver`
- `from`, `to` 파라미터는 `requested_at` 기준 UTC 타임스탬프 필터로 동작한다.
`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. 구현 참고
## 8. 인증 및 대시보드 API
- `POST /auth/login`
```json
{
"identifier": "user@example.com",
"password": "Sup3rS3cret!",
"remember_me": true
}
```
응답:
```json
{
"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`
```json
{
"refresh_token": "<jwt>"
}
```
응답은 로그인과 동일한 스키마를 따른다.
- 인증 실패 시 `401`, 잠금·권한 오류 시 `403`을 반환하며 `{ "error": { "code": 401, "message": "...", "details": [...] } }` 형식을 유지한다.
- 토큰/계정 상태별 메시지 매핑
- 잘못된 자격 증명: `invalid credentials`
- 비활성 계정 접근: `account is inactive`
- 만료된 토큰: `token expired`
- 재사용·서명 오류: `invalid token`
### 8.1 대시보드 요약
`GET /dashboard/summary`
```json
{
"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": "TXN-2025-0001",
"transaction_date": "2025-09-18",
"transaction_type": "입고",
"status_name": "상신",
"created_by": "김승인"
}
],
"pending_approvals": [
{
"approval_no": "APP-2025-0005",
"title": "출고 결재",
"step_summary": "2단계/3단계 진행중",
"requested_at": "2025-09-17T03:00:00Z"
}
]
}
}
```
---
## 9. 구현 참고
- FK 요약 정보는 기본 응답에 포함하며, 상세 정보가 필요하면 `include` 파라미터를 활용해 확장한다.
- 배열 기반 다건 작업은 전체를 트랜잭션 처리해야 한다. 실패 시 롤백하고 부분 처리 결과를 반환하지 않는다.
- `is_active` 변경은 권한·결재 등의 즉시성 요구를 고려하여 관련 캐시를 무효화한다.