@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
201 lines (198 loc) • 16.4 kB
JavaScript
import { jsxs, jsx } from 'react/jsx-runtime';
import { useState, useRef, useCallback, useEffect } from 'react';
import { Card, CardHeader, CardContent } from '../../../../components/ui/Card.js';
import { CardTitle } from '../../../../components/ui/CardTitle.js';
import { Button } from '../../../../components/ui/Button.js';
import { Modal, ModalContent, ModalHeader, ModalTitle } from '../../../../components/ui/Modal.js';
import { Input } from '../../../../components/ui/Input.js';
function DocumentUploader({ ragEngine, onDocumentUploaded, onDocumentDeleted, }) {
var _a;
const [documents, setDocuments] = useState([]);
const [isUploading, setIsUploading] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [selectedFile, setSelectedFile] = useState(null);
const [metadata, setMetadata] = useState({});
const [isMetadataModalOpen, setIsMetadataModalOpen] = useState(false);
const [previewDocument, setPreviewDocument] = useState(null);
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(null);
const fileInputRef = useRef(null);
// Supported file types
const supportedTypes = [
"application/pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword",
"text/plain",
"text/markdown",
"application/rtf",
];
const supportedExtensions = [".pdf", ".docx", ".doc", ".txt", ".md", ".rtf"];
const loadDocuments = useCallback(async () => {
try {
if (ragEngine.isStorageConfigured()) {
const docs = await ragEngine.listStorageDocuments();
const formattedDocs = docs.map((doc) => ({
id: doc.id,
name: doc.name,
size: doc.size,
uploadedAt: doc.updated_at,
metadata: doc.metadata || {},
path: doc.name,
}));
setDocuments(formattedDocs);
}
}
catch (err) {
console.error("문서 목록 로드 실패:", err);
setError("문서 목록을 불러오는데 실패했습니다.");
}
}, [ragEngine]);
useEffect(() => {
loadDocuments();
}, [loadDocuments]);
const clearMessages = () => {
setError(null);
setSuccess(null);
};
const validateFile = (file) => {
// Check file type
if (!supportedTypes.includes(file.type) &&
!supportedExtensions.some((ext) => file.name.toLowerCase().endsWith(ext))) {
setError(`지원하지 않는 파일 형식입니다. 지원 형식: ${supportedExtensions.join(", ")}`);
return false;
}
// Check file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
setError("파일 크기는 10MB를 초과할 수 없습니다.");
return false;
}
return true;
};
const handleFileSelect = (files) => {
if (!files || files.length === 0)
return;
const file = files[0];
if (validateFile(file)) {
setSelectedFile(file);
setMetadata({
title: file.name.replace(/\.[^/.]+$/, ""), // Remove extension
category: "",
tags: [],
description: "",
version: "1.0",
});
setIsMetadataModalOpen(true);
}
};
const handleDragOver = (e) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = (e) => {
e.preventDefault();
setIsDragging(false);
};
const handleDrop = (e) => {
e.preventDefault();
setIsDragging(false);
handleFileSelect(e.dataTransfer.files);
};
const uploadDocument = async () => {
if (!selectedFile)
return;
setIsUploading(true);
setUploadProgress(0);
clearMessages();
try {
// Simulate upload progress
const progressInterval = setInterval(() => {
setUploadProgress((prev) => {
if (prev >= 90) {
clearInterval(progressInterval);
return 90;
}
return prev + 10;
});
}, 200);
// Upload and add to RAG system
const result = await ragEngine.uploadAndAddDocument(selectedFile, selectedFile.name, metadata);
clearInterval(progressInterval);
setUploadProgress(100);
const newDocument = {
id: result.documentId,
name: selectedFile.name,
size: selectedFile.size,
uploadedAt: new Date().toISOString(),
metadata,
path: result.uploadResult.path,
url: result.uploadResult.url,
};
setDocuments((prev) => [...prev, newDocument]);
setSuccess(`문서 "${selectedFile.name}"가 성공적으로 업로드되었습니다.`);
onDocumentUploaded === null || onDocumentUploaded === void 0 ? void 0 : onDocumentUploaded(newDocument);
// Reset form
setSelectedFile(null);
setMetadata({});
setIsMetadataModalOpen(false);
setUploadProgress(0);
}
catch (err) {
setError(`업로드 실패: ${err instanceof Error ? err.message : "알 수 없는 오류"}`);
}
finally {
setIsUploading(false);
}
};
const deleteDocument = async (doc) => {
if (!confirm(`"${doc.name}" 문서를 삭제하시겠습니까?`))
return;
try {
await ragEngine.deleteDocumentFromStorage(doc.path);
setDocuments((prev) => prev.filter((d) => d.id !== doc.id));
setSuccess(`문서 "${doc.name}"가 삭제되었습니다.`);
onDocumentDeleted === null || onDocumentDeleted === void 0 ? void 0 : onDocumentDeleted(doc.id);
}
catch (err) {
setError(`삭제 실패: ${err instanceof Error ? err.message : "알 수 없는 오류"}`);
}
};
const handlePreviewDocument = (doc) => {
setPreviewDocument(doc);
setIsPreviewModalOpen(true);
};
const formatFileSize = (bytes) => {
if (bytes === 0)
return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
return (jsxs("div", { className: "space-y-6", children: [jsxs(Card, { children: [jsx(CardHeader, { children: jsx(CardTitle, { children: "\uBB38\uC11C \uC5C5\uB85C\uB4DC" }) }), jsxs(CardContent, { children: [jsx("div", { className: `border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${isDragging
? "border-blue-400 bg-blue-50"
: "border-gray-300 hover:border-gray-400"}`, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: jsxs("div", { className: "space-y-2", children: [jsx("div", { className: "text-4xl", children: "\uD83D\uDCC4" }), jsx("p", { className: "text-lg font-medium", children: "\uD30C\uC77C\uC744 \uC5EC\uAE30\uC5D0 \uB4DC\uB86D\uD558\uAC70\uB098 \uD074\uB9AD\uD558\uC5EC \uC120\uD0DD\uD558\uC138\uC694" }), jsx("p", { className: "text-sm text-gray-500", children: "\uC9C0\uC6D0 \uD615\uC2DD: PDF, DOCX, DOC, TXT, MD, RTF (\uCD5C\uB300 10MB)" })] }) }), jsx("input", { ref: fileInputRef, type: "file", accept: supportedExtensions.join(","), onChange: (e) => handleFileSelect(e.target.files), className: "hidden" })] })] }), jsxs(Card, { children: [jsx(CardHeader, { children: jsxs("div", { className: "flex items-center justify-between", children: [jsxs(CardTitle, { children: ["\uC5C5\uB85C\uB4DC\uB41C \uBB38\uC11C (", documents.length, "\uAC1C)"] }), jsx(Button, { variant: "outline", size: "sm", onClick: loadDocuments, children: "\uC0C8\uB85C\uACE0\uCE68" })] }) }), jsx(CardContent, { children: documents.length === 0 ? (jsx("p", { className: "text-gray-500 text-center py-8", children: "\uC5C5\uB85C\uB4DC\uB41C \uBB38\uC11C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." })) : (jsx("div", { className: "space-y-3", children: documents.map((doc) => (jsxs("div", { className: "flex items-center justify-between p-4 border rounded-lg", children: [jsxs("div", { className: "flex-1", children: [jsx("h4", { className: "font-medium", children: doc.metadata.title || doc.name }), jsxs("div", { className: "text-sm text-gray-600 space-y-1", children: [jsxs("p", { children: ["\uD30C\uC77C: ", doc.name, " (", formatFileSize(doc.size), ")"] }), jsxs("p", { children: ["\uC5C5\uB85C\uB4DC: ", new Date(doc.uploadedAt).toLocaleString()] }), doc.metadata.category && (jsxs("p", { children: ["\uCE74\uD14C\uACE0\uB9AC: ", doc.metadata.category] })), doc.metadata.tags && doc.metadata.tags.length > 0 && (jsxs("p", { children: ["\uD0DC\uADF8: ", doc.metadata.tags.join(", ")] })), doc.metadata.description && (jsxs("p", { children: ["\uC124\uBA85: ", doc.metadata.description] }))] })] }), jsxs("div", { className: "flex items-center gap-2", children: [jsx(Button, { variant: "outline", size: "sm", onClick: () => handlePreviewDocument(doc), children: "\uBBF8\uB9AC\uBCF4\uAE30" }), jsx(Button, { variant: "outline", size: "sm", onClick: () => deleteDocument(doc), children: "\uC0AD\uC81C" })] })] }, doc.id))) })) })] }), isMetadataModalOpen && selectedFile && (jsx(Modal, { open: isMetadataModalOpen, onOpenChange: setIsMetadataModalOpen, children: jsxs(ModalContent, { className: "max-w-md", children: [jsx(ModalHeader, { children: jsx(ModalTitle, { children: "\uBB38\uC11C \uBA54\uD0C0\uB370\uC774\uD130" }) }), jsxs("div", { className: "space-y-4 p-6", children: [jsxs("div", { children: [jsx("label", { className: "block text-sm font-medium mb-1", children: "\uC81C\uBAA9" }), jsx(Input, { value: metadata.title || "", onChange: (e) => setMetadata((prev) => ({ ...prev, title: e.target.value })), placeholder: "\uBB38\uC11C \uC81C\uBAA9" })] }), jsxs("div", { children: [jsx("label", { className: "block text-sm font-medium mb-1", children: "\uCE74\uD14C\uACE0\uB9AC" }), jsx(Input, { value: metadata.category || "", onChange: (e) => setMetadata((prev) => ({
...prev,
category: e.target.value,
})), placeholder: "\uC608: \uAE30\uC220\uBB38\uC11C, \uB9E4\uB274\uC5BC" })] }), jsxs("div", { children: [jsx("label", { className: "block text-sm font-medium mb-1", children: "\uD0DC\uADF8 (\uC27C\uD45C\uB85C \uAD6C\uBD84)" }), jsx(Input, { value: ((_a = metadata.tags) === null || _a === void 0 ? void 0 : _a.join(", ")) || "", onChange: (e) => setMetadata((prev) => ({
...prev,
tags: e.target.value
.split(",")
.map((tag) => tag.trim())
.filter(Boolean),
})), placeholder: "\uC608: AI, \uBA38\uC2E0\uB7EC\uB2DD, \uAC1C\uBC1C" })] }), jsxs("div", { children: [jsx("label", { className: "block text-sm font-medium mb-1", children: "\uC124\uBA85" }), jsx("textarea", { value: metadata.description || "", onChange: (e) => setMetadata((prev) => ({
...prev,
description: e.target.value,
})), placeholder: "\uBB38\uC11C\uC5D0 \uB300\uD55C \uAC04\uB2E8\uD55C \uC124\uBA85", className: "w-full h-20 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" })] }), jsxs("div", { children: [jsx("label", { className: "block text-sm font-medium mb-1", children: "\uBC84\uC804" }), jsx(Input, { value: metadata.version || "1.0", onChange: (e) => setMetadata((prev) => ({
...prev,
version: e.target.value,
})), placeholder: "1.0" })] }), isUploading && (jsxs("div", { className: "space-y-2", children: [jsxs("div", { className: "flex justify-between text-sm", children: [jsx("span", { children: "\uC5C5\uB85C\uB4DC \uC9C4\uD589\uB960" }), jsxs("span", { children: [uploadProgress, "%"] })] }), jsx("div", { className: "w-full bg-gray-200 rounded-full h-2", children: jsx("div", { className: "bg-blue-600 h-2 rounded-full transition-all duration-300", style: { width: `${uploadProgress}%` } }) })] })), jsxs("div", { className: "flex gap-2", children: [jsx(Button, { onClick: uploadDocument, disabled: isUploading, className: "flex-1", children: isUploading ? "업로드 중..." : "업로드" }), jsx(Button, { variant: "outline", onClick: () => {
setIsMetadataModalOpen(false);
setSelectedFile(null);
setMetadata({});
}, disabled: isUploading, children: "\uCDE8\uC18C" })] })] })] }) })), isPreviewModalOpen && previewDocument && (jsx(Modal, { open: isPreviewModalOpen, onOpenChange: setIsPreviewModalOpen, children: jsxs(ModalContent, { className: "max-w-2xl max-h-[80vh]", children: [jsx(ModalHeader, { children: jsx(ModalTitle, { children: previewDocument.metadata.title || previewDocument.name }) }), jsxs("div", { className: "p-6 space-y-4 overflow-y-auto", children: [jsxs("div", { className: "grid grid-cols-2 gap-4 text-sm", children: [jsxs("div", { children: [jsx("strong", { children: "\uD30C\uC77C\uBA85:" }), " ", previewDocument.name] }), jsxs("div", { children: [jsx("strong", { children: "\uD06C\uAE30:" }), " ", formatFileSize(previewDocument.size)] }), jsxs("div", { children: [jsx("strong", { children: "\uC5C5\uB85C\uB4DC:" }), " ", new Date(previewDocument.uploadedAt).toLocaleString()] }), jsxs("div", { children: [jsx("strong", { children: "\uBC84\uC804:" }), " ", previewDocument.metadata.version || "N/A"] }), previewDocument.metadata.category && (jsxs("div", { children: [jsx("strong", { children: "\uCE74\uD14C\uACE0\uB9AC:" }), " ", previewDocument.metadata.category] })), previewDocument.metadata.tags &&
previewDocument.metadata.tags.length > 0 && (jsxs("div", { className: "col-span-2", children: [jsx("strong", { children: "\uD0DC\uADF8:" }), " ", previewDocument.metadata.tags.join(", ")] }))] }), previewDocument.metadata.description && (jsxs("div", { children: [jsx("strong", { children: "\uC124\uBA85:" }), jsx("p", { className: "mt-1 text-gray-600", children: previewDocument.metadata.description })] })), jsx("div", { className: "pt-4 border-t", children: jsx("p", { className: "text-sm text-gray-500", children: "\uBB38\uC11C \uB0B4\uC6A9 \uBBF8\uB9AC\uBCF4\uAE30\uB294 \uD604\uC7AC \uC9C0\uC6D0\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uBB38\uC11C\uB294 RAG \uC2DC\uC2A4\uD15C\uC5D0 \uC815\uC0C1\uC801\uC73C\uB85C \uCC98\uB9AC\uB418\uC5B4 \uCC57\uBD07\uC5D0\uC11C \uD65C\uC6A9\uB429\uB2C8\uB2E4." }) }), jsx(Button, { onClick: () => setIsPreviewModalOpen(false), className: "w-full", children: "\uB2EB\uAE30" })] })] }) })), error && (jsx("div", { className: "p-4 bg-red-50 border border-red-200 rounded-lg", children: jsx("p", { className: "text-red-800", children: error }) })), success && (jsx("div", { className: "p-4 bg-green-50 border border-green-200 rounded-lg", children: jsx("p", { className: "text-green-800", children: success }) }))] }));
}
export { DocumentUploader };
//# sourceMappingURL=document-uploader.js.map