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 @@ -31,7 +31,31 @@ public enum BadgeType {
PERFECT_DRAWER("완벽한 출제자", "출제 시 전원이 정답을 맞췄습니다", "perfect_drawer.png", "PERFECT_DRAWS", 1),

// 특별
MASTER("학습 마스터", "모든 업적을 달성했습니다", "master.png", "ALL_BADGES", 1);
MASTER("학습 마스터", "모든 업적을 달성했습니다", "master.png", "ALL_BADGES", 1),

// 뉴스 - 읽기
NEWS_FIRST_READ("뉴스 첫 발걸음", "첫 번째 뉴스 읽기 완료", "news_first_read.png", "NEWS_READ", 1),
NEWS_READ_10("뉴스 탐험가", "뉴스 10개 읽기 완료", "news_read_10.png", "NEWS_READ", 10),
NEWS_READ_50("뉴스 애호가", "뉴스 50개 읽기 완료", "news_read_50.png", "NEWS_READ", 50),
NEWS_READ_100("뉴스 전문가", "뉴스 100개 읽기 완료", "news_read_100.png", "NEWS_READ", 100),

// 뉴스 - 퀴즈
NEWS_QUIZ_FIRST("퀴즈 도전", "첫 뉴스 퀴즈 완료", "news_quiz_first.png", "NEWS_QUIZ", 1),
NEWS_QUIZ_PERFECT("완벽한 이해", "뉴스 퀴즈에서 만점 달성", "news_quiz_perfect.png", "NEWS_QUIZ_PERFECT", 1),
NEWS_QUIZ_10("퀴즈 탐험가", "뉴스 퀴즈 10회 완료", "news_quiz_10.png", "NEWS_QUIZ", 10),
NEWS_QUIZ_50("퀴즈 마스터", "뉴스 퀴즈 50회 완료", "news_quiz_50.png", "NEWS_QUIZ", 50),

// 뉴스 - 단어 수집
NEWS_WORD_10("단어 수집가", "뉴스에서 단어 10개 수집", "news_word_10.png", "NEWS_WORD", 10),
NEWS_WORD_50("단어 사냥꾼", "뉴스에서 단어 50개 수집", "news_word_50.png", "NEWS_WORD", 50),
NEWS_WORD_100("단어 전문가", "뉴스에서 단어 100개 수집", "news_word_100.png", "NEWS_WORD", 100),

// 뉴스 - 연속 학습
NEWS_STREAK_7("일주일 뉴스 습관", "7일 연속 뉴스 읽기", "news_streak_7.png", "NEWS_STREAK", 7),
NEWS_STREAK_30("한 달 뉴스 습관", "30일 연속 뉴스 읽기", "news_streak_30.png", "NEWS_STREAK", 30),

// 뉴스 - 종합
NEWS_MASTER("뉴스 마스터", "읽기100+퀴즈50+단어100 달성", "news_master.png", "NEWS_MASTER", 1);

private static final String BUCKET_NAME = System.getenv("BUCKET_NAME");
private static final String BASE_URL = getBaseUrl();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ public class BadgeConditionStrategyFactory {
register(new GamesWonStrategy());
register(new QuickGuessesStrategy());
register(new PerfectDrawsStrategy());
// 뉴스 관련 전략
register(new NewsReadStrategy());
register(new NewsQuizStrategy());
register(new NewsQuizPerfectStrategy());
register(new NewsWordStrategy());
register(new NewsStreakStrategy());
register(new NewsMasterStrategy());
// 별도 로직이 필요한 카테고리
register(new NoOpStrategy("PERFECT_TEST"));
register(new NoOpStrategy("ALL_BADGES"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.mzc.secondproject.serverless.domain.badge.strategy;

import com.mzc.secondproject.serverless.domain.badge.enums.BadgeType;
import com.mzc.secondproject.serverless.domain.stats.model.UserStats;

/**
* 뉴스 마스터 뱃지 조건 전략
* 읽기 100개 + 퀴즈 50회 + 단어 100개 달성 시 획득
*/
public class NewsMasterStrategy implements BadgeConditionStrategy {

private static final int NEWS_READ_REQUIRED = 100;
private static final int NEWS_QUIZ_REQUIRED = 50;
private static final int NEWS_WORD_REQUIRED = 100;

@Override
public boolean checkCondition(BadgeType type, UserStats stats) {
int newsRead = stats.getNewsRead() != null ? stats.getNewsRead() : 0;
int newsQuiz = stats.getNewsQuizCompleted() != null ? stats.getNewsQuizCompleted() : 0;
int newsWord = stats.getNewsWordsCollected() != null ? stats.getNewsWordsCollected() : 0;

return newsRead >= NEWS_READ_REQUIRED
&& newsQuiz >= NEWS_QUIZ_REQUIRED
&& newsWord >= NEWS_WORD_REQUIRED;
}

@Override
public int calculateProgress(BadgeType type, UserStats stats) {
int newsRead = stats.getNewsRead() != null ? stats.getNewsRead() : 0;
int newsQuiz = stats.getNewsQuizCompleted() != null ? stats.getNewsQuizCompleted() : 0;
int newsWord = stats.getNewsWordsCollected() != null ? stats.getNewsWordsCollected() : 0;

// 3가지 조건의 평균 진행률 (각각 100%, 100%, 100% 기준)
int readProgress = Math.min(newsRead * 100 / NEWS_READ_REQUIRED, 100);
int quizProgress = Math.min(newsQuiz * 100 / NEWS_QUIZ_REQUIRED, 100);
int wordProgress = Math.min(newsWord * 100 / NEWS_WORD_REQUIRED, 100);

return (readProgress + quizProgress + wordProgress) / 3;
}

@Override
public String getCategory() {
return "NEWS_MASTER";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.mzc.secondproject.serverless.domain.badge.strategy;

import com.mzc.secondproject.serverless.domain.badge.enums.BadgeType;
import com.mzc.secondproject.serverless.domain.stats.model.UserStats;

/**
* 뉴스 퀴즈 만점 뱃지 조건 전략
*/
public class NewsQuizPerfectStrategy implements BadgeConditionStrategy {

@Override
public boolean checkCondition(BadgeType type, UserStats stats) {
return stats.getNewsQuizPerfect() != null && stats.getNewsQuizPerfect() >= type.getThreshold();
}

@Override
public int calculateProgress(BadgeType type, UserStats stats) {
return stats.getNewsQuizPerfect() != null ? stats.getNewsQuizPerfect() : 0;
}

@Override
public String getCategory() {
return "NEWS_QUIZ_PERFECT";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.mzc.secondproject.serverless.domain.badge.strategy;

import com.mzc.secondproject.serverless.domain.badge.enums.BadgeType;
import com.mzc.secondproject.serverless.domain.stats.model.UserStats;

/**
* 뉴스 퀴즈 완료 뱃지 조건 전략
*/
public class NewsQuizStrategy implements BadgeConditionStrategy {

@Override
public boolean checkCondition(BadgeType type, UserStats stats) {
return stats.getNewsQuizCompleted() != null && stats.getNewsQuizCompleted() >= type.getThreshold();
}

@Override
public int calculateProgress(BadgeType type, UserStats stats) {
return stats.getNewsQuizCompleted() != null ? stats.getNewsQuizCompleted() : 0;
}

@Override
public String getCategory() {
return "NEWS_QUIZ";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.mzc.secondproject.serverless.domain.badge.strategy;

import com.mzc.secondproject.serverless.domain.badge.enums.BadgeType;
import com.mzc.secondproject.serverless.domain.stats.model.UserStats;

/**
* 뉴스 읽기 뱃지 조건 전략
*/
public class NewsReadStrategy implements BadgeConditionStrategy {

@Override
public boolean checkCondition(BadgeType type, UserStats stats) {
return stats.getNewsRead() != null && stats.getNewsRead() >= type.getThreshold();
}

@Override
public int calculateProgress(BadgeType type, UserStats stats) {
return stats.getNewsRead() != null ? stats.getNewsRead() : 0;
}

@Override
public String getCategory() {
return "NEWS_READ";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.mzc.secondproject.serverless.domain.badge.strategy;

import com.mzc.secondproject.serverless.domain.badge.enums.BadgeType;
import com.mzc.secondproject.serverless.domain.stats.model.UserStats;

/**
* 뉴스 연속 읽기 뱃지 조건 전략
*/
public class NewsStreakStrategy implements BadgeConditionStrategy {

@Override
public boolean checkCondition(BadgeType type, UserStats stats) {
return stats.getNewsStreak() != null && stats.getNewsStreak() >= type.getThreshold();
}

@Override
public int calculateProgress(BadgeType type, UserStats stats) {
return stats.getNewsStreak() != null ? stats.getNewsStreak() : 0;
}

@Override
public String getCategory() {
return "NEWS_STREAK";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.mzc.secondproject.serverless.domain.badge.strategy;

import com.mzc.secondproject.serverless.domain.badge.enums.BadgeType;
import com.mzc.secondproject.serverless.domain.stats.model.UserStats;

/**
* 뉴스 단어 수집 뱃지 조건 전략
*/
public class NewsWordStrategy implements BadgeConditionStrategy {

@Override
public boolean checkCondition(BadgeType type, UserStats stats) {
return stats.getNewsWordsCollected() != null && stats.getNewsWordsCollected() >= type.getThreshold();
}

@Override
public int calculateProgress(BadgeType type, UserStats stats) {
return stats.getNewsWordsCollected() != null ? stats.getNewsWordsCollected() : 0;
}

@Override
public String getCategory() {
return "NEWS_WORD";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -416,13 +416,19 @@ private APIGatewayProxyResponseEvent collectWord(APIGatewayProxyRequestEvent req
String word = body.get("word").getAsString();
String context = body.has("context") ? body.get("context").getAsString() : "";

NewsWordCollect collected = wordService.collectWord(userId, articleId, word, context);
NewsWordService.WordCollectResult result = wordService.collectWord(userId, articleId, word, context);

if (collected == null) {
if (result == null || result.wordCollect() == null) {
return ResponseGenerator.fail(NewsErrorCode.WORD_ALREADY_COLLECTED);
}

return ResponseGenerator.ok("단어 수집 성공", collected);
Map<String, Object> responseData = new java.util.HashMap<>();
responseData.put("wordCollect", result.wordCollect());
if (result.newBadges() != null && !result.newBadges().isEmpty()) {
responseData.put("newBadges", result.newBadges());
}

return ResponseGenerator.ok("단어 수집 성공", responseData);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@

import com.mzc.secondproject.serverless.common.config.EnvConfig;
import com.mzc.secondproject.serverless.common.service.PollyService;
import com.mzc.secondproject.serverless.domain.badge.model.UserBadge;
import com.mzc.secondproject.serverless.domain.badge.service.BadgeService;
import com.mzc.secondproject.serverless.domain.news.model.NewsArticle;
import com.mzc.secondproject.serverless.domain.news.model.UserNewsRecord;
import com.mzc.secondproject.serverless.domain.news.repository.NewsArticleRepository;
import com.mzc.secondproject.serverless.domain.news.repository.UserNewsRepository;
import com.mzc.secondproject.serverless.domain.stats.model.UserStats;
import com.mzc.secondproject.serverless.domain.stats.repository.UserStatsRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand All @@ -24,29 +29,38 @@ public class NewsLearningService {
private final NewsArticleRepository articleRepository;
private final UserNewsRepository userNewsRepository;
private final PollyService pollyService;
private final UserStatsRepository userStatsRepository;
private final BadgeService badgeService;

public NewsLearningService() {
this.articleRepository = new NewsArticleRepository();
this.userNewsRepository = new UserNewsRepository();
this.pollyService = new PollyService(BUCKET_NAME, "news/audio/");
this.userStatsRepository = new UserStatsRepository();
this.badgeService = new BadgeService();
}

public NewsLearningService(NewsArticleRepository articleRepository,
UserNewsRepository userNewsRepository,
PollyService pollyService) {
PollyService pollyService,
UserStatsRepository userStatsRepository,
BadgeService badgeService) {
this.articleRepository = articleRepository;
this.userNewsRepository = userNewsRepository;
this.pollyService = pollyService;
this.userStatsRepository = userStatsRepository;
this.badgeService = badgeService;
}

/**
* 뉴스 읽기 완료 기록
* @return 새로 획득한 배지 목록
*/
public void markAsRead(String userId, String articleId) {
public List<UserBadge> markAsRead(String userId, String articleId) {
Optional<NewsArticle> article = articleRepository.findById(articleId);
if (article.isEmpty()) {
logger.warn("기사를 찾을 수 없음: {}", articleId);
return;
return new ArrayList<>();
}

NewsArticle a = article.get();
Expand All @@ -65,6 +79,23 @@ public void markAsRead(String userId, String articleId) {
}

logger.info("읽기 완료 기록: userId={}, articleId={}", userId, articleId);

// 통계 업데이트 및 배지 체크
List<UserBadge> newBadges = new ArrayList<>();
try {
UserStats updatedStats = userStatsRepository.incrementNewsReadStats(userId);
if (updatedStats != null) {
newBadges = badgeService.checkAndAwardBadges(userId, updatedStats);
if (!newBadges.isEmpty()) {
logger.info("새 배지 획득: userId={}, badges={}", userId,
newBadges.stream().map(UserBadge::getBadgeType).toList());
}
}
} catch (Exception e) {
logger.error("통계/배지 업데이트 실패: userId={}, error={}", userId, e.getMessage());
}

return newBadges;
}

/**
Expand Down
Loading