GitHub Actions
commited on
Commit
ยท
48ec158
1
Parent(s):
47869b5
Auto-deploy from GitHub Actions - 2025-12-14 03:20:12
Browse files- app/routes.py +12 -2
- templates/_admin_nav.html +21 -13
- templates/admin_menu.html +24 -7
app/routes.py
CHANGED
|
@@ -2,6 +2,7 @@ from flask import Blueprint, render_template, request, jsonify, send_from_direct
|
|
| 2 |
from flask import current_app
|
| 3 |
from flask_login import login_user, logout_user, login_required, current_user
|
| 4 |
from werkzeug.utils import secure_filename
|
|
|
|
| 5 |
from app.database import db, UploadedFile, User, ChatSession, ChatMessage, DocumentChunk, ParentChunk, SystemConfig, EpisodeAnalysis, GraphEntity, GraphRelationship, GraphEvent, ChatbotPrompt
|
| 6 |
from app.vector_db import get_vector_db
|
| 7 |
from app.gemini_client import get_gemini_client
|
|
@@ -179,12 +180,21 @@ def save_admin_menu_config(config_obj):
|
|
| 179 |
@main_bp.app_context_processor
|
| 180 |
def inject_admin_menu():
|
| 181 |
"""๋ชจ๋ ํ
ํ๋ฆฟ์์ admin_menu ์ฌ์ฉ ๊ฐ๋ฅํ๋๋ก ์ฃผ์
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
try:
|
| 183 |
if getattr(current_user, "is_authenticated", False) and getattr(current_user, "is_admin", False):
|
| 184 |
-
return {"admin_menu": get_admin_menu_config()}
|
| 185 |
except Exception:
|
| 186 |
pass
|
| 187 |
-
return {}
|
| 188 |
|
| 189 |
|
| 190 |
def ensure_chatbot_prompt_table_exists():
|
|
|
|
| 2 |
from flask import current_app
|
| 3 |
from flask_login import login_user, logout_user, login_required, current_user
|
| 4 |
from werkzeug.utils import secure_filename
|
| 5 |
+
from werkzeug.routing import BuildError
|
| 6 |
from app.database import db, UploadedFile, User, ChatSession, ChatMessage, DocumentChunk, ParentChunk, SystemConfig, EpisodeAnalysis, GraphEntity, GraphRelationship, GraphEvent, ChatbotPrompt
|
| 7 |
from app.vector_db import get_vector_db
|
| 8 |
from app.gemini_client import get_gemini_client
|
|
|
|
| 180 |
@main_bp.app_context_processor
|
| 181 |
def inject_admin_menu():
|
| 182 |
"""๋ชจ๋ ํ
ํ๋ฆฟ์์ admin_menu ์ฌ์ฉ ๊ฐ๋ฅํ๋๋ก ์ฃผ์
"""
|
| 183 |
+
def safe_url_for(endpoint, **values):
|
| 184 |
+
"""ํ
ํ๋ฆฟ์์ endpoint ๋ฏธ์กด์ฌ๋ก 500์ด ๋์ง ์๋๋ก ์์ ํ url_for ์ ๊ณต"""
|
| 185 |
+
try:
|
| 186 |
+
return url_for(endpoint, **values)
|
| 187 |
+
except BuildError:
|
| 188 |
+
return '#'
|
| 189 |
+
except Exception:
|
| 190 |
+
return '#'
|
| 191 |
+
|
| 192 |
try:
|
| 193 |
if getattr(current_user, "is_authenticated", False) and getattr(current_user, "is_admin", False):
|
| 194 |
+
return {"admin_menu": get_admin_menu_config(), "safe_url_for": safe_url_for}
|
| 195 |
except Exception:
|
| 196 |
pass
|
| 197 |
+
return {"safe_url_for": safe_url_for}
|
| 198 |
|
| 199 |
|
| 200 |
def ensure_chatbot_prompt_table_exists():
|
templates/_admin_nav.html
CHANGED
|
@@ -191,6 +191,14 @@
|
|
| 191 |
</style>
|
| 192 |
|
| 193 |
{% set _menu = admin_menu if admin_menu is defined else {'sections': [], 'actions': [{'label': '๋ฉ์ธ์ผ๋ก', 'endpoint': 'main.index'}, {'label': '๋ก๊ทธ์์', 'endpoint': 'main.logout'}]} %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
|
| 195 |
<div class="header">
|
| 196 |
<div class="header-title">
|
|
@@ -201,20 +209,20 @@
|
|
| 201 |
<div class="header-actions">
|
| 202 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 203 |
|
| 204 |
-
|
| 205 |
<div class="dropdown">
|
| 206 |
-
<button type="button" class="dropdown-toggle">{{ section
|
| 207 |
<div class="dropdown-menu">
|
| 208 |
-
{% for item in section
|
| 209 |
-
<a href="{{
|
| 210 |
{% endfor %}
|
| 211 |
</div>
|
| 212 |
</div>
|
| 213 |
{% endfor %}
|
| 214 |
|
| 215 |
-
{% for action in _menu
|
| 216 |
-
<a href="{{
|
| 217 |
-
{{ action
|
| 218 |
</a>
|
| 219 |
{% endfor %}
|
| 220 |
</div>
|
|
@@ -229,15 +237,15 @@
|
|
| 229 |
</div>
|
| 230 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 231 |
<div class="mobile-menu-items">
|
| 232 |
-
{% for section in _menu
|
| 233 |
-
<div class="mobile-menu-section">{{ section
|
| 234 |
-
{% for item in section
|
| 235 |
-
<a href="{{
|
| 236 |
{% endfor %}
|
| 237 |
{% endfor %}
|
| 238 |
<div class="mobile-menu-section">๊ธฐํ</div>
|
| 239 |
-
{% for action in _menu
|
| 240 |
-
<a href="{{
|
| 241 |
{% endfor %}
|
| 242 |
</div>
|
| 243 |
</div>
|
|
|
|
| 191 |
</style>
|
| 192 |
|
| 193 |
{% set _menu = admin_menu if admin_menu is defined else {'sections': [], 'actions': [{'label': '๋ฉ์ธ์ผ๋ก', 'endpoint': 'main.index'}, {'label': '๋ก๊ทธ์์', 'endpoint': 'main.logout'}]} %}
|
| 194 |
+
{# safe_url_for๊ฐ ์๋ฒ์์ ์ฃผ์
๋์ง ์์ ๊ฒฝ์ฐ์๋ 500์ด ๋์ง ์๋๋ก ํด๋ฐฑ #}
|
| 195 |
+
{% macro nav_url(endpoint) -%}
|
| 196 |
+
{%- if safe_url_for is defined -%}
|
| 197 |
+
{{ safe_url_for(endpoint) }}
|
| 198 |
+
{%- else -%}
|
| 199 |
+
{{ url_for(endpoint) }}
|
| 200 |
+
{%- endif -%}
|
| 201 |
+
{%- endmacro %}
|
| 202 |
|
| 203 |
<div class="header">
|
| 204 |
<div class="header-title">
|
|
|
|
| 209 |
<div class="header-actions">
|
| 210 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 211 |
|
| 212 |
+
{% for section in _menu['sections'] %}
|
| 213 |
<div class="dropdown">
|
| 214 |
+
<button type="button" class="dropdown-toggle">{{ section['label'] }}</button>
|
| 215 |
<div class="dropdown-menu">
|
| 216 |
+
{% for item in section['items'] %}
|
| 217 |
+
<a href="{{ nav_url(item['endpoint']) }}" class="dropdown-item">{{ item['label'] }}</a>
|
| 218 |
{% endfor %}
|
| 219 |
</div>
|
| 220 |
</div>
|
| 221 |
{% endfor %}
|
| 222 |
|
| 223 |
+
{% for action in _menu['actions'] %}
|
| 224 |
+
<a href="{{ nav_url(action['endpoint']) }}" class="btn" style="padding: 8px 16px; font-size: 14px; {% if not loop.first %}margin-left: 4px;{% endif %}">
|
| 225 |
+
{{ action['label'] }}
|
| 226 |
</a>
|
| 227 |
{% endfor %}
|
| 228 |
</div>
|
|
|
|
| 237 |
</div>
|
| 238 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 239 |
<div class="mobile-menu-items">
|
| 240 |
+
{% for section in _menu['sections'] %}
|
| 241 |
+
<div class="mobile-menu-section">{{ section['label'] }}</div>
|
| 242 |
+
{% for item in section['items'] %}
|
| 243 |
+
<a href="{{ nav_url(item['endpoint']) }}" class="mobile-menu-item" onclick="adminNavCloseMobileMenu()">{{ item['label'] }}</a>
|
| 244 |
{% endfor %}
|
| 245 |
{% endfor %}
|
| 246 |
<div class="mobile-menu-section">๊ธฐํ</div>
|
| 247 |
+
{% for action in _menu['actions'] %}
|
| 248 |
+
<a href="{{ nav_url(action['endpoint']) }}" class="mobile-menu-item" onclick="adminNavCloseMobileMenu()">{{ action['label'] }}</a>
|
| 249 |
{% endfor %}
|
| 250 |
</div>
|
| 251 |
</div>
|
templates/admin_menu.html
CHANGED
|
@@ -183,6 +183,12 @@
|
|
| 183 |
const textarea = document.getElementById('menuJson');
|
| 184 |
const alertEl = document.getElementById('alert');
|
| 185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
function showAlert(msg, type) {
|
| 187 |
alertEl.className = `alert ${type}`;
|
| 188 |
alertEl.textContent = msg;
|
|
@@ -192,10 +198,23 @@
|
|
| 192 |
return JSON.stringify(obj, null, 2);
|
| 193 |
}
|
| 194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
async function loadMenu() {
|
| 196 |
try {
|
| 197 |
-
const res = await
|
| 198 |
-
const data = await res.json();
|
| 199 |
if (!res.ok) {
|
| 200 |
showAlert(data.error || `๋ถ๋ฌ์ค๊ธฐ ์คํจ (${res.status})`, 'error');
|
| 201 |
return;
|
|
@@ -216,13 +235,12 @@
|
|
| 216 |
return;
|
| 217 |
}
|
| 218 |
try {
|
| 219 |
-
const res = await
|
| 220 |
method: 'PUT',
|
| 221 |
credentials: 'include',
|
| 222 |
headers: { 'Content-Type': 'application/json' },
|
| 223 |
body: JSON.stringify(obj)
|
| 224 |
});
|
| 225 |
-
const data = await res.json();
|
| 226 |
if (!res.ok) {
|
| 227 |
showAlert(data.error || `์ ์ฅ ์คํจ (${res.status})`, 'error');
|
| 228 |
return;
|
|
@@ -272,8 +290,7 @@
|
|
| 272 |
|
| 273 |
async function loadEndpoints() {
|
| 274 |
try {
|
| 275 |
-
const res = await
|
| 276 |
-
const data = await res.json();
|
| 277 |
if (!res.ok) {
|
| 278 |
showAlert(data.error || `endpoint ๋ชฉ๋ก ๋ก๋ ์คํจ (${res.status})`, 'error');
|
| 279 |
return;
|
|
@@ -281,7 +298,7 @@
|
|
| 281 |
allEndpoints = data.endpoints || [];
|
| 282 |
renderEndpointList('');
|
| 283 |
} catch (e) {
|
| 284 |
-
showAlert(`endpoint ๋ชฉ๋ก ๋ก๋ ์ค๋ฅ: ${e.message}`, 'error');
|
| 285 |
}
|
| 286 |
}
|
| 287 |
|
|
|
|
| 183 |
const textarea = document.getElementById('menuJson');
|
| 184 |
const alertEl = document.getElementById('alert');
|
| 185 |
|
| 186 |
+
function escapeHtml(text) {
|
| 187 |
+
const div = document.createElement('div');
|
| 188 |
+
div.textContent = text == null ? '' : String(text);
|
| 189 |
+
return div.innerHTML;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
function showAlert(msg, type) {
|
| 193 |
alertEl.className = `alert ${type}`;
|
| 194 |
alertEl.textContent = msg;
|
|
|
|
| 198 |
return JSON.stringify(obj, null, 2);
|
| 199 |
}
|
| 200 |
|
| 201 |
+
async function fetchJson(url, options = {}) {
|
| 202 |
+
const res = await fetch(url, options);
|
| 203 |
+
const text = await res.text();
|
| 204 |
+
let data = null;
|
| 205 |
+
try {
|
| 206 |
+
data = text ? JSON.parse(text) : null;
|
| 207 |
+
} catch (e) {
|
| 208 |
+
// HTML(<!doctype ...) ๋ฑ์ด ์ค๋ฉด ์ฌ๊ธฐ๋ก ๋ค์ด์ด
|
| 209 |
+
const preview = (text || '').slice(0, 120).replace(/\s+/g, ' ');
|
| 210 |
+
throw new Error(`Unexpected token '<' ๋ฑ JSON ํ์ฑ ์คํจ (status ${res.status}). ์๋ต ๋ฏธ๋ฆฌ๋ณด๊ธฐ: ${preview}`);
|
| 211 |
+
}
|
| 212 |
+
return { res, data };
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
async function loadMenu() {
|
| 216 |
try {
|
| 217 |
+
const { res, data } = await fetchJson('/api/admin/menu', { credentials: 'include' });
|
|
|
|
| 218 |
if (!res.ok) {
|
| 219 |
showAlert(data.error || `๋ถ๋ฌ์ค๊ธฐ ์คํจ (${res.status})`, 'error');
|
| 220 |
return;
|
|
|
|
| 235 |
return;
|
| 236 |
}
|
| 237 |
try {
|
| 238 |
+
const { res, data } = await fetchJson('/api/admin/menu', {
|
| 239 |
method: 'PUT',
|
| 240 |
credentials: 'include',
|
| 241 |
headers: { 'Content-Type': 'application/json' },
|
| 242 |
body: JSON.stringify(obj)
|
| 243 |
});
|
|
|
|
| 244 |
if (!res.ok) {
|
| 245 |
showAlert(data.error || `์ ์ฅ ์คํจ (${res.status})`, 'error');
|
| 246 |
return;
|
|
|
|
| 290 |
|
| 291 |
async function loadEndpoints() {
|
| 292 |
try {
|
| 293 |
+
const { res, data } = await fetchJson('/api/admin/menu/endpoints', { credentials: 'include' });
|
|
|
|
| 294 |
if (!res.ok) {
|
| 295 |
showAlert(data.error || `endpoint ๋ชฉ๋ก ๋ก๋ ์คํจ (${res.status})`, 'error');
|
| 296 |
return;
|
|
|
|
| 298 |
allEndpoints = data.endpoints || [];
|
| 299 |
renderEndpointList('');
|
| 300 |
} catch (e) {
|
| 301 |
+
showAlert(`endpoint ๋ชฉ๋ก ๋ก๋ ์ค๋ฅ: ${e.message}\n(๋๋ถ๋ถ ์๋ฒ๊ฐ ์ต์ ์ฝ๋๋ก ์ฌ์์๋์ง ์์ 404 HTML์ด ๋ด๋ ค์ค๋ ๊ฒฝ์ฐ์
๋๋ค)`, 'error');
|
| 302 |
}
|
| 303 |
}
|
| 304 |
|