Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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,6 +19,8 @@
import com.mzc.secondproject.serverless.domain.chatting.service.ChatMessageService;
import com.mzc.secondproject.serverless.domain.chatting.service.CommandService;
import com.mzc.secondproject.serverless.domain.chatting.service.GameService;
import com.mzc.secondproject.serverless.domain.user.model.User;
import com.mzc.secondproject.serverless.domain.user.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -44,6 +46,7 @@ public class WebSocketMessageHandler implements RequestHandler<Map<String, Objec
private final WebSocketBroadcaster broadcaster;
private final CommandService commandService;
private final GameService gameService;
private final UserService userService;

public WebSocketMessageHandler() {
this.chatMessageService = new ChatMessageService();
Expand All @@ -53,6 +56,7 @@ public WebSocketMessageHandler() {
this.broadcaster = new WebSocketBroadcaster();
this.commandService = new CommandService();
this.gameService = new GameService();
this.userService = new UserService();
}

@Override
Expand Down Expand Up @@ -155,6 +159,21 @@ private Map<String, Object> handleRegularMessage(String connectionId, MessagePay
// 일반 메시지 저장 및 브로드캐스트
String messageId = UUID.randomUUID().toString();
String now = Instant.now().toString();

// 닉네임 조회
String nickname = "Unknown";
try {
// DB에서 유저 정보(닉네임) 가져오기
User user = userService.getUserProfile(payload.userId);
if (user != null && user.getNickname() != null) {
nickname = user.getNickname();
} else {
// 혹시 없으면 UUID 사용
nickname = payload.userId;
}
} catch (Exception e) {
nickname = payload.userId;
}

ChatMessage message = ChatMessage.builder()
.pk("ROOM#" + payload.roomId)
Expand All @@ -166,6 +185,7 @@ private Map<String, Object> handleRegularMessage(String connectionId, MessagePay
.messageId(messageId)
.roomId(payload.roomId)
.userId(payload.userId)
.nickname(nickname)
.content(payload.content)
.messageType(messageType)
.createdAt(now)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class ChatMessage {
private String messageId;
private String roomId;
private String userId;
private String nickname;
private String content;
private String messageType; // TEXT, IMAGE, VOICE, AI_RESPONSE
private String createdAt;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,22 @@ public Optional<NewsArticle> getArticle(String articleId) {
}

/**
* 오늘의 뉴스 목록 조회
* 오늘의 뉴스 목록 조회 (오늘 기사 없으면 어제 기사 조회)
*/
public PaginatedResult<NewsArticle> getTodayNews(int limit, String cursor) {
String today = LocalDate.now().toString();
logger.debug("오늘의 뉴스 조회: date={}, limit={}", today, limit);
return articleRepository.findByDate(today, limit, cursor);

PaginatedResult<NewsArticle> result = articleRepository.findByDate(today, limit, cursor);

// 오늘 기사가 없으면 어제 기사 조회
if (result.items().isEmpty() && cursor == null) {
String yesterday = LocalDate.now().minusDays(1).toString();
logger.debug("오늘 기사 없음, 어제 기사 조회: date={}", yesterday);
result = articleRepository.findByDate(yesterday, limit, cursor);
}

return result;
}

/**
Expand Down
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