- meta_common.php에 favicon 링크 및 로고 컴포넌트 추가 - 모든 index.php 페이지에 favicon 적용 - 일부 페이지 타이틀에서 CodeBridgeExy → CodeBridgeX 수정 - 23개 파일 일괄 업데이트 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
873 lines
47 KiB
PHP
873 lines
47 KiB
PHP
<!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">
|
|
|
|
<!-- 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="'“' + v.change_summary + '”'"></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, '"')})" 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, '"')})" 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>
|