feat:E-Sign 필드 템플릿 저장/불러오기 및 계약 간 복사 기능
- EsignFieldTemplate, EsignFieldTemplateItem 모델 추가 - EsignApiController에 템플릿 CRUD + 적용/복사 메서드 5개 추가 - web.php에 템플릿 라우트 5개 추가 - fields.blade.php에 템플릿 드롭다운 메뉴 + 모달 3개 추가 (SaveTemplate, LoadTemplate, CopyFromContract) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,11 +5,14 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Mail\EsignRequestMail;
|
||||
use App\Models\ESign\EsignContract;
|
||||
use App\Models\ESign\EsignFieldTemplate;
|
||||
use App\Models\ESign\EsignFieldTemplateItem;
|
||||
use App\Models\ESign\EsignSigner;
|
||||
use App\Models\ESign\EsignSignField;
|
||||
use App\Models\ESign\EsignAuditLog;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@@ -350,4 +353,230 @@ public function download(int $id)
|
||||
'Content-Type' => 'application/pdf',
|
||||
]);
|
||||
}
|
||||
|
||||
// ─── 필드 템플릿 관련 메서드 ───
|
||||
|
||||
/**
|
||||
* 템플릿 목록 조회
|
||||
*/
|
||||
public function indexTemplates(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$query = EsignFieldTemplate::forTenant($tenantId)
|
||||
->where('is_active', true)
|
||||
->with('items');
|
||||
|
||||
if ($signerCount = $request->input('signer_count')) {
|
||||
$query->where('signer_count', $signerCount);
|
||||
}
|
||||
|
||||
$templates = $query->orderBy('created_at', 'desc')->get();
|
||||
|
||||
return response()->json(['success' => true, 'data' => $templates]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 저장 (현재 필드를 템플릿으로)
|
||||
*/
|
||||
public function storeTemplate(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:100',
|
||||
'description' => 'nullable|string',
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.signer_order' => 'required|integer|min:1',
|
||||
'items.*.page_number' => 'required|integer|min:1',
|
||||
'items.*.position_x' => 'required|numeric',
|
||||
'items.*.position_y' => 'required|numeric',
|
||||
'items.*.width' => 'required|numeric',
|
||||
'items.*.height' => 'required|numeric',
|
||||
'items.*.field_type' => 'required|in:signature,stamp,text,date,checkbox',
|
||||
'items.*.field_label' => 'nullable|string|max:100',
|
||||
'items.*.is_required' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
|
||||
// items에서 최대 signer_order를 추출하여 signer_count 결정
|
||||
$items = $request->input('items');
|
||||
$signerCount = max(array_column($items, 'signer_order'));
|
||||
|
||||
$template = DB::transaction(function () use ($tenantId, $request, $items, $signerCount) {
|
||||
$template = EsignFieldTemplate::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => $request->input('name'),
|
||||
'description' => $request->input('description'),
|
||||
'signer_count' => $signerCount,
|
||||
'is_active' => true,
|
||||
'created_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
foreach ($items as $i => $item) {
|
||||
EsignFieldTemplateItem::create([
|
||||
'template_id' => $template->id,
|
||||
'signer_order' => $item['signer_order'],
|
||||
'page_number' => $item['page_number'],
|
||||
'position_x' => $item['position_x'],
|
||||
'position_y' => $item['position_y'],
|
||||
'width' => $item['width'],
|
||||
'height' => $item['height'],
|
||||
'field_type' => $item['field_type'],
|
||||
'field_label' => $item['field_label'] ?? null,
|
||||
'is_required' => $item['is_required'] ?? true,
|
||||
'sort_order' => $i,
|
||||
]);
|
||||
}
|
||||
|
||||
return $template;
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '필드 템플릿이 저장되었습니다.',
|
||||
'data' => $template->load('items'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 삭제 (soft: is_active=false)
|
||||
*/
|
||||
public function destroyTemplate(int $id): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$template = EsignFieldTemplate::forTenant($tenantId)->findOrFail($id);
|
||||
|
||||
$template->update(['is_active' => false]);
|
||||
|
||||
return response()->json(['success' => true, 'message' => '템플릿이 삭제되었습니다.']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿을 계약에 적용
|
||||
*/
|
||||
public function applyTemplate(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'template_id' => 'required|integer',
|
||||
]);
|
||||
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$contract = EsignContract::forTenant($tenantId)->with('signers')->findOrFail($id);
|
||||
$template = EsignFieldTemplate::forTenant($tenantId)
|
||||
->where('is_active', true)
|
||||
->with('items')
|
||||
->findOrFail($request->input('template_id'));
|
||||
|
||||
// 서명자 수 확인
|
||||
$contractSignerCount = $contract->signers->count();
|
||||
if ($template->signer_count > $contractSignerCount) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => "템플릿에 필요한 서명자 수({$template->signer_count}명)가 계약의 서명자 수({$contractSignerCount}명)보다 많습니다.",
|
||||
], 422);
|
||||
}
|
||||
|
||||
// signer_order → signer_id 매핑
|
||||
$signerMap = [];
|
||||
foreach ($contract->signers as $signer) {
|
||||
$signerMap[$signer->sign_order] = $signer->id;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($contract, $template, $tenantId, $signerMap) {
|
||||
// 기존 필드 삭제
|
||||
EsignSignField::where('contract_id', $contract->id)->delete();
|
||||
|
||||
// 템플릿 아이템 → 필드 생성
|
||||
foreach ($template->items as $item) {
|
||||
$signerId = $signerMap[$item->signer_order] ?? null;
|
||||
if (!$signerId) continue;
|
||||
|
||||
EsignSignField::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'contract_id' => $contract->id,
|
||||
'signer_id' => $signerId,
|
||||
'page_number' => $item->page_number,
|
||||
'position_x' => $item->position_x,
|
||||
'position_y' => $item->position_y,
|
||||
'width' => $item->width,
|
||||
'height' => $item->height,
|
||||
'field_type' => $item->field_type,
|
||||
'field_label' => $item->field_label,
|
||||
'is_required' => $item->is_required,
|
||||
'sort_order' => $item->sort_order,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
$fields = EsignSignField::where('contract_id', $contract->id)->orderBy('sort_order')->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '템플릿이 적용되었습니다.',
|
||||
'data' => $fields,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 다른 계약에서 필드 복사
|
||||
*/
|
||||
public function copyFieldsFromContract(Request $request, int $id, int $sourceId): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$targetContract = EsignContract::forTenant($tenantId)->with('signers')->findOrFail($id);
|
||||
$sourceContract = EsignContract::forTenant($tenantId)->with(['signers', 'signFields'])->findOrFail($sourceId);
|
||||
|
||||
if ($sourceContract->signFields->isEmpty()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '소스 계약에 복사할 필드가 없습니다.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// 소스 계약 서명자의 sign_order → signer_id 매핑
|
||||
$sourceSignerOrderMap = [];
|
||||
foreach ($sourceContract->signers as $signer) {
|
||||
$sourceSignerOrderMap[$signer->id] = $signer->sign_order;
|
||||
}
|
||||
|
||||
// 대상 계약 sign_order → signer_id 매핑
|
||||
$targetSignerMap = [];
|
||||
foreach ($targetContract->signers as $signer) {
|
||||
$targetSignerMap[$signer->sign_order] = $signer->id;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($targetContract, $sourceContract, $tenantId, $sourceSignerOrderMap, $targetSignerMap) {
|
||||
// 기존 필드 삭제
|
||||
EsignSignField::where('contract_id', $targetContract->id)->delete();
|
||||
|
||||
foreach ($sourceContract->signFields as $field) {
|
||||
// 소스 signer_id → sign_order → 대상 signer_id
|
||||
$signOrder = $sourceSignerOrderMap[$field->signer_id] ?? null;
|
||||
$targetSignerId = $signOrder ? ($targetSignerMap[$signOrder] ?? null) : null;
|
||||
if (!$targetSignerId) continue;
|
||||
|
||||
EsignSignField::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'contract_id' => $targetContract->id,
|
||||
'signer_id' => $targetSignerId,
|
||||
'page_number' => $field->page_number,
|
||||
'position_x' => $field->position_x,
|
||||
'position_y' => $field->position_y,
|
||||
'width' => $field->width,
|
||||
'height' => $field->height,
|
||||
'field_type' => $field->field_type,
|
||||
'field_label' => $field->field_label,
|
||||
'is_required' => $field->is_required,
|
||||
'sort_order' => $field->sort_order,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
$fields = EsignSignField::where('contract_id', $targetContract->id)->orderBy('sort_order')->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '필드가 복사되었습니다.',
|
||||
'data' => $fields,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user