soyailabs / templates /admin_messages.html
GitHub Actions
Auto-deploy from GitHub Actions - 2025-12-12 16:41:27
1995f8f
<!DOCTYPE html>
<html lang="ko">
<head>
<script type="text/javascript">
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "ujskfvh0bu");
</script>
<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: 8px;
align-items: center;
flex-wrap: wrap;
}
/* ๋“œ๋กญ๋‹ค์šด ๋ฉ”๋‰ด ์Šคํƒ€์ผ */
.dropdown {
position: relative;
display: inline-block;
}
/* ๋ฒ„ํŠผ๊ณผ ๋ฉ”๋‰ด ์‚ฌ์ด 'ํ‹ˆ'์—์„œ hover๊ฐ€ ๋Š๊ฒจ ๋ฉ”๋‰ด๊ฐ€ ๋‹ซํžˆ๋Š” ํ˜„์ƒ ๋ฐฉ์ง€ */
.dropdown::after {
content: '';
position: absolute;
left: 0;
right: 0;
top: 100%;
height: 8px;
}
.dropdown-toggle {
padding: 8px 16px;
background: #f1f3f4;
color: #202124;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
.dropdown-toggle:hover {
background: #e8eaed;
}
.dropdown-toggle::after {
content: 'โ–ผ';
font-size: 10px;
transition: transform 0.2s;
}
.dropdown:hover .dropdown-toggle::after {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
top: calc(100% + 4px);
left: 0;
margin-top: 0;
background: white;
border: 1px solid #dadce0;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 200px;
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all 0.2s ease;
z-index: 10000;
padding: 4px 0;
pointer-events: none;
}
.dropdown:hover .dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
pointer-events: auto;
}
.dropdown-item {
display: block;
padding: 10px 16px;
color: #202124;
text-decoration: none;
font-size: 14px;
transition: background 0.2s;
}
.dropdown-item:hover {
background: #f8f9fa;
}
.dropdown-item:first-child {
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
.dropdown-item:last-child {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
}
.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>
{# ์‚ฌ์ดํŠธ ๊ด€๋ฆฌ #}
<div class="dropdown">
<button type="button" class="dropdown-toggle">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</button>
<div class="dropdown-menu">
<a href="{{ url_for('main.admin') }}" class="dropdown-item">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
<a href="{{ url_for('main.admin_tokens') }}" class="dropdown-item">ํ† ํฐ ํ†ต๊ณ„</a>
</div>
</div>
{# ์›น์†Œ์„ค ๊ด€๋ฆฌ #}
<div class="dropdown">
<button type="button" class="dropdown-toggle">์›น์†Œ์„ค ๊ด€๋ฆฌ</button>
<div class="dropdown-menu">
<a href="{{ url_for('main.admin_webnovels') }}" class="dropdown-item">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
<a href="{{ url_for('main.admin_messages') }}" class="dropdown-item">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
</div>
</div>
{# AI ์„ค์ • #}
<div class="dropdown">
<button type="button" class="dropdown-toggle">AI ์„ค์ •</button>
<div class="dropdown-menu">
<a href="{{ url_for('main.admin_settings') }}" class="dropdown-item">AI ์„ค์ •</a>
<a href="{{ url_for('main.admin_prompts') }}" class="dropdown-item">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
</div>
</div>
{# ์ฑ—๋ด‡ #}
<div class="dropdown">
<button type="button" class="dropdown-toggle">์ฑ—๋ด‡</button>
<div class="dropdown-menu">
<a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
<a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
</div>
</div>
{# ํŽธ์˜๊ธฐ๋Šฅ #}
<div class="dropdown">
<button type="button" class="dropdown-toggle">ํŽธ์˜๊ธฐ๋Šฅ</button>
<div class="dropdown-menu">
<a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์œ ํ‹ธ</a>
</div>
</div>
{# ๋ฉ”์ธ์œผ๋กœ #}
<a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ”์ธ์œผ๋กœ</a>
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋กœ๊ทธ์•„์›ƒ</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">
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</div>
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
<a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ† ํฐ ํ†ต๊ณ„</a>
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">์›น์†Œ์„ค ๊ด€๋ฆฌ</div>
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">AI ์„ค์ •</div>
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์„ค์ •</a>
<a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">์ฑ—๋ด‡</div>
<a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
<a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">ํŽธ์˜๊ธฐ๋Šฅ</div>
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์œ ํ‹ธ</a>
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">๊ธฐํƒ€</div>
<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>
<th>์ถœ๋ ฅ ํ† ํฐ</th>
<th>์‹œ๊ฐ„</th>
<th>์ž‘์—…</th>
</tr>
</thead>
<tbody id="messagesTableBody">
<tr>
<td colspan="9" 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()">&times;</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="9" 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 || '');
const modelName = msg.model_name || '-';
const inputTokens = msg.input_tokens !== null && msg.input_tokens !== undefined ? msg.input_tokens.toLocaleString() : '-';
const outputTokens = msg.output_tokens !== null && msg.output_tokens !== undefined ? msg.output_tokens.toLocaleString() : '-';
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>${modelName}</td>
<td style="text-align: right;">${inputTokens}</td>
<td style="text-align: right;">${outputTokens}</td>
<td>${date}</td>
<td>
<button class="btn btn-secondary" onclick="viewMessageById(${msg.id})" style="padding: 4px 8px; font-size: 12px;">์ƒ์„ธ ๋ณด๊ธฐ</button>
</td>
`;
// ๋ฉ”์‹œ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ data ์†์„ฑ์— ์ €์žฅ
row.setAttribute('data-message-id', msg.id);
row.setAttribute('data-message-role', msg.role);
row.setAttribute('data-message-content', escapeHtml(msg.content || ''));
row.setAttribute('data-message-model', modelName);
row.setAttribute('data-message-input-tokens', msg.input_tokens || '');
row.setAttribute('data-message-output-tokens', msg.output_tokens || '');
tbody.appendChild(row);
});
} else {
console.log('[๋ฉ”์‹œ์ง€ ๋ชฉ๋ก] ๋ฉ”์‹œ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค');
tbody.innerHTML = '<tr><td colspan="9" 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="9" 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);
}
// ๋ฉ”์‹œ์ง€ ์ƒ์„ธ ๋ณด๊ธฐ (ID๋กœ ์กฐํšŒ)
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];
const modelName = msg.model_name || '-';
const inputTokens = msg.input_tokens;
const outputTokens = msg.output_tokens;
// ๋ฉ”์‹œ์ง€ ์ƒ์„ธ ๋ณด๊ธฐ (ํ† ํฐ ์ •๋ณด ํฌํ•จ)
const modal = document.getElementById('messageModal');
const modalContent = document.getElementById('messageModalContent');
let tokenInfo = '';
if (msg.role === 'ai' && (inputTokens || outputTokens)) {
tokenInfo = `
<div style="margin-top: 16px; padding: 12px; background: #f8f9fa; border-radius: 6px;">
<div style="font-size: 14px; font-weight: 500; margin-bottom: 8px; color: #5f6368;">ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;">
<div>
<div style="font-size: 12px; color: #5f6368; margin-bottom: 4px;">๋ชจ๋ธ</div>
<div style="font-size: 16px; font-weight: 500;">${modelName}</div>
</div>
<div>
<div style="font-size: 12px; color: #5f6368; margin-bottom: 4px;">์ž…๋ ฅ ํ† ํฐ</div>
<div style="font-size: 16px; font-weight: 500;">${inputTokens ? parseInt(inputTokens).toLocaleString() : '-'}</div>
</div>
<div>
<div style="font-size: 12px; color: #5f6368; margin-bottom: 4px;">์ถœ๋ ฅ ํ† ํฐ</div>
<div style="font-size: 16px; font-weight: 500;">${outputTokens ? parseInt(outputTokens).toLocaleString() : '-'}</div>
</div>
</div>
</div>
`;
}
modalContent.innerHTML = `
<div class="message-view">
<div class="message-item ${msg.role}">
<div class="message-item-header">
<span class="message-item-role role-badge role-${msg.role}">${msg.role === 'user' ? '์‚ฌ์šฉ์ž' : 'AI'}</span>
<span class="message-item-time">๋ฉ”์‹œ์ง€ ID: ${msg.id}</span>
</div>
<div class="message-item-content">${escapeHtml(msg.content || '')}</div>
${tokenInfo}
</div>
</div>
`;
modal.classList.add('active');
} else {
// ๋ฐ์ดํ„ฐ ์†์„ฑ์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ (fallback)
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) {
// ๋ฐ์ดํ„ฐ ์†์„ฑ์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ (fallback)
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');
// ๋ฐ์ดํ„ฐ ์†์„ฑ์—์„œ ํ† ํฐ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
const row = document.querySelector(`tr[data-message-id="${messageId}"]`);
const modelName = row ? row.getAttribute('data-message-model') : '-';
const inputTokens = row ? row.getAttribute('data-message-input-tokens') : '';
const outputTokens = row ? row.getAttribute('data-message-output-tokens') : '';
let tokenInfo = '';
if (role === 'ai' && (inputTokens || outputTokens)) {
tokenInfo = `
<div style="margin-top: 16px; padding: 12px; background: #f8f9fa; border-radius: 6px;">
<div style="font-size: 14px; font-weight: 500; margin-bottom: 8px; color: #5f6368;">ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;">
<div>
<div style="font-size: 12px; color: #5f6368; margin-bottom: 4px;">๋ชจ๋ธ</div>
<div style="font-size: 16px; font-weight: 500;">${modelName}</div>
</div>
<div>
<div style="font-size: 12px; color: #5f6368; margin-bottom: 4px;">์ž…๋ ฅ ํ† ํฐ</div>
<div style="font-size: 16px; font-weight: 500;">${inputTokens ? parseInt(inputTokens).toLocaleString() : '-'}</div>
</div>
<div>
<div style="font-size: 12px; color: #5f6368; margin-bottom: 4px;">์ถœ๋ ฅ ํ† ํฐ</div>
<div style="font-size: 16px; font-weight: 500;">${outputTokens ? parseInt(outputTokens).toLocaleString() : '-'}</div>
</div>
</div>
</div>
`;
}
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>
${tokenInfo}
</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();
// ์„ธ์…˜ ID๊ฐ€ ์—†์–ด๋„ ์ „์ฒด ๋ฉ”์‹œ์ง€ ๋กœ๋“œ
loadMessages(1);
});
</script>
</body>
</html>