Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 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
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
28 changes: 14 additions & 14 deletions ServerlessFunction/gradlew

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 12 additions & 13 deletions ServerlessFunction/gradlew.bat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.mzc.secondproject.serverless.domain.speaking.handler.websocket;
package com.mzc.secondproject.serverless.domain.speaking.handler;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
Expand All @@ -9,6 +9,7 @@
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mzc.secondproject.serverless.common.util.JwtUtil;
import com.mzc.secondproject.serverless.domain.speaking.dto.response.SpeakingResponse;
import com.mzc.secondproject.serverless.domain.speaking.service.SpeakingService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -102,7 +103,7 @@ private APIGatewayProxyResponseEvent handleChat(String userId, String body) {
String audio = request.has("audio") ? request.get("audio").getAsString() : null;
String text = request.has("text") ? request.get("text").getAsString() : null;

SpeakingService.SpeakingResponse result;
SpeakingResponse result;

if (audio != null && !audio.isEmpty()) {
// 음성 입력 처리
Expand Down Expand Up @@ -134,7 +135,7 @@ private APIGatewayProxyResponseEvent handleReset(String userId, String body) {
}

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

if (sessionId == null || sessionId.isEmpty()) {
return response(400, Map.of("error", "sessionId is required"));
Expand Down Expand Up @@ -176,4 +177,6 @@ private APIGatewayProxyResponseEvent response(int statusCode, Map<String, Object
.withHeaders(CORS_HEADERS)
.withBody(gson.toJson(body));
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.mzc.secondproject.serverless.common.config.EnvConfig;
import com.mzc.secondproject.serverless.common.service.PollyService;
import com.mzc.secondproject.serverless.domain.opic.service.TranscribeProxyService;
import com.mzc.secondproject.serverless.domain.speaking.dto.response.SpeakingResponse;
import com.mzc.secondproject.serverless.domain.speaking.model.SpeakingSession;
import com.mzc.secondproject.serverless.domain.speaking.repository.SpeakingSessionRepository;

Expand Down Expand Up @@ -82,7 +83,7 @@ public SpeakingResponse processVoiceInput(String sessionId, String userId, Strin
logger.info("Step 1: Transcribing audio...");
TranscribeProxyService.TranscribeResult sttResult = transcribeService.transcribe(
audioBase64,
sessionId,
session.getSessionId(),
"en-US"
);
String userText = sttResult.transcript();
Expand Down Expand Up @@ -314,6 +315,7 @@ private List<Message> parseHistory(String historyJson) {
return history;
}


/**
* 히스토리 JSON 변환
*/
Expand All @@ -328,18 +330,9 @@ private String toJson(List<Message> history) {
return array.toString();
}

// ==================== Inner Classes ====================

private record Message(String role, String content) {}

/**
* Speaking 응답 DTO
* 대화 메시지 (히스토리용)
*/
public record SpeakingResponse(
String sessionId, // 세션 ID (다음 요청에 사용)
String userTranscript, // 사용자가 말한 내용 (STT 결과)
String aiText, // AI 응답 텍스트
String aiAudioUrl, // AI 응답 음성 URL (Polly)
double confidence // STT 신뢰도comp
) {}
private record Message(String role, String content) {}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static ProfileResponse from(User user) {
.email(user.getEmail())
.nickname(user.getNickname())
.level(user.getLevel())
.profileUrl(user.getProfileUrl())
.profileUrl(user.getProfileUrlForResponse())
.createdAt(user.getCreatedAt())
.updatedAt(user.getUpdatedAt())
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,20 @@ private APIGatewayProxyResponseEvent getMyProfile(
APIGatewayProxyRequestEvent request,
String userId // cognitoSub
) {

User user = userService.getProfile(userId, request);
ProfileResponse response = ProfileResponse.from(user);

// profileUrl을 Presigned URL로 변환
String presignedUrl = userService.getPresignedProfileUrl(user.getProfileUrl());

ProfileResponse response = ProfileResponse.builder()
.userId(user.getCognitoSub())
.email(user.getEmail())
.nickname(user.getNickname())
.level(user.getLevel())
.profileUrl(presignedUrl) // Presigned URL 사용
.createdAt(user.getCreatedAt())
.updatedAt(user.getUpdatedAt())
.build();

return ResponseGenerator.ok(user.getNickname() + " 환영합니다!", response);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ public class User {
private String nickname;
private String level;
private String profileUrl;
private String profileUrlForResponse;
private String createdAt;
private String updatedAt;
private String lastLoginAt;
private Long ttl;



/**
* 신규 사용자 생성
* - Lazy Registration 적용: 최초 프로필 조회 시 DynamoDB에 저장
Expand Down Expand Up @@ -114,7 +116,12 @@ public void updateProfileUrl(String newProfileUrl) {
this.profileUrl = newProfileUrl;
this.updatedAt = Instant.now().toString();
}


@DynamoDbIgnore
public String getProfileUrlForResponse() {
return profileUrlForResponse != null ? profileUrlForResponse : profileUrl;
}

public void updateLastLoginAt() {
this.lastLoginAt = Instant.now().toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import com.mzc.secondproject.serverless.domain.user.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;

Expand Down Expand Up @@ -52,18 +54,53 @@ public UserService(UserRepository userRepository) {
* @return User 객체
*/
public User getProfile(String userId, APIGatewayProxyRequestEvent request) {

return userRepository.findByCognitoSub(userId)
.map(user -> {
// 정상 DB에서 조회 완료
user.updateLastLoginAt();
userRepository.update(user);
return user;

User user = userRepository.findByCognitoSub(userId)
.map(u -> {
u.updateLastLoginAt();
userRepository.update(u);
return u;
})
.orElseGet(() -> {
// PostConfirmation 실패 대비 fallback
return createUserFromRequest(userId, request);
});
.orElseGet(() -> createUserFromRequest(userId, request));

// 프로필 URL을 Presigned URL로 변환
String presignedProfileUrl = getPresignedProfileUrl(user.getProfileUrl());
user.setProfileUrlForResponse(presignedProfileUrl); // 응답용으로만 설정

return user;
}

public String getPresignedProfileUrl(String s3Url) {
if (s3Url == null || s3Url.isEmpty()) {
return generateGetPresignedUrl("profile/default.png");
}
String key = extractKeyFromS3Url(s3Url);
return generateGetPresignedUrl(key);
}

private String generateGetPresignedUrl(String imageKey) {
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
.bucket(BUCKET_NAME)
.key(imageKey)
.build();

GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofHours(24))
.getObjectRequest(getObjectRequest)
.build();

return s3Presigner.presignGetObject(presignRequest).url().toString();
}


private String extractKeyFromS3Url(String s3Url) {
// https://group2-englishstudy.s3.amazonaws.com/profile/user123/img.png
// → profile/user123/img.png
String prefix = String.format("https://%s.s3.amazonaws.com/", BUCKET_NAME);
if (s3Url.startsWith(prefix)) {
return s3Url.substring(prefix.length());
}
return s3Url;
}

/**
Expand Down
Loading