feat: [esign] 전자서명 고도화 — 필기 문구 확인(Handwriting Verification) 전체 구현

- 마이그레이션: esign_verification_templates, esign_handwriting_verifications 테이블
- 모델: EsignVerificationTemplate, EsignHandwritingVerification
- 서비스: HandwritingVerificationService, TextSimilarityService
- HWR 어댑터: NaverClova, GoogleVision, Tesseract (전략 패턴 + 폴백)
- 컨트롤러: EsignVerificationController (대시보드/템플릿/데모/통계 API)
- 뷰: 대시보드, 확인 템플릿 관리, 필기 인식 데모 (React + Canvas)
- 라우트: /esign-verification/* (11개 엔드포인트)
- config/esign.php: HWR 엔진 설정
- 메뉴: 연구개발 > 전자서명 고도화 추가
This commit is contained in:
김보곤
2026-03-22 22:34:56 +09:00
parent d6429599b7
commit 0b4612abfc
7 changed files with 1111 additions and 3 deletions

View File

@@ -0,0 +1,215 @@
<?php
namespace App\Http\Controllers\ESign;
use App\Http\Controllers\Controller;
use App\Models\ESign\EsignHandwritingVerification;
use App\Models\ESign\EsignVerificationTemplate;
use App\Services\ESign\HandwritingVerificationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\Response;
class EsignVerificationController extends Controller
{
private HandwritingVerificationService $service;
public function __construct(HandwritingVerificationService $service)
{
$this->service = $service;
}
// ─── 화면 라우트 ───
public function dashboard(Request $request): View|Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('esign-verification.dashboard'));
}
return view('esign.verification.dashboard');
}
public function templates(Request $request): View|Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('esign-verification.templates'));
}
return view('esign.verification.templates');
}
public function demo(Request $request): View|Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('esign-verification.demo'));
}
return view('esign.verification.demo');
}
// ─── API: 확인 템플릿 CRUD ───
public function indexTemplates(Request $request): JsonResponse
{
$tenantId = Auth::user()->tenant_id;
$templates = EsignVerificationTemplate::where('tenant_id', $tenantId)
->where('is_active', true)
->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',
'category' => 'nullable|string|max:50',
'steps' => 'required|array|min:1',
'steps.*.order' => 'required|integer|min:1',
'steps.*.text' => 'required|string|max:200',
'steps.*.threshold' => 'required|numeric|min:50|max:100',
'pass_threshold' => 'nullable|numeric|min:50|max:100',
'max_attempts' => 'nullable|integer|min:1|max:20',
]);
$tenantId = Auth::user()->tenant_id;
$template = EsignVerificationTemplate::create([
'tenant_id' => $tenantId,
'name' => $request->input('name'),
'category' => $request->input('category'),
'steps' => $request->input('steps'),
'pass_threshold' => $request->input('pass_threshold', 80.00),
'max_attempts' => $request->input('max_attempts', 5),
'is_active' => true,
'created_by' => Auth::id(),
]);
return response()->json(['success' => true, 'data' => $template], 201);
}
public function showTemplate(int $id): JsonResponse
{
$tenantId = Auth::user()->tenant_id;
$template = EsignVerificationTemplate::where('tenant_id', $tenantId)
->findOrFail($id);
return response()->json(['success' => true, 'data' => $template]);
}
public function updateTemplate(Request $request, int $id): JsonResponse
{
$request->validate([
'name' => 'required|string|max:100',
'category' => 'nullable|string|max:50',
'steps' => 'required|array|min:1',
'steps.*.order' => 'required|integer|min:1',
'steps.*.text' => 'required|string|max:200',
'steps.*.threshold' => 'required|numeric|min:50|max:100',
'pass_threshold' => 'nullable|numeric|min:50|max:100',
'max_attempts' => 'nullable|integer|min:1|max:20',
]);
$tenantId = Auth::user()->tenant_id;
$template = EsignVerificationTemplate::where('tenant_id', $tenantId)
->findOrFail($id);
$template->update([
'name' => $request->input('name'),
'category' => $request->input('category'),
'steps' => $request->input('steps'),
'pass_threshold' => $request->input('pass_threshold', 80.00),
'max_attempts' => $request->input('max_attempts', 5),
]);
return response()->json(['success' => true, 'data' => $template]);
}
public function destroyTemplate(int $id): JsonResponse
{
$tenantId = Auth::user()->tenant_id;
$template = EsignVerificationTemplate::where('tenant_id', $tenantId)
->findOrFail($id);
$template->update(['is_active' => false]);
return response()->json(['success' => true, 'message' => '템플릿이 비활성화되었습니다.']);
}
// ─── API: 데모 (인식 테스트) ───
public function demoRecognize(Request $request): JsonResponse
{
$request->validate([
'image' => 'required|string',
'expected_text' => 'required|string|max:200',
]);
$result = $this->service->demo(
$request->input('image'),
$request->input('expected_text'),
);
return response()->json(['success' => true, 'data' => $result]);
}
// ─── API: 검증 이력 ───
public function verificationHistory(Request $request): JsonResponse
{
$tenantId = Auth::user()->tenant_id;
$query = EsignHandwritingVerification::where('tenant_id', $tenantId)
->orderBy('created_at', 'desc');
if ($request->filled('contract_id')) {
$query->where('contract_id', $request->input('contract_id'));
}
$data = $query->limit(100)->get();
return response()->json(['success' => true, 'data' => $data]);
}
// ─── API: 통계 ───
public function stats(): JsonResponse
{
$tenantId = Auth::user()->tenant_id;
$total = EsignHandwritingVerification::where('tenant_id', $tenantId)->count();
$passed = EsignHandwritingVerification::where('tenant_id', $tenantId)->where('is_passed', true)->count();
$failed = $total - $passed;
$avgScore = EsignHandwritingVerification::where('tenant_id', $tenantId)
->whereNotNull('similarity_score')
->avg('similarity_score') ?? 0;
$avgAttempts = EsignHandwritingVerification::where('tenant_id', $tenantId)
->where('is_passed', true)
->avg('attempt_number') ?? 0;
$templateCount = EsignVerificationTemplate::where('tenant_id', $tenantId)
->where('is_active', true)
->count();
return response()->json([
'success' => true,
'data' => [
'total_verifications' => $total,
'passed' => $passed,
'failed' => $failed,
'pass_rate' => $total > 0 ? round(($passed / $total) * 100, 1) : 0,
'avg_score' => round($avgScore, 1),
'avg_attempts' => round($avgAttempts, 1),
'template_count' => $templateCount,
],
]);
}
}