soyailabs / templates /admin_utils.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: 28px;
cursor: pointer;
color: #202124;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.mobile-menu-user {
padding: 12px 20px;
background: #f8f9fa;
border-bottom: 1px solid #dadce0;
font-size: 14px;
color: #5f6368;
}
.mobile-menu-items {
padding: 8px 0;
}
.mobile-menu-item {
display: block;
padding: 12px 20px;
color: #202124;
text-decoration: none;
font-size: 14px;
transition: background 0.2s;
}
.mobile-menu-item:hover {
background: #f8f9fa;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
display: inline-block;
transition: all 0.2s;
}
.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: 1200px;
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;
}
.alert {
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 16px;
font-size: 14px;
}
.alert.error {
background: #fce8e6;
color: #c5221f;
}
.alert.success {
background: #e8f5e9;
color: #137333;
}
.utils-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.util-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.2s;
}
.util-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.util-card-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
}
.util-card-description {
font-size: 14px;
color: #5f6368;
margin-bottom: 12px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #dadce0;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
}
.form-group textarea {
min-height: 200px;
resize: vertical;
}
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #1a73e8;
box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1);
}
.form-group-checkbox {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
}
.form-group-checkbox input {
width: auto;
}
.preview-box {
background: #f8f9fa;
border: 1px solid #dadce0;
border-radius: 6px;
padding: 12px;
margin-top: 12px;
max-height: 300px;
overflow-y: auto;
font-size: 13px;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
word-wrap: break-word;
}
.preview-box.empty {
color: #5f6368;
font-style: italic;
}
input[type="file"] {
cursor: pointer;
}
input[type="file"]:focus {
outline: none;
border-color: #1a73e8;
box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1);
}
@media (max-width: 768px) {
.header {
padding: 12px 16px;
}
.header-title {
font-size: 18px;
}
.menu-toggle {
display: block;
}
.header-actions {
display: none;
}
.container {
padding: 16px;
}
.utils-grid {
grid-template-columns: 1fr;
}
}
</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>
{# ์‚ฌ์ดํŠธ ๊ด€๋ฆฌ #}
<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 style="padding: 16px 0;">
<p style="margin-bottom: 16px; color: #5f6368; font-size: 14px;">
๋‹ค์–‘ํ•œ ํšŒ์ฐจ ๊ตฌ๋ถ„ ๋ฐฉ์‹(@n, #n, @ํ”„๋กค๋กœ๊ทธ ๋“ฑ)์„ #nํ™” ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๊ฑฐ๋‚˜ ์ง์ ‘ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
</p>
<div class="form-group">
<label for="episodeFileUpload">ํŒŒ์ผ ์—…๋กœ๋“œ</label>
<input type="file" id="episodeFileUpload" accept=".txt,.md" onchange="handleFileUpload(event)" style="width: 100%; padding: 8px; border: 1px solid #dadce0; border-radius: 6px; font-size: 14px;">
<small style="color: #5f6368; font-size: 12px; display: block; margin-top: 4px;">
ํ…์ŠคํŠธ ํŒŒ์ผ(.txt, .md)์„ ์—…๋กœ๋“œํ•˜์„ธ์š”
</small>
</div>
<div class="form-group" id="encodingGroup" style="display: none;">
<label for="fileEncoding">ํŒŒ์ผ ์ธ์ฝ”๋”ฉ</label>
<div style="display: flex; gap: 8px; align-items: center;">
<select id="fileEncoding" style="flex: 1; padding: 8px 12px; border: 1px solid #dadce0; border-radius: 6px; font-size: 14px;">
<option value="utf-8">UTF-8</option>
<option value="cp949">CP949 (EUC-KR)</option>
<option value="euc-kr">EUC-KR</option>
<option value="latin-1">Latin-1 (ISO-8859-1)</option>
<option value="utf-16">UTF-16</option>
<option value="utf-16-le">UTF-16 LE</option>
<option value="utf-16-be">UTF-16 BE</option>
</select>
<button class="btn btn-secondary" onclick="reloadFileWithEncoding()" style="padding: 8px 16px;">์ธ์ฝ”๋”ฉ ์ ์šฉ</button>
</div>
<div id="encodingInfo" style="margin-top: 8px; padding: 8px; background: #f8f9fa; border-radius: 4px; font-size: 12px; color: #5f6368; display: none;">
<span id="encodingStatus"></span>
</div>
</div>
<div class="form-group">
<label for="episodeContentInput">๋˜๋Š” ์ง์ ‘ ๋‚ด์šฉ ์ž…๋ ฅ</label>
<textarea id="episodeContentInput" placeholder="@1&#10;@2ํ™”&#10;3ํ™”&#10;... ํ˜•์‹์˜ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”"></textarea>
</div>
<div style="display: flex; gap: 8px; margin-top: 16px;">
<button class="btn btn-primary" onclick="convertEpisodeFormat()">๋ณ€ํ™˜ ์‹คํ–‰</button>
<button class="btn btn-secondary" onclick="previewConversion()">๋ฏธ๋ฆฌ๋ณด๊ธฐ</button>
<button class="btn btn-secondary" onclick="clearEpisodeForm()">์ดˆ๊ธฐํ™”</button>
<button class="btn btn-secondary" id="downloadBtn" onclick="downloadConvertedFile()" style="display: none;">๋‹ค์šด๋กœ๋“œ</button>
</div>
<div id="episodePreview" class="preview-box empty" style="display: none;">
๋ณ€ํ™˜ ๊ฒฐ๊ณผ๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.
</div>
<div id="conversionInfo" style="margin-top: 12px; padding: 12px; background: #e8f0fe; border-radius: 6px; display: none;">
<div style="font-size: 14px; color: #1967d2;">
<strong>๋ณ€ํ™˜ ์™„๋ฃŒ!</strong> ์œ„์˜ "๋‹ค์šด๋กœ๋“œ" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ๋ณ€ํ™˜๋œ ํŒŒ์ผ์„ ๋‹ค์šด๋กœ๋“œํ•˜์„ธ์š”.
</div>
</div>
</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();
}
}
function showAlert(message, type = 'success') {
const container = document.getElementById('alertContainer');
container.innerHTML = `<div class="alert ${type}">${message}</div>`;
setTimeout(() => {
container.innerHTML = '';
}, 5000);
}
// ํ˜„์žฌ ์„ ํƒ๋œ ํŒŒ์ผ ์ €์žฅ
let currentFile = null;
// ํŒŒ์ผ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ
async function handleFileUpload(event) {
const file = event.target.files[0];
if (!file) {
currentFile = null;
document.getElementById('encodingGroup').style.display = 'none';
return;
}
currentFile = file;
// ์ธ์ฝ”๋”ฉ ๊ฐ์ง€ API ํ˜ธ์ถœ
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/admin/utils/detect-encoding', {
method: 'POST',
credentials: 'include',
body: formData
});
const data = await response.json();
if (response.ok && data.detected_encoding) {
// ์ธ์ฝ”๋”ฉ ์ •๋ณด ํ‘œ์‹œ
document.getElementById('fileEncoding').value = data.detected_encoding;
const encodingInfo = document.getElementById('encodingInfo');
const encodingStatus = document.getElementById('encodingStatus');
encodingStatus.innerHTML = `๊ฐ์ง€๋œ ์ธ์ฝ”๋”ฉ: <strong>${data.detected_encoding}</strong> (์‹ ๋ขฐ๋„: ${Math.round(data.confidence * 100)}%)`;
encodingInfo.style.display = 'block';
document.getElementById('encodingGroup').style.display = 'block';
// ๊ฐ์ง€๋œ ์ธ์ฝ”๋”ฉ์œผ๋กœ ํŒŒ์ผ ์ฝ๊ธฐ
await loadFileWithEncoding(data.detected_encoding);
} else {
// ์ธ์ฝ”๋”ฉ ๊ฐ์ง€ ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ๊ฐ’(UTF-8)์œผ๋กœ ์‹œ๋„
document.getElementById('fileEncoding').value = 'utf-8';
document.getElementById('encodingInfo').style.display = 'none';
document.getElementById('encodingGroup').style.display = 'block';
await loadFileWithEncoding('utf-8');
}
} catch (error) {
console.error('์ธ์ฝ”๋”ฉ ๊ฐ์ง€ ์˜ค๋ฅ˜:', error);
// ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์‹œ๋„
document.getElementById('fileEncoding').value = 'utf-8';
document.getElementById('encodingGroup').style.display = 'block';
await loadFileWithEncoding('utf-8');
}
}
// ์ง€์ •๋œ ์ธ์ฝ”๋”ฉ์œผ๋กœ ํŒŒ์ผ ์ฝ๊ธฐ
async function loadFileWithEncoding(encoding) {
if (!currentFile) {
return;
}
try {
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('episodeContentInput').value = e.target.result;
showAlert(`ํŒŒ์ผ์ด ${encoding} ์ธ์ฝ”๋”ฉ์œผ๋กœ ๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, 'success');
};
reader.onerror = function() {
showAlert('ํŒŒ์ผ ์ฝ๊ธฐ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', 'error');
};
reader.readAsText(currentFile, encoding);
} catch (error) {
console.error('ํŒŒ์ผ ์ฝ๊ธฐ ์˜ค๋ฅ˜:', error);
showAlert(`ํŒŒ์ผ ์ฝ๊ธฐ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ${error.message}`, 'error');
}
}
// ์ธ์ฝ”๋”ฉ ๋ณ€๊ฒฝ ํ›„ ํŒŒ์ผ ๋‹ค์‹œ ์ฝ๊ธฐ
function reloadFileWithEncoding() {
const encoding = document.getElementById('fileEncoding').value;
if (!currentFile) {
showAlert('ํŒŒ์ผ์„ ๋จผ์ € ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”.', 'error');
return;
}
loadFileWithEncoding(encoding);
}
// ๋‹ค์šด๋กœ๋“œ URL ์ €์žฅ
let downloadUrl = null;
let downloadFilename = null;
// ํšŒ์ฐจ ํ˜•์‹ ๋ณ€ํ™˜
async function convertEpisodeFormat() {
const fileInput = document.getElementById('episodeFileUpload');
const content = document.getElementById('episodeContentInput').value.trim();
const file = fileInput.files[0];
if (!file && !content) {
showAlert('ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๊ฑฐ๋‚˜ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.', 'error');
return;
}
try {
const formData = new FormData();
if (file) {
formData.append('file', file);
// ์„ ํƒ๋œ ์ธ์ฝ”๋”ฉ ์ •๋ณด ์ถ”๊ฐ€
const encoding = document.getElementById('fileEncoding').value || 'utf-8';
formData.append('encoding', encoding);
} else if (content) {
// JSON์œผ๋กœ ์ „์†ก
const response = await fetch('/api/admin/utils/convert-episode-format', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
content: content,
filename: file ? file.name : 'converted_file.txt'
})
});
const data = await response.json();
if (response.ok) {
showAlert(data.message, 'success');
if (data.converted_content) {
document.getElementById('episodePreview').textContent = data.converted_content;
document.getElementById('episodePreview').style.display = 'block';
document.getElementById('episodePreview').classList.remove('empty');
}
if (data.download_url) {
downloadUrl = data.download_url;
downloadFilename = data.filename;
document.getElementById('downloadBtn').style.display = 'inline-block';
document.getElementById('conversionInfo').style.display = 'block';
}
} else {
showAlert(data.error || '๋ณ€ํ™˜ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', 'error');
}
return;
}
// ํŒŒ์ผ ์—…๋กœ๋“œ์ธ ๊ฒฝ์šฐ
const response = await fetch('/api/admin/utils/convert-episode-format', {
method: 'POST',
credentials: 'include',
body: formData
});
const data = await response.json();
if (response.ok) {
showAlert(data.message, 'success');
if (data.converted_content) {
document.getElementById('episodePreview').textContent = data.converted_content;
document.getElementById('episodePreview').style.display = 'block';
document.getElementById('episodePreview').classList.remove('empty');
}
if (data.download_url) {
downloadUrl = data.download_url;
downloadFilename = data.filename;
document.getElementById('downloadBtn').style.display = 'inline-block';
document.getElementById('conversionInfo').style.display = 'block';
}
} else {
showAlert(data.error || '๋ณ€ํ™˜ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', 'error');
}
} catch (error) {
console.error('๋ณ€ํ™˜ ์˜ค๋ฅ˜:', error);
showAlert(`์˜ค๋ฅ˜: ${error.message}`, 'error');
}
}
// ๋ณ€ํ™˜๋œ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ
function downloadConvertedFile() {
if (!downloadUrl) {
showAlert('๋‹ค์šด๋กœ๋“œํ•  ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.', 'error');
return;
}
// ์ƒˆ ์ฐฝ์—์„œ ๋‹ค์šด๋กœ๋“œ ๋งํฌ ์—ด๊ธฐ
window.location.href = downloadUrl;
}
// ๋ฏธ๋ฆฌ๋ณด๊ธฐ
async function previewConversion() {
const fileInput = document.getElementById('episodeFileUpload');
const content = document.getElementById('episodeContentInput').value.trim();
const file = fileInput.files[0];
if (!file && !content) {
showAlert('ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๊ฑฐ๋‚˜ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.', 'error');
return;
}
try {
let response;
if (file) {
const formData = new FormData();
formData.append('file', file);
// ์„ ํƒ๋œ ์ธ์ฝ”๋”ฉ ์ •๋ณด ์ถ”๊ฐ€
const encoding = document.getElementById('fileEncoding').value || 'utf-8';
formData.append('encoding', encoding);
response = await fetch('/api/admin/utils/convert-episode-format', {
method: 'POST',
credentials: 'include',
body: formData
});
} else {
response = await fetch('/api/admin/utils/convert-episode-format', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
content: content,
filename: 'preview.txt'
})
});
}
const data = await response.json();
if (response.ok && data.converted_content) {
const previewBox = document.getElementById('episodePreview');
previewBox.textContent = data.converted_content.substring(0, 2000) + (data.converted_content.length > 2000 ? '\n... (๋” ๋งŽ์€ ๋‚ด์šฉ์ด ์žˆ์Šต๋‹ˆ๋‹ค)' : '');
previewBox.style.display = 'block';
previewBox.classList.remove('empty');
} else {
showAlert(data.error || '๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', 'error');
}
} catch (error) {
console.error('๋ฏธ๋ฆฌ๋ณด๊ธฐ ์˜ค๋ฅ˜:', error);
showAlert(`์˜ค๋ฅ˜: ${error.message}`, 'error');
}
}
// ํผ ์ดˆ๊ธฐํ™”
function clearEpisodeForm() {
document.getElementById('episodeFileUpload').value = '';
document.getElementById('episodeContentInput').value = '';
document.getElementById('episodePreview').style.display = 'none';
document.getElementById('episodePreview').textContent = '๋ณ€ํ™˜ ๊ฒฐ๊ณผ๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.';
document.getElementById('episodePreview').classList.add('empty');
document.getElementById('downloadBtn').style.display = 'none';
document.getElementById('conversionInfo').style.display = 'none';
document.getElementById('encodingGroup').style.display = 'none';
document.getElementById('encodingInfo').style.display = 'none';
document.getElementById('fileEncoding').value = 'utf-8';
currentFile = null;
downloadUrl = null;
downloadFilename = null;
}
// ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ดˆ๊ธฐํ™”
document.addEventListener('DOMContentLoaded', function() {
console.log('์œ ํ‹ธ๋ฆฌํ‹ฐ ํŽ˜์ด์ง€ ๋กœ๋“œ ์™„๋ฃŒ');
});
</script>
</body>
</html>