@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
306 lines (271 loc) • 8.1 kB
text/typescript
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,
};
}