From 3fda9077a61d99ca4ec7b054542067ba505e7ff4 Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Mon, 3 Aug 2026 11:58:17 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9C=A0=ED=8A=9C=EB=B8=8C=20URL=20vid?= =?UTF-8?q?eoId=20=EC=B6=94=EC=B6=9C=20=EC=9C=A0=ED=8B=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit watch/shorts/embed/live/youtu.be 지원, 11자 ID 검증. Co-Authored-By: Claude Fable 5 --- .../domain/shortform/YoutubeVideoId.java | 21 +++++++++++++++ .../domain/shortform/YoutubeVideoIdTest.java | 27 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 src/main/java/com/hlab/yanalyst/domain/shortform/YoutubeVideoId.java create mode 100644 src/test/java/com/hlab/yanalyst/domain/shortform/YoutubeVideoIdTest.java diff --git a/src/main/java/com/hlab/yanalyst/domain/shortform/YoutubeVideoId.java b/src/main/java/com/hlab/yanalyst/domain/shortform/YoutubeVideoId.java new file mode 100644 index 0000000..3ec98fa --- /dev/null +++ b/src/main/java/com/hlab/yanalyst/domain/shortform/YoutubeVideoId.java @@ -0,0 +1,21 @@ +package com.hlab.yanalyst.domain.shortform; + +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** 유튜브 URL에서 11자 videoId를 뽑는다. youtube.com / youtu.be 도메인만 인정. */ +public final class YoutubeVideoId { + + private static final Pattern PATTERN = Pattern.compile( + "^https?://(?:www\\.|m\\.)?(?:youtube\\.com/(?:watch\\?[^#]*\\bv=|shorts/|embed/|live/)|youtu\\.be/)" + + "([A-Za-z0-9_-]{11})(?:[?&#/].*)?$"); + + private YoutubeVideoId() {} + + public static Optional from(String url) { + if (url == null || url.isBlank()) return Optional.empty(); + Matcher m = PATTERN.matcher(url.trim()); + return m.matches() ? Optional.of(m.group(1)) : Optional.empty(); + } +} diff --git a/src/test/java/com/hlab/yanalyst/domain/shortform/YoutubeVideoIdTest.java b/src/test/java/com/hlab/yanalyst/domain/shortform/YoutubeVideoIdTest.java new file mode 100644 index 0000000..448f915 --- /dev/null +++ b/src/test/java/com/hlab/yanalyst/domain/shortform/YoutubeVideoIdTest.java @@ -0,0 +1,27 @@ +package com.hlab.yanalyst.domain.shortform; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class YoutubeVideoIdTest { + + @Test + void 다양한_유튜브_url_형식에서_videoId를_추출한다() { + assertEquals("JW5mwZ8RsG8", YoutubeVideoId.from("https://www.youtube.com/watch?v=JW5mwZ8RsG8").orElseThrow()); + assertEquals("JW5mwZ8RsG8", YoutubeVideoId.from("https://www.youtube.com/watch?v=JW5mwZ8RsG8&t=10s").orElseThrow()); + assertEquals("JW5mwZ8RsG8", YoutubeVideoId.from("https://youtu.be/JW5mwZ8RsG8").orElseThrow()); + assertEquals("JW5mwZ8RsG8", YoutubeVideoId.from("https://www.youtube.com/shorts/JW5mwZ8RsG8").orElseThrow()); + assertEquals("JW5mwZ8RsG8", YoutubeVideoId.from("https://www.youtube.com/embed/JW5mwZ8RsG8").orElseThrow()); + assertEquals("JW5mwZ8RsG8", YoutubeVideoId.from("https://www.youtube.com/live/JW5mwZ8RsG8?feature=share").orElseThrow()); + } + + @Test + void 유튜브가_아니거나_id가_없으면_empty() { + assertTrue(YoutubeVideoId.from(null).isEmpty()); + assertTrue(YoutubeVideoId.from("").isEmpty()); + assertTrue(YoutubeVideoId.from("https://example.com/watch?v=JW5mwZ8RsG8").isEmpty()); + assertTrue(YoutubeVideoId.from("https://www.youtube.com/watch").isEmpty()); + assertTrue(YoutubeVideoId.from("https://www.youtube.com/watch?v=short").isEmpty()); + } +}