feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
@ extends ( 'layouts.app' )
@ section ( 'title' , '결재 상세' )
@ section ( 'content' )
< div class = " flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6 " >
< div >
< h1 class = " text-2xl font-bold text-gray-800 " > 결재 상세 </ h1 >
< p class = " text-sm text-gray-500 mt-1 " > {{ $approval -> document_number }} </ p >
</ div >
< div class = " flex gap-2 " >
@ if ( $approval -> isEditable () && $approval -> drafter_id === auth () -> id ())
< a href = " { { route('approvals.edit', $approval->id ) }} "
class = " bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition text-sm " >
수정
</ a >
@ endif
< a href = " { { route('approvals.drafts') }} " class = " text-gray-600 hover:text-gray-800 text-sm px-3 py-2 border rounded-lg " >
목록
</ a >
</ div >
</ div >
{{ -- 문서 정보 -- }}
< div class = " bg-white rounded-lg shadow-sm p-6 mb-6 " >
2026-03-05 11:23:32 +09:00
< div class = " flex justify-between items-start gap-4 mb-4 " >
< div class = " flex flex-wrap gap-y-3 " style = " gap-column: 0; " >
< div class = " pr-6 border-r border-gray-200 mr-6 " >
< span class = " text-xs text-gray-500 " > 상태 </ span >
< div class = " mt-1 " >
@ include ( 'approvals.partials._status-badge' , [ 'status' => $approval -> status ])
@ if ( $approval -> is_urgent )
< span class = " inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700 ml-1 " > 긴급 </ span >
@ endif
</ div >
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
</ div >
2026-03-05 11:23:32 +09:00
< div class = " pr-6 border-r border-gray-200 mr-6 " >
< span class = " text-xs text-gray-500 " > 양식 </ span >
< div class = " mt-1 text-sm font-medium " > {{ $approval -> form ? -> name ? ? '-' }} </ div >
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
</ div >
2026-03-05 11:23:32 +09:00
< div class = " pr-6 border-r border-gray-200 mr-6 " >
< span class = " text-xs text-gray-500 " > 기안자 </ span >
< div class = " mt-1 text-sm font-medium " > {{ $approval -> drafter ? -> name ? ? '-' }} </ div >
2026-02-27 23:41:49 +09:00
</ div >
2026-03-05 11:23:32 +09:00
< div class = " pr-6 { { $approval->completed_at || $approval->parent_doc_id ? ' border-r border-gray-200 mr-6' : '' }} " >
< span class = " text-xs text-gray-500 " > 기안일 </ span >
< div class = " mt-1 text-sm " > {{ $approval -> drafted_at ? -> format ( 'Y-m-d H:i' ) ? ? '-' }} </ div >
</ div >
@ if ( $approval -> completed_at )
< div class = " pr-6 { { $approval->parent_doc_id ? ' border-r border-gray-200 mr-6' : '' }} " >
< span class = " text-xs text-gray-500 " > 완료일 </ span >
< div class = " mt-1 text-sm " > {{ $approval -> completed_at -> format ( 'Y-m-d H:i' ) }} </ div >
</ div >
@ endif
@ if ( $approval -> parent_doc_id )
< div class = " pr-6 " >
< span class = " text-xs text-gray-500 " > 원본 문서 </ span >
< div class = " mt-1 text-sm " >
< a href = " { { route('approvals.show', $approval->parent_doc_id ) }} " class = " text-blue-600 hover:underline " >
{{ $approval -> parentDocument ? -> document_number ? ? '원본 보기' }}
</ a >
</ div >
</ div >
@ endif
</ div >
{{ -- 결재서명란 -- }}
< div class = " shrink-0 " >
@ include ( 'approvals.partials._approval-stamp-table' , [ 'approval' => $approval ])
</ div >
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
</ div >
2026-02-27 23:41:49 +09:00
{{ -- 회수 사유 표시 -- }}
@ if ( $approval -> status === 'cancelled' && $approval -> recall_reason )
< div class = " mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg " >
< span class = " text-xs font-medium text-yellow-700 " > 회수 사유 </ span >
< p class = " text-sm text-yellow-800 mt-1 " > {{ $approval -> recall_reason }} </ p >
</ div >
@ endif
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
< div class = " border-t pt-4 " >
< h2 class = " text-lg font-semibold text-gray-800 mb-2 " > {{ $approval -> title }} </ h2 >
2026-03-04 15:14:18 +09:00
@ if ( ! empty ( $approval -> content ) && $approval -> form ? -> code === 'expense' )
@ include ( 'approvals.partials._expense-show' , [ 'content' => $approval -> content ])
2026-03-05 18:53:42 +09:00
@ elseif ( ! empty ( $approval -> content ) && $approval -> form ? -> code === 'employment_cert' )
@ include ( 'approvals.partials._certificate-show' , [ 'content' => $approval -> content ])
2026-03-05 23:41:20 +09:00
@ elseif ( ! empty ( $approval -> content ) && $approval -> form ? -> code === 'career_cert' )
@ include ( 'approvals.partials._career-cert-show' , [ 'content' => $approval -> content ])
2026-03-05 23:57:42 +09:00
@ elseif ( ! empty ( $approval -> content ) && $approval -> form ? -> code === 'appointment_cert' )
@ include ( 'approvals.partials._appointment-cert-show' , [ 'content' => $approval -> content ])
2026-03-06 00:13:17 +09:00
@ elseif ( ! empty ( $approval -> content ) && $approval -> form ? -> code === 'resignation' )
@ include ( 'approvals.partials._resignation-show' , [ 'content' => $approval -> content ])
2026-03-04 15:14:18 +09:00
@ elseif ( $approval -> body && preg_match ( '/<[a-z][\s\S]*>/i' , $approval -> body ))
2026-02-28 14:18:16 +09:00
< div class = " prose prose-sm max-w-none text-gray-700 " >
{ !! strip_tags ( $approval -> body , '<p><br><strong><b><em><i><u><s><del><h1><h2><h3><h4><h5><h6><ul><ol><li><blockquote><pre><code><a><span><div><table><thead><tbody><tr><th><td>' ) !! }
</ div >
@ else
< div class = " prose prose-sm max-w-none text-gray-700 whitespace-pre-wrap " > {{ $approval -> body ? ? '(내용 없음)' }} </ div >
@ endif
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
</ div >
</ div >
2026-03-05 13:50:45 +09:00
{{ -- 반려 이력 -- }}
@ if ( ! empty ( $approval -> rejection_history ))
< div class = " bg-white rounded-lg shadow-sm p-6 mb-6 " >
< h3 class = " text-lg font-semibold text-gray-800 mb-3 flex items-center gap-2 " >
반려 이력
< span class = " inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700 " > {{ count ( $approval -> rejection_history ) }} 회 </ span >
</ h3 >
< div class = " space-y-3 " >
@ foreach ( $approval -> rejection_history as $history )
< div class = " border-l-3 border-red-300 pl-4 py-2 " style = " border-left: 3px solid #fca5a5; " >
< div class = " flex items-center gap-2 text-sm " >
< span class = " inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-50 text-red-600 " > {{ $history [ 'round' ] ? ? '-' }} 차 반려 </ span >
< span class = " font-medium text-gray-800 " > {{ $history [ 'approver_name' ] ? ? '' }} </ span >
@ if ( ! empty ( $history [ 'approver_position' ]))
< span class = " text-gray-400 text-xs " > {{ $history [ 'approver_position' ] }} </ span >
@ endif
< span class = " text-gray-400 text-xs " > {{ $history [ 'rejected_at' ] ? ? '' }} </ span >
</ div >
< p class = " text-sm text-gray-700 mt-1 " > {{ $history [ 'comment' ] ? ? '' }} </ p >
</ div >
@ endforeach
</ div >
</ div >
@ endif
2026-03-05 11:23:32 +09:00
{{ -- 결재 의견 -- }}
@ php
$stepsWithComments = $approval -> steps -> filter ( fn ( $s ) => $s -> comment );
@ endphp
@ if ( $stepsWithComments -> isNotEmpty ())
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
< div class = " bg-white rounded-lg shadow-sm p-6 mb-6 " >
2026-03-05 11:23:32 +09:00
< h3 class = " text-lg font-semibold text-gray-800 mb-2 " > 결재 의견 </ h3 >
< div class = " space-y-2 " >
@ foreach ( $stepsWithComments as $step )
< div class = " flex gap-3 p-3 bg-gray-50 rounded-lg " >
< div class = " shrink-0 " >
@ if ( $step -> status === 'approved' )
@ if (( $step -> approval_type ? ? 'normal' ) === 'pre_decided' )
< span class = " inline-flex items-center justify-center w-6 h-6 rounded-full bg-indigo-100 text-indigo-600 text-xs " >& #9889;</span>
@ else
< span class = " inline-flex items-center justify-center w-6 h-6 rounded-full bg-green-100 text-green-600 text-xs " >& #10003;</span>
@ endif
@ elseif ( $step -> status === 'on_hold' )
< span class = " inline-flex items-center justify-center w-6 h-6 rounded-full bg-amber-100 text-amber-600 text-xs " >& #9208;</span>
@ else
< span class = " inline-flex items-center justify-center w-6 h-6 rounded-full bg-red-100 text-red-600 text-xs " >& #10007;</span>
@ endif
</ div >
< div >
< div class = " text-sm font-medium " >
{{ $step -> approver_name ? ? ( $step -> approver ? -> name ? ? '' ) }}
@ if (( $step -> approval_type ? ? 'normal' ) === 'pre_decided' )
< span class = " text-xs text-indigo-500 font-normal " > ( 전결 ) </ span >
@ endif
@ if ( $step -> status === 'on_hold' )
< span class = " text-xs text-amber-500 font-normal " > ( 보류 ) </ span >
@ endif
< span class = " text-gray-400 font-normal text-xs " > {{ $step -> acted_at ? -> format ( 'Y-m-d H:i' ) }} </ span >
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
</ div >
2026-03-05 11:23:32 +09:00
< p class = " text-sm text-gray-600 mt-1 " > {{ $step -> comment }} </ p >
</ div >
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
</ div >
2026-03-05 11:23:32 +09:00
@ endforeach
</ div >
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
</ div >
2026-03-05 11:23:32 +09:00
@ endif
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
2026-02-27 23:41:49 +09:00
{{ -- 결재 처리 ( 승인 / 반려 / 보류 / 전결 ) -- }}
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
@ if ( $approval -> isActionable () && $approval -> isCurrentApprover ( auth () -> id ()))
< div class = " bg-white rounded-lg shadow-sm p-6 mb-6 " >
< h3 class = " text-lg font-semibold text-gray-800 mb-4 " > 결재 처리 </ h3 >
< div class = " mb-4 " >
< label class = " block text-sm font-medium text-gray-700 mb-1 " > 결재 의견 </ label >
2026-02-27 23:41:49 +09:00
< textarea id = " approval-comment " rows = " 3 " placeholder = " 의견을 입력하세요 (반려/보류 시 필수) "
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
class = " w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 " ></ textarea >
</ div >
2026-02-27 23:41:49 +09:00
< div class = " flex flex-wrap gap-2 " >
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
< button onclick = " processApproval('approve') "
class = " bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium " >
승인
</ button >
< button onclick = " processApproval('reject') "
class = " bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium " >
반려
</ button >
2026-02-27 23:41:49 +09:00
< button onclick = " processHold() "
class = " bg-amber-500 hover:bg-amber-600 text-white px-6 py-2 rounded-lg transition text-sm font-medium " >
보류
</ button >
< button onclick = " processPreDecide() "
class = " bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium " >
전결
</ button >
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
</ div >
</ div >
@ endif
2026-02-27 23:41:49 +09:00
{{ -- 보류 해제 ( 보류 상태에서 보류한 결재자만 ) -- }}
@ if ( $approval -> isHoldReleasable ())
@ php
$holdStep = $approval -> steps -> firstWhere ( 'status' , 'on_hold' );
$canRelease = $holdStep && $holdStep -> approver_id === auth () -> id ();
@ endphp
@ if ( $canRelease )
< div class = " bg-white rounded-lg shadow-sm p-6 mb-6 " >
< div class = " flex items-center gap-3 " >
< button onclick = " releaseHold() "
class = " bg-amber-500 hover:bg-amber-600 text-white px-6 py-2 rounded-lg transition text-sm font-medium " >
보류 해제
</ button >
< span class = " text-xs text-gray-500 " > 보류를 해제하고 결재를 다시 진행합니다 .</ span >
</ div >
</ div >
@ endif
@ endif
{{ -- 회수 ( 기안자 + pending / on_hold ) -- }}
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
@ if ( $approval -> isCancellable () && $approval -> drafter_id === auth () -> id ())
2026-02-27 23:41:49 +09:00
< div class = " bg-white rounded-lg shadow-sm p-6 mb-6 " >
@ php
$firstStep = $approval -> steps -> whereIn ( 'step_type' , [ 'approval' , 'agreement' ]) -> sortBy ( 'step_order' ) -> first ();
$canCancel = $firstStep && in_array ( $firstStep -> status , [ 'pending' , 'on_hold' ]);
@ endphp
@ if ( $canCancel )
< div class = " mb-3 " >
< label class = " block text-sm font-medium text-gray-700 mb-1 " > 회수 사유 </ label >
< textarea id = " recall-reason " rows = " 2 " placeholder = " 회수 사유를 입력하세요 "
class = " w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-yellow-500 " ></ textarea >
</ div >
< button onclick = " cancelApproval() "
class = " bg-yellow-500 hover:bg-yellow-600 text-white px-6 py-2 rounded-lg transition text-sm font-medium " >
결재 회수
</ button >
< span class = " text-xs text-gray-500 ml-2 " > 진행 중인 결재를 취소합니다 .</ span >
@ else
< p class = " text-sm text-gray-500 " > 첫 번째 결재자가 이미 처리하여 회수할 수 없습니다 .</ p >
@ endif
</ div >
@ endif
{{ -- 복사 재기안 ( 완료 / 반려 / 회수 상태에서 기안자만 ) -- }}
@ if ( $approval -> isCopyable () && $approval -> drafter_id === auth () -> id ())
2026-03-03 07:35:59 +09:00
< div class = " bg-white rounded-lg shadow-sm p-6 mb-6 " >
2026-02-27 23:41:49 +09:00
< button onclick = " copyForRedraft() "
class = " bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium " >
복사하여 재기안
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
</ button >
2026-02-27 23:41:49 +09:00
< span class = " text-xs text-gray-500 ml-2 " > 이 문서를 복사하여 새 결재를 작성합니다 .</ span >
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
</ div >
@ endif
2026-03-03 07:35:59 +09:00
{{ -- 삭제 ( 기안자 : draft만 / 관리자 : 모든 상태 ) -- }}
@ if ( $approval -> isDeletableBy ( auth () -> user ()))
< div class = " bg-white rounded-lg shadow-sm p-6 " >
< div class = " flex items-center gap-3 " >
< button onclick = " deleteApproval() "
class = " bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium " >
문서 삭제
</ button >
< span class = " text-xs text-gray-500 " > 이 결재 문서를 삭제합니다 . 삭제 후 복구할 수 없습니다 .</ span >
</ div >
</ div >
@ endif
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
@ endsection
@ push ( 'scripts' )
< script >
2026-03-05 11:36:58 +09:00
{{ -- 기안자가 완료 문서 열람 시 읽음 처리 -- }}
@ if ( $approval -> drafter_id === auth () -> id () && in_array ( $approval -> status , [ 'approved' , 'rejected' ]) && ! $approval -> drafter_read_at )
fetch ( '/api/admin/approvals/{{ $approval->id }}/mark-read-single' , {
method : 'POST' ,
headers : { 'Accept' : 'application/json' , 'X-CSRF-TOKEN' : '{{ csrf_token() }}' }
}) . catch (() => {});
@ endif
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
async function processApproval ( action ) {
const comment = document . getElementById ( 'approval-comment' ) ? . value || '' ;
if ( action === 'reject' && ! comment . trim ()) {
showToast ( '반려 시 사유를 입력해주세요.' , 'warning' );
return ;
}
if ( action === 'approve' && ! confirm ( '승인하시겠습니까?' )) return ;
if ( action === 'reject' && ! confirm ( '반려하시겠습니까?' )) return ;
try {
const response = await fetch ( `/api/admin/approvals/{{ $approval->id }}/${action}` , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'X-CSRF-TOKEN' : '{{ csrf_token() }}' ,
'Accept' : 'application/json' ,
},
body : JSON . stringify ({ comment : comment }),
});
const data = await response . json ();
if ( data . success ) {
showToast ( data . message , 'success' );
setTimeout (() => location . reload (), 500 );
} else {
showToast ( data . message || '처리에 실패했습니다.' , 'error' );
}
} catch ( e ) {
showToast ( '서버 오류가 발생했습니다.' , 'error' );
}
}
2026-02-27 23:41:49 +09:00
async function processHold () {
const comment = document . getElementById ( 'approval-comment' ) ? . value || '' ;
if ( ! comment . trim ()) {
showToast ( '보류 사유를 입력해주세요.' , 'warning' );
return ;
}
if ( ! confirm ( '이 결재를 보류하시겠습니까?' )) return ;
try {
const response = await fetch ( '/api/admin/approvals/{{ $approval->id }}/hold' , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'X-CSRF-TOKEN' : '{{ csrf_token() }}' ,
'Accept' : 'application/json' ,
},
body : JSON . stringify ({ comment : comment }),
});
const data = await response . json ();
if ( data . success ) {
showToast ( data . message , 'success' );
setTimeout (() => location . reload (), 500 );
} else {
showToast ( data . message || '보류에 실패했습니다.' , 'error' );
}
} catch ( e ) {
showToast ( '서버 오류가 발생했습니다.' , 'error' );
}
}
async function releaseHold () {
if ( ! confirm ( '보류를 해제하시겠습니까?' )) return ;
try {
const response = await fetch ( '/api/admin/approvals/{{ $approval->id }}/release-hold' , {
method : 'POST' ,
headers : {
'X-CSRF-TOKEN' : '{{ csrf_token() }}' ,
'Accept' : 'application/json' ,
},
});
const data = await response . json ();
if ( data . success ) {
showToast ( data . message , 'success' );
setTimeout (() => location . reload (), 500 );
} else {
showToast ( data . message || '보류 해제에 실패했습니다.' , 'error' );
}
} catch ( e ) {
showToast ( '서버 오류가 발생했습니다.' , 'error' );
}
}
async function processPreDecide () {
const comment = document . getElementById ( 'approval-comment' ) ? . value || '' ;
if ( ! confirm ( '전결 처리하시겠습니까?\n이후 모든 결재를 건너뛰고 문서를 최종 승인합니다.' )) return ;
try {
const response = await fetch ( '/api/admin/approvals/{{ $approval->id }}/pre-decide' , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'X-CSRF-TOKEN' : '{{ csrf_token() }}' ,
'Accept' : 'application/json' ,
},
body : JSON . stringify ({ comment : comment }),
});
const data = await response . json ();
if ( data . success ) {
showToast ( data . message , 'success' );
setTimeout (() => location . reload (), 500 );
} else {
showToast ( data . message || '전결에 실패했습니다.' , 'error' );
}
} catch ( e ) {
showToast ( '서버 오류가 발생했습니다.' , 'error' );
}
}
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
async function cancelApproval () {
2026-02-27 23:41:49 +09:00
const recallReason = document . getElementById ( 'recall-reason' ) ? . value || '' ;
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
if ( ! confirm ( '결재를 회수하시겠습니까? 이 작업은 되돌릴 수 없습니다.' )) return ;
try {
const response = await fetch ( '/api/admin/approvals/{{ $approval->id }}/cancel' , {
method : 'POST' ,
headers : {
2026-02-27 23:41:49 +09:00
'Content-Type' : 'application/json' ,
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
'X-CSRF-TOKEN' : '{{ csrf_token() }}' ,
'Accept' : 'application/json' ,
},
2026-02-27 23:41:49 +09:00
body : JSON . stringify ({ recall_reason : recallReason }),
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
});
const data = await response . json ();
if ( data . success ) {
showToast ( data . message , 'success' );
setTimeout (() => location . reload (), 500 );
} else {
showToast ( data . message || '회수에 실패했습니다.' , 'error' );
}
} catch ( e ) {
showToast ( '서버 오류가 발생했습니다.' , 'error' );
}
}
2026-02-27 23:41:49 +09:00
async function copyForRedraft () {
if ( ! confirm ( '이 문서를 복사하여 새 결재를 작성하시겠습니까?' )) return ;
try {
const response = await fetch ( '/api/admin/approvals/{{ $approval->id }}/copy' , {
method : 'POST' ,
headers : {
'X-CSRF-TOKEN' : '{{ csrf_token() }}' ,
'Accept' : 'application/json' ,
},
});
const data = await response . json ();
if ( data . success ) {
showToast ( data . message , 'success' );
setTimeout (() => {
location . href = '/approval-mgmt/' + data . data . id + '/edit' ;
}, 500 );
} else {
showToast ( data . message || '복사에 실패했습니다.' , 'error' );
}
} catch ( e ) {
showToast ( '서버 오류가 발생했습니다.' , 'error' );
}
}
2026-03-03 07:35:59 +09:00
async function deleteApproval () {
const status = '{{ $approval->status }}' ;
const isActive = [ 'pending' , 'on_hold' ] . includes ( status );
if ( isActive ) {
if ( ! confirm ( '이 문서는 현재 진행 중입니다.\n삭제하면 연관된 휴가 등의 처리도 취소됩니다.\n정말 삭제하시겠습니까?' )) return ;
}
if ( ! confirm ( '결재 문서를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.' )) return ;
try {
const response = await fetch ( '/api/admin/approvals/{{ $approval->id }}' , {
method : 'DELETE' ,
headers : {
'X-CSRF-TOKEN' : '{{ csrf_token() }}' ,
'Accept' : 'application/json' ,
},
});
const data = await response . json ();
if ( data . success ) {
showToast ( data . message , 'success' );
setTimeout (() => {
location . href = '{{ route("approvals.drafts") }}' ;
}, 500 );
} else {
showToast ( data . message || '삭제에 실패했습니다.' , 'error' );
}
} catch ( e ) {
showToast ( '서버 오류가 발생했습니다.' , 'error' );
}
}
2026-03-05 19:13:54 +09:00
// =========================================================================
// 재직증명서 미리보기 (show 페이지용)
// =========================================================================
2026-03-06 00:13:17 +09:00
@ if ( ! empty ( $approval -> content ) && $approval -> form ? -> code === 'resignation' )
const _resignationContent = @ json ( $approval -> content );
function openResignationShowPreview () {
const c = _resignationContent ;
const issueDate = c . issue_date || '' ;
const issueDateFormatted = issueDate ? issueDate . replace ( /-/ g , function ( m , i ) { return i === 4 ? '년 ' : '월 ' ; }) + '일' : '-' ;
const el = document . getElementById ( 'resignation-show-preview-content' );
el . innerHTML = _buildResignationHtml ({
department : c . department || '-' ,
position : c . position || '-' ,
name : c . name || '-' ,
resident : c . resident_number || '-' ,
hireDate : c . hire_date || '-' ,
resignDate : c . resign_date || '-' ,
address : c . address || '-' ,
reason : c . reason || '-' ,
issueDateFormatted : issueDateFormatted ,
company : c . company_name || '-' ,
ceoName : c . ceo_name || '-' ,
});
document . getElementById ( 'resignation-show-preview-modal' ) . style . display = '' ;
document . body . style . overflow = 'hidden' ;
}
function closeResignationShowPreview () {
document . getElementById ( 'resignation-show-preview-modal' ) . style . display = 'none' ;
document . body . style . overflow = '' ;
}
function printResignationShowPreview () {
const content = document . getElementById ( 'resignation-show-preview-content' ) . innerHTML ;
const win = window . open ( '' , '_blank' , 'width=800,height=1000' );
win . document . write ( '<html><head><title>사직서</title>' );
win . document . write ( '<style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:48px 56px;margin:0;} table{border-collapse:collapse;width:100%;} th,td{border:1px solid #333;padding:10px 14px;font-size:14px;} th{background:#f8f9fa;font-weight:600;text-align:left;white-space:nowrap;width:120px;} @media print{body{padding:40px 48px;}}</style>' );
win . document . write ( '</head><body>' );
win . document . write ( content );
win . document . write ( '</body></html>' );
win . document . close ();
win . onload = function () { win . print (); };
}
function _buildResignationHtml ( d ) {
const e = ( s ) => { const div = document . createElement ( 'div' ); div . textContent = s ; return div . innerHTML ; };
const thStyle = 'border:1px solid #333; padding:10px 14px; background:#f8f9fa; font-weight:600; text-align:left; white-space:nowrap; width:120px; font-size:14px;' ;
const tdStyle = 'border:1px solid #333; padding:10px 14px; font-size:14px;' ;
return `
< h1 style = " text-align:center; font-size:28px; font-weight:700; letter-spacing:12px; margin-bottom:40px; " > 사 직 서 </ h1 >
< table style = " border-collapse:collapse; width:100%; margin-bottom:20px; " >
< tr >< th style = " ${ thStyle } " > 소 속 </ th >< td style = " ${ tdStyle}">${e(d.department) } </td><th style= " $ { thStyle } " >직 위</th><td style= " $ { tdStyle } " > ${ e(d.position) } </td></tr>
< tr >< th style = " ${ thStyle } " > 성 명 </ th >< td style = " ${ tdStyle}">${e(d.name) } </td><th style= " $ { thStyle } " >주민등록번호</th><td style= " $ { tdStyle } " > ${ e(d.resident) } </td></tr>
< tr >< th style = " ${ thStyle } " > 입사일 </ th >< td style = " ${ tdStyle}">${e(d.hireDate) } </td><th style= " $ { thStyle } " >퇴사(예정)일</th><td style= " $ { tdStyle } " > ${ e(d.resignDate) } </td></tr>
< tr >< th style = " ${ thStyle } " > 주 소 </ th >< td style = " ${ tdStyle } " colspan = " 3 " > $ { e ( d . address )} </ td ></ tr >
< tr >< th style = " ${ thStyle } " > 사 유 </ th >< td style = " ${ tdStyle } " colspan = " 3 " > $ { e ( d . reason )} </ td ></ tr >
</ table >
< p style = " text-align:center; font-size:15px; line-height:1.8; margin:36px 0; " > 상기 본인은 위 사유로 인하여 사직하고자 < br > 이에 사직서를 제출하오니 허가하여 주시기 바랍니다 .</ p >
< p style = " text-align:center; font-size:15px; font-weight:500; margin-bottom:24px; " > $ { e ( d . issueDateFormatted )} </ p >
< p style = " text-align:center; font-size:14px; margin-bottom:48px; " > 신청인 & nbsp ; & nbsp ; $ { e ( d . name )} & nbsp ; & nbsp ; ( 인 ) </ p >
< div style = " text-align:center; margin-top:32px; " >
< p style = " font-size:16px; font-weight:600; " > $ { e ( d . company )} & nbsp ; & nbsp ; 대표이사 귀하 </ p >
</ div >
` ;
}
@ endif
2026-03-05 23:57:42 +09:00
@ if ( ! empty ( $approval -> content ) && $approval -> form ? -> code === 'appointment_cert' )
const _appointmentCertContent = @ json ( $approval -> content );
function openAppointmentCertShowPreview () {
const c = _appointmentCertContent ;
const issueDate = c . issue_date || '' ;
const issueDateFormatted = issueDate ? issueDate . replace ( /-/ g , function ( m , i ) { return i === 4 ? '년 ' : '월 ' ; }) + '일' : '-' ;
const el = document . getElementById ( 'appointment-cert-show-preview-content' );
el . innerHTML = _buildAppointmentCertHtml ({
name : c . name || '-' ,
resident : c . resident_number || '-' ,
department : c . department || '-' ,
phone : c . phone || '-' ,
hireDate : c . hire_date || '-' ,
resignDate : c . resign_date || '현재' ,
contractType : c . contract_type || '-' ,
purpose : c . purpose || '-' ,
issueDateFormatted : issueDateFormatted ,
company : c . company_name || '-' ,
ceoName : c . ceo_name || '-' ,
});
document . getElementById ( 'appointment-cert-show-preview-modal' ) . style . display = '' ;
document . body . style . overflow = 'hidden' ;
}
function closeAppointmentCertShowPreview () {
document . getElementById ( 'appointment-cert-show-preview-modal' ) . style . display = 'none' ;
document . body . style . overflow = '' ;
}
function printAppointmentCertShowPreview () {
const content = document . getElementById ( 'appointment-cert-show-preview-content' ) . innerHTML ;
const win = window . open ( '' , '_blank' , 'width=800,height=1000' );
win . document . write ( '<html><head><title>위촉증명서</title>' );
win . document . write ( '<style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:48px 56px;margin:0;} table{border-collapse:collapse;width:100%;} th,td{border:1px solid #333;padding:10px 14px;font-size:14px;} th{background:#f8f9fa;font-weight:600;text-align:left;white-space:nowrap;width:120px;} @media print{body{padding:40px 48px;}}</style>' );
win . document . write ( '</head><body>' );
win . document . write ( content );
win . document . write ( '</body></html>' );
win . document . close ();
win . onload = function () { win . print (); };
}
function _buildAppointmentCertHtml ( d ) {
const e = ( s ) => { const div = document . createElement ( 'div' ); div . textContent = s ; return div . innerHTML ; };
const thStyle = 'border:1px solid #333; padding:10px 14px; background:#f8f9fa; font-weight:600; text-align:left; white-space:nowrap; width:120px; font-size:14px;' ;
const tdStyle = 'border:1px solid #333; padding:10px 14px; font-size:14px;' ;
return `
< h1 style = " text-align:center; font-size:28px; font-weight:700; letter-spacing:12px; margin-bottom:40px; " > 위 촉 증 명 서 </ h1 >
< table style = " border-collapse:collapse; width:100%; margin-bottom:20px; " >
< tr >< th style = " ${ thStyle } " > 성 명 </ th >< td style = " ${ tdStyle } " colspan = " 3 " > $ { e ( d . name )} </ td ></ tr >
< tr >< th style = " ${ thStyle } " > 주민등록번호 </ th >< td style = " ${ tdStyle } " colspan = " 3 " > $ { e ( d . resident )} </ td ></ tr >
< tr >< th style = " ${ thStyle } " > 소 속 </ th >< td style = " ${ tdStyle}">${e(d.department) } </td><th style= " $ { thStyle } " >연 락 처</th><td style= " $ { tdStyle } " > ${ e(d.phone) } </td></tr>
< tr >< th style = " ${ thStyle } " > 위촉 ( 재직 ) 기간 </ th >< td style = " ${ tdStyle}">${e(d.hireDate) } ~ ${ e(d.resignDate) } </td><th style= " $ { thStyle } " >계약자격</th><td style= " $ { tdStyle } " > ${ e(d.contractType) } </td></tr>
< tr >< th style = " ${ thStyle } " > 용 도 </ th >< td style = " ${ tdStyle } " colspan = " 3 " > $ { e ( d . purpose )} </ td ></ tr >
</ table >
< p style = " text-align:center; font-size:15px; line-height:2; margin:36px 0; " > 위와 같이 위촉하였음을 증명합니다 .</ p >
< p style = " text-align:center; font-size:15px; font-weight:500; margin-bottom:48px; " > $ { e ( d . issueDateFormatted )} </ p >
< div style = " text-align:center; margin-top:32px; " >
< p style = " font-size:16px; font-weight:600; margin-bottom:8px; " > $ { e ( d . company )} </ p >
< p style = " font-size:14px; color:#555; " > 대표이사 & nbsp ; & nbsp ; $ { e ( d . ceoName )} & nbsp ; & nbsp ; ( 인 ) </ p >
</ div >
` ;
}
@ endif
2026-03-05 23:41:20 +09:00
@ if ( ! empty ( $approval -> content ) && $approval -> form ? -> code === 'career_cert' )
const _careerCertContent = @ json ( $approval -> content );
function openCareerCertShowPreview () {
const c = _careerCertContent ;
const issueDate = c . issue_date || '' ;
const issueDateFormatted = issueDate ? issueDate . replace ( /-/ g , function ( m , i ) { return i === 4 ? '년 ' : '월 ' ; }) + '일' : '-' ;
const el = document . getElementById ( 'career-cert-show-preview-content' );
el . innerHTML = _buildCareerCertHtml ({
name : c . name || '-' ,
birthDate : c . birth_date || '-' ,
address : c . address || '-' ,
company : c . company_name || '-' ,
businessNum : c . business_num || '-' ,
ceoName : c . ceo_name || '-' ,
phone : c . phone || '-' ,
companyAddress : c . company_address || '-' ,
department : c . department || '-' ,
position : c . position || '-' ,
hireDate : c . hire_date || '-' ,
resignDate : c . resign_date || '현재' ,
jobDescription : c . job_description || '-' ,
purpose : c . purpose || '-' ,
issueDateFormatted : issueDateFormatted ,
});
document . getElementById ( 'career-cert-show-preview-modal' ) . style . display = '' ;
document . body . style . overflow = 'hidden' ;
}
function closeCareerCertShowPreview () {
document . getElementById ( 'career-cert-show-preview-modal' ) . style . display = 'none' ;
document . body . style . overflow = '' ;
}
function printCareerCertShowPreview () {
const content = document . getElementById ( 'career-cert-show-preview-content' ) . innerHTML ;
const win = window . open ( '' , '_blank' , 'width=800,height=1000' );
win . document . write ( '<html><head><title>경력증명서</title>' );
win . document . write ( '<style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:48px 56px;margin:0;} table{border-collapse:collapse;width:100%;} th,td{border:1px solid #333;padding:10px 14px;font-size:14px;} th{background:#f8f9fa;font-weight:600;text-align:left;white-space:nowrap;width:120px;} @media print{body{padding:40px 48px;}}</style>' );
win . document . write ( '</head><body>' );
win . document . write ( content );
win . document . write ( '</body></html>' );
win . document . close ();
win . onload = function () { win . print (); };
}
function _buildCareerCertHtml ( d ) {
const e = ( s ) => { const div = document . createElement ( 'div' ); div . textContent = s ; return div . innerHTML ; };
const thStyle = 'border:1px solid #333; padding:10px 14px; background:#f8f9fa; font-weight:600; text-align:left; white-space:nowrap; width:120px; font-size:14px;' ;
const tdStyle = 'border:1px solid #333; padding:10px 14px; font-size:14px;' ;
return `
< h1 style = " text-align:center; font-size:28px; font-weight:700; letter-spacing:12px; margin-bottom:40px; " > 경 력 증 명 서 </ h1 >
< h3 style = " font-size:15px; font-weight:600; margin:24px 0 10px; color:#333; " > 1. 인적사항 </ h3 >
< table style = " border-collapse:collapse; width:100%; margin-bottom:20px; " >
< tr >< th style = " ${ thStyle } " > 성 명 </ th >< td style = " ${ tdStyle}">${e(d.name) } </td><th style= " $ { thStyle } " >생년월일</th><td style= " $ { tdStyle } " > ${ e(d.birthDate) } </td></tr>
< tr >< th style = " ${ thStyle } " > 주 소 </ th >< td style = " ${ tdStyle } " colspan = " 3 " > $ { e ( d . address )} </ td ></ tr >
</ table >
< h3 style = " font-size:15px; font-weight:600; margin:24px 0 10px; color:#333; " > 2. 경력사항 </ h3 >
< table style = " border-collapse:collapse; width:100%; margin-bottom:20px; " >
< tr >< th style = " ${ thStyle } " > 회 사 명 </ th >< td style = " ${ tdStyle}">${e(d.company) } </td><th style= " $ { thStyle } " >사업자번호</th><td style= " $ { tdStyle } " > ${ e(d.businessNum) } </td></tr>
< tr >< th style = " ${ thStyle } " > 대 표 자 </ th >< td style = " ${ tdStyle}">${e(d.ceoName) } </td><th style= " $ { thStyle } " >대표전화</th><td style= " $ { tdStyle } " > ${ e(d.phone) } </td></tr>
< tr >< th style = " ${ thStyle } " > 소 재 지 </ th >< td style = " ${ tdStyle } " colspan = " 3 " > $ { e ( d . companyAddress )} </ td ></ tr >
< tr >< th style = " ${ thStyle } " > 소속부서 </ th >< td style = " ${ tdStyle}">${e(d.department) } </td><th style= " $ { thStyle } " >직위/직급</th><td style= " $ { tdStyle } " > ${ e(d.position) } </td></tr>
< tr >< th style = " ${ thStyle } " > 근무기간 </ th >< td style = " ${ tdStyle } " colspan = " 3 " > $ { e ( d . hireDate )} ~ $ { e ( d . resignDate )} </ td ></ tr >
< tr >< th style = " ${ thStyle } " > 담당업무 </ th >< td style = " ${ tdStyle } " colspan = " 3 " > $ { e ( d . jobDescription )} </ td ></ tr >
</ table >
< h3 style = " font-size:15px; font-weight:600; margin:24px 0 10px; color:#333; " > 3. 발급정보 </ h3 >
< table style = " border-collapse:collapse; width:100%; margin-bottom:32px; " >
< tr >< th style = " ${ thStyle } " > 사용용도 </ th >< td style = " ${ tdStyle } " colspan = " 3 " > $ { e ( d . purpose )} </ td ></ tr >
</ table >
< p style = " text-align:center; font-size:15px; line-height:2; margin:36px 0; " > 위 사람은 당사에 재직 ( 근무 ) 하였음을 증명합니다 .</ p >
< p style = " text-align:center; font-size:15px; font-weight:500; margin-bottom:48px; " > $ { e ( d . issueDateFormatted )} </ p >
< div style = " text-align:center; margin-top:32px; " >
< p style = " font-size:16px; font-weight:600; margin-bottom:8px; " > $ { e ( d . company )} </ p >
< p style = " font-size:14px; color:#555; " > 대표이사 & nbsp ; & nbsp ; $ { e ( d . ceoName )} & nbsp ; & nbsp ; ( 인 ) </ p >
</ div >
` ;
}
@ endif
2026-03-05 19:13:54 +09:00
@ if ( ! empty ( $approval -> content ) && $approval -> form ? -> code === 'employment_cert' )
const _certContent = @ json ( $approval -> content );
function openCertShowPreview () {
const c = _certContent ;
const issueDate = c . issue_date || '' ;
const issueDateFormatted = issueDate ? issueDate . replace ( /-/ g , function ( m , i ) { return i === 4 ? '년 ' : '월 ' ; }) + '일' : '-' ;
const el = document . getElementById ( 'cert-show-preview-content' );
el . innerHTML = _buildCertHtml ({
name : c . name || '-' ,
resident : c . resident_number || '-' ,
address : c . address || '-' ,
company : c . company_name || '-' ,
businessNum : c . business_num || '-' ,
department : c . department || '-' ,
position : c . position || '-' ,
hireDate : c . hire_date || '-' ,
purpose : c . purpose || '-' ,
issueDateFormatted : issueDateFormatted ,
2026-03-06 09:19:32 +09:00
ceoName : c . ceo_name || '-' ,
2026-03-05 19:13:54 +09:00
});
document . getElementById ( 'cert-show-preview-modal' ) . style . display = '' ;
document . body . style . overflow = 'hidden' ;
}
function closeCertShowPreview () {
document . getElementById ( 'cert-show-preview-modal' ) . style . display = 'none' ;
document . body . style . overflow = '' ;
}
function printCertShowPreview () {
const content = document . getElementById ( 'cert-show-preview-content' ) . innerHTML ;
const win = window . open ( '' , '_blank' , 'width=800,height=1000' );
win . document . write ( '<html><head><title>재직증명서</title>' );
win . document . write ( '<style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:48px 56px;margin:0;} table{border-collapse:collapse;width:100%;} th,td{border:1px solid #333;padding:10px 14px;font-size:14px;} th{background:#f8f9fa;font-weight:600;text-align:left;white-space:nowrap;width:120px;} @media print{body{padding:40px 48px;}}</style>' );
win . document . write ( '</head><body>' );
win . document . write ( content );
win . document . write ( '</body></html>' );
win . document . close ();
win . onload = function () { win . print (); };
}
function _buildCertHtml ( d ) {
const e = ( s ) => { const div = document . createElement ( 'div' ); div . textContent = s ; return div . innerHTML ; };
const thStyle = 'border:1px solid #333; padding:10px 14px; background:#f8f9fa; font-weight:600; text-align:left; white-space:nowrap; width:120px; font-size:14px;' ;
const tdStyle = 'border:1px solid #333; padding:10px 14px; font-size:14px;' ;
return `
< h1 style = " text-align:center; font-size:28px; font-weight:700; letter-spacing:12px; margin-bottom:40px; " > 재 직 증 명 서 </ h1 >
< h3 style = " font-size:15px; font-weight:600; margin:24px 0 10px; color:#333; " > 1. 인적사항 </ h3 >
< table style = " border-collapse:collapse; width:100%; margin-bottom:20px; " >
< tr >< th style = " ${ thStyle } " > 성 명 </ th >< td style = " ${ tdStyle}">${e(d.name) } </td><th style= " $ { thStyle } " >주민등록번호</th><td style= " $ { tdStyle } " > ${ e(d.resident) } </td></tr>
< tr >< th style = " ${ thStyle } " > 주 소 </ th >< td style = " ${ tdStyle } " colspan = " 3 " > $ { e ( d . address )} </ td ></ tr >
</ table >
< h3 style = " font-size:15px; font-weight:600; margin:24px 0 10px; color:#333; " > 2. 재직사항 </ h3 >
< table style = " border-collapse:collapse; width:100%; margin-bottom:20px; " >
< tr >< th style = " ${ thStyle } " > 회 사 명 </ th >< td style = " ${ tdStyle } " colspan = " 3 " > $ { e ( d . company )} </ td ></ tr >
< tr >< th style = " ${ thStyle } " > 사업자번호 </ th >< td style = " ${ tdStyle } " colspan = " 3 " > $ { e ( d . businessNum )} </ td ></ tr >
< tr >< th style = " ${ thStyle } " > 근무부서 </ th >< td style = " ${ tdStyle}">${e(d.department) } </td><th style= " $ { thStyle } " >직 급</th><td style= " $ { tdStyle } " > ${ e(d.position) } </td></tr>
< tr >< th style = " ${ thStyle } " > 재직기간 </ th >< td style = " ${ tdStyle } " colspan = " 3 " > $ { e ( d . hireDate )} </ td ></ tr >
</ table >
< h3 style = " font-size:15px; font-weight:600; margin:24px 0 10px; color:#333; " > 3. 발급정보 </ h3 >
< table style = " border-collapse:collapse; width:100%; margin-bottom:32px; " >
< tr >< th style = " ${ thStyle } " > 사용용도 </ th >< td style = " ${ tdStyle } " colspan = " 3 " > $ { e ( d . purpose )} </ td ></ tr >
</ table >
< p style = " text-align:center; font-size:15px; line-height:2; margin:36px 0; " > 위 사항을 증명합니다 .</ p >
< p style = " text-align:center; font-size:15px; font-weight:500; margin-bottom:48px; " > $ { e ( d . issueDateFormatted )} </ p >
< div style = " text-align:center; margin-top:32px; " >
< p style = " font-size:16px; font-weight:600; margin-bottom:8px; " > $ { e ( d . company )} </ p >
2026-03-06 09:19:32 +09:00
< p style = " font-size:14px; color:#555; " > 대표이사 & nbsp ; & nbsp ; $ { e ( d . ceoName )} & nbsp ; & nbsp ; ( 인 ) </ p >
2026-03-05 19:13:54 +09:00
</ div >
` ;
}
@ endif
feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:17 +09:00
</ script >
@ endpush