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
30 changes: 25 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,33 @@ Ling Level API는 모바일 영어 학습 앱을 위한 Spring Boot 기반 백

## 주요 기능

- 책, 아티클, 커스텀 콘텐츠 기반 학습 흐름 제공
- 단어 조회, 변형어 저장, Spring AI와 AWS Bedrock 기반 단어 분석
- 학습 진행도와 일 단위 스트릭 계산
- 추천, 배너, 북마크, FCM 푸시 알림
- 관리자용 콘텐츠, 크롤링, 마이그레이션 API
- CEFR 레벨별 영어 독해 콘텐츠 제공 (문학, 아티클, 웹 콘텐츠)
- 단어 사전 검색 및 개인 단어장 저장
- 사용자 정의 콘텐츠 레벨링 기능
- 스트릭 기반 연속 학습 기록
- Prometheus, Grafana, Sentry 기반 모니터링

## 미리보기

<details>
<summary>앱 화면 미리보기</summary>

| 콘텐츠 레벨링 | 단어 사전 |
| --- | --- |
| <img src="docs/assets/readme/leveling.png" alt="콘텐츠 레벨링 화면" width="420"> | <img src="docs/assets/readme/word.png" alt="단어 사전 검색 화면" width="420"> |

| 사용자 정의 콘텐츠 | 연속 학습 기록 |
| --- | --- |
| <img src="docs/assets/readme/user_content.png" alt="사용자 정의 콘텐츠 화면" width="420"> | <img src="docs/assets/readme/streak.png" alt="스트릭 기반 연속 학습 기록 화면" width="320"> |

</details>

## 시스템 구조

<p align="center">
<img src="docs/assets/readme/architecture.png" alt="Ling Level API 시스템 구조" width="720">
</p>

## 기술 스택

- Java 17, Spring Boot 3.5, Spring Security
Expand Down
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,9 @@ dependencies {
}

tasks.named('test') {
useJUnitPlatform()
useJUnitPlatform {
if (!project.hasProperty('includeExternalTests')) {
excludeTags 'external'
}
}
}
Binary file added docs/assets/readme/architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/readme/leveling.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/readme/streak.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/readme/user_content.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/readme/word.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
import com.rometools.rome.io.XmlReader;
import org.jdom2.Element;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.net.URL;
import java.util.List;

@DisplayName("YouTube RSS 구조 분석 테스트")
@Tag("external")
class YouTubeRssStructureTest {

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.rometools.rome.io.XmlReader;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.net.URL;
Expand All @@ -27,6 +28,7 @@ void setUp() {
}

@Test
@Tag("external")
@DisplayName("실제 YouTube RSS 피드에서 duration 추출 및 Shorts 필터링 테스트")
void testRealYouTubeFeedWithDuration() throws Exception {
// given: Kurzgesagt YouTube 채널 RSS 피드
Expand Down Expand Up @@ -97,6 +99,24 @@ void testRealYouTubeFeedWithDuration() throws Exception {
System.out.println("===========================================");
}

@Test
@DisplayName("YouTube Shorts URL이면 필터링")
void testYouTubeShortsUrl() {
// given: YouTube Shorts URL
SyndEntry entry = mock(SyndEntry.class);
when(entry.getLink()).thenReturn("https://www.youtube.com/shorts/test123");

FeedSource feedSource = mock(FeedSource.class);

// when: 필터 실행
FeedFilterResult result = filter.filter(entry, feedSource);

// then: 필터링되어야 함
assertFalse(result.isPassed(), "YouTube Shorts URL은 필터링되어야 함");
assertEquals("YouTubeShortsFilter", result.getFilterName());
assertTrue(result.getReason().contains("YouTube Shorts"));
}

@Test
@DisplayName("URL이 null이면 통과")
void testNullUrl() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
import com.rometools.rome.io.XmlReader;
import org.jdom2.Element;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.net.URL;

@DisplayName("Dev 환경 디버깅용 테스트")
@Tag("external")
class DevDebugTest {

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.rometools.rome.io.SyndFeedInput;
import com.rometools.rome.io.XmlReader;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.net.URL;
Expand All @@ -13,6 +14,7 @@
import static org.junit.jupiter.api.Assertions.*;

@DisplayName("RSS Feed 크롤링 테스트")
@Tag("external")
class FeedCrawlingServiceTest {

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.rometools.rome.io.SyndFeedInput;
import com.rometools.rome.io.XmlReader;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.lang.reflect.Method;
Expand All @@ -14,6 +15,7 @@
import static org.junit.jupiter.api.Assertions.assertTrue;

@DisplayName("Feed Description 추출 통합 테스트")
@Tag("external")
class FeedDescriptionExtractionTest {

@Test
Expand Down Expand Up @@ -202,4 +204,4 @@ void testAllSourcesDescriptionExtraction() throws Exception {
assertTrue((totalSuccess * 100.0 / totalTested) >= 80.0, "80% 이상의 성공률이 필요함");
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;

Expand All @@ -17,6 +18,7 @@
import static org.junit.jupiter.api.Assertions.*;

@DisplayName("Formula1 & ESPN 썸네일 추출 DSL 테스트")
@Tag("external")
@DisabledIfEnvironmentVariable(named = "CI", matches = "true",
disabledReason = "외부 RSS/웹 페이지 의존 통합 테스트는 CI 환경에서 불안정하여 로컬에서만 실행")
class Formula1EspnThumbnailTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.rometools.rome.io.SyndFeedInput;
import com.rometools.rome.io.XmlReader;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.net.URL;
Expand All @@ -13,6 +14,7 @@
import static org.junit.jupiter.api.Assertions.*;

@DisplayName("Medium RSS 피드 테스트")
@Tag("external")
class MediumRssTest {

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.rometools.rome.io.SyndFeedInput;
import com.rometools.rome.io.XmlReader;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;

Expand All @@ -15,6 +16,7 @@
import static org.junit.jupiter.api.Assertions.*;

@DisplayName("새로운 RSS Feed 소스 파싱 테스트 (Formula1, ESPN)")
@Tag("external")
@DisabledIfEnvironmentVariable(named = "CI", matches = "true",
disabledReason = "외부 RSS 의존 통합 테스트는 CI 환경에서 불안정하여 로컬에서만 실행")
class NewFeedSourcesTest {
Expand Down
Loading