Skip to content

에디터 유튜브 링크 자동 임베드#558

Merged
HA-SEUNG-JEONG merged 8 commits into
developfrom
feat/youtube-embedded
Apr 15, 2026
Merged

에디터 유튜브 링크 자동 임베드#558
HA-SEUNG-JEONG merged 8 commits into
developfrom
feat/youtube-embedded

Conversation

@HA-SEUNG-JEONG

@HA-SEUNG-JEONG HA-SEUNG-JEONG commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • 유튜브 링크를 에디터에 붙여넣으면 자동으로 embed iframe으로 변환
  • YouTubeEmbedExtension Tiptap 노드로 iframe을 안전한 블록 노드로 관리
  • 마크다운/HTML 컨텐츠 렌더링 시에도 standalone 유튜브 URL을 embed로 치환
  • CSP frame-srcwww.youtube-nocookie.com 허용 추가
  • 에디터 모듈 리팩토링: 훅 분리, 상수명 정규화, 스타일 상수 추출

Changes

Features

파일 설명
youtube-utils.ts YouTube URL 파싱, embed HTML 생성, standalone 링크 → iframe 치환 유틸
extensions.ts YouTubeEmbedExtension — iframe을 ProseMirror 블록 노드로 처리
markdown-editor.tsx 유튜브 URL 붙여넣기 시 자동 embed 변환
markdown-content-core.tsx 뷰어 렌더링 시 유튜브 iframe 속성 적용
markdown-sanitizer.ts sanitize 파이프라인에서 유효한 유튜브 iframe 보존
next.config.ts CSP frame-src에 youtube-nocookie.com 추가
global.css 유튜브 embed iframe 반응형 스타일 토큰 추가

Refactoring

파일 설명
use-active-code-block-control.ts 코드블록 위치 계산 로직을 전용 훅으로 분리
image-utils.ts IMAGE_MIME_TO_EXT / IMAGE_MIME_TO_EXTS 상수명 명확화
markdown-content-assets.ts PASS_THROUGH_PREFIXES 상수 추출, /images/ 분기 통합
markdown-content.tsx MARKDOWN_CONTENT_BASE_STYLES 모듈 레벨 상수 추출
youtube-utils.test.ts 테스트 설명 한국어화

⚠️ 2026-04-04-181546-implement-the-following-plan.txt — 계획 파일이 커밋에 포함되어 있습니다. 머지 전 삭제 커밋 추가를 권장합니다.

Test plan

  • 유튜브 watch URL (youtube.com/watch?v=...) 붙여넣기 → iframe embed로 변환 확인
  • youtu.be 단축 URL 붙여넣기 → embed 변환 확인
  • Shorts / Live URL 붙여넣기 → embed 변환 확인
  • 시작 시간(?t=90, ?start=43) 파라미터가 embed URL에 반영되는지 확인
  • 인라인(문장 중간) 유튜브 링크는 embed 변환 없이 그대로 유지되는지 확인
  • 뷰어(MarkdownContent)에서 기존 저장된 유튜브 iframe이 정상 렌더링되는지 확인
  • 비유튜브 iframe이 sanitize 단계에서 제거되는지 확인
  • 에디터 창 크기 조절 시 코드블록 언어 선택 드롭다운 위치가 올바른지 확인

🤖 Generated with Claude Code

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 마크다운 에디터와 콘텐츠에 YouTube 동영상 임베딩 기능 추가
    • 독립적인 YouTube 링크를 자동으로 임베드 형식으로 변환
    • YouTube 임베드 스타일링 개선 (16:9 비율, 모서리 둥글게, 선택 상태 표시)
  • Tests

    • YouTube 임베딩 기능에 대한 테스트 추가

HA-SEUNG-JEONG and others added 4 commits April 14, 2026 11:22
유튜브 embed CSP 허용(frame-src), 코드블록 위치 계산 훅 분리,
상수명 정규화(IMAGE_MIME_TO_EXT/EXTS), PASS_THROUGH_PREFIXES 추출,
스타일 상수 모듈 레벨로 분리, 테스트 설명 한국어화 포함

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
study-platform-client-dev Error Error Apr 15, 2026 3:44am

@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@HA-SEUNG-JEONG has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 26 minutes and 11 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 26 minutes and 11 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6ec6aa53-df9d-492d-a06c-74df22975ef1

📥 Commits

Reviewing files that changed from the base of the PR and between c233d88 and adf3c2e.

📒 Files selected for processing (8)
  • src/app/global.css
  • src/components/common/ui/editor/extensions.ts
  • src/components/common/ui/editor/image-utils.ts
  • src/components/common/ui/editor/markdown-content.tsx
  • src/components/common/ui/editor/markdown-editor.tsx
  • src/components/common/ui/editor/use-active-code-block-control.ts
  • src/components/common/ui/editor/youtube-utils.ts
  • src/components/common/ui/rich-text/markdown-content-core.tsx
📝 Walkthrough

Walkthrough

YouTube 임베드 기능이 마크다운 에디터와 콘텐츠 렌더링 경로에 추가되었습니다. 독립 실행형 YouTube URL을 감지하여 iframe 요소로 변환하고, CSP 정책을 업데이트하며, 새로운 유틸리티와 스타일링을 포함합니다.

Changes

Cohort / File(s) Summary
CSP 및 보안 설정
next.config.ts
YouTube 노쿠키 도메인(https://www.youtube-nocookie.com)을 CSP frame-src 지시문에 추가하여 iframe 임베드 허용
YouTube 유틸리티
src/components/common/ui/editor/youtube-utils.ts, src/components/common/ui/editor/youtube-utils.test.ts
YouTube URL 감지 및 정규화, 임베드 URL 생성, iframe 속성 빌드, 독립 실행형 링크를 HTML iframe으로 변환하는 로직과 해당 테스트 추가
에디터 확장 및 훅
src/components/common/ui/editor/extensions.ts, src/components/common/ui/editor/use-active-code-block-control.ts
TipTap YouTubeEmbedExtension 블록 노드 추가 및 새로운 useActiveCodeBlockControl 훅 추가(코드 블록 위치/언어 추적)
마크다운 에디터 로직
src/components/common/ui/editor/markdown-editor.tsx, src/components/common/ui/editor/markdown-content.tsx
YouTube URL 감지 및 임베드 삽입 기능, 클립보드 처리 개선, 콘텐츠 전처리 단계 추가
HTML 새니타이제이션
src/components/common/ui/editor/markdown-sanitizer.ts, src/components/common/ui/rich-text/markdown-content-core.tsx
DOMPurify 설정에서 iframe 및 관련 속성 허용, YouTube iframe 속성 후처리 단계 추가
유틸리티 및 스타일
src/components/common/ui/editor/image-utils.ts, src/components/common/ui/editor/markdown-content-assets.ts, src/app/global.css
상수 이름 변경(리팩토링), URL 해석 로직 단순화, YouTube iframe 스타일링 규칙 추가

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Editor as TipTap Editor
    participant Preprocessor as URL Preprocessor
    participant Sanitizer as HTML Sanitizer
    participant Renderer as DOM Renderer

    User->>Editor: Paste YouTube URL
    Editor->>Preprocessor: replaceStandaloneYouTubeLinksWithEmbeds(content)
    Preprocessor->>Preprocessor: extractYouTubeEmbedInfo(url)
    Preprocessor->>Preprocessor: createYouTubeEmbedHtml(url)
    Preprocessor-->>Editor: HTML with <iframe> markup
    Editor->>Sanitizer: Parse & sanitize HTML
    Sanitizer->>Sanitizer: Allow iframe + media attributes
    Sanitizer->>Sanitizer: applyYouTubeIframeAttributes(doc)
    Sanitizer-->>Editor: Sanitized HTML
    Editor->>Renderer: Render to DOM
    Renderer-->>User: Display YouTube embed with styling
Loading
sequenceDiagram
    participant User
    participant Editor as TipTap Editor
    participant Extension as YouTubeEmbedExtension
    participant Hook as useActiveCodeBlockControl

    User->>Editor: Focus editor / Select text
    Editor->>Hook: Track editor state + window resize
    Hook->>Hook: Measure code block position
    Hook-->>Editor: ActiveCodeBlockControl (position/language)
    
    User->>Editor: Paste YouTube URL (in text)
    Editor->>Extension: insertYouTubeEmbed()
    Extension->>Extension: Validate selection (not in codeBlock)
    Extension->>Editor: Insert youtubeEmbed node + paragraph
    Editor-->>User: Display embedded YouTube iframe
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 ✨ YouTube의 밤, 마크다운 숲에서
iframe의 스타일이 춤을 추네,
URL 파서가 링크를 찾아내고,
새니타이저가 안전하게 지키며,
에디터의 노드는 비디오를 품네. 🎬

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 '에디터 유튜브 링크 자동 임베드'로 변경사항의 핵심(유튜브 링크를 자동으로 iframe으로 변환)을 정확하고 간결하게 설명하고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/youtube-embedded

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/common/ui/rich-text/markdown-content-core.tsx (1)

210-221: ⚠️ Potential issue | 🟠 Major

유튜브 치환 후에 나머지 Markdown 파싱이 건너뛰어집니다.

replaceStandaloneYouTubeLinksWithEmbeds()가 standalone URL을 <iframe>로 바꾸면, Line 214의 isHtmlContent(contentWithEmbeds)가 바로 true가 됩니다. 그래서 **bold**, 리스트, 헤딩 같은 주변 Markdown이 더 이상 marked.parse()를 거치지 못하고 평문으로 남습니다. HTML 여부는 원본 content 기준으로 판단하고, 원본이 Markdown이었다면 치환 후 문자열도 그대로 marked.parse()에 넣는 쪽이 맞습니다.

🔧 Proposed fix
-    const contentWithEmbeds = replaceStandaloneYouTubeLinksWithEmbeds(content);
+    const isOriginalHtml = isHtmlContent(content);
+    const contentWithEmbeds = replaceStandaloneYouTubeLinksWithEmbeds(content);

     let html: string;

-    if (isHtmlContent(contentWithEmbeds)) {
+    if (isOriginalHtml) {
       html = contentWithEmbeds;
     } else {
       const rendered = marked.parse(contentWithEmbeds, {
         breaks: true,
         gfm: true,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/ui/rich-text/markdown-content-core.tsx` around lines
210 - 221, The current logic checks isHtmlContent against contentWithEmbeds so
embeds converted to <iframe> short-circuit Markdown parsing; change the branch
to determine HTML-ness from the original content (use isHtmlContent(content))
and then: if original content is HTML set html = contentWithEmbeds, otherwise
run marked.parse on contentWithEmbeds (using marked.parse options currently
present) and assign the resulting string to html; update references to
contentWithEmbeds, content, isHtmlContent,
replaceStandaloneYouTubeLinksWithEmbeds, and marked.parse accordingly.
🧹 Nitpick comments (1)
src/components/common/ui/editor/use-active-code-block-control.ts (1)

51-53: language 읽기는 단언보다 가드가 낫습니다.

여기서는 런타임 데이터에 as string | undefined를 씌우고 있어서, 예상 밖 값이 들어와도 타입 시스템만 조용히 통과합니다. 속성 존재 여부를 가드한 뒤 fallback을 두는 쪽이 이 코드베이스 규칙에도 맞습니다.

As per coding guidelines, "Use in guard with fallback instead of as type assertion for enum-like string type assertions when the backend may send unknown values. TypeScript as does not protect at runtime."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/ui/editor/use-active-code-block-control.ts` around
lines 51 - 53, Replace the unsafe assertion on
editor.getAttributes('codeBlock').language with a runtime guard: read attrs =
editor.getAttributes('codeBlock'), check that attrs has a "language" key and
that typeof attrs.language === 'string' (or use the 'in' operator) and only then
assign to the local variable language; otherwise set language = 'plaintext'.
Update any code using the current language constant to use this guarded value
(references: editor.getAttributes('codeBlock'), language).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/global.css`:
- Around line 1123-1141: Replace hardcoded spacing/radius/outline pixel values
in the selectors .tiptap-editor .tiptap iframe.youtube-embed, .tiptap-editor
.tiptap img.ProseMirror-selectednode, and .tiptap-editor .tiptap
iframe.youtube-embed.ProseMirror-selectednode with theme tokens: change
border-radius: 8px to border-radius: var(--radius-100), margin-bottom: 12px to
margin-bottom: var(--spacing-150), outline: 2px solid var(--color-border-brand)
to outline: var(--border-width-100) solid var(--color-border-brand) (or the
project border-width token), and outline-offset: 2px to outline-offset:
var(--spacing-25); after edits run yarn prettier:fix to apply project formatting
rules.

In `@src/components/common/ui/editor/extensions.ts`:
- Around line 149-151: The youtubeEmbed atom node fallback currently returns
['p', 0], which inserts a content hole and causes RangeError since youtubeEmbed
is a leaf atom; change the fallback to return a leaf-safe DOM spec without a
content hole (for example ['p'] or ['span'] with no hole) in the branch that
checks if (!attrs) so the youtubeEmbed node returns a valid leaf element rather
than an array containing a content hole.

In `@src/components/common/ui/editor/image-utils.ts`:
- Around line 111-116: The current getExtensionFromMime uses an unsafe type
assertion (mimeType as SupportedImageMimeType); instead call or use the existing
normalizeImageMimeType to first narrow/validate the mimeType (or perform an
`in`-style guard against IMAGE_MIME_TO_EXT keys) and then lookup
IMAGE_MIME_TO_EXT using the narrowed value, falling back to
mimeType.split('/')[1]?.toLowerCase() ?? ''. Update getExtensionFromMime to
accept the normalized/guarded mime string (or perform the guard inline) so you
remove the `as` assertion and safely handle unknown backend values.

In `@src/components/common/ui/editor/markdown-content.tsx`:
- Around line 58-67: isHtmlContent is being called after
replaceStandaloneYouTubeLinksWithEmbeds, so an injected <iframe> can make it
return true and skip Markdown parsing; instead, determine HTML-ness from the
original normalizedContent and then decide rendering. Change the flow so you
call isHtmlContent(normalizedContent) first, store the result (e.g., isHtml),
then compute normalizedContentWithEmbeds =
replaceStandaloneYouTubeLinksWithEmbeds(normalizedContent) and if isHtml set
html = normalizedContentWithEmbeds else set html =
marked.parse(normalizedContentWithEmbeds, { breaks: true }) so Markdown is only
skipped when the original content is actually HTML; reference
variables/functions: normalizedContent, normalizedContentWithEmbeds,
replaceStandaloneYouTubeLinksWithEmbeds, isHtmlContent, marked.parse.

In `@src/components/common/ui/editor/use-active-code-block-control.ts`:
- Around line 20-60: The memoization prevents recalculation of code-block
control coordinates; remove the useMemo wrapper and compute the returned value
each render instead. Specifically, replace the "return useMemo(() => { ... },
[editor, wrapperRef])" with the same body executed directly and return the
result (keep the existing resize forceUpdate state and effect), ensuring you
still reference editor.isActive('codeBlock'), wrapperRef.current,
editor.state.selection ($from), $from.before(),
editor.view.nodeDOM(codeBlockPos), and editor.getAttributes('codeBlock') to
derive language, top, and left; do not rely on useMemo or its dependency array
so coordinates update on selection/language/resize changes.

---

Outside diff comments:
In `@src/components/common/ui/rich-text/markdown-content-core.tsx`:
- Around line 210-221: The current logic checks isHtmlContent against
contentWithEmbeds so embeds converted to <iframe> short-circuit Markdown
parsing; change the branch to determine HTML-ness from the original content (use
isHtmlContent(content)) and then: if original content is HTML set html =
contentWithEmbeds, otherwise run marked.parse on contentWithEmbeds (using
marked.parse options currently present) and assign the resulting string to html;
update references to contentWithEmbeds, content, isHtmlContent,
replaceStandaloneYouTubeLinksWithEmbeds, and marked.parse accordingly.

---

Nitpick comments:
In `@src/components/common/ui/editor/use-active-code-block-control.ts`:
- Around line 51-53: Replace the unsafe assertion on
editor.getAttributes('codeBlock').language with a runtime guard: read attrs =
editor.getAttributes('codeBlock'), check that attrs has a "language" key and
that typeof attrs.language === 'string' (or use the 'in' operator) and only then
assign to the local variable language; otherwise set language = 'plaintext'.
Update any code using the current language constant to use this guarded value
(references: editor.getAttributes('codeBlock'), language).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 40ee5e84-767f-4594-ac43-818efdcee17c

📥 Commits

Reviewing files that changed from the base of the PR and between 475abce and c233d88.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (12)
  • next.config.ts
  • src/app/global.css
  • src/components/common/ui/editor/extensions.ts
  • src/components/common/ui/editor/image-utils.ts
  • src/components/common/ui/editor/markdown-content-assets.ts
  • src/components/common/ui/editor/markdown-content.tsx
  • src/components/common/ui/editor/markdown-editor.tsx
  • src/components/common/ui/editor/markdown-sanitizer.ts
  • src/components/common/ui/editor/use-active-code-block-control.ts
  • src/components/common/ui/editor/youtube-utils.test.ts
  • src/components/common/ui/editor/youtube-utils.ts
  • src/components/common/ui/rich-text/markdown-content-core.tsx

Comment thread src/app/global.css
Comment thread src/components/common/ui/editor/extensions.ts
Comment thread src/components/common/ui/editor/image-utils.ts
Comment thread src/components/common/ui/editor/markdown-content.tsx
Comment thread src/components/common/ui/editor/use-active-code-block-control.ts Outdated
@HA-SEUNG-JEONG HA-SEUNG-JEONG merged commit a119575 into develop Apr 15, 2026
8 of 9 checks passed
@HA-SEUNG-JEONG HA-SEUNG-JEONG deleted the feat/youtube-embedded branch April 15, 2026 03:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant