diff --git a/README.md b/README.md index b49e1b0..da90303 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,33 @@ Ling Level API는 모바일 영어 학습 앱을 위한 Spring Boot 기반 백 ## 주요 기능 -- 책, 아티클, 커스텀 콘텐츠 기반 학습 흐름 제공 -- 단어 조회, 변형어 저장, Spring AI와 AWS Bedrock 기반 단어 분석 -- 학습 진행도와 일 단위 스트릭 계산 -- 추천, 배너, 북마크, FCM 푸시 알림 -- 관리자용 콘텐츠, 크롤링, 마이그레이션 API +- CEFR 레벨별 영어 독해 콘텐츠 제공 (문학, 아티클, 웹 콘텐츠) +- 단어 사전 검색 및 개인 단어장 저장 +- 사용자 정의 콘텐츠 레벨링 기능 +- 스트릭 기반 연속 학습 기록 - Prometheus, Grafana, Sentry 기반 모니터링 +## 미리보기 + +
+앱 화면 미리보기 + +| 콘텐츠 레벨링 | 단어 사전 | +| --- | --- | +| 콘텐츠 레벨링 화면 | 단어 사전 검색 화면 | + +| 사용자 정의 콘텐츠 | 연속 학습 기록 | +| --- | --- | +| 사용자 정의 콘텐츠 화면 | 스트릭 기반 연속 학습 기록 화면 | + +
+ +## 시스템 구조 + +

+ Ling Level API 시스템 구조 +

+ ## 기술 스택 - Java 17, Spring Boot 3.5, Spring Security diff --git a/build.gradle b/build.gradle index aff278b..b741c40 100644 --- a/build.gradle +++ b/build.gradle @@ -71,5 +71,9 @@ dependencies { } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform { + if (!project.hasProperty('includeExternalTests')) { + excludeTags 'external' + } + } } diff --git a/docs/assets/readme/architecture.png b/docs/assets/readme/architecture.png new file mode 100644 index 0000000..5431cfb Binary files /dev/null and b/docs/assets/readme/architecture.png differ diff --git a/docs/assets/readme/leveling.png b/docs/assets/readme/leveling.png new file mode 100644 index 0000000..ac0508c Binary files /dev/null and b/docs/assets/readme/leveling.png differ diff --git a/docs/assets/readme/streak.png b/docs/assets/readme/streak.png new file mode 100644 index 0000000..b943f01 Binary files /dev/null and b/docs/assets/readme/streak.png differ diff --git a/docs/assets/readme/user_content.png b/docs/assets/readme/user_content.png new file mode 100644 index 0000000..1224f2d Binary files /dev/null and b/docs/assets/readme/user_content.png differ diff --git a/docs/assets/readme/word.png b/docs/assets/readme/word.png new file mode 100644 index 0000000..9fb9d54 Binary files /dev/null and b/docs/assets/readme/word.png differ diff --git a/src/test/java/com/linglevel/api/content/feed/filter/filters/YouTubeRssStructureTest.java b/src/test/java/com/linglevel/api/content/feed/filter/filters/YouTubeRssStructureTest.java index de94103..de4e7f5 100644 --- a/src/test/java/com/linglevel/api/content/feed/filter/filters/YouTubeRssStructureTest.java +++ b/src/test/java/com/linglevel/api/content/feed/filter/filters/YouTubeRssStructureTest.java @@ -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 diff --git a/src/test/java/com/linglevel/api/content/feed/filter/filters/YouTubeShortsFilterTest.java b/src/test/java/com/linglevel/api/content/feed/filter/filters/YouTubeShortsFilterTest.java index 67ba2e9..65cedd9 100644 --- a/src/test/java/com/linglevel/api/content/feed/filter/filters/YouTubeShortsFilterTest.java +++ b/src/test/java/com/linglevel/api/content/feed/filter/filters/YouTubeShortsFilterTest.java @@ -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; @@ -27,6 +28,7 @@ void setUp() { } @Test + @Tag("external") @DisplayName("실제 YouTube RSS 피드에서 duration 추출 및 Shorts 필터링 테스트") void testRealYouTubeFeedWithDuration() throws Exception { // given: Kurzgesagt YouTube 채널 RSS 피드 @@ -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() { diff --git a/src/test/java/com/linglevel/api/content/feed/service/DevDebugTest.java b/src/test/java/com/linglevel/api/content/feed/service/DevDebugTest.java index 16f9949..229fe7b 100644 --- a/src/test/java/com/linglevel/api/content/feed/service/DevDebugTest.java +++ b/src/test/java/com/linglevel/api/content/feed/service/DevDebugTest.java @@ -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 diff --git a/src/test/java/com/linglevel/api/content/feed/service/FeedCrawlingServiceTest.java b/src/test/java/com/linglevel/api/content/feed/service/FeedCrawlingServiceTest.java index 60e4f5a..d1e0b04 100644 --- a/src/test/java/com/linglevel/api/content/feed/service/FeedCrawlingServiceTest.java +++ b/src/test/java/com/linglevel/api/content/feed/service/FeedCrawlingServiceTest.java @@ -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; @@ -13,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.*; @DisplayName("RSS Feed 크롤링 테스트") +@Tag("external") class FeedCrawlingServiceTest { @Test diff --git a/src/test/java/com/linglevel/api/content/feed/service/FeedDescriptionExtractionTest.java b/src/test/java/com/linglevel/api/content/feed/service/FeedDescriptionExtractionTest.java index b5e05c4..c33646c 100644 --- a/src/test/java/com/linglevel/api/content/feed/service/FeedDescriptionExtractionTest.java +++ b/src/test/java/com/linglevel/api/content/feed/service/FeedDescriptionExtractionTest.java @@ -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; @@ -14,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; @DisplayName("Feed Description 추출 통합 테스트") +@Tag("external") class FeedDescriptionExtractionTest { @Test @@ -202,4 +204,4 @@ void testAllSourcesDescriptionExtraction() throws Exception { assertTrue((totalSuccess * 100.0 / totalTested) >= 80.0, "80% 이상의 성공률이 필요함"); } -} \ No newline at end of file +} diff --git a/src/test/java/com/linglevel/api/content/feed/service/Formula1EspnThumbnailTest.java b/src/test/java/com/linglevel/api/content/feed/service/Formula1EspnThumbnailTest.java index 62b0693..a2effdb 100644 --- a/src/test/java/com/linglevel/api/content/feed/service/Formula1EspnThumbnailTest.java +++ b/src/test/java/com/linglevel/api/content/feed/service/Formula1EspnThumbnailTest.java @@ -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; @@ -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 { diff --git a/src/test/java/com/linglevel/api/content/feed/service/MediumRssTest.java b/src/test/java/com/linglevel/api/content/feed/service/MediumRssTest.java index 349b14d..06759f8 100644 --- a/src/test/java/com/linglevel/api/content/feed/service/MediumRssTest.java +++ b/src/test/java/com/linglevel/api/content/feed/service/MediumRssTest.java @@ -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; @@ -13,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.*; @DisplayName("Medium RSS 피드 테스트") +@Tag("external") class MediumRssTest { @Test diff --git a/src/test/java/com/linglevel/api/content/feed/service/NewFeedSourcesTest.java b/src/test/java/com/linglevel/api/content/feed/service/NewFeedSourcesTest.java index 7086f2d..3a9ad8d 100644 --- a/src/test/java/com/linglevel/api/content/feed/service/NewFeedSourcesTest.java +++ b/src/test/java/com/linglevel/api/content/feed/service/NewFeedSourcesTest.java @@ -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; @@ -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 {