|
|
<!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; |
|
|
} |
|
|
|
|
|
|
|
|
.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="๋ฉ๋ด ๋ซ๊ธฐ">×</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()">×</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> |
|
|
`; |
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
|
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'); |
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
loadMessages(1); |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|
|
|
|
|
|
|