diff --git a/prt/api/init_db.php b/prt/api/init_db.php new file mode 100644 index 0000000..2c3f0e5 --- /dev/null +++ b/prt/api/init_db.php @@ -0,0 +1,24 @@ +exec("ALTER TABLE prompt_items ADD COLUMN IF NOT EXISTS sort_order INT DEFAULT 0 AFTER current_version_id"); + + $sql = file_get_contents(__DIR__ . "/../schema.sql"); + + // Split combined SQL into individual queries for PDO if needed, + // but exec() can often handle multiple statements depending on driver config. + // For safer execution, we'll try to run it. + $pdo->exec($sql); + + echo json_encode(["success" => true, "message" => "Database schema initialized successfully."]); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(["success" => false, "error" => $e->getMessage()]); +} +?> diff --git a/prt/api/prompts.php b/prt/api/prompts.php new file mode 100644 index 0000000..ed652cf --- /dev/null +++ b/prt/api/prompts.php @@ -0,0 +1,105 @@ +query("SELECT * FROM prompt_categories ORDER BY COALESCE(parent_id, 0) ASC, sort_order ASC, name ASC")->fetchAll(PDO::FETCH_ASSOC); + $prompts = $pdo->query("SELECT * FROM prompt_items ORDER BY sort_order ASC, updated_at DESC")->fetchAll(PDO::FETCH_ASSOC); + $versions = $pdo->query("SELECT * FROM prompt_versions ORDER BY version_number DESC")->fetchAll(PDO::FETCH_ASSOC); + echo json_encode(['success' => true, 'categories' => $categories, 'prompts' => $prompts, 'versions' => $versions]); + break; + + case 'move_category': + $input = json_decode(file_get_contents('php://input'), true); + // input: { id, parent_id, sort_order } + $stmt = $pdo->prepare("UPDATE prompt_categories SET parent_id = ?, sort_order = ? WHERE id = ?"); + $stmt->execute([$input['parent_id'], $input['sort_order'], $input['id']]); + echo json_encode(['success' => true]); + break; + + case 'move_prompt': + $input = json_decode(file_get_contents('php://input'), true); + // input: { id, category_id, sort_order } + $stmt = $pdo->prepare("UPDATE prompt_items SET category_id = ?, sort_order = ? WHERE id = ?"); + $stmt->execute([$input['category_id'], $input['sort_order'], $input['id']]); + echo json_encode(['success' => true]); + break; + + case 'save_category': + $input = json_decode(file_get_contents('php://input'), true); + if (isset($input['id']) && is_numeric($input['id'])) { + $stmt = $pdo->prepare("UPDATE prompt_categories SET name = ?, parent_id = ? WHERE id = ?"); + $stmt->execute([$input['name'], $input['parent_id'], $input['id']]); + $id = $input['id']; + } else { + $stmt = $pdo->prepare("INSERT INTO prompt_categories (name, parent_id) VALUES (?, ?)"); + $stmt->execute([$input['name'], $input['parent_id']]); + $id = $pdo->lastInsertId(); + } + echo json_encode(['success' => true, 'id' => $id]); + break; + + case 'delete_category': + $input = json_decode(file_get_contents('php://input'), true); + $stmt = $pdo->prepare("DELETE FROM prompt_categories WHERE id = ?"); + $stmt->execute([$input['id']]); + echo json_encode(['success' => true]); + break; + + case 'save_prompt': + $input = json_decode(file_get_contents('php://input'), true); + if (isset($input['id']) && is_numeric($input['id'])) { + $stmt = $pdo->prepare("UPDATE prompt_items SET name = ?, category_id = ?, description = ? WHERE id = ?"); + $stmt->execute([$input['name'], $input['category_id'], $input['description'] ?? '', $input['id']]); + $id = $input['id']; + } else { + $stmt = $pdo->prepare("INSERT INTO prompt_items (name, category_id, description) VALUES (?, ?, ?)"); + $stmt->execute([$input['name'], $input['category_id'], $input['description'] ?? '']); + $id = $pdo->lastInsertId(); + } + echo json_encode(['success' => true, 'id' => $id]); + break; + + case 'delete_prompt': + $input = json_decode(file_get_contents('php://input'), true); + $stmt = $pdo->prepare("DELETE FROM prompt_items WHERE id = ?"); + $stmt->execute([$input['id']]); + echo json_encode(['success' => true]); + break; + + case 'save_version': + $input = json_decode(file_get_contents('php://input'), true); + + // Get next version number + $stmt = $pdo->prepare("SELECT MAX(version_number) as max_v FROM prompt_versions WHERE prompt_id = ?"); + $stmt->execute([$input['prompt_id']]); + $row = $stmt->fetch(); + $nextV = ($row['max_v'] ?? 0) + 1; + + $stmt = $pdo->prepare("INSERT INTO prompt_versions (prompt_id, content, version_number, change_summary) VALUES (?, ?, ?, ?)"); + $stmt->execute([$input['prompt_id'], $input['content'], $nextV, $input['change_summary']]); + $versionId = $pdo->lastInsertId(); + + // Update current_version_id in prompt_items + $stmt = $pdo->prepare("UPDATE prompt_items SET current_version_id = ?, updated_at = NOW() WHERE id = ?"); + $stmt->execute([$versionId, $input['prompt_id']]); + + echo json_encode(['success' => true, 'id' => $versionId, 'version_number' => $nextV]); + break; + + default: + echo json_encode(['success' => false, 'error' => 'Invalid action']); + break; + } +} catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} +?> diff --git a/prt/index.php b/prt/index.php new file mode 100644 index 0000000..682b7b3 --- /dev/null +++ b/prt/index.php @@ -0,0 +1,867 @@ + + + + + + SAM PRT | 프롬프트 연구 터미널 + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + +
+
+ + +
+ +
+
+
+
+ +
+
+

+

관련 프롬프트들을 그룹화합니다

+
+
+ +
+
+ + +
+ +
+ + +
+
+
+
+
+ + +
+ +
+
+
+
+ +
+
+

+

구체적인 프롬프트 과제명을 입력하세요

+
+
+ +
+
+ + +
+ +
+ + +
+
+
+
+
+ + + + + diff --git a/prt/ref/.gitignore b/prt/ref/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/prt/ref/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/prt/ref/App.tsx b/prt/ref/App.tsx new file mode 100644 index 0000000..b916797 --- /dev/null +++ b/prt/ref/App.tsx @@ -0,0 +1,204 @@ + +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(() => { + 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 ( +
+ + + {selectedPrompt ? ( + + ) : ( +
+
+ + + +
+

연구를 시작할 프롬프트를 선택하세요

+

+ 사이드바에서 기존 연구 과제를 선택하거나 새로운 분야를 추가하여 프롬프트의 진화 과정을 기록하십시오. +

+
+ 혁신 파트너 + Codebridge-X +
+
+ )} +
+ ); +}; + +export default App; diff --git a/prt/ref/README.md b/prt/ref/README.md new file mode 100644 index 0000000..983f2bd --- /dev/null +++ b/prt/ref/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/1Z2kAlFwh0UXFVckjV9KPZLXtpLoCfhoV + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/prt/ref/components/Icons.tsx b/prt/ref/components/Icons.tsx new file mode 100644 index 0000000..3c20703 --- /dev/null +++ b/prt/ref/components/Icons.tsx @@ -0,0 +1,38 @@ + +import React from 'react'; + +export const FolderIcon = ({ className = "w-5 h-5" }) => ( + + + +); + +export const FileIcon = ({ className = "w-5 h-5" }) => ( + + + +); + +export const PlusIcon = ({ className = "w-5 h-5" }) => ( + + + +); + +export const HistoryIcon = ({ className = "w-5 h-5" }) => ( + + + +); + +export const SparklesIcon = ({ className = "w-5 h-5" }) => ( + + + +); + +export const TrashIcon = ({ className = "w-5 h-5" }) => ( + + + + ); diff --git a/prt/ref/components/PromptEditor.tsx b/prt/ref/components/PromptEditor.tsx new file mode 100644 index 0000000..e732b9c --- /dev/null +++ b/prt/ref/components/PromptEditor.tsx @@ -0,0 +1,176 @@ + +import React, { useState, useEffect } from 'react'; +import { Prompt, PromptVersion, Category } from '../types'; +import { SparklesIcon, HistoryIcon, TrashIcon } from './Icons'; +import { optimizePrompt } from '../services/geminiService'; + +interface PromptEditorProps { + prompt: Prompt; + versions: PromptVersion[]; + categoryName: string; + onSave: (content: string, summary: string) => void; + onDelete: (id: string) => void; +} + +const PromptEditor: React.FC = ({ prompt, versions, categoryName, onSave, onDelete }) => { + const currentVersion = versions.find(v => v.id === prompt.currentVersionId); + const [content, setContent] = useState(currentVersion?.content || ''); + const [summary, setSummary] = useState(''); + const [isOptimizing, setIsOptimizing] = useState(false); + const [showHistory, setShowHistory] = useState(false); + + useEffect(() => { + setContent(currentVersion?.content || ''); + setSummary(''); + }, [prompt.id, prompt.currentVersionId, currentVersion]); + + const handleOptimize = async () => { + if (!content.trim()) return; + setIsOptimizing(true); + const optimized = await optimizePrompt(content, categoryName); + setContent(optimized); + setIsOptimizing(false); + }; + + const handleSave = () => { + if (!content.trim()) return; + onSave(content, summary || `${versions.length + 1}차 고도화 진행`); + }; + + return ( +
+ {/* Header */} +
+
+
+ + + +
+
+
+ {categoryName} + / + 진화 단계 v{currentVersion?.versionNumber || 1} +
+

{prompt.name}

+
+
+
+ +
+ +
+
+ +
+ {/* Editor Area */} +
+
+
+
+
+ 활성 연구 워크스페이스 +
+ +
+ +
+