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
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
Expand Up @@ -111,7 +111,7 @@ private APIGatewayProxyResponseEvent getNewsList(APIGatewayProxyRequestEvent req
result = queryService.getTodayNews(limit, cursor);
}

return buildPaginatedResponse(result);
return buildPaginatedResponse(result, getUserId(request));
}

/**
Expand All @@ -126,7 +126,7 @@ private APIGatewayProxyResponseEvent getTodayNews(APIGatewayProxyRequestEvent re
int limit = parseLimit(params.get("limit"));

PaginatedResult<NewsArticle> result = queryService.getTodayNews(limit, cursor);
return buildPaginatedResponse(result);
return buildPaginatedResponse(result, getUserId(request));
}

/**
Expand All @@ -143,7 +143,7 @@ private APIGatewayProxyResponseEvent getRecommendedNews(APIGatewayProxyRequestEv
int limit = parseLimit(params.get("limit"));

PaginatedResult<NewsArticle> result = queryService.getRecommendedNews(userLevel, limit, cursor);
return buildPaginatedResponse(result);
return buildPaginatedResponse(result, getUserId(request));
}

/**
Expand All @@ -158,15 +158,64 @@ private APIGatewayProxyResponseEvent getNewsDetail(APIGatewayProxyRequestEvent r
return ResponseGenerator.fail(NewsErrorCode.ARTICLE_NOT_FOUND);
}

return ResponseGenerator.ok("뉴스 조회 성공", article.get());
// 로그인한 사용자의 경우 북마크/읽기 상태 추가
String userId = getUserId(request);
Map<String, Object> response = new HashMap<>();
response.put("article", article.get());

if (userId != null) {
response.put("isBookmarked", learningService.isBookmarked(userId, articleId));
response.put("isRead", learningService.hasRead(userId, articleId));
} else {
response.put("isBookmarked", false);
response.put("isRead", false);
}

return ResponseGenerator.ok("뉴스 조회 성공", response);
}

/**
* 페이지네이션 응답 생성
*/
private APIGatewayProxyResponseEvent buildPaginatedResponse(PaginatedResult<NewsArticle> result) {
return buildPaginatedResponse(result, null);
}

/**
* 페이지네이션 응답 생성 (북마크 상태 포함)
*/
private APIGatewayProxyResponseEvent buildPaginatedResponse(PaginatedResult<NewsArticle> result, String userId) {
List<Map<String, Object>> articlesWithStatus = new java.util.ArrayList<>();
java.util.Set<String> bookmarkedIds = java.util.Collections.emptySet();

// 로그인한 사용자의 경우 북마크 상태 조회
if (userId != null && !result.items().isEmpty()) {
List<String> articleIds = result.items().stream()
.map(NewsArticle::getArticleId)
.toList();
bookmarkedIds = learningService.getBookmarkedArticleIds(userId, articleIds);
}

for (NewsArticle article : result.items()) {
Map<String, Object> articleWithStatus = new HashMap<>();
articleWithStatus.put("articleId", article.getArticleId());
articleWithStatus.put("title", article.getTitle());
articleWithStatus.put("summary", article.getSummary());
articleWithStatus.put("source", article.getSource());
articleWithStatus.put("publishedAt", article.getPublishedAt());
articleWithStatus.put("keywords", article.getKeywords());
articleWithStatus.put("highlightWords", article.getHighlightWords());
articleWithStatus.put("category", article.getCategory());
articleWithStatus.put("level", article.getLevel());
articleWithStatus.put("cefrLevel", article.getCefrLevel());
articleWithStatus.put("imageUrl", article.getImageUrl());
articleWithStatus.put("readCount", article.getReadCount());
articleWithStatus.put("isBookmarked", bookmarkedIds.contains(article.getArticleId()));
articlesWithStatus.add(articleWithStatus);
}

Map<String, Object> response = new HashMap<>();
response.put("articles", result.items());
response.put("articles", articlesWithStatus);
response.put("nextCursor", result.nextCursor());
response.put("hasMore", result.hasMore());
response.put("count", result.items().size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
public class KeywordInfo {

private String word; // 영어 단어
private String meaning; // 한국어 뜻
private String meaning; // 영어 뜻 (간단한 정의)
private String meaningKo; // 한국어 뜻
private String example; // 기사에서 발췌한 예문
private String level; // 단어 난이도 (BEGINNER, INTERMEDIATE, ADVANCED)
private Integer position; // 기사 내 위치 (문장 번호 또는 단어 인덱스)
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,19 @@ public List<UserNewsRecord> getUserBookmarks(String userId, int limit) {
return results.subList(0, Math.min(results.size(), limit));
}

/**
* 여러 기사의 북마크 여부 확인 (배치)
*/
public Set<String> getBookmarkedArticleIds(String userId, List<String> articleIds) {
Set<String> bookmarkedIds = new HashSet<>();
for (String articleId : articleIds) {
if (isBookmarked(userId, articleId)) {
bookmarkedIds.add(articleId);
}
}
return bookmarkedIds;
}

/**
* 사용자 뉴스 통계 조회
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,15 @@ private List<KeywordInfo> extractKeywords(String content) {
*/
private AnalysisResult generateSummaryAndQuiz(String content, String cefrLevel) {
String systemPrompt = """
You are an English learning assistant. Analyze the news article and create learning materials.
You are an English learning assistant for Korean learners. Analyze the news article and create learning materials.

Respond in this exact JSON format:
{
"summary": "3-line summary in English (each line separated by newline)",
"keywords": [
{"word": "economy", "meaning": "the system of trade and industry", "meaningKo": "경제", "example": "The economy is growing steadily."},
{"word": "policy", "meaning": "a plan of action adopted by government", "meaningKo": "정책", "example": "The new policy affects all citizens."}
],
"highlightWords": ["word1", "word2", "word3"],
"quiz": [
{
Expand Down Expand Up @@ -211,9 +215,16 @@ private AnalysisResult generateSummaryAndQuiz(String content, String cefrLevel)
]
}

Create exactly 3 quiz questions.
highlightWords should contain 3-5 difficult words for learners.
Adjust difficulty based on CEFR level: """ + cefrLevel;
IMPORTANT:
- keywords: Extract 5-8 important vocabulary words from the article. Include:
- word: the English word
- meaning: simple English definition
- meaningKo: Korean translation of the word (한국어 뜻)
- example: example sentence from the article
- highlightWords: 3-5 difficult words that learners should pay attention to (just the words, no definitions).
- category: Choose EXACTLY ONE from: WORLD, POLITICS, BUSINESS, TECH, SCIENCE, HEALTH, SPORTS, ENTERTAINMENT, LIFESTYLE
- Create exactly 3 quiz questions.
- Adjust difficulty based on CEFR level: """ + cefrLevel;

String userPrompt = "Create learning materials for this article:\n\n" + truncate(content, 1500);

Expand Down Expand Up @@ -268,6 +279,21 @@ private AnalysisResult parseAnalysisResult(String response) {
JsonObject json = gson.fromJson(jsonStr, JsonObject.class);

String summary = json.has("summary") ? json.get("summary").getAsString() : null;
String category = json.has("category") ? json.get("category").getAsString().toUpperCase() : "WORLD";

// keywords 파싱
List<KeywordInfo> keywords = new ArrayList<>();
if (json.has("keywords")) {
json.getAsJsonArray("keywords").forEach(e -> {
JsonObject k = e.getAsJsonObject();
keywords.add(KeywordInfo.builder()
.word(k.has("word") ? k.get("word").getAsString() : "")
.meaning(k.has("meaning") ? k.get("meaning").getAsString() : "")
.meaningKo(k.has("meaningKo") ? k.get("meaningKo").getAsString() : "")
.example(k.has("example") ? k.get("example").getAsString() : "")
.build());
});
}

List<String> highlightWords = new ArrayList<>();
if (json.has("highlightWords")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
* 뉴스 학습 부가 기능 서비스
Expand Down Expand Up @@ -49,6 +50,12 @@ public void markAsRead(String userId, String articleId) {
return;
}

// 이미 읽은 기사인지 확인 (중복 조회수 증가 방지)
if (userNewsRepository.hasRead(userId, articleId)) {
logger.debug("이미 읽은 기사: userId={}, articleId={}", userId, articleId);
return;
}

NewsArticle a = article.get();
userNewsRepository.saveReadRecord(
userId,
Expand All @@ -58,7 +65,7 @@ public void markAsRead(String userId, String articleId) {
a.getCategory()
);

// 조회수 증가
// 조회수 증가 (새로운 읽기만)
String date = extractDateFromPk(a.getPk());
if (date != null) {
articleRepository.incrementReadCount(date, articleId);
Expand Down Expand Up @@ -105,7 +112,21 @@ public boolean isBookmarked(String userId, String articleId) {
}

/**
* 사용자 북마크 목록 조회
* 읽기 여부 확인
*/
public boolean hasRead(String userId, String articleId) {
return userNewsRepository.hasRead(userId, articleId);
}

/**
* 여러 기사의 북마크 여부 확인 (배치)
*/
public Set<String> getBookmarkedArticleIds(String userId, List<String> articleIds) {
return userNewsRepository.getBookmarkedArticleIds(userId, articleIds);
}

/**
* 사용자 북마크 목록 조회 (기사 정보 포함)
*/
public List<UserNewsRecord> getUserBookmarks(String userId, int limit) {
return userNewsRepository.getUserBookmarks(userId, limit);
Expand Down
Loading