video 블록 및 리펙터링
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
import type { CSSProperties } from "react";
|
||||
import type { VideoBlockProps } from "@/features/editor/state/editorStore";
|
||||
|
||||
export type VideoPlatform = "youtube" | "vimeo" | "html5";
|
||||
|
||||
export type VideoPlatformInput = VideoPlatform | "auto" | null | undefined;
|
||||
|
||||
// 비디오 sourceUrl 을 정규화한다: null/undefined 를 빈 문자열로 만들고 양 끝 공백을 제거한다.
|
||||
export function normalizeVideoSourceUrl(source: string | null | undefined): string {
|
||||
if (!source) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return source.trim();
|
||||
}
|
||||
|
||||
// platform 값과 URL 을 기반으로 최종 비디오 플랫폼을 결정한다.
|
||||
// - platform 이 auto 가 아니면 해당 값을 우선 사용한다.
|
||||
// - auto 인 경우 URL 을 검사해 youtube/vimeo/html5 를 판별한다.
|
||||
export function resolveVideoPlatform(rawUrl: string, platform: VideoPlatformInput): VideoPlatform {
|
||||
if (platform && platform !== "auto") {
|
||||
return platform;
|
||||
}
|
||||
|
||||
const normalized = normalizeVideoSourceUrl(rawUrl);
|
||||
const lower = normalized.toLowerCase();
|
||||
|
||||
if (lower.includes("youtube.com") || lower.includes("youtu.be")) {
|
||||
return "youtube";
|
||||
}
|
||||
|
||||
if (lower.includes("vimeo.com")) {
|
||||
return "vimeo";
|
||||
}
|
||||
|
||||
return "html5";
|
||||
}
|
||||
|
||||
export type BuildEmbedUrlOptions = {
|
||||
enableVimeoEmbed?: boolean;
|
||||
};
|
||||
|
||||
// 플랫폼과 URL 을 기반으로 최종 embed URL 을 생성한다.
|
||||
// - YouTube: 항상 watch/short/youtu.be 형태에서 embed URL 로 변환.
|
||||
// - Vimeo: enableVimeoEmbed 가 true 인 경우에만 player.vimeo.com 기반 embed URL 로 변환.
|
||||
// - HTML5: 항상 정규화된 원본 URL 을 그대로 사용.
|
||||
export function buildVideoEmbedUrl(
|
||||
rawUrl: string,
|
||||
platform: VideoPlatform,
|
||||
options?: BuildEmbedUrlOptions,
|
||||
): string {
|
||||
const normalized = normalizeVideoSourceUrl(rawUrl);
|
||||
|
||||
if (platform === "youtube") {
|
||||
const idMatch =
|
||||
normalized.match(/[?&]v=([^&]+)/) ||
|
||||
normalized.match(/youtu\.be\/([^?&]+)/) ||
|
||||
normalized.match(/youtube\.com\/shorts\/([^?&]+)/);
|
||||
const videoId = idMatch && idMatch[1] ? idMatch[1] : "";
|
||||
if (videoId) {
|
||||
return `https://www.youtube.com/embed/${videoId}`;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (platform === "vimeo" && options?.enableVimeoEmbed) {
|
||||
const idMatch = normalized.match(/vimeo\.com\/(\d+)/);
|
||||
const videoId = idMatch && idMatch[1] ? idMatch[1] : "";
|
||||
if (videoId) {
|
||||
return `https://player.vimeo.com/video/${videoId}`;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export interface VideoExportEscapers {
|
||||
escapeAttr: (value: string) => string;
|
||||
}
|
||||
|
||||
export interface VideoExportTokens {
|
||||
wrapperStyleParts: string[];
|
||||
videoStyleParts: string[];
|
||||
outerStyleParts: string[];
|
||||
aspectClass: string;
|
||||
}
|
||||
|
||||
export function computeVideoExportTokens(
|
||||
props: VideoBlockProps,
|
||||
escapers: VideoExportEscapers,
|
||||
): VideoExportTokens {
|
||||
const { escapeAttr } = escapers;
|
||||
|
||||
const aspect = props.aspectRatio ?? "16:9";
|
||||
const aspectClass =
|
||||
aspect === "4:3" ? " pb-video-wrapper--4by3" : aspect === "1:1" ? " pb-video-wrapper--1by1" : "";
|
||||
|
||||
const wrapperStyleParts: string[] = [];
|
||||
const videoStyleParts: string[] = [];
|
||||
|
||||
const widthMode = props.widthMode ?? "auto";
|
||||
if (widthMode === "fixed" && typeof props.widthPx === "number" && props.widthPx > 0) {
|
||||
wrapperStyleParts.push(`width:${props.widthPx}px`);
|
||||
videoStyleParts.push(`width:${props.widthPx}px`);
|
||||
} else if (widthMode === "full") {
|
||||
wrapperStyleParts.push("width:100%");
|
||||
videoStyleParts.push("width:100%");
|
||||
}
|
||||
|
||||
if (typeof props.backgroundColorCustom === "string" && props.backgroundColorCustom.trim() !== "") {
|
||||
wrapperStyleParts.push(`background-color:${escapeAttr(props.backgroundColorCustom.trim())}`);
|
||||
}
|
||||
|
||||
if (typeof props.cardPaddingPx === "number" && props.cardPaddingPx >= 0) {
|
||||
wrapperStyleParts.push(`padding:${props.cardPaddingPx}px`);
|
||||
}
|
||||
|
||||
if (typeof props.borderRadiusPx === "number" && props.borderRadiusPx >= 0) {
|
||||
wrapperStyleParts.push(`border-radius:${props.borderRadiusPx}px`);
|
||||
}
|
||||
|
||||
const align =
|
||||
props.align === "left" ? "flex-start" : props.align === "right" ? "flex-end" : "center";
|
||||
const outerStyleParts = ["display:flex", `justify-content:${align}`];
|
||||
|
||||
return {
|
||||
wrapperStyleParts,
|
||||
videoStyleParts,
|
||||
outerStyleParts,
|
||||
aspectClass,
|
||||
};
|
||||
}
|
||||
|
||||
export interface VideoPublicTokens {
|
||||
wrapperStyle: CSSProperties;
|
||||
videoStyle: CSSProperties;
|
||||
justifyClass: string;
|
||||
aspectClass: string;
|
||||
}
|
||||
|
||||
const pxToEm = (px: number, base = 16) => `${px / base}em`;
|
||||
|
||||
export function computeVideoPublicTokens(props: VideoBlockProps): VideoPublicTokens {
|
||||
const aspect = props.aspectRatio ?? "16:9";
|
||||
const aspectClass =
|
||||
aspect === "4:3" ? " pb-video-wrapper--4by3" : aspect === "1:1" ? " pb-video-wrapper--1by1" : "";
|
||||
|
||||
const wrapperStyle: CSSProperties = {};
|
||||
const videoStyle: CSSProperties = {};
|
||||
|
||||
const widthMode = props.widthMode ?? "auto";
|
||||
if (widthMode === "fixed" && typeof props.widthPx === "number" && props.widthPx > 0) {
|
||||
const widthEm = pxToEm(props.widthPx);
|
||||
wrapperStyle.width = widthEm;
|
||||
videoStyle.width = widthEm;
|
||||
} else if (widthMode === "full") {
|
||||
wrapperStyle.width = "100%";
|
||||
videoStyle.width = "100%";
|
||||
}
|
||||
|
||||
if (props.backgroundColorCustom && props.backgroundColorCustom.trim() !== "") {
|
||||
wrapperStyle.backgroundColor = props.backgroundColorCustom.trim();
|
||||
}
|
||||
|
||||
if (typeof (props as any).cardPaddingPx === "number" && (props as any).cardPaddingPx >= 0) {
|
||||
wrapperStyle.padding = pxToEm((props as any).cardPaddingPx as number);
|
||||
}
|
||||
|
||||
if (typeof (props as any).borderRadiusPx === "number" && (props as any).borderRadiusPx >= 0) {
|
||||
wrapperStyle.borderRadius = pxToEm((props as any).borderRadiusPx as number);
|
||||
}
|
||||
|
||||
const align = props.align ?? "center";
|
||||
const justifyClass =
|
||||
align === "left" ? "justify-start" : align === "right" ? "justify-end" : "justify-center";
|
||||
|
||||
return {
|
||||
wrapperStyle,
|
||||
videoStyle,
|
||||
justifyClass,
|
||||
aspectClass,
|
||||
};
|
||||
}
|
||||
|
||||
export interface VideoEditorTokens {
|
||||
wrapperStyle: CSSProperties;
|
||||
videoStyle: CSSProperties;
|
||||
justifyClass: string;
|
||||
aspectClass: string;
|
||||
}
|
||||
|
||||
export function computeVideoEditorTokens(props: VideoBlockProps): VideoEditorTokens {
|
||||
const aspect = props.aspectRatio ?? "16:9";
|
||||
const aspectClass =
|
||||
aspect === "4:3" ? " pb-video-wrapper--4by3" : aspect === "1:1" ? " pb-video-wrapper--1by1" : "";
|
||||
|
||||
const wrapperStyle: CSSProperties = {};
|
||||
const videoStyle: CSSProperties = {};
|
||||
|
||||
const widthMode = props.widthMode ?? "auto";
|
||||
if (widthMode === "fixed" && typeof props.widthPx === "number" && props.widthPx > 0) {
|
||||
wrapperStyle.width = `${props.widthPx}px`;
|
||||
videoStyle.width = `${props.widthPx}px`;
|
||||
} else if (widthMode === "full") {
|
||||
wrapperStyle.width = "100%";
|
||||
videoStyle.width = "100%";
|
||||
}
|
||||
|
||||
if (props.backgroundColorCustom && props.backgroundColorCustom.trim() !== "") {
|
||||
wrapperStyle.backgroundColor = props.backgroundColorCustom.trim();
|
||||
}
|
||||
if (typeof props.cardPaddingPx === "number" && props.cardPaddingPx >= 0) {
|
||||
wrapperStyle.padding = `${props.cardPaddingPx}px`;
|
||||
}
|
||||
if (typeof props.borderRadiusPx === "number" && props.borderRadiusPx >= 0) {
|
||||
wrapperStyle.borderRadius = `${props.borderRadiusPx}px`;
|
||||
}
|
||||
|
||||
const align = props.align ?? "center";
|
||||
const justifyClass =
|
||||
align === "left" ? "justify-start" : align === "right" ? "justify-end" : "justify-center";
|
||||
|
||||
return {
|
||||
wrapperStyle,
|
||||
videoStyle,
|
||||
justifyClass,
|
||||
aspectClass,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user