Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8296952
feat : WebSocket Connect, Disconnect 핸들러 & SpeakingConnecion 모델 구현
hye-inA Jan 22, 2026
fc6f9e6
feat : WebSocket 메시지 처리 handler, service 구현
hye-inA Jan 22, 2026
f39bdf9
feat : WebSocket 연결 정보 Repository 구현
hye-inA Jan 22, 2026
ccaa034
fix : 오타 수정
hye-inA Jan 22, 2026
e87606c
Merge branch 'prod' of https://github.com/Language-Study-Prooject/BE_…
hye-inA Jan 22, 2026
6810f41
refactor : websocket -> rest api 전환
hye-inA Jan 22, 2026
76045eb
feat : speaking handler REST로 교체
hye-inA Jan 22, 2026
2c7094a
feat : speaking 관련 dto 생성
hye-inA Jan 22, 2026
5ad8fd6
refacotor : 기존 service 코드 로직 재사용 및 repository 리펙토링
hye-inA Jan 22, 2026
474108f
Merge pull request #489 from Language-Study-Prooject/feature/472/news…
DDINGJOO Jan 22, 2026
af48d60
Merge branch 'prod' of https://github.com/Language-Study-Prooject/BE_…
hye-inA Jan 22, 2026
f4ab156
feat : prod branch merge
hye-inA Jan 22, 2026
6c4cc89
feat : speaking rest API 람다 함수 추가
hye-inA Jan 22, 2026
efcf597
Merge branch 'develop' into feature/speaking-ai-service
hye-inA Jan 22, 2026
1e776a5
Merge branch 'prod' of https://github.com/Language-Study-Prooject/BE_…
hye-inA Jan 22, 2026
78045ee
Merge branch 'prod' into feature/speaking-ai-service
hye-inA Jan 22, 2026
158dcee
refactor : speaking service 재사용
hye-inA Jan 22, 2026
cb88a63
Merge branch 'feature/speaking-ai-service' of https://github.com/Lang…
hye-inA Jan 22, 2026
2cc5db0
refactor : AI 영어 회화 연습 코드 리팩토링
hye-inA Jan 22, 2026
726efb7
feat : handleChat 메서드 JsonNull 체크 푸가
hye-inA Jan 22, 2026
8aed923
refactor : session_id가 null 체크 추가
hye-inA Jan 23, 2026
07f1526
feat : test 브랜치 동기화
hye-inA Jan 23, 2026
50cf863
feat : template 환경변수 리펙토링
hye-inA Jan 23, 2026
d855f58
Merge branch 'test' of https://github.com/Language-Study-Prooject/BE_…
hye-inA Jan 23, 2026
81188b0
Merge branch 'test' of https://github.com/Language-Study-Prooject/BE_…
hye-inA Jan 23, 2026
686b9ab
Merge branch 'test' of https://github.com/Language-Study-Prooject/BE_…
hye-inA Jan 23, 2026
28f8494
feat : test branch 내용 동기화
hye-inA Jan 23, 2026
b73aa2e
feat : Speaking 관련 template 람다 함수 및 테이블 추가
hye-inA Jan 23, 2026
4f49d49
Merge branch 'test' of https://github.com/Language-Study-Prooject/BE_…
hye-inA Jan 23, 2026
8754b1d
feat : test 브랜치 Merge
hye-inA Jan 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,142 +19,145 @@

/**
* Speaking API 핸들러
* <p>
*
* POST /api/speaking/chat - 대화 (음성 또는 텍스트)
* POST /api/speaking/reset - 대화 초기화
*/
public class SpeakingHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

private static final Logger logger = LoggerFactory.getLogger(SpeakingHandler.class);
private static final Gson gson = new GsonBuilder().create();

private static final Map<String, String> CORS_HEADERS = Map.of(
"Content-Type", "application/json",
"Access-Control-Allow-Origin", "*",
"Access-Control-Allow-Headers", "Content-Type,Authorization",
"Access-Control-Allow-Methods", "POST,OPTIONS"
);

private final SpeakingService speakingService;

public SpeakingHandler() {
this.speakingService = new SpeakingService();
}

@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context context) {
logger.info("Speaking API request received");

// OPTIONS 요청 처리 (CORS preflight)
if ("OPTIONS".equalsIgnoreCase(event.getHttpMethod())) {
return response(200, Map.of("message", "OK"));
}

try {
// JWT 토큰 검증
String authHeader = event.getHeaders().get("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return response(401, Map.of("error", "Authorization header is required"));
}

String token = authHeader.substring(7);
if (!JwtUtil.isValid(token)) {
return response(401, Map.of("error", "Invalid or expired token"));
}

Optional<String> userIdOpt = JwtUtil.extractUserId(token);
if (userIdOpt.isEmpty()) {
return response(401, Map.of("error", "Invalid token"));
}

String userId = userIdOpt.get();
String path = event.getPath();
String body = event.getBody();

logger.info("Processing request: path={}, userId={}", path, userId);

// 라우팅
if (path.endsWith("/chat")) {
return handleChat(userId, body);
} else if (path.endsWith("/reset")) {
return handleReset(userId, body);
} else {
return response(404, Map.of("error", "Not found"));
}

} catch (Exception e) {
logger.error("Error processing request: {}", e.getMessage(), e);
return response(500, Map.of("error", "Internal server error: " + e.getMessage()));
}
}

/**
* 대화 처리 (음성 또는 텍스트)
*/
private APIGatewayProxyResponseEvent handleChat(String userId, String body) {
if (body == null || body.isEmpty()) {
return response(400, Map.of("error", "Request body is required"));
}

JsonObject request = JsonParser.parseString(body).getAsJsonObject();

String sessionId = request.has("sessionId") ? request.get("sessionId").getAsString() : null;
String level = request.has("level") ? request.get("level").getAsString() : "INTERMEDIATE";
String audio = request.has("audio") ? request.get("audio").getAsString() : null;
String text = request.has("text") ? request.get("text").getAsString() : null;

SpeakingResponse result;

if (audio != null && !audio.isEmpty()) {
// 음성 입력 처리
logger.info("Processing voice input");
result = speakingService.processVoiceInput(sessionId, userId, audio, level);
} else if (text != null && !text.trim().isEmpty()) {
// 텍스트 입력 처리
logger.info("Processing text input: {}", text);
result = speakingService.processTextInput(sessionId, userId, text.trim(), level);
} else {
return response(400, Map.of("error", "Either 'audio' or 'text' is required"));
}

return response(200, Map.of(
"sessionId", result.sessionId(),
"userTranscript", result.userTranscript(),
"aiText", result.aiText(),
"aiAudioUrl", result.aiAudioUrl(),
"confidence", result.confidence()
));
}

/**
* 대화 초기화
*/
private APIGatewayProxyResponseEvent handleReset(String userId, String body) {
if (body == null || body.isEmpty()) {
return response(400, Map.of("error", "Request body is required"));
}

JsonObject request = JsonParser.parseString(body).getAsJsonObject();
String sessionId = request.has("sessionId") ? request.get("sessionId").getAsString() : null;

if (sessionId == null || sessionId.isEmpty()) {
return response(400, Map.of("error", "sessionId is required"));
}

speakingService.resetConversation(sessionId);

return response(200, Map.of(
"message", "Conversation reset successfully",
"sessionId", sessionId
));
}

private APIGatewayProxyResponseEvent response(int statusCode, Map<String, Object> body) {
return new APIGatewayProxyResponseEvent()
.withStatusCode(statusCode)
.withHeaders(CORS_HEADERS)
.withBody(gson.toJson(body));
}



private static final Logger logger = LoggerFactory.getLogger(SpeakingHandler.class);
private static final Gson gson = new GsonBuilder().create();

private static final Map<String, String> CORS_HEADERS = Map.of(
"Content-Type", "application/json",
"Access-Control-Allow-Origin", "*",
"Access-Control-Allow-Headers", "Content-Type,Authorization",
"Access-Control-Allow-Methods", "POST,OPTIONS"
);

private final SpeakingService speakingService;

public SpeakingHandler() {
this.speakingService = new SpeakingService();
}

@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context context) {
logger.info("Speaking API request received");

// OPTIONS 요청 처리 (CORS preflight)
if ("OPTIONS".equalsIgnoreCase(event.getHttpMethod())) {
return response(200, Map.of("message", "OK"));
}

try {
// 사용자 인증 정보 추출 (Cognito Authorizer -> requestContext)
if (event.getRequestContext() == null || event.getRequestContext().getAuthorizer() == null) {
logger.error("No Authorizer found in request context");
return response(401, Map.of("error", "Unauthorized: User context missing"));
}

Map<String, Object> authorizer = event.getRequestContext().getAuthorizer();
Map<String, Object> claims = (Map<String, Object>) authorizer.get("claims");

if (claims == null) {
return response(401, Map.of("error", "Unauthorized: Claims missing"));
}

String userId = (String) claims.get("sub"); // Cognito User Pool의 고유 ID (UUID 형태)

// 요청 정보 추출
String path = event.getPath();
String body = event.getBody();

logger.info("Processing request: path={}, userId={}", path, userId);

// 라우팅
if (path != null && path.endsWith("/chat")) {
return handleChat(userId, body);
} else if (path != null && path.endsWith("/reset")) {
return handleReset(userId, body);
} else {
return response(404, Map.of("error", "Not found"));
}

} catch (Exception e) {
logger.error("Error processing request: {}", e.getMessage(), e);
return response(500, Map.of("error", "Internal server error: " + e.getMessage()));
}
}

/**
* 대화 처리 (음성 또는 텍스트)
*/
private APIGatewayProxyResponseEvent handleChat(String userId, String body) {
if (body == null || body.isEmpty()) {
return response(400, Map.of("error", "Request body is required"));
}

JsonObject request = JsonParser.parseString(body).getAsJsonObject();

String sessionId = request.has("sessionId") && !request.get("sessionId").isJsonNull()
? request.get("sessionId").getAsString() : null;
String level = request.has("level") && !request.get("level").isJsonNull()
? request.get("level").getAsString() : "INTERMEDIATE";
String audio = request.has("audio") && !request.get("audio").isJsonNull()
? request.get("audio").getAsString() : null;
String text = request.has("text") && !request.get("text").isJsonNull()
? request.get("text").getAsString() : null;

SpeakingResponse result;

if (audio != null && !audio.isEmpty()) {
// 음성 입력 처리
logger.info("Processing voice event");
result = speakingService.processVoiceInput(sessionId, userId, audio, level);
} else if (text != null && !text.trim().isEmpty()) {
// 텍스트 입력 처리
logger.info("Processing text event: {}", text);
result = speakingService.processTextInput(sessionId, userId, text.trim(), level);
} else {
return response(400, Map.of("error", "Either 'audio' or 'text' is required"));
}

return response(200, Map.of(
"sessionId", result.sessionId(),
"userTranscript", result.userTranscript(),
"aiText", result.aiText(),
"aiAudioUrl", result.aiAudioUrl(),
"confidence", result.confidence()
));
}

/**
* 대화 초기화
*/
private APIGatewayProxyResponseEvent handleReset(String userId, String body) {
if (body == null || body.isEmpty()) {
return response(400, Map.of("error", "Request body is required"));
}

JsonObject request = JsonParser.parseString(body).getAsJsonObject();
String sessionId = request.has("sessionId") ? request.get("sessionId").getAsString() : null;

if (sessionId == null || sessionId.isEmpty()) {
return response(400, Map.of("error", "sessionId is required"));
}

speakingService.resetConversation(sessionId);

return response(200, Map.of(
"message", "Conversation reset successfully",
"sessionId", sessionId
));
}

private APIGatewayProxyResponseEvent response(int statusCode, Map<String, Object> body) {
return new APIGatewayProxyResponseEvent()
.withStatusCode(statusCode)
.withHeaders(CORS_HEADERS)
.withBody(gson.toJson(body));
}


}
Loading