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 기반 모니터링
+## 미리보기
+
+
+앱 화면 미리보기
+
+| 콘텐츠 레벨링 | 단어 사전 |
+| --- | --- |
+|
|
|
+
+| 사용자 정의 콘텐츠 | 연속 학습 기록 |
+| --- | --- |
+|
|
|
+
+
+
+## 시스템 구조
+
+
+
+
+
## 기술 스택
- 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 {