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 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
- {% for section in _menu.sections %}
205
  <div class="dropdown">
206
- <button type="button" class="dropdown-toggle">{{ section.label }}</button>
207
  <div class="dropdown-menu">
208
- {% for item in section.items %}
209
- <a href="{{ url_for(item.endpoint) }}" class="dropdown-item">{{ item.label }}</a>
210
  {% endfor %}
211
  </div>
212
  </div>
213
  {% endfor %}
214
 
215
- {% for action in _menu.actions %}
216
- <a href="{{ url_for(action.endpoint) }}" class="btn" style="padding: 8px 16px; font-size: 14px; {% if not loop.first %}margin-left: 4px;{% endif %}">
217
- {{ action.label }}
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.sections %}
233
- <div class="mobile-menu-section">{{ section.label }}</div>
234
- {% for item in section.items %}
235
- <a href="{{ url_for(item.endpoint) }}" class="mobile-menu-item" onclick="adminNavCloseMobileMenu()">{{ item.label }}</a>
236
  {% endfor %}
237
  {% endfor %}
238
  <div class="mobile-menu-section">๊ธฐํƒ€</div>
239
- {% for action in _menu.actions %}
240
- <a href="{{ url_for(action.endpoint) }}" class="mobile-menu-item" onclick="adminNavCloseMobileMenu()">{{ action.label }}</a>
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 fetch('/api/admin/menu', { credentials: 'include' });
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 fetch('/api/admin/menu', {
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 fetch('/api/admin/menu/endpoints', { credentials: 'include' });
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