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 >
2026-01-31 18:47:01 +09:00
<!-- 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 : 6 px ;
height : 6 px ;
}
::- webkit - scrollbar - track {
background : transparent ;
}
::- webkit - scrollbar - thumb {
background : #e2e8f0;
border - radius : 10 px ;
}
::- webkit - scrollbar - thumb : hover {
background : #cbd5e1;
}
. glass - panel {
background : rgba ( 255 , 255 , 255 , 0.8 );
backdrop - filter : blur ( 12 px );
- webkit - backdrop - filter : blur ( 12 px );
}
. sidebar - border {
border - right : 1 px solid #f1f5f9;
}
. research - card {
transition : all 0.3 s cubic - bezier ( 0.4 , 0 , 0.2 , 1 );
}
. research - card : hover {
transform : translateY ( - 2 px );
}
. 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 2 s infinite ;
}
/* Modal backdrop animation */
. modal - overlay {
background : rgba ( 15 , 23 , 42 , 0.6 );
backdrop - filter : blur ( 4 px );
}
</ 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 >