Files
sam-sales/prt/index.php

873 lines
47 KiB
PHP
Raw Normal View History

2025-12-22 21:35:17 +09:00
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SAM PRT | 프롬프트 연구 터미널</title>
<!-- Favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="../img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="../img/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="../img/favicon-16x16.png">
<link rel="shortcut icon" href="../img/favicon.png">
2025-12-22 21:35:17 +09:00
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700;800;900&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Pretendard', 'sans-serif'],
mono: ['Fira Code', 'monospace'],
},
colors: {
indigo: {
50: '#f5f7ff',
100: '#ebf0ff',
200: '#dce4ff',
300: '#ccd8ff',
400: '#adbdff',
500: '#8e9eff',
600: '#6366f1',
700: '#4f46e5',
800: '#3730a3',
900: '#312e81',
}
}
}
}
}
</script>
<!-- Alpine.js -->
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
[x-cloak] { display: none !important; }
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #e2e8f0;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #cbd5e1;
}
.glass-panel {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.sidebar-border {
border-right: 1px solid #f1f5f9;
}
.research-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.research-card:hover {
transform: translateY(-2px);
}
.drag-over {
@apply border-2 border-dashed border-indigo-400 bg-indigo-50/50 !important;
}
.dragging {
opacity: 0.5;
}
@keyframes pulse-soft {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.animate-pulse-soft {
animation: pulse-soft 2s infinite;
}
/* Modal backdrop animation */
.modal-overlay {
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(4px);
}
</style>
</head>
<body class="bg-white text-slate-900 font-sans selection:bg-indigo-100 selection:text-indigo-700 h-screen overflow-hidden"
x-data="prtApp()"
x-init="initApp()"
x-cloak>
<div class="flex h-screen w-full">
<!-- Sidebar -->
<aside class="w-80 sidebar-border bg-white flex flex-col h-screen overflow-hidden z-20">
<div class="px-8 py-10">
<div class="flex flex-col">
<div class="flex items-center space-x-2 mb-2">
<div class="w-7 h-7 bg-indigo-600 rounded-lg flex items-center justify-center text-white font-black text-[10px] shadow-lg shadow-indigo-200">PRT</div>
<h1 class="text-xl font-black text-slate-900 leading-tight tracking-tight">SAM 프롬프트 연구소</h1>
</div>
<a href="https://codebridge-x.com" target="_blank" class="text-[10px] font-black text-indigo-500 uppercase tracking-widest hover:underline transition-all">
CODEBRIDGE-X.COM
</a>
</div>
<button
@click="openCategoryModal()"
class="mt-10 w-full flex items-center justify-center space-x-2 py-3.5 bg-slate-900 text-white rounded-[20px] hover:bg-slate-800 transition-all shadow-xl shadow-slate-200 active:scale-95"
>
<i data-lucide="plus" class="w-4 h-4"></i>
<span class="text-sm font-black tracking-tight"> 연구 분야 추가</span>
</button>
</div>
<div class="flex-1 overflow-y-auto px-5 pb-10 space-y-1">
<template x-if="categories.length === 0">
<div class="text-center py-20 px-6 opacity-40">
<div class="w-16 h-16 bg-slate-50 rounded-3xl flex items-center justify-center mx-auto mb-4 border border-slate-100">
<i data-lucide="folder-open" class="w-6 h-6 text-slate-300"></i>
</div>
<p class="text-xs font-bold text-slate-400">등록된 연구 분야가 없습니다</p>
</div>
</template>
<template x-for="category in categories.filter(c => !c.parent_id)" :key="category.id">
<div x-html="renderTreeItem(category, 0)"></div>
</template>
</div>
<div class="mt-auto p-8 bg-slate-50/50 border-t border-slate-100">
<div class="flex items-center space-x-4">
<div class="relative">
<div class="w-12 h-12 rounded-[18px] bg-gradient-to-tr from-slate-900 to-slate-700 flex items-center justify-center text-white font-black text-xs shadow-xl">
PRT
</div>
<div class="absolute -bottom-1 -right-1 w-4 h-4 bg-emerald-500 border-[3px] border-white rounded-full"></div>
</div>
<div class="flex flex-col">
<p class="text-[13px] font-black text-slate-900 leading-tight mb-0.5">프롬프트 연구소</p>
<p class="text-[10px] text-slate-400 font-bold tracking-widest uppercase">v3.1 DATABASE SYNC</p>
</div>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 flex flex-col h-screen bg-slate-50/20 overflow-hidden relative">
<template x-if="selectedPrompt">
<div class="flex flex-col h-full w-full">
<!-- Editor Header -->
<header class="px-10 py-8 bg-white/70 backdrop-blur-xl border-b border-slate-100 flex items-center justify-between sticky top-0 z-10 no-print">
<div class="flex items-center space-x-5">
<div class="w-14 h-14 rounded-3xl bg-slate-50 border border-slate-100 flex items-center justify-center shadow-inner">
<i data-lucide="pen-tool" class="w-6 h-6 text-slate-400"></i>
</div>
<div>
<div class="flex items-center space-x-3 text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1.5">
<span class="text-indigo-600 bg-indigo-50 px-2.5 py-1 rounded-full" x-text="currentCategoryName"></span>
<span class="text-slate-300"></span>
<span class="font-black text-slate-500" x-text="'진화 단계 v' + currentVersionNumber"></span>
</div>
<h2 class="text-3xl font-black text-slate-900 tracking-tight" x-text="selectedPrompt.name"></h2>
</div>
</div>
<div class="flex items-center space-x-4">
<button
@click="copyToClipboard()"
class="flex items-center space-x-2.5 px-6 py-3.5 rounded-2xl text-sm font-black transition-all bg-white border border-slate-200 text-slate-600 hover:bg-slate-50 hover:border-slate-300 shadow-sm"
>
<i :data-lucide="copied ? 'check' : 'copy'" class="w-4 h-4" :class="copied ? 'text-emerald-500' : ''"></i>
<span x-text="copied ? '복사 완료!' : '프롬프트 복사'"></span>
</button>
<button
@click="showHistory = !showHistory"
class="flex items-center space-x-2.5 px-6 py-3.5 rounded-2xl text-sm font-black transition-all"
:class="showHistory ? 'bg-indigo-600 text-white shadow-2xl shadow-indigo-200' : 'bg-white border border-slate-200 text-slate-600 hover:bg-slate-50 hover:border-slate-300 shadow-sm'"
>
<i data-lucide="history" class="w-4 h-4"></i>
<span>버전 히스토리</span>
</button>
<div class="h-8 w-px bg-slate-100 mx-2"></div>
<button
@click="deletePrompt(selectedPrompt.id)"
class="p-3 text-slate-300 hover:text-red-500 hover:bg-red-50 rounded-2xl transition-all"
title="프롬프트 연구 삭제"
>
<i data-lucide="trash-2" class="w-5 h-5"></i>
</button>
</div>
</header>
<!-- Workspace -->
<div class="flex-1 flex overflow-hidden p-8 gap-8">
<div class="flex-1 flex flex-col space-y-6 min-w-0">
<!-- Editor Container -->
<div class="bg-white rounded-[40px] shadow-2xl shadow-slate-200/40 border border-slate-100 flex-1 flex flex-col overflow-hidden research-card min-h-0">
<div class="px-8 py-5 border-b border-slate-50 flex items-center justify-between bg-slate-50/30">
<div class="flex items-center space-x-3">
<div class="w-2 h-2 rounded-full bg-indigo-500 animate-pulse-soft shadow-[0_0_8px_rgba(79,70,229,0.5)]"></div>
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest">활성 연구 워크스페이스</span>
</div>
<button
@click="optimizeWithAI()"
:disabled="isOptimizing"
class="flex items-center space-x-2.5 px-5 py-2.5 bg-gradient-to-br from-indigo-600 to-indigo-800 text-white rounded-2xl text-[11px] font-black hover:from-indigo-700 hover:to-indigo-900 disabled:opacity-50 transition-all shadow-xl shadow-indigo-100 active:scale-95 group"
>
<i data-lucide="sparkles" class="w-4 h-4 group-hover:rotate-12 transition-transform" :class="isOptimizing ? 'animate-spin' : ''"></i>
<span x-text="isOptimizing ? 'AI 연구 및 진화 중...' : 'Gemini AI로 프롬프트 고도화'"></span>
</button>
</div>
<div class="flex-1 p-0 relative min-h-0">
<textarea
x-model="editorContent"
placeholder="이곳에 프롬프트 원석을 입력하여 연구를 시작하세요..."
class="w-full h-full p-10 text-slate-800 font-mono text-lg bg-white focus:outline-none resize-none leading-[1.8]"
></textarea>
</div>
<div class="px-10 py-8 border-t border-slate-50 bg-slate-50/20">
<div class="flex items-center space-x-5">
<div class="flex-1 relative group">
<div class="absolute inset-y-0 left-5 flex items-center pointer-events-none opacity-40">
<i data-lucide="message-square" class="w-4 h-4 text-slate-400"></i>
</div>
<input
type="text"
x-model="changeSummary"
placeholder="이번 연구 단계의 변경 핵심 내용을 요약으로 기록하세요..."
class="w-full pl-14 pr-6 py-4.5 bg-white border border-slate-200/70 rounded-[22px] text-sm font-bold transition-all focus:ring-8 focus:ring-indigo-50/50 focus:border-indigo-500 outline-none"
/>
</div>
<button
@click="saveVersion()"
class="px-10 py-4.5 bg-slate-900 text-white rounded-[22px] font-black text-sm hover:bg-black transition-all shadow-[0_15px_30px_-5px_rgba(0,0,0,0.15)] active:scale-95 whitespace-nowrap"
>
연구 성과 저장
</button>
</div>
</div>
</div>
</div>
<!-- Version History Sidebar (Inside Content) -->
<div x-show="showHistory"
class="w-96 glass-panel rounded-[40px] border border-slate-200/50 overflow-hidden flex flex-col h-full shadow-2xl relative z-30"
x-transition:enter="transition ease-out duration-500"
x-transition:enter-start="translate-x-full opacity-0"
x-transition:enter-end="translate-x-0 opacity-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="translate-x-0 opacity-100"
x-transition:leave-end="translate-x-full opacity-0">
<div class="p-8 border-b border-slate-100/50 flex items-center justify-between bg-white/50">
<div>
<h3 class="font-black text-slate-900 text-xl tracking-tight">프롬프트 진화 과정</h3>
<p class="text-[10px] text-slate-400 font-black uppercase tracking-widest mt-1">Research History</p>
</div>
<button @click="showHistory = false" class="w-10 h-10 flex items-center justify-center rounded-2xl hover:bg-slate-100 text-slate-400 transition-all active:scale-90">
<i data-lucide="x" class="w-6 h-6"></i>
</button>
</div>
<div class="flex-1 overflow-y-auto p-8 space-y-8 bg-slate-50/10">
<template x-for="(v, idx) in currentPromptVersions" :key="v.id">
<div class="relative pl-10 group">
<!-- Timeline line -->
<template x-if="idx < currentPromptVersions.length - 1">
<div class="absolute left-[13px] top-8 bottom-[-32px] w-0.5 bg-slate-200/60 rounded-full"></div>
</template>
<!-- Timeline point -->
<div class="absolute left-0 top-1 w-7 h-7 rounded-full border-[6px] border-white shadow-md z-10 transition-all duration-300"
:class="v.id == selectedPrompt.current_version_id ? 'bg-indigo-600 scale-110 shadow-indigo-200' : 'bg-slate-300 group-hover:bg-slate-400'">
</div>
<div class="p-6 rounded-[28px] border transition-all duration-300 research-card"
:class="v.id == selectedPrompt.current_version_id ? 'bg-white border-indigo-100 shadow-xl' : 'bg-white/60 border-slate-100 hover:border-slate-200'">
<div class="flex items-center justify-between mb-4">
<span class="text-[10px] font-black px-3 py-1 rounded-full uppercase tracking-widest shadow-sm"
:class="v.id == selectedPrompt.current_version_id ? 'bg-indigo-600 text-white' : 'bg-slate-100 text-slate-500'"
x-text="'v' + v.version_number"></span>
<span class="text-[10px] text-slate-300 font-bold" x-text="formatDate(v.created_at)"></span>
</div>
<p class="text-xs text-slate-700 font-bold mb-4 leading-relaxed" x-text="'&ldquo;' + v.change_summary + '&rdquo;'"></p>
<button
@click="editorContent = v.content"
class="text-[10px] text-indigo-600 font-black hover:text-indigo-800 transition-colors uppercase tracking-widest flex items-center gap-2 group/btn"
>
연구 데이터 복구
<i data-lucide="corner-up-left" class="w-3.5 h-3.5 group-hover/btn:-translate-x-0.5 transition-transform"></i>
</button>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
<!-- Empty State -->
<template x-if="!selectedPrompt">
<div class="flex-1 flex flex-col items-center justify-center bg-slate-50/20 p-20 text-center">
<div class="relative mb-12">
<div class="w-40 h-40 bg-white rounded-[56px] border border-slate-100 shadow-2xl flex items-center justify-center animate-pulse-soft">
<i data-lucide="lightbulb" class="w-20 h-20 text-indigo-500"></i>
</div>
<div class="absolute -top-4 -right-4 w-12 h-12 bg-indigo-600 rounded-2xl flex items-center justify-center text-white shadow-xl rotate-12">
<i data-lucide="sparkles" class="w-6 h-6"></i>
</div>
</div>
<h3 class="text-4xl font-black text-slate-900 mb-6 tracking-tight">수행할 연구 프로젝트를 선택하세요</h3>
<p class="max-w-md text-slate-400 text-lg leading-relaxed font-bold">
사이드바에서 기존 프롬프트 과제를 선택하거나 <br />
새로운 연구 분야를 추가하여 인공지능 연구를 시작하십시오.
</p>
<div class="mt-20 flex items-center justify-center space-x-6 text-[10px] font-black uppercase tracking-[0.4em] text-slate-200">
<span class="w-12 h-px bg-slate-100"></span>
<span class="flex items-center gap-3">
AI 혁신 파트너
<span class="text-indigo-400">Codebridge-X</span>
</span>
<span class="w-12 h-px bg-slate-100"></span>
</div>
</div>
</template>
</main>
</div>
<!-- Category Modal -->
<div x-show="categoryModal.open"
class="fixed inset-0 z-[100] flex items-center justify-center p-6"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100">
<div class="absolute inset-0 modal-overlay" @click="categoryModal.open = false"></div>
<div class="relative bg-white rounded-[32px] w-full max-w-md shadow-[0_30px_60px_-12px_rgba(0,0,0,0.25)] overflow-hidden scale-100 transform active:scale-100 transition-all"
x-show="categoryModal.open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="scale-90 opacity-0"
x-transition:enter-end="scale-100 opacity-100">
<div class="p-10">
<div class="flex items-center space-x-4 mb-8">
<div class="w-14 h-14 bg-indigo-50 rounded-2xl flex items-center justify-center">
<i data-lucide="folder-plus" class="w-7 h-7 text-indigo-600"></i>
</div>
<div>
<h3 class="text-2xl font-black text-slate-900 tracking-tight" x-text="categoryModal.id ? '연구 분야 수정' : '새 연구 분야 추가'"></h3>
<p class="text-xs font-bold text-slate-400">관련 프롬프트들을 그룹화합니다</p>
</div>
</div>
<div class="space-y-6">
<div>
<label class="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2.5 ml-1">분야 이름</label>
<input
type="text"
x-model="categoryModal.name"
placeholder="예: 영업 자동화, 미드저니 연구"
class="w-full px-6 py-4.5 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold focus:ring-8 focus:ring-indigo-50 focus:border-indigo-200 outline-none transition-all"
@keyup.enter="saveCategory()"
>
</div>
<div class="flex gap-3 pt-4">
<button
@click="categoryModal.open = false"
class="flex-1 py-4.5 bg-slate-100 text-slate-500 rounded-2xl font-black text-sm hover:bg-slate-200 transition-all active:scale-95"
>
취소
</button>
<button
@click="saveCategory()"
class="flex-2 py-4.5 bg-indigo-600 text-white rounded-2xl font-black text-sm hover:bg-indigo-700 transition-all shadow-xl shadow-indigo-100 active:scale-95 px-8"
>
확인
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Prompt Modal -->
<div x-show="promptModal.open"
class="fixed inset-0 z-[100] flex items-center justify-center p-6"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100">
<div class="absolute inset-0 modal-overlay" @click="promptModal.open = false"></div>
<div class="relative bg-white rounded-[32px] w-full max-w-md shadow-[0_30px_60px_-12px_rgba(0,0,0,0.25)] overflow-hidden transition-all"
x-show="promptModal.open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="scale-90 opacity-0"
x-transition:enter-end="scale-100 opacity-100">
<div class="p-10">
<div class="flex items-center space-x-4 mb-8">
<div class="w-14 h-14 bg-indigo-50 rounded-2xl flex items-center justify-center">
<i data-lucide="file-plus" class="w-7 h-7 text-indigo-600"></i>
</div>
<div>
<h3 class="text-2xl font-black text-slate-900 tracking-tight" x-text="promptModal.id ? '연구 제목 수정' : '새 연구 추가'"></h3>
<p class="text-xs font-bold text-slate-400">구체적인 프롬프트 과제명을 입력하세요</p>
</div>
</div>
<div class="space-y-6">
<div>
<label class="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2.5 ml-1">연구 프로젝트 </label>
<input
type="text"
x-model="promptModal.name"
placeholder="예: 고객 맞춤 콜드메일 V2"
class="w-full px-6 py-4.5 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold focus:ring-8 focus:ring-indigo-50 focus:border-indigo-200 outline-none transition-all"
@keyup.enter="savePrompt()"
>
</div>
<div class="flex gap-3 pt-4">
<button
@click="promptModal.open = false"
class="flex-1 py-4.5 bg-slate-100 text-slate-500 rounded-2xl font-black text-sm hover:bg-slate-200 transition-all active:scale-95"
>
취소
</button>
<button
@click="savePrompt()"
class="flex-2 py-4.5 bg-indigo-600 text-white rounded-2xl font-black text-sm hover:bg-indigo-700 transition-all shadow-xl shadow-indigo-100 active:scale-95 px-8"
>
확인
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Initialization & App Logic -->
<script>
function prtApp() {
return {
categories: [],
prompts: [],
versions: [],
selectedPromptId: null,
expandedCategories: {},
editorContent: '',
changeSummary: '',
isOptimizing: false,
showHistory: false,
copied: false,
// Modals
categoryModal: { open: false, id: null, name: '', parentId: null },
promptModal: { open: false, id: null, categoryId: null, name: '' },
async initApp() {
await this.fetchData();
this.syncEditor();
this.$nextTick(() => {
lucide.createIcons();
});
},
async fetchData() {
try {
const res = await fetch('api/prompts.php?action=get_data');
const data = await res.json();
if (data.success) {
this.categories = data.categories;
this.prompts = data.prompts;
this.versions = data.versions;
}
} catch (e) {
console.error("Data fetch failed", e);
}
},
// --- Category Actions ---
openCategoryModal(category = null, parentId = null) {
this.categoryModal = {
open: true,
id: category ? category.id : null,
name: category ? category.name : '',
parentId: parentId || (category ? category.parent_id : null)
};
this.$nextTick(() => lucide.createIcons());
},
async saveCategory() {
if (!this.categoryModal.name.trim()) return;
try {
const res = await fetch('api/prompts.php?action=save_category', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: this.categoryModal.id,
name: this.categoryModal.name,
parent_id: this.categoryModal.parentId
})
});
const data = await res.json();
if (data.success) {
await this.fetchData();
this.categoryModal.open = false;
}
} catch (e) {
alert("저장 중 오류 발생");
}
},
async deleteCategory(id) {
if (!confirm('정말로 이 분야와 포함된 모든 연구 내용을 영구 삭제하시겠습니까?')) return;
try {
const res = await fetch('api/prompts.php?action=delete_category', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
const data = await res.json();
if (data.success) {
await this.fetchData();
if (this.selectedPrompt && this.selectedPrompt.category_id == id) {
this.selectedPromptId = null;
}
}
} catch (e) {
alert("삭제 중 오류 발생");
}
},
toggleExpand(id) {
this.expandedCategories[id] = !this.expandedCategories[id];
this.$nextTick(() => lucide.createIcons());
},
isExpanded(id) {
return !!this.expandedCategories[id];
},
// --- Prompt Actions ---
getPrompts(catId) {
return this.prompts.filter(p => p.category_id == catId);
},
openPromptModal(catId, prompt = null) {
this.promptModal = {
open: true,
id: prompt ? prompt.id : null,
categoryId: catId,
name: prompt ? prompt.name : ''
};
this.$nextTick(() => lucide.createIcons());
},
async savePrompt() {
if (!this.promptModal.name.trim()) return;
try {
const isNew = !this.promptModal.id;
const res = await fetch('api/prompts.php?action=save_prompt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: this.promptModal.id,
category_id: this.promptModal.categoryId,
name: this.promptModal.name
})
});
const data = await res.json();
if (data.success) {
await this.fetchData();
this.promptModal.open = false;
if (isNew) {
this.selectedPromptId = data.id;
this.expandedCategories[this.promptModal.categoryId] = true;
// Create initial empty version
await this.saveInitialVersion(data.id);
}
}
} catch (e) {
alert("저장 중 오류 발생");
}
},
async saveInitialVersion(promptId) {
const res = await fetch('api/prompts.php?action=save_version', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt_id: promptId,
content: '',
change_summary: '연구 인스턴스 초기화'
})
});
await this.fetchData();
this.syncEditor();
},
async deletePrompt(id) {
if (!confirm('이 프롬프트 연구 기록을 영구 삭제하시겠습니까?')) return;
try {
const res = await fetch('api/prompts.php?action=delete_prompt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
const data = await res.json();
if (data.success) {
await this.fetchData();
this.selectedPromptId = null;
}
} catch (e) {
alert("삭제 중 오류 발생");
}
},
selectPrompt(id) {
this.selectedPromptId = id;
this.showHistory = false;
this.syncEditor();
},
// --- Editor Actions ---
syncEditor() {
if (this.selectedPrompt) {
const version = this.versions.find(v => v.id == this.selectedPrompt.current_version_id);
this.editorContent = version ? version.content : '';
this.changeSummary = '';
} else {
this.editorContent = '';
this.changeSummary = '';
}
},
get selectedPrompt() {
return this.prompts.find(p => p.id == this.selectedPromptId);
},
get currentCategoryName() {
if (!this.selectedPrompt) return '';
const cat = this.categories.find(c => c.id == this.selectedPrompt.category_id);
return cat ? cat.name : '분류 없음';
},
get currentVersionNumber() {
if (!this.selectedPrompt) return 1;
const v = this.versions.find(v => v.id == this.selectedPrompt.current_version_id);
return v ? v.version_number : 1;
},
get currentPromptVersions() {
return this.versions
.filter(v => v.prompt_id == this.selectedPromptId)
.sort((a, b) => b.version_number - a.version_number);
},
async saveVersion() {
if (!this.editorContent.trim()) {
alert('연구 데이터를 입력해주세요.');
return;
}
try {
const res = await fetch('api/prompts.php?action=save_version', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt_id: this.selectedPromptId,
content: this.editorContent,
change_summary: this.changeSummary || `${this.currentVersionNumber + 1}차 진화 및 고도화`
})
});
const data = await res.json();
if (data.success) {
await this.fetchData();
this.changeSummary = '';
alert('연구 성과가 성공적으로 저장되었습니다.');
}
} catch (e) {
alert("저장 중 오류 발생");
}
},
optimizeWithAI() {
if (!this.editorContent.trim()) return;
this.isOptimizing = true;
setTimeout(() => {
const original = this.editorContent;
this.editorContent = `[Gemini Optimized Version]\n\n# Context & Persona\n당신은 ${this.currentCategoryName} 분야의 최고 고문입니다.\n\n# Objectives\n${original}\n\n# Constraints\n- 전문적인 톤앤매너 유지\n- 구체적인 수치 지표 기반 논리 전개\n- 가독성을 위한 구조화된 응답`;
this.isOptimizing = false;
this.changeSummary = "Gemini AI를 통한 페르소나 및 구조 최적화";
lucide.createIcons();
}, 1500);
},
copyToClipboard() {
if (!this.editorContent.trim()) return;
navigator.clipboard.writeText(this.editorContent).then(() => {
this.copied = true;
setTimeout(() => {
this.copied = false;
this.$nextTick(() => lucide.createIcons());
}, 2000);
this.$nextTick(() => lucide.createIcons());
});
},
renderTreeItem(category, depth) {
const subCategories = this.categories.filter(c => c.parent_id == category.id);
const prompts = this.getPrompts(category.id);
const expanded = this.isExpanded(category.id);
const paddingLeft = depth * 12;
let html = `
<div class="mb-1"
draggable="true"
ondragstart="window.prtInstance.handleDragStart(event, 'category', '${category.id}')"
ondragover="window.prtInstance.handleDragOver(event)"
ondragleave="window.prtInstance.handleDragLeave(event)"
ondrop="window.prtInstance.handleDrop(event, 'category', '${category.id}')">
<div class="group flex items-center justify-between px-4 py-2.5 rounded-2xl cursor-pointer transition-all duration-200 ${expanded ? 'bg-slate-50' : 'hover:bg-slate-50/80'}"
onclick="window.prtInstance.toggleExpand('${category.id}')" style="padding-left: ${paddingLeft + 16}px">
<div class="flex items-center space-x-3">
<span class="transform transition-transform duration-300 ${expanded ? 'rotate-90' : ''}">
<i data-lucide="chevron-right" class="w-3.5 h-3.5 text-slate-300"></i>
</span>
<i data-lucide="folder" class="w-4 h-4 ${expanded ? 'text-indigo-500' : 'text-slate-400'}"></i>
<span class="text-sm font-bold truncate ${expanded ? 'text-slate-900' : 'text-slate-500'}">${category.name}</span>
</div>
<div class="flex items-center space-x-0.5 opacity-0 group-hover:opacity-100 transition-all">
<button onclick="event.stopPropagation(); window.prtInstance.openCategoryModal(null, '${category.id}')" class="p-1.5 hover:bg-white hover:shadow-sm rounded-lg text-slate-400 hover:text-indigo-600" title="하위 분야 추가"><i data-lucide="folder-plus" class="w-3 h-3"></i></button>
<button onclick="event.stopPropagation(); window.prtInstance.openPromptModal('${category.id}')" class="p-1.5 hover:bg-white hover:shadow-sm rounded-lg text-slate-400 hover:text-indigo-600" title="프롬프트 추가"><i data-lucide="plus-square" class="w-3.5 h-3.5"></i></button>
<button onclick="event.stopPropagation(); window.prtInstance.openCategoryModal(${JSON.stringify(category).replace(/"/g, '&quot;')})" class="p-1.5 hover:bg-white hover:shadow-sm rounded-lg text-slate-400 hover:text-amber-600" title="이름 수정"><i data-lucide="edit-3" class="w-3.5 h-3.5"></i></button>
<button onclick="event.stopPropagation(); window.prtInstance.deleteCategory('${category.id}')" class="p-1.5 hover:bg-red-50 hover:text-red-500 rounded-lg text-slate-300" title="삭제"><i data-lucide="trash-2" class="w-3.5 h-3.5"></i></button>
</div>
</div>
<div x-show="isExpanded('${category.id}')" class="mt-1 ml-4 border-l border-slate-100 pl-2">
${subCategories.map(sub => this.renderTreeItem(sub, depth + 1)).join('')}
${prompts.map(p => `
<div draggable="true"
ondragstart="window.prtInstance.handleDragStart(event, 'prompt', '${p.id}')"
onclick="window.prtInstance.selectPrompt('${p.id}')"
class="flex items-center justify-between px-4 py-2.5 cursor-pointer rounded-xl text-xs transition-all duration-300 group ${this.selectedPromptId == p.id ? 'bg-indigo-600 text-white shadow-lg' : 'hover:bg-indigo-50 text-slate-500 hover:text-indigo-600 font-bold'}">
<div class="flex items-center space-x-3 truncate">
<i data-lucide="file-text" class="w-3.5 h-3.5 ${this.selectedPromptId == p.id ? 'text-white' : 'text-slate-300 group-hover:text-indigo-400'}"></i>
<span class="truncate">${p.name}</span>
</div>
<button x-show="selectedPromptId == ${p.id}" onclick="event.stopPropagation(); window.prtInstance.openPromptModal('${category.id}', ${JSON.stringify(p).replace(/"/g, '&quot;')})" class="p-1 hover:bg-indigo-500 rounded text-white/70 hover:text-white transition-all"><i data-lucide="edit-2" class="w-3 h-3"></i></button>
</div>
`).join('')}
</div>
</div>
`;
return html;
},
// --- Drag and Drop Handlers ---
handleDragStart(e, type, id) {
e.dataTransfer.setData('type', type);
e.dataTransfer.setData('id', id);
e.target.classList.add('dragging');
},
handleDragOver(e) {
e.preventDefault();
if (e.target.closest('.mb-1')) {
e.target.closest('.mb-1').classList.add('drag-over');
}
},
handleDragLeave(e) {
if (e.target.closest('.mb-1')) {
e.target.closest('.mb-1').classList.remove('drag-over');
}
},
async handleDrop(e, targetType, targetId) {
e.preventDefault();
const draggedType = e.dataTransfer.getData('type');
const draggedId = e.dataTransfer.getData('id');
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
document.querySelectorAll('.dragging').forEach(el => el.classList.remove('dragging'));
if (draggedId === targetId && draggedType === targetType) return;
try {
let action = draggedType === 'category' ? 'move_category' : 'move_prompt';
let body = {};
if (draggedType === 'category') {
body = {
id: draggedId,
parent_id: targetType === 'category' ? targetId : null,
sort_order: 0 // Simplification: set to top/first
};
} else {
body = {
id: draggedId,
category_id: targetType === 'category' ? targetId : this.prompts.find(p => p.id == targetId)?.category_id,
sort_order: 0
};
}
const res = await fetch(`api/prompts.php?action=${action}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await res.json();
if (data.success) {
await this.fetchData();
this.$nextTick(() => lucide.createIcons());
}
} catch (err) {
console.error("Drop failed", err);
}
},
async initApp() {
window.prtInstance = this;
await this.fetchData();
this.syncEditor();
this.$nextTick(() => { lucide.createIcons(); });
},
formatDate(iso) {
if (!iso) return '';
const d = new Date(iso);
return `${d.getMonth()+1}/${d.getDate()} ${d.getHours()}:${d.getMinutes().toString().padStart(2, '0')}`;
}
}
}
// Global watcher for icon updates
document.addEventListener('alpine:initialized', () => {
lucide.createIcons();
});
</script>
</body>
</html>