- 마이그레이션: 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 엔진 설정 - 메뉴: 연구개발 > 전자서명 고도화 추가
216 lines
7.2 KiB
PHP
216 lines
7.2 KiB
PHP
<?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,
|
|
],
|
|
]);
|
|
}
|
|
}
|