Files
sam-sales/prt/ref/App.tsx
2025-12-22 21:35:17 +09:00

205 lines
7.4 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import Sidebar from './components/Sidebar';
import PromptEditor from './components/PromptEditor';
import { AppState, Category, Prompt, PromptVersion } from './types';
import { generatePromptTitle } from './services/geminiService';
const INITIAL_DATA: AppState = {
categories: [
{ id: 'cat-1', name: '웹 프론트엔드', parentId: null },
{ id: 'cat-2', name: 'AI 엔지니어링', parentId: null },
{ id: 'cat-3', name: '클라우드 아키텍처', parentId: null },
],
prompts: [
{
id: 'p-1',
categoryId: 'cat-2',
name: 'Gemini 최적화 로직',
description: '프롬프트 최적화를 위한 핵심 시스템 프롬프트 연구',
tags: ['ai', 'optimization'],
currentVersionId: 'v-1',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
],
versions: [
{
id: 'v-1',
promptId: 'p-1',
content: '당신은 세계적인 프롬프트 엔지니어입니다. 입력받은 프롬프트를 분석하여 논리적 구조, 제약 조건, 그리고 창의적인 페르소나를 강화하여 재작성하세요.',
versionNumber: 1,
changeSummary: '초기 연구 가설 설정',
createdAt: new Date().toISOString()
}
],
selectedCategoryId: null,
selectedPromptId: 'p-1',
};
const App: React.FC = () => {
const [state, setState] = useState<AppState>(() => {
const saved = localStorage.getItem('sam_prompt_state');
return saved ? JSON.parse(saved) : INITIAL_DATA;
});
useEffect(() => {
localStorage.setItem('sam_prompt_state', JSON.stringify(state));
}, [state]);
const handleSelectPrompt = (id: string) => {
setState(prev => ({ ...prev, selectedPromptId: id }));
};
const handleAddCategory = (parentId: string | null) => {
const name = prompt('새로운 연구 분야의 이름을 입력하세요 (예: DevOps, Mobile, Security):');
if (!name) return;
const newCategory: Category = {
id: `cat-${Date.now()}`,
name,
parentId
};
setState(prev => ({
...prev,
categories: [...prev.categories, newCategory]
}));
};
const handleDeleteCategory = (id: string) => {
if (!confirm('정말로 이 분야의 모든 연구 내용을 삭제하시겠습니까?')) return;
setState(prev => ({
...prev,
categories: prev.categories.filter(c => c.id !== id),
prompts: prev.prompts.filter(p => p.categoryId !== id),
selectedPromptId: prev.prompts.find(p => p.categoryId === id)?.id === prev.selectedPromptId ? null : prev.selectedPromptId
}));
};
const handleAddPrompt = async (categoryId: string) => {
const id = `p-${Date.now()}`;
const versionId = `v-${Date.now()}`;
const newPrompt: Prompt = {
id,
categoryId,
name: '제목 없는 연구 프로젝트',
description: '',
tags: [],
currentVersionId: versionId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const newVersion: PromptVersion = {
id: versionId,
promptId: id,
content: '',
versionNumber: 1,
changeSummary: '신규 연구 프로젝트 시작',
createdAt: new Date().toISOString(),
};
setState(prev => ({
...prev,
prompts: [...prev.prompts, newPrompt],
versions: [...prev.versions, newVersion],
selectedPromptId: id
}));
};
const handleSaveVersion = async (content: string, summary: string) => {
if (!state.selectedPromptId) return;
const currentPrompt = state.prompts.find(p => p.id === state.selectedPromptId);
if (!currentPrompt) return;
const currentVersions = state.versions.filter(v => v.promptId === state.selectedPromptId);
const nextVersionNumber = Math.max(0, ...currentVersions.map(v => v.versionNumber)) + 1;
const newVersionId = `v-${Date.now()}`;
// Auto-update name if it's still default
let updatedName = currentPrompt.name;
if (updatedName === '제목 없는 연구 프로젝트' && content.length > 10) {
updatedName = await generatePromptTitle(content);
}
const newVersion: PromptVersion = {
id: newVersionId,
promptId: state.selectedPromptId,
content,
versionNumber: nextVersionNumber,
changeSummary: summary,
createdAt: new Date().toISOString(),
};
setState(prev => ({
...prev,
prompts: prev.prompts.map(p => p.id === state.selectedPromptId ? {
...p,
name: updatedName,
currentVersionId: newVersionId,
updatedAt: new Date().toISOString()
} : p),
versions: [...prev.versions, newVersion]
}));
};
const handleDeletePrompt = (id: string) => {
if (!confirm('이 프롬프트 연구 기록을 삭제하시겠습니까?')) return;
setState(prev => ({
...prev,
prompts: prev.prompts.filter(p => p.id !== id),
versions: prev.versions.filter(v => v.promptId !== id),
selectedPromptId: prev.selectedPromptId === id ? null : prev.selectedPromptId
}));
};
const selectedPrompt = state.prompts.find(p => p.id === state.selectedPromptId);
const selectedPromptVersions = state.versions.filter(v => v.promptId === state.selectedPromptId);
const selectedCategory = state.categories.find(c => c.id === selectedPrompt?.categoryId);
return (
<div className="flex h-screen bg-white text-slate-900 antialiased overflow-hidden">
<Sidebar
categories={state.categories}
prompts={state.prompts}
onSelectPrompt={handleSelectPrompt}
onAddCategory={handleAddCategory}
onAddPrompt={handleAddPrompt}
onDeleteCategory={handleDeleteCategory}
selectedPromptId={state.selectedPromptId}
/>
{selectedPrompt ? (
<PromptEditor
prompt={selectedPrompt}
versions={selectedPromptVersions}
categoryName={selectedCategory?.name || '분류 없음'}
onSave={handleSaveVersion}
onDelete={handleDeletePrompt}
/>
) : (
<div className="flex-1 flex flex-col items-center justify-center bg-slate-50/50 p-10 text-center">
<div className="w-32 h-32 bg-white rounded-[40px] border border-slate-100 shadow-2xl shadow-slate-200/50 flex items-center justify-center mb-10">
<svg className="w-14 h-14 text-indigo-500 animate-pulse" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
</div>
<h3 className="text-3xl font-black text-slate-900 mb-4 tracking-tight"> </h3>
<p className="max-w-md text-slate-500 leading-relaxed font-bold">
.
</p>
<div className="mt-12 flex items-center space-x-2 text-[10px] font-black uppercase tracking-widest text-slate-300">
<span> </span>
<span className="text-indigo-400">Codebridge-X</span>
</div>
</div>
)}
</div>
);
};
export default App;