|
|
<!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> |
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
|
background: #f8f9fa; |
|
|
color: #202124; |
|
|
} |
|
|
|
|
|
.header { |
|
|
background: white; |
|
|
border-bottom: 1px solid #dadce0; |
|
|
padding: 16px 24px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: space-between; |
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
|
|
|
.header-title { |
|
|
font-size: 20px; |
|
|
font-weight: 500; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 12px; |
|
|
} |
|
|
|
|
|
.header-actions { |
|
|
display: flex; |
|
|
gap: 12px; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.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: 6px; |
|
|
font-size: 14px; |
|
|
font-weight: 500; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s; |
|
|
text-decoration: none; |
|
|
display: inline-block; |
|
|
} |
|
|
|
|
|
.btn-primary { |
|
|
background: #1a73e8; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-primary:hover { |
|
|
background: #1557b0; |
|
|
} |
|
|
|
|
|
.btn-secondary { |
|
|
background: #f1f3f4; |
|
|
color: #202124; |
|
|
} |
|
|
|
|
|
.btn-secondary:hover { |
|
|
background: #e8eaed; |
|
|
} |
|
|
|
|
|
.container { |
|
|
max-width: 1400px; |
|
|
margin: 0 auto; |
|
|
padding: 24px; |
|
|
} |
|
|
|
|
|
.page-header { |
|
|
margin-bottom: 24px; |
|
|
} |
|
|
|
|
|
.page-header h1 { |
|
|
font-size: 28px; |
|
|
font-weight: 600; |
|
|
margin-bottom: 8px; |
|
|
} |
|
|
|
|
|
.page-header p { |
|
|
color: #5f6368; |
|
|
} |
|
|
|
|
|
.card { |
|
|
background: white; |
|
|
border-radius: 8px; |
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); |
|
|
padding: 24px; |
|
|
margin-bottom: 24px; |
|
|
} |
|
|
|
|
|
.card-header { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.card-title { |
|
|
font-size: 18px; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
.filters { |
|
|
display: flex; |
|
|
gap: 12px; |
|
|
margin-bottom: 20px; |
|
|
flex-wrap: wrap; |
|
|
} |
|
|
|
|
|
.filter-group { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 4px; |
|
|
} |
|
|
|
|
|
.filter-group label { |
|
|
font-size: 12px; |
|
|
font-weight: 500; |
|
|
color: #5f6368; |
|
|
} |
|
|
|
|
|
.filter-group select, |
|
|
.filter-group input { |
|
|
padding: 8px 12px; |
|
|
border: 1px solid #dadce0; |
|
|
border-radius: 6px; |
|
|
font-size: 14px; |
|
|
font-family: inherit; |
|
|
} |
|
|
|
|
|
.filter-group select:focus, |
|
|
.filter-group input:focus { |
|
|
outline: none; |
|
|
border-color: #1a73e8; |
|
|
box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1); |
|
|
} |
|
|
|
|
|
table { |
|
|
width: 100%; |
|
|
border-collapse: collapse; |
|
|
} |
|
|
|
|
|
thead { |
|
|
background: #f8f9fa; |
|
|
} |
|
|
|
|
|
th, td { |
|
|
padding: 12px; |
|
|
text-align: left; |
|
|
border-bottom: 1px solid #e8eaed; |
|
|
} |
|
|
|
|
|
th { |
|
|
font-weight: 500; |
|
|
font-size: 14px; |
|
|
color: #5f6368; |
|
|
} |
|
|
|
|
|
td { |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
.message-content { |
|
|
max-width: 500px; |
|
|
overflow: hidden; |
|
|
text-overflow: ellipsis; |
|
|
white-space: nowrap; |
|
|
} |
|
|
|
|
|
.message-content-full { |
|
|
max-width: none; |
|
|
white-space: normal; |
|
|
word-wrap: break-word; |
|
|
} |
|
|
|
|
|
.role-badge { |
|
|
display: inline-block; |
|
|
padding: 4px 8px; |
|
|
border-radius: 4px; |
|
|
font-size: 12px; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
.role-user { |
|
|
background: #e8f0fe; |
|
|
color: #1967d2; |
|
|
} |
|
|
|
|
|
.role-ai { |
|
|
background: #e8f5e9; |
|
|
color: #137333; |
|
|
} |
|
|
|
|
|
.pagination { |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
gap: 8px; |
|
|
margin-top: 20px; |
|
|
} |
|
|
|
|
|
.pagination button { |
|
|
padding: 8px 12px; |
|
|
border: 1px solid #dadce0; |
|
|
background: white; |
|
|
border-radius: 6px; |
|
|
cursor: pointer; |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
.pagination button:hover:not(:disabled) { |
|
|
background: #f1f3f4; |
|
|
} |
|
|
|
|
|
.pagination button:disabled { |
|
|
opacity: 0.5; |
|
|
cursor: not-allowed; |
|
|
} |
|
|
|
|
|
.pagination .page-info { |
|
|
padding: 8px 12px; |
|
|
color: #5f6368; |
|
|
} |
|
|
|
|
|
.modal { |
|
|
display: none; |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background: rgba(0, 0, 0, 0.5); |
|
|
z-index: 1000; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
.modal.active { |
|
|
display: flex; |
|
|
} |
|
|
|
|
|
.modal-content { |
|
|
background: white; |
|
|
border-radius: 8px; |
|
|
padding: 24px; |
|
|
width: 90%; |
|
|
max-width: 800px; |
|
|
max-height: 90vh; |
|
|
overflow-y: auto; |
|
|
} |
|
|
|
|
|
.modal-header { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.modal-title { |
|
|
font-size: 20px; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
.modal-close { |
|
|
background: none; |
|
|
border: none; |
|
|
font-size: 24px; |
|
|
cursor: pointer; |
|
|
color: #5f6368; |
|
|
} |
|
|
|
|
|
.message-view { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 16px; |
|
|
} |
|
|
|
|
|
.message-item { |
|
|
padding: 12px; |
|
|
border-radius: 8px; |
|
|
border-left: 4px solid; |
|
|
} |
|
|
|
|
|
.message-item.user { |
|
|
background: #e8f0fe; |
|
|
border-color: #1a73e8; |
|
|
} |
|
|
|
|
|
.message-item.ai { |
|
|
background: #e8f5e9; |
|
|
border-color: #34a853; |
|
|
} |
|
|
|
|
|
.message-item-header { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
margin-bottom: 8px; |
|
|
} |
|
|
|
|
|
.message-item-role { |
|
|
font-weight: 500; |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
.message-item-time { |
|
|
font-size: 12px; |
|
|
color: #5f6368; |
|
|
} |
|
|
|
|
|
.message-item-content { |
|
|
white-space: pre-wrap; |
|
|
word-wrap: break-word; |
|
|
line-height: 1.6; |
|
|
} |
|
|
|
|
|
.alert { |
|
|
padding: 12px 16px; |
|
|
border-radius: 6px; |
|
|
margin-bottom: 16px; |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
.alert.error { |
|
|
background: #fce8e6; |
|
|
color: #c5221f; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="header"> |
|
|
<div class="header-title"> |
|
|
<span>π€</span> |
|
|
<span>SOY NV AI λ©μμ§ νμΈ</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_prompts') }}" 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="λ©λ΄ λ«κΈ°">×</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_prompts') }}" 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 id="alertContainer"></div> |
|
|
|
|
|
|
|
|
<div class="card"> |
|
|
<div class="card-header"> |
|
|
<div class="card-title">λν μΈμ
λͺ©λ‘</div> |
|
|
</div> |
|
|
|
|
|
<div class="filters"> |
|
|
<div class="filter-group"> |
|
|
<label>μ¬μ©μ νν°</label> |
|
|
<select id="userFilter"> |
|
|
<option value="">μ 체 μ¬μ©μ</option> |
|
|
</select> |
|
|
</div> |
|
|
<div class="filter-group"> |
|
|
<label>κ²μ</label> |
|
|
<input type="text" id="searchInput" placeholder="μΈμ
μ λͺ© κ²μ..."> |
|
|
</div> |
|
|
<div class="filter-group" style="align-items: flex-end;"> |
|
|
<button class="btn btn-primary" onclick="loadSessions()">μ‘°ν</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<table> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>ID</th> |
|
|
<th>μ¬μ©μ</th> |
|
|
<th>μ λͺ©</th> |
|
|
<th>νμ΅ λͺ¨λΈ</th> |
|
|
<th>λ΅λ³ λͺ¨λΈ</th> |
|
|
<th>λ©μμ§ μ</th> |
|
|
<th>μμ±μΌ</th> |
|
|
<th>μμ μΌ</th> |
|
|
<th>μμ
</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody id="sessionsTableBody"> |
|
|
<tr> |
|
|
<td colspan="8" style="text-align: center; padding: 20px; color: #5f6368;">λ‘λ© μ€...</td> |
|
|
</tr> |
|
|
</tbody> |
|
|
</table> |
|
|
|
|
|
<div class="pagination" id="sessionsPagination"></div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="card"> |
|
|
<div class="card-header"> |
|
|
<div class="card-title">λ©μμ§ λͺ©λ‘</div> |
|
|
</div> |
|
|
|
|
|
<div class="filters"> |
|
|
<div class="filter-group"> |
|
|
<label>μΈμ
ID</label> |
|
|
<input type="number" id="sessionIdFilter" placeholder="μΈμ
ID"> |
|
|
</div> |
|
|
<div class="filter-group" style="align-items: flex-end;"> |
|
|
<button class="btn btn-primary" onclick="loadMessages()">μ‘°ν</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<table> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>ID</th> |
|
|
<th>μΈμ
ID</th> |
|
|
<th>μν </th> |
|
|
<th>λ΄μ©</th> |
|
|
<th>μκ°</th> |
|
|
<th>μμ
</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody id="messagesTableBody"> |
|
|
<tr> |
|
|
<td colspan="6" style="text-align: center; padding: 20px; color: #5f6368;">μΈμ
μ μ ννκ±°λ νν°λ₯Ό μ¬μ©νμΈμ</td> |
|
|
</tr> |
|
|
</tbody> |
|
|
</table> |
|
|
|
|
|
<div class="pagination" id="messagesPagination"></div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="messageModal" class="modal"> |
|
|
<div class="modal-content"> |
|
|
<div class="modal-header"> |
|
|
<div class="modal-title">λ©μμ§ μμΈ</div> |
|
|
<button class="modal-close" onclick="closeMessageModal()">×</button> |
|
|
</div> |
|
|
<div id="messageModalContent"></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(); |
|
|
} |
|
|
} |
|
|
|
|
|
let currentSessionsPage = 1; |
|
|
let currentMessagesPage = 1; |
|
|
let selectedSessionId = null; |
|
|
|
|
|
function showAlert(message, type = 'error') { |
|
|
const container = document.getElementById('alertContainer'); |
|
|
container.innerHTML = `<div class="alert ${type}">${message}</div>`; |
|
|
setTimeout(() => { |
|
|
container.innerHTML = ''; |
|
|
}, 5000); |
|
|
} |
|
|
|
|
|
|
|
|
async function loadUsers() { |
|
|
try { |
|
|
const response = await fetch('/api/admin/users'); |
|
|
const data = await response.json(); |
|
|
|
|
|
const userFilter = document.getElementById('userFilter'); |
|
|
userFilter.innerHTML = '<option value="">μ 체 μ¬μ©μ</option>'; |
|
|
|
|
|
if (data.users) { |
|
|
data.users.forEach(user => { |
|
|
const option = document.createElement('option'); |
|
|
option.value = user.id; |
|
|
option.textContent = `${user.nickname || user.username} (${user.username})`; |
|
|
userFilter.appendChild(option); |
|
|
}); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('μ¬μ©μ λͺ©λ‘ λ‘λ μ€λ₯:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function loadSessions(page = 1) { |
|
|
try { |
|
|
const userId = document.getElementById('userFilter').value; |
|
|
const search = document.getElementById('searchInput').value; |
|
|
|
|
|
let url = `/api/admin/sessions?page=${page}&per_page=20`; |
|
|
if (userId) { |
|
|
url += `&user_id=${userId}`; |
|
|
} |
|
|
|
|
|
const response = await fetch(url); |
|
|
const data = await response.json(); |
|
|
|
|
|
const tbody = document.getElementById('sessionsTableBody'); |
|
|
tbody.innerHTML = ''; |
|
|
|
|
|
if (data.sessions && data.sessions.length > 0) { |
|
|
data.sessions.forEach(session => { |
|
|
const row = document.createElement('tr'); |
|
|
const createdDate = new Date(session.created_at).toLocaleString('ko-KR'); |
|
|
const updatedDate = new Date(session.updated_at).toLocaleString('ko-KR'); |
|
|
|
|
|
|
|
|
if (search && !session.title.toLowerCase().includes(search.toLowerCase())) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const analysisModel = session.analysis_model || session.model_name || '-'; |
|
|
const answerModel = session.answer_model || session.model_name || '-'; |
|
|
|
|
|
row.innerHTML = ` |
|
|
<td>${session.id}</td> |
|
|
<td>${session.nickname || session.username || 'Unknown'}</td> |
|
|
<td>${session.title || '-'}</td> |
|
|
<td>${analysisModel}</td> |
|
|
<td>${answerModel}</td> |
|
|
<td>${session.message_count || 0}</td> |
|
|
<td>${createdDate}</td> |
|
|
<td>${updatedDate}</td> |
|
|
<td> |
|
|
<button class="btn btn-secondary" onclick="viewSessionMessages(${session.id})" style="padding: 4px 8px; font-size: 12px;">λ©μμ§ λ³΄κΈ°</button> |
|
|
</td> |
|
|
`; |
|
|
tbody.appendChild(row); |
|
|
}); |
|
|
} else { |
|
|
tbody.innerHTML = '<tr><td colspan="9" style="text-align: center; padding: 20px; color: #5f6368;">λν μΈμ
μ΄ μμ΅λλ€.</td></tr>'; |
|
|
} |
|
|
|
|
|
|
|
|
updateSessionsPagination(data.current_page, data.pages); |
|
|
currentSessionsPage = data.current_page; |
|
|
|
|
|
} catch (error) { |
|
|
showAlert(`λν μΈμ
μ‘°ν μ€λ₯: ${error.message}`, 'error'); |
|
|
console.error('λν μΈμ
μ‘°ν μ€λ₯:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function loadMessages(page = 1) { |
|
|
const tbody = document.getElementById('messagesTableBody'); |
|
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 20px; color: #5f6368;">λ‘λ© μ€...</td></tr>'; |
|
|
|
|
|
try { |
|
|
const sessionId = document.getElementById('sessionIdFilter').value; |
|
|
|
|
|
let url = `/api/admin/messages?page=${page}&per_page=50`; |
|
|
if (sessionId) { |
|
|
url += `&session_id=${sessionId}`; |
|
|
} else if (selectedSessionId) { |
|
|
url += `&session_id=${selectedSessionId}`; |
|
|
} |
|
|
|
|
|
console.log('[λ©μμ§ λͺ©λ‘] μμ² URL:', url); |
|
|
const response = await fetch(url); |
|
|
|
|
|
if (!response.ok) { |
|
|
const errorData = await response.json().catch(() => ({ error: `HTTP ${response.status}` })); |
|
|
throw new Error(errorData.error || `HTTP ${response.status}`); |
|
|
} |
|
|
|
|
|
const data = await response.json(); |
|
|
console.log('[λ©μμ§ λͺ©λ‘] μλ΅ λ°μ΄ν°:', data); |
|
|
|
|
|
tbody.innerHTML = ''; |
|
|
|
|
|
if (data.messages && data.messages.length > 0) { |
|
|
console.log('[λ©μμ§ λͺ©λ‘] λ©μμ§ κ°μ:', data.messages.length); |
|
|
data.messages.forEach(msg => { |
|
|
const row = document.createElement('tr'); |
|
|
const date = new Date(msg.created_at).toLocaleString('ko-KR'); |
|
|
const contentPreview = msg.content && msg.content.length > 100 ? msg.content.substring(0, 100) + '...' : (msg.content || ''); |
|
|
|
|
|
row.innerHTML = ` |
|
|
<td>${msg.id}</td> |
|
|
<td>${msg.session_id}</td> |
|
|
<td><span class="role-badge role-${msg.role}">${msg.role === 'user' ? 'μ¬μ©μ' : 'AI'}</span></td> |
|
|
<td class="message-content">${escapeHtml(contentPreview)}</td> |
|
|
<td>${date}</td> |
|
|
<td> |
|
|
<button class="btn btn-secondary" onclick="viewMessageById(${msg.id})" style="padding: 4px 8px; font-size: 12px;">μμΈ λ³΄κΈ°</button> |
|
|
</td> |
|
|
`; |
|
|
|
|
|
row.setAttribute('data-message-id', msg.id); |
|
|
row.setAttribute('data-message-role', msg.role); |
|
|
row.setAttribute('data-message-content', escapeHtml(msg.content || '')); |
|
|
tbody.appendChild(row); |
|
|
}); |
|
|
} else { |
|
|
console.log('[λ©μμ§ λͺ©λ‘] λ©μμ§κ° μμ΅λλ€'); |
|
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 20px; color: #5f6368;">λ©μμ§κ° μμ΅λλ€.</td></tr>'; |
|
|
} |
|
|
|
|
|
|
|
|
if (data.current_page !== undefined && data.pages !== undefined) { |
|
|
updateMessagesPagination(data.current_page, data.pages); |
|
|
currentMessagesPage = data.current_page; |
|
|
} else { |
|
|
|
|
|
document.getElementById('messagesPagination').innerHTML = ''; |
|
|
} |
|
|
|
|
|
} catch (error) { |
|
|
console.error('[λ©μμ§ λͺ©λ‘] μ‘°ν μ€λ₯:', error); |
|
|
showAlert(`λ©μμ§ μ‘°ν μ€λ₯: ${error.message}`, 'error'); |
|
|
tbody.innerHTML = `<tr><td colspan="6" style="text-align: center; padding: 20px; color: #ea4335;">λ©μμ§ μ‘°ν μ€ μ€λ₯κ° λ°μνμ΅λλ€.<br><small>${error.message || 'μ μ μλ μ€λ₯'}</small></td></tr>`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function viewSessionMessages(sessionId) { |
|
|
selectedSessionId = sessionId; |
|
|
document.getElementById('sessionIdFilter').value = sessionId; |
|
|
loadMessages(1); |
|
|
} |
|
|
|
|
|
|
|
|
async function viewMessageById(messageId) { |
|
|
try { |
|
|
const response = await fetch(`/api/admin/messages?page=1&per_page=1&message_id=${messageId}`); |
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.messages && data.messages.length > 0) { |
|
|
const msg = data.messages[0]; |
|
|
viewMessage(msg.id, msg.role, msg.content); |
|
|
} else { |
|
|
|
|
|
const row = document.querySelector(`tr[data-message-id="${messageId}"]`); |
|
|
if (row) { |
|
|
const role = row.getAttribute('data-message-role'); |
|
|
const content = row.getAttribute('data-message-content'); |
|
|
viewMessage(messageId, role, content); |
|
|
} else { |
|
|
showAlert('λ©μμ§λ₯Ό μ°Ύμ μ μμ΅λλ€.', 'error'); |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
|
|
|
const row = document.querySelector(`tr[data-message-id="${messageId}"]`); |
|
|
if (row) { |
|
|
const role = row.getAttribute('data-message-role'); |
|
|
const content = row.getAttribute('data-message-content'); |
|
|
viewMessage(messageId, role, content); |
|
|
} else { |
|
|
showAlert(`λ©μμ§ μ‘°ν μ€λ₯: ${error.message}`, 'error'); |
|
|
console.error('λ©μμ§ μ‘°ν μ€λ₯:', error); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function viewMessage(messageId, role, content) { |
|
|
const modal = document.getElementById('messageModal'); |
|
|
const modalContent = document.getElementById('messageModalContent'); |
|
|
|
|
|
modalContent.innerHTML = ` |
|
|
<div class="message-view"> |
|
|
<div class="message-item ${role}"> |
|
|
<div class="message-item-header"> |
|
|
<span class="message-item-role role-badge role-${role}">${role === 'user' ? 'μ¬μ©μ' : 'AI'}</span> |
|
|
<span class="message-item-time">λ©μμ§ ID: ${messageId}</span> |
|
|
</div> |
|
|
<div class="message-item-content">${content}</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
modal.classList.add('active'); |
|
|
} |
|
|
|
|
|
function closeMessageModal() { |
|
|
document.getElementById('messageModal').classList.remove('active'); |
|
|
} |
|
|
|
|
|
|
|
|
function updateSessionsPagination(currentPage, totalPages) { |
|
|
const pagination = document.getElementById('sessionsPagination'); |
|
|
pagination.innerHTML = ''; |
|
|
|
|
|
pagination.appendChild(createPaginationButton('μ΄μ ', currentPage > 1, () => loadSessions(currentPage - 1))); |
|
|
pagination.appendChild(createPaginationInfo(`${currentPage} / ${totalPages}`)); |
|
|
pagination.appendChild(createPaginationButton('λ€μ', currentPage < totalPages, () => loadSessions(currentPage + 1))); |
|
|
} |
|
|
|
|
|
function updateMessagesPagination(currentPage, totalPages) { |
|
|
const pagination = document.getElementById('messagesPagination'); |
|
|
pagination.innerHTML = ''; |
|
|
|
|
|
pagination.appendChild(createPaginationButton('μ΄μ ', currentPage > 1, () => loadMessages(currentPage - 1))); |
|
|
pagination.appendChild(createPaginationInfo(`${currentPage} / ${totalPages}`)); |
|
|
pagination.appendChild(createPaginationButton('λ€μ', currentPage < totalPages, () => loadMessages(currentPage + 1))); |
|
|
} |
|
|
|
|
|
function createPaginationButton(text, enabled, onClick) { |
|
|
const button = document.createElement('button'); |
|
|
button.textContent = text; |
|
|
button.disabled = !enabled; |
|
|
if (enabled) { |
|
|
button.onclick = onClick; |
|
|
} |
|
|
return button; |
|
|
} |
|
|
|
|
|
function createPaginationInfo(text) { |
|
|
const span = document.createElement('span'); |
|
|
span.className = 'page-info'; |
|
|
span.textContent = text; |
|
|
return span; |
|
|
} |
|
|
|
|
|
function escapeHtml(text) { |
|
|
const div = document.createElement('div'); |
|
|
div.textContent = text; |
|
|
return div.innerHTML; |
|
|
} |
|
|
|
|
|
|
|
|
document.getElementById('messageModal').addEventListener('click', function(e) { |
|
|
if (e.target === this) { |
|
|
closeMessageModal(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('searchInput').addEventListener('keypress', function(e) { |
|
|
if (e.key === 'Enter') { |
|
|
loadSessions(1); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
window.addEventListener('load', () => { |
|
|
loadUsers(); |
|
|
loadSessions(); |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|
|
|
|
|
|
|