From d7a656a047347c0242b48043ab614152f2b649d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 9 Feb 2026 21:54:30 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EC=9D=8C=EC=84=B1=EC=9E=85=EB=A0=A5=20STT?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=EB=9F=89=20AI=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=B6=94=EC=A0=81=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit logSttUsage 엔드포인트 추가, 녹음 종료 시 duration 전송 AI 토큰 사용량에 '공사현장사진대지-음성입력' 카테고리로 기록 Co-Authored-By: Claude Opus 4.6 --- .../Juil/ConstructionSitePhotoController.php | 12 ++++++++++ .../views/juil/construction-photos.blade.php | 22 ++++++++++++++++++- routes/web.php | 1 + 3 files changed, 34 insertions(+), 1 deletion(-) 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'); }); });