mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 07:56:24 +00:00
STT 테스트 환경 구성 및 유저스토리 업데이트
- docker-compose.test.yml 추가: 테스트용 컨테이너 환경 구성 - STT 테스트 설정 및 컨트롤러 테스트 코드 추가 - application.yml 업데이트 - 테스트 스크립트 추가 - 유저스토리 문서 업데이트 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
694a84e4f5
commit
ad8e0adbd8
@ -49,7 +49,7 @@
|
||||
- 실시간 협업: WebSocket 기반 실시간 동기화, 버전 관리, 충돌 해결
|
||||
- 템플릿 관리: 회의록 템플릿 관리
|
||||
- 통계 생성: 회의 및 Todo 통계
|
||||
3. **STT** - 음성 녹음 관리, 음성-텍스트 변환, 화자 식별 (기본 기능)
|
||||
3. **STT** - 음성 스트리밍 처리, 실시간 음성-텍스트 변환 (기본 기능)
|
||||
4. **AI** - AI 기반 회의록 자동화, Todo 추출, 지능형 검색 (RAG 통합)
|
||||
- LLM 기반 회의록 자동 작성
|
||||
- Todo 자동 추출 및 담당자 식별
|
||||
@ -478,30 +478,29 @@ UFR-MEET-055: [회의록수정] 회의 참석자로서 | 나는, 검증이 완
|
||||
3. STT 서비스 (음성 인식 및 변환 - 기본 기능)
|
||||
1) 음성 인식 및 변환
|
||||
UFR-STT-010: [음성녹음인식] 회의 참석자로서 | 나는, 발언 내용이 자동으로 기록되기 위해 | 음성이 실시간으로 녹음되고 인식되기를 원한다.
|
||||
- 시나리오: 음성 녹음 및 발언 인식
|
||||
회의가 시작된 상황에서 | 참석자가 발언을 시작하면 | 음성이 자동으로 녹음되고 화자가 식별되며 발언이 인식된다.
|
||||
- 시나리오: 음성 실시간 인식
|
||||
회의가 시작된 상황에서 | 참석자가 발언을 시작하면 | 음성이 실시간으로 텍스트로 변환된다.
|
||||
|
||||
[음성 녹음 처리]
|
||||
[음성 스트리밍 처리]
|
||||
- 오디오 스트림 실시간 캡처
|
||||
- 회의 ID와 연결
|
||||
- 음성 데이터 저장 (Azure 스토리지)
|
||||
- **음성 파일은 저장하지 않음** (실시간 스트리밍만 처리)
|
||||
|
||||
[발언 인식 처리]
|
||||
[음성 인식 처리]
|
||||
- AI 음성인식 엔진 연동 (Azure Speech 등)
|
||||
- 화자 자동 식별
|
||||
- 참석자 목록 매칭
|
||||
- 음성 특징 분석
|
||||
- 실시간 텍스트 변환
|
||||
- 타임스탬프 기록
|
||||
- 발언 구간 구분
|
||||
|
||||
[처리 결과]
|
||||
- 음성 녹음이 시작됨 (녹음 ID)
|
||||
- 발언이 인식됨 (발언 ID, 화자, 타임스탬프)
|
||||
- 음성 스트리밍이 시작됨 (세션 ID)
|
||||
- 텍스트가 변환됨 (세그먼트 ID, 텍스트, 타임스탬프)
|
||||
- 실시간으로 텍스트 변환 요청 (UFR-STT-020 연동)
|
||||
- **음성 파일은 저장되지 않고 스트리밍만 처리됨**
|
||||
- **화자 식별 기능 없음** (단순 텍스트 변환만)
|
||||
|
||||
[성능 요구사항]
|
||||
- 발언 인식 지연 시간: 1초 이내
|
||||
- 화자 식별 정확도: 90% 이상
|
||||
- 음성 인식 지연 시간: 1초 이내
|
||||
- 변환 정확도: 85% 이상
|
||||
|
||||
[비고]
|
||||
- STT는 기본 기능으로 경쟁사 대부분이 제공하는 기능임
|
||||
|
||||
46
docker-compose.test.yml
Normal file
46
docker-compose.test.yml
Normal file
@ -0,0 +1,46 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Test Database
|
||||
postgres-test:
|
||||
image: postgres:15-alpine
|
||||
environment:
|
||||
POSTGRES_DB: sttdb_test
|
||||
POSTGRES_USER: testuser
|
||||
POSTGRES_PASSWORD: testpass
|
||||
ports:
|
||||
- "5433:5432"
|
||||
volumes:
|
||||
- postgres_test_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U testuser -d sttdb_test"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Test Redis
|
||||
redis-test:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6380:6379"
|
||||
command: redis-server --requirepass testpass
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-a", "testpass", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
# Test Azure Storage Emulator (Azurite)
|
||||
azurite-test:
|
||||
image: mcr.microsoft.com/azure-storage/azurite
|
||||
ports:
|
||||
- "10000:10000" # Blob service
|
||||
- "10001:10001" # Queue service
|
||||
- "10002:10002" # Table service
|
||||
command: azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0
|
||||
volumes:
|
||||
- azurite_data:/data
|
||||
|
||||
volumes:
|
||||
postgres_test_data:
|
||||
azurite_data:
|
||||
@ -4,9 +4,9 @@ spring:
|
||||
|
||||
# Database Configuration
|
||||
datasource:
|
||||
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:4.230.65.89}:${DB_PORT:5432}/${DB_NAME:sttdb}
|
||||
username: ${DB_USERNAME:hgzerouser}
|
||||
password: ${DB_PASSWORD:Hi5Jessica!}
|
||||
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:sttdb}
|
||||
username: ${DB_USERNAME:stt_user}
|
||||
password: ${DB_PASSWORD:}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
@ -30,9 +30,9 @@ spring:
|
||||
# Redis Configuration
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:20.249.177.114}
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:Hi5Jessica!}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
timeout: 2000ms
|
||||
lettuce:
|
||||
pool:
|
||||
@ -40,7 +40,7 @@ spring:
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
max-wait: -1ms
|
||||
database: ${REDIS_DATABASE:2}
|
||||
database: ${REDIS_DATABASE:3}
|
||||
|
||||
# Server Configuration
|
||||
server:
|
||||
@ -48,9 +48,9 @@ server:
|
||||
|
||||
# JWT Configuration
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600}
|
||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800}
|
||||
secret: ${JWT_SECRET:HGZero_Secret_Key_For_Development_Only}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:1800}
|
||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:86400}
|
||||
|
||||
# CORS Configuration
|
||||
cors:
|
||||
|
||||
@ -0,0 +1,82 @@
|
||||
package com.unicorn.hgzero.stt.config;
|
||||
|
||||
import com.unicorn.hgzero.stt.event.publisher.EventPublisher;
|
||||
import com.unicorn.hgzero.stt.repository.jpa.RecordingRepository;
|
||||
import com.unicorn.hgzero.stt.repository.jpa.SpeakerRepository;
|
||||
import com.unicorn.hgzero.stt.repository.jpa.TranscriptionRepository;
|
||||
import com.unicorn.hgzero.stt.repository.jpa.TranscriptSegmentRepository;
|
||||
import com.unicorn.hgzero.stt.service.RecordingService;
|
||||
import com.unicorn.hgzero.stt.service.SpeakerService;
|
||||
import com.unicorn.hgzero.stt.service.TranscriptionService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import redis.embedded.RedisServer;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* 테스트용 설정 클래스
|
||||
* Embedded Redis 서버와 Mock Bean들을 제공
|
||||
*/
|
||||
@TestConfiguration
|
||||
@EnableJpaRepositories(basePackages = "com.unicorn.hgzero.stt.repository.jpa")
|
||||
@EntityScan(basePackages = "com.unicorn.hgzero.stt.repository.entity")
|
||||
@ComponentScan(basePackages = {"com.unicorn.hgzero.stt.service", "com.unicorn.hgzero.stt.config"})
|
||||
public class TestConfig {
|
||||
|
||||
private RedisServer redisServer;
|
||||
|
||||
@PostConstruct
|
||||
public void startRedis() {
|
||||
try {
|
||||
redisServer = new RedisServer(6370);
|
||||
redisServer.start();
|
||||
} catch (Exception e) {
|
||||
// Redis 서버 시작 실패 시 로그만 남기고 계속 진행
|
||||
System.err.println("Failed to start embedded Redis server: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stopRedis() {
|
||||
if (redisServer != null && redisServer.isActive()) {
|
||||
redisServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public EventPublisher eventPublisher() {
|
||||
return mock(EventPublisher.class);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnMissingBean
|
||||
public RedisConnectionFactory redisConnectionFactory() {
|
||||
// Embedded Redis에 연결하는 실제 ConnectionFactory
|
||||
return new LettuceConnectionFactory("localhost", 6370);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnMissingBean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package com.unicorn.hgzero.stt.config;
|
||||
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import redis.embedded.RedisServer;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
/**
|
||||
* WebMvcTest용 최소한의 설정 클래스
|
||||
* Repository는 MockBean으로 별도 처리
|
||||
*/
|
||||
@TestConfiguration
|
||||
@ComponentScan(basePackages = "com.unicorn.hgzero.stt.config")
|
||||
public class WebMvcTestConfig {
|
||||
|
||||
private RedisServer redisServer;
|
||||
|
||||
@PostConstruct
|
||||
public void startRedis() {
|
||||
try {
|
||||
redisServer = new RedisServer(6371); // 다른 포트 사용
|
||||
redisServer.start();
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to start embedded Redis server: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stopRedis() {
|
||||
if (redisServer != null && redisServer.isActive()) {
|
||||
redisServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public RedisConnectionFactory redisConnectionFactory() {
|
||||
return new LettuceConnectionFactory("localhost", 6371);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
package com.unicorn.hgzero.stt.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.unicorn.hgzero.stt.dto.RecordingDto;
|
||||
import com.unicorn.hgzero.stt.service.RecordingService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* 간단한 녹음 컨트롤러 테스트
|
||||
*/
|
||||
@WebMvcTest(RecordingController.class)
|
||||
@DisplayName("간단한 녹음 컨트롤러 테스트")
|
||||
class SimpleRecordingControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@MockBean
|
||||
private RecordingService recordingService;
|
||||
|
||||
private RecordingDto.PrepareRequest prepareRequest;
|
||||
private RecordingDto.PrepareResponse prepareResponse;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
prepareRequest = RecordingDto.PrepareRequest.builder()
|
||||
.meetingId("MEETING-001")
|
||||
.sessionId("SESSION-001")
|
||||
.language("ko-KR")
|
||||
.build();
|
||||
|
||||
prepareResponse = RecordingDto.PrepareResponse.builder()
|
||||
.recordingId("REC-20250123-001")
|
||||
.sessionId("SESSION-001")
|
||||
.status("READY")
|
||||
.streamUrl("wss://api.example.com/stt/v1/ws/stt/SESSION-001")
|
||||
.storagePath("recordings/MEETING-001/SESSION-001.wav")
|
||||
.estimatedInitTime(1100)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("녹음 준비 API 성공")
|
||||
void prepareRecording_Success() throws Exception {
|
||||
// Given
|
||||
when(recordingService.prepareRecording(any(RecordingDto.PrepareRequest.class)))
|
||||
.thenReturn(prepareResponse);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(post("/api/v1/stt/recordings/prepare")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(prepareRequest)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.recordingId").value("REC-20250123-001"))
|
||||
.andExpect(jsonPath("$.data.sessionId").value("SESSION-001"))
|
||||
.andExpect(jsonPath("$.data.status").value("READY"));
|
||||
|
||||
verify(recordingService).prepareRecording(any(RecordingDto.PrepareRequest.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("녹음 시작 API 성공")
|
||||
void startRecording_Success() throws Exception {
|
||||
// Given
|
||||
String recordingId = "REC-20250123-001";
|
||||
RecordingDto.StartRequest startRequest = RecordingDto.StartRequest.builder()
|
||||
.startedBy("user001")
|
||||
.build();
|
||||
|
||||
RecordingDto.StatusResponse statusResponse = RecordingDto.StatusResponse.builder()
|
||||
.recordingId(recordingId)
|
||||
.status("RECORDING")
|
||||
.startTime(LocalDateTime.now())
|
||||
.duration(0)
|
||||
.build();
|
||||
|
||||
when(recordingService.startRecording(eq(recordingId), any(RecordingDto.StartRequest.class)))
|
||||
.thenReturn(statusResponse);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(post("/api/v1/stt/recordings/{recordingId}/start", recordingId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(startRequest)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.recordingId").value(recordingId))
|
||||
.andExpect(jsonPath("$.data.status").value("RECORDING"));
|
||||
|
||||
verify(recordingService).startRecording(eq(recordingId), any(RecordingDto.StartRequest.class));
|
||||
}
|
||||
}
|
||||
50
test-scripts/run-integration-tests.sh
Executable file
50
test-scripts/run-integration-tests.sh
Executable file
@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
|
||||
# STT 서비스 통합 테스트 실행 스크립트
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 STT 서비스 통합 테스트 시작"
|
||||
|
||||
# 1. Docker 테스트 환경 시작
|
||||
echo "📦 Docker 테스트 환경 시작..."
|
||||
docker-compose -f docker-compose.test.yml up -d
|
||||
|
||||
# 2. 서비스 준비 대기
|
||||
echo "⏳ 서비스 준비 대기 중..."
|
||||
sleep 15
|
||||
|
||||
# 3. 데이터베이스 준비 확인
|
||||
echo "🔍 PostgreSQL 연결 확인..."
|
||||
until docker-compose -f docker-compose.test.yml exec -T postgres-test pg_isready -U testuser -d sttdb_test; do
|
||||
echo "PostgreSQL 준비 중..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# 4. Redis 연결 확인
|
||||
echo "🔍 Redis 연결 확인..."
|
||||
until docker-compose -f docker-compose.test.yml exec -T redis-test redis-cli -a testpass ping | grep PONG; do
|
||||
echo "Redis 준비 중..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# 5. 통합 테스트 실행
|
||||
echo "🧪 통합 테스트 실행..."
|
||||
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-23.0.2.jdk/Contents/Home
|
||||
./gradlew :stt:test -Dspring.profiles.active=integration-test
|
||||
|
||||
# 6. 결과 출력
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ 통합 테스트 성공!"
|
||||
else
|
||||
echo "❌ 통합 테스트 실패!"
|
||||
fi
|
||||
|
||||
# 7. Docker 환경 정리 (선택)
|
||||
read -p "Docker 테스트 환경을 정리하시겠습니까? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "🧹 Docker 테스트 환경 정리 중..."
|
||||
docker-compose -f docker-compose.test.yml down -v
|
||||
echo "✅ 정리 완료"
|
||||
fi
|
||||
Loading…
x
Reference in New Issue
Block a user