feat:음성입력 STT 사용량 AI 토큰 추적 연동

logSttUsage 엔드포인트 추가, 녹음 종료 시 duration 전송
AI 토큰 사용량에 '공사현장사진대지-음성입력' 카테고리로 기록

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-09 21:54:30 +09:00
parent e638d97d65
commit d7a656a047
3 changed files with 34 additions and 1 deletions

View File

@@ -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]);
}
}

View File

@@ -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('');

View File

@@ -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');
});
});