feat: [approval] 전자결재 모듈 API 구현
- 마이그레이션 4개 (approval_forms, approval_lines, approvals, approval_steps) - 모델 4개 (ApprovalForm, ApprovalLine, Approval, ApprovalStep) - ApprovalService 비즈니스 로직 (양식/결재선 CRUD, 기안함/결재함/참조함, 결재 액션) - 컨트롤러 3개 (ApprovalFormController, ApprovalLineController, ApprovalController) - FormRequest 13개 (양식/결재선/문서 검증) - Swagger 문서 3개 (26개 엔드포인트) - i18n 메시지/에러 키 추가 - 라우트 26개 등록
This commit is contained in:
139
CURRENT_WORKS.md
139
CURRENT_WORKS.md
@@ -1,5 +1,144 @@
|
||||
# SAM API 작업 현황
|
||||
|
||||
## 2025-12-17 (화) - 전자결재 모듈 API 개발
|
||||
|
||||
### 작업 목표
|
||||
- `docs/plans/erp-api-development-plan.md` Phase 2의 3.1 전자결재 모듈
|
||||
- 결재 양식 (approval_forms), 결재선 템플릿 (approval_lines), 결재 문서 (approvals) CRUD API 구현
|
||||
- 기안함, 결재함, 참조함 조회 및 결재 액션 (상신, 승인, 반려, 회수) 기능
|
||||
|
||||
### 생성된 마이그레이션 (4개)
|
||||
|
||||
| 파일명 | 설명 |
|
||||
|--------|------|
|
||||
| `2025_12_17_200001_create_approval_forms_table.php` | 결재 양식 테이블 (name, code, category, template JSON) |
|
||||
| `2025_12_17_200002_create_approval_lines_table.php` | 결재선 템플릿 테이블 (name, steps JSON, is_default) |
|
||||
| `2025_12_17_200003_create_approvals_table.php` | 결재 문서 테이블 (form_id, drafter_id, title, content JSON, status) |
|
||||
| `2025_12_17_200004_create_approval_steps_table.php` | 결재 단계 테이블 (approval_id, step_order, type, user_id, status) |
|
||||
|
||||
### 생성된 모델 (4개)
|
||||
|
||||
**app/Models/Tenants/ApprovalForm.php:**
|
||||
- 결재 양식 모델 (BelongsToTenant, SoftDeletes)
|
||||
- Relations: creator(), approvals()
|
||||
- Scopes: active()
|
||||
|
||||
**app/Models/Tenants/ApprovalLine.php:**
|
||||
- 결재선 템플릿 모델 (BelongsToTenant, SoftDeletes)
|
||||
- Relations: creator()
|
||||
- Methods: getStepCountAttribute()
|
||||
|
||||
**app/Models/Tenants/Approval.php:**
|
||||
- 결재 문서 모델 (BelongsToTenant, SoftDeletes)
|
||||
- 상태: draft → pending → approved/rejected/cancelled
|
||||
- Relations: form(), drafter(), steps(), currentStepApprover()
|
||||
- Methods: canEdit(), canDelete(), canSubmit(), canAction(), canCancel()
|
||||
|
||||
**app/Models/Tenants/ApprovalStep.php:**
|
||||
- 결재 단계 모델 (SoftDeletes)
|
||||
- 단계 유형: approval, agreement, reference
|
||||
- Relations: approval(), user()
|
||||
- Methods: isPending(), isApproved(), isRejected()
|
||||
|
||||
### 생성된 서비스 (1개)
|
||||
|
||||
**app/Services/ApprovalService.php:**
|
||||
- Form CRUD: formIndex, formShow, formStore, formUpdate, formDestroy, formActive
|
||||
- Line CRUD: lineIndex, lineShow, lineStore, lineUpdate, lineDestroy
|
||||
- Approval CRUD: show, store, update, destroy
|
||||
- 기안함: drafts, draftsSummary
|
||||
- 결재함: inbox, inboxSummary
|
||||
- 참조함: referenceList
|
||||
- 액션: submit, approve, reject, cancel, markRead, markUnread
|
||||
|
||||
### 생성된 FormRequest (13개)
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `FormIndexRequest.php` | 양식 목록 조회 파라미터 |
|
||||
| `FormStoreRequest.php` | 양식 생성 검증 (name, code, template) |
|
||||
| `FormUpdateRequest.php` | 양식 수정 검증 |
|
||||
| `LineIndexRequest.php` | 결재선 목록 조회 파라미터 |
|
||||
| `LineStoreRequest.php` | 결재선 생성 검증 (name, steps) |
|
||||
| `LineUpdateRequest.php` | 결재선 수정 검증 |
|
||||
| `IndexRequest.php` | 기안함 조회 파라미터 |
|
||||
| `InboxIndexRequest.php` | 결재함 조회 파라미터 |
|
||||
| `ReferenceIndexRequest.php` | 참조함 조회 파라미터 |
|
||||
| `StoreRequest.php` | 문서 생성 검증 (form_id, title) |
|
||||
| `UpdateRequest.php` | 문서 수정 검증 |
|
||||
| `SubmitRequest.php` | 상신 검증 (steps 필수) |
|
||||
| `RejectRequest.php` | 반려 검증 (comment 필수) |
|
||||
|
||||
### 생성된 컨트롤러 (3개)
|
||||
|
||||
| 파일 | 엔드포인트 |
|
||||
|------|-----------|
|
||||
| `ApprovalFormController.php` | index, active, show, store, update, destroy |
|
||||
| `ApprovalLineController.php` | index, show, store, update, destroy |
|
||||
| `ApprovalController.php` | drafts, draftsSummary, inbox, inboxSummary, reference, show, store, update, destroy, submit, approve, reject, cancel, markRead, markUnread |
|
||||
|
||||
### 수정된 파일
|
||||
|
||||
**routes/api.php:**
|
||||
- Approval Forms 라우트 그룹 추가 (6개 라우트)
|
||||
- Approval Lines 라우트 그룹 추가 (5개 라우트)
|
||||
- Approvals 라우트 그룹 추가 (15개 라우트)
|
||||
|
||||
**lang/ko/message.php:**
|
||||
- approval 섹션 추가 (16개 키)
|
||||
|
||||
**lang/ko/error.php:**
|
||||
- approval 섹션 추가 (15개 키)
|
||||
|
||||
### 생성된 Swagger 문서 (3개)
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `app/Swagger/v1/ApprovalFormApi.php` | 결재 양식 API 문서 (6개 엔드포인트) |
|
||||
| `app/Swagger/v1/ApprovalLineApi.php` | 결재선 API 문서 (5개 엔드포인트) |
|
||||
| `app/Swagger/v1/ApprovalApi.php` | 전자결재 API 문서 (15개 엔드포인트) |
|
||||
|
||||
### API 엔드포인트
|
||||
|
||||
**결재 양식 API (Approval Forms):**
|
||||
- `GET /api/v1/approval-forms` - 목록 조회
|
||||
- `POST /api/v1/approval-forms` - 생성
|
||||
- `GET /api/v1/approval-forms/active` - 활성 양식 (셀렉트박스용)
|
||||
- `GET /api/v1/approval-forms/{id}` - 상세 조회
|
||||
- `PATCH /api/v1/approval-forms/{id}` - 수정
|
||||
- `DELETE /api/v1/approval-forms/{id}` - 삭제
|
||||
|
||||
**결재선 API (Approval Lines):**
|
||||
- `GET /api/v1/approval-lines` - 목록 조회
|
||||
- `POST /api/v1/approval-lines` - 생성
|
||||
- `GET /api/v1/approval-lines/{id}` - 상세 조회
|
||||
- `PATCH /api/v1/approval-lines/{id}` - 수정
|
||||
- `DELETE /api/v1/approval-lines/{id}` - 삭제
|
||||
|
||||
**전자결재 API (Approvals):**
|
||||
- `GET /api/v1/approvals/drafts` - 기안함
|
||||
- `GET /api/v1/approvals/drafts/summary` - 기안함 현황
|
||||
- `GET /api/v1/approvals/inbox` - 결재함
|
||||
- `GET /api/v1/approvals/inbox/summary` - 결재함 현황
|
||||
- `GET /api/v1/approvals/reference` - 참조함
|
||||
- `POST /api/v1/approvals` - 문서 생성
|
||||
- `GET /api/v1/approvals/{id}` - 상세 조회
|
||||
- `PATCH /api/v1/approvals/{id}` - 수정
|
||||
- `DELETE /api/v1/approvals/{id}` - 삭제
|
||||
- `POST /api/v1/approvals/{id}/submit` - 상신
|
||||
- `POST /api/v1/approvals/{id}/approve` - 승인
|
||||
- `POST /api/v1/approvals/{id}/reject` - 반려
|
||||
- `POST /api/v1/approvals/{id}/cancel` - 회수
|
||||
- `POST /api/v1/approvals/{id}/read` - 열람
|
||||
- `POST /api/v1/approvals/{id}/unread` - 미열람
|
||||
|
||||
### 검증 완료
|
||||
- ✅ Pint 스타일 검사 통과 (19개 파일)
|
||||
- ✅ 라우트 등록 확인 (26개)
|
||||
- ✅ Swagger 문서 생성 완료
|
||||
|
||||
---
|
||||
|
||||
## 2025-12-17 (화) - 매출/매입 관리 API 개발
|
||||
|
||||
### 작업 목표
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 논리적 데이터베이스 관계 문서
|
||||
|
||||
> **자동 생성**: 2025-12-15 16:04:28
|
||||
> **자동 생성**: 2025-12-17 23:09:53
|
||||
> **소스**: Eloquent 모델 관계 분석
|
||||
|
||||
## 📊 모델별 관계 현황
|
||||
@@ -166,8 +166,8 @@ ### items
|
||||
**모델**: `App\Models\Items\Item`
|
||||
|
||||
- **category()**: belongsTo → `categories`
|
||||
- **files()**: hasMany → `files`
|
||||
- **details()**: hasOne → `item_details`
|
||||
- **files()**: morphMany → `files`
|
||||
|
||||
### item_details
|
||||
**모델**: `App\Models\Items\ItemDetail`
|
||||
@@ -295,6 +295,17 @@ ### role_menu_permissions
|
||||
- **role()**: belongsTo → `roles`
|
||||
- **menu()**: belongsTo → `menus`
|
||||
|
||||
### prices
|
||||
**모델**: `App\Models\Products\Price`
|
||||
|
||||
- **clientGroup()**: belongsTo → `client_groups`
|
||||
- **revisions()**: hasMany → `price_revisions`
|
||||
|
||||
### price_revisions
|
||||
**모델**: `App\Models\Products\PriceRevision`
|
||||
|
||||
- **price()**: belongsTo → `prices`
|
||||
|
||||
### lots
|
||||
**모델**: `App\Models\Qualitys\Lot`
|
||||
|
||||
@@ -335,6 +346,20 @@ ### attendances
|
||||
- **creator()**: belongsTo → `users`
|
||||
- **updater()**: belongsTo → `users`
|
||||
|
||||
### bank_accounts
|
||||
**모델**: `App\Models\Tenants\BankAccount`
|
||||
|
||||
- **assignedUser()**: belongsTo → `users`
|
||||
- **creator()**: belongsTo → `users`
|
||||
- **updater()**: belongsTo → `users`
|
||||
|
||||
### cards
|
||||
**모델**: `App\Models\Tenants\Card`
|
||||
|
||||
- **assignedUser()**: belongsTo → `users`
|
||||
- **creator()**: belongsTo → `users`
|
||||
- **updater()**: belongsTo → `users`
|
||||
|
||||
### departments
|
||||
**모델**: `App\Models\Tenants\Department`
|
||||
|
||||
@@ -344,6 +369,24 @@ ### departments
|
||||
- **users()**: belongsToMany → `users`
|
||||
- **permissionOverrides()**: morphMany → `permission_overrides`
|
||||
|
||||
### deposits
|
||||
**모델**: `App\Models\Tenants\Deposit`
|
||||
|
||||
- **bankAccount()**: belongsTo → `bank_accounts`
|
||||
|
||||
### leaves
|
||||
**모델**: `App\Models\Tenants\Leave`
|
||||
|
||||
- **user()**: belongsTo → `users`
|
||||
- **approver()**: belongsTo → `users`
|
||||
- **creator()**: belongsTo → `users`
|
||||
- **updater()**: belongsTo → `users`
|
||||
|
||||
### leave_balances
|
||||
**모델**: `App\Models\Tenants\LeaveBalance`
|
||||
|
||||
- **user()**: belongsTo → `users`
|
||||
|
||||
### payments
|
||||
**모델**: `App\Models\Tenants\Payment`
|
||||
|
||||
@@ -360,11 +403,27 @@ ### plans
|
||||
|
||||
- **subscriptions()**: hasMany → `subscriptions`
|
||||
|
||||
### purchases
|
||||
**모델**: `App\Models\Tenants\Purchase`
|
||||
|
||||
- **withdrawal()**: belongsTo → `withdrawals`
|
||||
|
||||
### sales
|
||||
**모델**: `App\Models\Tenants\Sale`
|
||||
|
||||
- **deposit()**: belongsTo → `deposits`
|
||||
|
||||
### setting_field_defs
|
||||
**모델**: `App\Models\Tenants\SettingFieldDef`
|
||||
|
||||
- **tenantSettings()**: hasMany → `tenant_field_settings`
|
||||
|
||||
### sites
|
||||
**모델**: `App\Models\Tenants\Site`
|
||||
|
||||
- **creator()**: belongsTo → `users`
|
||||
- **updater()**: belongsTo → `users`
|
||||
|
||||
### subscriptions
|
||||
**모델**: `App\Models\Tenants\Subscription`
|
||||
|
||||
@@ -411,3 +470,8 @@ ### tenant_user_profiles
|
||||
- **department()**: belongsTo → `departments`
|
||||
- **manager()**: belongsTo → `users`
|
||||
|
||||
### withdrawals
|
||||
**모델**: `App\Models\Tenants\Withdrawal`
|
||||
|
||||
- **bankAccount()**: belongsTo → `bank_accounts`
|
||||
|
||||
|
||||
186
app/Http/Controllers/Api/V1/ApprovalController.php
Normal file
186
app/Http/Controllers/Api/V1/ApprovalController.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Approval\InboxIndexRequest;
|
||||
use App\Http\Requests\Approval\IndexRequest;
|
||||
use App\Http\Requests\Approval\ReferenceIndexRequest;
|
||||
use App\Http\Requests\Approval\RejectRequest;
|
||||
use App\Http\Requests\Approval\StoreRequest;
|
||||
use App\Http\Requests\Approval\SubmitRequest;
|
||||
use App\Http\Requests\Approval\UpdateRequest;
|
||||
use App\Services\ApprovalService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ApprovalController extends Controller
|
||||
{
|
||||
public function __construct(private ApprovalService $service) {}
|
||||
|
||||
/**
|
||||
* 기안함 - 내가 기안한 문서 목록
|
||||
* GET /v1/approvals/drafts
|
||||
*/
|
||||
public function drafts(IndexRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->drafts($request->validated());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 기안함 현황 카드
|
||||
* GET /v1/approvals/drafts/summary
|
||||
*/
|
||||
public function draftsSummary(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->service->draftsSummary();
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재함 - 내가 결재해야 할 문서 목록
|
||||
* GET /v1/approvals/inbox
|
||||
*/
|
||||
public function inbox(InboxIndexRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->inbox($request->validated());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재함 현황 카드
|
||||
* GET /v1/approvals/inbox/summary
|
||||
*/
|
||||
public function inboxSummary(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->service->inboxSummary();
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 참조함 - 내가 참조된 문서 목록
|
||||
* GET /v1/approvals/reference
|
||||
*/
|
||||
public function reference(ReferenceIndexRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->reference($request->validated());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 문서 상세
|
||||
* GET /v1/approvals/{id}
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->show($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 문서 생성 (임시저장 또는 상신)
|
||||
* POST /v1/approvals
|
||||
*/
|
||||
public function store(StoreRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->store($request->validated());
|
||||
}, __('message.approval.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 문서 수정 (임시저장 상태만)
|
||||
* PATCH /v1/approvals/{id}
|
||||
*/
|
||||
public function update(int $id, UpdateRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return $this->service->update($id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 문서 삭제 (임시저장 상태만)
|
||||
* DELETE /v1/approvals/{id}
|
||||
*/
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->destroy($id);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 상신
|
||||
* POST /v1/approvals/{id}/submit
|
||||
*/
|
||||
public function submit(int $id, SubmitRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return $this->service->submit($id, $request->validated());
|
||||
}, __('message.approval.submitted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 승인
|
||||
* POST /v1/approvals/{id}/approve
|
||||
*/
|
||||
public function approve(int $id, Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return $this->service->approve($id, $request->input('comment'));
|
||||
}, __('message.approval.approved'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 반려
|
||||
* POST /v1/approvals/{id}/reject
|
||||
*/
|
||||
public function reject(int $id, RejectRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return $this->service->reject($id, $request->input('comment'));
|
||||
}, __('message.approval.rejected'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 회수 (기안자만)
|
||||
* POST /v1/approvals/{id}/cancel
|
||||
*/
|
||||
public function cancel(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->cancel($id);
|
||||
}, __('message.approval.cancelled'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 참조 열람 처리
|
||||
* POST /v1/approvals/{id}/read
|
||||
*/
|
||||
public function markRead(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->markRead($id);
|
||||
}, __('message.approval.marked_read'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 참조 미열람 처리
|
||||
* POST /v1/approvals/{id}/unread
|
||||
*/
|
||||
public function markUnread(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->markUnread($id);
|
||||
}, __('message.approval.marked_unread'));
|
||||
}
|
||||
}
|
||||
82
app/Http/Controllers/Api/V1/ApprovalFormController.php
Normal file
82
app/Http/Controllers/Api/V1/ApprovalFormController.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Approval\FormIndexRequest;
|
||||
use App\Http\Requests\Approval\FormStoreRequest;
|
||||
use App\Http\Requests\Approval\FormUpdateRequest;
|
||||
use App\Services\ApprovalService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class ApprovalFormController extends Controller
|
||||
{
|
||||
public function __construct(private ApprovalService $service) {}
|
||||
|
||||
/**
|
||||
* 결재 양식 목록
|
||||
* GET /v1/approval-forms
|
||||
*/
|
||||
public function index(FormIndexRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->formIndex($request->validated());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 결재 양식 목록 (셀렉트박스용)
|
||||
* GET /v1/approval-forms/active
|
||||
*/
|
||||
public function active(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->service->formActive();
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 양식 상세
|
||||
* GET /v1/approval-forms/{id}
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->formShow($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 양식 생성
|
||||
* POST /v1/approval-forms
|
||||
*/
|
||||
public function store(FormStoreRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->formStore($request->validated());
|
||||
}, __('message.approval.form_created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 양식 수정
|
||||
* PATCH /v1/approval-forms/{id}
|
||||
*/
|
||||
public function update(int $id, FormUpdateRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return $this->service->formUpdate($id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 양식 삭제
|
||||
* DELETE /v1/approval-forms/{id}
|
||||
*/
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->formDestroy($id);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
71
app/Http/Controllers/Api/V1/ApprovalLineController.php
Normal file
71
app/Http/Controllers/Api/V1/ApprovalLineController.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Approval\LineIndexRequest;
|
||||
use App\Http\Requests\Approval\LineStoreRequest;
|
||||
use App\Http\Requests\Approval\LineUpdateRequest;
|
||||
use App\Services\ApprovalService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class ApprovalLineController extends Controller
|
||||
{
|
||||
public function __construct(private ApprovalService $service) {}
|
||||
|
||||
/**
|
||||
* 결재선 목록
|
||||
* GET /v1/approval-lines
|
||||
*/
|
||||
public function index(LineIndexRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->lineIndex($request->validated());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재선 상세
|
||||
* GET /v1/approval-lines/{id}
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->lineShow($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재선 생성
|
||||
* POST /v1/approval-lines
|
||||
*/
|
||||
public function store(LineStoreRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->lineStore($request->validated());
|
||||
}, __('message.approval.line_created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재선 수정
|
||||
* PATCH /v1/approval-lines/{id}
|
||||
*/
|
||||
public function update(int $id, LineUpdateRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return $this->service->lineUpdate($id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재선 삭제
|
||||
* DELETE /v1/approval-lines/{id}
|
||||
*/
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->lineDestroy($id);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
29
app/Http/Requests/Approval/FormIndexRequest.php
Normal file
29
app/Http/Requests/Approval/FormIndexRequest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use App\Models\Tenants\ApprovalForm;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class FormIndexRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$categories = implode(',', ApprovalForm::CATEGORIES);
|
||||
|
||||
return [
|
||||
'category' => "nullable|string|in:{$categories}",
|
||||
'is_active' => 'nullable|boolean',
|
||||
'search' => 'nullable|string|max:100',
|
||||
'sort_by' => 'nullable|string|in:created_at,name,code,category',
|
||||
'sort_dir' => 'nullable|string|in:asc,desc',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
'page' => 'nullable|integer|min:1',
|
||||
];
|
||||
}
|
||||
}
|
||||
44
app/Http/Requests/Approval/FormStoreRequest.php
Normal file
44
app/Http/Requests/Approval/FormStoreRequest.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use App\Models\Tenants\ApprovalForm;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class FormStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$categories = implode(',', ApprovalForm::CATEGORIES);
|
||||
|
||||
return [
|
||||
'name' => 'required|string|max:100',
|
||||
'code' => 'required|string|max:50|regex:/^[a-zA-Z0-9_-]+$/',
|
||||
'category' => "nullable|string|in:{$categories}",
|
||||
'template' => 'required|array',
|
||||
'template.fields' => 'required|array',
|
||||
'template.fields.*.name' => 'required|string|max:50',
|
||||
'template.fields.*.type' => 'required|string|in:text,textarea,number,date,select,checkbox,file',
|
||||
'template.fields.*.label' => 'required|string|max:100',
|
||||
'template.fields.*.required' => 'nullable|boolean',
|
||||
'template.fields.*.options' => 'nullable|array',
|
||||
'is_active' => 'nullable|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => __('validation.required', ['attribute' => '양식명']),
|
||||
'code.required' => __('validation.required', ['attribute' => '양식코드']),
|
||||
'code.regex' => __('validation.regex', ['attribute' => '양식코드']),
|
||||
'template.required' => __('validation.required', ['attribute' => '템플릿']),
|
||||
'template.fields.required' => __('validation.required', ['attribute' => '필드']),
|
||||
];
|
||||
}
|
||||
}
|
||||
40
app/Http/Requests/Approval/FormUpdateRequest.php
Normal file
40
app/Http/Requests/Approval/FormUpdateRequest.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use App\Models\Tenants\ApprovalForm;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class FormUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$categories = implode(',', ApprovalForm::CATEGORIES);
|
||||
|
||||
return [
|
||||
'name' => 'nullable|string|max:100',
|
||||
'code' => 'nullable|string|max:50|regex:/^[a-zA-Z0-9_-]+$/',
|
||||
'category' => "nullable|string|in:{$categories}",
|
||||
'template' => 'nullable|array',
|
||||
'template.fields' => 'required_with:template|array',
|
||||
'template.fields.*.name' => 'required_with:template.fields|string|max:50',
|
||||
'template.fields.*.type' => 'required_with:template.fields|string|in:text,textarea,number,date,select,checkbox,file',
|
||||
'template.fields.*.label' => 'required_with:template.fields|string|max:100',
|
||||
'template.fields.*.required' => 'nullable|boolean',
|
||||
'template.fields.*.options' => 'nullable|array',
|
||||
'is_active' => 'nullable|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'code.regex' => __('validation.regex', ['attribute' => '양식코드']),
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Http/Requests/Approval/InboxIndexRequest.php
Normal file
24
app/Http/Requests/Approval/InboxIndexRequest.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class InboxIndexRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'nullable|string|in:requested,scheduled,completed,rejected',
|
||||
'sort_by' => 'nullable|string|in:created_at,drafted_at,completed_at,title',
|
||||
'sort_dir' => 'nullable|string|in:asc,desc',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
'page' => 'nullable|integer|min:1',
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Requests/Approval/IndexRequest.php
Normal file
28
app/Http/Requests/Approval/IndexRequest.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use App\Models\Tenants\Approval;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class IndexRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$statuses = implode(',', Approval::STATUSES);
|
||||
|
||||
return [
|
||||
'status' => "nullable|string|in:{$statuses}",
|
||||
'search' => 'nullable|string|max:100',
|
||||
'sort_by' => 'nullable|string|in:created_at,drafted_at,completed_at,title,status',
|
||||
'sort_dir' => 'nullable|string|in:asc,desc',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
'page' => 'nullable|integer|min:1',
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Http/Requests/Approval/LineIndexRequest.php
Normal file
24
app/Http/Requests/Approval/LineIndexRequest.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LineIndexRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'search' => 'nullable|string|max:100',
|
||||
'sort_by' => 'nullable|string|in:created_at,name,is_default',
|
||||
'sort_dir' => 'nullable|string|in:asc,desc',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
'page' => 'nullable|integer|min:1',
|
||||
];
|
||||
}
|
||||
}
|
||||
40
app/Http/Requests/Approval/LineStoreRequest.php
Normal file
40
app/Http/Requests/Approval/LineStoreRequest.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use App\Models\Tenants\ApprovalLine;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LineStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$stepTypes = implode(',', ApprovalLine::STEP_TYPES);
|
||||
|
||||
return [
|
||||
'name' => 'required|string|max:100',
|
||||
'steps' => 'required|array|min:1',
|
||||
'steps.*.type' => "required|string|in:{$stepTypes}",
|
||||
'steps.*.user_id' => 'required|integer|exists:users,id',
|
||||
'is_default' => 'nullable|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => __('validation.required', ['attribute' => '결재선명']),
|
||||
'steps.required' => __('validation.required', ['attribute' => '결재단계']),
|
||||
'steps.min' => __('validation.min.array', ['attribute' => '결재단계', 'min' => 1]),
|
||||
'steps.*.type.required' => __('validation.required', ['attribute' => '단계유형']),
|
||||
'steps.*.type.in' => __('validation.in', ['attribute' => '단계유형']),
|
||||
'steps.*.user_id.required' => __('validation.required', ['attribute' => '결재자']),
|
||||
'steps.*.user_id.exists' => __('validation.exists', ['attribute' => '결재자']),
|
||||
];
|
||||
}
|
||||
}
|
||||
36
app/Http/Requests/Approval/LineUpdateRequest.php
Normal file
36
app/Http/Requests/Approval/LineUpdateRequest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use App\Models\Tenants\ApprovalLine;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LineUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$stepTypes = implode(',', ApprovalLine::STEP_TYPES);
|
||||
|
||||
return [
|
||||
'name' => 'nullable|string|max:100',
|
||||
'steps' => 'nullable|array|min:1',
|
||||
'steps.*.type' => "required_with:steps|string|in:{$stepTypes}",
|
||||
'steps.*.user_id' => 'required_with:steps|integer|exists:users,id',
|
||||
'is_default' => 'nullable|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'steps.min' => __('validation.min.array', ['attribute' => '결재단계', 'min' => 1]),
|
||||
'steps.*.type.in' => __('validation.in', ['attribute' => '단계유형']),
|
||||
'steps.*.user_id.exists' => __('validation.exists', ['attribute' => '결재자']),
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Http/Requests/Approval/ReferenceIndexRequest.php
Normal file
24
app/Http/Requests/Approval/ReferenceIndexRequest.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ReferenceIndexRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'is_read' => 'nullable|boolean',
|
||||
'sort_by' => 'nullable|string|in:created_at,drafted_at,completed_at,title',
|
||||
'sort_dir' => 'nullable|string|in:asc,desc',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
'page' => 'nullable|integer|min:1',
|
||||
];
|
||||
}
|
||||
}
|
||||
27
app/Http/Requests/Approval/RejectRequest.php
Normal file
27
app/Http/Requests/Approval/RejectRequest.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class RejectRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'comment' => 'required|string|max:1000',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'comment.required' => __('error.approval.reject_comment_required'),
|
||||
];
|
||||
}
|
||||
}
|
||||
43
app/Http/Requests/Approval/StoreRequest.php
Normal file
43
app/Http/Requests/Approval/StoreRequest.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use App\Models\Tenants\ApprovalLine;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$stepTypes = implode(',', ApprovalLine::STEP_TYPES);
|
||||
|
||||
return [
|
||||
'form_id' => 'required|integer|exists:approval_forms,id',
|
||||
'title' => 'required|string|max:200',
|
||||
'content' => 'required|array',
|
||||
'attachments' => 'nullable|array',
|
||||
'attachments.*' => 'integer|exists:files,id',
|
||||
'submit' => 'nullable|boolean',
|
||||
'steps' => 'required_if:submit,true|array|min:1',
|
||||
'steps.*.type' => "required_with:steps|string|in:{$stepTypes}",
|
||||
'steps.*.user_id' => 'required_with:steps|integer|exists:users,id',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'form_id.required' => __('validation.required', ['attribute' => '결재양식']),
|
||||
'form_id.exists' => __('validation.exists', ['attribute' => '결재양식']),
|
||||
'title.required' => __('validation.required', ['attribute' => '제목']),
|
||||
'content.required' => __('validation.required', ['attribute' => '내용']),
|
||||
'steps.required_if' => __('error.approval.steps_required'),
|
||||
'steps.min' => __('validation.min.array', ['attribute' => '결재단계', 'min' => 1]),
|
||||
];
|
||||
}
|
||||
}
|
||||
37
app/Http/Requests/Approval/SubmitRequest.php
Normal file
37
app/Http/Requests/Approval/SubmitRequest.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use App\Models\Tenants\ApprovalLine;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SubmitRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$stepTypes = implode(',', ApprovalLine::STEP_TYPES);
|
||||
|
||||
return [
|
||||
'steps' => 'required|array|min:1',
|
||||
'steps.*.type' => "required|string|in:{$stepTypes}",
|
||||
'steps.*.user_id' => 'required|integer|exists:users,id',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'steps.required' => __('error.approval.steps_required'),
|
||||
'steps.min' => __('validation.min.array', ['attribute' => '결재단계', 'min' => 1]),
|
||||
'steps.*.type.required' => __('validation.required', ['attribute' => '단계유형']),
|
||||
'steps.*.type.in' => __('validation.in', ['attribute' => '단계유형']),
|
||||
'steps.*.user_id.required' => __('validation.required', ['attribute' => '결재자']),
|
||||
'steps.*.user_id.exists' => __('validation.exists', ['attribute' => '결재자']),
|
||||
];
|
||||
}
|
||||
}
|
||||
31
app/Http/Requests/Approval/UpdateRequest.php
Normal file
31
app/Http/Requests/Approval/UpdateRequest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'form_id' => 'nullable|integer|exists:approval_forms,id',
|
||||
'title' => 'nullable|string|max:200',
|
||||
'content' => 'nullable|array',
|
||||
'attachments' => 'nullable|array',
|
||||
'attachments.*' => 'integer|exists:files,id',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'form_id.exists' => __('validation.exists', ['attribute' => '결재양식']),
|
||||
];
|
||||
}
|
||||
}
|
||||
320
app/Models/Tenants/Approval.php
Normal file
320
app/Models/Tenants/Approval.php
Normal file
@@ -0,0 +1,320 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 결재 문서 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property string $document_number
|
||||
* @property int $form_id
|
||||
* @property string $title
|
||||
* @property array $content
|
||||
* @property string $status
|
||||
* @property int $drafter_id
|
||||
* @property \Carbon\Carbon|null $drafted_at
|
||||
* @property \Carbon\Carbon|null $completed_at
|
||||
* @property int $current_step
|
||||
* @property array|null $attachments
|
||||
* @property int|null $created_by
|
||||
* @property int|null $updated_by
|
||||
* @property int|null $deleted_by
|
||||
*/
|
||||
class Approval extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'approvals';
|
||||
|
||||
protected $casts = [
|
||||
'content' => 'array',
|
||||
'attachments' => 'array',
|
||||
'drafted_at' => 'datetime',
|
||||
'completed_at' => 'datetime',
|
||||
'current_step' => 'integer',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'document_number',
|
||||
'form_id',
|
||||
'title',
|
||||
'content',
|
||||
'status',
|
||||
'drafter_id',
|
||||
'drafted_at',
|
||||
'completed_at',
|
||||
'current_step',
|
||||
'attachments',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'status' => 'draft',
|
||||
'current_step' => 0,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 상태 상수
|
||||
// =========================================================================
|
||||
|
||||
public const STATUS_DRAFT = 'draft'; // 임시저장
|
||||
|
||||
public const STATUS_PENDING = 'pending'; // 결재 진행중
|
||||
|
||||
public const STATUS_APPROVED = 'approved'; // 승인 완료
|
||||
|
||||
public const STATUS_REJECTED = 'rejected'; // 반려
|
||||
|
||||
public const STATUS_CANCELLED = 'cancelled'; // 회수/취소
|
||||
|
||||
public const STATUSES = [
|
||||
self::STATUS_DRAFT,
|
||||
self::STATUS_PENDING,
|
||||
self::STATUS_APPROVED,
|
||||
self::STATUS_REJECTED,
|
||||
self::STATUS_CANCELLED,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 결재 양식
|
||||
*/
|
||||
public function form(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ApprovalForm::class, 'form_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 기안자
|
||||
*/
|
||||
public function drafter(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'drafter_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 단계들
|
||||
*/
|
||||
public function steps(): HasMany
|
||||
{
|
||||
return $this->hasMany(ApprovalStep::class, 'approval_id')->orderBy('step_order');
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재자 단계들 (참조 제외)
|
||||
*/
|
||||
public function approverSteps(): HasMany
|
||||
{
|
||||
return $this->hasMany(ApprovalStep::class, 'approval_id')
|
||||
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT])
|
||||
->orderBy('step_order');
|
||||
}
|
||||
|
||||
/**
|
||||
* 참조자 단계들
|
||||
*/
|
||||
public function referenceSteps(): HasMany
|
||||
{
|
||||
return $this->hasMany(ApprovalStep::class, 'approval_id')
|
||||
->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE)
|
||||
->orderBy('step_order');
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성자
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정자
|
||||
*/
|
||||
public function updater(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 특정 상태
|
||||
*/
|
||||
public function scopeWithStatus($query, string $status)
|
||||
{
|
||||
return $query->where('status', $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 임시저장
|
||||
*/
|
||||
public function scopeDraft($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_DRAFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 진행중
|
||||
*/
|
||||
public function scopePending($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_PENDING);
|
||||
}
|
||||
|
||||
/**
|
||||
* 승인 완료
|
||||
*/
|
||||
public function scopeApproved($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_APPROVED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 반려
|
||||
*/
|
||||
public function scopeRejected($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_REJECTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 완료됨 (승인 또는 반려)
|
||||
*/
|
||||
public function scopeCompleted($query)
|
||||
{
|
||||
return $query->whereIn('status', [self::STATUS_APPROVED, self::STATUS_REJECTED]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 기안자
|
||||
*/
|
||||
public function scopeByDrafter($query, int $userId)
|
||||
{
|
||||
return $query->where('drafter_id', $userId);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 수정 가능 여부 (임시저장 상태만)
|
||||
*/
|
||||
public function isEditable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_DRAFT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상신 가능 여부
|
||||
*/
|
||||
public function isSubmittable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_DRAFT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 승인/반려 가능 여부
|
||||
*/
|
||||
public function isActionable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 회수 가능 여부 (기안자만, 진행중 상태)
|
||||
*/
|
||||
public function isCancellable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제 가능 여부 (임시저장만)
|
||||
*/
|
||||
public function isDeletable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_DRAFT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 라벨
|
||||
*/
|
||||
public function getStatusLabelAttribute(): string
|
||||
{
|
||||
return match ($this->status) {
|
||||
self::STATUS_DRAFT => '임시저장',
|
||||
self::STATUS_PENDING => '진행',
|
||||
self::STATUS_APPROVED => '완료',
|
||||
self::STATUS_REJECTED => '반려',
|
||||
self::STATUS_CANCELLED => '회수',
|
||||
default => $this->status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 결재자 확인
|
||||
*/
|
||||
public function getCurrentApproverStep(): ?ApprovalStep
|
||||
{
|
||||
return $this->steps()
|
||||
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT])
|
||||
->where('status', ApprovalStep::STATUS_PENDING)
|
||||
->orderBy('step_order')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 사용자가 현재 결재자인지 확인
|
||||
*/
|
||||
public function isCurrentApprover(int $userId): bool
|
||||
{
|
||||
$currentStep = $this->getCurrentApproverStep();
|
||||
|
||||
return $currentStep && $currentStep->approver_id === $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 사용자가 참조자인지 확인
|
||||
*/
|
||||
public function isReferee(int $userId): bool
|
||||
{
|
||||
return $this->referenceSteps()
|
||||
->where('approver_id', $userId)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 진행률
|
||||
*/
|
||||
public function getProgressAttribute(): array
|
||||
{
|
||||
$totalSteps = $this->approverSteps()->count();
|
||||
$completedSteps = $this->approverSteps()
|
||||
->whereIn('status', [ApprovalStep::STATUS_APPROVED, ApprovalStep::STATUS_REJECTED])
|
||||
->count();
|
||||
|
||||
return [
|
||||
'total' => $totalSteps,
|
||||
'completed' => $completedSteps,
|
||||
'percentage' => $totalSteps > 0 ? round(($completedSteps / $totalSteps) * 100) : 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
133
app/Models/Tenants/ApprovalForm.php
Normal file
133
app/Models/Tenants/ApprovalForm.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 결재 양식 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property string $name
|
||||
* @property string $code
|
||||
* @property string|null $category
|
||||
* @property array $template
|
||||
* @property bool $is_active
|
||||
* @property int|null $created_by
|
||||
* @property int|null $updated_by
|
||||
* @property int|null $deleted_by
|
||||
*/
|
||||
class ApprovalForm extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'approval_forms';
|
||||
|
||||
protected $casts = [
|
||||
'template' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'name',
|
||||
'code',
|
||||
'category',
|
||||
'template',
|
||||
'is_active',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'is_active' => true,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 카테고리 상수
|
||||
// =========================================================================
|
||||
|
||||
public const CATEGORY_REQUEST = 'request'; // 품의서
|
||||
|
||||
public const CATEGORY_EXPENSE = 'expense'; // 지출결의서
|
||||
|
||||
public const CATEGORY_EXPENSE_ESTIMATE = 'expense_estimate'; // 지출 예상 내역서
|
||||
|
||||
public const CATEGORIES = [
|
||||
self::CATEGORY_REQUEST,
|
||||
self::CATEGORY_EXPENSE,
|
||||
self::CATEGORY_EXPENSE_ESTIMATE,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 이 양식으로 생성된 결재 문서들
|
||||
*/
|
||||
public function approvals(): HasMany
|
||||
{
|
||||
return $this->hasMany(Approval::class, 'form_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성자
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정자
|
||||
*/
|
||||
public function updater(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 활성 양식만
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 카테고리
|
||||
*/
|
||||
public function scopeCategory($query, string $category)
|
||||
{
|
||||
return $query->where('category', $category);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 카테고리 라벨
|
||||
*/
|
||||
public function getCategoryLabelAttribute(): string
|
||||
{
|
||||
return match ($this->category) {
|
||||
self::CATEGORY_REQUEST => '품의서',
|
||||
self::CATEGORY_EXPENSE => '지출결의서',
|
||||
self::CATEGORY_EXPENSE_ESTIMATE => '지출 예상 내역서',
|
||||
default => $this->category ?? '',
|
||||
};
|
||||
}
|
||||
}
|
||||
119
app/Models/Tenants/ApprovalLine.php
Normal file
119
app/Models/Tenants/ApprovalLine.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 결재선 템플릿 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property string $name
|
||||
* @property array $steps
|
||||
* @property bool $is_default
|
||||
* @property int|null $created_by
|
||||
* @property int|null $updated_by
|
||||
* @property int|null $deleted_by
|
||||
*/
|
||||
class ApprovalLine extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'approval_lines';
|
||||
|
||||
protected $casts = [
|
||||
'steps' => 'array',
|
||||
'is_default' => 'boolean',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'name',
|
||||
'steps',
|
||||
'is_default',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'is_default' => false,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 단계 유형 상수
|
||||
// =========================================================================
|
||||
|
||||
public const STEP_TYPE_APPROVAL = 'approval'; // 결재
|
||||
|
||||
public const STEP_TYPE_AGREEMENT = 'agreement'; // 합의
|
||||
|
||||
public const STEP_TYPE_REFERENCE = 'reference'; // 참조
|
||||
|
||||
public const STEP_TYPES = [
|
||||
self::STEP_TYPE_APPROVAL,
|
||||
self::STEP_TYPE_AGREEMENT,
|
||||
self::STEP_TYPE_REFERENCE,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 생성자
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정자
|
||||
*/
|
||||
public function updater(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 기본 결재선만
|
||||
*/
|
||||
public function scopeDefault($query)
|
||||
{
|
||||
return $query->where('is_default', true);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 결재선 단계 수
|
||||
*/
|
||||
public function getStepCountAttribute(): int
|
||||
{
|
||||
return count($this->steps ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재자 목록 (user_id만)
|
||||
*/
|
||||
public function getApproverIdsAttribute(): array
|
||||
{
|
||||
return collect($this->steps ?? [])
|
||||
->pluck('user_id')
|
||||
->filter()
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
181
app/Models/Tenants/ApprovalStep.php
Normal file
181
app/Models/Tenants/ApprovalStep.php
Normal file
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 결재 단계 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $approval_id
|
||||
* @property int $step_order
|
||||
* @property string $step_type
|
||||
* @property int $approver_id
|
||||
* @property string $status
|
||||
* @property string|null $comment
|
||||
* @property \Carbon\Carbon|null $acted_at
|
||||
* @property bool $is_read
|
||||
* @property \Carbon\Carbon|null $read_at
|
||||
*/
|
||||
class ApprovalStep extends Model
|
||||
{
|
||||
protected $table = 'approval_steps';
|
||||
|
||||
protected $casts = [
|
||||
'step_order' => 'integer',
|
||||
'acted_at' => 'datetime',
|
||||
'is_read' => 'boolean',
|
||||
'read_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'approval_id',
|
||||
'step_order',
|
||||
'step_type',
|
||||
'approver_id',
|
||||
'status',
|
||||
'comment',
|
||||
'acted_at',
|
||||
'is_read',
|
||||
'read_at',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'status' => 'pending',
|
||||
'is_read' => false,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 상태 상수
|
||||
// =========================================================================
|
||||
|
||||
public const STATUS_PENDING = 'pending'; // 대기
|
||||
|
||||
public const STATUS_APPROVED = 'approved'; // 승인
|
||||
|
||||
public const STATUS_REJECTED = 'rejected'; // 반려
|
||||
|
||||
public const STATUS_SKIPPED = 'skipped'; // 건너뜀
|
||||
|
||||
public const STATUSES = [
|
||||
self::STATUS_PENDING,
|
||||
self::STATUS_APPROVED,
|
||||
self::STATUS_REJECTED,
|
||||
self::STATUS_SKIPPED,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 결재 문서
|
||||
*/
|
||||
public function approval(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Approval::class, 'approval_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재자/참조자
|
||||
*/
|
||||
public function approver(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'approver_id');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 대기 중
|
||||
*/
|
||||
public function scopePending($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_PENDING);
|
||||
}
|
||||
|
||||
/**
|
||||
* 승인됨
|
||||
*/
|
||||
public function scopeApproved($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_APPROVED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 결재자
|
||||
*/
|
||||
public function scopeByApprover($query, int $userId)
|
||||
{
|
||||
return $query->where('approver_id', $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 단계만 (참조 제외)
|
||||
*/
|
||||
public function scopeApprovalOnly($query)
|
||||
{
|
||||
return $query->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 참조만
|
||||
*/
|
||||
public function scopeReferenceOnly($query)
|
||||
{
|
||||
return $query->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 결재 가능 여부
|
||||
*/
|
||||
public function isActionable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_PENDING
|
||||
&& in_array($this->step_type, [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 참조인지 확인
|
||||
*/
|
||||
public function isReference(): bool
|
||||
{
|
||||
return $this->step_type === ApprovalLine::STEP_TYPE_REFERENCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 라벨
|
||||
*/
|
||||
public function getStatusLabelAttribute(): string
|
||||
{
|
||||
return match ($this->status) {
|
||||
self::STATUS_PENDING => '대기',
|
||||
self::STATUS_APPROVED => '승인',
|
||||
self::STATUS_REJECTED => '반려',
|
||||
self::STATUS_SKIPPED => '건너뜀',
|
||||
default => $this->status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 단계 유형 라벨
|
||||
*/
|
||||
public function getStepTypeLabelAttribute(): string
|
||||
{
|
||||
return match ($this->step_type) {
|
||||
ApprovalLine::STEP_TYPE_APPROVAL => '결재',
|
||||
ApprovalLine::STEP_TYPE_AGREEMENT => '합의',
|
||||
ApprovalLine::STEP_TYPE_REFERENCE => '참조',
|
||||
default => $this->step_type,
|
||||
};
|
||||
}
|
||||
}
|
||||
949
app/Services/ApprovalService.php
Normal file
949
app/Services/ApprovalService.php
Normal file
@@ -0,0 +1,949 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Tenants\Approval;
|
||||
use App\Models\Tenants\ApprovalForm;
|
||||
use App\Models\Tenants\ApprovalLine;
|
||||
use App\Models\Tenants\ApprovalStep;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ApprovalService extends Service
|
||||
{
|
||||
// =========================================================================
|
||||
// 결재 양식 관리
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 결재 양식 목록
|
||||
*/
|
||||
public function formIndex(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = ApprovalForm::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with('creator:id,name');
|
||||
|
||||
// 카테고리 필터
|
||||
if (! empty($params['category'])) {
|
||||
$query->where('category', $params['category']);
|
||||
}
|
||||
|
||||
// 활성 상태 필터
|
||||
if (isset($params['is_active'])) {
|
||||
$query->where('is_active', $params['is_active']);
|
||||
}
|
||||
|
||||
// 검색
|
||||
if (! empty($params['search'])) {
|
||||
$query->where(function ($q) use ($params) {
|
||||
$q->where('name', 'like', "%{$params['search']}%")
|
||||
->orWhere('code', 'like', "%{$params['search']}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'created_at';
|
||||
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
|
||||
$perPage = $params['per_page'] ?? 20;
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 결재 양식 목록 (셀렉트박스용)
|
||||
*/
|
||||
public function formActive(): Collection
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return ApprovalForm::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->active()
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'code', 'category']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 양식 상세
|
||||
*/
|
||||
public function formShow(int $id): ApprovalForm
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return ApprovalForm::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with('creator:id,name')
|
||||
->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 양식 생성
|
||||
*/
|
||||
public function formStore(array $data): ApprovalForm
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
// 코드 중복 확인
|
||||
$exists = ApprovalForm::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $data['code'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
throw new BadRequestHttpException(__('error.approval.form_code_exists'));
|
||||
}
|
||||
|
||||
return ApprovalForm::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => $data['name'],
|
||||
'code' => $data['code'],
|
||||
'category' => $data['category'] ?? null,
|
||||
'template' => $data['template'],
|
||||
'is_active' => $data['is_active'] ?? true,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 양식 수정
|
||||
*/
|
||||
public function formUpdate(int $id, array $data): ApprovalForm
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$form = ApprovalForm::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
// 코드 중복 확인 (자기 자신 제외)
|
||||
if (isset($data['code']) && $data['code'] !== $form->code) {
|
||||
$exists = ApprovalForm::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $data['code'])
|
||||
->where('id', '!=', $id)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
throw new BadRequestHttpException(__('error.approval.form_code_exists'));
|
||||
}
|
||||
}
|
||||
|
||||
$form->fill([
|
||||
'name' => $data['name'] ?? $form->name,
|
||||
'code' => $data['code'] ?? $form->code,
|
||||
'category' => $data['category'] ?? $form->category,
|
||||
'template' => $data['template'] ?? $form->template,
|
||||
'is_active' => $data['is_active'] ?? $form->is_active,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
$form->save();
|
||||
|
||||
return $form->fresh('creator:id,name');
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 양식 삭제
|
||||
*/
|
||||
public function formDestroy(int $id): bool
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$form = ApprovalForm::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
// 사용 중인 양식인지 확인
|
||||
$inUse = Approval::query()
|
||||
->where('form_id', $id)
|
||||
->exists();
|
||||
|
||||
if ($inUse) {
|
||||
throw new BadRequestHttpException(__('error.approval.form_in_use'));
|
||||
}
|
||||
|
||||
$form->deleted_by = $userId;
|
||||
$form->save();
|
||||
$form->delete();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 결재선 템플릿 관리
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 결재선 목록
|
||||
*/
|
||||
public function lineIndex(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = ApprovalLine::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with('creator:id,name');
|
||||
|
||||
// 검색
|
||||
if (! empty($params['search'])) {
|
||||
$query->where('name', 'like', "%{$params['search']}%");
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'created_at';
|
||||
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
|
||||
$perPage = $params['per_page'] ?? 20;
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재선 상세
|
||||
*/
|
||||
public function lineShow(int $id): ApprovalLine
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return ApprovalLine::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with('creator:id,name')
|
||||
->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재선 생성
|
||||
*/
|
||||
public function lineStore(array $data): ApprovalLine
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
||||
// 기본 결재선으로 설정 시 기존 기본값 해제
|
||||
if (! empty($data['is_default'])) {
|
||||
ApprovalLine::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('is_default', true)
|
||||
->update(['is_default' => false]);
|
||||
}
|
||||
|
||||
return ApprovalLine::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => $data['name'],
|
||||
'steps' => $data['steps'],
|
||||
'is_default' => $data['is_default'] ?? false,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재선 수정
|
||||
*/
|
||||
public function lineUpdate(int $id, array $data): ApprovalLine
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
|
||||
$line = ApprovalLine::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
// 기본 결재선으로 설정 시 기존 기본값 해제
|
||||
if (! empty($data['is_default']) && ! $line->is_default) {
|
||||
ApprovalLine::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('is_default', true)
|
||||
->update(['is_default' => false]);
|
||||
}
|
||||
|
||||
$line->fill([
|
||||
'name' => $data['name'] ?? $line->name,
|
||||
'steps' => $data['steps'] ?? $line->steps,
|
||||
'is_default' => $data['is_default'] ?? $line->is_default,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
$line->save();
|
||||
|
||||
return $line->fresh('creator:id,name');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재선 삭제
|
||||
*/
|
||||
public function lineDestroy(int $id): bool
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$line = ApprovalLine::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
$line->deleted_by = $userId;
|
||||
$line->save();
|
||||
$line->delete();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 결재 문서 관리
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 기안함 - 내가 기안한 문서
|
||||
*/
|
||||
public function drafts(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$query = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('drafter_id', $userId)
|
||||
->with(['form:id,name,code,category', 'drafter:id,name']);
|
||||
|
||||
// 상태 필터
|
||||
if (! empty($params['status'])) {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
|
||||
// 검색
|
||||
if (! empty($params['search'])) {
|
||||
$query->where(function ($q) use ($params) {
|
||||
$q->where('title', 'like', "%{$params['search']}%")
|
||||
->orWhere('document_number', 'like', "%{$params['search']}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'created_at';
|
||||
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
|
||||
$perPage = $params['per_page'] ?? 20;
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기안함 현황 카드
|
||||
*/
|
||||
public function draftsSummary(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$counts = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('drafter_id', $userId)
|
||||
->selectRaw('status, COUNT(*) as count')
|
||||
->groupBy('status')
|
||||
->pluck('count', 'status')
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'total' => array_sum($counts),
|
||||
'draft' => $counts[Approval::STATUS_DRAFT] ?? 0,
|
||||
'pending' => $counts[Approval::STATUS_PENDING] ?? 0,
|
||||
'approved' => $counts[Approval::STATUS_APPROVED] ?? 0,
|
||||
'rejected' => $counts[Approval::STATUS_REJECTED] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재함 - 내가 결재해야 할 문서
|
||||
*/
|
||||
public function inbox(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$query = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereHas('steps', function ($q) use ($userId) {
|
||||
$q->where('approver_id', $userId)
|
||||
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT]);
|
||||
})
|
||||
->with(['form:id,name,code,category', 'drafter:id,name', 'steps' => function ($q) use ($userId) {
|
||||
$q->where('approver_id', $userId);
|
||||
}]);
|
||||
|
||||
// 상태 필터
|
||||
if (! empty($params['status'])) {
|
||||
if ($params['status'] === 'requested') {
|
||||
// 결재 요청 (현재 내 차례)
|
||||
$query->where('status', Approval::STATUS_PENDING)
|
||||
->whereHas('steps', function ($q) use ($userId) {
|
||||
$q->where('approver_id', $userId)
|
||||
->where('status', ApprovalStep::STATUS_PENDING)
|
||||
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT]);
|
||||
});
|
||||
} elseif ($params['status'] === 'scheduled') {
|
||||
// 예정 (아직 내 차례 아님)
|
||||
$query->where('status', Approval::STATUS_PENDING)
|
||||
->whereHas('steps', function ($q) use ($userId) {
|
||||
$q->where('approver_id', $userId)
|
||||
->where('status', ApprovalStep::STATUS_PENDING);
|
||||
})
|
||||
->whereDoesntHave('steps', function ($q) use ($userId) {
|
||||
$q->where('approver_id', $userId)
|
||||
->where('status', ApprovalStep::STATUS_PENDING)
|
||||
->whereRaw('step_order = (SELECT MIN(s2.step_order) FROM approval_steps s2 WHERE s2.approval_id = approval_steps.approval_id AND s2.status = ?)', [ApprovalStep::STATUS_PENDING]);
|
||||
});
|
||||
} elseif ($params['status'] === 'completed') {
|
||||
// 내가 처리 완료한 문서
|
||||
$query->whereHas('steps', function ($q) use ($userId) {
|
||||
$q->where('approver_id', $userId)
|
||||
->whereIn('status', [ApprovalStep::STATUS_APPROVED, ApprovalStep::STATUS_REJECTED]);
|
||||
});
|
||||
} elseif ($params['status'] === 'rejected') {
|
||||
// 내가 반려한 문서
|
||||
$query->whereHas('steps', function ($q) use ($userId) {
|
||||
$q->where('approver_id', $userId)
|
||||
->where('status', ApprovalStep::STATUS_REJECTED);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'created_at';
|
||||
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
|
||||
$perPage = $params['per_page'] ?? 20;
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재함 현황 카드
|
||||
*/
|
||||
public function inboxSummary(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
// 결재 요청 (현재 내 차례)
|
||||
$requested = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('status', Approval::STATUS_PENDING)
|
||||
->whereHas('steps', function ($q) use ($userId) {
|
||||
$q->where('approver_id', $userId)
|
||||
->where('status', ApprovalStep::STATUS_PENDING)
|
||||
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT])
|
||||
->whereRaw('step_order = (SELECT MIN(s2.step_order) FROM approval_steps s2 WHERE s2.approval_id = approval_steps.approval_id AND s2.status = ?)', [ApprovalStep::STATUS_PENDING]);
|
||||
})
|
||||
->count();
|
||||
|
||||
// 예정 (내 차례 대기중)
|
||||
$scheduled = ApprovalStep::query()
|
||||
->where('approver_id', $userId)
|
||||
->where('status', ApprovalStep::STATUS_PENDING)
|
||||
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT])
|
||||
->whereHas('approval', function ($q) use ($tenantId) {
|
||||
$q->where('tenant_id', $tenantId)
|
||||
->where('status', Approval::STATUS_PENDING);
|
||||
})
|
||||
->count() - $requested;
|
||||
|
||||
// 완료 (내가 처리한 문서)
|
||||
$completed = ApprovalStep::query()
|
||||
->where('approver_id', $userId)
|
||||
->whereIn('status', [ApprovalStep::STATUS_APPROVED, ApprovalStep::STATUS_REJECTED])
|
||||
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT])
|
||||
->whereHas('approval', function ($q) use ($tenantId) {
|
||||
$q->where('tenant_id', $tenantId);
|
||||
})
|
||||
->count();
|
||||
|
||||
// 반려 (내가 반려한 문서)
|
||||
$rejected = ApprovalStep::query()
|
||||
->where('approver_id', $userId)
|
||||
->where('status', ApprovalStep::STATUS_REJECTED)
|
||||
->whereHas('approval', function ($q) use ($tenantId) {
|
||||
$q->where('tenant_id', $tenantId);
|
||||
})
|
||||
->count();
|
||||
|
||||
return [
|
||||
'requested' => max(0, $requested),
|
||||
'scheduled' => max(0, $scheduled),
|
||||
'completed' => $completed,
|
||||
'rejected' => $rejected,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 참조함 - 내가 참조된 문서
|
||||
*/
|
||||
public function reference(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$query = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereHas('steps', function ($q) use ($userId) {
|
||||
$q->where('approver_id', $userId)
|
||||
->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE);
|
||||
})
|
||||
->with(['form:id,name,code,category', 'drafter:id,name', 'steps' => function ($q) use ($userId) {
|
||||
$q->where('approver_id', $userId)
|
||||
->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE);
|
||||
}]);
|
||||
|
||||
// 열람 상태 필터
|
||||
if (isset($params['is_read'])) {
|
||||
$query->whereHas('steps', function ($q) use ($userId, $params) {
|
||||
$q->where('approver_id', $userId)
|
||||
->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE)
|
||||
->where('is_read', $params['is_read']);
|
||||
});
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'created_at';
|
||||
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
|
||||
$perPage = $params['per_page'] ?? 20;
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 문서 상세
|
||||
*/
|
||||
public function show(int $id): Approval
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with([
|
||||
'form:id,name,code,category,template',
|
||||
'drafter:id,name,email',
|
||||
'steps.approver:id,name,email',
|
||||
])
|
||||
->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 문서 생성 (임시저장 또는 상신)
|
||||
*/
|
||||
public function store(array $data): Approval
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
||||
// 양식 확인
|
||||
$form = ApprovalForm::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('id', $data['form_id'])
|
||||
->active()
|
||||
->firstOrFail();
|
||||
|
||||
// 문서번호 생성
|
||||
$documentNumber = $this->generateDocumentNumber($tenantId);
|
||||
|
||||
$status = ! empty($data['submit']) ? Approval::STATUS_PENDING : Approval::STATUS_DRAFT;
|
||||
|
||||
$approval = Approval::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'document_number' => $documentNumber,
|
||||
'form_id' => $data['form_id'],
|
||||
'title' => $data['title'],
|
||||
'content' => $data['content'],
|
||||
'status' => $status,
|
||||
'drafter_id' => $userId,
|
||||
'drafted_at' => $status === Approval::STATUS_PENDING ? now() : null,
|
||||
'attachments' => $data['attachments'] ?? null,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
// 결재선 생성 (상신 시)
|
||||
if ($status === Approval::STATUS_PENDING && ! empty($data['steps'])) {
|
||||
$this->createApprovalSteps($approval, $data['steps']);
|
||||
}
|
||||
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
'drafter:id,name',
|
||||
'steps.approver:id,name',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 문서 수정 (임시저장 상태만)
|
||||
*/
|
||||
public function update(int $id, array $data): Approval
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
|
||||
$approval = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
if (! $approval->isEditable()) {
|
||||
throw new BadRequestHttpException(__('error.approval.not_editable'));
|
||||
}
|
||||
|
||||
$approval->fill([
|
||||
'form_id' => $data['form_id'] ?? $approval->form_id,
|
||||
'title' => $data['title'] ?? $approval->title,
|
||||
'content' => $data['content'] ?? $approval->content,
|
||||
'attachments' => $data['attachments'] ?? $approval->attachments,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
$approval->save();
|
||||
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
'drafter:id,name',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 문서 삭제 (임시저장 상태만)
|
||||
*/
|
||||
public function destroy(int $id): bool
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $tenantId, $userId) {
|
||||
$approval = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
if (! $approval->isDeletable()) {
|
||||
throw new BadRequestHttpException(__('error.approval.not_deletable'));
|
||||
}
|
||||
|
||||
$approval->deleted_by = $userId;
|
||||
$approval->save();
|
||||
$approval->delete();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 상신
|
||||
*/
|
||||
public function submit(int $id, array $data): Approval
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
|
||||
$approval = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
if (! $approval->isSubmittable()) {
|
||||
throw new BadRequestHttpException(__('error.approval.not_submittable'));
|
||||
}
|
||||
|
||||
if (empty($data['steps'])) {
|
||||
throw new BadRequestHttpException(__('error.approval.steps_required'));
|
||||
}
|
||||
|
||||
// 결재선 생성
|
||||
$this->createApprovalSteps($approval, $data['steps']);
|
||||
|
||||
$approval->status = Approval::STATUS_PENDING;
|
||||
$approval->drafted_at = now();
|
||||
$approval->current_step = 1;
|
||||
$approval->updated_by = $userId;
|
||||
$approval->save();
|
||||
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
'drafter:id,name',
|
||||
'steps.approver:id,name',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 승인
|
||||
*/
|
||||
public function approve(int $id, ?string $comment = null): Approval
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $comment, $tenantId, $userId) {
|
||||
$approval = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
if (! $approval->isActionable()) {
|
||||
throw new BadRequestHttpException(__('error.approval.not_actionable'));
|
||||
}
|
||||
|
||||
// 현재 내 결재 단계 찾기
|
||||
$myStep = $approval->steps()
|
||||
->where('approver_id', $userId)
|
||||
->where('status', ApprovalStep::STATUS_PENDING)
|
||||
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT])
|
||||
->orderBy('step_order')
|
||||
->first();
|
||||
|
||||
if (! $myStep) {
|
||||
throw new BadRequestHttpException(__('error.approval.not_your_turn'));
|
||||
}
|
||||
|
||||
// 내 단계 승인
|
||||
$myStep->status = ApprovalStep::STATUS_APPROVED;
|
||||
$myStep->comment = $comment;
|
||||
$myStep->acted_at = now();
|
||||
$myStep->save();
|
||||
|
||||
// 다음 결재자 확인
|
||||
$nextStep = $approval->steps()
|
||||
->where('status', ApprovalStep::STATUS_PENDING)
|
||||
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT])
|
||||
->orderBy('step_order')
|
||||
->first();
|
||||
|
||||
if (! $nextStep) {
|
||||
// 모든 결재 완료
|
||||
$approval->status = Approval::STATUS_APPROVED;
|
||||
$approval->completed_at = now();
|
||||
}
|
||||
|
||||
$approval->current_step = $myStep->step_order + 1;
|
||||
$approval->updated_by = $userId;
|
||||
$approval->save();
|
||||
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
'drafter:id,name',
|
||||
'steps.approver:id,name',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 반려
|
||||
*/
|
||||
public function reject(int $id, string $comment): Approval
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $comment, $tenantId, $userId) {
|
||||
$approval = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
if (! $approval->isActionable()) {
|
||||
throw new BadRequestHttpException(__('error.approval.not_actionable'));
|
||||
}
|
||||
|
||||
// 현재 내 결재 단계 찾기
|
||||
$myStep = $approval->steps()
|
||||
->where('approver_id', $userId)
|
||||
->where('status', ApprovalStep::STATUS_PENDING)
|
||||
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT])
|
||||
->orderBy('step_order')
|
||||
->first();
|
||||
|
||||
if (! $myStep) {
|
||||
throw new BadRequestHttpException(__('error.approval.not_your_turn'));
|
||||
}
|
||||
|
||||
// 반려 처리
|
||||
$myStep->status = ApprovalStep::STATUS_REJECTED;
|
||||
$myStep->comment = $comment;
|
||||
$myStep->acted_at = now();
|
||||
$myStep->save();
|
||||
|
||||
// 문서 반려 상태로 변경
|
||||
$approval->status = Approval::STATUS_REJECTED;
|
||||
$approval->completed_at = now();
|
||||
$approval->updated_by = $userId;
|
||||
$approval->save();
|
||||
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
'drafter:id,name',
|
||||
'steps.approver:id,name',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 회수 (기안자만)
|
||||
*/
|
||||
public function cancel(int $id): Approval
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $tenantId, $userId) {
|
||||
$approval = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
if (! $approval->isCancellable()) {
|
||||
throw new BadRequestHttpException(__('error.approval.not_cancellable'));
|
||||
}
|
||||
|
||||
// 기안자만 회수 가능
|
||||
if ($approval->drafter_id !== $userId) {
|
||||
throw new BadRequestHttpException(__('error.approval.only_drafter_can_cancel'));
|
||||
}
|
||||
|
||||
$approval->status = Approval::STATUS_CANCELLED;
|
||||
$approval->completed_at = now();
|
||||
$approval->updated_by = $userId;
|
||||
$approval->save();
|
||||
|
||||
// 결재 단계들 삭제
|
||||
$approval->steps()->delete();
|
||||
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
'drafter:id,name',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 참조 열람 처리
|
||||
*/
|
||||
public function markRead(int $id): Approval
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$approval = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
$step = $approval->steps()
|
||||
->where('approver_id', $userId)
|
||||
->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE)
|
||||
->first();
|
||||
|
||||
if (! $step) {
|
||||
throw new NotFoundHttpException(__('error.approval.not_referee'));
|
||||
}
|
||||
|
||||
$step->is_read = true;
|
||||
$step->read_at = now();
|
||||
$step->save();
|
||||
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
'drafter:id,name',
|
||||
'steps.approver:id,name',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 참조 미열람 처리
|
||||
*/
|
||||
public function markUnread(int $id): Approval
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$approval = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
$step = $approval->steps()
|
||||
->where('approver_id', $userId)
|
||||
->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE)
|
||||
->first();
|
||||
|
||||
if (! $step) {
|
||||
throw new NotFoundHttpException(__('error.approval.not_referee'));
|
||||
}
|
||||
|
||||
$step->is_read = false;
|
||||
$step->read_at = null;
|
||||
$step->save();
|
||||
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
'drafter:id,name',
|
||||
'steps.approver:id,name',
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 문서번호 생성
|
||||
*/
|
||||
private function generateDocumentNumber(int $tenantId): string
|
||||
{
|
||||
$prefix = 'AP';
|
||||
$date = now()->format('Ymd');
|
||||
|
||||
$lastNumber = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('document_number', 'like', "{$prefix}-{$date}-%")
|
||||
->orderByDesc('document_number')
|
||||
->value('document_number');
|
||||
|
||||
if ($lastNumber) {
|
||||
$sequence = (int) substr($lastNumber, -4) + 1;
|
||||
} else {
|
||||
$sequence = 1;
|
||||
}
|
||||
|
||||
return sprintf('%s-%s-%04d', $prefix, $date, $sequence);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 단계 생성
|
||||
*/
|
||||
private function createApprovalSteps(Approval $approval, array $steps): void
|
||||
{
|
||||
$order = 1;
|
||||
foreach ($steps as $step) {
|
||||
ApprovalStep::create([
|
||||
'approval_id' => $approval->id,
|
||||
'step_order' => $order++,
|
||||
'step_type' => $step['type'],
|
||||
'approver_id' => $step['user_id'],
|
||||
'status' => ApprovalStep::STATUS_PENDING,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
646
app/Swagger/v1/ApprovalApi.php
Normal file
646
app/Swagger/v1/ApprovalApi.php
Normal file
@@ -0,0 +1,646 @@
|
||||
<?php
|
||||
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(name="Approvals", description="전자결재 문서 관리")
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="Approval",
|
||||
* type="object",
|
||||
* description="결재 문서 정보",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1, description="문서 ID"),
|
||||
* @OA\Property(property="tenant_id", type="integer", example=1, description="테넌트 ID"),
|
||||
* @OA\Property(property="form_id", type="integer", example=1, description="양식 ID"),
|
||||
* @OA\Property(property="drafter_id", type="integer", example=10, description="기안자 ID"),
|
||||
* @OA\Property(property="doc_number", type="string", example="APR-2024-0001", nullable=true, description="문서번호"),
|
||||
* @OA\Property(property="title", type="string", example="휴가 신청", description="제목"),
|
||||
* @OA\Property(property="content", type="object", description="문서 내용 JSON"),
|
||||
* @OA\Property(property="status", type="string", enum={"draft","pending","approved","rejected","cancelled"}, example="pending", description="상태"),
|
||||
* @OA\Property(property="current_step", type="integer", example=1, nullable=true, description="현재 결재 단계"),
|
||||
* @OA\Property(property="submitted_at", type="string", format="date-time", nullable=true, description="상신일시"),
|
||||
* @OA\Property(property="completed_at", type="string", format="date-time", nullable=true, description="완료일시"),
|
||||
* @OA\Property(property="form", type="object", nullable=true, description="결재 양식 정보",
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="name", type="string", example="품의서"),
|
||||
* @OA\Property(property="code", type="string", example="REQUEST_01")
|
||||
* ),
|
||||
* @OA\Property(property="drafter", type="object", nullable=true, description="기안자 정보",
|
||||
* @OA\Property(property="id", type="integer", example=10),
|
||||
* @OA\Property(property="name", type="string", example="홍길동")
|
||||
* ),
|
||||
* @OA\Property(property="steps", type="array", description="결재 단계", @OA\Items(ref="#/components/schemas/ApprovalStepDetail")),
|
||||
* @OA\Property(property="created_at", type="string", format="date-time", example="2024-01-01T09:00:00Z"),
|
||||
* @OA\Property(property="updated_at", type="string", format="date-time", example="2024-01-01T09:00:00Z")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ApprovalStepDetail",
|
||||
* type="object",
|
||||
* description="결재 단계 상세 정보",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="step_order", type="integer", example=1, description="단계 순서"),
|
||||
* @OA\Property(property="type", type="string", enum={"approval","agreement","reference"}, example="approval", description="단계 유형"),
|
||||
* @OA\Property(property="user_id", type="integer", example=20, description="결재자 ID"),
|
||||
* @OA\Property(property="status", type="string", enum={"pending","approved","rejected","skipped"}, example="pending", description="단계 상태"),
|
||||
* @OA\Property(property="comment", type="string", nullable=true, description="결재 의견"),
|
||||
* @OA\Property(property="acted_at", type="string", format="date-time", nullable=true, description="결재일시"),
|
||||
* @OA\Property(property="is_read", type="boolean", example=false, description="열람 여부 (참조)"),
|
||||
* @OA\Property(property="read_at", type="string", format="date-time", nullable=true, description="열람일시"),
|
||||
* @OA\Property(property="user", type="object", nullable=true, description="결재자 정보",
|
||||
* @OA\Property(property="id", type="integer", example=20),
|
||||
* @OA\Property(property="name", type="string", example="김부장")
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ApprovalStoreRequest",
|
||||
* type="object",
|
||||
* required={"form_id", "title"},
|
||||
* description="결재 문서 생성/임시저장 요청",
|
||||
*
|
||||
* @OA\Property(property="form_id", type="integer", example=1, description="양식 ID"),
|
||||
* @OA\Property(property="title", type="string", example="휴가 신청", description="제목"),
|
||||
* @OA\Property(property="content", type="object", description="문서 내용 JSON"),
|
||||
* @OA\Property(property="steps", type="array", description="결재 단계 (임시저장 시 선택)", @OA\Items(type="object",
|
||||
* @OA\Property(property="type", type="string", enum={"approval","agreement","reference"}, example="approval"),
|
||||
* @OA\Property(property="user_id", type="integer", example=20)
|
||||
* ))
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ApprovalUpdateRequest",
|
||||
* type="object",
|
||||
* description="결재 문서 수정 요청 (임시저장 상태만 가능)",
|
||||
*
|
||||
* @OA\Property(property="title", type="string", example="휴가 신청 (수정)", description="제목"),
|
||||
* @OA\Property(property="content", type="object", description="문서 내용 JSON"),
|
||||
* @OA\Property(property="steps", type="array", description="결재 단계", @OA\Items(type="object"))
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ApprovalSubmitRequest",
|
||||
* type="object",
|
||||
* required={"steps"},
|
||||
* description="결재 상신 요청",
|
||||
*
|
||||
* @OA\Property(property="steps", type="array", description="결재 단계 (필수)", @OA\Items(type="object",
|
||||
* @OA\Property(property="type", type="string", enum={"approval","agreement","reference"}, example="approval"),
|
||||
* @OA\Property(property="user_id", type="integer", example=20)
|
||||
* ))
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ApprovalSummary",
|
||||
* type="object",
|
||||
* description="결재 현황 요약",
|
||||
*
|
||||
* @OA\Property(property="draft", type="integer", example=3, description="임시저장"),
|
||||
* @OA\Property(property="pending", type="integer", example=5, description="진행중"),
|
||||
* @OA\Property(property="approved", type="integer", example=10, description="승인완료"),
|
||||
* @OA\Property(property="rejected", type="integer", example=2, description="반려")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="InboxSummary",
|
||||
* type="object",
|
||||
* description="결재함 현황 요약",
|
||||
*
|
||||
* @OA\Property(property="requested", type="integer", example=3, description="결재 요청"),
|
||||
* @OA\Property(property="scheduled", type="integer", example=2, description="결재 예정"),
|
||||
* @OA\Property(property="completed", type="integer", example=15, description="결재 완료"),
|
||||
* @OA\Property(property="rejected", type="integer", example=1, description="반려")
|
||||
* )
|
||||
*/
|
||||
class ApprovalApi
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/approvals/drafts",
|
||||
* tags={"Approvals"},
|
||||
* summary="기안함 - 내가 작성한 문서 목록",
|
||||
* description="로그인 사용자가 기안한 결재 문서 목록을 조회합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="status", in="query", description="상태 필터", @OA\Schema(type="string", enum={"draft","pending","approved","rejected","cancelled"})),
|
||||
* @OA\Parameter(name="search", in="query", description="검색어 (제목, 문서번호)", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="sort_by", in="query", description="정렬 기준", @OA\Schema(type="string", enum={"created_at","submitted_at","completed_at","title"}, default="created_at")),
|
||||
* @OA\Parameter(name="sort_dir", in="query", description="정렬 방향", @OA\Schema(type="string", enum={"asc","desc"}, default="desc")),
|
||||
* @OA\Parameter(ref="#/components/parameters/Page"),
|
||||
* @OA\Parameter(ref="#/components/parameters/Size"),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="data",
|
||||
* type="object",
|
||||
* @OA\Property(property="current_page", type="integer", example=1),
|
||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Approval")),
|
||||
* @OA\Property(property="per_page", type="integer", example=20),
|
||||
* @OA\Property(property="total", type="integer", example=15)
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function drafts() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/approvals/drafts/summary",
|
||||
* tags={"Approvals"},
|
||||
* summary="기안함 현황 카드",
|
||||
* description="기안 문서의 상태별 건수를 조회합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ApprovalSummary"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function draftsSummary() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/approvals/inbox",
|
||||
* tags={"Approvals"},
|
||||
* summary="결재함 - 내가 결재할/결재한 문서",
|
||||
* description="로그인 사용자에게 결재 요청된 문서 목록을 조회합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="filter", in="query", description="필터 (requested: 결재요청, scheduled: 결재예정, completed: 결재완료, rejected: 반려)", @OA\Schema(type="string", enum={"requested","scheduled","completed","rejected"})),
|
||||
* @OA\Parameter(name="search", in="query", description="검색어 (제목, 문서번호, 기안자)", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="sort_by", in="query", description="정렬 기준", @OA\Schema(type="string", enum={"created_at","submitted_at","title"}, default="submitted_at")),
|
||||
* @OA\Parameter(name="sort_dir", in="query", description="정렬 방향", @OA\Schema(type="string", enum={"asc","desc"}, default="desc")),
|
||||
* @OA\Parameter(ref="#/components/parameters/Page"),
|
||||
* @OA\Parameter(ref="#/components/parameters/Size"),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="data",
|
||||
* type="object",
|
||||
* @OA\Property(property="current_page", type="integer", example=1),
|
||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Approval")),
|
||||
* @OA\Property(property="per_page", type="integer", example=20),
|
||||
* @OA\Property(property="total", type="integer", example=10)
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function inbox() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/approvals/inbox/summary",
|
||||
* tags={"Approvals"},
|
||||
* summary="결재함 현황 카드",
|
||||
* description="결재함의 상태별 건수를 조회합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/InboxSummary"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function inboxSummary() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/approvals/reference",
|
||||
* tags={"Approvals"},
|
||||
* summary="참조함 - 참조로 받은 문서",
|
||||
* description="로그인 사용자가 참조자로 지정된 문서 목록을 조회합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="is_read", in="query", description="열람 여부 필터", @OA\Schema(type="boolean")),
|
||||
* @OA\Parameter(name="search", in="query", description="검색어 (제목, 문서번호, 기안자)", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="sort_by", in="query", description="정렬 기준", @OA\Schema(type="string", enum={"created_at","submitted_at","title"}, default="submitted_at")),
|
||||
* @OA\Parameter(name="sort_dir", in="query", description="정렬 방향", @OA\Schema(type="string", enum={"asc","desc"}, default="desc")),
|
||||
* @OA\Parameter(ref="#/components/parameters/Page"),
|
||||
* @OA\Parameter(ref="#/components/parameters/Size"),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="data",
|
||||
* type="object",
|
||||
* @OA\Property(property="current_page", type="integer", example=1),
|
||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Approval")),
|
||||
* @OA\Property(property="per_page", type="integer", example=20),
|
||||
* @OA\Property(property="total", type="integer", example=8)
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function reference() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/approvals/{id}",
|
||||
* tags={"Approvals"},
|
||||
* summary="결재 문서 상세 조회",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="문서 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Approval"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="문서를 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function show() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/approvals",
|
||||
* tags={"Approvals"},
|
||||
* summary="결재 문서 생성 (임시저장)",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/ApprovalStoreRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=201,
|
||||
* description="생성 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Approval"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="양식을 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function store() {}
|
||||
|
||||
/**
|
||||
* @OA\Patch(
|
||||
* path="/api/v1/approvals/{id}",
|
||||
* tags={"Approvals"},
|
||||
* summary="결재 문서 수정",
|
||||
* description="임시저장 상태의 문서만 수정할 수 있습니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="문서 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/ApprovalUpdateRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="수정 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Approval"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="잘못된 요청 또는 수정 불가 상태", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="문서를 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function update() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/approvals/{id}",
|
||||
* tags={"Approvals"},
|
||||
* summary="결재 문서 삭제",
|
||||
* description="임시저장 상태의 문서만 삭제할 수 있습니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="문서 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="삭제 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string", example="삭제 완료"),
|
||||
* @OA\Property(property="data", type="boolean", example=true)
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="삭제 불가 상태", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="문서를 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function destroy() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/approvals/{id}/submit",
|
||||
* tags={"Approvals"},
|
||||
* summary="결재 상신",
|
||||
* description="임시저장 문서를 결재선에 상신합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="문서 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/ApprovalSubmitRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="상신 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Approval"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="상신 불가 상태", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="문서를 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function submit() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/approvals/{id}/approve",
|
||||
* tags={"Approvals"},
|
||||
* summary="결재 승인",
|
||||
* description="현재 순서의 결재자가 문서를 승인합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="문서 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=false,
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="comment", type="string", example="승인합니다.", description="결재 의견 (선택)")
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="승인 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Approval"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="결재 불가 상태 또는 결재 순서 아님", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="문서를 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function approve() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/approvals/{id}/reject",
|
||||
* tags={"Approvals"},
|
||||
* summary="결재 반려",
|
||||
* description="현재 순서의 결재자가 문서를 반려합니다. 반려 사유는 필수입니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="문서 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* required={"comment"},
|
||||
*
|
||||
* @OA\Property(property="comment", type="string", example="서류가 미비합니다. 재작성 바랍니다.", description="반려 사유 (필수)")
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="반려 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Approval"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="결재 불가 상태 또는 결재 순서 아님", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="문서를 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=422, description="반려 사유 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function reject() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/approvals/{id}/cancel",
|
||||
* tags={"Approvals"},
|
||||
* summary="결재 회수",
|
||||
* description="기안자가 진행중인 문서를 회수합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="문서 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="회수 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Approval"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="회수 불가 상태 또는 기안자가 아님", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="문서를 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function cancel() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/approvals/{id}/read",
|
||||
* tags={"Approvals"},
|
||||
* summary="열람 처리",
|
||||
* description="참조자가 문서를 열람 처리합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="문서 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="열람 처리 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Approval"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="참조자가 아님", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="문서를 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function markRead() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/approvals/{id}/unread",
|
||||
* tags={"Approvals"},
|
||||
* summary="미열람 처리",
|
||||
* description="참조자가 문서를 미열람 처리합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="문서 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="미열람 처리 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Approval"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="참조자가 아님", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="문서를 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function markUnread() {}
|
||||
}
|
||||
277
app/Swagger/v1/ApprovalFormApi.php
Normal file
277
app/Swagger/v1/ApprovalFormApi.php
Normal file
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(name="Approval Forms", description="결재 양식 관리")
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ApprovalForm",
|
||||
* type="object",
|
||||
* description="결재 양식 정보",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1, description="양식 ID"),
|
||||
* @OA\Property(property="tenant_id", type="integer", example=1, description="테넌트 ID"),
|
||||
* @OA\Property(property="name", type="string", example="품의서", description="양식명"),
|
||||
* @OA\Property(property="code", type="string", example="REQUEST_01", description="양식 코드"),
|
||||
* @OA\Property(property="category", type="string", enum={"request","expense","expense_estimate"}, example="request", nullable=true, description="카테고리"),
|
||||
* @OA\Property(property="template", type="object", description="템플릿 JSON",
|
||||
* @OA\Property(property="fields", type="array", @OA\Items(type="object",
|
||||
* @OA\Property(property="name", type="string", example="title"),
|
||||
* @OA\Property(property="type", type="string", example="text"),
|
||||
* @OA\Property(property="label", type="string", example="제목"),
|
||||
* @OA\Property(property="required", type="boolean", example=true)
|
||||
* ))
|
||||
* ),
|
||||
* @OA\Property(property="is_active", type="boolean", example=true, description="활성 여부"),
|
||||
* @OA\Property(property="creator", type="object", nullable=true, description="생성자 정보",
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="name", type="string", example="관리자")
|
||||
* ),
|
||||
* @OA\Property(property="created_at", type="string", format="date-time", example="2024-01-01T09:00:00Z"),
|
||||
* @OA\Property(property="updated_at", type="string", format="date-time", example="2024-01-01T09:00:00Z")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ApprovalFormCreateRequest",
|
||||
* type="object",
|
||||
* required={"name", "code", "template"},
|
||||
* description="결재 양식 생성 요청",
|
||||
*
|
||||
* @OA\Property(property="name", type="string", example="품의서", description="양식명"),
|
||||
* @OA\Property(property="code", type="string", example="REQUEST_01", description="양식 코드 (영문, 숫자, _, -)"),
|
||||
* @OA\Property(property="category", type="string", enum={"request","expense","expense_estimate"}, example="request", description="카테고리"),
|
||||
* @OA\Property(property="template", type="object", description="템플릿 JSON",
|
||||
* @OA\Property(property="fields", type="array", @OA\Items(type="object"))
|
||||
* ),
|
||||
* @OA\Property(property="is_active", type="boolean", example=true, description="활성 여부")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ApprovalFormUpdateRequest",
|
||||
* type="object",
|
||||
* description="결재 양식 수정 요청",
|
||||
*
|
||||
* @OA\Property(property="name", type="string", example="품의서", description="양식명"),
|
||||
* @OA\Property(property="code", type="string", example="REQUEST_01", description="양식 코드"),
|
||||
* @OA\Property(property="category", type="string", enum={"request","expense","expense_estimate"}, description="카테고리"),
|
||||
* @OA\Property(property="template", type="object", description="템플릿 JSON"),
|
||||
* @OA\Property(property="is_active", type="boolean", description="활성 여부")
|
||||
* )
|
||||
*/
|
||||
class ApprovalFormApi
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/approval-forms",
|
||||
* tags={"Approval Forms"},
|
||||
* summary="결재 양식 목록 조회",
|
||||
* description="필터/검색/페이지네이션으로 결재 양식 목록을 조회합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="category", in="query", description="카테고리 필터", @OA\Schema(type="string", enum={"request","expense","expense_estimate"})),
|
||||
* @OA\Parameter(name="is_active", in="query", description="활성 상태 필터", @OA\Schema(type="boolean")),
|
||||
* @OA\Parameter(name="search", in="query", description="검색어 (이름, 코드)", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="sort_by", in="query", description="정렬 기준", @OA\Schema(type="string", enum={"created_at","name","code","category"}, default="created_at")),
|
||||
* @OA\Parameter(name="sort_dir", in="query", description="정렬 방향", @OA\Schema(type="string", enum={"asc","desc"}, default="desc")),
|
||||
* @OA\Parameter(ref="#/components/parameters/Page"),
|
||||
* @OA\Parameter(ref="#/components/parameters/Size"),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="data",
|
||||
* type="object",
|
||||
* @OA\Property(property="current_page", type="integer", example=1),
|
||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/ApprovalForm")),
|
||||
* @OA\Property(property="per_page", type="integer", example=20),
|
||||
* @OA\Property(property="total", type="integer", example=10)
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function index() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/approval-forms/active",
|
||||
* tags={"Approval Forms"},
|
||||
* summary="활성 결재 양식 목록 (셀렉트박스용)",
|
||||
* description="활성화된 결재 양식만 간략하게 조회합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="data",
|
||||
* type="array",
|
||||
*
|
||||
* @OA\Items(type="object",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="name", type="string", example="품의서"),
|
||||
* @OA\Property(property="code", type="string", example="REQUEST_01"),
|
||||
* @OA\Property(property="category", type="string", example="request")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function active() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/approval-forms/{id}",
|
||||
* tags={"Approval Forms"},
|
||||
* summary="결재 양식 상세 조회",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="양식 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ApprovalForm"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="양식을 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function show() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/approval-forms",
|
||||
* tags={"Approval Forms"},
|
||||
* summary="결재 양식 생성",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/ApprovalFormCreateRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=201,
|
||||
* description="생성 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ApprovalForm"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="잘못된 요청 (코드 중복 등)", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function store() {}
|
||||
|
||||
/**
|
||||
* @OA\Patch(
|
||||
* path="/api/v1/approval-forms/{id}",
|
||||
* tags={"Approval Forms"},
|
||||
* summary="결재 양식 수정",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="양식 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/ApprovalFormUpdateRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="수정 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ApprovalForm"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="양식을 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function update() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/approval-forms/{id}",
|
||||
* tags={"Approval Forms"},
|
||||
* summary="결재 양식 삭제",
|
||||
* description="사용 중인 양식은 삭제할 수 없습니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="양식 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="삭제 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string", example="삭제 완료"),
|
||||
* @OA\Property(property="data", type="boolean", example=true)
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="삭제 불가 (사용 중)", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="양식을 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function destroy() {}
|
||||
}
|
||||
223
app/Swagger/v1/ApprovalLineApi.php
Normal file
223
app/Swagger/v1/ApprovalLineApi.php
Normal file
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(name="Approval Lines", description="결재선 템플릿 관리")
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ApprovalLine",
|
||||
* type="object",
|
||||
* description="결재선 템플릿 정보",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1, description="결재선 ID"),
|
||||
* @OA\Property(property="tenant_id", type="integer", example=1, description="테넌트 ID"),
|
||||
* @OA\Property(property="name", type="string", example="기본 결재선", description="결재선명"),
|
||||
* @OA\Property(property="steps", type="array", description="결재 단계", @OA\Items(type="object",
|
||||
* @OA\Property(property="type", type="string", enum={"approval","agreement","reference"}, example="approval", description="단계 유형"),
|
||||
* @OA\Property(property="user_id", type="integer", example=10, description="결재자 ID")
|
||||
* )),
|
||||
* @OA\Property(property="is_default", type="boolean", example=false, description="기본 결재선 여부"),
|
||||
* @OA\Property(property="step_count", type="integer", example=3, description="단계 수"),
|
||||
* @OA\Property(property="creator", type="object", nullable=true, description="생성자 정보",
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="name", type="string", example="관리자")
|
||||
* ),
|
||||
* @OA\Property(property="created_at", type="string", format="date-time", example="2024-01-01T09:00:00Z"),
|
||||
* @OA\Property(property="updated_at", type="string", format="date-time", example="2024-01-01T09:00:00Z")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ApprovalLineCreateRequest",
|
||||
* type="object",
|
||||
* required={"name", "steps"},
|
||||
* description="결재선 생성 요청",
|
||||
*
|
||||
* @OA\Property(property="name", type="string", example="기본 결재선", description="결재선명"),
|
||||
* @OA\Property(property="steps", type="array", description="결재 단계", @OA\Items(type="object",
|
||||
* @OA\Property(property="type", type="string", enum={"approval","agreement","reference"}, example="approval", description="단계 유형"),
|
||||
* @OA\Property(property="user_id", type="integer", example=10, description="결재자 ID")
|
||||
* )),
|
||||
* @OA\Property(property="is_default", type="boolean", example=false, description="기본 결재선 여부")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ApprovalLineUpdateRequest",
|
||||
* type="object",
|
||||
* description="결재선 수정 요청",
|
||||
*
|
||||
* @OA\Property(property="name", type="string", example="기본 결재선", description="결재선명"),
|
||||
* @OA\Property(property="steps", type="array", description="결재 단계", @OA\Items(type="object")),
|
||||
* @OA\Property(property="is_default", type="boolean", description="기본 결재선 여부")
|
||||
* )
|
||||
*/
|
||||
class ApprovalLineApi
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/approval-lines",
|
||||
* tags={"Approval Lines"},
|
||||
* summary="결재선 목록 조회",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="search", in="query", description="검색어 (이름)", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="sort_by", in="query", description="정렬 기준", @OA\Schema(type="string", enum={"created_at","name","is_default"}, default="created_at")),
|
||||
* @OA\Parameter(name="sort_dir", in="query", description="정렬 방향", @OA\Schema(type="string", enum={"asc","desc"}, default="desc")),
|
||||
* @OA\Parameter(ref="#/components/parameters/Page"),
|
||||
* @OA\Parameter(ref="#/components/parameters/Size"),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="data",
|
||||
* type="object",
|
||||
* @OA\Property(property="current_page", type="integer", example=1),
|
||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/ApprovalLine")),
|
||||
* @OA\Property(property="per_page", type="integer", example=20),
|
||||
* @OA\Property(property="total", type="integer", example=5)
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function index() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/approval-lines/{id}",
|
||||
* tags={"Approval Lines"},
|
||||
* summary="결재선 상세 조회",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="결재선 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ApprovalLine"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="결재선을 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function show() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/approval-lines",
|
||||
* tags={"Approval Lines"},
|
||||
* summary="결재선 생성",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/ApprovalLineCreateRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=201,
|
||||
* description="생성 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ApprovalLine"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function store() {}
|
||||
|
||||
/**
|
||||
* @OA\Patch(
|
||||
* path="/api/v1/approval-lines/{id}",
|
||||
* tags={"Approval Lines"},
|
||||
* summary="결재선 수정",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="결재선 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/ApprovalLineUpdateRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="수정 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ApprovalLine"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="결재선을 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function update() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/approval-lines/{id}",
|
||||
* tags={"Approval Lines"},
|
||||
* summary="결재선 삭제",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="결재선 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="삭제 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string", example="삭제 완료"),
|
||||
* @OA\Property(property="data", type="boolean", example=true)
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="결재선을 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function destroy() {}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('approval_forms', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->onDelete('cascade')->comment('테넌트 ID');
|
||||
$table->string('name', 100)->comment('양식명');
|
||||
$table->string('code', 50)->comment('양식코드');
|
||||
$table->string('category', 50)->nullable()->comment('분류');
|
||||
$table->json('template')->comment('양식 템플릿 (필드 정의)');
|
||||
$table->boolean('is_active')->default(true)->comment('활성 여부');
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete()->comment('생성자');
|
||||
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete()->comment('수정자');
|
||||
$table->foreignId('deleted_by')->nullable()->constrained('users')->nullOnDelete()->comment('삭제자');
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'code'], 'uk_tenant_code');
|
||||
$table->index('tenant_id', 'idx_tenant');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('approval_forms');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('approval_lines', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->onDelete('cascade')->comment('테넌트 ID');
|
||||
$table->string('name', 100)->comment('결재선 이름');
|
||||
$table->json('steps')->comment('결재선 단계 [{order, type: approval/agreement/reference, user_id, position}]');
|
||||
$table->boolean('is_default')->default(false)->comment('기본 결재선 여부');
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete()->comment('생성자');
|
||||
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete()->comment('수정자');
|
||||
$table->foreignId('deleted_by')->nullable()->constrained('users')->nullOnDelete()->comment('삭제자');
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('tenant_id', 'idx_tenant');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('approval_lines');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('approvals', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->onDelete('cascade')->comment('테넌트 ID');
|
||||
$table->string('document_number', 50)->comment('문서번호');
|
||||
$table->foreignId('form_id')->constrained('approval_forms')->onDelete('restrict')->comment('결재 양식 ID');
|
||||
$table->string('title', 200)->comment('제목');
|
||||
$table->json('content')->comment('양식 데이터');
|
||||
$table->string('status', 20)->default('draft')->comment('상태: draft/pending/approved/rejected/cancelled');
|
||||
$table->foreignId('drafter_id')->constrained('users')->onDelete('restrict')->comment('기안자');
|
||||
$table->timestamp('drafted_at')->nullable()->comment('기안일시');
|
||||
$table->timestamp('completed_at')->nullable()->comment('완료일시');
|
||||
$table->integer('current_step')->default(0)->comment('현재 결재 단계');
|
||||
$table->json('attachments')->nullable()->comment('첨부파일 IDs');
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete()->comment('생성자');
|
||||
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete()->comment('수정자');
|
||||
$table->foreignId('deleted_by')->nullable()->constrained('users')->nullOnDelete()->comment('삭제자');
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'document_number'], 'uk_tenant_document');
|
||||
$table->index(['tenant_id', 'status'], 'idx_tenant_status');
|
||||
$table->index('drafter_id', 'idx_drafter');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('approvals');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('approval_steps', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('approval_id')->constrained('approvals')->onDelete('cascade')->comment('결재 문서 ID');
|
||||
$table->integer('step_order')->comment('결재 순서');
|
||||
$table->string('step_type', 20)->comment('단계 유형: approval/agreement/reference');
|
||||
$table->foreignId('approver_id')->constrained('users')->onDelete('restrict')->comment('결재자 ID');
|
||||
$table->string('status', 20)->default('pending')->comment('상태: pending/approved/rejected/skipped');
|
||||
$table->text('comment')->nullable()->comment('결재 의견');
|
||||
$table->timestamp('acted_at')->nullable()->comment('결재일시');
|
||||
$table->boolean('is_read')->default(false)->comment('열람 여부 (참조용)');
|
||||
$table->timestamp('read_at')->nullable()->comment('열람일시 (참조용)');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('approval_id', 'idx_approval');
|
||||
$table->index(['approver_id', 'status'], 'idx_approver_status');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('approval_steps');
|
||||
}
|
||||
};
|
||||
@@ -173,6 +173,25 @@
|
||||
'balance_not_found' => '휴가 잔여일수 정보를 찾을 수 없습니다.',
|
||||
],
|
||||
|
||||
// 전자결재 관련
|
||||
'approval' => [
|
||||
'not_found' => '결재 문서를 찾을 수 없습니다.',
|
||||
'form_not_found' => '결재 양식을 찾을 수 없습니다.',
|
||||
'form_code_exists' => '중복된 양식 코드입니다.',
|
||||
'form_in_use' => '사용 중인 양식은 삭제할 수 없습니다.',
|
||||
'line_not_found' => '결재선을 찾을 수 없습니다.',
|
||||
'not_editable' => '임시저장 상태의 문서만 수정할 수 있습니다.',
|
||||
'not_deletable' => '임시저장 상태의 문서만 삭제할 수 있습니다.',
|
||||
'not_submittable' => '임시저장 상태의 문서만 상신할 수 있습니다.',
|
||||
'not_actionable' => '진행중인 문서에서만 결재 가능합니다.',
|
||||
'not_cancellable' => '진행중인 문서만 회수할 수 있습니다.',
|
||||
'not_your_turn' => '현재 결재 순서가 아닙니다.',
|
||||
'only_drafter_can_cancel' => '기안자만 회수할 수 있습니다.',
|
||||
'steps_required' => '결재선은 필수입니다.',
|
||||
'reject_comment_required' => '반려 사유는 필수입니다.',
|
||||
'not_referee' => '참조자가 아닙니다.',
|
||||
],
|
||||
|
||||
// 근무 설정 관련
|
||||
'work_setting' => [
|
||||
'invalid_work_type' => '유효하지 않은 근무 유형입니다.',
|
||||
|
||||
@@ -218,6 +218,26 @@
|
||||
'balance_updated' => '휴가 일수가 설정되었습니다.',
|
||||
],
|
||||
|
||||
// 전자결재 관리
|
||||
'approval' => [
|
||||
'fetched' => '결재 문서를 조회했습니다.',
|
||||
'created' => '결재 문서가 작성되었습니다.',
|
||||
'updated' => '결재 문서가 수정되었습니다.',
|
||||
'deleted' => '결재 문서가 삭제되었습니다.',
|
||||
'submitted' => '결재가 상신되었습니다.',
|
||||
'approved' => '결재가 승인되었습니다.',
|
||||
'rejected' => '결재가 반려되었습니다.',
|
||||
'cancelled' => '결재가 회수되었습니다.',
|
||||
'marked_read' => '열람 처리되었습니다.',
|
||||
'marked_unread' => '미열람 처리되었습니다.',
|
||||
'form_created' => '결재 양식이 등록되었습니다.',
|
||||
'form_updated' => '결재 양식이 수정되었습니다.',
|
||||
'form_deleted' => '결재 양식이 삭제되었습니다.',
|
||||
'line_created' => '결재선이 등록되었습니다.',
|
||||
'line_updated' => '결재선이 수정되었습니다.',
|
||||
'line_deleted' => '결재선이 삭제되었습니다.',
|
||||
],
|
||||
|
||||
// 근무 설정 관리
|
||||
'work_setting' => [
|
||||
'fetched' => '근무 설정을 조회했습니다.',
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
use App\Http\Controllers\Api\V1\ItemsController;
|
||||
use App\Http\Controllers\Api\V1\ItemsFileController;
|
||||
// use App\Http\Controllers\Api\V1\MaterialController; // REMOVED: materials 테이블 삭제됨
|
||||
use App\Http\Controllers\Api\V1\ApprovalController;
|
||||
use App\Http\Controllers\Api\V1\ApprovalFormController;
|
||||
use App\Http\Controllers\Api\V1\ApprovalLineController;
|
||||
use App\Http\Controllers\Api\V1\LeaveController;
|
||||
use App\Http\Controllers\Api\V1\MenuController;
|
||||
use App\Http\Controllers\Api\V1\ModelSetController;
|
||||
@@ -266,6 +269,50 @@
|
||||
Route::post('/{id}/cancel', [LeaveController::class, 'cancel'])->name('v1.leaves.cancel');
|
||||
});
|
||||
|
||||
// Approval Form API (결재 양식)
|
||||
Route::prefix('approval-forms')->group(function () {
|
||||
Route::get('', [ApprovalFormController::class, 'index'])->name('v1.approval-forms.index');
|
||||
Route::post('', [ApprovalFormController::class, 'store'])->name('v1.approval-forms.store');
|
||||
Route::get('/active', [ApprovalFormController::class, 'active'])->name('v1.approval-forms.active');
|
||||
Route::get('/{id}', [ApprovalFormController::class, 'show'])->whereNumber('id')->name('v1.approval-forms.show');
|
||||
Route::patch('/{id}', [ApprovalFormController::class, 'update'])->whereNumber('id')->name('v1.approval-forms.update');
|
||||
Route::delete('/{id}', [ApprovalFormController::class, 'destroy'])->whereNumber('id')->name('v1.approval-forms.destroy');
|
||||
});
|
||||
|
||||
// Approval Line API (결재선)
|
||||
Route::prefix('approval-lines')->group(function () {
|
||||
Route::get('', [ApprovalLineController::class, 'index'])->name('v1.approval-lines.index');
|
||||
Route::post('', [ApprovalLineController::class, 'store'])->name('v1.approval-lines.store');
|
||||
Route::get('/{id}', [ApprovalLineController::class, 'show'])->whereNumber('id')->name('v1.approval-lines.show');
|
||||
Route::patch('/{id}', [ApprovalLineController::class, 'update'])->whereNumber('id')->name('v1.approval-lines.update');
|
||||
Route::delete('/{id}', [ApprovalLineController::class, 'destroy'])->whereNumber('id')->name('v1.approval-lines.destroy');
|
||||
});
|
||||
|
||||
// Approval API (전자결재)
|
||||
Route::prefix('approvals')->group(function () {
|
||||
// 기안함
|
||||
Route::get('/drafts', [ApprovalController::class, 'drafts'])->name('v1.approvals.drafts');
|
||||
Route::get('/drafts/summary', [ApprovalController::class, 'draftsSummary'])->name('v1.approvals.drafts.summary');
|
||||
// 결재함
|
||||
Route::get('/inbox', [ApprovalController::class, 'inbox'])->name('v1.approvals.inbox');
|
||||
Route::get('/inbox/summary', [ApprovalController::class, 'inboxSummary'])->name('v1.approvals.inbox.summary');
|
||||
// 참조함
|
||||
Route::get('/reference', [ApprovalController::class, 'reference'])->name('v1.approvals.reference');
|
||||
// CRUD
|
||||
Route::post('', [ApprovalController::class, 'store'])->name('v1.approvals.store');
|
||||
Route::get('/{id}', [ApprovalController::class, 'show'])->whereNumber('id')->name('v1.approvals.show');
|
||||
Route::patch('/{id}', [ApprovalController::class, 'update'])->whereNumber('id')->name('v1.approvals.update');
|
||||
Route::delete('/{id}', [ApprovalController::class, 'destroy'])->whereNumber('id')->name('v1.approvals.destroy');
|
||||
// 액션
|
||||
Route::post('/{id}/submit', [ApprovalController::class, 'submit'])->whereNumber('id')->name('v1.approvals.submit');
|
||||
Route::post('/{id}/approve', [ApprovalController::class, 'approve'])->whereNumber('id')->name('v1.approvals.approve');
|
||||
Route::post('/{id}/reject', [ApprovalController::class, 'reject'])->whereNumber('id')->name('v1.approvals.reject');
|
||||
Route::post('/{id}/cancel', [ApprovalController::class, 'cancel'])->whereNumber('id')->name('v1.approvals.cancel');
|
||||
// 참조 열람
|
||||
Route::post('/{id}/read', [ApprovalController::class, 'markRead'])->whereNumber('id')->name('v1.approvals.read');
|
||||
Route::post('/{id}/unread', [ApprovalController::class, 'markUnread'])->whereNumber('id')->name('v1.approvals.unread');
|
||||
});
|
||||
|
||||
// Site API (현장 관리)
|
||||
Route::prefix('sites')->group(function () {
|
||||
Route::get('', [SiteController::class, 'index'])->name('v1.sites.index');
|
||||
|
||||
Reference in New Issue
Block a user