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