UNPKG

@restnfeel/agentc-starter-kit

Version:

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

353 lines (350 loc) 26.4 kB
"use client"; import { jsxs, jsx } from 'react/jsx-runtime'; import { useState, useRef, useCallback, useEffect } from 'react'; import { Card, CardHeader, CardContent } from '../ui/Card.js'; import { CardTitle } from '../ui/CardTitle.js'; import { Button } from '../ui/Button.js'; import { Input } from '../ui/Input.js'; import { Modal, ModalContent, ModalHeader, ModalTitle } from '../ui/Modal.js'; function KnowledgeEditor({ ragEngine, onEntryCreated, onEntryUpdated, onEntryDeleted, }) { var _a; const [entries, setEntries] = useState([]); const [filteredEntries, setFilteredEntries] = useState([]); const [editingEntry, setEditingEntry] = useState(null); const [isEditorOpen, setIsEditorOpen] = useState(false); const [isBulkMode, setIsBulkMode] = useState(false); const [selectedEntries, setSelectedEntries] = useState(new Set()); // Form states const [formData, setFormData] = useState({ title: "", content: "", category: "", tags: [], description: "", priority: "medium", status: "active", }); // Filter states const [searchQuery, setSearchQuery] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [categoryFilter, setCategoryFilter] = useState(""); const [tagFilter, setTagFilter] = useState(""); // Auto-save states const [lastSaved, setLastSaved] = useState(null); const [isSaving, setIsSaving] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // Validation states const [validationErrors, setValidationErrors] = useState({}); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const autoSaveTimeoutRef = useRef(null); const textareaRef = useRef(null); // Categories and tags from existing entries const categories = [ ...new Set(entries.map((e) => e.category).filter(Boolean)), ]; const allTags = [...new Set(entries.flatMap((e) => e.tags).filter(Boolean))]; const loadEntries = useCallback(async () => { try { // This would load knowledge entries from your storage/database // For now, we'll use localStorage as a mock const stored = localStorage.getItem("knowledge-entries"); if (stored) { const loadedEntries = JSON.parse(stored); setEntries(loadedEntries); setFilteredEntries(loadedEntries); } } catch (err) { console.error("지식 항목 로드 실패:", err); setError("지식 항목을 불러오는데 실패했습니다."); } }, []); useEffect(() => { loadEntries(); }, [loadEntries]); // Filter entries useEffect(() => { let filtered = entries; if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); filtered = filtered.filter((entry) => { var _a; return entry.title.toLowerCase().includes(query) || entry.content.toLowerCase().includes(query) || ((_a = entry.description) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes(query)) || entry.tags.some((tag) => tag.toLowerCase().includes(query)); }); } if (statusFilter !== "all") { filtered = filtered.filter((entry) => entry.status === statusFilter); } if (categoryFilter) { filtered = filtered.filter((entry) => entry.category === categoryFilter); } if (tagFilter) { filtered = filtered.filter((entry) => entry.tags.includes(tagFilter)); } setFilteredEntries(filtered); }, [entries, searchQuery, statusFilter, categoryFilter, tagFilter]); const clearMessages = () => { setError(null); setSuccess(null); }; const validateForm = (data) => { var _a, _b, _c; const errors = {}; if (!((_a = data.title) === null || _a === void 0 ? void 0 : _a.trim())) { errors.title = "제목은 필수입니다."; } if (!((_b = data.content) === null || _b === void 0 ? void 0 : _b.trim())) { errors.content = "내용은 필수입니다."; } if (data.content && data.content.trim().length < 10) { errors.content = "내용은 최소 10자 이상이어야 합니다."; } if (!((_c = data.category) === null || _c === void 0 ? void 0 : _c.trim())) { errors.category = "카테고리는 필수입니다."; } return errors; }; const saveEntry = async (entryData, isAutoSave = false) => { try { if (!isAutoSave) { const errors = validateForm(entryData); if (Object.keys(errors).length > 0) { setValidationErrors(errors); return false; } } setIsSaving(true); clearMessages(); const now = new Date().toISOString(); const entry = { id: (editingEntry === null || editingEntry === void 0 ? void 0 : editingEntry.id) || `kb_${Date.now()}`, title: entryData.title || "", content: entryData.content || "", category: entryData.category || "", tags: entryData.tags || [], description: entryData.description || "", priority: entryData.priority || "medium", status: entryData.status || "active", createdAt: (editingEntry === null || editingEntry === void 0 ? void 0 : editingEntry.createdAt) || now, updatedAt: now, }; // Save to RAG system as a document if (entry.content.trim()) { const content = `# ${entry.title}\n\n${entry.description ? `${entry.description}\n\n` : ""}${entry.content}`; const blob = new Blob([content], { type: "text/markdown" }); const file = new File([blob], `${entry.title}.md`, { type: "text/markdown", }); const metadata = { title: entry.title, category: entry.category, tags: entry.tags, type: "knowledge-entry", priority: entry.priority, status: entry.status, entryId: entry.id, }; if (editingEntry === null || editingEntry === void 0 ? void 0 : editingEntry.id) { // Update existing entry await ragEngine.uploadAndAddDocument(file, `${entry.title}.md`, metadata); } else { // Add new entry await ragEngine.uploadAndAddDocument(file, `${entry.title}.md`, metadata); } } // Update local storage const updatedEntries = editingEntry ? entries.map((e) => (e.id === editingEntry.id ? entry : e)) : [...entries, entry]; setEntries(updatedEntries); localStorage.setItem("knowledge-entries", JSON.stringify(updatedEntries)); setLastSaved(new Date()); setHasUnsavedChanges(false); if (!isAutoSave) { setSuccess(editingEntry ? "지식 항목이 업데이트되었습니다." : "새 지식 항목이 생성되었습니다."); if (editingEntry) { onEntryUpdated === null || onEntryUpdated === void 0 ? void 0 : onEntryUpdated(entry); } else { onEntryCreated === null || onEntryCreated === void 0 ? void 0 : onEntryCreated(entry); } setIsEditorOpen(false); resetForm(); } return true; } catch (err) { setError(`저장 실패: ${err instanceof Error ? err.message : "알 수 없는 오류"}`); return false; } finally { setIsSaving(false); } }; const autoSave = useCallback(() => { if (hasUnsavedChanges && editingEntry) { saveEntry(formData, true); } }, [formData, hasUnsavedChanges, editingEntry, saveEntry]); // Auto-save with debounce useEffect(() => { if (hasUnsavedChanges && editingEntry) { if (autoSaveTimeoutRef.current) { clearTimeout(autoSaveTimeoutRef.current); } autoSaveTimeoutRef.current = setTimeout(() => { autoSave(); }, 2000); // Auto-save after 2 seconds of inactivity } return () => { if (autoSaveTimeoutRef.current) { clearTimeout(autoSaveTimeoutRef.current); } }; }, [hasUnsavedChanges, editingEntry, autoSave]); const resetForm = () => { setFormData({ title: "", content: "", category: "", tags: [], description: "", priority: "medium", status: "active", }); setEditingEntry(null); setValidationErrors({}); setHasUnsavedChanges(false); }; const openEditor = (entry) => { if (entry) { setEditingEntry(entry); setFormData({ title: entry.title, content: entry.content, category: entry.category, tags: [...entry.tags], description: entry.description, priority: entry.priority, status: entry.status, }); } else { resetForm(); } setIsEditorOpen(true); setHasUnsavedChanges(false); }; const deleteEntry = async (entry) => { if (!confirm(`"${entry.title}" 항목을 삭제하시겠습니까?`)) return; try { const updatedEntries = entries.filter((e) => e.id !== entry.id); setEntries(updatedEntries); localStorage.setItem("knowledge-entries", JSON.stringify(updatedEntries)); setSuccess(`"${entry.title}" 항목이 삭제되었습니다.`); onEntryDeleted === null || onEntryDeleted === void 0 ? void 0 : onEntryDeleted(entry.id); } catch (err) { setError(`삭제 실패: ${err instanceof Error ? err.message : "알 수 없는 오류"}`); } }; const bulkDelete = async () => { if (!confirm(`선택된 ${selectedEntries.size}개 항목을 삭제하시겠습니까?`)) return; try { const updatedEntries = entries.filter((e) => !selectedEntries.has(e.id)); setEntries(updatedEntries); localStorage.setItem("knowledge-entries", JSON.stringify(updatedEntries)); setSuccess(`${selectedEntries.size}개 항목이 삭제되었습니다.`); setSelectedEntries(new Set()); setIsBulkMode(false); } catch (err) { setError(`벌크 삭제 실패: ${err instanceof Error ? err.message : "알 수 없는 오류"}`); } }; const bulkUpdateStatus = async (status) => { try { const updatedEntries = entries.map((e) => selectedEntries.has(e.id) ? { ...e, status, updatedAt: new Date().toISOString() } : e); setEntries(updatedEntries); localStorage.setItem("knowledge-entries", JSON.stringify(updatedEntries)); setSuccess(`${selectedEntries.size}개 항목의 상태가 "${status}"로 변경되었습니다.`); setSelectedEntries(new Set()); setIsBulkMode(false); } catch (err) { setError(`벌크 상태 변경 실패: ${err instanceof Error ? err.message : "알 수 없는 오류"}`); } }; const updateFormData = (field, value) => { setFormData((prev) => ({ ...prev, [field]: value })); setHasUnsavedChanges(true); // Clear validation error for this field if (validationErrors[field]) { setValidationErrors((prev) => { const newErrors = { ...prev }; delete newErrors[field]; return newErrors; }); } }; const insertMarkdown = (markdown) => { if (textareaRef.current) { const textarea = textareaRef.current; const start = textarea.selectionStart; const end = textarea.selectionEnd; const currentContent = formData.content || ""; const newContent = currentContent.substring(0, start) + markdown + currentContent.substring(end); updateFormData("content", newContent); // Move cursor to end of inserted text setTimeout(() => { textarea.focus(); textarea.setSelectionRange(start + markdown.length, start + markdown.length); }, 0); } }; const formatTime = (dateString) => { return new Date(dateString).toLocaleString(); }; return (jsxs("div", { className: "space-y-6", children: [jsxs(Card, { children: [jsx(CardHeader, { children: jsxs("div", { className: "flex items-center justify-between", children: [jsx(CardTitle, { children: "\uC9C0\uC2DD \uBCA0\uC774\uC2A4 \uD3B8\uC9D1\uAE30" }), jsxs("div", { className: "flex items-center gap-2", children: [jsx(Button, { onClick: () => openEditor(), children: "\uC0C8 \uD56D\uBAA9 \uCD94\uAC00" }), jsx(Button, { variant: "outline", onClick: () => setIsBulkMode(!isBulkMode), children: isBulkMode ? "벌크 모드 종료" : "벌크 편집" })] })] }) }), jsxs(CardContent, { children: [jsxs("div", { className: "grid grid-cols-1 md:grid-cols-4 gap-4 mb-4", children: [jsx(Input, { placeholder: "\uAC80\uC0C9...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value) }), jsxs("select", { value: statusFilter, onChange: (e) => setStatusFilter(e.target.value), className: "px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500", children: [jsx("option", { value: "all", children: "\uBAA8\uB4E0 \uC0C1\uD0DC" }), jsx("option", { value: "active", children: "\uD65C\uC131" }), jsx("option", { value: "draft", children: "\uC784\uC2DC\uC800\uC7A5" }), jsx("option", { value: "archived", children: "\uBCF4\uAD00\uB428" })] }), jsxs("select", { value: categoryFilter, onChange: (e) => setCategoryFilter(e.target.value), className: "px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500", children: [jsx("option", { value: "", children: "\uBAA8\uB4E0 \uCE74\uD14C\uACE0\uB9AC" }), categories.map((cat) => (jsx("option", { value: cat, children: cat }, cat)))] }), jsxs("select", { value: tagFilter, onChange: (e) => setTagFilter(e.target.value), className: "px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500", children: [jsx("option", { value: "", children: "\uBAA8\uB4E0 \uD0DC\uADF8" }), allTags.map((tag) => (jsx("option", { value: tag, children: tag }, tag)))] })] }), isBulkMode && selectedEntries.size > 0 && (jsxs("div", { className: "flex items-center gap-2 p-3 bg-blue-50 rounded-lg mb-4", children: [jsxs("span", { className: "text-sm font-medium", children: [selectedEntries.size, "\uAC1C \uD56D\uBAA9 \uC120\uD0DD\uB428"] }), jsx(Button, { size: "sm", onClick: () => bulkUpdateStatus("active"), children: "\uD65C\uC131\uD654" }), jsx(Button, { size: "sm", onClick: () => bulkUpdateStatus("archived"), children: "\uBCF4\uAD00" }), jsx(Button, { size: "sm", variant: "outline", onClick: bulkDelete, children: "\uC0AD\uC81C" }), jsx(Button, { size: "sm", variant: "outline", onClick: () => setSelectedEntries(new Set()), children: "\uC120\uD0DD \uD574\uC81C" })] }))] })] }), jsxs(Card, { children: [jsx(CardHeader, { children: jsxs(CardTitle, { children: ["\uC9C0\uC2DD \uD56D\uBAA9 (", filteredEntries.length, "\uAC1C)"] }) }), jsx(CardContent, { children: filteredEntries.length === 0 ? (jsx("p", { className: "text-gray-500 text-center py-8", children: searchQuery || statusFilter !== "all" || categoryFilter || tagFilter ? "검색 조건에 맞는 항목이 없습니다." : "지식 항목이 없습니다. 새 항목을 추가해보세요." })) : (jsx("div", { className: "space-y-3", children: filteredEntries.map((entry) => (jsxs("div", { className: "flex items-start gap-3 p-4 border rounded-lg", children: [isBulkMode && (jsx("input", { type: "checkbox", checked: selectedEntries.has(entry.id), onChange: (e) => { const newSelected = new Set(selectedEntries); if (e.target.checked) { newSelected.add(entry.id); } else { newSelected.delete(entry.id); } setSelectedEntries(newSelected); }, className: "mt-1" })), jsxs("div", { className: "flex-1", children: [jsxs("div", { className: "flex items-start justify-between", children: [jsxs("div", { children: [jsx("h4", { className: "font-medium text-lg", children: entry.title }), entry.description && (jsx("p", { className: "text-sm text-gray-600 mb-2", children: entry.description })), jsxs("div", { className: "text-sm text-gray-500 space-y-1", children: [jsxs("p", { children: ["\uCE74\uD14C\uACE0\uB9AC: ", entry.category] }), entry.tags.length > 0 && (jsxs("p", { children: ["\uD0DC\uADF8: ", entry.tags.join(", ")] })), jsxs("p", { children: ["\uC6B0\uC120\uC21C\uC704: ", entry.priority] }), jsxs("p", { children: ["\uC0C1\uD0DC: ", entry.status] }), jsxs("p", { children: ["\uB9C8\uC9C0\uB9C9 \uC218\uC815: ", formatTime(entry.updatedAt)] })] })] }), jsxs("div", { className: "flex items-center gap-2", children: [jsx(Button, { variant: "outline", size: "sm", onClick: () => openEditor(entry), children: "\uD3B8\uC9D1" }), jsx(Button, { variant: "outline", size: "sm", onClick: () => deleteEntry(entry), children: "\uC0AD\uC81C" })] })] }), jsx("div", { className: "mt-3 p-3 bg-gray-50 rounded text-sm", children: jsx("p", { className: "line-clamp-3", children: entry.content }) })] })] }, entry.id))) })) })] }), isEditorOpen && (jsx(Modal, { open: isEditorOpen, onOpenChange: setIsEditorOpen, children: jsxs(ModalContent, { className: "max-w-4xl max-h-[90vh]", children: [jsx(ModalHeader, { children: jsxs(ModalTitle, { children: [editingEntry ? "지식 항목 편집" : "새 지식 항목", hasUnsavedChanges && (jsx("span", { className: "ml-2 text-sm text-orange-600", children: "\u25CF \uC800\uC7A5\uB418\uC9C0 \uC54A\uC740 \uBCC0\uACBD\uC0AC\uD56D" })), lastSaved && (jsxs("span", { className: "ml-2 text-sm text-green-600", children: ["\uB9C8\uC9C0\uB9C9 \uC800\uC7A5: ", lastSaved.toLocaleTimeString()] }))] }) }), jsxs("div", { className: "p-6 space-y-4 overflow-y-auto", children: [jsxs("div", { className: "grid grid-cols-2 gap-4", children: [jsxs("div", { children: [jsx("label", { className: "block text-sm font-medium mb-1", children: "\uC81C\uBAA9 *" }), jsx(Input, { value: formData.title || "", onChange: (e) => updateFormData("title", e.target.value), placeholder: "\uC9C0\uC2DD \uD56D\uBAA9 \uC81C\uBAA9", className: validationErrors.title ? "border-red-500" : "" }), validationErrors.title && (jsx("p", { className: "text-red-500 text-sm mt-1", children: validationErrors.title }))] }), jsxs("div", { children: [jsx("label", { className: "block text-sm font-medium mb-1", children: "\uCE74\uD14C\uACE0\uB9AC *" }), jsx(Input, { value: formData.category || "", onChange: (e) => updateFormData("category", e.target.value), placeholder: "\uC608: FAQ, \uAE30\uC220\uBB38\uC11C, \uAC00\uC774\uB4DC", list: "categories", className: validationErrors.category ? "border-red-500" : "" }), jsx("datalist", { id: "categories", children: categories.map((cat) => (jsx("option", { value: cat }, cat))) }), validationErrors.category && (jsx("p", { className: "text-red-500 text-sm mt-1", children: validationErrors.category }))] })] }), jsxs("div", { children: [jsx("label", { className: "block text-sm font-medium mb-1", children: "\uC124\uBA85" }), jsx(Input, { value: formData.description || "", onChange: (e) => updateFormData("description", e.target.value), placeholder: "\uAC04\uB2E8\uD55C \uC124\uBA85" })] }), jsxs("div", { className: "grid grid-cols-3 gap-4", children: [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 = formData.tags) === null || _a === void 0 ? void 0 : _a.join(", ")) || "", onChange: (e) => updateFormData("tags", e.target.value .split(",") .map((tag) => tag.trim()) .filter(Boolean)), placeholder: "\uC608: \uC790\uC8FC\uBB3B\uB294\uC9C8\uBB38, \uAE30\uBCF8\uC124\uC815" })] }), jsxs("div", { children: [jsx("label", { className: "block text-sm font-medium mb-1", children: "\uC6B0\uC120\uC21C\uC704" }), jsxs("select", { value: formData.priority || "medium", onChange: (e) => updateFormData("priority", e.target.value), className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500", children: [jsx("option", { value: "high", children: "\uB192\uC74C" }), jsx("option", { value: "medium", children: "\uBCF4\uD1B5" }), jsx("option", { value: "low", children: "\uB0AE\uC74C" })] })] }), jsxs("div", { children: [jsx("label", { className: "block text-sm font-medium mb-1", children: "\uC0C1\uD0DC" }), jsxs("select", { value: formData.status || "active", onChange: (e) => updateFormData("status", e.target.value), className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500", children: [jsx("option", { value: "active", children: "\uD65C\uC131" }), jsx("option", { value: "draft", children: "\uC784\uC2DC\uC800\uC7A5" }), jsx("option", { value: "archived", children: "\uBCF4\uAD00\uB428" })] })] })] }), jsxs("div", { children: [jsx("label", { className: "block text-sm font-medium mb-1", children: "\uB0B4\uC6A9 (\uB9C8\uD06C\uB2E4\uC6B4 \uC9C0\uC6D0) *" }), jsxs("div", { className: "flex items-center gap-2 mb-2 p-2 bg-gray-50 rounded", children: [jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: () => insertMarkdown("**굵게**"), children: jsx("strong", { children: "B" }) }), jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: () => insertMarkdown("*기울임*"), children: jsx("em", { children: "I" }) }), jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: () => insertMarkdown("\n- "), children: "\u2022" }), jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: () => insertMarkdown("\n1. "), children: "1." }), jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: () => insertMarkdown("[링크 텍스트](URL)"), children: "\uB9C1\uD06C" }), jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: () => insertMarkdown("`코드`"), children: "\uCF54\uB4DC" })] }), jsx("textarea", { ref: textareaRef, value: formData.content || "", onChange: (e) => updateFormData("content", e.target.value), placeholder: "\uC9C0\uC2DD \uB0B4\uC6A9\uC744 \uC785\uB825\uD558\uC138\uC694...\n\n\uC608\uC2DC:\n- \uC694\uC810 \uD615\uD0DC\uB85C \uC791\uC131\n- **\uC911\uC694\uD55C \uB0B4\uC6A9**\uC740 \uAD75\uAC8C\n- \uAC04\uACB0\uD558\uACE0 \uBA85\uD655\uD558\uAC8C", className: `w-full h-64 px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm ${validationErrors.content ? "border-red-500" : "border-gray-300"}` }), validationErrors.content && (jsx("p", { className: "text-red-500 text-sm mt-1", children: validationErrors.content }))] }), jsxs("div", { className: "flex items-center justify-between pt-4 border-t", children: [jsx("div", { className: "flex items-center gap-2", children: isSaving && (jsx("span", { className: "text-sm text-gray-600", children: "\uC800\uC7A5 \uC911..." })) }), jsxs("div", { className: "flex gap-2", children: [jsx(Button, { variant: "outline", onClick: () => { if (hasUnsavedChanges && !confirm("저장하지 않은 변경사항이 있습니다. 계속하시겠습니까?")) { return; } setIsEditorOpen(false); resetForm(); }, children: "\uCDE8\uC18C" }), jsx(Button, { onClick: () => saveEntry({ ...formData, status: "draft" }), variant: "outline", disabled: isSaving, children: "\uC784\uC2DC\uC800\uC7A5" }), jsx(Button, { onClick: () => saveEntry({ ...formData, status: "active" }), disabled: isSaving, children: editingEntry ? "업데이트" : "저장" })] })] })] })] }) })), 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 { KnowledgeEditor }; //# sourceMappingURL=knowledge-editor.js.map