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
1 change: 1 addition & 0 deletions ServerlessFunction/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies {
implementation 'software.amazon.awssdk:url-connection-client'
implementation 'software.amazon.awssdk:ssm'
implementation 'software.amazon.awssdk:scheduler'
implementation 'software.amazon.awssdk:sqs'

// AWS X-Ray SDK (다운스트림 서비스 추적용)
implementation 'com.amazonaws:aws-xray-recorder-sdk-core:2.15.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.sns.SnsClient;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.ssm.SsmClient;

/**
Expand Down Expand Up @@ -60,7 +61,12 @@ public final class AwsClients {
private static final SsmClient SSM_CLIENT = SsmClient.builder()
.overrideConfiguration(XRAY_CONFIG)
.build();


// SQS
private static final SqsClient SQS_CLIENT = SqsClient.builder()
.overrideConfiguration(XRAY_CONFIG)
.build();

private AwsClients() {
// 인스턴스화 방지
}
Expand Down Expand Up @@ -104,4 +110,8 @@ public static ComprehendClient comprehend() {
public static SsmClient ssm() {
return SSM_CLIENT;
}

public static SqsClient sqs() {
return SQS_CLIENT;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,18 @@ public final class EnvConfig {
private EnvConfig() {
// 유틸리티 클래스 - 인스턴스화 방지
}


/**
* 선택적 환경 변수를 가져옵니다.
* 환경 변수가 설정되지 않은 경우 null을 반환합니다.
*
* @param name 환경 변수 이름
* @return 환경 변수 값 또는 null
*/
public static String get(String name) {
return System.getenv(name);
}

/**
* 필수 환경 변수를 가져옵니다.
* 환경 변수가 설정되지 않았거나 빈 문자열인 경우 IllegalStateException을 발생시킵니다.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.mzc.secondproject.serverless.common.util;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;

Expand All @@ -10,9 +12,25 @@
* JSON 파싱 관련 공통 유틸리티
*/
public class JsonUtil {


private static final Gson GSON = new GsonBuilder().create();

private JsonUtil() {
}

/**
* 객체를 JSON 문자열로 변환
*/
public static String toJson(Object obj) {
return GSON.toJson(obj);
}

/**
* JSON 문자열을 객체로 변환
*/
public static <T> T fromJson(String json, Class<T> clazz) {
return GSON.fromJson(json, clazz);
}

// 응답에서 JSON 부분만 추출
public static String extractJson(String response) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.mzc.secondproject.serverless.domain.badge.repository.BadgeRepository;
import com.mzc.secondproject.serverless.domain.badge.strategy.BadgeConditionStrategy;
import com.mzc.secondproject.serverless.domain.badge.strategy.BadgeConditionStrategyFactory;
import com.mzc.secondproject.serverless.domain.notification.service.NotificationPublisher;
import com.mzc.secondproject.serverless.domain.stats.model.UserStats;
import com.mzc.secondproject.serverless.domain.stats.repository.UserStatsRepository;
import org.slf4j.Logger;
Expand All @@ -24,20 +25,23 @@ public class BadgeService {

private final BadgeRepository badgeRepository;
private final UserStatsRepository userStatsRepository;

private final NotificationPublisher notificationPublisher;

/**
* 기본 생성자 (Lambda에서 사용)
*/
public BadgeService() {
this(new BadgeRepository(), new UserStatsRepository());
this(new BadgeRepository(), new UserStatsRepository(), NotificationPublisher.getInstance());
}

/**
* 의존성 주입 생성자 (테스트 용이성)
*/
public BadgeService(BadgeRepository badgeRepository, UserStatsRepository userStatsRepository) {
public BadgeService(BadgeRepository badgeRepository, UserStatsRepository userStatsRepository,
NotificationPublisher notificationPublisher) {
this.badgeRepository = badgeRepository;
this.userStatsRepository = userStatsRepository;
this.notificationPublisher = notificationPublisher;
}

/**
Expand Down Expand Up @@ -98,6 +102,15 @@ public List<UserBadge> checkAndAwardBadges(String userId, UserStats stats) {
badgeRepository.save(badge);
newBadges.add(badge);
logger.info("Badge awarded: userId={}, badge={}", userId, type.name());

// 알림 발행
notificationPublisher.publishBadgeEarned(
userId,
type.name(),
type.getName(),
type.getDescription(),
badge.getImageUrl()
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.mzc.secondproject.serverless.domain.chatting.repository.ConnectionRepository;
import com.mzc.secondproject.serverless.domain.chatting.repository.GameRoundRepository;
import com.mzc.secondproject.serverless.domain.chatting.repository.GameSessionRepository;
import com.mzc.secondproject.serverless.domain.notification.service.NotificationPublisher;
import com.mzc.secondproject.serverless.domain.vocabulary.model.Word;
import com.mzc.secondproject.serverless.domain.vocabulary.repository.WordRepository;
import org.slf4j.Logger;
Expand All @@ -37,30 +38,33 @@ public class GameService {
private final WordRepository wordRepository;
private final GameStatsService gameStatsService;
private final GameSchedulerClient gameSchedulerClient;

private final NotificationPublisher notificationPublisher;

/**
* 기본 생성자 (Lambda에서 사용)
*/
public GameService() {
this(new ChatRoomRepository(), new ConnectionRepository(),
new GameRoundRepository(), new GameSessionRepository(),
new WordRepository(), new GameStatsService(), new GameSchedulerClient());
new WordRepository(), new GameStatsService(), new GameSchedulerClient(),
NotificationPublisher.getInstance());
}

/**
* 의존성 주입 생성자 (테스트 용이성)
*/
public GameService(ChatRoomRepository chatRoomRepository, ConnectionRepository connectionRepository,
GameRoundRepository gameRoundRepository, GameSessionRepository gameSessionRepository,
WordRepository wordRepository, GameStatsService gameStatsService,
GameSchedulerClient gameSchedulerClient) {
GameSchedulerClient gameSchedulerClient, NotificationPublisher notificationPublisher) {
this.chatRoomRepository = chatRoomRepository;
this.connectionRepository = connectionRepository;
this.gameRoundRepository = gameRoundRepository;
this.gameSessionRepository = gameSessionRepository;
this.wordRepository = wordRepository;
this.gameStatsService = gameStatsService;
this.gameSchedulerClient = gameSchedulerClient;
this.notificationPublisher = notificationPublisher;
}

/**
Expand Down Expand Up @@ -508,7 +512,10 @@ private CommandResult finishGame(GameSession session, ChatRoom room, String reas
} catch (Exception e) {
logger.error("Failed to update game stats: roomId={}, error={}", room.getRoomId(), e.getMessage());
}


// 게임 종료 알림 발행 (각 플레이어별)
publishGameEndNotifications(session, room.getRoomId());

// 최종 점수 정렬
StringBuilder sb = new StringBuilder("🎮 게임 종료!\n\n📊 최종 순위:\n");
if (session.getScores() != null && !session.getScores().isEmpty()) {
Expand Down Expand Up @@ -698,11 +705,11 @@ private List<Map<String, Object>> buildRankingList(Map<String, Integer> scores)
if (scores == null || scores.isEmpty()) {
return List.of();
}

List<Map.Entry<String, Integer>> sorted = scores.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.toList();

List<Map<String, Object>> ranking = new ArrayList<>();
for (int i = 0; i < sorted.size(); i++) {
Map<String, Object> entry = new HashMap<>();
Expand All @@ -713,7 +720,39 @@ private List<Map<String, Object>> buildRankingList(Map<String, Integer> scores)
}
return ranking;
}


/**
* 게임 종료 알림 발행
*/
private void publishGameEndNotifications(GameSession session, String roomId) {
if (session.getScores() == null || session.getScores().isEmpty()) {
return;
}

List<Map.Entry<String, Integer>> sorted = session.getScores().entrySet().stream()
.sorted((a, b) -> b.getValue().compareTo(a.getValue()))
.toList();

int totalPlayers = sorted.size();

for (int i = 0; i < sorted.size(); i++) {
int rank = i + 1;
String userId = sorted.get(i).getKey();
int score = sorted.get(i).getValue();
boolean isWinner = rank == 1;

notificationPublisher.publishGameEnd(
userId,
roomId,
session.getGameSessionId(),
rank,
totalPlayers,
score,
isWinner
);
}
}

// ========== Result DTOs ==========

public record GameStartResult(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.mzc.secondproject.serverless.domain.news.config;

import com.mzc.secondproject.serverless.common.config.EnvConfig;

/**
* 뉴스 도메인 설정
* 상수 및 환경변수 관리
*/
public final class NewsConfig {

private NewsConfig() {
}

// ========== Environment Variables ==========
private static final String BUCKET_NAME = EnvConfig.getOrDefault("NEWS_BUCKET_NAME", "group2-englishstudy");

// ========== TTS 설정 ==========
/** TTS 텍스트 최대 길이 */
public static final int TTS_MAX_TEXT_LENGTH = 3000;

/** TTS 오디오 저장 경로 */
public static final String TTS_AUDIO_PREFIX = "news/audio/";

/** 기본 TTS 음성 */
public static final String DEFAULT_VOICE = "Joanna";

// ========== 페이지네이션 ==========
/** 기본 페이지 크기 */
public static final int DEFAULT_PAGE_SIZE = 10;

/** 최대 페이지 크기 */
public static final int MAX_PAGE_SIZE = 50;

// ========== 퀴즈 피드백 ==========
public static final String FEEDBACK_PERFECT = "Perfect! You understood the article completely.";
public static final String FEEDBACK_GREAT = "Great job! You have a solid understanding of the article.";
public static final String FEEDBACK_GOOD = "Good effort! Review the highlighted words for better comprehension.";
public static final String FEEDBACK_KEEP_PRACTICING = "Keep practicing! Try reading the article again before retaking the quiz.";
public static final String FEEDBACK_DONT_GIVE_UP = "Don't give up! Focus on vocabulary and main ideas.";

// ========== Score 기준 ==========
public static final int SCORE_PERFECT = 100;
public static final int SCORE_GREAT_THRESHOLD = 80;
public static final int SCORE_GOOD_THRESHOLD = 60;
public static final int SCORE_KEEP_PRACTICING_THRESHOLD = 40;

// ========== Getter Methods ==========
public static String bucketName() {
return BUCKET_NAME;
}

/**
* 점수에 따른 피드백 생성
*/
public static String getFeedbackByScore(int score) {
if (score == SCORE_PERFECT) {
return FEEDBACK_PERFECT;
} else if (score >= SCORE_GREAT_THRESHOLD) {
return FEEDBACK_GREAT;
} else if (score >= SCORE_GOOD_THRESHOLD) {
return FEEDBACK_GOOD;
} else if (score >= SCORE_KEEP_PRACTICING_THRESHOLD) {
return FEEDBACK_KEEP_PRACTICING;
} else {
return FEEDBACK_DONT_GIVE_UP;
}
}

/**
* limit 값 파싱 및 유효성 검증
*/
public static int parseLimit(String limitStr) {
if (limitStr == null) {
return DEFAULT_PAGE_SIZE;
}
try {
int limit = Integer.parseInt(limitStr);
return Math.min(Math.max(limit, 1), MAX_PAGE_SIZE);
} catch (NumberFormatException e) {
return DEFAULT_PAGE_SIZE;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,16 @@ public static String userNewsCommentsPk(String userId) {
public static String userNewsStatPk(String userId) {
return "USER_NEWS_STAT#" + userId;
}

// === Utility Methods ===

/**
* PK에서 날짜 추출 (NEWS#2024-01-15 → 2024-01-15)
*/
public static String extractDateFromPk(String pk) {
if (pk == null || !pk.startsWith(NEWS)) {
return null;
}
return pk.substring(NEWS.length());
}
}
Loading