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개 등록
2025-12-17 23:23:20 +09:00
< ? php
namespace App\Services ;
2026-03-07 02:58:32 +09:00
use App\Models\Documents\Document ;
2026-03-11 16:57:54 +09:00
use App\Models\Members\User ;
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개 등록
2025-12-17 23:23:20 +09:00
use App\Models\Tenants\Approval ;
2026-03-11 16:57:54 +09:00
use App\Models\Tenants\ApprovalDelegation ;
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개 등록
2025-12-17 23:23:20 +09:00
use App\Models\Tenants\ApprovalForm ;
use App\Models\Tenants\ApprovalLine ;
use App\Models\Tenants\ApprovalStep ;
2026-03-11 16:57:54 +09:00
use App\Models\Tenants\Leave ;
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개 등록
2025-12-17 23:23:20 +09:00
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
{
2026-01-23 10:05:50 +09:00
public function __construct (
protected TodayIssueObserverService $todayIssueService
) {}
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개 등록
2025-12-17 23:23:20 +09:00
// =========================================================================
// 결재 양식 관리
// =========================================================================
/**
* 결재 양식 목록
*/
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 )
2025-12-28 02:49:46 +09:00
-> with ([
'form:id,name,code,category' ,
'drafter:id,name' ,
'drafter.tenantProfile:id,user_id,position_key,department_id' ,
'drafter.tenantProfile.department:id,name' ,
'steps.approver:id,name' ,
'steps.approver.tenantProfile:id,user_id,position_key,department_id' ,
'steps.approver.tenantProfile.department:id,name' ,
]);
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개 등록
2025-12-17 23:23:20 +09:00
// 상태 필터
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 ]);
})
2026-01-22 23:19:05 +09:00
-> with ([
'form:id,name,code,category' ,
'drafter:id,name' ,
'drafter.tenantProfile:id,user_id,position_key,department_id' ,
'drafter.tenantProfile.department:id,name' ,
'steps' => function ( $q ) use ( $userId ) {
$q -> where ( 'approver_id' , $userId );
},
'steps.approver:id,name' ,
'steps.approver.tenantProfile:id,user_id,position_key,department_id' ,
'steps.approver.tenantProfile.department:id,name' ,
]);
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개 등록
2025-12-17 23:23:20 +09:00
// 상태 필터
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 );
});
}
}
2026-03-07 02:58:32 +09:00
// 날짜 범위 필터
if ( ! empty ( $params [ 'start_date' ])) {
$query -> whereDate ( 'created_at' , '>=' , $params [ 'start_date' ]);
}
if ( ! empty ( $params [ 'end_date' ])) {
$query -> whereDate ( 'created_at' , '<=' , $params [ 'end_date' ]);
}
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개 등록
2025-12-17 23:23:20 +09:00
// 정렬
$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 ();
2026-03-07 02:58:32 +09:00
$approval = Approval :: query ()
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개 등록
2025-12-17 23:23:20 +09:00
-> where ( 'tenant_id' , $tenantId )
-> with ([
'form:id,name,code,category,template' ,
'drafter:id,name,email' ,
2025-12-28 02:49:46 +09:00
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'drafter.tenantProfile.department:id,name' ,
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개 등록
2025-12-17 23:23:20 +09:00
'steps.approver:id,name,email' ,
2025-12-28 02:49:46 +09:00
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'steps.approver.tenantProfile.department:id,name' ,
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개 등록
2025-12-17 23:23:20 +09:00
])
-> findOrFail ( $id );
2026-03-07 02:58:32 +09:00
// Document 브릿지: 연결된 문서 데이터 로딩
if ( $approval -> linkable_type === Document :: class ) {
$approval -> load ([
'linkable.template' ,
'linkable.template.approvalLines' ,
'linkable.data' ,
'linkable.approvals.user:id,name' ,
'linkable.attachments' ,
]);
}
return $approval ;
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개 등록
2025-12-17 23:23:20 +09:00
}
/**
* 결재 문서 생성 ( 임시저장 또는 상신 )
*/
public function store ( array $data ) : Approval
{
$tenantId = $this -> tenantId ();
$userId = $this -> apiUserId ();
return DB :: transaction ( function () use ( $data , $tenantId , $userId ) {
2025-12-28 02:49:46 +09:00
// 양식 확인 (form_id 또는 form_code 지원)
$formQuery = ApprovalForm :: query ()
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개 등록
2025-12-17 23:23:20 +09:00
-> where ( 'tenant_id' , $tenantId )
2025-12-28 02:49:46 +09:00
-> active ();
if ( ! empty ( $data [ 'form_id' ])) {
$form = $formQuery -> where ( 'id' , $data [ 'form_id' ]) -> firstOrFail ();
} elseif ( ! empty ( $data [ 'form_code' ])) {
$form = $formQuery -> where ( 'code' , $data [ 'form_code' ]) -> firstOrFail ();
} else {
throw new BadRequestHttpException ( __ ( 'error.approval.form_required' ));
}
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개 등록
2025-12-17 23:23:20 +09:00
// 문서번호 생성
$documentNumber = $this -> generateDocumentNumber ( $tenantId );
$status = ! empty ( $data [ 'submit' ]) ? Approval :: STATUS_PENDING : Approval :: STATUS_DRAFT ;
$approval = Approval :: create ([
'tenant_id' => $tenantId ,
'document_number' => $documentNumber ,
2025-12-28 02:49:46 +09:00
'form_id' => $form -> id ,
2026-03-11 16:57:54 +09:00
'line_id' => $data [ 'line_id' ] ? ? null ,
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개 등록
2025-12-17 23:23:20 +09:00
'title' => $data [ 'title' ],
'content' => $data [ 'content' ],
2026-03-11 16:57:54 +09:00
'body' => $data [ 'body' ] ? ? null ,
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개 등록
2025-12-17 23:23:20 +09:00
'status' => $status ,
2026-03-11 16:57:54 +09:00
'is_urgent' => $data [ 'is_urgent' ] ? ? false ,
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개 등록
2025-12-17 23:23:20 +09:00
'drafter_id' => $userId ,
2026-03-11 16:57:54 +09:00
'department_id' => $data [ 'department_id' ] ? ? null ,
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개 등록
2025-12-17 23:23:20 +09:00
'drafted_at' => $status === Approval :: STATUS_PENDING ? now () : null ,
'attachments' => $data [ 'attachments' ] ? ? null ,
'created_by' => $userId ,
'updated_by' => $userId ,
]);
2025-12-28 02:49:46 +09:00
// 결재선 생성 (steps가 있으면 항상 저장)
if ( ! empty ( $data [ 'steps' ])) {
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개 등록
2025-12-17 23:23:20 +09:00
$this -> createApprovalSteps ( $approval , $data [ 'steps' ]);
}
return $approval -> fresh ([
'form:id,name,code,category' ,
'drafter:id,name' ,
2025-12-28 02:49:46 +09:00
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'drafter.tenantProfile.department:id,name' ,
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개 등록
2025-12-17 23:23:20 +09:00
'steps.approver:id,name' ,
2025-12-28 02:49:46 +09:00
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'steps.approver.tenantProfile.department:id,name' ,
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개 등록
2025-12-17 23:23:20 +09:00
]);
});
}
/**
* 결재 문서 수정 ( 임시저장 상태만 )
*/
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' ));
}
2025-12-28 02:49:46 +09:00
// form_id 또는 form_code로 양식 ID 결정
$formId = $approval -> form_id ;
if ( ! empty ( $data [ 'form_id' ])) {
$formId = $data [ 'form_id' ];
} elseif ( ! empty ( $data [ 'form_code' ])) {
$form = ApprovalForm :: query ()
-> where ( 'tenant_id' , $tenantId )
-> where ( 'code' , $data [ 'form_code' ])
-> active ()
-> first ();
if ( $form ) {
$formId = $form -> id ;
}
}
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개 등록
2025-12-17 23:23:20 +09:00
$approval -> fill ([
2025-12-28 02:49:46 +09:00
'form_id' => $formId ,
2026-03-11 16:57:54 +09:00
'line_id' => $data [ 'line_id' ] ? ? $approval -> line_id ,
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개 등록
2025-12-17 23:23:20 +09:00
'title' => $data [ 'title' ] ? ? $approval -> title ,
'content' => $data [ 'content' ] ? ? $approval -> content ,
2026-03-11 16:57:54 +09:00
'body' => $data [ 'body' ] ? ? $approval -> body ,
'is_urgent' => $data [ 'is_urgent' ] ? ? $approval -> is_urgent ,
'department_id' => $data [ 'department_id' ] ? ? $approval -> department_id ,
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개 등록
2025-12-17 23:23:20 +09:00
'attachments' => $data [ 'attachments' ] ? ? $approval -> attachments ,
'updated_by' => $userId ,
]);
$approval -> save ();
2025-12-28 02:49:46 +09:00
// 결재선 수정 (steps가 전달된 경우)
if ( ! empty ( $data [ 'steps' ])) {
// 기존 결재선 삭제 후 새로 생성
$approval -> steps () -> delete ();
$this -> createApprovalSteps ( $approval , $data [ 'steps' ]);
}
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개 등록
2025-12-17 23:23:20 +09:00
return $approval -> fresh ([
'form:id,name,code,category' ,
'drafter:id,name' ,
2025-12-28 02:49:46 +09:00
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'drafter.tenantProfile.department:id,name' ,
'steps.approver:id,name' ,
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'steps.approver.tenantProfile.department:id,name' ,
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개 등록
2025-12-17 23:23:20 +09:00
]);
});
}
/**
* 결재 문서 삭제 ( 임시저장 상태만 )
*/
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 ;
});
}
/**
2026-03-11 16:57:54 +09:00
* 결재 상신 ( draft → pending , rejected → pending 재상신 포함 )
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개 등록
2025-12-17 23:23:20 +09:00
*/
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 )
2026-03-11 16:57:54 +09:00
-> with ( 'steps' )
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개 등록
2025-12-17 23:23:20 +09:00
-> findOrFail ( $id );
if ( ! $approval -> isSubmittable ()) {
throw new BadRequestHttpException ( __ ( 'error.approval.not_submittable' ));
}
2026-01-23 10:05:50 +09:00
// 기존 결재선 확인 (steps 없이 상신하는 경우)
if ( empty ( $data [ 'steps' ])) {
2025-12-28 02:49:46 +09:00
$existingSteps = $approval -> steps ()
-> whereIn ( 'step_type' , [ ApprovalLine :: STEP_TYPE_APPROVAL , ApprovalLine :: STEP_TYPE_AGREEMENT ])
-> count ();
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개 등록
2025-12-17 23:23:20 +09:00
2025-12-28 02:49:46 +09:00
if ( $existingSteps === 0 ) {
throw new BadRequestHttpException ( __ ( 'error.approval.steps_required' ));
}
}
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개 등록
2025-12-17 23:23:20 +09:00
2026-03-11 16:57:54 +09:00
// 반려 후 재상신 처리
$isResubmit = $approval -> status === Approval :: STATUS_REJECTED ;
if ( $isResubmit ) {
// 반려 이력 저장
$rejectedStep = $approval -> steps
-> firstWhere ( 'status' , ApprovalStep :: STATUS_REJECTED );
if ( $rejectedStep ) {
$history = $approval -> rejection_history ? ? [];
$history [] = [
'round' => $approval -> resubmit_count + 1 ,
'approver_name' => $rejectedStep -> approver_name ? ? '' ,
'approver_position' => $rejectedStep -> approver_position ? ? '' ,
'comment' => $rejectedStep -> comment ,
'rejected_at' => $rejectedStep -> acted_at ? -> format ( 'Y-m-d H:i:s' ),
];
$approval -> rejection_history = $history ;
}
// 기존 steps를 모두 pending으로 초기화
$approval -> steps () -> update ([
'status' => ApprovalStep :: STATUS_PENDING ,
'comment' => null ,
'acted_at' => null ,
]);
}
// approval을 pending으로 변경
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개 등록
2025-12-17 23:23:20 +09:00
$approval -> status = Approval :: STATUS_PENDING ;
$approval -> drafted_at = now ();
$approval -> current_step = 1 ;
2026-03-11 16:57:54 +09:00
$approval -> resubmit_count = $isResubmit
? ( $approval -> resubmit_count ? ? 0 ) + 1
: ( $approval -> resubmit_count ? ? 0 );
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개 등록
2025-12-17 23:23:20 +09:00
$approval -> updated_by = $userId ;
$approval -> save ();
2026-03-11 16:57:54 +09:00
// steps가 있으면 새로 생성
2026-01-23 10:05:50 +09:00
if ( ! empty ( $data [ 'steps' ])) {
$approval -> steps () -> delete ();
$this -> createApprovalSteps ( $approval , $data [ 'steps' ]);
} else {
2026-03-11 16:57:54 +09:00
// 기존 결재선 사용 시 알림 발송
2026-01-23 10:05:50 +09:00
$firstPendingStep = $approval -> steps ()
-> where ( 'status' , ApprovalStep :: STATUS_PENDING )
-> whereIn ( 'step_type' , [ ApprovalLine :: STEP_TYPE_APPROVAL , ApprovalLine :: STEP_TYPE_AGREEMENT ])
-> orderBy ( 'step_order' )
-> first ();
if ( $firstPendingStep ) {
$this -> todayIssueService -> handleApprovalStepChange ( $firstPendingStep );
}
}
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개 등록
2025-12-17 23:23:20 +09:00
return $approval -> fresh ([
'form:id,name,code,category' ,
'drafter:id,name' ,
2025-12-28 02:49:46 +09:00
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'drafter.tenantProfile.department:id,name' ,
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개 등록
2025-12-17 23:23:20 +09:00
'steps.approver:id,name' ,
2025-12-28 02:49:46 +09:00
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'steps.approver.tenantProfile.department:id,name' ,
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개 등록
2025-12-17 23:23:20 +09:00
]);
});
}
/**
* 결재 승인
*/
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 ();
2026-03-11 16:57:54 +09:00
$approval -> drafter_read_at = null ;
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개 등록
2025-12-17 23:23:20 +09:00
}
$approval -> current_step = $myStep -> step_order + 1 ;
$approval -> updated_by = $userId ;
$approval -> save ();
2026-03-07 02:58:32 +09:00
// Document 브릿지 동기화
$this -> syncToLinkedDocument ( $approval );
2026-03-11 16:57:54 +09:00
// Leave 연동 (승인 완료 시)
if ( $approval -> status === Approval :: STATUS_APPROVED ) {
$this -> handleApprovalCompleted ( $approval );
}
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개 등록
2025-12-17 23:23:20 +09:00
return $approval -> fresh ([
'form:id,name,code,category' ,
'drafter:id,name' ,
2025-12-28 02:49:46 +09:00
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'drafter.tenantProfile.department:id,name' ,
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개 등록
2025-12-17 23:23:20 +09:00
'steps.approver:id,name' ,
2025-12-28 02:49:46 +09:00
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'steps.approver.tenantProfile.department:id,name' ,
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개 등록
2025-12-17 23:23:20 +09:00
]);
});
}
/**
* 결재 반려
*/
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 ();
2026-03-11 16:57:54 +09:00
$approval -> drafter_read_at = null ;
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개 등록
2025-12-17 23:23:20 +09:00
$approval -> updated_by = $userId ;
$approval -> save ();
2026-03-07 02:58:32 +09:00
// Document 브릿지 동기화
$this -> syncToLinkedDocument ( $approval );
2026-03-11 16:57:54 +09:00
// Leave 연동 (반려 시)
$this -> handleApprovalRejected ( $approval , $comment );
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개 등록
2025-12-17 23:23:20 +09:00
return $approval -> fresh ([
'form:id,name,code,category' ,
'drafter:id,name' ,
2025-12-28 02:49:46 +09:00
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'drafter.tenantProfile.department:id,name' ,
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개 등록
2025-12-17 23:23:20 +09:00
'steps.approver:id,name' ,
2025-12-28 02:49:46 +09:00
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'steps.approver.tenantProfile.department:id,name' ,
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개 등록
2025-12-17 23:23:20 +09:00
]);
});
}
/**
2026-03-11 16:57:54 +09:00
* 결재 회수 ( 기안자만 , pending / on_hold → cancelled )
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개 등록
2025-12-17 23:23:20 +09:00
*/
2026-03-11 16:57:54 +09:00
public function cancel ( int $id , ? string $recallReason = null ) : Approval
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개 등록
2025-12-17 23:23:20 +09:00
{
$tenantId = $this -> tenantId ();
$userId = $this -> apiUserId ();
2026-03-11 16:57:54 +09:00
return DB :: transaction ( function () use ( $id , $recallReason , $tenantId , $userId ) {
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개 등록
2025-12-17 23:23:20 +09:00
$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' ));
}
2026-03-11 16:57:54 +09:00
// 첫 번째 결재자가 이미 처리했으면 회수 불가
$firstApproverStep = $approval -> steps ()
-> whereIn ( 'step_type' , [ ApprovalLine :: STEP_TYPE_APPROVAL , ApprovalLine :: STEP_TYPE_AGREEMENT ])
-> orderBy ( 'step_order' )
-> first ();
if ( $firstApproverStep
&& $firstApproverStep -> status !== ApprovalStep :: STATUS_PENDING
&& $firstApproverStep -> status !== ApprovalStep :: STATUS_ON_HOLD ) {
throw new BadRequestHttpException ( __ ( 'error.approval.first_approver_already_acted' ));
}
// 모든 pending/on_hold steps → skipped
$approval -> steps ()
-> whereIn ( 'status' , [ ApprovalStep :: STATUS_PENDING , ApprovalStep :: STATUS_ON_HOLD ])
-> update ([ 'status' => ApprovalStep :: STATUS_SKIPPED ]);
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개 등록
2025-12-17 23:23:20 +09:00
$approval -> status = Approval :: STATUS_CANCELLED ;
$approval -> completed_at = now ();
2026-03-11 16:57:54 +09:00
$approval -> recall_reason = $recallReason ;
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개 등록
2025-12-17 23:23:20 +09:00
$approval -> updated_by = $userId ;
$approval -> save ();
2026-03-11 16:57:54 +09:00
// Document 브릿지 동기화
2026-03-07 02:58:32 +09:00
$this -> syncToLinkedDocument ( $approval );
2026-03-11 16:57:54 +09:00
// Leave 연동 (회수 시)
$this -> handleApprovalCancelled ( $approval );
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개 등록
2025-12-17 23:23:20 +09:00
return $approval -> fresh ([
'form:id,name,code,category' ,
'drafter:id,name' ,
2026-03-11 16:57:54 +09:00
'steps.approver:id,name' ,
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개 등록
2025-12-17 23:23:20 +09:00
]);
});
}
2026-03-11 16:57:54 +09:00
/**
* 보류 ( 현재 결재자만 , pending → on_hold )
*/
public function hold ( 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 )
-> with ( 'steps' )
-> findOrFail ( $id );
if ( ! $approval -> isHoldable ()) {
throw new BadRequestHttpException ( __ ( 'error.approval.not_holdable' ));
}
$currentStep = $approval -> getCurrentApproverStep ();
if ( ! $currentStep || $currentStep -> approver_id !== $userId ) {
throw new BadRequestHttpException ( __ ( 'error.approval.not_your_turn' ));
}
$currentStep -> status = ApprovalStep :: STATUS_ON_HOLD ;
$currentStep -> comment = $comment ;
$currentStep -> acted_at = now ();
$currentStep -> save ();
$approval -> status = Approval :: STATUS_ON_HOLD ;
$approval -> updated_by = $userId ;
$approval -> save ();
return $approval -> fresh ([
'form:id,name,code,category' ,
'drafter:id,name' ,
'steps.approver:id,name' ,
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'steps.approver.tenantProfile.department:id,name' ,
]);
});
}
/**
* 보류 해제 ( 보류한 결재자만 , on_hold → pending )
*/
public function releaseHold ( int $id ) : Approval
{
$tenantId = $this -> tenantId ();
$userId = $this -> apiUserId ();
return DB :: transaction ( function () use ( $id , $tenantId , $userId ) {
$approval = Approval :: query ()
-> where ( 'tenant_id' , $tenantId )
-> with ( 'steps' )
-> findOrFail ( $id );
if ( ! $approval -> isHoldReleasable ()) {
throw new BadRequestHttpException ( __ ( 'error.approval.not_hold_releasable' ));
}
$holdStep = $approval -> steps ()
-> where ( 'status' , ApprovalStep :: STATUS_ON_HOLD )
-> first ();
if ( ! $holdStep || $holdStep -> approver_id !== $userId ) {
throw new BadRequestHttpException ( __ ( 'error.approval.only_holder_can_release' ));
}
$holdStep -> status = ApprovalStep :: STATUS_PENDING ;
$holdStep -> comment = null ;
$holdStep -> acted_at = null ;
$holdStep -> save ();
$approval -> status = Approval :: STATUS_PENDING ;
$approval -> updated_by = $userId ;
$approval -> save ();
return $approval -> fresh ([
'form:id,name,code,category' ,
'drafter:id,name' ,
'steps.approver:id,name' ,
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'steps.approver.tenantProfile.department:id,name' ,
]);
});
}
/**
* 전결 ( 현재 결재자가 이후 모든 결재를 건너뛰고 최종 승인 )
*/
public function preDecide ( 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 )
-> with ( 'steps' )
-> findOrFail ( $id );
if ( ! $approval -> isActionable ()) {
throw new BadRequestHttpException ( __ ( 'error.approval.not_actionable' ));
}
$currentStep = $approval -> getCurrentApproverStep ();
if ( ! $currentStep || $currentStep -> approver_id !== $userId ) {
throw new BadRequestHttpException ( __ ( 'error.approval.not_your_turn' ));
}
// 현재 step → approved + pre_decided
$currentStep -> status = ApprovalStep :: STATUS_APPROVED ;
$currentStep -> approval_type = 'pre_decided' ;
$currentStep -> comment = $comment ;
$currentStep -> acted_at = now ();
$currentStep -> save ();
// 이후 모든 pending approval/agreement steps → skipped
$approval -> steps ()
-> where ( 'step_order' , '>' , $currentStep -> step_order )
-> whereIn ( 'step_type' , [ ApprovalLine :: STEP_TYPE_APPROVAL , ApprovalLine :: STEP_TYPE_AGREEMENT ])
-> where ( 'status' , ApprovalStep :: STATUS_PENDING )
-> update ([ 'status' => ApprovalStep :: STATUS_SKIPPED ]);
// 문서 최종 승인
$approval -> status = Approval :: STATUS_APPROVED ;
$approval -> completed_at = now ();
$approval -> drafter_read_at = null ;
$approval -> updated_by = $userId ;
$approval -> save ();
// Document 브릿지 동기화
$this -> syncToLinkedDocument ( $approval );
// Leave 연동 (승인 완료)
$this -> handleApprovalCompleted ( $approval );
return $approval -> fresh ([
'form:id,name,code,category' ,
'drafter:id,name' ,
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'drafter.tenantProfile.department:id,name' ,
'steps.approver:id,name' ,
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'steps.approver.tenantProfile.department:id,name' ,
]);
});
}
/**
* 복사 재기안 ( 완료 / 반려 / 회수 문서를 복사하여 새 draft 생성 )
*/
public function copyForRedraft ( int $id ) : Approval
{
$tenantId = $this -> tenantId ();
$userId = $this -> apiUserId ();
return DB :: transaction ( function () use ( $id , $tenantId , $userId ) {
$original = Approval :: query ()
-> where ( 'tenant_id' , $tenantId )
-> with ( 'steps' )
-> findOrFail ( $id );
if ( ! $original -> isCopyable ()) {
throw new BadRequestHttpException ( __ ( 'error.approval.not_copyable' ));
}
if ( $original -> drafter_id !== $userId ) {
throw new BadRequestHttpException ( __ ( 'error.approval.only_drafter_can_copy' ));
}
$documentNumber = $this -> generateDocumentNumber ( $tenantId );
$newApproval = Approval :: create ([
'tenant_id' => $tenantId ,
'document_number' => $documentNumber ,
'form_id' => $original -> form_id ,
'line_id' => $original -> line_id ,
'title' => $original -> title ,
'content' => $original -> content ,
'body' => $original -> body ,
'status' => Approval :: STATUS_DRAFT ,
'is_urgent' => $original -> is_urgent ,
'drafter_id' => $userId ,
'department_id' => $original -> department_id ,
'current_step' => 0 ,
'parent_doc_id' => $original -> id ,
'created_by' => $userId ,
'updated_by' => $userId ,
]);
// 결재선 복사 (모두 pending 상태로, 스냅샷 유지)
foreach ( $original -> steps as $step ) {
ApprovalStep :: create ([
2026-03-11 17:13:08 +09:00
'tenant_id' => $tenantId ,
2026-03-11 16:57:54 +09:00
'approval_id' => $newApproval -> id ,
'step_order' => $step -> step_order ,
'step_type' => $step -> step_type ,
'approver_id' => $step -> approver_id ,
'approver_name' => $step -> approver_name ,
'approver_department' => $step -> approver_department ,
'approver_position' => $step -> approver_position ,
'status' => ApprovalStep :: STATUS_PENDING ,
]);
}
return $newApproval -> fresh ([
'form:id,name,code,category' ,
'drafter:id,name' ,
'steps.approver:id,name' ,
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'steps.approver.tenantProfile.department:id,name' ,
]);
});
}
/**
* 완료함 - 내가 기안한 완료 문서 + 내가 결재 처리한 문서
*/
public function completed ( array $params ) : LengthAwarePaginator
{
$tenantId = $this -> tenantId ();
$userId = $this -> apiUserId ();
$query = Approval :: query ()
-> where ( 'tenant_id' , $tenantId )
-> where ( function ( $q ) use ( $userId ) {
$q -> where ( function ( $sub ) use ( $userId ) {
$sub -> where ( 'drafter_id' , $userId )
-> whereIn ( 'status' , [
Approval :: STATUS_APPROVED ,
Approval :: STATUS_REJECTED ,
Approval :: STATUS_CANCELLED ,
]);
})
-> orWhereHas ( 'steps' , function ( $sub ) use ( $userId ) {
$sub -> where ( 'approver_id' , $userId )
-> whereIn ( 'status' , [ ApprovalStep :: STATUS_APPROVED , ApprovalStep :: STATUS_REJECTED ]);
});
})
-> with ([
'form:id,name,code,category' ,
'drafter:id,name' ,
'drafter.tenantProfile:id,user_id,position_key,department_id' ,
'drafter.tenantProfile.department:id,name' ,
'steps.approver:id,name' ,
]);
if ( ! empty ( $params [ 'search' ])) {
$query -> where ( function ( $q ) use ( $params ) {
$q -> where ( 'title' , 'like' , " % { $params [ 'search' ] } % " )
-> orWhere ( 'document_number' , 'like' , " % { $params [ 'search' ] } % " );
});
}
if ( ! empty ( $params [ 'status' ])) {
$query -> where ( 'status' , $params [ 'status' ]);
}
$sortBy = $params [ 'sort_by' ] ? ? 'updated_at' ;
$sortDir = $params [ 'sort_dir' ] ? ? 'desc' ;
$query -> orderBy ( $sortBy , $sortDir );
$perPage = $params [ 'per_page' ] ? ? 20 ;
return $query -> paginate ( $perPage );
}
/**
* 완료함 현황 카드
*/
public function completedSummary () : array
{
$tenantId = $this -> tenantId ();
$userId = $this -> apiUserId ();
$myCompleted = Approval :: query ()
-> where ( 'tenant_id' , $tenantId )
-> where ( 'drafter_id' , $userId )
-> whereIn ( 'status' , [ Approval :: STATUS_APPROVED , Approval :: STATUS_REJECTED , Approval :: STATUS_CANCELLED ])
-> selectRaw ( 'status, COUNT(*) as count' )
-> groupBy ( 'status' )
-> pluck ( 'count' , 'status' )
-> toArray ();
$unreadCount = Approval :: query ()
-> where ( 'tenant_id' , $tenantId )
-> where ( 'drafter_id' , $userId )
-> whereIn ( 'status' , [ Approval :: STATUS_APPROVED , Approval :: STATUS_REJECTED ])
-> whereNull ( 'drafter_read_at' )
-> count ();
return [
'approved' => $myCompleted [ Approval :: STATUS_APPROVED ] ? ? 0 ,
'rejected' => $myCompleted [ Approval :: STATUS_REJECTED ] ? ? 0 ,
'cancelled' => $myCompleted [ Approval :: STATUS_CANCELLED ] ? ? 0 ,
'unread' => $unreadCount ,
];
}
/**
* 미처리 건수 ( 뱃지용 )
*/
public function badgeCounts () : array
{
$tenantId = $this -> tenantId ();
$userId = $this -> apiUserId ();
$pendingCount = 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 ();
$draftCount = Approval :: query ()
-> where ( 'tenant_id' , $tenantId )
-> where ( 'status' , Approval :: STATUS_PENDING )
-> where ( 'drafter_id' , $userId )
-> count ();
$referenceUnreadCount = ApprovalStep :: query ()
-> where ( 'approver_id' , $userId )
-> where ( 'step_type' , ApprovalLine :: STEP_TYPE_REFERENCE )
-> where ( 'is_read' , false )
-> whereHas ( 'approval' , function ( $q ) use ( $tenantId ) {
$q -> where ( 'tenant_id' , $tenantId );
})
-> count ();
$completedUnreadCount = Approval :: query ()
-> where ( 'tenant_id' , $tenantId )
-> where ( 'drafter_id' , $userId )
-> whereIn ( 'status' , [ Approval :: STATUS_APPROVED , Approval :: STATUS_REJECTED ])
-> whereNull ( 'drafter_read_at' )
-> count ();
return [
'pending' => $pendingCount ,
'draft' => $draftCount ,
'reference_unread' => $referenceUnreadCount ,
'completed_unread' => $completedUnreadCount ,
];
}
/**
* 완료함 미읽음 일괄 읽음 처리
*/
public function markCompletedAsRead () : int
{
$tenantId = $this -> tenantId ();
$userId = $this -> apiUserId ();
return Approval :: query ()
-> where ( 'tenant_id' , $tenantId )
-> where ( 'drafter_id' , $userId )
-> whereIn ( 'status' , [ Approval :: STATUS_APPROVED , Approval :: STATUS_REJECTED ])
-> whereNull ( 'drafter_read_at' )
-> update ([ 'drafter_read_at' => now ()]);
}
2026-03-07 02:58:32 +09:00
/**
* Approval → Document 브릿지 동기화
* 결재 승인 / 반려 / 회수 시 연결된 Document의 상태와 결재란을 동기화
*/
private function syncToLinkedDocument ( Approval $approval ) : void
{
if ( $approval -> linkable_type !== Document :: class ) {
return ;
}
$document = Document :: find ( $approval -> linkable_id );
if ( ! $document ) {
return ;
}
// approval_steps → document_approvals 동기화 (승인자 이름/시각 반영)
foreach ( $approval -> steps as $step ) {
if ( $step -> status === ApprovalStep :: STATUS_PENDING ) {
continue ;
}
$docApproval = $document -> approvals ()
-> where ( 'step' , $step -> step_order )
-> first ();
if ( $docApproval ) {
$docApproval -> update ([
'status' => strtoupper ( $step -> status ),
'acted_at' => $step -> acted_at ,
'comment' => $step -> comment ,
]);
}
}
// Document 전체 상태 동기화
$documentStatus = match ( $approval -> status ) {
Approval :: STATUS_APPROVED => Document :: STATUS_APPROVED ,
Approval :: STATUS_REJECTED => Document :: STATUS_REJECTED ,
Approval :: STATUS_CANCELLED => Document :: STATUS_CANCELLED ,
default => Document :: STATUS_PENDING ,
};
$document -> update ([
'status' => $documentStatus ,
'completed_at' => in_array ( $approval -> status , [
Approval :: STATUS_APPROVED ,
Approval :: STATUS_REJECTED ,
]) ? now () : null ,
]);
}
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개 등록
2025-12-17 23:23:20 +09:00
/**
* 참조 열람 처리
*/
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' ,
2025-12-28 02:49:46 +09:00
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'drafter.tenantProfile.department:id,name' ,
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개 등록
2025-12-17 23:23:20 +09:00
'steps.approver:id,name' ,
2025-12-28 02:49:46 +09:00
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'steps.approver.tenantProfile.department:id,name' ,
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개 등록
2025-12-17 23:23:20 +09:00
]);
}
/**
* 참조 미열람 처리
*/
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' ,
2025-12-28 02:49:46 +09:00
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'drafter.tenantProfile.department:id,name' ,
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개 등록
2025-12-17 23:23:20 +09:00
'steps.approver:id,name' ,
2025-12-28 02:49:46 +09:00
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name' ,
'steps.approver.tenantProfile.department:id,name' ,
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개 등록
2025-12-17 23:23:20 +09:00
]);
}
// =========================================================================
// 헬퍼 메서드
// =========================================================================
/**
* 문서번호 생성
*/
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 );
}
/**
2026-03-11 16:57:54 +09:00
* 결재 단계 생성 + 결재자 스냅샷 저장
2025-12-28 02:49:46 +09:00
* 프론트엔드 호환성 : step_type / approver_id 또는 type / user_id 지원
2025-12-30 15:06:52 +09:00
* 중복 결재자 자동 제거
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개 등록
2025-12-17 23:23:20 +09:00
*/
private function createApprovalSteps ( Approval $approval , array $steps ) : void
{
2026-03-11 16:57:54 +09:00
$tenantId = $this -> tenantId ();
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개 등록
2025-12-17 23:23:20 +09:00
$order = 1 ;
2025-12-30 15:06:52 +09:00
$processedApprovers = []; // 중복 체크용
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개 등록
2025-12-17 23:23:20 +09:00
foreach ( $steps as $step ) {
2025-12-28 02:49:46 +09:00
// 필드명 호환성: step_type 또는 type
$stepType = $step [ 'step_type' ] ? ? $step [ 'type' ] ? ? null ;
// 필드명 호환성: approver_id 또는 user_id
$approverId = $step [ 'approver_id' ] ? ? $step [ 'user_id' ] ? ? null ;
// step_order가 있으면 사용, 없으면 자동 증가
$stepOrder = $step [ 'step_order' ] ? ? $order ++ ;
if ( $stepType && $approverId ) {
2025-12-30 15:06:52 +09:00
// 동일 결재자 중복 건너뛰기
if ( in_array ( $approverId , $processedApprovers , true )) {
continue ;
}
$processedApprovers [] = $approverId ;
2026-03-11 16:57:54 +09:00
// 결재자 스냅샷 (이름/부서/직위)
$approverName = $step [ 'approver_name' ] ? ? '' ;
$approverDepartment = $step [ 'approver_department' ] ? ? null ;
$approverPosition = $step [ 'approver_position' ] ? ? null ;
// 스냅샷이 비어있으면 DB에서 조회
if ( empty ( $approverName )) {
$user = User :: find ( $approverId );
if ( $user ) {
$approverName = $user -> name ;
$profile = $user -> tenantProfile ? ? $user -> tenantProfiles ()
-> where ( 'tenant_id' , $tenantId )
-> first ();
if ( $profile ) {
$approverDepartment = $approverDepartment ? : ( $profile -> department ? -> name ? ? null );
$approverPosition = $approverPosition ? : ( $profile -> position_label ? ? $profile -> job_title_label ? ? null );
}
}
}
2025-12-28 02:49:46 +09:00
ApprovalStep :: create ([
2026-03-11 17:13:08 +09:00
'tenant_id' => $approval -> tenant_id ,
2025-12-28 02:49:46 +09:00
'approval_id' => $approval -> id ,
'step_order' => $stepOrder ,
'step_type' => $stepType ,
'approver_id' => $approverId ,
2026-03-11 16:57:54 +09:00
'approver_name' => $approverName ,
'approver_department' => $approverDepartment ,
'approver_position' => $approverPosition ,
2025-12-28 02:49:46 +09:00
'status' => ApprovalStep :: STATUS_PENDING ,
]);
}
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개 등록
2025-12-17 23:23:20 +09:00
}
}
2026-03-11 16:57:54 +09:00
// =========================================================================
// Leave 연동 (휴가/근태신청/사유서)
// =========================================================================
/**
* 휴가 / 근태신청 / 사유서 관련 결재 양식인지 확인
*/
private function isLeaveRelatedForm ( ? string $code ) : bool
{
return in_array ( $code , [ 'leave' , 'attendance_request' , 'reason_report' ]);
}
/**
* 결재 최종 승인 시 연동 처리
*/
private function handleApprovalCompleted ( Approval $approval ) : void
{
$approval -> loadMissing ( 'form' );
if ( ! $approval -> form || ! $this -> isLeaveRelatedForm ( $approval -> form -> code )) {
return ;
}
$leave = Leave :: where ( 'approval_id' , $approval -> id ) -> first ();
// 기안함에서 직접 올린 경우: Leave 레코드 자동 생성
if ( ! $leave && ! empty ( $approval -> content [ 'leave_type' ])) {
$leave = $this -> createLeaveFromApproval ( $approval );
}
if ( $leave && $leave -> status === 'pending' ) {
$leave -> update ([
'status' => 'approved' ,
'updated_by' => $approval -> updated_by ,
]);
}
}
/**
* 결재 반려 시 연동 처리
*/
private function handleApprovalRejected ( Approval $approval , string $comment ) : void
{
$approval -> loadMissing ( 'form' );
if ( ! $approval -> form || ! $this -> isLeaveRelatedForm ( $approval -> form -> code )) {
return ;
}
$leave = Leave :: where ( 'approval_id' , $approval -> id ) -> first ();
if ( $leave && $leave -> status === 'pending' ) {
$leave -> update ([
'status' => 'rejected' ,
'updated_by' => $approval -> updated_by ,
]);
}
}
/**
* 결재 회수 시 연동 처리
*/
private function handleApprovalCancelled ( Approval $approval ) : void
{
$approval -> loadMissing ( 'form' );
if ( ! $approval -> form || ! $this -> isLeaveRelatedForm ( $approval -> form -> code )) {
return ;
}
$leave = Leave :: where ( 'approval_id' , $approval -> id ) -> first ();
if ( $leave && in_array ( $leave -> status , [ 'pending' , 'approved' ])) {
$leave -> update ([
'status' => 'cancelled' ,
'updated_by' => $approval -> updated_by ,
]);
}
}
/**
* 결재 삭제 시 연동 처리
*/
private function handleApprovalDeleted ( Approval $approval ) : void
{
$approval -> loadMissing ( 'form' );
if ( ! $approval -> form || ! $this -> isLeaveRelatedForm ( $approval -> form -> code )) {
return ;
}
$leave = Leave :: where ( 'approval_id' , $approval -> id ) -> first ();
if ( $leave && in_array ( $leave -> status , [ 'pending' , 'approved' ])) {
$leave -> update ([
'status' => 'cancelled' ,
'updated_by' => $this -> apiUserId (),
]);
}
}
/**
* 기안함에서 직접 올린 결재 → Leave 레코드 자동 생성
*/
private function createLeaveFromApproval ( Approval $approval ) : Leave
{
$content = $approval -> content ;
return Leave :: create ([
'tenant_id' => $approval -> tenant_id ,
'user_id' => $content [ 'user_id' ] ? ? $approval -> drafter_id ,
'leave_type' => $content [ 'leave_type' ],
'start_date' => $content [ 'start_date' ],
'end_date' => $content [ 'end_date' ],
'days' => $content [ 'days' ] ? ? 0 ,
'reason' => $content [ 'reason' ] ? ? null ,
'status' => 'pending' ,
'approval_id' => $approval -> id ,
'created_by' => $approval -> drafter_id ,
'updated_by' => $approval -> drafter_id ,
]);
}
// =========================================================================
// 위임 관리
// =========================================================================
/**
* 위임 목록
*/
public function delegationIndex ( array $params ) : LengthAwarePaginator
{
$tenantId = $this -> tenantId ();
$query = ApprovalDelegation :: query ()
-> where ( 'tenant_id' , $tenantId )
-> with ([ 'delegator:id,name' , 'delegate:id,name' ]);
if ( ! empty ( $params [ 'delegator_id' ])) {
$query -> where ( 'delegator_id' , $params [ 'delegator_id' ]);
}
if ( isset ( $params [ 'is_active' ])) {
$query -> where ( 'is_active' , $params [ 'is_active' ]);
}
$query -> orderByDesc ( 'created_at' );
$perPage = $params [ 'per_page' ] ? ? 20 ;
return $query -> paginate ( $perPage );
}
/**
* 위임 생성
*/
public function delegationStore ( array $data ) : ApprovalDelegation
{
$tenantId = $this -> tenantId ();
$userId = $this -> apiUserId ();
return ApprovalDelegation :: create ([
'tenant_id' => $tenantId ,
'delegator_id' => $data [ 'delegator_id' ],
'delegate_id' => $data [ 'delegate_id' ],
'start_date' => $data [ 'start_date' ],
'end_date' => $data [ 'end_date' ],
'form_ids' => $data [ 'form_ids' ] ? ? null ,
'notify_delegator' => $data [ 'notify_delegator' ] ? ? true ,
'is_active' => $data [ 'is_active' ] ? ? true ,
'reason' => $data [ 'reason' ] ? ? null ,
'created_by' => $userId ,
]);
}
/**
* 위임 수정
*/
public function delegationUpdate ( int $id , array $data ) : ApprovalDelegation
{
$tenantId = $this -> tenantId ();
$delegation = ApprovalDelegation :: query ()
-> where ( 'tenant_id' , $tenantId )
-> findOrFail ( $id );
$delegation -> fill ([
'delegate_id' => $data [ 'delegate_id' ] ? ? $delegation -> delegate_id ,
'start_date' => $data [ 'start_date' ] ? ? $delegation -> start_date ,
'end_date' => $data [ 'end_date' ] ? ? $delegation -> end_date ,
'form_ids' => array_key_exists ( 'form_ids' , $data ) ? $data [ 'form_ids' ] : $delegation -> form_ids ,
'notify_delegator' => $data [ 'notify_delegator' ] ? ? $delegation -> notify_delegator ,
'is_active' => $data [ 'is_active' ] ? ? $delegation -> is_active ,
'reason' => $data [ 'reason' ] ? ? $delegation -> reason ,
]);
$delegation -> save ();
return $delegation -> fresh ([ 'delegator:id,name' , 'delegate:id,name' ]);
}
/**
* 위임 삭제
*/
public function delegationDestroy ( int $id ) : bool
{
$tenantId = $this -> tenantId ();
$delegation = ApprovalDelegation :: query ()
-> where ( 'tenant_id' , $tenantId )
-> findOrFail ( $id );
return $delegation -> delete ();
}
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개 등록
2025-12-17 23:23:20 +09:00
}