diff --git a/app/Http/Controllers/Juil/ConstructionSitePhotoController.php b/app/Http/Controllers/Juil/ConstructionSitePhotoController.php index f48a99b0..5a2a05a8 100644 --- a/app/Http/Controllers/Juil/ConstructionSitePhotoController.php +++ b/app/Http/Controllers/Juil/ConstructionSitePhotoController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Juil; +use App\Helpers\AiTokenHelper; use App\Http\Controllers\Controller; use App\Models\Juil\ConstructionSitePhoto; use App\Services\ConstructionSitePhotoService; @@ -233,4 +234,15 @@ public function downloadPhoto(Request $request, int $id, string $type): Response ->header('Content-Disposition', "{$disposition}; filename=\"{$encodedFilename}\"; filename*=UTF-8''{$encodedFilename}") ->header('Cache-Control', 'private, max-age=3600'); } + + public function logSttUsage(Request $request): JsonResponse + { + $validated = $request->validate([ + 'duration_seconds' => 'required|integer|min:1', + ]); + + AiTokenHelper::saveSttUsage('공사현장사진대지-음성입력', $validated['duration_seconds']); + + return response()->json(['success' => true]); + } } diff --git a/resources/views/juil/construction-photos.blade.php b/resources/views/juil/construction-photos.blade.php index 52ee209d..907c8474 100644 --- a/resources/views/juil/construction-photos.blade.php +++ b/resources/views/juil/construction-photos.blade.php @@ -25,6 +25,7 @@ deletePhoto: (id, type) => `/juil/construction-photos/${id}/photo/${type}`, downloadPhoto: (id, type) => `/juil/construction-photos/${id}/download/${type}`, photoUrl: (id, type) => `/juil/construction-photos/${id}/download/${type}?inline=1`, + logSttUsage: '/juil/construction-photos/log-stt-usage', }; const CSRF_TOKEN = document.querySelector('meta[name="csrf-token"]')?.content || ''; @@ -58,16 +59,30 @@ function VoiceInputButton({ onResult, disabled }) { const [recording, setRecording] = useState(false); const [interim, setInterim] = useState(''); const recognitionRef = useRef(null); + const startTimeRef = useRef(null); const isSupported = typeof window !== 'undefined' && (window.SpeechRecognition || window.webkitSpeechRecognition); + const logUsage = useCallback((startTime) => { + const duration = Math.max(1, Math.round((Date.now() - startTime) / 1000)); + apiFetch(API.logSttUsage, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ duration_seconds: duration }), + }).catch(() => {}); + }, []); + const stopRecording = useCallback(() => { recognitionRef.current?.stop(); recognitionRef.current = null; + if (startTimeRef.current) { + logUsage(startTimeRef.current); + startTimeRef.current = null; + } setRecording(false); setInterim(''); - }, []); + }, [logUsage]); const startRecording = useCallback(() => { const SR = window.SpeechRecognition || window.webkitSpeechRecognition; @@ -96,12 +111,17 @@ function VoiceInputButton({ onResult, disabled }) { }; recognition.onerror = () => stopRecording(); recognition.onend = () => { + if (startTimeRef.current) { + logUsage(startTimeRef.current); + startTimeRef.current = null; + } setRecording(false); setInterim(''); recognitionRef.current = null; }; recognitionRef.current = recognition; + startTimeRef.current = Date.now(); recognition.start(); setRecording(true); setInterim(''); diff --git a/routes/web.php b/routes/web.php index 19314e3b..460444e2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1325,5 +1325,6 @@ Route::delete('/{id}', [ConstructionSitePhotoController::class, 'destroy'])->name('destroy'); Route::delete('/{id}/photo/{type}', [ConstructionSitePhotoController::class, 'deletePhoto'])->name('delete-photo'); Route::get('/{id}/download/{type}', [ConstructionSitePhotoController::class, 'downloadPhoto'])->name('download'); + Route::post('/log-stt-usage', [ConstructionSitePhotoController::class, 'logSttUsage'])->name('log-stt-usage'); }); });