GitHub Actions commited on
Commit
b096243
ยท
1 Parent(s): 1995f8f

Auto-deploy from GitHub Actions - 2025-12-14 02:26:35

Browse files
app/routes.py CHANGED
@@ -13,6 +13,139 @@ import json
13
 
14
  main_bp = Blueprint('main', __name__)
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  def ensure_chatbot_prompt_table_exists():
18
  """chatbot_prompt ํ…Œ์ด๋ธ”์ด ์—†์œผ๋ฉด ์ƒ์„ฑ (์šด์˜ ํ™˜๊ฒฝ์—์„œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ˆ„๋ฝ ๋Œ€๋น„)"""
@@ -2123,6 +2256,12 @@ def admin_webnovels():
2123
  """์›น์†Œ์„ค ๊ด€๋ฆฌ ํŽ˜์ด์ง€"""
2124
  return render_template('admin_webnovels.html')
2125
 
 
 
 
 
 
 
2126
  @main_bp.route('/admin/prompts')
2127
  @admin_required
2128
  def admin_prompts():
@@ -4682,6 +4821,30 @@ def list_files_with_chatbot_prompts():
4682
  except Exception as e:
4683
  return jsonify({'error': f'ํ”„๋กฌํ”„ํŠธ ํŒŒ์ผ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜: {str(e)}'}), 500
4684
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4685
  @main_bp.route('/api/files/<int:file_id>/chunks', methods=['GET'])
4686
  @login_required
4687
  def get_file_chunks(file_id):
 
13
 
14
  main_bp = Blueprint('main', __name__)
15
 
16
+ # -----------------------------
17
+ # Admin menu configuration
18
+ # -----------------------------
19
+
20
+ ADMIN_MENU_CONFIG_KEY = "admin_menu_config_v1"
21
+
22
+
23
+ def get_default_admin_menu():
24
+ """๊ด€๋ฆฌ์ž ์ƒ๋‹จ ๋ฉ”๋‰ด ๊ธฐ๋ณธ ๊ตฌ์„ฑ(๊ด€๋ฆฌ ํŽ˜์ด์ง€์—์„œ ์ดˆ๊ธฐํ™”/๋ณต๊ตฌ์šฉ)"""
25
+ return {
26
+ "version": 1,
27
+ "sections": [
28
+ {
29
+ "label": "์‚ฌ์ดํŠธ ๊ด€๋ฆฌ",
30
+ "items": [
31
+ {"label": "์‚ฌ์šฉ์ž ๊ด€๋ฆฌ", "endpoint": "main.admin"},
32
+ {"label": "ํ† ํฐ ํ†ต๊ณ„", "endpoint": "main.admin_tokens"},
33
+ {"label": "๋ฉ”๋‰ด ๊ด€๋ฆฌ", "endpoint": "main.admin_menu"},
34
+ ],
35
+ },
36
+ {
37
+ "label": "์›น์†Œ์„ค ๊ด€๋ฆฌ",
38
+ "items": [
39
+ {"label": "์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ", "endpoint": "main.admin_webnovels"},
40
+ {"label": "๋ฉ”์‹œ์ง€ ํ™•์ธ", "endpoint": "main.admin_messages"},
41
+ ],
42
+ },
43
+ {
44
+ "label": "AI ์„ค์ •",
45
+ "items": [
46
+ {"label": "AI ์„ค์ •", "endpoint": "main.admin_settings"},
47
+ {"label": "ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ", "endpoint": "main.admin_prompts"},
48
+ ],
49
+ },
50
+ {
51
+ "label": "์ฑ—๋ด‡",
52
+ "items": [
53
+ {"label": "ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ", "endpoint": "main.admin_tags"},
54
+ {"label": "์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ", "endpoint": "main.admin_chatbot_prompts"},
55
+ ],
56
+ },
57
+ {
58
+ "label": "ํŽธ์˜๊ธฐ๋Šฅ",
59
+ "items": [
60
+ {"label": "์œ ํ‹ธ", "endpoint": "main.admin_utils"},
61
+ ],
62
+ },
63
+ ],
64
+ "actions": [
65
+ {"label": "๋ฉ”์ธ์œผ๋กœ", "endpoint": "main.index"},
66
+ {"label": "๋กœ๊ทธ์•„์›ƒ", "endpoint": "main.logout"},
67
+ ],
68
+ }
69
+
70
+
71
+ def validate_admin_menu_config(config_obj):
72
+ """๋ฉ”๋‰ด JSON ๊ตฌ์กฐ ๊ฒ€์ฆ. (bool_ok, error_message)"""
73
+ if not isinstance(config_obj, dict):
74
+ return False, "์ตœ์ƒ์œ„๋Š” JSON ๊ฐ์ฒด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."
75
+ if config_obj.get("version") != 1:
76
+ return False, "version์€ 1์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."
77
+ sections = config_obj.get("sections")
78
+ if not isinstance(sections, list):
79
+ return False, "sections๋Š” ๋ฐฐ์—ด์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."
80
+ for s in sections:
81
+ if not isinstance(s, dict):
82
+ return False, "sections ํ•ญ๋ชฉ์€ ๊ฐ์ฒด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."
83
+ if not s.get("label") or not isinstance(s.get("label"), str):
84
+ return False, "section.label์€ ๋ฌธ์ž์—ด์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."
85
+ items = s.get("items")
86
+ if not isinstance(items, list):
87
+ return False, f"'{s.get('label')}' section์˜ items๋Š” ๋ฐฐ์—ด์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."
88
+ for it in items:
89
+ if not isinstance(it, dict):
90
+ return False, f"'{s.get('label')}' section์˜ item์€ ๊ฐ์ฒด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."
91
+ if not it.get("label") or not isinstance(it.get("label"), str):
92
+ return False, f"'{s.get('label')}' section์˜ item.label์€ ๋ฌธ์ž์—ด์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."
93
+ endpoint = it.get("endpoint")
94
+ if not endpoint or not isinstance(endpoint, str):
95
+ return False, f"'{s.get('label')}' section์˜ item.endpoint๋Š” ๋ฌธ์ž์—ด์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."
96
+ actions = config_obj.get("actions", [])
97
+ if actions is not None:
98
+ if not isinstance(actions, list):
99
+ return False, "actions๋Š” ๋ฐฐ์—ด์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."
100
+ for a in actions:
101
+ if not isinstance(a, dict):
102
+ return False, "actions ํ•ญ๋ชฉ์€ ๊ฐ์ฒด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."
103
+ if not a.get("label") or not isinstance(a.get("label"), str):
104
+ return False, "actions.label์€ ๋ฌธ์ž์—ด์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."
105
+ if not a.get("endpoint") or not isinstance(a.get("endpoint"), str):
106
+ return False, "actions.endpoint๋Š” ๋ฌธ์ž์—ด์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."
107
+ return True, None
108
+
109
+
110
+ def get_admin_menu_config():
111
+ """DB(SystemConfig)์— ์ €์žฅ๋œ ๋ฉ”๋‰ด ๊ตฌ์„ฑ์„ ์ฝ๊ณ , ์—†๊ฑฐ๋‚˜ ๊นจ์กŒ์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜"""
112
+ try:
113
+ raw = SystemConfig.get_config(ADMIN_MENU_CONFIG_KEY, None)
114
+ if not raw:
115
+ return get_default_admin_menu()
116
+ obj = json.loads(raw)
117
+ ok, err = validate_admin_menu_config(obj)
118
+ if not ok:
119
+ print(f"[admin_menu] ์ €์žฅ๋œ ๋ฉ”๋‰ด ์„ค์ •์ด ์œ ํšจํ•˜์ง€ ์•Š์•„ ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ: {err}")
120
+ return get_default_admin_menu()
121
+ return obj
122
+ except Exception as e:
123
+ print(f"[admin_menu] ๋ฉ”๋‰ด ์„ค์ • ๋กœ๋“œ ์˜ค๋ฅ˜, ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ: {e}")
124
+ return get_default_admin_menu()
125
+
126
+
127
+ def save_admin_menu_config(config_obj):
128
+ ok, err = validate_admin_menu_config(config_obj)
129
+ if not ok:
130
+ raise ValueError(err)
131
+ SystemConfig.set_config(
132
+ ADMIN_MENU_CONFIG_KEY,
133
+ json.dumps(config_obj, ensure_ascii=False, indent=2),
134
+ description="๊ด€๋ฆฌ์ž ์ƒ๋‹จ ๋ฉ”๋‰ด ๊ตฌ์„ฑ(JSON)"
135
+ )
136
+ return True
137
+
138
+
139
+ @main_bp.app_context_processor
140
+ def inject_admin_menu():
141
+ """๋ชจ๋“  ํ…œํ”Œ๋ฆฟ์—์„œ admin_menu ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋„๋ก ์ฃผ์ž…"""
142
+ try:
143
+ if getattr(current_user, "is_authenticated", False) and getattr(current_user, "is_admin", False):
144
+ return {"admin_menu": get_admin_menu_config()}
145
+ except Exception:
146
+ pass
147
+ return {}
148
+
149
 
150
  def ensure_chatbot_prompt_table_exists():
151
  """chatbot_prompt ํ…Œ์ด๋ธ”์ด ์—†์œผ๋ฉด ์ƒ์„ฑ (์šด์˜ ํ™˜๊ฒฝ์—์„œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ˆ„๋ฝ ๋Œ€๋น„)"""
 
2256
  """์›น์†Œ์„ค ๊ด€๋ฆฌ ํŽ˜์ด์ง€"""
2257
  return render_template('admin_webnovels.html')
2258
 
2259
+ @main_bp.route('/admin/menu')
2260
+ @admin_required
2261
+ def admin_menu():
2262
+ """๊ด€๋ฆฌ์ž ์ƒ๋‹จ ๋ฉ”๋‰ด ๊ด€๋ฆฌ ํŽ˜์ด์ง€"""
2263
+ return render_template('admin_menu.html')
2264
+
2265
  @main_bp.route('/admin/prompts')
2266
  @admin_required
2267
  def admin_prompts():
 
4821
  except Exception as e:
4822
  return jsonify({'error': f'ํ”„๋กฌํ”„ํŠธ ํŒŒ์ผ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜: {str(e)}'}), 500
4823
 
4824
+
4825
+ @main_bp.route('/api/admin/menu', methods=['GET'])
4826
+ @admin_required
4827
+ def get_admin_menu_api():
4828
+ """๊ด€๋ฆฌ์ž ๋ฉ”๋‰ด ๊ตฌ์„ฑ ์กฐํšŒ"""
4829
+ try:
4830
+ return jsonify(get_admin_menu_config()), 200
4831
+ except Exception as e:
4832
+ return jsonify({'error': f'๋ฉ”๋‰ด ์„ค์ • ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜: {str(e)}'}), 500
4833
+
4834
+
4835
+ @main_bp.route('/api/admin/menu', methods=['PUT'])
4836
+ @admin_required
4837
+ def update_admin_menu_api():
4838
+ """๊ด€๋ฆฌ์ž ๋ฉ”๋‰ด ๊ตฌ์„ฑ ์ €์žฅ"""
4839
+ try:
4840
+ data = request.get_json(silent=True) or {}
4841
+ save_admin_menu_config(data)
4842
+ return jsonify({'message': '๋ฉ”๋‰ด ๊ตฌ์„ฑ์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'}), 200
4843
+ except ValueError as ve:
4844
+ return jsonify({'error': str(ve)}), 400
4845
+ except Exception as e:
4846
+ return jsonify({'error': f'๋ฉ”๋‰ด ์„ค์ • ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜: {str(e)}'}), 500
4847
+
4848
  @main_bp.route('/api/files/<int:file_id>/chunks', methods=['GET'])
4849
  @login_required
4850
  def get_file_chunks(file_id):
templates/_admin_nav.html ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {# ๊ณตํ†ต ๊ด€๋ฆฌ์ž ์ƒ๋‹จ ๋„ค๋น„๊ฒŒ์ด์…˜ + ๋ชจ๋ฐ”์ผ ๋ฉ”๋‰ด (๋ฉ”๋‰ด ๊ตฌ์„ฑ์€ admin_menu ๋ณ€์ˆ˜) #}
2
+ <div class="admin-nav">
3
+ <style>
4
+ .admin-nav .header {
5
+ background: white;
6
+ border-bottom: 1px solid #dadce0;
7
+ padding: 16px 24px;
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: space-between;
11
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
12
+ position: relative;
13
+ z-index: 100;
14
+ }
15
+ .admin-nav .header-title {
16
+ font-size: 20px;
17
+ font-weight: 500;
18
+ display: flex;
19
+ align-items: center;
20
+ gap: 12px;
21
+ }
22
+ .admin-nav .header-actions {
23
+ display: flex;
24
+ gap: 8px;
25
+ align-items: center;
26
+ flex-wrap: wrap;
27
+ position: relative;
28
+ z-index: 101;
29
+ }
30
+ .admin-nav .dropdown { position: relative; display: inline-block; z-index: 10001; }
31
+ .admin-nav .dropdown::after { content: ''; position: absolute; left: 0; right: 0; top: 100%; height: 8px; }
32
+ .admin-nav .dropdown-toggle {
33
+ padding: 8px 16px;
34
+ background: #f1f3f4;
35
+ color: #202124;
36
+ border: none;
37
+ border-radius: 6px;
38
+ font-size: 14px;
39
+ font-weight: 500;
40
+ cursor: pointer;
41
+ transition: all 0.2s;
42
+ display: flex;
43
+ align-items: center;
44
+ gap: 6px;
45
+ }
46
+ .admin-nav .dropdown-toggle:hover { background: #e8eaed; }
47
+ .admin-nav .dropdown-toggle::after { content: 'โ–ผ'; font-size: 10px; transition: transform 0.2s; }
48
+ .admin-nav .dropdown:hover .dropdown-toggle::after { transform: rotate(180deg); }
49
+ .admin-nav .dropdown-menu {
50
+ position: absolute;
51
+ top: calc(100% + 4px);
52
+ left: 0;
53
+ background: white;
54
+ border: 1px solid #dadce0;
55
+ border-radius: 6px;
56
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
57
+ min-width: 200px;
58
+ opacity: 0;
59
+ visibility: hidden;
60
+ transform: translateY(-8px);
61
+ transition: all 0.2s ease;
62
+ z-index: 10002;
63
+ padding: 4px 0;
64
+ pointer-events: none;
65
+ white-space: nowrap;
66
+ }
67
+ .admin-nav .dropdown:hover .dropdown-menu {
68
+ opacity: 1;
69
+ visibility: visible;
70
+ transform: translateY(0);
71
+ pointer-events: auto;
72
+ }
73
+ .admin-nav .dropdown-item {
74
+ display: block;
75
+ padding: 10px 16px;
76
+ color: #202124;
77
+ text-decoration: none;
78
+ font-size: 14px;
79
+ transition: background 0.2s;
80
+ }
81
+ .admin-nav .dropdown-item:hover { background: #f8f9fa; }
82
+
83
+ .admin-nav .btn {
84
+ padding: 8px 16px;
85
+ background: #f1f3f4;
86
+ color: #202124;
87
+ border: none;
88
+ border-radius: 6px;
89
+ font-size: 14px;
90
+ font-weight: 500;
91
+ cursor: pointer;
92
+ transition: all 0.2s;
93
+ text-decoration: none;
94
+ display: inline-flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ }
98
+ .admin-nav .btn:hover { background: #e8eaed; }
99
+
100
+ .admin-nav .menu-toggle {
101
+ display: none;
102
+ background: none;
103
+ border: none;
104
+ font-size: 24px;
105
+ cursor: pointer;
106
+ padding: 8px;
107
+ color: #202124;
108
+ }
109
+
110
+ .admin-nav .mobile-menu {
111
+ display: none;
112
+ position: fixed;
113
+ top: 0;
114
+ left: 0;
115
+ right: 0;
116
+ bottom: 0;
117
+ background: rgba(0, 0, 0, 0.5);
118
+ z-index: 1000;
119
+ }
120
+ .admin-nav .mobile-menu.active { display: block; }
121
+ .admin-nav .mobile-menu-content {
122
+ position: fixed;
123
+ top: 0;
124
+ right: -100%;
125
+ width: 280px;
126
+ max-width: 80%;
127
+ height: 100%;
128
+ background: white;
129
+ box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
130
+ transition: right 0.3s ease;
131
+ overflow-y: auto;
132
+ z-index: 1001;
133
+ }
134
+ .admin-nav .mobile-menu.active .mobile-menu-content { right: 0; }
135
+ .admin-nav .mobile-menu-header {
136
+ padding: 16px 20px;
137
+ border-bottom: 1px solid #dadce0;
138
+ display: flex;
139
+ justify-content: space-between;
140
+ align-items: center;
141
+ background: white;
142
+ position: sticky;
143
+ top: 0;
144
+ z-index: 10;
145
+ }
146
+ .admin-nav .mobile-menu-title { font-size: 18px; font-weight: 500; }
147
+ .admin-nav .mobile-menu-close {
148
+ background: none;
149
+ border: none;
150
+ font-size: 24px;
151
+ cursor: pointer;
152
+ color: #202124;
153
+ width: 32px;
154
+ height: 32px;
155
+ display: flex;
156
+ align-items: center;
157
+ justify-content: center;
158
+ }
159
+ .admin-nav .mobile-menu-user {
160
+ padding: 16px 20px;
161
+ border-bottom: 1px solid #dadce0;
162
+ color: #5f6368;
163
+ font-size: 14px;
164
+ }
165
+ .admin-nav .mobile-menu-items { padding: 8px 0; }
166
+ .admin-nav .mobile-menu-section {
167
+ padding: 8px 20px;
168
+ font-size: 11px;
169
+ font-weight: 600;
170
+ color: #5f6368;
171
+ text-transform: uppercase;
172
+ border-bottom: 1px solid #f1f3f4;
173
+ margin-top: 8px;
174
+ }
175
+ .admin-nav .mobile-menu-item {
176
+ display: block;
177
+ padding: 12px 20px;
178
+ color: #202124;
179
+ text-decoration: none;
180
+ border-bottom: 1px solid #f1f3f4;
181
+ transition: background 0.2s;
182
+ }
183
+ .admin-nav .mobile-menu-item:hover { background: #f8f9fa; }
184
+
185
+ @media (max-width: 768px) {
186
+ .admin-nav .header-actions { display: none; }
187
+ .admin-nav .menu-toggle { display: block; }
188
+ .admin-nav .header { padding: 12px 16px; }
189
+ .admin-nav .header-title { font-size: 18px; }
190
+ }
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">
197
+ {% if admin_nav_icon is defined %}<span>{{ admin_nav_icon }}</span>{% endif %}
198
+ <span>{{ admin_nav_title if admin_nav_title is defined else '๊ด€๋ฆฌ์ž' }}</span>
199
+ </div>
200
+ <button class="menu-toggle" onclick="adminNavToggleMobileMenu()" aria-label="๋ฉ”๋‰ด ์—ด๊ธฐ">โ˜ฐ</button>
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>
221
+ </div>
222
+
223
+ <!-- ๋ชจ๋ฐ”์ผ ๋ฉ”๋‰ด -->
224
+ <div class="mobile-menu" id="adminNavMobileMenu" onclick="adminNavCloseOnBackdrop(event)">
225
+ <div class="mobile-menu-content" onclick="event.stopPropagation()">
226
+ <div class="mobile-menu-header">
227
+ <div class="mobile-menu-title">๋ฉ”๋‰ด</div>
228
+ <button class="mobile-menu-close" onclick="adminNavToggleMobileMenu()" aria-label="๋ฉ”๋‰ด ๋‹ซ๊ธฐ">&times;</button>
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>
244
+ </div>
245
+
246
+ <script>
247
+ function adminNavToggleMobileMenu() {
248
+ const menu = document.getElementById('adminNavMobileMenu');
249
+ if (!menu) return;
250
+ menu.classList.toggle('active');
251
+ document.body.style.overflow = menu.classList.contains('active') ? 'hidden' : '';
252
+ }
253
+ function adminNavCloseMobileMenu() {
254
+ const menu = document.getElementById('adminNavMobileMenu');
255
+ if (!menu) return;
256
+ menu.classList.remove('active');
257
+ document.body.style.overflow = '';
258
+ }
259
+ function adminNavCloseOnBackdrop(event) {
260
+ if (event && event.target && event.target.id === 'adminNavMobileMenu') {
261
+ adminNavCloseMobileMenu();
262
+ }
263
+ }
264
+ </script>
265
+ </div>
266
+
267
+
templates/admin.html CHANGED
@@ -660,99 +660,9 @@
660
  </style>
661
  </head>
662
  <body>
663
- <div class="header">
664
- <div class="header-title">
665
- <span>๐Ÿค–</span>
666
- <span>SOY NV AI ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€</span>
667
- </div>
668
- <button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ์—ด๊ธฐ">โ˜ฐ</button>
669
- <div class="header-actions">
670
- <span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
671
-
672
- {# ์‚ฌ์ดํŠธ ๊ด€๋ฆฌ #}
673
- <div class="dropdown">
674
- <button type="button" class="dropdown-toggle">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</button>
675
- <div class="dropdown-menu">
676
- <a href="{{ url_for('main.admin') }}" class="dropdown-item">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
677
- <a href="{{ url_for('main.admin_tokens') }}" class="dropdown-item">ํ† ํฐ ํ†ต๊ณ„</a>
678
- </div>
679
- </div>
680
-
681
- {# ์›น์†Œ์„ค ๊ด€๋ฆฌ #}
682
- <div class="dropdown">
683
- <button type="button" class="dropdown-toggle">์›น์†Œ์„ค ๊ด€๋ฆฌ</button>
684
- <div class="dropdown-menu">
685
- <a href="{{ url_for('main.admin_webnovels') }}" class="dropdown-item">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
686
- <a href="{{ url_for('main.admin_messages') }}" class="dropdown-item">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
687
- </div>
688
- </div>
689
-
690
- {# AI ์„ค์ • #}
691
- <div class="dropdown">
692
- <button type="button" class="dropdown-toggle">AI ์„ค์ •</button>
693
- <div class="dropdown-menu">
694
- <a href="{{ url_for('main.admin_settings') }}" class="dropdown-item">AI ์„ค์ •</a>
695
- <a href="{{ url_for('main.admin_prompts') }}" class="dropdown-item">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
696
- </div>
697
- </div>
698
-
699
- {# ์ฑ—๋ด‡ #}
700
- <div class="dropdown">
701
- <button type="button" class="dropdown-toggle">์ฑ—๋ด‡</button>
702
- <div class="dropdown-menu">
703
- <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
704
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
705
- </div>
706
- </div>
707
-
708
- {# ํŽธ์˜๊ธฐ๋Šฅ #}
709
- <div class="dropdown">
710
- <button type="button" class="dropdown-toggle">ํŽธ์˜๊ธฐ๋Šฅ</button>
711
- <div class="dropdown-menu">
712
- <a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์œ ํ‹ธ</a>
713
- </div>
714
- </div>
715
-
716
- {# ๋ฉ”์ธ์œผ๋กœ #}
717
- <a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ”์ธ์œผ๋กœ</a>
718
- <a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋กœ๊ทธ์•„์›ƒ</a>
719
- </div>
720
- </div>
721
-
722
- <!-- ๋ชจ๋ฐ”์ผ ๋ฉ”๋‰ด -->
723
- <div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
724
- <div class="mobile-menu-content" onclick="event.stopPropagation()">
725
- <div class="mobile-menu-header">
726
- <div class="mobile-menu-title">๋ฉ”๋‰ด</div>
727
- <button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ๋‹ซ๊ธฐ">&times;</button>
728
- </div>
729
- <div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
730
- <div class="mobile-menu-items">
731
- <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</div>
732
- <a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
733
- <a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ† ํฐ ํ†ต๊ณ„</a>
734
-
735
- <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>
736
- <a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
737
- <a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
738
-
739
- <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>
740
- <a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์„ค์ •</a>
741
- <a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
742
-
743
- <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>
744
- <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
745
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
746
-
747
- <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>
748
- <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์œ ํ‹ธ</a>
749
-
750
- <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>
751
- <a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์ธ์œผ๋กœ</a>
752
- <a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋กœ๊ทธ์•„์›ƒ</a>
753
- </div>
754
- </div>
755
- </div>
756
 
757
  <div class="container">
758
  <div class="page-header">
 
660
  </style>
661
  </head>
662
  <body>
663
+ {% set admin_nav_title = '๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€' %}
664
+ {% set admin_nav_icon = '๐Ÿค–' %}
665
+ {% include '_admin_nav.html' %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
 
667
  <div class="container">
668
  <div class="page-header">
templates/admin_chatbot_prompts.html CHANGED
@@ -159,20 +159,9 @@
159
  </style>
160
  </head>
161
  <body>
162
- <div class="header">
163
- <div class="header-title">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</div>
164
- <div class="header-actions">
165
- <div class="dropdown">
166
- <button type="button" class="dropdown-toggle">์ฑ—๋ด‡</button>
167
- <div class="dropdown-menu">
168
- <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
169
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
170
- </div>
171
- </div>
172
- <a href="{{ url_for('main.admin') }}" class="btn btn-secondary">๊ด€๋ฆฌ์ž</a>
173
- <a href="{{ url_for('main.logout') }}" class="btn btn-secondary">๋กœ๊ทธ์•„์›ƒ</a>
174
- </div>
175
- </div>
176
 
177
  <div class="container">
178
  <div class="file-selector">
 
159
  </style>
160
  </head>
161
  <body>
162
+ {% set admin_nav_title = '์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ' %}
163
+ {% set admin_nav_icon = '๐Ÿค–' %}
164
+ {% include '_admin_nav.html' %}
 
 
 
 
 
 
 
 
 
 
 
165
 
166
  <div class="container">
167
  <div class="file-selector">
templates/admin_files.html CHANGED
@@ -579,99 +579,9 @@
579
  </style>
580
  </head>
581
  <body>
582
- <div class="header">
583
- <div class="header-title">
584
- <span>๐Ÿค–</span>
585
- <span>SOY NV AI ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€</span>
586
- </div>
587
- <button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ์—ด๊ธฐ">โ˜ฐ</button>
588
- <div class="header-actions">
589
- <span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
590
-
591
- {# ์‚ฌ์ดํŠธ ๊ด€๋ฆฌ #}
592
- <div class="dropdown">
593
- <button type="button" class="dropdown-toggle">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</button>
594
- <div class="dropdown-menu">
595
- <a href="{{ url_for('main.admin') }}" class="dropdown-item">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
596
- <a href="{{ url_for('main.admin_tokens') }}" class="dropdown-item">ํ† ํฐ ํ†ต๊ณ„</a>
597
- </div>
598
- </div>
599
-
600
- {# ์›น์†Œ์„ค ๊ด€๋ฆฌ #}
601
- <div class="dropdown">
602
- <button type="button" class="dropdown-toggle">์›น์†Œ์„ค ๊ด€๋ฆฌ</button>
603
- <div class="dropdown-menu">
604
- <a href="{{ url_for('main.admin_webnovels') }}" class="dropdown-item">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
605
- <a href="{{ url_for('main.admin_messages') }}" class="dropdown-item">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
606
- </div>
607
- </div>
608
-
609
- {# AI ์„ค์ • #}
610
- <div class="dropdown">
611
- <button type="button" class="dropdown-toggle">AI ์„ค์ •</button>
612
- <div class="dropdown-menu">
613
- <a href="{{ url_for('main.admin_settings') }}" class="dropdown-item">AI ์„ค์ •</a>
614
- <a href="{{ url_for('main.admin_prompts') }}" class="dropdown-item">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
615
- </div>
616
- </div>
617
-
618
- {# ์ฑ—๋ด‡ #}
619
- <div class="dropdown">
620
- <button type="button" class="dropdown-toggle">์ฑ—๋ด‡</button>
621
- <div class="dropdown-menu">
622
- <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
623
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
624
- </div>
625
- </div>
626
-
627
- {# ํŽธ์˜๊ธฐ๋Šฅ #}
628
- <div class="dropdown">
629
- <button type="button" class="dropdown-toggle">ํŽธ์˜๊ธฐ๋Šฅ</button>
630
- <div class="dropdown-menu">
631
- <a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์œ ํ‹ธ</a>
632
- </div>
633
- </div>
634
-
635
- {# ๋ฉ”์ธ์œผ๋กœ #}
636
- <a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ”์ธ์œผ๋กœ</a>
637
- <a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋กœ๊ทธ์•„์›ƒ</a>
638
- </div>
639
- </div>
640
-
641
- <!-- ๋ชจ๋ฐ”์ผ ๋ฉ”๋‰ด -->
642
- <div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
643
- <div class="mobile-menu-content" onclick="event.stopPropagation()">
644
- <div class="mobile-menu-header">
645
- <div class="mobile-menu-title">๋ฉ”๋‰ด</div>
646
- <button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ๋‹ซ๊ธฐ">&times;</button>
647
- </div>
648
- <div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
649
- <div class="mobile-menu-items">
650
- <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</div>
651
- <a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
652
- <a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ† ํฐ ํ†ต๊ณ„</a>
653
-
654
- <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>
655
- <a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
656
- <a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
657
-
658
- <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>
659
- <a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์„ค์ •</a>
660
- <a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
661
-
662
- <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>
663
- <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
664
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
665
-
666
- <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>
667
- <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์œ ํ‹ธ</a>
668
-
669
- <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>
670
- <a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์ธ์œผ๋กœ</a>
671
- <a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋กœ๊ทธ์•„์›ƒ</a>
672
- </div>
673
- </div>
674
- </div>
675
 
676
  <div class="container">
677
  <div class="page-header">
 
579
  </style>
580
  </head>
581
  <body>
582
+ {% set admin_nav_title = 'ํŒŒ์ผ ๊ด€๋ฆฌ' %}
583
+ {% set admin_nav_icon = '๐Ÿ“' %}
584
+ {% include '_admin_nav.html' %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585
 
586
  <div class="container">
587
  <div class="page-header">
templates/admin_menu.html ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>๊ด€๋ฆฌ์ž ๋ฉ”๋‰ด ๊ด€๋ฆฌ - SOY NV AI</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
9
+ <style>
10
+ * { margin: 0; padding: 0; box-sizing: border-box; }
11
+ body {
12
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
13
+ background: #f8f9fa;
14
+ color: #202124;
15
+ overflow-x: hidden;
16
+ }
17
+ .container { max-width: 1100px; margin: 24px auto; padding: 0 24px; }
18
+ .card {
19
+ background: white;
20
+ border: 1px solid #dadce0;
21
+ border-radius: 10px;
22
+ overflow: hidden;
23
+ }
24
+ .card-header {
25
+ padding: 14px 16px;
26
+ border-bottom: 1px solid #eee;
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: space-between;
30
+ gap: 12px;
31
+ flex-wrap: wrap;
32
+ }
33
+ .card-title { font-size: 16px; font-weight: 600; }
34
+ .actions { display:flex; gap: 8px; flex-wrap: wrap; }
35
+ .btn {
36
+ padding: 8px 14px;
37
+ border: none;
38
+ border-radius: 6px;
39
+ font-size: 14px;
40
+ font-weight: 600;
41
+ cursor: pointer;
42
+ }
43
+ .btn-primary { background: #1a73e8; color: white; }
44
+ .btn-primary:hover { background: #1557b0; }
45
+ .btn-secondary { background: #f1f3f4; color: #202124; }
46
+ .btn-secondary:hover { background: #e8eaed; }
47
+ .btn-danger { background: #c5221f; color: white; }
48
+ .btn-danger:hover { background: #a50e0e; }
49
+ .card-body { padding: 16px; }
50
+ textarea {
51
+ width: 100%;
52
+ min-height: 420px;
53
+ border: 1px solid #dadce0;
54
+ border-radius: 8px;
55
+ padding: 12px;
56
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
57
+ font-size: 12.5px;
58
+ line-height: 1.5;
59
+ background: #fff;
60
+ }
61
+ .hint {
62
+ margin-top: 10px;
63
+ color: #5f6368;
64
+ font-size: 13px;
65
+ line-height: 1.6;
66
+ }
67
+ .alert {
68
+ margin-top: 12px;
69
+ padding: 10px 12px;
70
+ border-radius: 8px;
71
+ font-size: 13px;
72
+ border: 1px solid transparent;
73
+ display:none;
74
+ }
75
+ .alert.success { display:block; background:#e6f4ea; border-color:#b7dfc0; color:#137333; }
76
+ .alert.error { display:block; background:#fce8e6; border-color:#f6aea9; color:#c5221f; }
77
+ </style>
78
+ </head>
79
+ <body>
80
+ {% set admin_nav_title = '๋ฉ”๋‰ด ๊ด€๋ฆฌ' %}
81
+ {% set admin_nav_icon = '๐Ÿงญ' %}
82
+ {% include '_admin_nav.html' %}
83
+
84
+ <div class="container">
85
+ <div class="card">
86
+ <div class="card-header">
87
+ <div class="card-title">๊ด€๋ฆฌ์ž ์ƒ๋‹จ ๋ฉ”๋‰ด ๊ตฌ์„ฑ(JSON)</div>
88
+ <div class="actions">
89
+ <button class="btn btn-secondary" onclick="loadMenu()">์ƒˆ๋กœ๊ณ ์นจ</button>
90
+ <button class="btn btn-danger" onclick="resetToDefault()">๊ธฐ๋ณธ๊ฐ’์œผ๋กœ</button>
91
+ <button class="btn btn-primary" onclick="saveMenu()">์ €์žฅ</button>
92
+ </div>
93
+ </div>
94
+ <div class="card-body">
95
+ <textarea id="menuJson" spellcheck="false"></textarea>
96
+ <div id="alert" class="alert"></div>
97
+ <div class="hint">
98
+ - <strong>sections</strong>: ์ƒ๋‹จ ๋“œ๋กญ๋‹ค์šด ๊ทธ๋ฃน ๋ชฉ๋ก<br>
99
+ - ๊ฐ item์€ <strong>{ label, endpoint }</strong> ํ˜•์‹์ด๋ฉฐ endpoint๋Š” ์˜ˆ: <code>main.admin_webnovels</code><br>
100
+ - <strong>actions</strong>: ์šฐ์ธก ๊ณ ์ • ๋ฒ„ํŠผ(๋ฉ”์ธ์œผ๋กœ/๋กœ๊ทธ์•„์›ƒ ๋“ฑ)
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <script>
107
+ const textarea = document.getElementById('menuJson');
108
+ const alertEl = document.getElementById('alert');
109
+
110
+ function showAlert(msg, type) {
111
+ alertEl.className = `alert ${type}`;
112
+ alertEl.textContent = msg;
113
+ }
114
+
115
+ function pretty(obj) {
116
+ return JSON.stringify(obj, null, 2);
117
+ }
118
+
119
+ async function loadMenu() {
120
+ try {
121
+ const res = await fetch('/api/admin/menu', { credentials: 'include' });
122
+ const data = await res.json();
123
+ if (!res.ok) {
124
+ showAlert(data.error || `๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์‹คํŒจ (${res.status})`, 'error');
125
+ return;
126
+ }
127
+ textarea.value = pretty(data);
128
+ showAlert('๋ฉ”๋‰ด ์„ค์ •์„ ๋ถˆ๋Ÿฌ์™”์Šต๋‹ˆ๋‹ค.', 'success');
129
+ } catch (e) {
130
+ showAlert(`๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์˜ค๋ฅ˜: ${e.message}`, 'error');
131
+ }
132
+ }
133
+
134
+ async function saveMenu() {
135
+ let obj;
136
+ try {
137
+ obj = JSON.parse(textarea.value);
138
+ } catch (e) {
139
+ showAlert(`JSON ํŒŒ์‹ฑ ์˜ค๋ฅ˜: ${e.message}`, 'error');
140
+ return;
141
+ }
142
+ try {
143
+ const res = await fetch('/api/admin/menu', {
144
+ method: 'PUT',
145
+ credentials: 'include',
146
+ headers: { 'Content-Type': 'application/json' },
147
+ body: JSON.stringify(obj)
148
+ });
149
+ const data = await res.json();
150
+ if (!res.ok) {
151
+ showAlert(data.error || `์ €์žฅ ์‹คํŒจ (${res.status})`, 'error');
152
+ return;
153
+ }
154
+ showAlert('์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ๊ณ ์นจ ์—†์ด๋„ ๋‹ค์Œ ํŽ˜์ด์ง€ ์ด๋™๋ถ€ํ„ฐ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค.', 'success');
155
+ } catch (e) {
156
+ showAlert(`์ €์žฅ ์˜ค๋ฅ˜: ${e.message}`, 'error');
157
+ }
158
+ }
159
+
160
+ async function resetToDefault() {
161
+ if (!confirm('๊ธฐ๋ณธ ๋ฉ”๋‰ด ๊ตฌ์„ฑ์œผ๋กœ ๋˜๋Œ๋ฆด๊นŒ์š”? (์ €์žฅ ์ „๊นŒ์ง€๋Š” ๋ฐ˜์˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค)')) return;
162
+ // ์„œ๋ฒ„ ๊ธฐ๋ณธ๊ฐ’์€ GET ์‘๋‹ต๊ณผ ๋™์ผํ•˜๋ฏ€๋กœ, ํ˜„์žฌ๊ฐ’์„ GET์œผ๋กœ ๋‹ค์‹œ ๋ฎ์–ด์“ด ๋’ค ์ €์žฅํ•˜๋ฉด ๋จ.
163
+ await loadMenu();
164
+ }
165
+
166
+ loadMenu();
167
+ </script>
168
+ </body>
169
+ </html>
170
+
171
+
templates/admin_messages.html CHANGED
@@ -565,99 +565,9 @@
565
  </style>
566
  </head>
567
  <body>
568
- <div class="header">
569
- <div class="header-title">
570
- <span>๐Ÿค–</span>
571
- <span>SOY NV AI ๋ฉ”์‹œ์ง€ ํ™•์ธ</span>
572
- </div>
573
- <button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ์—ด๊ธฐ">โ˜ฐ</button>
574
- <div class="header-actions">
575
- <span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
576
-
577
- {# ์‚ฌ์ดํŠธ ๊ด€๋ฆฌ #}
578
- <div class="dropdown">
579
- <button type="button" class="dropdown-toggle">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</button>
580
- <div class="dropdown-menu">
581
- <a href="{{ url_for('main.admin') }}" class="dropdown-item">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
582
- <a href="{{ url_for('main.admin_tokens') }}" class="dropdown-item">ํ† ํฐ ํ†ต๊ณ„</a>
583
- </div>
584
- </div>
585
-
586
- {# ์›น์†Œ์„ค ๊ด€๋ฆฌ #}
587
- <div class="dropdown">
588
- <button type="button" class="dropdown-toggle">์›น์†Œ์„ค ๊ด€๋ฆฌ</button>
589
- <div class="dropdown-menu">
590
- <a href="{{ url_for('main.admin_webnovels') }}" class="dropdown-item">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
591
- <a href="{{ url_for('main.admin_messages') }}" class="dropdown-item">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
592
- </div>
593
- </div>
594
-
595
- {# AI ์„ค์ • #}
596
- <div class="dropdown">
597
- <button type="button" class="dropdown-toggle">AI ์„ค์ •</button>
598
- <div class="dropdown-menu">
599
- <a href="{{ url_for('main.admin_settings') }}" class="dropdown-item">AI ์„ค์ •</a>
600
- <a href="{{ url_for('main.admin_prompts') }}" class="dropdown-item">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
601
- </div>
602
- </div>
603
-
604
- {# ์ฑ—๋ด‡ #}
605
- <div class="dropdown">
606
- <button type="button" class="dropdown-toggle">์ฑ—๋ด‡</button>
607
- <div class="dropdown-menu">
608
- <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
609
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
610
- </div>
611
- </div>
612
-
613
- {# ํŽธ์˜๊ธฐ๋Šฅ #}
614
- <div class="dropdown">
615
- <button type="button" class="dropdown-toggle">ํŽธ์˜๊ธฐ๋Šฅ</button>
616
- <div class="dropdown-menu">
617
- <a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์œ ํ‹ธ</a>
618
- </div>
619
- </div>
620
-
621
- {# ๋ฉ”์ธ์œผ๋กœ #}
622
- <a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ”์ธ์œผ๋กœ</a>
623
- <a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋กœ๊ทธ์•„์›ƒ</a>
624
- </div>
625
- </div>
626
-
627
- <!-- ๋ชจ๋ฐ”์ผ ๋ฉ”๋‰ด -->
628
- <div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
629
- <div class="mobile-menu-content" onclick="event.stopPropagation()">
630
- <div class="mobile-menu-header">
631
- <div class="mobile-menu-title">๋ฉ”๋‰ด</div>
632
- <button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ๋‹ซ๊ธฐ">&times;</button>
633
- </div>
634
- <div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
635
- <div class="mobile-menu-items">
636
- <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</div>
637
- <a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
638
- <a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ† ํฐ ํ†ต๊ณ„</a>
639
-
640
- <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>
641
- <a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
642
- <a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
643
-
644
- <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>
645
- <a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์„ค์ •</a>
646
- <a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
647
-
648
- <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>
649
- <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
650
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
651
-
652
- <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>
653
- <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์œ ํ‹ธ</a>
654
-
655
- <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>
656
- <a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์ธ์œผ๋กœ</a>
657
- <a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋กœ๊ทธ์•„์›ƒ</a>
658
- </div>
659
- </div>
660
- </div>
661
 
662
  <div class="container">
663
  <div class="page-header">
 
565
  </style>
566
  </head>
567
  <body>
568
+ {% set admin_nav_title = '๋ฉ”์‹œ์ง€ ํ™•์ธ' %}
569
+ {% set admin_nav_icon = '๐Ÿ’ฌ' %}
570
+ {% include '_admin_nav.html' %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
 
572
  <div class="container">
573
  <div class="page-header">
templates/admin_prompts.html CHANGED
@@ -419,99 +419,9 @@
419
  </style>
420
  </head>
421
  <body>
422
- <div class="header">
423
- <div class="header-title">
424
- <span>๐Ÿ“</span>
425
- <span>ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</span>
426
- </div>
427
- <button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ์—ด๊ธฐ">โ˜ฐ</button>
428
- <div class="header-actions">
429
- <span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
430
-
431
- {# ์‚ฌ์ดํŠธ ๊ด€๋ฆฌ #}
432
- <div class="dropdown">
433
- <button type="button" class="dropdown-toggle">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</button>
434
- <div class="dropdown-menu">
435
- <a href="{{ url_for('main.admin') }}" class="dropdown-item">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
436
- <a href="{{ url_for('main.admin_tokens') }}" class="dropdown-item">ํ† ํฐ ํ†ต๊ณ„</a>
437
- </div>
438
- </div>
439
-
440
- {# ์›น์†Œ์„ค ๊ด€๋ฆฌ #}
441
- <div class="dropdown">
442
- <button type="button" class="dropdown-toggle">์›น์†Œ์„ค ๊ด€๋ฆฌ</button>
443
- <div class="dropdown-menu">
444
- <a href="{{ url_for('main.admin_webnovels') }}" class="dropdown-item">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
445
- <a href="{{ url_for('main.admin_messages') }}" class="dropdown-item">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
446
- </div>
447
- </div>
448
-
449
- {# AI ์„ค์ • #}
450
- <div class="dropdown">
451
- <button type="button" class="dropdown-toggle">AI ์„ค์ •</button>
452
- <div class="dropdown-menu">
453
- <a href="{{ url_for('main.admin_settings') }}" class="dropdown-item">AI ์„ค์ •</a>
454
- <a href="{{ url_for('main.admin_prompts') }}" class="dropdown-item">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
455
- </div>
456
- </div>
457
-
458
- {# ์ฑ—๋ด‡ #}
459
- <div class="dropdown">
460
- <button type="button" class="dropdown-toggle">์ฑ—๋ด‡</button>
461
- <div class="dropdown-menu">
462
- <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
463
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
464
- </div>
465
- </div>
466
-
467
- {# ํŽธ์˜๊ธฐ๋Šฅ #}
468
- <div class="dropdown">
469
- <button type="button" class="dropdown-toggle">ํŽธ์˜๊ธฐ๋Šฅ</button>
470
- <div class="dropdown-menu">
471
- <a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์œ ํ‹ธ</a>
472
- </div>
473
- </div>
474
-
475
- {# ๋ฉ”์ธ์œผ๋กœ #}
476
- <a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ”์ธ์œผ๋กœ</a>
477
- <a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋กœ๊ทธ์•„์›ƒ</a>
478
- </div>
479
- </div>
480
-
481
- <!-- ๋ชจ๋ฐ”์ผ ๋ฉ”๋‰ด -->
482
- <div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
483
- <div class="mobile-menu-content" onclick="event.stopPropagation()">
484
- <div class="mobile-menu-header">
485
- <div class="mobile-menu-title">๋ฉ”๋‰ด</div>
486
- <button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ๋‹ซ๊ธฐ">&times;</button>
487
- </div>
488
- <div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
489
- <div class="mobile-menu-items">
490
- <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</div>
491
- <a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
492
- <a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ† ํฐ ํ†ต๊ณ„</a>
493
-
494
- <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>
495
- <a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
496
- <a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
497
-
498
- <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>
499
- <a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์„ค์ •</a>
500
- <a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
501
-
502
- <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>
503
- <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
504
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
505
-
506
- <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>
507
- <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์œ ํ‹ธ</a>
508
-
509
- <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>
510
- <a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์ธ์œผ๋กœ</a>
511
- <a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋กœ๊ทธ์•„์›ƒ</a>
512
- </div>
513
- </div>
514
- </div>
515
 
516
  <div class="container">
517
  <div class="page-header">
 
419
  </style>
420
  </head>
421
  <body>
422
+ {% set admin_nav_title = 'ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ' %}
423
+ {% set admin_nav_icon = '๐Ÿ“' %}
424
+ {% include '_admin_nav.html' %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
 
426
  <div class="container">
427
  <div class="page-header">
templates/admin_settings.html CHANGED
@@ -382,99 +382,9 @@
382
  </style>
383
  </head>
384
  <body>
385
- <div class="header">
386
- <div class="header-title">
387
- <span>โš™๏ธ</span>
388
- <span>AI ์„ค์ • ๊ด€๋ฆฌ</span>
389
- </div>
390
- <button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ์—ด๊ธฐ">โ˜ฐ</button>
391
- <div class="header-actions">
392
- <span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
393
-
394
- {# ์‚ฌ์ดํŠธ ๊ด€๋ฆฌ #}
395
- <div class="dropdown">
396
- <button type="button" class="dropdown-toggle">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</button>
397
- <div class="dropdown-menu">
398
- <a href="{{ url_for('main.admin') }}" class="dropdown-item">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
399
- <a href="{{ url_for('main.admin_tokens') }}" class="dropdown-item">ํ† ํฐ ํ†ต๊ณ„</a>
400
- </div>
401
- </div>
402
-
403
- {# ์›น์†Œ์„ค ๊ด€๋ฆฌ #}
404
- <div class="dropdown">
405
- <button type="button" class="dropdown-toggle">์›น์†Œ์„ค ๊ด€๋ฆฌ</button>
406
- <div class="dropdown-menu">
407
- <a href="{{ url_for('main.admin_webnovels') }}" class="dropdown-item">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
408
- <a href="{{ url_for('main.admin_messages') }}" class="dropdown-item">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
409
- </div>
410
- </div>
411
-
412
- {# AI ์„ค์ • #}
413
- <div class="dropdown">
414
- <button type="button" class="dropdown-toggle">AI ์„ค์ •</button>
415
- <div class="dropdown-menu">
416
- <a href="{{ url_for('main.admin_settings') }}" class="dropdown-item">AI ์„ค์ •</a>
417
- <a href="{{ url_for('main.admin_prompts') }}" class="dropdown-item">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
418
- </div>
419
- </div>
420
-
421
- {# ์ฑ—๋ด‡ #}
422
- <div class="dropdown">
423
- <button type="button" class="dropdown-toggle">์ฑ—๋ด‡</button>
424
- <div class="dropdown-menu">
425
- <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
426
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
427
- </div>
428
- </div>
429
-
430
- {# ํŽธ์˜๊ธฐ๋Šฅ #}
431
- <div class="dropdown">
432
- <button type="button" class="dropdown-toggle">ํŽธ์˜๊ธฐ๋Šฅ</button>
433
- <div class="dropdown-menu">
434
- <a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์œ ํ‹ธ</a>
435
- </div>
436
- </div>
437
-
438
- {# ๋ฉ”์ธ์œผ๋กœ #}
439
- <a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ”์ธ์œผ๋กœ</a>
440
- <a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋กœ๊ทธ์•„์›ƒ</a>
441
- </div>
442
- </div>
443
-
444
- <!-- ๋ชจ๋ฐ”์ผ ๋ฉ”๋‰ด -->
445
- <div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
446
- <div class="mobile-menu-content" onclick="event.stopPropagation()">
447
- <div class="mobile-menu-header">
448
- <div class="mobile-menu-title">๋ฉ”๋‰ด</div>
449
- <button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ๋‹ซ๊ธฐ">&times;</button>
450
- </div>
451
- <div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
452
- <div class="mobile-menu-items">
453
- <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</div>
454
- <a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
455
- <a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ† ํฐ ํ†ต๊ณ„</a>
456
-
457
- <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>
458
- <a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
459
- <a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
460
-
461
- <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>
462
- <a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์„ค์ •</a>
463
- <a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
464
-
465
- <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>
466
- <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
467
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
468
-
469
- <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>
470
- <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์œ ํ‹ธ</a>
471
-
472
- <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>
473
- <a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์ธ์œผ๋กœ</a>
474
- <a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋กœ๊ทธ์•„์›ƒ</a>
475
- </div>
476
- </div>
477
- </div>
478
 
479
  <div class="container">
480
  <div class="page-header">
 
382
  </style>
383
  </head>
384
  <body>
385
+ {% set admin_nav_title = 'AI ์„ค์ • ๊ด€๋ฆฌ' %}
386
+ {% set admin_nav_icon = 'โš™๏ธ' %}
387
+ {% include '_admin_nav.html' %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
 
389
  <div class="container">
390
  <div class="page-header">
templates/admin_tags.html CHANGED
@@ -381,99 +381,9 @@
381
  </style>
382
  </head>
383
  <body>
384
- <div class="header">
385
- <div class="header-title">
386
- <span>๐Ÿท๏ธ</span>
387
- <span>ํƒœ๊ทธ ๋ณด๊ธฐ</span>
388
- </div>
389
- <button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ์—ด๊ธฐ">โ˜ฐ</button>
390
- <div class="header-actions">
391
- <span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
392
-
393
- {# ์‚ฌ์ดํŠธ ๊ด€๋ฆฌ #}
394
- <div class="dropdown">
395
- <button type="button" class="dropdown-toggle">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</button>
396
- <div class="dropdown-menu">
397
- <a href="{{ url_for('main.admin') }}" class="dropdown-item">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
398
- <a href="{{ url_for('main.admin_tokens') }}" class="dropdown-item">ํ† ํฐ ํ†ต๊ณ„</a>
399
- </div>
400
- </div>
401
-
402
- {# ์›น์†Œ์„ค ๊ด€๋ฆฌ #}
403
- <div class="dropdown">
404
- <button type="button" class="dropdown-toggle">์›น์†Œ์„ค ๊ด€๋ฆฌ</button>
405
- <div class="dropdown-menu">
406
- <a href="{{ url_for('main.admin_webnovels') }}" class="dropdown-item">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
407
- <a href="{{ url_for('main.admin_messages') }}" class="dropdown-item">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
408
- </div>
409
- </div>
410
-
411
- {# AI ์„ค์ • #}
412
- <div class="dropdown">
413
- <button type="button" class="dropdown-toggle">AI ์„ค์ •</button>
414
- <div class="dropdown-menu">
415
- <a href="{{ url_for('main.admin_settings') }}" class="dropdown-item">AI ์„ค์ •</a>
416
- <a href="{{ url_for('main.admin_prompts') }}" class="dropdown-item">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
417
- </div>
418
- </div>
419
-
420
- {# ์ฑ—๋ด‡ #}
421
- <div class="dropdown">
422
- <button type="button" class="dropdown-toggle">์ฑ—๋ด‡</button>
423
- <div class="dropdown-menu">
424
- <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
425
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
426
- </div>
427
- </div>
428
-
429
- {# ํŽธ์˜๊ธฐ๋Šฅ #}
430
- <div class="dropdown">
431
- <button type="button" class="dropdown-toggle">ํŽธ์˜๊ธฐ๋Šฅ</button>
432
- <div class="dropdown-menu">
433
- <a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์œ ํ‹ธ</a>
434
- </div>
435
- </div>
436
-
437
- {# ๋ฉ”์ธ์œผ๋กœ #}
438
- <a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ”์ธ์œผ๋กœ</a>
439
- <a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋กœ๊ทธ์•„์›ƒ</a>
440
- </div>
441
- </div>
442
-
443
- <!-- ๋ชจ๋ฐ”์ผ ๋ฉ”๋‰ด -->
444
- <div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
445
- <div class="mobile-menu-content" onclick="event.stopPropagation()">
446
- <div class="mobile-menu-header">
447
- <div class="mobile-menu-title">๋ฉ”๋‰ด</div>
448
- <button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ๋‹ซ๊ธฐ">&times;</button>
449
- </div>
450
- <div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
451
- <div class="mobile-menu-items">
452
- <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</div>
453
- <a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
454
- <a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ† ํฐ ํ†ต๊ณ„</a>
455
-
456
- <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>
457
- <a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
458
- <a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
459
-
460
- <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>
461
- <a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์„ค์ •</a>
462
- <a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
463
-
464
- <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>
465
- <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
466
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
467
-
468
- <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>
469
- <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์œ ํ‹ธ</a>
470
-
471
- <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>
472
- <a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์ธ์œผ๋กœ</a>
473
- <a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋กœ๊ทธ์•„์›ƒ</a>
474
- </div>
475
- </div>
476
- </div>
477
 
478
  <div class="container">
479
  <div class="file-selector">
 
381
  </style>
382
  </head>
383
  <body>
384
+ {% set admin_nav_title = 'ํƒœ๊ทธ ๋ณด๊ธฐ' %}
385
+ {% set admin_nav_icon = '๐Ÿท๏ธ' %}
386
+ {% include '_admin_nav.html' %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
 
388
  <div class="container">
389
  <div class="file-selector">
templates/admin_tokens.html CHANGED
@@ -444,99 +444,9 @@
444
  </style>
445
  </head>
446
  <body>
447
- <div class="header">
448
- <div class="header-title">
449
- <span>๐Ÿ“Š</span>
450
- <span>ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰ ํ†ต๊ณ„</span>
451
- </div>
452
- <button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ์—ด๊ธฐ">โ˜ฐ</button>
453
- <div class="header-actions">
454
- <span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
455
-
456
- {# ์‚ฌ์ดํŠธ ๊ด€๋ฆฌ #}
457
- <div class="dropdown">
458
- <button type="button" class="dropdown-toggle">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</button>
459
- <div class="dropdown-menu">
460
- <a href="{{ url_for('main.admin') }}" class="dropdown-item">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
461
- <a href="{{ url_for('main.admin_tokens') }}" class="dropdown-item">ํ† ํฐ ํ†ต๊ณ„</a>
462
- </div>
463
- </div>
464
-
465
- {# ์›น์†Œ์„ค ๊ด€๋ฆฌ #}
466
- <div class="dropdown">
467
- <button type="button" class="dropdown-toggle">์›น์†Œ์„ค ๊ด€๋ฆฌ</button>
468
- <div class="dropdown-menu">
469
- <a href="{{ url_for('main.admin_webnovels') }}" class="dropdown-item">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
470
- <a href="{{ url_for('main.admin_messages') }}" class="dropdown-item">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
471
- </div>
472
- </div>
473
-
474
- {# AI ์„ค์ • #}
475
- <div class="dropdown">
476
- <button type="button" class="dropdown-toggle">AI ์„ค์ •</button>
477
- <div class="dropdown-menu">
478
- <a href="{{ url_for('main.admin_settings') }}" class="dropdown-item">AI ์„ค์ •</a>
479
- <a href="{{ url_for('main.admin_prompts') }}" class="dropdown-item">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
480
- </div>
481
- </div>
482
-
483
- {# ์ฑ—๋ด‡ #}
484
- <div class="dropdown">
485
- <button type="button" class="dropdown-toggle">์ฑ—๋ด‡</button>
486
- <div class="dropdown-menu">
487
- <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
488
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
489
- </div>
490
- </div>
491
-
492
- {# ํŽธ์˜๊ธฐ๋Šฅ #}
493
- <div class="dropdown">
494
- <button type="button" class="dropdown-toggle">ํŽธ์˜๊ธฐ๋Šฅ</button>
495
- <div class="dropdown-menu">
496
- <a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์œ ํ‹ธ</a>
497
- </div>
498
- </div>
499
-
500
- {# ๋ฉ”์ธ์œผ๋กœ #}
501
- <a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ”์ธ์œผ๋กœ</a>
502
- <a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋กœ๊ทธ์•„์›ƒ</a>
503
- </div>
504
- </div>
505
-
506
- <!-- ๋ชจ๋ฐ”์ผ ๋ฉ”๋‰ด -->
507
- <div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
508
- <div class="mobile-menu-content" onclick="event.stopPropagation()">
509
- <div class="mobile-menu-header">
510
- <div class="mobile-menu-title">๋ฉ”๋‰ด</div>
511
- <button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ๋‹ซ๊ธฐ">&times;</button>
512
- </div>
513
- <div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
514
- <div class="mobile-menu-items">
515
- <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</div>
516
- <a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
517
- <a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ† ํฐ ํ†ต๊ณ„</a>
518
-
519
- <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>
520
- <a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
521
- <a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
522
-
523
- <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>
524
- <a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์„ค์ •</a>
525
- <a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
526
-
527
- <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>
528
- <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
529
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
530
-
531
- <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>
532
- <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์œ ํ‹ธ</a>
533
-
534
- <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>
535
- <a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์ธ์œผ๋กœ</a>
536
- <a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋กœ๊ทธ์•„์›ƒ</a>
537
- </div>
538
- </div>
539
- </div>
540
 
541
  <div class="container">
542
  <div class="page-header">
 
444
  </style>
445
  </head>
446
  <body>
447
+ {% set admin_nav_title = 'ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰ ํ†ต๊ณ„' %}
448
+ {% set admin_nav_icon = '๐Ÿ“Š' %}
449
+ {% include '_admin_nav.html' %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
 
451
  <div class="container">
452
  <div class="page-header">
templates/admin_utils.html CHANGED
@@ -465,99 +465,9 @@
465
  </style>
466
  </head>
467
  <body>
468
- <div class="header">
469
- <div class="header-title">
470
- <span>๐Ÿ”ง</span>
471
- <span>์œ ํ‹ธ๋ฆฌํ‹ฐ</span>
472
- </div>
473
- <button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ์—ด๊ธฐ">โ˜ฐ</button>
474
- <div class="header-actions">
475
- <span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
476
-
477
- {# ์‚ฌ์ดํŠธ ๊ด€๋ฆฌ #}
478
- <div class="dropdown">
479
- <button type="button" class="dropdown-toggle">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</button>
480
- <div class="dropdown-menu">
481
- <a href="{{ url_for('main.admin') }}" class="dropdown-item">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
482
- <a href="{{ url_for('main.admin_tokens') }}" class="dropdown-item">ํ† ํฐ ํ†ต๊ณ„</a>
483
- </div>
484
- </div>
485
-
486
- {# ์›น์†Œ์„ค ๊ด€๋ฆฌ #}
487
- <div class="dropdown">
488
- <button type="button" class="dropdown-toggle">์›น์†Œ์„ค ๊ด€๋ฆฌ</button>
489
- <div class="dropdown-menu">
490
- <a href="{{ url_for('main.admin_webnovels') }}" class="dropdown-item">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
491
- <a href="{{ url_for('main.admin_messages') }}" class="dropdown-item">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
492
- </div>
493
- </div>
494
-
495
- {# AI ์„ค์ • #}
496
- <div class="dropdown">
497
- <button type="button" class="dropdown-toggle">AI ์„ค์ •</button>
498
- <div class="dropdown-menu">
499
- <a href="{{ url_for('main.admin_settings') }}" class="dropdown-item">AI ์„ค์ •</a>
500
- <a href="{{ url_for('main.admin_prompts') }}" class="dropdown-item">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
501
- </div>
502
- </div>
503
-
504
- {# ์ฑ—๋ด‡ #}
505
- <div class="dropdown">
506
- <button type="button" class="dropdown-toggle">์ฑ—๋ด‡</button>
507
- <div class="dropdown-menu">
508
- <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
509
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
510
- </div>
511
- </div>
512
-
513
- {# ํŽธ์˜๊ธฐ๋Šฅ #}
514
- <div class="dropdown">
515
- <button type="button" class="dropdown-toggle">ํŽธ์˜๊ธฐ๋Šฅ</button>
516
- <div class="dropdown-menu">
517
- <a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์œ ํ‹ธ</a>
518
- </div>
519
- </div>
520
-
521
- {# ๋ฉ”์ธ์œผ๋กœ #}
522
- <a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ”์ธ์œผ๋กœ</a>
523
- <a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋กœ๊ทธ์•„์›ƒ</a>
524
- </div>
525
- </div>
526
-
527
- <!-- ๋ชจ๋ฐ”์ผ ๋ฉ”๋‰ด -->
528
- <div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
529
- <div class="mobile-menu-content" onclick="event.stopPropagation()">
530
- <div class="mobile-menu-header">
531
- <div class="mobile-menu-title">๋ฉ”๋‰ด</div>
532
- <button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ๋‹ซ๊ธฐ">&times;</button>
533
- </div>
534
- <div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
535
- <div class="mobile-menu-items">
536
- <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</div>
537
- <a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
538
- <a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ† ํฐ ํ†ต๊ณ„</a>
539
-
540
- <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>
541
- <a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
542
- <a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
543
-
544
- <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>
545
- <a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์„ค์ •</a>
546
- <a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๏ฟฝ๏ฟฝ๏ฟฝ๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
547
-
548
- <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>
549
- <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
550
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
551
-
552
- <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>
553
- <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์œ ํ‹ธ</a>
554
-
555
- <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>
556
- <a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์ธ์œผ๋กœ</a>
557
- <a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋กœ๊ทธ์•„์›ƒ</a>
558
- </div>
559
- </div>
560
- </div>
561
 
562
  <div class="container">
563
  <div class="page-header">
 
465
  </style>
466
  </head>
467
  <body>
468
+ {% set admin_nav_title = '์œ ํ‹ธ' %}
469
+ {% set admin_nav_icon = '๐Ÿ”ง' %}
470
+ {% include '_admin_nav.html' %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
 
472
  <div class="container">
473
  <div class="page-header">
templates/admin_webnovels.html CHANGED
@@ -797,99 +797,9 @@
797
  </style>
798
  </head>
799
  <body>
800
- <div class="header">
801
- <div class="header-title">
802
- <span>๐Ÿ“š</span>
803
- <span>์›น์†Œ์„ค ๊ด€๋ฆฌ</span>
804
- </div>
805
- <button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ์—ด๊ธฐ">โ˜ฐ</button>
806
- <div class="header-actions">
807
- <span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
808
-
809
- {# ์‚ฌ์ดํŠธ ๊ด€๋ฆฌ #}
810
- <div class="dropdown">
811
- <button type="button" class="dropdown-toggle">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</button>
812
- <div class="dropdown-menu">
813
- <a href="{{ url_for('main.admin') }}" class="dropdown-item">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
814
- <a href="{{ url_for('main.admin_tokens') }}" class="dropdown-item">ํ† ํฐ ํ†ต๊ณ„</a>
815
- </div>
816
- </div>
817
-
818
- {# ์›น์†Œ์„ค ๊ด€๋ฆฌ #}
819
- <div class="dropdown">
820
- <button type="button" class="dropdown-toggle">์›น์†Œ์„ค ๊ด€๋ฆฌ</button>
821
- <div class="dropdown-menu">
822
- <a href="{{ url_for('main.admin_webnovels') }}" class="dropdown-item">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
823
- <a href="{{ url_for('main.admin_messages') }}" class="dropdown-item">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
824
- </div>
825
- </div>
826
-
827
- {# AI ์„ค์ • #}
828
- <div class="dropdown">
829
- <button type="button" class="dropdown-toggle">AI ์„ค์ •</button>
830
- <div class="dropdown-menu">
831
- <a href="{{ url_for('main.admin_settings') }}" class="dropdown-item">AI ์„ค์ •</a>
832
- <a href="{{ url_for('main.admin_prompts') }}" class="dropdown-item">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
833
- </div>
834
- </div>
835
-
836
- {# ์ฑ—๋ด‡ #}
837
- <div class="dropdown">
838
- <button type="button" class="dropdown-toggle">์ฑ—๋ด‡</button>
839
- <div class="dropdown-menu">
840
- <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
841
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
842
- </div>
843
- </div>
844
-
845
- {# ํŽธ์˜๊ธฐ๋Šฅ #}
846
- <div class="dropdown">
847
- <button type="button" class="dropdown-toggle">ํŽธ์˜๊ธฐ๋Šฅ</button>
848
- <div class="dropdown-menu">
849
- <a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์œ ํ‹ธ</a>
850
- </div>
851
- </div>
852
-
853
- {# ๋ฉ”์ธ์œผ๋กœ #}
854
- <a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ”์ธ์œผ๋กœ</a>
855
- <a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋กœ๊ทธ์•„์›ƒ</a>
856
- </div>
857
- </div>
858
-
859
- <!-- ๋ชจ๋ฐ”์ผ ๋ฉ”๋‰ด -->
860
- <div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
861
- <div class="mobile-menu-content" onclick="event.stopPropagation()">
862
- <div class="mobile-menu-header">
863
- <div class="mobile-menu-title">๋ฉ”๋‰ด</div>
864
- <button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ๋‹ซ๊ธฐ">&times;</button>
865
- </div>
866
- <div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
867
- <div class="mobile-menu-items">
868
- <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</div>
869
- <a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
870
- <a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ† ํฐ ํ†ต๊ณ„</a>
871
-
872
- <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>
873
- <a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
874
- <a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
875
-
876
- <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>
877
- <a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์„ค์ •</a>
878
- <a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
879
-
880
- <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>
881
- <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
882
- <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ</a>
883
-
884
- <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>
885
- <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์œ ํ‹ธ</a>
886
-
887
- <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>
888
- <a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์ธ์œผ๋กœ</a>
889
- <a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋กœ๊ทธ์•„์›ƒ</a>
890
- </div>
891
- </div>
892
- </div>
893
 
894
  <div class="container">
895
  <div class="page-header">
 
797
  </style>
798
  </head>
799
  <body>
800
+ {% set admin_nav_title = '์›น์†Œ์„ค ๊ด€๋ฆฌ' %}
801
+ {% set admin_nav_icon = '๐Ÿ“š' %}
802
+ {% include '_admin_nav.html' %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
803
 
804
  <div class="container">
805
  <div class="page-header">