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 ;
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
{
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 );
});
}
}
// 정렬
$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' ,
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 );
}
/**
* 결재 문서 생성 ( 임시저장 또는 상신 )
*/
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 ,
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' ],
'status' => $status ,
'drafter_id' => $userId ,
'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 ,
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 ,
'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 ;
});
}
/**
* 결재 상신
*/
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' ));
}
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-01-23 10:05:50 +09:00
// 먼저 approval을 pending으로 변경 (Observer가 올바른 상태로 트리거되도록)
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 ;
$approval -> updated_by = $userId ;
$approval -> save ();
2026-01-23 10:05:50 +09:00
// steps가 있으면 새로 생성 (approval이 pending 상태일 때 생성해야 알림 발송)
if ( ! empty ( $data [ 'steps' ])) {
// 기존 결재선 삭제 후 새로 생성
$approval -> steps () -> delete ();
$this -> createApprovalSteps ( $approval , $data [ 'steps' ]);
} else {
// 기존 결재선 사용 시, Observer가 트리거되지 않으므로 수동으로 알림 발송
$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 ();
}
$approval -> current_step = $myStep -> step_order + 1 ;
$approval -> updated_by = $userId ;
$approval -> 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 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' ,
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 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' ,
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 );
}
/**
* 결재 단계 생성
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
{
$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 ;
2025-12-28 02:49:46 +09:00
ApprovalStep :: create ([
'approval_id' => $approval -> id ,
'step_order' => $stepOrder ,
'step_type' => $stepType ,
'approver_id' => $approverId ,
'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
}
}
}