soyailabs / templates /admin_prompts.html
SOY NV AI
Add Hugging Face Spaces deployment support and file public/private feature
ae31891
raw
history blame
15.2 kB
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ν”„λ‘¬ν”„νŠΈ 관리 - SOY NV AI</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f5f5f5;
color: #202124;
line-height: 1.6;
}
.header {
background: #ffffff;
border-bottom: 1px solid #dadce0;
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.header-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: 500;
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.menu-toggle {
display: none;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
padding: 8px;
color: #202124;
}
.mobile-menu {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.mobile-menu.active {
display: block;
}
.mobile-menu-content {
position: fixed;
top: 0;
right: -100%;
width: 280px;
max-width: 80%;
height: 100%;
background: white;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
transition: right 0.3s ease;
overflow-y: auto;
z-index: 1001;
}
.mobile-menu.active .mobile-menu-content {
right: 0;
}
.mobile-menu-header {
padding: 16px 20px;
border-bottom: 1px solid #dadce0;
display: flex;
justify-content: space-between;
align-items: center;
background: white;
position: sticky;
top: 0;
z-index: 10;
}
.mobile-menu-title {
font-size: 18px;
font-weight: 500;
}
.mobile-menu-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #202124;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.mobile-menu-items {
padding: 8px 0;
}
.mobile-menu-item {
display: block;
padding: 12px 20px;
color: #202124;
text-decoration: none;
border-bottom: 1px solid #f1f3f4;
transition: background 0.2s;
}
.mobile-menu-item:hover {
background: #f8f9fa;
}
.mobile-menu-user {
padding: 16px 20px;
border-bottom: 1px solid #dadce0;
color: #5f6368;
font-size: 14px;
}
@media (max-width: 768px) {
.header {
padding: 12px 16px;
}
.header-title {
font-size: 18px;
}
.header-title span:first-child {
display: none;
}
.menu-toggle {
display: block;
}
.header-actions {
display: none;
}
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
text-decoration: none;
display: inline-block;
transition: background-color 0.2s;
}
.btn-primary {
background: #1a73e8;
color: white;
}
.btn-primary:hover {
background: #1557b0;
}
.btn-secondary {
background: #f1f3f4;
color: #202124;
}
.btn-secondary:hover {
background: #e8eaed;
}
.btn-success {
background: #34a853;
color: white;
}
.btn-success:hover {
background: #2d8e47;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
font-size: 24px;
margin-bottom: 8px;
}
.page-header p {
color: #5f6368;
}
.prompt-editor {
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
margin-bottom: 24px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #202124;
}
.form-group textarea {
width: 100%;
min-height: 300px;
padding: 12px;
border: 1px solid #dadce0;
border-radius: 4px;
font-size: 14px;
font-family: 'Consolas', 'Monaco', monospace;
resize: vertical;
line-height: 1.5;
}
.form-group textarea:focus {
outline: none;
border-color: #1a73e8;
box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.1);
}
.form-help {
margin-top: 8px;
font-size: 12px;
color: #5f6368;
}
.alert {
padding: 12px 16px;
border-radius: 4px;
margin-bottom: 16px;
display: none;
}
.alert.success {
background: #e8f5e9;
color: #137333;
border: 1px solid #34a853;
}
.alert.error {
background: #fce8e6;
color: #c5221f;
border: 1px solid #ea4335;
}
.alert.show {
display: block;
}
.button-group {
display: flex;
gap: 12px;
margin-top: 24px;
}
.prompt-info {
background: #f8f9fa;
border-left: 4px solid #1a73e8;
padding: 16px;
border-radius: 4px;
margin-bottom: 24px;
}
.prompt-info h3 {
margin-bottom: 8px;
font-size: 16px;
}
.prompt-info p {
margin-bottom: 4px;
font-size: 14px;
color: #5f6368;
}
</style>
</head>
<body>
<div class="header">
<div class="header-title">
<span>πŸ“</span>
<span>ν”„λ‘¬ν”„νŠΈ 관리</span>
</div>
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="메뉴 μ—΄κΈ°">☰</button>
<div class="header-actions">
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
<a href="{{ url_for('main.admin') }}" class="btn btn-secondary">μ‚¬μš©μž 관리</a>
<a href="{{ url_for('main.admin_webnovels') }}" class="btn btn-secondary">μ›Ήμ†Œμ„€ 관리</a>
<a href="{{ url_for('main.admin_files') }}" class="btn btn-secondary">파일 λͺ©λ‘</a>
<a href="{{ url_for('main.admin_messages') }}" class="btn btn-secondary">λ©”μ‹œμ§€ 확인</a>
<a href="{{ url_for('main.admin_settings') }}" class="btn btn-secondary">AI μ„€μ •</a>
<a href="{{ url_for('main.index') }}" class="btn btn-secondary">λ©”μΈμœΌλ‘œ</a>
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary">λ‘œκ·Έμ•„μ›ƒ</a>
</div>
</div>
<!-- λͺ¨λ°”일 메뉴 -->
<div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
<div class="mobile-menu-content" onclick="event.stopPropagation()">
<div class="mobile-menu-header">
<div class="mobile-menu-title">메뉴</div>
<button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="메뉴 λ‹«κΈ°">&times;</button>
</div>
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
<div class="mobile-menu-items">
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μ‚¬μš©μž 관리</a>
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μ›Ήμ†Œμ„€ 관리</a>
<a href="{{ url_for('main.admin_files') }}" class="mobile-menu-item" onclick="closeMobileMenu()">파일 λͺ©λ‘</a>
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ©”μ‹œμ§€ 확인</a>
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI μ„€μ •</a>
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ©”μΈμœΌλ‘œ</a>
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ‘œκ·Έμ•„μ›ƒ</a>
</div>
</div>
</div>
<div class="container">
<div class="page-header">
<h1>μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ 관리</h1>
<p>μ§ˆλ¬Έν•  λ•Œ μžλ™μœΌλ‘œ λΆ™μ΄λŠ” ν”„λ‘¬ν”„νŠΈλ₯Ό μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이 ν”„λ‘¬ν”„νŠΈλŠ” λŒ€ν™” λ©”μ‹œμ§€μ—λŠ” ν‘œμ‹œλ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.</p>
</div>
<div class="alert" id="alert"></div>
<div class="prompt-info">
<h3>πŸ’‘ ν”„λ‘¬ν”„νŠΈ μ‚¬μš© 방법</h3>
<p>β€’ μ„€μ •ν•œ ν”„λ‘¬ν”„νŠΈλŠ” λͺ¨λ“  질문 μ•žμ— μžλ™μœΌλ‘œ λΆ™μ–΄μ„œ AIμ—κ²Œ μ „λ‹¬λ©λ‹ˆλ‹€.</p>
<p>β€’ ν”„λ‘¬ν”„νŠΈλŠ” μ‚¬μš©μžμ—κ²ŒλŠ” 보이지 μ•ŠμœΌλ©°, AI 응닡 ν’ˆμ§ˆ ν–₯상을 μœ„ν•΄ μ‚¬μš©λ©λ‹ˆλ‹€.</p>
<p>β€’ 예: "항상 μ •μ€‘ν•˜κ²Œ λ‹΅λ³€ν•˜μ„Έμš”." λ˜λŠ” "μ›Ήμ†Œμ„€μ˜ λ§₯락을 κ³ λ €ν•˜μ—¬ λ‹΅λ³€ν•˜μ„Έμš”."</p>
</div>
<div class="prompt-editor">
<div class="form-group">
<label for="promptContent">μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ</label>
<textarea id="promptContent" placeholder="예: 항상 μ •μ€‘ν•˜κ³  μƒμ„Έν•˜κ²Œ λ‹΅λ³€ν•˜μ„Έμš”. μ›Ήμ†Œμ„€μ˜ λ§₯락을 κ³ λ €ν•˜μ—¬ 일관성 μžˆλŠ” 닡변을 μ œκ³΅ν•˜μ„Έμš”."></textarea>
<div class="form-help">
질문 μ•žμ— μžλ™μœΌλ‘œ 좔가될 ν”„λ‘¬ν”„νŠΈλ₯Ό μž…λ ₯ν•˜μ„Έμš”. λΉ„μ›Œλ‘λ©΄ ν”„λ‘¬ν”„νŠΈλ₯Ό μ‚¬μš©ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
</div>
</div>
<div class="button-group">
<button class="btn btn-primary" onclick="savePrompt()">μ €μž₯</button>
<button class="btn btn-secondary" onclick="loadPrompt()">μƒˆλ‘œκ³ μΉ¨</button>
<button class="btn btn-secondary" onclick="clearPrompt()">μ΄ˆκΈ°ν™”</button>
</div>
</div>
</div>
<script>
function toggleMobileMenu() {
const menu = document.getElementById('mobileMenu');
menu.classList.toggle('active');
document.body.style.overflow = menu.classList.contains('active') ? 'hidden' : '';
}
function closeMobileMenu() {
const menu = document.getElementById('mobileMenu');
menu.classList.remove('active');
document.body.style.overflow = '';
}
function closeMobileMenuOnBackdrop(event) {
if (event.target.id === 'mobileMenu') {
closeMobileMenu();
}
}
// νŽ˜μ΄μ§€ λ‘œλ“œ μ‹œ ν”„λ‘¬ν”„νŠΈ 뢈러였기
window.addEventListener('DOMContentLoaded', () => {
loadPrompt();
});
// ν”„λ‘¬ν”„νŠΈ 뢈러였기
async function loadPrompt() {
try {
const response = await fetch('/api/admin/prompts', {
credentials: 'include'
});
const data = await response.json();
if (response.ok) {
document.getElementById('promptContent').value = data.prompt || '';
} else {
showAlert('ν”„λ‘¬ν”„νŠΈλ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ' + (data.error || 'μ•Œ 수 μ—†λŠ” 였λ₯˜'), 'error');
}
} catch (error) {
showAlert('ν”„λ‘¬ν”„νŠΈλ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ' + error.message, 'error');
console.error('ν”„λ‘¬ν”„νŠΈ 뢈러였기 였λ₯˜:', error);
}
}
// ν”„λ‘¬ν”„νŠΈ μ €μž₯
async function savePrompt() {
const promptContent = document.getElementById('promptContent').value.trim();
try {
const response = await fetch('/api/admin/prompts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
prompt: promptContent
})
});
const data = await response.json();
if (response.ok) {
showAlert('ν”„λ‘¬ν”„νŠΈκ°€ μ„±κ³΅μ μœΌλ‘œ μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€.', 'success');
} else {
showAlert('ν”„λ‘¬ν”„νŠΈ μ €μž₯ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ' + (data.error || 'μ•Œ 수 μ—†λŠ” 였λ₯˜'), 'error');
}
} catch (error) {
showAlert('ν”„λ‘¬ν”„νŠΈ μ €μž₯ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ' + error.message, 'error');
console.error('ν”„λ‘¬ν”„νŠΈ μ €μž₯ 였λ₯˜:', error);
}
}
// ν”„λ‘¬ν”„νŠΈ μ΄ˆκΈ°ν™”
function clearPrompt() {
if (confirm('ν”„λ‘¬ν”„νŠΈλ₯Ό μ΄ˆκΈ°ν™”ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?')) {
document.getElementById('promptContent').value = '';
}
}
// μ•Œλ¦Ό ν‘œμ‹œ
function showAlert(message, type) {
const alert = document.getElementById('alert');
alert.textContent = message;
alert.className = `alert ${type} show`;
setTimeout(() => {
alert.classList.remove('show');
}, 5000);
}
</script>
</body>
</html>