UNPKG

@restnfeel/agentc-starter-kit

Version:

한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템

306 lines (271 loc) 8.1 kB
import { useCallback, useRef } from "react"; interface MediaFile { id: string; fileName: string; originalName: string; mimeType: string; size: number; url: string; cdnUrl?: string; thumbnailUrl?: string; createdAt: string; tags: string[]; description?: string; dimensions?: { width: number; height: number; }; } interface UseEditorMediaDropProps { onMediaInsert: (html: string, media: MediaFile) => void; onUploadStart?: () => void; onUploadProgress?: (progress: number) => void; onUploadComplete?: (media: MediaFile) => void; onUploadError?: (error: string) => void; acceptedTypes?: string[]; maxFileSize?: number; // bytes } export function useEditorMediaDrop({ onMediaInsert, onUploadStart, onUploadProgress, onUploadComplete, onUploadError, acceptedTypes = ["image/*", "video/*"], maxFileSize = 10 * 1024 * 1024, // 10MB }: UseEditorMediaDropProps) { const dragCountRef = useRef(0); // 파일 타입 검증 const validateFile = useCallback( (file: File): string | null => { // 파일 크기 검증 if (file.size > maxFileSize) { return `파일 크기가 너무 큽니다. 최대 ${Math.round( maxFileSize / 1024 / 1024 )}MB까지 가능합니다.`; } // 파일 타입 검증 const isAccepted = acceptedTypes.some((type) => { if (type.endsWith("/*")) { return file.type.startsWith(type.slice(0, -1)); } return file.type === type; }); if (!isAccepted) { return `지원하지 않는 파일 형식입니다. 허용된 형식: ${acceptedTypes.join( ", " )}`; } return null; }, [acceptedTypes, maxFileSize] ); // 미디어 HTML 생성 const generateMediaHtml = useCallback((media: MediaFile): string => { if (media.mimeType.startsWith("image/")) { const width = media.dimensions?.width; const height = media.dimensions?.height; const aspectRatio = width && height ? (height / width) * 100 : undefined; return ` <figure class="media-figure" data-media-id="${media.id}"> <div class="media-container" ${ aspectRatio ? `style="padding-bottom: ${aspectRatio}%"` : "" }> <img src="${media.cdnUrl || media.url}" alt="${media.description || media.originalName}" loading="lazy" ${width ? `width="${width}"` : ""} ${height ? `height="${height}"` : ""} /> </div> ${ media.description ? `<figcaption>${media.description}</figcaption>` : "" } </figure> `; } else if (media.mimeType.startsWith("video/")) { return ` <figure class="media-figure" data-media-id="${media.id}"> <video controls preload="metadata" ${ media.dimensions?.width ? `width="${media.dimensions.width}"` : "" } ${ media.dimensions?.height ? `height="${media.dimensions.height}"` : "" } > <source src="${media.cdnUrl || media.url}" type="${media.mimeType}"> 브라우저가 비디오를 지원하지 않습니다. </video> ${ media.description ? `<figcaption>${media.description}</figcaption>` : "" } </figure> `; } else { // 일반 파일 링크 return ` <div class="media-file" data-media-id="${media.id}"> <a href="${media.cdnUrl || media.url}" download="${ media.originalName }"> 📎 ${media.originalName} </a> </div> `; } }, []); // 파일 업로드 const uploadFile = useCallback( async (file: File): Promise<MediaFile | null> => { const validationError = validateFile(file); if (validationError) { onUploadError?.(validationError); return null; } onUploadStart?.(); try { const formData = new FormData(); formData.append("file", file); formData.append("enableProcessing", "true"); formData.append("enableCDN", "true"); const response = await fetch("/api/media/upload", { method: "POST", body: formData, }); if (!response.ok) { throw new Error(`업로드 실패: ${response.statusText}`); } const result = await response.json(); if (result.error) { throw new Error(result.error); } const media: MediaFile = { id: result.fileId, fileName: result.fileName, originalName: file.name, mimeType: file.type, size: file.size, url: result.url, cdnUrl: result.cdnUrl, thumbnailUrl: result.thumbnailUrl, createdAt: new Date().toISOString(), tags: [], description: "", dimensions: result.dimensions, }; onUploadProgress?.(100); onUploadComplete?.(media); return media; } catch (error) { const errorMessage = error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다."; onUploadError?.(errorMessage); return null; } }, [ validateFile, onUploadStart, onUploadProgress, onUploadComplete, onUploadError, ] ); // 드래그 진입 const handleDragEnter = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); dragCountRef.current++; if (e.dataTransfer) { e.dataTransfer.dropEffect = "copy"; } }, []); // 드래그 오버 const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer) { e.dataTransfer.dropEffect = "copy"; } }, []); // 드래그 나가기 const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); dragCountRef.current--; }, []); // 드롭 const handleDrop = useCallback( async (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); dragCountRef.current = 0; const files = Array.from(e.dataTransfer.files); for (const file of files) { const media = await uploadFile(file); if (media) { const html = generateMediaHtml(media); onMediaInsert(html, media); } } }, [uploadFile, generateMediaHtml, onMediaInsert] ); // 라이브러리에서 선택한 미디어 삽입 const insertMedia = useCallback( (media: MediaFile | MediaFile[]) => { const mediaList = Array.isArray(media) ? media : [media]; mediaList.forEach((item) => { const html = generateMediaHtml(item); onMediaInsert(html, item); }); }, [generateMediaHtml, onMediaInsert] ); // 미디어 교체 const replaceMedia = useCallback( (oldMediaId: string, newMedia: MediaFile) => { const html = generateMediaHtml(newMedia); // DOM에서 기존 미디어 요소 찾기 및 교체 const oldElement = document.querySelector( `[data-media-id="${oldMediaId}"]` ); if (oldElement) { const tempDiv = document.createElement("div"); tempDiv.innerHTML = html; const newElement = tempDiv.firstElementChild; if (newElement) { oldElement.parentNode?.replaceChild(newElement, oldElement); } } }, [generateMediaHtml] ); return { // 드래그 앤 드롭 이벤트 핸들러 dragHandlers: { onDragEnter: handleDragEnter, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, }, // 유틸리티 함수들 uploadFile, insertMedia, replaceMedia, generateMediaHtml, validateFile, }; }