docs:음성입력 STT 가이드 v1.1 - Alpine.js 구현 패턴 추가

- 영업 전략 시나리오 / 매니저 상담 프로세스 STT 개선 내용 반영
- Alpine.js vs React 구현 비교표
- Alpine.js startSpeechRecognition() 코드 + 프리뷰 패널 Blade 코드
- 영업 시나리오 추가 기능 (음성 녹음, 파형 시각화, GCS 백업, 재생)
- 데이터 흐름도 (MediaRecorder + STT + 서버 저장)
- onend 자동 재시작 패턴 (긴 녹음 대응)
- 참조 구현 파일 목록 확장

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-10 09:20:49 +09:00
parent 2ed9d07901
commit 397b3ba711

View File

@@ -1,9 +1,9 @@
# 음성 입력(STT) 기술 가이드 # 음성 입력(STT) 기술 가이드
> **문서 버전**: 1.0 > **문서 버전**: 1.1
> **작성일**: 2026-02-10 > **작성일**: 2026-02-10
> **최초 적용**: 공사현장 사진대지 (`/juil/construction-photos`) > **적용 페이지**: 공사현장 사진대지, 영업 전략 시나리오, 매니저 상담 프로세스
> **대상 프로젝트**: MNG (React 18 + Babel in-browser) > **대상 프로젝트**: MNG (React 18 + Alpine.js)
--- ---
@@ -637,9 +637,222 @@ const toggle = useCallback((e) => {
한 페이지에 여러 VoiceInputButton을 배치할 수 있다. 각 인스턴스는 독립적인 `recognitionRef`를 가지므로 충돌하지 않는다. 단, **동시에 2개 이상 녹음은 불가**하다 (브라우저 마이크 제한). 한 버튼이 녹음 중일 때 다른 버튼을 누르면 기존 녹음이 중단된다 (브라우저 동작). 한 페이지에 여러 VoiceInputButton을 배치할 수 있다. 각 인스턴스는 독립적인 `recognitionRef`를 가지므로 충돌하지 않는다. 단, **동시에 2개 이상 녹음은 불가**하다 (브라우저 마이크 제한). 한 버튼이 녹음 중일 때 다른 버튼을 누르면 기존 녹음이 중단된다 (브라우저 동작).
### 9.7 onend 자동 재시작 (긴 녹음)
`continuous: true`여도 브라우저가 무음 감지 시 자동으로 `onend`를 호출한다. 녹음이 계속 진행 중이라면 `onend`에서 재시작해야 한다.
```javascript
// Alpine.js 패턴
recognition.onend = () => {
if (this.isRecording && this.recognition) {
try { this.recognition.start(); } catch (e) {}
}
};
// React 패턴 (VoiceInputButton)
// onend에서 logUsage + dismiss 타이머 처리
recognition.onend = () => {
if (startTimeRef.current) {
logUsage(startTimeRef.current);
startTimeRef.current = null;
}
setRecording(false);
dismissTimerRef.current = setTimeout(() => setFinalizedSegments([]), 2000);
};
```
영업 시나리오는 `onend`에서 재시작하여 긴 상담도 끊김 없이 인식한다. 반면 공사현장 사진대지는 짧은 입력이므로 재시작하지 않는다.
--- ---
## 10. 향후 확장 가능성 ## 10. Alpine.js 구현 (영업/매니저 시나리오)
영업 전략 시나리오와 매니저 상담 프로세스는 **Alpine.js + Blade** 기반이다. React 없이 동일한 STT 규칙을 적용한다.
### 10.1 적용 파일
| 파일 | 경로 | 용도 |
|------|------|------|
| voice-recorder.blade.php | `resources/views/sales/modals/voice-recorder.blade.php` | 음성 녹음 + STT 컴포넌트 |
| scenario-modal.blade.php | `resources/views/sales/modals/scenario-modal.blade.php` | 시나리오 모달 (voice-recorder 포함) |
| consultation-log.blade.php | `resources/views/sales/modals/consultation-log.blade.php` | 상담 기록 표시/재생 |
### 10.2 React vs Alpine.js 차이점
| 항목 | React (공사현장 사진대지) | Alpine.js (영업 시나리오) |
|------|--------------------------|--------------------------|
| 상태 관리 | `useState`, `useRef` | `x-data` 속성 |
| 확정 텍스트 | `finalizedSegments` state | `finalizedSegments` 배열 |
| 미확정 텍스트 | `interimText` state | `interimTranscript` |
| 자동 스크롤 | `useEffect` + `previewRef` | `$nextTick()` + `$refs` |
| 반복 렌더링 | `{arr.map((seg, i) => <span>)}` | `<template x-for="(seg, i) in arr">` |
| 조건부 표시 | `{condition && <Component />}` | `x-show="condition"` |
| 용도 | input 필드 옆 간단 음성 입력 | 음성 녹음 + 파일 저장 + STT |
### 10.3 핵심 코드 (Alpine.js)
#### x-data 상태 정의
```javascript
x-data="{
// ... 기존 녹음 상태 ...
transcript: '', // 확정 텍스트 합산 (서버 저장용)
interimTranscript: '', // 현재 미확정 텍스트
finalizedSegments: [], // 확정 텍스트 세그먼트 배열 (프리뷰용)
// ...
}"
```
#### startSpeechRecognition()
```javascript
startSpeechRecognition() {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) return;
this.recognition = new SpeechRecognition();
this.recognition.lang = 'ko-KR';
this.recognition.continuous = true;
this.recognition.interimResults = true;
this.recognition.maxAlternatives = 1;
this.transcript = '';
this.interimTranscript = '';
this.finalizedSegments = [];
this.recognition.onresult = (event) => {
let currentInterim = '';
// ★ event.resultIndex부터 순회 (중복 방지)
for (let i = event.resultIndex; i < event.results.length; i++) {
const text = event.results[i][0].transcript;
if (event.results[i].isFinal) {
// ★ 확정: finalizedSegments에 영구 저장
this.finalizedSegments.push(text);
currentInterim = '';
} else {
// 미확정: 교정만 허용
currentInterim = text;
}
}
// transcript 합산 (서버 저장용)
this.transcript = this.finalizedSegments.join(' ');
this.interimTranscript = currentInterim;
// 자동 스크롤
this.$nextTick(() => {
if (this.$refs.transcriptContainer) {
this.$refs.transcriptContainer.scrollTop =
this.$refs.transcriptContainer.scrollHeight;
}
});
};
// 긴 녹음 시 자동 재시작
this.recognition.onend = () => {
if (this.isRecording && this.recognition) {
try { this.recognition.start(); } catch (e) {}
}
};
this.recognition.start();
}
```
### 10.4 프리뷰 패널 UI (Alpine.js Blade)
```blade
{{-- 다크 프리뷰 패널 --}}
<div x-show="finalizedSegments.length > 0 || interimTranscript"
class="bg-gray-900 rounded-lg border border-gray-700 overflow-hidden">
{{-- 헤더: 인식 중/완료 상태 표시 --}}
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-700">
<div class="flex items-center gap-2">
<p class="text-xs font-medium text-gray-400">음성 인식 결과</p>
<template x-if="isRecording">
<span class="flex items-center gap-1 text-xs text-red-400">
<span class="w-1.5 h-1.5 bg-red-400 rounded-full animate-pulse"></span>
인식 중
</span>
</template>
<template x-if="!isRecording && finalizedSegments.length > 0">
<span class="text-green-400 text-xs">&#10003; 완료</span>
</template>
</div>
<p class="text-xs text-gray-500" x-text="transcript.length + ' 자'"></p>
</div>
{{-- 텍스트 영역 --}}
<div class="p-3 max-h-32 overflow-y-auto" x-ref="transcriptContainer"
style="line-height: 1.6;">
{{-- 확정: 흰색 일반체 (삭제 불가) --}}
<template x-for="(seg, i) in finalizedSegments" :key="i">
<span class="text-white text-sm font-normal
transition-colors duration-300" x-text="seg"></span>
</template>
{{-- 미확정: 회색 이탤릭 (교정 가능) --}}
<span x-show="interimTranscript"
class="text-gray-400 text-sm italic
transition-colors duration-200"
x-text="interimTranscript"></span>
{{-- 대기: 녹음 중 + 텍스트 없음 --}}
<span x-show="isRecording && finalizedSegments.length === 0 && !interimTranscript"
class="text-gray-500 text-sm flex items-center gap-1.5">
<span class="w-1.5 h-1.5 bg-red-400 rounded-full animate-pulse"></span>
말씀하세요...
</span>
</div>
</div>
```
### 10.5 영업 시나리오만의 추가 기능
영업/매니저 시나리오의 voice-recorder는 단순 STT 외에 다음 기능을 포함한다:
| 기능 | 설명 | API |
|------|------|-----|
| **음성 파일 녹음** | MediaRecorder로 webm 캡처 | `navigator.mediaDevices.getUserMedia()` |
| **파형 시각화** | Canvas + Web Audio API | `AudioContext.createAnalyser()` |
| **자동 저장** | 녹음 중지 시 서버로 FormData 전송 | `ConsultationController::uploadAudio()` |
| **GCS 백업** | 10MB 이상 파일은 GCS에도 저장 | `GoogleCloudStorageService` |
| **Transcript 저장** | STT 결과를 audio 레코드와 함께 DB 저장 | `sales_consultations.transcript` |
| **재생/다운로드** | 저장된 음성 파일 재생 및 다운로드 | `ConsultationController::downloadAudio()` |
### 10.6 데이터 흐름 (영업 시나리오)
```
사용자 마이크
├──→ MediaRecorder (webm 녹음)
│ └──→ audioBlob
├──→ Web Audio API (파형 시각화)
│ └──→ Canvas 파형 그리기
└──→ SpeechRecognition (STT)
├──→ finalizedSegments[] (확정 세그먼트)
│ └──→ transcript (합산, 서버 저장용)
└──→ interimTranscript (미확정)
└──→ 프리뷰 패널에만 표시
[녹음 중지]
└──→ FormData { audio, transcript, duration }
└──→ POST /sales/consultations/upload-audio
└──→ DB + (GCS if > 10MB)
└──→ HTMX 상담기록 갱신
```
---
## 11. 향후 확장 가능성
| 기능 | 설명 | 난이도 | | 기능 | 설명 | 난이도 |
|------|------|--------| |------|------|--------|
@@ -653,12 +866,22 @@ const toggle = useCallback((e) => {
## 부록 A: 참조 구현 파일 ## 부록 A: 참조 구현 파일
### React 구현 (공사현장 사진대지)
| 파일 | 설명 | | 파일 | 설명 |
|------|------| |------|------|
| `mng/resources/views/juil/construction-photos.blade.php` | 최초 적용 (VoiceInputButton 전체 코드) | | `mng/resources/views/juil/construction-photos.blade.php` | VoiceInputButton 전체 코드 (React) |
| `mng/app/Http/Controllers/Juil/ConstructionSitePhotoController.php` | logSttUsage 엔드포인트 | | `mng/app/Http/Controllers/Juil/ConstructionSitePhotoController.php` | logSttUsage 엔드포인트 |
| `mng/app/Helpers/AiTokenHelper.php` | saveSttUsage 헬퍼 | | `mng/app/Helpers/AiTokenHelper.php` | saveSttUsage / saveGcsStorageUsage 헬퍼 |
| `mng/routes/web.php` | STT 라우트 등록 위치 |
### Alpine.js 구현 (영업/매니저 시나리오)
| 파일 | 설명 |
|------|------|
| `mng/resources/views/sales/modals/voice-recorder.blade.php` | 음성 녹음 + STT (Alpine.js) |
| `mng/resources/views/sales/modals/scenario-modal.blade.php` | 시나리오 모달 (voice-recorder 포함) |
| `mng/resources/views/sales/modals/consultation-log.blade.php` | 상담 기록 재생/표시 |
| `mng/app/Http/Controllers/Sales/ConsultationController.php` | 음성 업로드/다운로드/삭제 |
## 부록 B: CSS 클래스 요약 ## 부록 B: CSS 클래스 요약