130 lines
3.3 KiB
Vue
130 lines
3.3 KiB
Vue
<script setup>
|
|
const props = defineProps({
|
|
url: {
|
|
type: String,
|
|
default: ''
|
|
}
|
|
})
|
|
|
|
const { theme } = useThemeMode()
|
|
|
|
/**
|
|
* YouTube 영상 ID를 추출한다.
|
|
* @param {string} value - 임베드 URL
|
|
* @returns {string} YouTube 영상 ID
|
|
*/
|
|
const getYouTubeId = (value) => {
|
|
try {
|
|
const parsedUrl = new URL(value)
|
|
|
|
if (parsedUrl.hostname.includes('youtu.be')) {
|
|
return parsedUrl.pathname.replace('/', '')
|
|
}
|
|
|
|
if (parsedUrl.hostname.includes('youtube.com')) {
|
|
return parsedUrl.searchParams.get('v') || parsedUrl.pathname.split('/').pop() || ''
|
|
}
|
|
} catch {
|
|
return ''
|
|
}
|
|
|
|
return ''
|
|
}
|
|
|
|
/**
|
|
* Twitter/X 게시물 ID를 추출한다.
|
|
* @param {string} value - 트윗 URL
|
|
* @returns {string} 상태 ID
|
|
*/
|
|
const getTweetId = (value) => {
|
|
try {
|
|
const trimmed = value.trim()
|
|
const parsedUrl = new URL(trimmed)
|
|
const host = parsedUrl.hostname.replace(/^www\./, '')
|
|
|
|
if (!['twitter.com', 'x.com', 'mobile.twitter.com'].includes(host)) {
|
|
return ''
|
|
}
|
|
|
|
const parts = parsedUrl.pathname.split('/').filter(Boolean)
|
|
const statusIdx = parts.indexOf('status')
|
|
|
|
if (statusIdx >= 0 && parts[statusIdx + 1]) {
|
|
return parts[statusIdx + 1].split(/[?#]/)[0] || ''
|
|
}
|
|
} catch {
|
|
return ''
|
|
}
|
|
|
|
return ''
|
|
}
|
|
|
|
const youtubeId = computed(() => getYouTubeId(props.url))
|
|
const youtubeEmbedUrl = computed(() => youtubeId.value ? `https://www.youtube.com/embed/${youtubeId.value}` : '')
|
|
const tweetId = computed(() => getTweetId(props.url))
|
|
|
|
/**
|
|
* 외부 링크로 열어도 되는 URL인지 확인한다.
|
|
* @param {string} value - 검사할 URL
|
|
* @returns {boolean} 허용 여부
|
|
*/
|
|
const isSafeExternalUrl = (value) => {
|
|
try {
|
|
const parsedUrl = new URL(value)
|
|
return ['http:', 'https:'].includes(parsedUrl.protocol)
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
const safeExternalUrl = computed(() => isSafeExternalUrl(props.url) ? props.url : '')
|
|
|
|
/**
|
|
* Twitter 공식 embed iframe 주소
|
|
* @returns {string}
|
|
*/
|
|
const tweetEmbedUrl = computed(() => {
|
|
if (!tweetId.value) {
|
|
return ''
|
|
}
|
|
|
|
const twitterTheme = theme.value === 'dark' ? 'dark' : 'light'
|
|
|
|
return `https://platform.twitter.com/embed/Tweet.html?id=${encodeURIComponent(tweetId.value)}&theme=${twitterTheme}&dnt=true`
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="prose-embed prose-embed-card my-8 overflow-hidden rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)]">
|
|
<iframe
|
|
v-if="youtubeEmbedUrl"
|
|
class="prose-embed__frame aspect-video w-full"
|
|
:src="youtubeEmbedUrl"
|
|
title="Embedded video"
|
|
loading="lazy"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
allowfullscreen
|
|
/>
|
|
<iframe
|
|
v-else-if="tweetEmbedUrl"
|
|
:key="tweetEmbedUrl"
|
|
class="prose-embed__tweet min-h-[420px] w-full border-0 sm:min-h-[458px]"
|
|
:src="tweetEmbedUrl"
|
|
title="Embedded post"
|
|
loading="lazy"
|
|
/>
|
|
<a
|
|
v-else-if="safeExternalUrl"
|
|
class="prose-embed__link block p-5 text-sm font-semibold text-[var(--site-text)] hover:opacity-70"
|
|
:href="safeExternalUrl"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
{{ url }}
|
|
</a>
|
|
<p v-else class="prose-embed__invalid p-5 text-sm font-semibold text-[var(--site-muted)]">
|
|
지원하지 않는 임베드 URL입니다.
|
|
</p>
|
|
</div>
|
|
</template>
|