GitHub Actions
commited on
Commit
ยท
8d65b61
1
Parent(s):
e59e120
Auto-deploy from GitHub Actions - 2025-12-12 13:10:39
Browse files- app/routes.py +497 -46
- templates/admin.html +159 -14
- templates/admin_files.html +176 -135
- templates/admin_messages.html +162 -17
- templates/admin_prompts.html +161 -16
- templates/admin_settings.html +161 -18
- templates/admin_tags.html +820 -0
- templates/admin_tokens.html +167 -15
- templates/admin_utils.html +161 -16
- templates/admin_webnovels.html +284 -57
app/routes.py
CHANGED
|
@@ -1991,6 +1991,12 @@ def admin_files():
|
|
| 1991 |
"""ํ์ผ ๋ชฉ๋ก ๊ด๋ฆฌ ํ์ด์ง"""
|
| 1992 |
return render_template('admin_files.html')
|
| 1993 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1994 |
@main_bp.route('/admin/utils')
|
| 1995 |
@admin_required
|
| 1996 |
def admin_utils():
|
|
@@ -4943,10 +4949,10 @@ def process_graph(file_id):
|
|
| 4943 |
except Exception as e:
|
| 4944 |
return jsonify({'error': f'Graph Extraction ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}', 'step': 'graph'}), 500
|
| 4945 |
|
| 4946 |
-
@main_bp.route('/api/files/<int:file_id>/process/tags', methods=['POST'])
|
| 4947 |
@login_required
|
| 4948 |
-
def
|
| 4949 |
-
"""ํ๊ทธ ์์ฑ (Parent Chunk, ํ์ฐจ ๋ถ์, GraphRAG ํ์ฉ) - ๊ณ์ธต์ ๊ตฌ์กฐ"""
|
| 4950 |
try:
|
| 4951 |
file = UploadedFile.query.get_or_404(file_id)
|
| 4952 |
|
|
@@ -4973,7 +4979,7 @@ def generate_tags(file_id):
|
|
| 4973 |
"""
|
| 4974 |
|
| 4975 |
# 2. ํ์ฐจ ๋ถ์ (์์ฝ)
|
| 4976 |
-
analyses = EpisodeAnalysis.query.filter_by(file_id=file_id).order_by(EpisodeAnalysis.id).limit(
|
| 4977 |
analysis_text = ""
|
| 4978 |
if analyses:
|
| 4979 |
analysis_text = "[ํ์ฐจ๋ณ ๋ถ์ (์์ฝ)]\n"
|
|
@@ -4982,40 +4988,107 @@ def generate_tags(file_id):
|
|
| 4982 |
content_preview = analysis.analysis_content[:300] + "..." if len(analysis.analysis_content) > 300 else analysis.analysis_content
|
| 4983 |
analysis_text += f"- {analysis.episode_title}: {content_preview}\n"
|
| 4984 |
|
| 4985 |
-
|
| 4986 |
-
|
| 4987 |
-
|
| 4988 |
-
|
| 4989 |
-
|
| 4990 |
-
|
| 4991 |
-
|
| 4992 |
-
|
| 4993 |
-
|
| 4994 |
-
|
| 4995 |
-
|
| 4996 |
-
if
|
| 4997 |
-
|
| 4998 |
-
|
| 4999 |
-
|
| 5000 |
-
|
| 5001 |
-
|
| 5002 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5003 |
# ๋ฐ์ดํฐ๊ฐ ๋๋ฌด ์์ผ๋ฉด ํ๊ทธ ์์ฑ ๋ถ๊ฐ
|
| 5004 |
-
if not parent_chunk_text and not analysis_text and not
|
| 5005 |
return jsonify({'error': 'ํ๊ทธ๋ฅผ ์์ฑํ ์ถฉ๋ถํ ๋ฐ์ดํฐ(Parent Chunk, ํ์ฐจ ๋ถ์ ๋ฑ)๊ฐ ์์ต๋๋ค.'}), 400
|
| 5006 |
|
| 5007 |
# ํ๋กฌํํธ ๊ตฌ์ฑ
|
| 5008 |
prompt = f"""
|
| 5009 |
-
๋ค์์ ์น์์ค์ ๋ถ์ ๋ฐ์ดํฐ์
๋๋ค. ์ด ๋ฐ์ดํฐ๋ฅผ ๋ฐํ์ผ๋ก ๊ฐ ์น์
๋ณ๋ก
|
| 5010 |
|
| 5011 |
{parent_chunk_text}
|
| 5012 |
|
| 5013 |
{analysis_text}
|
| 5014 |
|
| 5015 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5016 |
|
| 5017 |
[์๊ตฌ์ฌํญ]
|
| 5018 |
-
๋ค์ JSON
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5019 |
|
| 5020 |
{{
|
| 5021 |
"parent_chunk": {{
|
|
@@ -5025,14 +5098,42 @@ def generate_tags(file_id):
|
|
| 5025 |
"others": ["ํ๊ทธ1", "ํ๊ทธ2"]
|
| 5026 |
}},
|
| 5027 |
"episodes": {{
|
| 5028 |
-
"
|
|
|
|
| 5029 |
"characters": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5030 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5031 |
}},
|
| 5032 |
-
"
|
| 5033 |
"characters": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
|
|
|
| 5034 |
"relationships": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5035 |
"events": ["ํ๊ทธ1", "ํ๊ทธ2"]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5036 |
}}
|
| 5037 |
}}
|
| 5038 |
|
|
@@ -5040,7 +5141,7 @@ def generate_tags(file_id):
|
|
| 5040 |
"""
|
| 5041 |
|
| 5042 |
# AI ํธ์ถ
|
| 5043 |
-
print(f"[ํ๊ทธ ์์ฑ] '{file.original_filename}' ํ๊ทธ ์์ฑ ์์ฒญ (๋ชจ๋ธ: {file.model_name})")
|
| 5044 |
|
| 5045 |
response_text = None
|
| 5046 |
|
|
@@ -5056,8 +5157,8 @@ def generate_tags(file_id):
|
|
| 5056 |
result = gemini_client.generate_response(
|
| 5057 |
prompt=prompt,
|
| 5058 |
model_name=gemini_model_name,
|
| 5059 |
-
temperature=0.
|
| 5060 |
-
max_output_tokens=
|
| 5061 |
)
|
| 5062 |
if not result['error'] and result.get('response'):
|
| 5063 |
response_text = result['response'].strip()
|
|
@@ -5072,23 +5173,40 @@ def generate_tags(file_id):
|
|
| 5072 |
from app.core.config import get_config
|
| 5073 |
config = get_config()
|
| 5074 |
|
|
|
|
|
|
|
|
|
|
| 5075 |
ollama_response = requests.post(
|
| 5076 |
f"{config.OLLAMA_BASE_URL}/api/generate",
|
| 5077 |
json={
|
| 5078 |
'model': file.model_name,
|
| 5079 |
'prompt': prompt,
|
| 5080 |
'stream': False,
|
| 5081 |
-
'options': {'temperature': 0.5}
|
| 5082 |
-
'format': 'json'
|
| 5083 |
},
|
| 5084 |
-
timeout=
|
| 5085 |
)
|
| 5086 |
|
|
|
|
|
|
|
| 5087 |
if ollama_response.status_code == 200:
|
| 5088 |
response_data = ollama_response.json()
|
| 5089 |
response_text = response_data.get('response', '').strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5090 |
except Exception as e:
|
| 5091 |
-
print(f"[ํ๊ทธ ์์ฑ] Ollama ์ค๋ฅ: {str(e)}")
|
|
|
|
|
|
|
| 5092 |
|
| 5093 |
if not response_text:
|
| 5094 |
return jsonify({'error': 'AI ์๋ต์ ๋ฐ์ง ๋ชปํ์ต๋๋ค.'}), 500
|
|
@@ -5099,32 +5217,365 @@ def generate_tags(file_id):
|
|
| 5099 |
|
| 5100 |
tags_data = {}
|
| 5101 |
try:
|
|
|
|
| 5102 |
# JSON ์ถ์ถ ์๋
|
| 5103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5104 |
if json_match:
|
| 5105 |
json_str = json_match.group(0)
|
| 5106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5107 |
else:
|
| 5108 |
# JSON ํ์ฑ ์คํจ ์
|
| 5109 |
-
print(f"[ํ๊ทธ ์์ฑ] JSON ํ์์ด ์๋: {response_text[:
|
| 5110 |
-
return jsonify({'error': 'AI ์๋ต์ด ์ฌ๋ฐ๋ฅธ JSON ํ์์ด ์๋๋๋ค.'}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5111 |
except Exception as e:
|
| 5112 |
-
print(f"[ํ๊ทธ ์์ฑ] ํ์ฑ ์ค๋ฅ: {str(e)}")
|
| 5113 |
return jsonify({'error': f'ํ๊ทธ ํ์ฑ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}'}), 500
|
| 5114 |
|
| 5115 |
-
# DB ์ ์ฅ (
|
| 5116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5117 |
db.session.commit()
|
| 5118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5119 |
return jsonify({
|
| 5120 |
-
'message': 'ํ๊ทธ๊ฐ ์์ฑ๋์์ต๋๋ค.',
|
| 5121 |
'tags': tags_data,
|
| 5122 |
-
'file_id': file.id
|
|
|
|
| 5123 |
}), 200
|
| 5124 |
|
| 5125 |
except Exception as e:
|
| 5126 |
db.session.rollback()
|
| 5127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5128 |
|
| 5129 |
@main_bp.route('/api/files/<int:file_id>/metadata', methods=['POST'])
|
| 5130 |
@login_required
|
|
|
|
| 1991 |
"""ํ์ผ ๋ชฉ๋ก ๊ด๋ฆฌ ํ์ด์ง"""
|
| 1992 |
return render_template('admin_files.html')
|
| 1993 |
|
| 1994 |
+
@main_bp.route('/admin/tags')
|
| 1995 |
+
@admin_required
|
| 1996 |
+
def admin_tags():
|
| 1997 |
+
"""ํ๊ทธ ๋ณด๊ธฐ ํ์ด์ง"""
|
| 1998 |
+
return render_template('admin_tags.html')
|
| 1999 |
+
|
| 2000 |
@main_bp.route('/admin/utils')
|
| 2001 |
@admin_required
|
| 2002 |
def admin_utils():
|
|
|
|
| 4949 |
except Exception as e:
|
| 4950 |
return jsonify({'error': f'Graph Extraction ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}', 'step': 'graph'}), 500
|
| 4951 |
|
| 4952 |
+
@main_bp.route('/api/files/<int:file_id>/process/tags/detailed', methods=['POST'])
|
| 4953 |
@login_required
|
| 4954 |
+
def generate_detailed_tags(file_id):
|
| 4955 |
+
"""์์ธ ํ๊ทธ ์์ฑ (Parent Chunk, ํ์ฐจ ๋ถ์, GraphRAG ํ์ฉ) - ๊ณ์ธต์ ๊ตฌ์กฐ"""
|
| 4956 |
try:
|
| 4957 |
file = UploadedFile.query.get_or_404(file_id)
|
| 4958 |
|
|
|
|
| 4979 |
"""
|
| 4980 |
|
| 4981 |
# 2. ํ์ฐจ ๋ถ์ (์์ฝ)
|
| 4982 |
+
analyses = EpisodeAnalysis.query.filter_by(file_id=file_id).order_by(EpisodeAnalysis.id).limit(50).all()
|
| 4983 |
analysis_text = ""
|
| 4984 |
if analyses:
|
| 4985 |
analysis_text = "[ํ์ฐจ๋ณ ๋ถ์ (์์ฝ)]\n"
|
|
|
|
| 4988 |
content_preview = analysis.analysis_content[:300] + "..." if len(analysis.analysis_content) > 300 else analysis.analysis_content
|
| 4989 |
analysis_text += f"- {analysis.episode_title}: {content_preview}\n"
|
| 4990 |
|
| 4991 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] ํ์ฐจ ๋ถ์ ๋ฐ์ดํฐ ๊ธธ์ด: {len(analysis_text)} (ํญ๋ชฉ ์: {len(analyses)})")
|
| 4992 |
+
|
| 4993 |
+
# 3. GraphRAG (์ ์ฒด, ํ์ฐจ๋ณ, ์ธ๋ฌผ๋ณ ๋ถ์์ ์ํ ๋ฐ์ดํฐ ์์ง)
|
| 4994 |
+
all_entities = GraphEntity.query.filter_by(file_id=file_id).all()
|
| 4995 |
+
all_relationships = GraphRelationship.query.filter_by(file_id=file_id).all()
|
| 4996 |
+
all_events = GraphEvent.query.filter_by(file_id=file_id).all()
|
| 4997 |
+
|
| 4998 |
+
# 3-1. ์ ์ฒด ์์ฝ
|
| 4999 |
+
total_graph_summary = ""
|
| 5000 |
+
if all_entities or all_relationships or all_events:
|
| 5001 |
+
total_graph_summary = "[GraphRAG ๋ฐ์ดํฐ (์ ์ฒด)]\n"
|
| 5002 |
+
if all_entities:
|
| 5003 |
+
unique_entities = sorted(list(set([f"{e.entity_name}({e.entity_type})" for e in all_entities])))
|
| 5004 |
+
# ๋๋ฌด ๋ง์ผ๋ฉด ์ผ๋ถ๋ง
|
| 5005 |
+
total_graph_summary += f"์ฃผ์ ์ํฐํฐ({len(unique_entities)}๊ฐ): {', '.join(unique_entities[:50])}\n"
|
| 5006 |
+
if all_relationships:
|
| 5007 |
+
unique_rels = sorted(list(set([f"{r.source}->{r.target}({r.relationship_type})" for r in all_relationships])))
|
| 5008 |
+
total_graph_summary += f"์ฃผ์ ๊ด๊ณ({len(unique_rels)}๊ฐ): {', '.join(unique_rels[:50])}\n"
|
| 5009 |
+
if all_events:
|
| 5010 |
+
unique_events = sorted(list(set([e.event_name for e in all_events])))
|
| 5011 |
+
total_graph_summary += f"์ฃผ์ ์ฌ๊ฑด({len(unique_events)}๊ฐ): {', '.join(unique_events[:30])}\n"
|
| 5012 |
+
|
| 5013 |
+
# 3-2. ํ์ฐจ๋ณ ์์ฝ (์๋ถ๋ถ 5๊ฐ ํ์ฐจ๋ง ์ํ๋ง)
|
| 5014 |
+
episode_graph_summary = ""
|
| 5015 |
+
episode_titles = sorted(list(set([e.episode_title for e in all_events] + [e.episode_title for e in all_entities])))
|
| 5016 |
+
if episode_titles:
|
| 5017 |
+
episode_graph_summary = "[GraphRAG ๋ฐ์ดํฐ (ํ์ฐจ๋ณ ์ํ)]\n"
|
| 5018 |
+
for title in episode_titles[:5]:
|
| 5019 |
+
ep_entities = [e.entity_name for e in all_entities if e.episode_title == title]
|
| 5020 |
+
ep_events = [e.event_name for e in all_events if e.episode_title == title]
|
| 5021 |
+
episode_graph_summary += f"- {title}: ์ฃผ์ ์ํฐํฐ({', '.join(ep_entities[:5])}), ์ฃผ์ ์ฌ๊ฑด({', '.join(ep_events[:3])})\n"
|
| 5022 |
+
|
| 5023 |
+
# 3-3. ์ธ๋ฌผ๋ณ ์์ฝ (์ฃผ์ ์ธ๋ฌผ 5๋ช
)
|
| 5024 |
+
character_graph_summary = ""
|
| 5025 |
+
from collections import Counter
|
| 5026 |
+
# entity_type์ด 'character' ๋๋ '์ธ๋ฌผ'์ธ ๊ฒ๋ง ์นด์ดํธ
|
| 5027 |
+
char_counts = Counter([e.entity_name for e in all_entities if e.entity_type in ['character', '์ธ๋ฌผ']])
|
| 5028 |
+
top_characters = [char for char, count in char_counts.most_common(5)]
|
| 5029 |
+
|
| 5030 |
+
if top_characters:
|
| 5031 |
+
character_graph_summary = "[GraphRAG ๋ฐ์ดํฐ (์ฃผ์ ์ธ๋ฌผ๋ณ)]\n"
|
| 5032 |
+
for char in top_characters:
|
| 5033 |
+
# ํด๋น ์ธ๋ฌผ์ด ํฌํจ๋ ๊ด๊ณ๋ ์ฌ๊ฑด
|
| 5034 |
+
char_rels = [f"{r.target}({r.relationship_type})" for r in all_relationships if r.source == char]
|
| 5035 |
+
char_events = [e.event_name for e in all_events if char in (e.participants or "")]
|
| 5036 |
+
character_graph_summary += f"- {char}: ์ฃผ์ ๊ด๊ณ({', '.join(char_rels[:5])}), ๊ด๋ จ ์ฌ๊ฑด({', '.join(char_events[:3])})\n"
|
| 5037 |
+
|
| 5038 |
+
# 3-4. ์์ธ ๊ด๊ณ/์ฌ๊ฑด ์์ฝ
|
| 5039 |
+
detail_graph_summary = ""
|
| 5040 |
+
# ์ธ๋ฌผ-์ธ๋ฌผ ๊ด๊ณ (์๊ฐ์/ID์)
|
| 5041 |
+
# ID๊ฐ ์์ฑ ์์์ด๋ฏ๋ก ์๊ฐ ํ๋ฆ์ ์ด๋ ์ ๋ ๋ฐ์ํจ
|
| 5042 |
+
sorted_relationships = sorted(all_relationships, key=lambda r: r.id)
|
| 5043 |
+
if sorted_relationships:
|
| 5044 |
+
detail_graph_summary += "[GraphRAG (์ธ๋ฌผ-์ธ๋ฌผ ๊ด๊ณ ์๊ฐ์ - ์ ์ฒด)]\n"
|
| 5045 |
+
# ๋ชจ๋ ๊ด๊ณ ํฌํจ (ํ ํฐ ์ ํ ๋ด์์)
|
| 5046 |
+
for rel in sorted_relationships:
|
| 5047 |
+
detail_graph_summary += f"- {rel.source} -> {rel.target}: {rel.relationship_type}\n"
|
| 5048 |
+
|
| 5049 |
+
# ์ฌ๊ฑด-์ธ๋ฌผ๋ค ๊ด๊ณ
|
| 5050 |
+
if all_events:
|
| 5051 |
+
detail_graph_summary += "\n[GraphRAG (์ฌ๊ฑด-์ธ๋ฌผ๋ค ๊ด๊ณ - ์ ์ฒด)]\n"
|
| 5052 |
+
# ๋ชจ๋ ์ฌ๊ฑด ํฌํจ
|
| 5053 |
+
for evt in all_events:
|
| 5054 |
+
participants_str = evt.participants if evt.participants else "์ ๋ณด ์์"
|
| 5055 |
+
# JSON ๋ฌธ์์ด์ธ ๊ฒฝ์ฐ ํ์ฑ ์๋
|
| 5056 |
+
if participants_str.startswith('['):
|
| 5057 |
+
try:
|
| 5058 |
+
import json
|
| 5059 |
+
p_list = json.loads(participants_str)
|
| 5060 |
+
if isinstance(p_list, list):
|
| 5061 |
+
participants_str = ", ".join(p_list)
|
| 5062 |
+
except:
|
| 5063 |
+
pass
|
| 5064 |
+
detail_graph_summary += f"- ์ฌ๊ฑด '{evt.event_name}': ์ฐธ์ฌ์ [{participants_str}]\n"
|
| 5065 |
+
|
| 5066 |
# ๋ฐ์ดํฐ๊ฐ ๋๋ฌด ์์ผ๋ฉด ํ๊ทธ ์์ฑ ๋ถ๊ฐ
|
| 5067 |
+
if not parent_chunk_text and not analysis_text and not total_graph_summary:
|
| 5068 |
return jsonify({'error': 'ํ๊ทธ๋ฅผ ์์ฑํ ์ถฉ๋ถํ ๋ฐ์ดํฐ(Parent Chunk, ํ์ฐจ ๋ถ์ ๋ฑ)๊ฐ ์์ต๋๋ค.'}), 400
|
| 5069 |
|
| 5070 |
# ํ๋กฌํํธ ๊ตฌ์ฑ
|
| 5071 |
prompt = f"""
|
| 5072 |
+
๋ค์์ ์น์์ค์ ๋ถ์ ๋ฐ์ดํฐ์
๋๋ค. ์ด ๋ฐ์ดํฐ๋ฅผ ๋ฐํ์ผ๋ก ๊ฐ ์น์
๋ณ๋ก ๋งค์ฐ ์์ธํ๊ณ ๊ตฌ์ฒด์ ์ธ ํ๊ทธ๋ฅผ ์ถ์ถํด์ฃผ์ธ์.
|
| 5073 |
|
| 5074 |
{parent_chunk_text}
|
| 5075 |
|
| 5076 |
{analysis_text}
|
| 5077 |
|
| 5078 |
+
{total_graph_summary}
|
| 5079 |
+
|
| 5080 |
+
{episode_graph_summary}
|
| 5081 |
+
|
| 5082 |
+
{character_graph_summary}
|
| 5083 |
+
|
| 5084 |
+
{detail_graph_summary}
|
| 5085 |
|
| 5086 |
[์๊ตฌ์ฌํญ]
|
| 5087 |
+
๋ค์ JSON ๊ตฌ์กฐ์ ๋ง์ถฐ ํ๊ทธ๋ฅผ ์์ฑํ์ธ์.
|
| 5088 |
+
๊ฐ ํญ๋ชฉ๋น 10~20๊ฐ์ ํ๊ทธ๋ฅผ ์์ฑํ๋ฉฐ, ๋จ์ํ ๋จ์ด๋ณด๋ค๋ ๊ตฌ์ฒด์ ์ธ ๋ด์ฉ์ ๋ด์ '๊ตฌ' ํํ์ ํ๊ทธ๋ฅผ ์ ํธํฉ๋๋ค.
|
| 5089 |
+
(์: "๋ณต์" -> "๊ฐ๋ฌธ ๋ฉธ๋ง์ ๋ํ ์ฒ์ ํ ๋ณต์", "์ฑ์ฅ" -> "๋ง๋ฒ์ ์ฌ๋ฅ์ ๊ธ๊ฒฉํ ๊ฐํ", "๊ฐ๋ฑ" -> "ํฉ์ ๊ณ์น๊ถ์ ๋๋ฌ์ผ ํ์ ๊ฐ์ ์ํฌ")
|
| 5090 |
+
|
| 5091 |
+
ํนํ 'graph_rag_detail' ํญ๋ชฉ์ ์
๋ ฅ๋ ๋ชจ๋ ๊ด๊ณ์ ์ฌ๊ฑด ๋ฐ์ดํฐ๋ฅผ ๋น ์ง์์ด ๋ถ์ํ์ฌ, ๊ฐ๋ฅํ ํ ๋ชจ๋ ์ธ๋ฌผ ๊ด๊ณ ๋ณํ์ ์ฌ๊ฑด ์ ์ธ๋ฌผ๋ค์ ์ญํ ์ ์์ธํ ๋ฌ์ฌํ๋ ํ๊ทธ๋ฅผ ์์ฑํ์ธ์. ํ๊ทธ ๊ฐ์ ์ ํ์ ๋์ง ๋ง๊ณ ํ๋ถํ๊ฒ ์์ฑํ์ธ์.
|
| 5092 |
|
| 5093 |
{{
|
| 5094 |
"parent_chunk": {{
|
|
|
|
| 5098 |
"others": ["ํ๊ทธ1", "ํ๊ทธ2"]
|
| 5099 |
}},
|
| 5100 |
"episodes": {{
|
| 5101 |
+
"story": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5102 |
+
"events": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5103 |
"characters": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5104 |
+
"relationships_change": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5105 |
+
"appearance": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5106 |
+
"clothing": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5107 |
+
"items": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5108 |
+
"others": ["ํ๊ทธ1", "ํ๊ทธ2"]
|
| 5109 |
+
}},
|
| 5110 |
+
"graph_rag_total": {{
|
| 5111 |
+
"characters": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5112 |
+
"locations": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5113 |
+
"relationships": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5114 |
+
"events": ["ํ๊ทธ1", "ํ๊ทธ2"]
|
| 5115 |
+
}},
|
| 5116 |
+
"graph_rag_by_episode": {{
|
| 5117 |
+
"characters": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5118 |
+
"locations": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5119 |
+
"relationships": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5120 |
+
"events": ["ํ๊ทธ1", "ํ๊ทธ2"]
|
| 5121 |
+
}},
|
| 5122 |
+
"graph_rag_by_character": {{
|
| 5123 |
+
"characters": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5124 |
+
"locations": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5125 |
+
"relationships": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5126 |
+
"events": ["ํ๊ทธ1", "ํ๊ทธ2"]
|
| 5127 |
}},
|
| 5128 |
+
"graph_rag_by_event": {{
|
| 5129 |
"characters": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5130 |
+
"locations": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5131 |
"relationships": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5132 |
"events": ["ํ๊ทธ1", "ํ๊ทธ2"]
|
| 5133 |
+
}},
|
| 5134 |
+
"graph_rag_detail": {{
|
| 5135 |
+
"person_person_relationships_chronological": ["๋ชจ๋ ์ฃผ์ ์ธ๋ฌผ ๊ด๊ณ์ ์๊ฐ์ ๋ณํ๋ฅผ ์์ธํ ๋ฌ์ฌ"],
|
| 5136 |
+
"event_person_relationships": ["๋ชจ๋ ์ฌ๊ฑด๋ณ ์ฃผ์ ์ธ๋ฌผ์ ์ญํ ๊ณผ ๊ด๊ณ๋ฅผ ์์ธํ ๋ฌ์ฌ"]
|
| 5137 |
}}
|
| 5138 |
}}
|
| 5139 |
|
|
|
|
| 5141 |
"""
|
| 5142 |
|
| 5143 |
# AI ํธ์ถ
|
| 5144 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] '{file.original_filename}' ์์ธ ํ๊ทธ ์์ฑ ์์ฒญ (๋ชจ๋ธ: {file.model_name})")
|
| 5145 |
|
| 5146 |
response_text = None
|
| 5147 |
|
|
|
|
| 5157 |
result = gemini_client.generate_response(
|
| 5158 |
prompt=prompt,
|
| 5159 |
model_name=gemini_model_name,
|
| 5160 |
+
temperature=0.6,
|
| 5161 |
+
max_output_tokens=4096
|
| 5162 |
)
|
| 5163 |
if not result['error'] and result.get('response'):
|
| 5164 |
response_text = result['response'].strip()
|
|
|
|
| 5173 |
from app.core.config import get_config
|
| 5174 |
config = get_config()
|
| 5175 |
|
| 5176 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] Ollama API ํธ์ถ ์์: {config.OLLAMA_BASE_URL}, ๋ชจ๋ธ: {file.model_name}")
|
| 5177 |
+
|
| 5178 |
+
# JSON ํ์ ์ง์ ์ฌ๋ถ์ ๊ด๊ณ์์ด ๋จผ์ ์๋ (์ผ๋ถ ๋ชจ๋ธ์ JSON ํ์์ ์ง์ํ์ง ์์)
|
| 5179 |
ollama_response = requests.post(
|
| 5180 |
f"{config.OLLAMA_BASE_URL}/api/generate",
|
| 5181 |
json={
|
| 5182 |
'model': file.model_name,
|
| 5183 |
'prompt': prompt,
|
| 5184 |
'stream': False,
|
| 5185 |
+
'options': {'temperature': 0.5, 'num_predict': 4096}
|
| 5186 |
+
# 'format': 'json' ์ ๊ฑฐ - ๋ชจ๋ ๋ชจ๋ธ์ด ์ง์ํ์ง ์์ ์ ์์
|
| 5187 |
},
|
| 5188 |
+
timeout=300 # ํ์์์์ 5๋ถ์ผ๋ก ์ฆ๊ฐ
|
| 5189 |
)
|
| 5190 |
|
| 5191 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] Ollama ์๋ต ์ํ: {ollama_response.status_code}")
|
| 5192 |
+
|
| 5193 |
if ollama_response.status_code == 200:
|
| 5194 |
response_data = ollama_response.json()
|
| 5195 |
response_text = response_data.get('response', '').strip()
|
| 5196 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] Ollama ์๋ต ์์ ์๋ฃ, ๊ธธ์ด: {len(response_text)}")
|
| 5197 |
+
else:
|
| 5198 |
+
error_text = ollama_response.text[:500] if ollama_response.text else "์๋ต ์์"
|
| 5199 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] Ollama API ์ค๋ฅ ({ollama_response.status_code}): {error_text}")
|
| 5200 |
+
except requests.exceptions.Timeout:
|
| 5201 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] Ollama ์์ฒญ ํ์์์ (300์ด ์ด๊ณผ)")
|
| 5202 |
+
return jsonify({'error': 'AI ์๋ต ์์ฑ ์๊ฐ์ด ์ด๊ณผ๋์์ต๋๋ค. ๋ชจ๋ธ์ด ๋๋ฌด ๋๋ฆฌ๊ฑฐ๋ ํ๋กฌํํธ๊ฐ ๋๋ฌด ํฝ๋๋ค.'}), 500
|
| 5203 |
+
except requests.exceptions.ConnectionError:
|
| 5204 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] Ollama ์๋ฒ ์ฐ๊ฒฐ ์คํจ: {config.OLLAMA_BASE_URL}")
|
| 5205 |
+
return jsonify({'error': f'Ollama ์๋ฒ์ ์ฐ๊ฒฐํ ์ ์์ต๋๋ค. ์๋ฒ๊ฐ ์คํ ์ค์ธ์ง ํ์ธํ์ธ์. ({config.OLLAMA_BASE_URL})'}), 500
|
| 5206 |
except Exception as e:
|
| 5207 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] Ollama ์ค๋ฅ: {str(e)}")
|
| 5208 |
+
import traceback
|
| 5209 |
+
traceback.print_exc()
|
| 5210 |
|
| 5211 |
if not response_text:
|
| 5212 |
return jsonify({'error': 'AI ์๋ต์ ๋ฐ์ง ๋ชปํ์ต๋๋ค.'}), 500
|
|
|
|
| 5217 |
|
| 5218 |
tags_data = {}
|
| 5219 |
try:
|
| 5220 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] AI ์๋ต ๊ธธ์ด: {len(response_text)}")
|
| 5221 |
# JSON ์ถ์ถ ์๋
|
| 5222 |
+
# Markdown ์ฝ๋ ๋ธ๋ก ์ ๊ฑฐ
|
| 5223 |
+
cleaned_text = re.sub(r'```json\s*', '', response_text)
|
| 5224 |
+
cleaned_text = re.sub(r'```\s*', '', cleaned_text)
|
| 5225 |
+
cleaned_text = cleaned_text.strip()
|
| 5226 |
+
|
| 5227 |
+
json_match = re.search(r'\{.*\}', cleaned_text, re.DOTALL)
|
| 5228 |
if json_match:
|
| 5229 |
json_str = json_match.group(0)
|
| 5230 |
+
try:
|
| 5231 |
+
tags_data = json.loads(json_str)
|
| 5232 |
+
except json.JSONDecodeError as je:
|
| 5233 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] JSON ํ์ฑ ์คํจ: {je}")
|
| 5234 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] ํ์ฑ ์คํจํ ๋ฌธ์์ด ์ผ๋ถ: {json_str[:200]}...")
|
| 5235 |
+
# ์ผํ ๋ฌธ์ ๋ฑ ๊ฐ๋จํ ๋ณต๊ตฌ ์๋
|
| 5236 |
+
try:
|
| 5237 |
+
# Trailing comma ์ ๊ฑฐ
|
| 5238 |
+
json_str_fixed = re.sub(r',\s*([\]}])', r'\1', json_str)
|
| 5239 |
+
tags_data = json.loads(json_str_fixed)
|
| 5240 |
+
print("[์์ธ ํ๊ทธ ์์ฑ] JSON ๋ณต๊ตฌ ์ฑ๊ณต")
|
| 5241 |
+
except:
|
| 5242 |
+
# ๋ณต๊ตฌ ์คํจ ์
|
| 5243 |
+
raise je
|
| 5244 |
+
|
| 5245 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] ์์ฑ๋ ํ๊ทธ ๊ตฌ์กฐ: {list(tags_data.keys())}")
|
| 5246 |
+
if 'episodes' in tags_data:
|
| 5247 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] episodes ํ๊ทธ ์ํ: {str(tags_data['episodes'])[:100]}...")
|
| 5248 |
else:
|
| 5249 |
# JSON ํ์ฑ ์คํจ ์
|
| 5250 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] JSON ํ์์ด ์๋: {response_text[:200]}...")
|
| 5251 |
+
return jsonify({'error': 'AI ์๋ต์ด ์ฌ๋ฐ๋ฅธ JSON ํ์์ด ์๋๋๋ค. (๋ก๊ทธ ํ์ธ ํ์)'}), 500
|
| 5252 |
+
except Exception as e:
|
| 5253 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] ํ์ฑ ์ค๋ฅ: {str(e)}")
|
| 5254 |
+
# ์ตํ์ ์๋จ: ํ
์คํธ ํ์ผ๋ก๋ผ๋ ์ ์ฅํ ์ง ๊ณ ๋ฏผํด๋ด์ผ ํจ.
|
| 5255 |
+
return jsonify({'error': f'ํ๊ทธ ํ์ฑ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}'}), 500
|
| 5256 |
+
|
| 5257 |
+
# DB ์ ์ฅ (๊ธฐ์กด ํ๊ทธ๊ฐ ์์ผ๋ฉด ๋ณํฉ, type ํ๋ ์ถ๊ฐ)
|
| 5258 |
+
import json
|
| 5259 |
+
existing_tags = None
|
| 5260 |
+
if file.tags:
|
| 5261 |
+
try:
|
| 5262 |
+
existing_tags = json.loads(file.tags)
|
| 5263 |
+
except:
|
| 5264 |
+
pass
|
| 5265 |
+
|
| 5266 |
+
tags_data_with_type = {
|
| 5267 |
+
'type': 'detailed',
|
| 5268 |
+
'tags': tags_data
|
| 5269 |
+
}
|
| 5270 |
+
|
| 5271 |
+
# ๊ธฐ์กด ์ผ๋ฐ ํ๊ทธ๊ฐ ์์ผ๋ฉด ๋ณํฉ
|
| 5272 |
+
if existing_tags and existing_tags.get('type') == 'simple':
|
| 5273 |
+
tags_data_with_type['simple_tags'] = existing_tags.get('tags', {})
|
| 5274 |
+
|
| 5275 |
+
tags_json_str = json.dumps(tags_data_with_type, ensure_ascii=False)
|
| 5276 |
+
file.tags = tags_json_str
|
| 5277 |
+
db.session.commit()
|
| 5278 |
+
|
| 5279 |
+
# ์ ์ฅ ํ์ธ
|
| 5280 |
+
db.session.refresh(file)
|
| 5281 |
+
if file.tags:
|
| 5282 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] DB ์ ์ฅ ์ฑ๊ณต - ํ์ผ ID: {file.id}, ํ๊ทธ ๊ธธ์ด: {len(file.tags)}")
|
| 5283 |
+
else:
|
| 5284 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] ๊ฒฝ๊ณ : DB ์ ์ฅ ํ ํ๊ทธ๊ฐ None์
๋๋ค!")
|
| 5285 |
+
|
| 5286 |
+
return jsonify({
|
| 5287 |
+
'message': '์์ธ ํ๊ทธ๊ฐ ์์ฑ๋์์ต๋๋ค.',
|
| 5288 |
+
'tags': tags_data,
|
| 5289 |
+
'file_id': file.id,
|
| 5290 |
+
'type': 'detailed'
|
| 5291 |
+
}), 200
|
| 5292 |
+
|
| 5293 |
+
except Exception as e:
|
| 5294 |
+
db.session.rollback()
|
| 5295 |
+
error_msg = str(e)
|
| 5296 |
+
print(f"[์์ธ ํ๊ทธ ์์ฑ] ์์ธ ๋ฐ์: {error_msg}")
|
| 5297 |
+
import traceback
|
| 5298 |
+
traceback.print_exc()
|
| 5299 |
+
return jsonify({'error': f'์์ธ ํ๊ทธ ์์ฑ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {error_msg}'}), 500
|
| 5300 |
+
|
| 5301 |
+
@main_bp.route('/api/files/<int:file_id>/process/tags/simple', methods=['POST'])
|
| 5302 |
+
@login_required
|
| 5303 |
+
def generate_simple_tags(file_id):
|
| 5304 |
+
"""์ผ๋ฐ ํ๊ทธ ์์ฑ (๊ฐ๋จํ ํค์๋ ํํ)"""
|
| 5305 |
+
try:
|
| 5306 |
+
file = UploadedFile.query.get_or_404(file_id)
|
| 5307 |
+
|
| 5308 |
+
# ๊ถํ ํ์ธ
|
| 5309 |
+
if not current_user.is_admin and file.uploaded_by != current_user.id:
|
| 5310 |
+
return jsonify({'error': '๊ถํ์ด ์์ต๋๋ค.'}), 403
|
| 5311 |
+
|
| 5312 |
+
# ๋ชจ๋ธ ํ์ธ
|
| 5313 |
+
if not file.model_name:
|
| 5314 |
+
return jsonify({'error': 'ํ์ผ์ ์ฐ๊ฒฐ๋ AI ๋ชจ๋ธ์ด ์์ต๋๋ค.'}), 400
|
| 5315 |
+
|
| 5316 |
+
# ๋ฐ์ดํฐ ์์ง (์์ธ ํ๊ทธ์ ๋์ผ)
|
| 5317 |
+
parent_chunk = ParentChunk.query.filter_by(file_id=file_id).first()
|
| 5318 |
+
analyses = EpisodeAnalysis.query.filter_by(file_id=file_id).order_by(EpisodeAnalysis.id).limit(50).all()
|
| 5319 |
+
all_entities = GraphEntity.query.filter_by(file_id=file_id).all()
|
| 5320 |
+
all_relationships = GraphRelationship.query.filter_by(file_id=file_id).all()
|
| 5321 |
+
all_events = GraphEvent.query.filter_by(file_id=file_id).all()
|
| 5322 |
+
|
| 5323 |
+
# ๋ฐ์ดํฐ๊ฐ ๋๋ฌด ์์ผ๋ฉด ํ๊ทธ ์์ฑ ๋ถ๊ฐ
|
| 5324 |
+
if not parent_chunk and not analyses and not all_entities:
|
| 5325 |
+
return jsonify({'error': 'ํ๊ทธ๋ฅผ ์์ฑํ ์ถฉ๋ถํ ๋ฐ์ดํฐ(Parent Chunk, ํ์ฐจ ๋ถ์ ๋ฑ)๊ฐ ์์ต๋๋ค.'}), 400
|
| 5326 |
+
|
| 5327 |
+
# ํ๋กฌํํธ ๊ตฌ์ฑ (๊ฐ๋จํ ํค์๋ ํํ)
|
| 5328 |
+
prompt_parts = []
|
| 5329 |
+
|
| 5330 |
+
if parent_chunk:
|
| 5331 |
+
prompt_parts.append(f"[Parent Chunk]\n์ธ๊ณ๊ด: {parent_chunk.world_view or '์์'}\n์ฃผ์ ์บ๋ฆญํฐ: {parent_chunk.characters or '์์'}\n์ด์ผ๊ธฐ: {parent_chunk.story or '์์'}\n๊ธฐํ: {parent_chunk.others or '์์'}")
|
| 5332 |
+
|
| 5333 |
+
if analyses:
|
| 5334 |
+
prompt_parts.append("[ํ์ฐจ๋ณ ๋ถ์]\n" + "\n".join([f"- {a.episode_title}: {a.analysis_content[:200]}..." for a in analyses[:10]]))
|
| 5335 |
+
|
| 5336 |
+
if all_entities or all_relationships or all_events:
|
| 5337 |
+
graph_parts = []
|
| 5338 |
+
if all_entities:
|
| 5339 |
+
entities_list = sorted(list(set([e.entity_name for e in all_entities])))
|
| 5340 |
+
graph_parts.append(f"์ธ๋ฌผ/์ฅ์: {', '.join(entities_list[:30])}")
|
| 5341 |
+
if all_relationships:
|
| 5342 |
+
rels_list = [f"{r.source}-{r.target}({r.relationship_type})" for r in all_relationships[:20]]
|
| 5343 |
+
graph_parts.append(f"๊ด๊ณ: {', '.join(rels_list)}")
|
| 5344 |
+
if all_events:
|
| 5345 |
+
events_list = [e.event_name for e in all_events[:20]]
|
| 5346 |
+
graph_parts.append(f"์ฌ๊ฑด: {', '.join(events_list)}")
|
| 5347 |
+
if graph_parts:
|
| 5348 |
+
prompt_parts.append("[GraphRAG]\n" + "\n".join(graph_parts))
|
| 5349 |
+
|
| 5350 |
+
# GraphRAG ์์ธ ๋ฐ์ดํฐ ์ถ๊ฐ (์ผ๋ฐ ํ๊ทธ์๋ ํฌํจ)
|
| 5351 |
+
detail_graph_summary = ""
|
| 5352 |
+
if all_relationships:
|
| 5353 |
+
sorted_relationships = sorted(all_relationships, key=lambda r: r.id)
|
| 5354 |
+
detail_graph_summary += "[GraphRAG (์ธ๋ฌผ-์ธ๋ฌผ ๊ด๊ณ ์๊ฐ์)]\n"
|
| 5355 |
+
for rel in sorted_relationships:
|
| 5356 |
+
detail_graph_summary += f"- {rel.source} -> {rel.target}: {rel.relationship_type}\n"
|
| 5357 |
+
|
| 5358 |
+
if all_events:
|
| 5359 |
+
detail_graph_summary += "\n[GraphRAG (์ฌ๊ฑด-์ธ๋ฌผ๋ค ๊ด๊ณ)]\n"
|
| 5360 |
+
for evt in all_events:
|
| 5361 |
+
participants_str = evt.participants if evt.participants else "์ ๋ณด ์์"
|
| 5362 |
+
if participants_str.startswith('['):
|
| 5363 |
+
try:
|
| 5364 |
+
import json
|
| 5365 |
+
p_list = json.loads(participants_str)
|
| 5366 |
+
if isinstance(p_list, list):
|
| 5367 |
+
participants_str = ", ".join(p_list)
|
| 5368 |
+
except:
|
| 5369 |
+
pass
|
| 5370 |
+
detail_graph_summary += f"- ์ฌ๊ฑด '{evt.event_name}': ์ฐธ์ฌ์ [{participants_str}]\n"
|
| 5371 |
+
|
| 5372 |
+
if detail_graph_summary:
|
| 5373 |
+
prompt_parts.append(detail_graph_summary)
|
| 5374 |
+
|
| 5375 |
+
prompt = "\n\n".join(prompt_parts) + """
|
| 5376 |
+
|
| 5377 |
+
[์๊ตฌ์ฌํญ]
|
| 5378 |
+
์ ๋ฐ์ดํฐ๋ฅผ ๋ฐํ์ผ๋ก ๊ฐ ์น์
๋ณ๋ก ๊ฐ๋จํ ํค์๋ ํ๊ทธ๋ฅผ ์์ฑํ์ธ์. ๊ฐ ํญ๋ชฉ๋น 5~10๊ฐ์ ๊ฐ๋จํ ํ๊ทธ๋ฅผ ์์ฑํ๋ฉฐ, ๋จ์ด๋ ์งง์ ๊ตฌ ํํ๋ก ์์ฑํ์ธ์.
|
| 5379 |
+
|
| 5380 |
+
ํนํ 'graph_rag_detail' ํญ๋ชฉ์ ๊ด๊ณ ํ๊ทธ๋ "์ธ๋ฌผA โ ์ธ๋ฌผB: ๊ด๊ณ์ ํ" ํ์์ผ๋ก ์์ฑํ๊ฑฐ๋, "์ธ๋ฌผA์ ์ธ๋ฌผB์ ๊ด๊ณ์ ํ" ๊ฐ์ ์ดํดํ๊ธฐ ์ฌ์ด ํ์์ผ๋ก ์์ฑํ์ธ์.
|
| 5381 |
+
|
| 5382 |
+
๋ค์ JSON ๊ตฌ์กฐ์ ๋ง์ถฐ ํ๊ทธ๋ฅผ ์์ฑํ์ธ์:
|
| 5383 |
+
|
| 5384 |
+
{
|
| 5385 |
+
"parent_chunk": {
|
| 5386 |
+
"world_view": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5387 |
+
"characters": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5388 |
+
"story": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5389 |
+
"others": ["ํ๊ทธ1", "ํ๊ทธ2"]
|
| 5390 |
+
},
|
| 5391 |
+
"episodes": {
|
| 5392 |
+
"story": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5393 |
+
"events": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5394 |
+
"characters": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5395 |
+
"relationships_change": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5396 |
+
"appearance": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5397 |
+
"clothing": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5398 |
+
"items": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5399 |
+
"others": ["ํ๊ทธ1", "ํ๊ทธ2"]
|
| 5400 |
+
},
|
| 5401 |
+
"graph_rag_total": {
|
| 5402 |
+
"characters": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5403 |
+
"locations": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5404 |
+
"relationships": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5405 |
+
"events": ["ํ๊ทธ1", "ํ๊ทธ2"]
|
| 5406 |
+
},
|
| 5407 |
+
"graph_rag_by_episode": {
|
| 5408 |
+
"characters": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5409 |
+
"locations": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5410 |
+
"relationships": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5411 |
+
"events": ["ํ๊ทธ1", "ํ๊ทธ2"]
|
| 5412 |
+
},
|
| 5413 |
+
"graph_rag_by_character": {
|
| 5414 |
+
"characters": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5415 |
+
"locations": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5416 |
+
"relationships": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5417 |
+
"events": ["ํ๊ทธ1", "ํ๊ทธ2"]
|
| 5418 |
+
},
|
| 5419 |
+
"graph_rag_by_event": {
|
| 5420 |
+
"characters": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5421 |
+
"locations": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5422 |
+
"relationships": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5423 |
+
"events": ["ํ๊ทธ1", "ํ๊ทธ2"]
|
| 5424 |
+
},
|
| 5425 |
+
"graph_rag_detail": {
|
| 5426 |
+
"person_person_relationships_chronological": ["ํ๊ทธ1", "ํ๊ทธ2"],
|
| 5427 |
+
"event_person_relationships": ["ํ๊ทธ1", "ํ๊ทธ2"]
|
| 5428 |
+
}
|
| 5429 |
+
}
|
| 5430 |
+
|
| 5431 |
+
๋ฐ๋์ JSON ํ์์ผ๋ก๋ง ์๋ตํ์ธ์. ๋ค๋ฅธ ์ค๋ช
์ ํฌํจํ์ง ๋ง์ธ์.
|
| 5432 |
+
"""
|
| 5433 |
+
|
| 5434 |
+
# AI ํธ์ถ
|
| 5435 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] '{file.original_filename}' ์ผ๋ฐ ํ๊ทธ ์์ฑ ์์ฒญ (๋ชจ๋ธ: {file.model_name})")
|
| 5436 |
+
|
| 5437 |
+
response_text = None
|
| 5438 |
+
|
| 5439 |
+
# Gemini ๋ชจ๋ธ์ธ ๊ฒฝ์ฐ
|
| 5440 |
+
if 'gemini' in file.model_name.lower():
|
| 5441 |
+
try:
|
| 5442 |
+
gemini_model_name = file.model_name
|
| 5443 |
+
if gemini_model_name.lower().startswith('gemini:'):
|
| 5444 |
+
gemini_model_name = gemini_model_name.split(':', 1)[1].strip()
|
| 5445 |
+
|
| 5446 |
+
gemini_client = get_gemini_client()
|
| 5447 |
+
if gemini_client.is_configured():
|
| 5448 |
+
result = gemini_client.generate_response(
|
| 5449 |
+
prompt=prompt,
|
| 5450 |
+
model_name=gemini_model_name,
|
| 5451 |
+
temperature=0.4,
|
| 5452 |
+
max_output_tokens=2048
|
| 5453 |
+
)
|
| 5454 |
+
if not result['error'] and result.get('response'):
|
| 5455 |
+
response_text = result['response'].strip()
|
| 5456 |
+
except Exception as e:
|
| 5457 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] Gemini ์ค๋ฅ: {str(e)}")
|
| 5458 |
+
|
| 5459 |
+
# Ollama ๋ชจ๋ธ์ธ ๊ฒฝ์ฐ (๋๋ Gemini ์คํจ ์)
|
| 5460 |
+
if not response_text:
|
| 5461 |
+
try:
|
| 5462 |
+
import requests
|
| 5463 |
+
from app.core.config import get_config
|
| 5464 |
+
config = get_config()
|
| 5465 |
+
|
| 5466 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] Ollama API ํธ์ถ ์์: {config.OLLAMA_BASE_URL}, ๋ชจ๋ธ: {file.model_name}")
|
| 5467 |
+
|
| 5468 |
+
ollama_response = requests.post(
|
| 5469 |
+
f"{config.OLLAMA_BASE_URL}/api/generate",
|
| 5470 |
+
json={
|
| 5471 |
+
'model': file.model_name,
|
| 5472 |
+
'prompt': prompt,
|
| 5473 |
+
'stream': False,
|
| 5474 |
+
'options': {'temperature': 0.4, 'num_predict': 2048}
|
| 5475 |
+
},
|
| 5476 |
+
timeout=180
|
| 5477 |
+
)
|
| 5478 |
+
|
| 5479 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] Ollama ์๋ต ์ํ: {ollama_response.status_code}")
|
| 5480 |
+
|
| 5481 |
+
if ollama_response.status_code == 200:
|
| 5482 |
+
response_data = ollama_response.json()
|
| 5483 |
+
response_text = response_data.get('response', '').strip()
|
| 5484 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] Ollama ์๋ต ์์ ์๋ฃ, ๊ธธ์ด: {len(response_text)}")
|
| 5485 |
+
else:
|
| 5486 |
+
error_text = ollama_response.text[:500] if ollama_response.text else "์๋ต ์์"
|
| 5487 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] Ollama API ์ค๋ฅ ({ollama_response.status_code}): {error_text}")
|
| 5488 |
+
except requests.exceptions.Timeout:
|
| 5489 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] Ollama ์์ฒญ ํ์์์ (180์ด ์ด๊ณผ)")
|
| 5490 |
+
return jsonify({'error': 'AI ์๋ต ์์ฑ ์๊ฐ์ด ์ด๊ณผ๋์์ต๋๋ค.'}), 500
|
| 5491 |
+
except requests.exceptions.ConnectionError:
|
| 5492 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] Ollama ์๋ฒ ์ฐ๊ฒฐ ์คํจ: {config.OLLAMA_BASE_URL}")
|
| 5493 |
+
return jsonify({'error': f'Ollama ์๋ฒ์ ์ฐ๊ฒฐํ ์ ์์ต๋๋ค. ์๋ฒ๊ฐ ์คํ ์ค์ธ์ง ํ์ธํ์ธ์. ({config.OLLAMA_BASE_URL})'}), 500
|
| 5494 |
+
except Exception as e:
|
| 5495 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] Ollama ์ค๋ฅ: {str(e)}")
|
| 5496 |
+
import traceback
|
| 5497 |
+
traceback.print_exc()
|
| 5498 |
+
|
| 5499 |
+
if not response_text:
|
| 5500 |
+
return jsonify({'error': 'AI ์๋ต์ ๋ฐ์ง ๋ชปํ์ต๋๋ค.'}), 500
|
| 5501 |
+
|
| 5502 |
+
# JSON ํ์ฑ
|
| 5503 |
+
import json
|
| 5504 |
+
import re
|
| 5505 |
+
|
| 5506 |
+
tags_data = {}
|
| 5507 |
+
try:
|
| 5508 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] AI ์๋ต ๊ธธ์ด: {len(response_text)}")
|
| 5509 |
+
cleaned_text = re.sub(r'```json\s*', '', response_text)
|
| 5510 |
+
cleaned_text = re.sub(r'```\s*', '', cleaned_text)
|
| 5511 |
+
cleaned_text = cleaned_text.strip()
|
| 5512 |
+
|
| 5513 |
+
json_match = re.search(r'\{.*\}', cleaned_text, re.DOTALL)
|
| 5514 |
+
if json_match:
|
| 5515 |
+
json_str = json_match.group(0)
|
| 5516 |
+
try:
|
| 5517 |
+
tags_data = json.loads(json_str)
|
| 5518 |
+
except json.JSONDecodeError as je:
|
| 5519 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] JSON ํ์ฑ ์คํจ: {je}")
|
| 5520 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] ํ์ฑ ์คํจํ ๋ฌธ์์ด ์ผ๋ถ: {json_str[:200]}...")
|
| 5521 |
+
try:
|
| 5522 |
+
json_str_fixed = re.sub(r',\s*([\]}])', r'\1', json_str)
|
| 5523 |
+
tags_data = json.loads(json_str_fixed)
|
| 5524 |
+
print("[์ผ๋ฐ ํ๊ทธ ์์ฑ] JSON ๋ณต๊ตฌ ์ฑ๊ณต")
|
| 5525 |
+
except:
|
| 5526 |
+
raise je
|
| 5527 |
+
|
| 5528 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] ์์ฑ๋ ํ๊ทธ ๊ตฌ์กฐ: {list(tags_data.keys())}")
|
| 5529 |
+
else:
|
| 5530 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] JSON ํ์์ด ์๋: {response_text[:200]}...")
|
| 5531 |
+
return jsonify({'error': 'AI ์๋ต์ด ์ฌ๋ฐ๋ฅธ JSON ํ์์ด ์๋๋๋ค. (๋ก๊ทธ ํ์ธ ํ์)'}), 500
|
| 5532 |
except Exception as e:
|
| 5533 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] ํ์ฑ ์ค๋ฅ: {str(e)}")
|
| 5534 |
return jsonify({'error': f'ํ๊ทธ ํ์ฑ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}'}), 500
|
| 5535 |
|
| 5536 |
+
# DB ์ ์ฅ (๊ธฐ์กด ํ๊ทธ๊ฐ ์์ผ๋ฉด ๋ณํฉ, type ํ๋ ์ถ๊ฐ)
|
| 5537 |
+
import json
|
| 5538 |
+
existing_tags = None
|
| 5539 |
+
if file.tags:
|
| 5540 |
+
try:
|
| 5541 |
+
existing_tags = json.loads(file.tags)
|
| 5542 |
+
except:
|
| 5543 |
+
pass
|
| 5544 |
+
|
| 5545 |
+
tags_data_with_type = {
|
| 5546 |
+
'type': 'simple',
|
| 5547 |
+
'tags': tags_data
|
| 5548 |
+
}
|
| 5549 |
+
|
| 5550 |
+
# ๊ธฐ์กด ์์ธ ํ๊ทธ๊ฐ ์์ผ๋ฉด ๋ณํฉ
|
| 5551 |
+
if existing_tags and existing_tags.get('type') == 'detailed':
|
| 5552 |
+
tags_data_with_type['detailed_tags'] = existing_tags.get('tags', {})
|
| 5553 |
+
|
| 5554 |
+
tags_json_str = json.dumps(tags_data_with_type, ensure_ascii=False)
|
| 5555 |
+
file.tags = tags_json_str
|
| 5556 |
db.session.commit()
|
| 5557 |
|
| 5558 |
+
# ์ ์ฅ ํ์ธ
|
| 5559 |
+
db.session.refresh(file)
|
| 5560 |
+
if file.tags:
|
| 5561 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] DB ์ ์ฅ ์ฑ๊ณต - ํ์ผ ID: {file.id}, ํ๊ทธ ๊ธธ์ด: {len(file.tags)}")
|
| 5562 |
+
else:
|
| 5563 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] ๊ฒฝ๊ณ : DB ์ ์ฅ ํ ํ๊ทธ๊ฐ None์
๋๋ค!")
|
| 5564 |
+
|
| 5565 |
return jsonify({
|
| 5566 |
+
'message': '์ผ๋ฐ ํ๊ทธ๊ฐ ์์ฑ๋์์ต๋๋ค.',
|
| 5567 |
'tags': tags_data,
|
| 5568 |
+
'file_id': file.id,
|
| 5569 |
+
'type': 'simple'
|
| 5570 |
}), 200
|
| 5571 |
|
| 5572 |
except Exception as e:
|
| 5573 |
db.session.rollback()
|
| 5574 |
+
error_msg = str(e)
|
| 5575 |
+
print(f"[์ผ๋ฐ ํ๊ทธ ์์ฑ] ์์ธ ๋ฐ์: {error_msg}")
|
| 5576 |
+
import traceback
|
| 5577 |
+
traceback.print_exc()
|
| 5578 |
+
return jsonify({'error': f'์ผ๋ฐ ํ๊ทธ ์์ฑ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {error_msg}'}), 500
|
| 5579 |
|
| 5580 |
@main_bp.route('/api/files/<int:file_id>/metadata', methods=['POST'])
|
| 5581 |
@login_required
|
templates/admin.html
CHANGED
|
@@ -46,8 +46,103 @@
|
|
| 46 |
|
| 47 |
.header-actions {
|
| 48 |
display: flex;
|
| 49 |
-
gap:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
align-items: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
.menu-toggle {
|
|
@@ -573,15 +668,53 @@
|
|
| 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 |
-
<
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 585 |
</div>
|
| 586 |
</div>
|
| 587 |
|
|
@@ -594,13 +727,25 @@
|
|
| 594 |
</div>
|
| 595 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 596 |
<div class="mobile-menu-items">
|
| 597 |
-
<
|
| 598 |
-
<a href="{{ url_for('main.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 599 |
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์์ง ํ์ธ</a>
|
| 600 |
-
|
|
|
|
| 601 |
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์ค์ </a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
| 603 |
-
|
|
|
|
| 604 |
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 605 |
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 606 |
</div>
|
|
|
|
| 46 |
|
| 47 |
.header-actions {
|
| 48 |
display: flex;
|
| 49 |
+
gap: 8px;
|
| 50 |
+
align-items: center;
|
| 51 |
+
flex-wrap: wrap;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* ๋๋กญ๋ค์ด ๋ฉ๋ด ์คํ์ผ */
|
| 55 |
+
.dropdown {
|
| 56 |
+
position: relative;
|
| 57 |
+
display: inline-block;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* ๋ฒํผ๊ณผ ๋ฉ๋ด ์ฌ์ด 'ํ'์์ hover๊ฐ ๋๊ฒจ ๋ฉ๋ด๊ฐ ๋ซํ๋ ํ์ ๋ฐฉ์ง */
|
| 61 |
+
.dropdown::after {
|
| 62 |
+
content: '';
|
| 63 |
+
position: absolute;
|
| 64 |
+
left: 0;
|
| 65 |
+
right: 0;
|
| 66 |
+
top: 100%;
|
| 67 |
+
height: 8px;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.dropdown-toggle {
|
| 71 |
+
padding: 8px 16px;
|
| 72 |
+
background: #f1f3f4;
|
| 73 |
+
color: #202124;
|
| 74 |
+
border: none;
|
| 75 |
+
border-radius: 6px;
|
| 76 |
+
font-size: 14px;
|
| 77 |
+
font-weight: 500;
|
| 78 |
+
cursor: pointer;
|
| 79 |
+
transition: all 0.2s;
|
| 80 |
+
display: flex;
|
| 81 |
align-items: center;
|
| 82 |
+
gap: 6px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.dropdown-toggle:hover {
|
| 86 |
+
background: #e8eaed;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.dropdown-toggle::after {
|
| 90 |
+
content: 'โผ';
|
| 91 |
+
font-size: 10px;
|
| 92 |
+
transition: transform 0.2s;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.dropdown:hover .dropdown-toggle::after {
|
| 96 |
+
transform: rotate(180deg);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.dropdown-menu {
|
| 100 |
+
position: absolute;
|
| 101 |
+
top: calc(100% + 4px);
|
| 102 |
+
left: 0;
|
| 103 |
+
margin-top: 0;
|
| 104 |
+
background: white;
|
| 105 |
+
border: 1px solid #dadce0;
|
| 106 |
+
border-radius: 6px;
|
| 107 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 108 |
+
min-width: 200px;
|
| 109 |
+
opacity: 0;
|
| 110 |
+
visibility: hidden;
|
| 111 |
+
transform: translateY(-8px);
|
| 112 |
+
transition: all 0.2s ease;
|
| 113 |
+
z-index: 10000;
|
| 114 |
+
padding: 4px 0;
|
| 115 |
+
pointer-events: none;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.dropdown:hover .dropdown-menu {
|
| 119 |
+
opacity: 1;
|
| 120 |
+
visibility: visible;
|
| 121 |
+
transform: translateY(0);
|
| 122 |
+
pointer-events: auto;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.dropdown-item {
|
| 126 |
+
display: block;
|
| 127 |
+
padding: 10px 16px;
|
| 128 |
+
color: #202124;
|
| 129 |
+
text-decoration: none;
|
| 130 |
+
font-size: 14px;
|
| 131 |
+
transition: background 0.2s;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.dropdown-item:hover {
|
| 135 |
+
background: #f8f9fa;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.dropdown-item:first-child {
|
| 139 |
+
border-top-left-radius: 6px;
|
| 140 |
+
border-top-right-radius: 6px;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.dropdown-item:last-child {
|
| 144 |
+
border-bottom-left-radius: 6px;
|
| 145 |
+
border-bottom-right-radius: 6px;
|
| 146 |
}
|
| 147 |
|
| 148 |
.menu-toggle {
|
|
|
|
| 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 |
+
</div>
|
| 705 |
+
</div>
|
| 706 |
+
|
| 707 |
+
{# ํธ์๊ธฐ๋ฅ #}
|
| 708 |
+
<div class="dropdown">
|
| 709 |
+
<button type="button" class="dropdown-toggle">ํธ์๊ธฐ๋ฅ</button>
|
| 710 |
+
<div class="dropdown-menu">
|
| 711 |
+
<a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์ ํธ</a>
|
| 712 |
+
</div>
|
| 713 |
+
</div>
|
| 714 |
+
|
| 715 |
+
{# ๋ฉ์ธ์ผ๋ก #}
|
| 716 |
+
<a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ์ธ์ผ๋ก</a>
|
| 717 |
+
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋ก๊ทธ์์</a>
|
| 718 |
</div>
|
| 719 |
</div>
|
| 720 |
|
|
|
|
| 727 |
</div>
|
| 728 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 729 |
<div class="mobile-menu-items">
|
| 730 |
+
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์ฌ์ดํธ ๊ด๋ฆฌ</div>
|
| 731 |
+
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฌ์ฉ์ ๊ด๋ฆฌ</a>
|
| 732 |
+
<a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ ํฐ ํต๊ณ</a>
|
| 733 |
+
|
| 734 |
+
<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>
|
| 735 |
+
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์น์์ค ์
๋ก๋ ๋ฐ ๊ด๋ฆฌ</a>
|
| 736 |
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์์ง ํ์ธ</a>
|
| 737 |
+
|
| 738 |
+
<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>
|
| 739 |
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์ค์ </a>
|
| 740 |
+
<a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๋กฌํํธ ๊ด๋ฆฌ</a>
|
| 741 |
+
|
| 742 |
+
<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>
|
| 743 |
+
<a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๊ทธ/ํ๋กฌํํธ</a>
|
| 744 |
+
|
| 745 |
+
<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>
|
| 746 |
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
| 747 |
+
|
| 748 |
+
<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>
|
| 749 |
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 750 |
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 751 |
</div>
|
templates/admin_files.html
CHANGED
|
@@ -44,10 +44,107 @@
|
|
| 44 |
gap: 12px;
|
| 45 |
}
|
| 46 |
|
| 47 |
-
|
| 48 |
display: flex;
|
| 49 |
-
gap:
|
| 50 |
align-items: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
.menu-toggle {
|
|
@@ -323,6 +420,21 @@
|
|
| 323 |
max-height: 90vh;
|
| 324 |
overflow-y: auto;
|
| 325 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
|
| 327 |
.modal-header {
|
| 328 |
display: flex;
|
|
@@ -475,16 +587,53 @@
|
|
| 475 |
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ๋ด ์ด๊ธฐ">โฐ</button>
|
| 476 |
<div class="header-actions">
|
| 477 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
<
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
</div>
|
| 489 |
</div>
|
| 490 |
|
|
@@ -497,14 +646,25 @@
|
|
| 497 |
</div>
|
| 498 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 499 |
<div class="mobile-menu-items">
|
|
|
|
| 500 |
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฌ์ฉ์ ๊ด๋ฆฌ</a>
|
| 501 |
-
<a href="{{ url_for('main.
|
|
|
|
|
|
|
|
|
|
| 502 |
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์์ง ํ์ธ</a>
|
| 503 |
-
|
|
|
|
| 504 |
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์ค์ </a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
| 506 |
-
|
| 507 |
-
<
|
| 508 |
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 509 |
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 510 |
</div>
|
|
@@ -674,18 +834,6 @@
|
|
| 674 |
</div>
|
| 675 |
</div>
|
| 676 |
|
| 677 |
-
<!-- ํ๊ทธ ๋ณด๊ธฐ ๋ชจ๋ฌ -->
|
| 678 |
-
<div id="tagsModal" class="modal">
|
| 679 |
-
<div class="modal-content">
|
| 680 |
-
<div class="modal-header">
|
| 681 |
-
<div class="modal-title" id="tagsModalTitle">ํ๊ทธ ๋ชฉ๋ก</div>
|
| 682 |
-
<button class="modal-close" onclick="closeTagsModal()">×</button>
|
| 683 |
-
</div>
|
| 684 |
-
<div id="tagsContent" class="modal-body">
|
| 685 |
-
ํ๊ทธ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...
|
| 686 |
-
</div>
|
| 687 |
-
</div>
|
| 688 |
-
</div>
|
| 689 |
|
| 690 |
<!-- vis-network ๋ผ์ด๋ธ๋ฌ๋ฆฌ -->
|
| 691 |
<script type="text/javascript" src="https://unpkg.com/vis-network@latest/standalone/umd/vis-network.min.js"></script>
|
|
@@ -811,7 +959,6 @@
|
|
| 811 |
<div class="file-actions">
|
| 812 |
<button class="btn btn-secondary" onclick="viewSummary(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">์์ฝ ๋ด์ฉ ๋ณด๊ธฐ</button>
|
| 813 |
<button class="btn btn-info" onclick="viewChunks(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">์ฒญํฌ ๋ณด๊ธฐ</button>
|
| 814 |
-
<button class="btn" onclick="viewTags(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px; margin-right: 4px; background: #673ab7; color: white;">ํ๊ทธ ๋ณด๊ธฐ</button>
|
| 815 |
<button class="btn btn-primary" onclick="viewGraphRAG(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">GraphRAG ๋ณด๊ธฐ</button>
|
| 816 |
<button class="btn btn-success" onclick="viewGraphRAGVisualization(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px;">๊ทธ๋ํ ์๊ฐํ</button>
|
| 817 |
</div>
|
|
@@ -1849,112 +1996,6 @@
|
|
| 1849 |
}
|
| 1850 |
});
|
| 1851 |
|
| 1852 |
-
async function viewTags(fileId, fileName) {
|
| 1853 |
-
const modal = document.getElementById('tagsModal');
|
| 1854 |
-
const title = document.getElementById('tagsModalTitle');
|
| 1855 |
-
const content = document.getElementById('tagsContent');
|
| 1856 |
-
|
| 1857 |
-
title.textContent = `ํ๊ทธ ๋ชฉ๋ก - ${fileName}`;
|
| 1858 |
-
content.innerHTML = '<div style="text-align: center; padding: 24px; color: #5f6368;">ํ๊ทธ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...</div>';
|
| 1859 |
-
modal.classList.add('active');
|
| 1860 |
-
|
| 1861 |
-
try {
|
| 1862 |
-
// ํ์ผ ์ ๋ณด ๋ค์ ์กฐํ (ํ๊ทธ ์ ๋ณด ํฌํจ)
|
| 1863 |
-
const response = await fetch('/api/files', { credentials: 'include' });
|
| 1864 |
-
const data = await response.json();
|
| 1865 |
-
const file = data.files.find(f => f.id === fileId);
|
| 1866 |
-
|
| 1867 |
-
if (file && file.tags) {
|
| 1868 |
-
try {
|
| 1869 |
-
const tags = JSON.parse(file.tags);
|
| 1870 |
-
let html = '';
|
| 1871 |
-
|
| 1872 |
-
// ๊ณ์ธต์ ๊ตฌ์กฐ ๋ ๋๋ง
|
| 1873 |
-
|
| 1874 |
-
// Parent Chunk
|
| 1875 |
-
if (tags.parent_chunk) {
|
| 1876 |
-
html += '<div style="margin-bottom: 24px;">';
|
| 1877 |
-
html += '<h3 style="font-size: 15px; font-weight: 700; color: #1a73e8; margin-bottom: 12px; border-bottom: 2px solid #e8f0fe; padding-bottom: 8px;">Parent Chunk (์ ์ฒด ์์ฝ)</h3>';
|
| 1878 |
-
html += renderTagsGroup(tags.parent_chunk);
|
| 1879 |
-
html += '</div>';
|
| 1880 |
-
}
|
| 1881 |
-
|
| 1882 |
-
// Episodes
|
| 1883 |
-
if (tags.episodes) {
|
| 1884 |
-
html += '<div style="margin-bottom: 24px;">';
|
| 1885 |
-
html += '<h3 style="font-size: 15px; font-weight: 700; color: #1a73e8; margin-bottom: 12px; border-bottom: 2px solid #e8f0fe; padding-bottom: 8px;">ํ์ฐจ๋ณ ๋ถ์</h3>';
|
| 1886 |
-
html += renderTagsGroup(tags.episodes);
|
| 1887 |
-
html += '</div>';
|
| 1888 |
-
}
|
| 1889 |
-
|
| 1890 |
-
// GraphRAG
|
| 1891 |
-
if (tags.graph_rag) {
|
| 1892 |
-
html += '<div style="margin-bottom: 24px;">';
|
| 1893 |
-
html += '<h3 style="font-size: 15px; font-weight: 700; color: #1a73e8; margin-bottom: 12px; border-bottom: 2px solid #e8f0fe; padding-bottom: 8px;">GraphRAG (์ง์ ๊ทธ๋ํ)</h3>';
|
| 1894 |
-
html += renderTagsGroup(tags.graph_rag);
|
| 1895 |
-
html += '</div>';
|
| 1896 |
-
}
|
| 1897 |
-
|
| 1898 |
-
// ๊ตฌ๋ฒ์ ํ๊ทธ (๋ฐฐ์ด) ํธํ์ฑ - ๊ณ์ธต์ ๊ตฌ์กฐ๊ฐ ์๋ ๊ฒฝ์ฐ
|
| 1899 |
-
if (!tags.parent_chunk && !tags.episodes && !tags.graph_rag && Array.isArray(tags)) {
|
| 1900 |
-
html += '<div style="display: flex; flex-wrap: wrap; gap: 8px;">';
|
| 1901 |
-
tags.forEach(tag => {
|
| 1902 |
-
html += `<span style="background: #e8f0fe; color: #1a73e8; padding: 6px 12px; border-radius: 16px; font-size: 13px; border: 1px solid #d2e3fc;">#${escapeHtml(tag)}</span>`;
|
| 1903 |
-
});
|
| 1904 |
-
html += '</div>';
|
| 1905 |
-
}
|
| 1906 |
-
|
| 1907 |
-
if (!html) {
|
| 1908 |
-
html = '<div style="text-align: center; padding: 20px; color: #5f6368;">ํ๊ทธ ํ์์ด ์ฌ๋ฐ๋ฅด์ง ์๊ฑฐ๋ ๋น์ด์์ต๋๋ค.</div>';
|
| 1909 |
-
}
|
| 1910 |
-
|
| 1911 |
-
content.innerHTML = html;
|
| 1912 |
-
} catch (e) {
|
| 1913 |
-
console.error('ํ๊ทธ ํ์ฑ ์ค๋ฅ:', e);
|
| 1914 |
-
content.innerHTML = '<div style="text-align: center; padding: 20px; color: #c5221f;">ํ๊ทธ ๋ฐ์ดํฐ๋ฅผ ํ์ฑํ ์ ์์ต๋๋ค.</div>';
|
| 1915 |
-
}
|
| 1916 |
-
} else {
|
| 1917 |
-
content.innerHTML = '<div style="text-align: center; padding: 20px; color: #5f6368;">์์ฑ๋ ํ๊ทธ๊ฐ ์์ต๋๋ค.</div>';
|
| 1918 |
-
}
|
| 1919 |
-
} catch (error) {
|
| 1920 |
-
console.error('ํ๊ทธ ๋ก๋ ์ค๋ฅ:', error);
|
| 1921 |
-
content.innerHTML = '<div style="text-align: center; padding: 20px; color: #c5221f;">ํ๊ทธ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.</div>';
|
| 1922 |
-
}
|
| 1923 |
-
}
|
| 1924 |
-
|
| 1925 |
-
function renderTagsGroup(group) {
|
| 1926 |
-
let html = '<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px;">';
|
| 1927 |
-
|
| 1928 |
-
// ํค ์ด๋ฆ์ ํ๊ธ๋ก ๋งคํ
|
| 1929 |
-
const keyMap = {
|
| 1930 |
-
'world_view': '์ธ๊ณ๊ด',
|
| 1931 |
-
'characters': '์ธ๋ฌผ',
|
| 1932 |
-
'story': '์คํ ๋ฆฌ',
|
| 1933 |
-
'others': '๊ธฐํ',
|
| 1934 |
-
'items': '์์ดํ
/์์ฌ',
|
| 1935 |
-
'relationships': '๊ด๊ณ',
|
| 1936 |
-
'events': '์ฌ๊ฑด'
|
| 1937 |
-
};
|
| 1938 |
-
|
| 1939 |
-
for (const [key, values] of Object.entries(group)) {
|
| 1940 |
-
if (values && values.length > 0) {
|
| 1941 |
-
const label = keyMap[key] || key;
|
| 1942 |
-
html += `<div style="background: #f8f9fa; padding: 12px; border-radius: 8px; border: 1px solid #e8eaed;">`;
|
| 1943 |
-
html += `<div style="font-size: 13px; font-weight: 700; color: #5f6368; margin-bottom: 8px;">${escapeHtml(label)}</div>`;
|
| 1944 |
-
html += `<div style="display: flex; flex-wrap: wrap; gap: 6px;">`;
|
| 1945 |
-
values.forEach(tag => {
|
| 1946 |
-
html += `<span style="background: white; border: 1px solid #dadce0; padding: 4px 8px; border-radius: 4px; font-size: 12px; color: #3c4043;">${escapeHtml(tag)}</span>`;
|
| 1947 |
-
});
|
| 1948 |
-
html += `</div></div>`;
|
| 1949 |
-
}
|
| 1950 |
-
}
|
| 1951 |
-
html += '</div>';
|
| 1952 |
-
return html;
|
| 1953 |
-
}
|
| 1954 |
-
|
| 1955 |
-
function closeTagsModal() {
|
| 1956 |
-
document.getElementById('tagsModal').classList.remove('active');
|
| 1957 |
-
}
|
| 1958 |
|
| 1959 |
// ํ์ด์ง ๋ก๋ ์ ์ด๊ธฐํ
|
| 1960 |
window.addEventListener('load', () => {
|
|
|
|
| 44 |
gap: 12px;
|
| 45 |
}
|
| 46 |
|
| 47 |
+
.header-actions {
|
| 48 |
display: flex;
|
| 49 |
+
gap: 8px;
|
| 50 |
align-items: center;
|
| 51 |
+
flex-wrap: wrap;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* ๋๋กญ๋ค์ด ๋ฉ๋ด ์คํ์ผ */
|
| 55 |
+
.dropdown {
|
| 56 |
+
position: relative;
|
| 57 |
+
display: inline-block;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* ๋ฒํผ๊ณผ ๋ฉ๋ด ์ฌ์ด 'ํ'์์ hover๊ฐ ๋๊ฒจ ๋ฉ๋ด๊ฐ ๋ซํ๋ ํ์ ๋ฐฉ์ง */
|
| 61 |
+
.dropdown::after {
|
| 62 |
+
content: '';
|
| 63 |
+
position: absolute;
|
| 64 |
+
left: 0;
|
| 65 |
+
right: 0;
|
| 66 |
+
top: 100%;
|
| 67 |
+
height: 8px;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.dropdown-toggle {
|
| 71 |
+
padding: 8px 16px;
|
| 72 |
+
background: #f1f3f4;
|
| 73 |
+
color: #202124;
|
| 74 |
+
border: none;
|
| 75 |
+
border-radius: 6px;
|
| 76 |
+
font-size: 14px;
|
| 77 |
+
font-weight: 500;
|
| 78 |
+
cursor: pointer;
|
| 79 |
+
transition: all 0.2s;
|
| 80 |
+
display: flex;
|
| 81 |
+
align-items: center;
|
| 82 |
+
gap: 6px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.dropdown-toggle:hover {
|
| 86 |
+
background: #e8eaed;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.dropdown-toggle::after {
|
| 90 |
+
content: 'โผ';
|
| 91 |
+
font-size: 10px;
|
| 92 |
+
transition: transform 0.2s;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.dropdown:hover .dropdown-toggle::after {
|
| 96 |
+
transform: rotate(180deg);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.dropdown-menu {
|
| 100 |
+
position: absolute;
|
| 101 |
+
top: calc(100% + 4px);
|
| 102 |
+
left: 0;
|
| 103 |
+
margin-top: 0;
|
| 104 |
+
background: white;
|
| 105 |
+
border: 1px solid #dadce0;
|
| 106 |
+
border-radius: 6px;
|
| 107 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 108 |
+
min-width: 200px;
|
| 109 |
+
opacity: 0;
|
| 110 |
+
visibility: hidden;
|
| 111 |
+
transform: translateY(-8px);
|
| 112 |
+
transition: all 0.2s ease;
|
| 113 |
+
z-index: 10000;
|
| 114 |
+
padding: 4px 0;
|
| 115 |
+
pointer-events: none;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.dropdown:hover .dropdown-menu {
|
| 119 |
+
opacity: 1;
|
| 120 |
+
visibility: visible;
|
| 121 |
+
transform: translateY(0);
|
| 122 |
+
pointer-events: auto;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
.dropdown-item {
|
| 128 |
+
display: block;
|
| 129 |
+
padding: 10px 16px;
|
| 130 |
+
color: #202124;
|
| 131 |
+
text-decoration: none;
|
| 132 |
+
font-size: 14px;
|
| 133 |
+
transition: background 0.2s;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.dropdown-item:hover {
|
| 137 |
+
background: #f8f9fa;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.dropdown-item:first-child {
|
| 141 |
+
border-top-left-radius: 6px;
|
| 142 |
+
border-top-right-radius: 6px;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.dropdown-item:last-child {
|
| 146 |
+
border-bottom-left-radius: 6px;
|
| 147 |
+
border-bottom-right-radius: 6px;
|
| 148 |
}
|
| 149 |
|
| 150 |
.menu-toggle {
|
|
|
|
| 420 |
max-height: 90vh;
|
| 421 |
overflow-y: auto;
|
| 422 |
}
|
| 423 |
+
|
| 424 |
+
.modal-body {
|
| 425 |
+
max-height: calc(90vh - 100px);
|
| 426 |
+
overflow-y: auto;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
#tagsContent {
|
| 430 |
+
max-height: calc(90vh - 120px);
|
| 431 |
+
overflow-y: auto;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
#simpleTagsContent, #detailedTagsContent {
|
| 435 |
+
max-height: calc(90vh - 180px);
|
| 436 |
+
overflow-y: auto;
|
| 437 |
+
}
|
| 438 |
|
| 439 |
.modal-header {
|
| 440 |
display: flex;
|
|
|
|
| 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 |
+
</div>
|
| 624 |
+
</div>
|
| 625 |
+
|
| 626 |
+
{# ํธ์๊ธฐ๋ฅ #}
|
| 627 |
+
<div class="dropdown">
|
| 628 |
+
<button type="button" class="dropdown-toggle">ํธ์๊ธฐ๋ฅ</button>
|
| 629 |
+
<div class="dropdown-menu">
|
| 630 |
+
<a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์ ํธ</a>
|
| 631 |
+
</div>
|
| 632 |
+
</div>
|
| 633 |
+
|
| 634 |
+
{# ๋ฉ์ธ์ผ๋ก #}
|
| 635 |
+
<a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ์ธ์ผ๋ก</a>
|
| 636 |
+
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋ก๊ทธ์์</a>
|
| 637 |
</div>
|
| 638 |
</div>
|
| 639 |
|
|
|
|
| 646 |
</div>
|
| 647 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 648 |
<div class="mobile-menu-items">
|
| 649 |
+
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์ฌ์ดํธ ๊ด๋ฆฌ</div>
|
| 650 |
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฌ์ฉ์ ๊ด๋ฆฌ</a>
|
| 651 |
+
<a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ ํฐ ํต๊ณ</a>
|
| 652 |
+
|
| 653 |
+
<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>
|
| 654 |
+
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์น์์ค ์
๋ก๋ ๋ฐ ๊ด๋ฆฌ</a>
|
| 655 |
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์์ง ํ์ธ</a>
|
| 656 |
+
|
| 657 |
+
<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>
|
| 658 |
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์ค์ </a>
|
| 659 |
+
<a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๋กฌํํธ ๊ด๋ฆฌ</a>
|
| 660 |
+
|
| 661 |
+
<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>
|
| 662 |
+
<a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๊ทธ/ํ๋กฌํํธ</a>
|
| 663 |
+
|
| 664 |
+
<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>
|
| 665 |
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
| 666 |
+
|
| 667 |
+
<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>
|
| 668 |
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 669 |
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 670 |
</div>
|
|
|
|
| 834 |
</div>
|
| 835 |
</div>
|
| 836 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 837 |
|
| 838 |
<!-- vis-network ๋ผ์ด๋ธ๋ฌ๋ฆฌ -->
|
| 839 |
<script type="text/javascript" src="https://unpkg.com/vis-network@latest/standalone/umd/vis-network.min.js"></script>
|
|
|
|
| 959 |
<div class="file-actions">
|
| 960 |
<button class="btn btn-secondary" onclick="viewSummary(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">์์ฝ ๋ด์ฉ ๋ณด๊ธฐ</button>
|
| 961 |
<button class="btn btn-info" onclick="viewChunks(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">์ฒญํฌ ๋ณด๊ธฐ</button>
|
|
|
|
| 962 |
<button class="btn btn-primary" onclick="viewGraphRAG(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">GraphRAG ๋ณด๊ธฐ</button>
|
| 963 |
<button class="btn btn-success" onclick="viewGraphRAGVisualization(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px;">๊ทธ๋ํ ์๊ฐํ</button>
|
| 964 |
</div>
|
|
|
|
| 1996 |
}
|
| 1997 |
});
|
| 1998 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1999 |
|
| 2000 |
// ํ์ด์ง ๋ก๋ ์ ์ด๊ธฐํ
|
| 2001 |
window.addEventListener('load', () => {
|
templates/admin_messages.html
CHANGED
|
@@ -44,10 +44,107 @@
|
|
| 44 |
gap: 12px;
|
| 45 |
}
|
| 46 |
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
display: flex;
|
| 49 |
-
gap: 12px;
|
| 50 |
align-items: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
.menu-toggle {
|
|
@@ -476,16 +573,53 @@
|
|
| 476 |
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ๋ด ์ด๊ธฐ">โฐ</button>
|
| 477 |
<div class="header-actions">
|
| 478 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
<
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
</div>
|
| 490 |
</div>
|
| 491 |
|
|
@@ -498,14 +632,25 @@
|
|
| 498 |
</div>
|
| 499 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 500 |
<div class="mobile-menu-items">
|
|
|
|
| 501 |
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฌ์ฉ์ ๊ด๋ฆฌ</a>
|
| 502 |
-
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์น์์ค ๊ด๋ฆฌ</a>
|
| 503 |
-
<a href="{{ url_for('main.admin_files') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ์ผ ๋ชฉ๋ก</a>
|
| 504 |
-
<a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๋กฌํํธ ๊ด๋ฆฌ</a>
|
| 505 |
-
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์ค์ </a>
|
| 506 |
-
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
| 507 |
<a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ ํฐ ํต๊ณ</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
|
|
|
|
|
|
| 509 |
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 510 |
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 511 |
</div>
|
|
|
|
| 44 |
gap: 12px;
|
| 45 |
}
|
| 46 |
|
| 47 |
+
.header-actions {
|
| 48 |
+
display: flex;
|
| 49 |
+
gap: 8px;
|
| 50 |
+
align-items: center;
|
| 51 |
+
flex-wrap: wrap;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* ๋๋กญ๋ค์ด ๋ฉ๋ด ์คํ์ผ */
|
| 55 |
+
.dropdown {
|
| 56 |
+
position: relative;
|
| 57 |
+
display: inline-block;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* ๋ฒํผ๊ณผ ๋ฉ๋ด ์ฌ์ด 'ํ'์์ hover๊ฐ ๋๊ฒจ ๋ฉ๋ด๊ฐ ๋ซํ๋ ํ์ ๋ฐฉ์ง */
|
| 61 |
+
.dropdown::after {
|
| 62 |
+
content: '';
|
| 63 |
+
position: absolute;
|
| 64 |
+
left: 0;
|
| 65 |
+
right: 0;
|
| 66 |
+
top: 100%;
|
| 67 |
+
height: 8px;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.dropdown-toggle {
|
| 71 |
+
padding: 8px 16px;
|
| 72 |
+
background: #f1f3f4;
|
| 73 |
+
color: #202124;
|
| 74 |
+
border: none;
|
| 75 |
+
border-radius: 6px;
|
| 76 |
+
font-size: 14px;
|
| 77 |
+
font-weight: 500;
|
| 78 |
+
cursor: pointer;
|
| 79 |
+
transition: all 0.2s;
|
| 80 |
display: flex;
|
|
|
|
| 81 |
align-items: center;
|
| 82 |
+
gap: 6px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.dropdown-toggle:hover {
|
| 86 |
+
background: #e8eaed;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.dropdown-toggle::after {
|
| 90 |
+
content: 'โผ';
|
| 91 |
+
font-size: 10px;
|
| 92 |
+
transition: transform 0.2s;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.dropdown:hover .dropdown-toggle::after {
|
| 96 |
+
transform: rotate(180deg);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.dropdown-menu {
|
| 100 |
+
position: absolute;
|
| 101 |
+
top: calc(100% + 4px);
|
| 102 |
+
left: 0;
|
| 103 |
+
margin-top: 0;
|
| 104 |
+
background: white;
|
| 105 |
+
border: 1px solid #dadce0;
|
| 106 |
+
border-radius: 6px;
|
| 107 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 108 |
+
min-width: 200px;
|
| 109 |
+
opacity: 0;
|
| 110 |
+
visibility: hidden;
|
| 111 |
+
transform: translateY(-8px);
|
| 112 |
+
transition: all 0.2s ease;
|
| 113 |
+
z-index: 10000;
|
| 114 |
+
padding: 4px 0;
|
| 115 |
+
pointer-events: none;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.dropdown:hover .dropdown-menu {
|
| 119 |
+
opacity: 1;
|
| 120 |
+
visibility: visible;
|
| 121 |
+
transform: translateY(0);
|
| 122 |
+
pointer-events: auto;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
.dropdown-item {
|
| 128 |
+
display: block;
|
| 129 |
+
padding: 10px 16px;
|
| 130 |
+
color: #202124;
|
| 131 |
+
text-decoration: none;
|
| 132 |
+
font-size: 14px;
|
| 133 |
+
transition: background 0.2s;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.dropdown-item:hover {
|
| 137 |
+
background: #f8f9fa;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.dropdown-item:first-child {
|
| 141 |
+
border-top-left-radius: 6px;
|
| 142 |
+
border-top-right-radius: 6px;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.dropdown-item:last-child {
|
| 146 |
+
border-bottom-left-radius: 6px;
|
| 147 |
+
border-bottom-right-radius: 6px;
|
| 148 |
}
|
| 149 |
|
| 150 |
.menu-toggle {
|
|
|
|
| 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 |
+
</div>
|
| 610 |
+
</div>
|
| 611 |
+
|
| 612 |
+
{# ํธ์๊ธฐ๋ฅ #}
|
| 613 |
+
<div class="dropdown">
|
| 614 |
+
<button type="button" class="dropdown-toggle">ํธ์๊ธฐ๋ฅ</button>
|
| 615 |
+
<div class="dropdown-menu">
|
| 616 |
+
<a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์ ํธ</a>
|
| 617 |
+
</div>
|
| 618 |
+
</div>
|
| 619 |
+
|
| 620 |
+
{# ๋ฉ์ธ์ผ๋ก #}
|
| 621 |
+
<a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ์ธ์ผ๋ก</a>
|
| 622 |
+
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋ก๊ทธ์์</a>
|
| 623 |
</div>
|
| 624 |
</div>
|
| 625 |
|
|
|
|
| 632 |
</div>
|
| 633 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 634 |
<div class="mobile-menu-items">
|
| 635 |
+
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์ฌ์ดํธ ๊ด๋ฆฌ</div>
|
| 636 |
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฌ์ฉ์ ๊ด๋ฆฌ</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
<a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ ํฐ ํต๊ณ</a>
|
| 638 |
+
|
| 639 |
+
<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>
|
| 640 |
+
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์น์์ค ์
๋ก๋ ๋ฐ ๊ด๋ฆฌ</a>
|
| 641 |
+
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์์ง ํ์ธ</a>
|
| 642 |
+
|
| 643 |
+
<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>
|
| 644 |
+
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์ค์ </a>
|
| 645 |
+
<a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๋กฌํํธ ๊ด๋ฆฌ</a>
|
| 646 |
+
|
| 647 |
+
<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>
|
| 648 |
+
<a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๊ทธ/ํ๋กฌํํธ</a>
|
| 649 |
+
|
| 650 |
+
<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>
|
| 651 |
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
| 652 |
+
|
| 653 |
+
<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>
|
| 654 |
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 655 |
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 656 |
</div>
|
templates/admin_prompts.html
CHANGED
|
@@ -46,10 +46,107 @@
|
|
| 46 |
font-weight: 500;
|
| 47 |
}
|
| 48 |
|
| 49 |
-
|
| 50 |
display: flex;
|
| 51 |
-
align-items: center;
|
| 52 |
gap: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
.menu-toggle {
|
|
@@ -330,16 +427,53 @@
|
|
| 330 |
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ๋ด ์ด๊ธฐ">โฐ</button>
|
| 331 |
<div class="header-actions">
|
| 332 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
<
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
</div>
|
| 344 |
</div>
|
| 345 |
|
|
@@ -352,14 +486,25 @@
|
|
| 352 |
</div>
|
| 353 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 354 |
<div class="mobile-menu-items">
|
|
|
|
| 355 |
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฌ์ฉ์ ๊ด๋ฆฌ</a>
|
| 356 |
-
<a href="{{ url_for('main.
|
| 357 |
-
|
|
|
|
|
|
|
| 358 |
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์์ง ํ์ธ</a>
|
|
|
|
|
|
|
| 359 |
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์ค์ </a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
| 361 |
-
|
| 362 |
-
<
|
| 363 |
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 364 |
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 365 |
</div>
|
|
|
|
| 46 |
font-weight: 500;
|
| 47 |
}
|
| 48 |
|
| 49 |
+
.header-actions {
|
| 50 |
display: flex;
|
|
|
|
| 51 |
gap: 8px;
|
| 52 |
+
align-items: center;
|
| 53 |
+
flex-wrap: wrap;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* ๋๋กญ๋ค์ด ๋ฉ๋ด ์คํ์ผ */
|
| 57 |
+
.dropdown {
|
| 58 |
+
position: relative;
|
| 59 |
+
display: inline-block;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/* ๋ฒํผ๊ณผ ๋ฉ๋ด ์ฌ์ด 'ํ'์์ hover๊ฐ ๋๊ฒจ ๋ฉ๋ด๊ฐ ๋ซํ๋ ํ์ ๋ฐฉ์ง */
|
| 63 |
+
.dropdown::after {
|
| 64 |
+
content: '';
|
| 65 |
+
position: absolute;
|
| 66 |
+
left: 0;
|
| 67 |
+
right: 0;
|
| 68 |
+
top: 100%;
|
| 69 |
+
height: 8px;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.dropdown-toggle {
|
| 73 |
+
padding: 8px 16px;
|
| 74 |
+
background: #f1f3f4;
|
| 75 |
+
color: #202124;
|
| 76 |
+
border: none;
|
| 77 |
+
border-radius: 6px;
|
| 78 |
+
font-size: 14px;
|
| 79 |
+
font-weight: 500;
|
| 80 |
+
cursor: pointer;
|
| 81 |
+
transition: all 0.2s;
|
| 82 |
+
display: flex;
|
| 83 |
+
align-items: center;
|
| 84 |
+
gap: 6px;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.dropdown-toggle:hover {
|
| 88 |
+
background: #e8eaed;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.dropdown-toggle::after {
|
| 92 |
+
content: 'โผ';
|
| 93 |
+
font-size: 10px;
|
| 94 |
+
transition: transform 0.2s;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.dropdown:hover .dropdown-toggle::after {
|
| 98 |
+
transform: rotate(180deg);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.dropdown-menu {
|
| 102 |
+
position: absolute;
|
| 103 |
+
top: calc(100% + 4px);
|
| 104 |
+
left: 0;
|
| 105 |
+
margin-top: 0;
|
| 106 |
+
background: white;
|
| 107 |
+
border: 1px solid #dadce0;
|
| 108 |
+
border-radius: 6px;
|
| 109 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 110 |
+
min-width: 200px;
|
| 111 |
+
opacity: 0;
|
| 112 |
+
visibility: hidden;
|
| 113 |
+
transform: translateY(-8px);
|
| 114 |
+
transition: all 0.2s ease;
|
| 115 |
+
z-index: 10000;
|
| 116 |
+
padding: 4px 0;
|
| 117 |
+
pointer-events: none;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.dropdown:hover .dropdown-menu {
|
| 121 |
+
opacity: 1;
|
| 122 |
+
visibility: visible;
|
| 123 |
+
transform: translateY(0);
|
| 124 |
+
pointer-events: auto;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
.dropdown-item {
|
| 130 |
+
display: block;
|
| 131 |
+
padding: 10px 16px;
|
| 132 |
+
color: #202124;
|
| 133 |
+
text-decoration: none;
|
| 134 |
+
font-size: 14px;
|
| 135 |
+
transition: background 0.2s;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.dropdown-item:hover {
|
| 139 |
+
background: #f8f9fa;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.dropdown-item:first-child {
|
| 143 |
+
border-top-left-radius: 6px;
|
| 144 |
+
border-top-right-radius: 6px;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.dropdown-item:last-child {
|
| 148 |
+
border-bottom-left-radius: 6px;
|
| 149 |
+
border-bottom-right-radius: 6px;
|
| 150 |
}
|
| 151 |
|
| 152 |
.menu-toggle {
|
|
|
|
| 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 |
+
</div>
|
| 464 |
+
</div>
|
| 465 |
+
|
| 466 |
+
{# ํธ์๊ธฐ๋ฅ #}
|
| 467 |
+
<div class="dropdown">
|
| 468 |
+
<button type="button" class="dropdown-toggle">ํธ์๊ธฐ๋ฅ</button>
|
| 469 |
+
<div class="dropdown-menu">
|
| 470 |
+
<a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์ ํธ</a>
|
| 471 |
+
</div>
|
| 472 |
+
</div>
|
| 473 |
+
|
| 474 |
+
{# ๋ฉ์ธ์ผ๋ก #}
|
| 475 |
+
<a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ์ธ์ผ๋ก</a>
|
| 476 |
+
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋ก๊ทธ์์</a>
|
| 477 |
</div>
|
| 478 |
</div>
|
| 479 |
|
|
|
|
| 486 |
</div>
|
| 487 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 488 |
<div class="mobile-menu-items">
|
| 489 |
+
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์ฌ์ดํธ ๊ด๋ฆฌ</div>
|
| 490 |
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฌ์ฉ์ ๊ด๋ฆฌ</a>
|
| 491 |
+
<a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ ํฐ ํต๊ณ</a>
|
| 492 |
+
|
| 493 |
+
<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>
|
| 494 |
+
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์น์์ค ์
๋ก๋ ๋ฐ ๊ด๋ฆฌ</a>
|
| 495 |
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์์ง ํ์ธ</a>
|
| 496 |
+
|
| 497 |
+
<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>
|
| 498 |
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์ค์ </a>
|
| 499 |
+
<a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๋กฌํํธ ๊ด๋ฆฌ</a>
|
| 500 |
+
|
| 501 |
+
<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>
|
| 502 |
+
<a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๊ทธ/ํ๋กฌํํธ</a>
|
| 503 |
+
|
| 504 |
+
<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>
|
| 505 |
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
| 506 |
+
|
| 507 |
+
<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>
|
| 508 |
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 509 |
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 510 |
</div>
|
templates/admin_settings.html
CHANGED
|
@@ -44,10 +44,107 @@
|
|
| 44 |
gap: 12px;
|
| 45 |
}
|
| 46 |
|
| 47 |
-
|
| 48 |
display: flex;
|
| 49 |
-
gap:
|
| 50 |
align-items: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
.menu-toggle {
|
|
@@ -293,17 +390,53 @@
|
|
| 293 |
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ๋ด ์ด๊ธฐ">โฐ</button>
|
| 294 |
<div class="header-actions">
|
| 295 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
<
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
</div>
|
| 308 |
</div>
|
| 309 |
|
|
@@ -316,15 +449,25 @@
|
|
| 316 |
</div>
|
| 317 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 318 |
<div class="mobile-menu-items">
|
|
|
|
| 319 |
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฌ์ฉ์ ๊ด๋ฆฌ</a>
|
| 320 |
-
<a href="{{ url_for('main.
|
| 321 |
-
|
|
|
|
|
|
|
| 322 |
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์์ง ํ์ธ</a>
|
| 323 |
-
|
|
|
|
| 324 |
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์ค์ </a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
| 326 |
-
|
| 327 |
-
<
|
| 328 |
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 329 |
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 330 |
</div>
|
|
|
|
| 44 |
gap: 12px;
|
| 45 |
}
|
| 46 |
|
| 47 |
+
.header-actions {
|
| 48 |
display: flex;
|
| 49 |
+
gap: 8px;
|
| 50 |
align-items: center;
|
| 51 |
+
flex-wrap: wrap;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* ๋๋กญ๋ค์ด ๋ฉ๋ด ์คํ์ผ */
|
| 55 |
+
.dropdown {
|
| 56 |
+
position: relative;
|
| 57 |
+
display: inline-block;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* ๋ฒํผ๊ณผ ๋ฉ๋ด ์ฌ์ด 'ํ'์์ hover๊ฐ ๋๊ฒจ ๋ฉ๋ด๊ฐ ๋ซํ๋ ํ์ ๋ฐฉ์ง */
|
| 61 |
+
.dropdown::after {
|
| 62 |
+
content: '';
|
| 63 |
+
position: absolute;
|
| 64 |
+
left: 0;
|
| 65 |
+
right: 0;
|
| 66 |
+
top: 100%;
|
| 67 |
+
height: 8px;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.dropdown-toggle {
|
| 71 |
+
padding: 8px 16px;
|
| 72 |
+
background: #f1f3f4;
|
| 73 |
+
color: #202124;
|
| 74 |
+
border: none;
|
| 75 |
+
border-radius: 6px;
|
| 76 |
+
font-size: 14px;
|
| 77 |
+
font-weight: 500;
|
| 78 |
+
cursor: pointer;
|
| 79 |
+
transition: all 0.2s;
|
| 80 |
+
display: flex;
|
| 81 |
+
align-items: center;
|
| 82 |
+
gap: 6px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.dropdown-toggle:hover {
|
| 86 |
+
background: #e8eaed;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.dropdown-toggle::after {
|
| 90 |
+
content: 'โผ';
|
| 91 |
+
font-size: 10px;
|
| 92 |
+
transition: transform 0.2s;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.dropdown:hover .dropdown-toggle::after {
|
| 96 |
+
transform: rotate(180deg);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.dropdown-menu {
|
| 100 |
+
position: absolute;
|
| 101 |
+
top: calc(100% + 4px);
|
| 102 |
+
left: 0;
|
| 103 |
+
margin-top: 0;
|
| 104 |
+
background: white;
|
| 105 |
+
border: 1px solid #dadce0;
|
| 106 |
+
border-radius: 6px;
|
| 107 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 108 |
+
min-width: 200px;
|
| 109 |
+
opacity: 0;
|
| 110 |
+
visibility: hidden;
|
| 111 |
+
transform: translateY(-8px);
|
| 112 |
+
transition: all 0.2s ease;
|
| 113 |
+
z-index: 10000;
|
| 114 |
+
padding: 4px 0;
|
| 115 |
+
pointer-events: none;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.dropdown:hover .dropdown-menu {
|
| 119 |
+
opacity: 1;
|
| 120 |
+
visibility: visible;
|
| 121 |
+
transform: translateY(0);
|
| 122 |
+
pointer-events: auto;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
.dropdown-item {
|
| 128 |
+
display: block;
|
| 129 |
+
padding: 10px 16px;
|
| 130 |
+
color: #202124;
|
| 131 |
+
text-decoration: none;
|
| 132 |
+
font-size: 14px;
|
| 133 |
+
transition: background 0.2s;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.dropdown-item:hover {
|
| 137 |
+
background: #f8f9fa;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.dropdown-item:first-child {
|
| 141 |
+
border-top-left-radius: 6px;
|
| 142 |
+
border-top-right-radius: 6px;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.dropdown-item:last-child {
|
| 146 |
+
border-bottom-left-radius: 6px;
|
| 147 |
+
border-bottom-right-radius: 6px;
|
| 148 |
}
|
| 149 |
|
| 150 |
.menu-toggle {
|
|
|
|
| 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 |
+
</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 |
|
|
|
|
| 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 |
+
|
| 467 |
+
<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>
|
| 468 |
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
| 469 |
+
|
| 470 |
+
<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>
|
| 471 |
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 472 |
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 473 |
</div>
|
templates/admin_tags.html
ADDED
|
@@ -0,0 +1,820 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ko">
|
| 3 |
+
<head>
|
| 4 |
+
<script type="text/javascript">
|
| 5 |
+
(function(c,l,a,r,i,t,y){
|
| 6 |
+
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
| 7 |
+
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
| 8 |
+
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
| 9 |
+
})(window, document, "clarity", "script", "ujskfvh0bu");
|
| 10 |
+
</script>
|
| 11 |
+
<meta charset="UTF-8">
|
| 12 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 13 |
+
<title>ํ๊ทธ ๋ณด๊ธฐ - SOY NV AI</title>
|
| 14 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
|
| 15 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
| 16 |
+
<style>
|
| 17 |
+
* {
|
| 18 |
+
margin: 0;
|
| 19 |
+
padding: 0;
|
| 20 |
+
box-sizing: border-box;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
body {
|
| 24 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 25 |
+
background: #f8f9fa;
|
| 26 |
+
color: #202124;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.header {
|
| 30 |
+
background: white;
|
| 31 |
+
border-bottom: 1px solid #dadce0;
|
| 32 |
+
padding: 16px 24px;
|
| 33 |
+
display: flex;
|
| 34 |
+
align-items: center;
|
| 35 |
+
justify-content: space-between;
|
| 36 |
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.header-title {
|
| 40 |
+
font-size: 20px;
|
| 41 |
+
font-weight: 500;
|
| 42 |
+
display: flex;
|
| 43 |
+
align-items: center;
|
| 44 |
+
gap: 12px;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.header-actions {
|
| 48 |
+
display: flex;
|
| 49 |
+
gap: 8px;
|
| 50 |
+
align-items: center;
|
| 51 |
+
flex-wrap: wrap;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* ๋๋กญ๋ค์ด ๋ฉ๋ด ์คํ์ผ */
|
| 55 |
+
.dropdown {
|
| 56 |
+
position: relative;
|
| 57 |
+
display: inline-block;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* ๋ฒํผ๊ณผ ๋ฉ๋ด ์ฌ์ด 'ํ'์์ hover๊ฐ ๋๊ฒจ ๋ฉ๋ด๊ฐ ๋ซํ๋ ํ์ ๋ฐฉ์ง */
|
| 61 |
+
.dropdown::after {
|
| 62 |
+
content: '';
|
| 63 |
+
position: absolute;
|
| 64 |
+
left: 0;
|
| 65 |
+
right: 0;
|
| 66 |
+
top: 100%;
|
| 67 |
+
height: 8px;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.dropdown-toggle {
|
| 71 |
+
padding: 8px 16px;
|
| 72 |
+
background: #f1f3f4;
|
| 73 |
+
color: #202124;
|
| 74 |
+
border: none;
|
| 75 |
+
border-radius: 6px;
|
| 76 |
+
font-size: 14px;
|
| 77 |
+
font-weight: 500;
|
| 78 |
+
cursor: pointer;
|
| 79 |
+
transition: all 0.2s;
|
| 80 |
+
display: flex;
|
| 81 |
+
align-items: center;
|
| 82 |
+
gap: 6px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.dropdown-toggle:hover {
|
| 86 |
+
background: #e8eaed;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.dropdown-toggle::after {
|
| 90 |
+
content: 'โผ';
|
| 91 |
+
font-size: 10px;
|
| 92 |
+
transition: transform 0.2s;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.dropdown:hover .dropdown-toggle::after {
|
| 96 |
+
transform: rotate(180deg);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.dropdown-menu {
|
| 100 |
+
position: absolute;
|
| 101 |
+
top: calc(100% + 4px);
|
| 102 |
+
left: 0;
|
| 103 |
+
margin-top: 0;
|
| 104 |
+
background: white;
|
| 105 |
+
border: 1px solid #dadce0;
|
| 106 |
+
border-radius: 6px;
|
| 107 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 108 |
+
min-width: 200px;
|
| 109 |
+
opacity: 0;
|
| 110 |
+
visibility: hidden;
|
| 111 |
+
transform: translateY(-8px);
|
| 112 |
+
transition: all 0.2s ease;
|
| 113 |
+
z-index: 10000;
|
| 114 |
+
padding: 4px 0;
|
| 115 |
+
pointer-events: none;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.dropdown:hover .dropdown-menu {
|
| 119 |
+
opacity: 1;
|
| 120 |
+
visibility: visible;
|
| 121 |
+
transform: translateY(0);
|
| 122 |
+
pointer-events: auto;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
.dropdown-item {
|
| 128 |
+
display: block;
|
| 129 |
+
padding: 10px 16px;
|
| 130 |
+
color: #202124;
|
| 131 |
+
text-decoration: none;
|
| 132 |
+
font-size: 14px;
|
| 133 |
+
transition: background 0.2s;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.dropdown-item:hover {
|
| 137 |
+
background: #f8f9fa;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.dropdown-item:first-child {
|
| 141 |
+
border-top-left-radius: 6px;
|
| 142 |
+
border-top-right-radius: 6px;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.dropdown-item:last-child {
|
| 146 |
+
border-bottom-left-radius: 6px;
|
| 147 |
+
border-bottom-right-radius: 6px;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.menu-toggle {
|
| 151 |
+
display: none;
|
| 152 |
+
background: none;
|
| 153 |
+
border: none;
|
| 154 |
+
font-size: 24px;
|
| 155 |
+
cursor: pointer;
|
| 156 |
+
padding: 8px;
|
| 157 |
+
color: #202124;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.mobile-menu {
|
| 161 |
+
display: none;
|
| 162 |
+
position: fixed;
|
| 163 |
+
top: 0;
|
| 164 |
+
left: 0;
|
| 165 |
+
right: 0;
|
| 166 |
+
bottom: 0;
|
| 167 |
+
background: rgba(0, 0, 0, 0.5);
|
| 168 |
+
z-index: 1000;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.mobile-menu.active {
|
| 172 |
+
display: block;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.mobile-menu-content {
|
| 176 |
+
position: fixed;
|
| 177 |
+
top: 0;
|
| 178 |
+
right: -100%;
|
| 179 |
+
width: 280px;
|
| 180 |
+
max-width: 80%;
|
| 181 |
+
height: 100%;
|
| 182 |
+
background: white;
|
| 183 |
+
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
| 184 |
+
transition: right 0.3s ease;
|
| 185 |
+
overflow-y: auto;
|
| 186 |
+
z-index: 1001;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.mobile-menu.active .mobile-menu-content {
|
| 190 |
+
right: 0;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.mobile-menu-header {
|
| 194 |
+
padding: 16px 20px;
|
| 195 |
+
border-bottom: 1px solid #dadce0;
|
| 196 |
+
display: flex;
|
| 197 |
+
justify-content: space-between;
|
| 198 |
+
align-items: center;
|
| 199 |
+
background: white;
|
| 200 |
+
position: sticky;
|
| 201 |
+
top: 0;
|
| 202 |
+
z-index: 10;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.mobile-menu-title {
|
| 206 |
+
font-size: 18px;
|
| 207 |
+
font-weight: 500;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.mobile-menu-close {
|
| 211 |
+
background: none;
|
| 212 |
+
border: none;
|
| 213 |
+
font-size: 24px;
|
| 214 |
+
cursor: pointer;
|
| 215 |
+
color: #202124;
|
| 216 |
+
padding: 0;
|
| 217 |
+
width: 32px;
|
| 218 |
+
height: 32px;
|
| 219 |
+
display: flex;
|
| 220 |
+
align-items: center;
|
| 221 |
+
justify-content: center;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.mobile-menu-items {
|
| 225 |
+
padding: 8px 0;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.mobile-menu-item {
|
| 229 |
+
display: block;
|
| 230 |
+
padding: 12px 20px;
|
| 231 |
+
color: #202124;
|
| 232 |
+
text-decoration: none;
|
| 233 |
+
border-bottom: 1px solid #f1f3f4;
|
| 234 |
+
transition: background 0.2s;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.mobile-menu-item:hover {
|
| 238 |
+
background: #f8f9fa;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.mobile-menu-user {
|
| 242 |
+
padding: 16px 20px;
|
| 243 |
+
border-bottom: 1px solid #dadce0;
|
| 244 |
+
color: #5f6368;
|
| 245 |
+
font-size: 14px;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.btn {
|
| 249 |
+
padding: 8px 16px;
|
| 250 |
+
border: none;
|
| 251 |
+
border-radius: 6px;
|
| 252 |
+
font-size: 14px;
|
| 253 |
+
font-weight: 500;
|
| 254 |
+
cursor: pointer;
|
| 255 |
+
text-decoration: none;
|
| 256 |
+
display: inline-block;
|
| 257 |
+
transition: all 0.2s;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.btn-secondary {
|
| 261 |
+
background: #f1f3f4;
|
| 262 |
+
color: #202124;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.btn-secondary:hover {
|
| 266 |
+
background: #e8eaed;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.container {
|
| 270 |
+
max-width: 1600px;
|
| 271 |
+
margin: 0 auto;
|
| 272 |
+
padding: 24px;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.file-selector {
|
| 276 |
+
background: white;
|
| 277 |
+
border-radius: 8px;
|
| 278 |
+
padding: 20px;
|
| 279 |
+
margin-bottom: 24px;
|
| 280 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.file-selector label {
|
| 284 |
+
display: block;
|
| 285 |
+
font-size: 14px;
|
| 286 |
+
font-weight: 500;
|
| 287 |
+
margin-bottom: 12px;
|
| 288 |
+
color: #202124;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.file-selector select {
|
| 292 |
+
width: 100%;
|
| 293 |
+
padding: 10px 12px;
|
| 294 |
+
border: 1px solid #dadce0;
|
| 295 |
+
border-radius: 6px;
|
| 296 |
+
font-size: 14px;
|
| 297 |
+
font-family: inherit;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.file-selector select:focus {
|
| 301 |
+
outline: none;
|
| 302 |
+
border-color: #1a73e8;
|
| 303 |
+
box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1);
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.tags-container {
|
| 307 |
+
background: white;
|
| 308 |
+
border-radius: 8px;
|
| 309 |
+
padding: 24px;
|
| 310 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
| 311 |
+
display: flex;
|
| 312 |
+
gap: 24px;
|
| 313 |
+
min-height: 400px;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.tags-column {
|
| 317 |
+
flex: 1;
|
| 318 |
+
min-width: 0;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.tags-column-title {
|
| 322 |
+
font-size: 18px;
|
| 323 |
+
font-weight: 600;
|
| 324 |
+
margin-bottom: 16px;
|
| 325 |
+
padding-bottom: 8px;
|
| 326 |
+
border-bottom: 2px solid;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.tags-column-title.simple {
|
| 330 |
+
color: #4caf50;
|
| 331 |
+
border-bottom-color: #c8e6c9;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
.tags-column-title.detailed {
|
| 335 |
+
color: #673ab7;
|
| 336 |
+
border-bottom-color: #d1c4e9;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.divider {
|
| 340 |
+
width: 1px;
|
| 341 |
+
background: #e8eaed;
|
| 342 |
+
flex-shrink: 0;
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
.loading {
|
| 346 |
+
text-align: center;
|
| 347 |
+
padding: 24px;
|
| 348 |
+
color: #5f6368;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
.empty {
|
| 352 |
+
text-align: center;
|
| 353 |
+
padding: 20px;
|
| 354 |
+
color: #5f6368;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.error {
|
| 358 |
+
text-align: center;
|
| 359 |
+
padding: 20px;
|
| 360 |
+
color: #c5221f;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
@media (max-width: 768px) {
|
| 364 |
+
.header-actions {
|
| 365 |
+
display: none;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.menu-toggle {
|
| 369 |
+
display: block;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.tags-container {
|
| 373 |
+
flex-direction: column;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
.divider {
|
| 377 |
+
width: 100%;
|
| 378 |
+
height: 1px;
|
| 379 |
+
}
|
| 380 |
+
}
|
| 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 |
+
</div>
|
| 426 |
+
</div>
|
| 427 |
+
|
| 428 |
+
{# ํธ์๊ธฐ๋ฅ #}
|
| 429 |
+
<div class="dropdown">
|
| 430 |
+
<button type="button" class="dropdown-toggle">ํธ์๊ธฐ๋ฅ</button>
|
| 431 |
+
<div class="dropdown-menu">
|
| 432 |
+
<a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์ ํธ</a>
|
| 433 |
+
</div>
|
| 434 |
+
</div>
|
| 435 |
+
|
| 436 |
+
{# ๋ฉ์ธ์ผ๋ก #}
|
| 437 |
+
<a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ์ธ์ผ๋ก</a>
|
| 438 |
+
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋ก๊ทธ์์</a>
|
| 439 |
+
</div>
|
| 440 |
+
</div>
|
| 441 |
+
|
| 442 |
+
<!-- ๋ชจ๋ฐ์ผ ๋ฉ๋ด -->
|
| 443 |
+
<div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
|
| 444 |
+
<div class="mobile-menu-content" onclick="event.stopPropagation()">
|
| 445 |
+
<div class="mobile-menu-header">
|
| 446 |
+
<div class="mobile-menu-title">๋ฉ๋ด</div>
|
| 447 |
+
<button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="๋ฉ๋ด ๋ซ๊ธฐ">×</button>
|
| 448 |
+
</div>
|
| 449 |
+
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 450 |
+
<div class="mobile-menu-items">
|
| 451 |
+
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์ฌ์ดํธ ๊ด๋ฆฌ</div>
|
| 452 |
+
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฌ์ฉ์ ๊ด๋ฆฌ</a>
|
| 453 |
+
<a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ ํฐ ํต๊ณ</a>
|
| 454 |
+
|
| 455 |
+
<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>
|
| 456 |
+
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์น์์ค ์
๋ก๋ ๋ฐ ๊ด๋ฆฌ</a>
|
| 457 |
+
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์์ง ํ์ธ</a>
|
| 458 |
+
|
| 459 |
+
<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>
|
| 460 |
+
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์ค์ </a>
|
| 461 |
+
<a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๋กฌํํธ ๊ด๋ฆฌ</a>
|
| 462 |
+
|
| 463 |
+
<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>
|
| 464 |
+
<a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๊ทธ/ํ๋กฌํํธ</a>
|
| 465 |
+
|
| 466 |
+
<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>
|
| 467 |
+
<a href="{{ url_for('main.admin_utils') }}" 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.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 471 |
+
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 472 |
+
</div>
|
| 473 |
+
</div>
|
| 474 |
+
</div>
|
| 475 |
+
|
| 476 |
+
<div class="container">
|
| 477 |
+
<div class="file-selector">
|
| 478 |
+
<label for="fileSelect">ํ์ผ ์ ํ:</label>
|
| 479 |
+
<select id="fileSelect" onchange="loadTags()">
|
| 480 |
+
<option value="">ํ์ผ์ ์ ํํ์ธ์...</option>
|
| 481 |
+
</select>
|
| 482 |
+
</div>
|
| 483 |
+
|
| 484 |
+
<div class="tags-container">
|
| 485 |
+
<!-- ์ข์ธก: ์ผ๋ฐ ํ๊ทธ -->
|
| 486 |
+
<div class="tags-column" id="simpleTagsColumn">
|
| 487 |
+
<div class="tags-column-title simple">์ผ๋ฐ ํ๊ทธ</div>
|
| 488 |
+
<div id="simpleTagsContent" class="loading">ํ์ผ์ ์ ํํ๋ฉด ํ๊ทธ๊ฐ ํ์๋ฉ๋๋ค.</div>
|
| 489 |
+
</div>
|
| 490 |
+
<!-- ๊ตฌ๋ถ์ -->
|
| 491 |
+
<div class="divider"></div>
|
| 492 |
+
<!-- ์ฐ์ธก: ์์ธ ํ๊ทธ -->
|
| 493 |
+
<div class="tags-column" id="detailedTagsColumn">
|
| 494 |
+
<div class="tags-column-title detailed">์์ธ ํ๊ทธ</div>
|
| 495 |
+
<div id="detailedTagsContent" class="loading">ํ์ผ์ ์ ํํ๋ฉด ํ๊ทธ๊ฐ ํ์๋ฉ๋๋ค.</div>
|
| 496 |
+
</div>
|
| 497 |
+
</div>
|
| 498 |
+
</div>
|
| 499 |
+
|
| 500 |
+
<script>
|
| 501 |
+
function toggleMobileMenu() {
|
| 502 |
+
const menu = document.getElementById('mobileMenu');
|
| 503 |
+
menu.classList.toggle('active');
|
| 504 |
+
document.body.style.overflow = menu.classList.contains('active') ? 'hidden' : '';
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
function closeMobileMenu() {
|
| 508 |
+
const menu = document.getElementById('mobileMenu');
|
| 509 |
+
menu.classList.remove('active');
|
| 510 |
+
document.body.style.overflow = '';
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
function closeMobileMenuOnBackdrop(event) {
|
| 514 |
+
if (event.target.id === 'mobileMenu') {
|
| 515 |
+
closeMobileMenu();
|
| 516 |
+
}
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
function escapeHtml(text) {
|
| 520 |
+
const div = document.createElement('div');
|
| 521 |
+
div.textContent = text;
|
| 522 |
+
return div.innerHTML;
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
async function loadFiles() {
|
| 526 |
+
const select = document.getElementById('fileSelect');
|
| 527 |
+
try {
|
| 528 |
+
const response = await fetch('/api/files', { credentials: 'include' });
|
| 529 |
+
if (!response.ok) throw new Error('ํ์ผ ๋ชฉ๋ก์ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.');
|
| 530 |
+
|
| 531 |
+
const data = await response.json();
|
| 532 |
+
const files = data.files || [];
|
| 533 |
+
|
| 534 |
+
// ๊ธฐ์กด ์ต์
์ ๊ฑฐ (์ฒซ ๋ฒ์งธ "ํ์ผ์ ์ ํํ์ธ์..." ์ ์ธ)
|
| 535 |
+
while (select.options.length > 1) {
|
| 536 |
+
select.remove(1);
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
files.forEach(file => {
|
| 540 |
+
const option = document.createElement('option');
|
| 541 |
+
option.value = file.id;
|
| 542 |
+
option.textContent = file.original_filename;
|
| 543 |
+
select.appendChild(option);
|
| 544 |
+
});
|
| 545 |
+
} catch (error) {
|
| 546 |
+
console.error('ํ์ผ ๋ชฉ๋ก ๋ก๋ ์ค๋ฅ:', error);
|
| 547 |
+
}
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
async function loadTags() {
|
| 551 |
+
const select = document.getElementById('fileSelect');
|
| 552 |
+
const fileId = select.value;
|
| 553 |
+
const simpleContent = document.getElementById('simpleTagsContent');
|
| 554 |
+
const detailedContent = document.getElementById('detailedTagsContent');
|
| 555 |
+
|
| 556 |
+
if (!fileId) {
|
| 557 |
+
simpleContent.className = 'loading';
|
| 558 |
+
simpleContent.textContent = 'ํ์ผ์ ์ ํํ๋ฉด ํ๊ทธ๊ฐ ํ์๋ฉ๋๋ค.';
|
| 559 |
+
detailedContent.className = 'loading';
|
| 560 |
+
detailedContent.textContent = 'ํ์ผ์ ์ ํํ๋ฉด ํ๊ทธ๊ฐ ํ์๋ฉ๋๋ค.';
|
| 561 |
+
return;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
simpleContent.className = 'loading';
|
| 565 |
+
simpleContent.textContent = 'ํ๊ทธ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...';
|
| 566 |
+
detailedContent.className = 'loading';
|
| 567 |
+
detailedContent.textContent = 'ํ๊ทธ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...';
|
| 568 |
+
|
| 569 |
+
try {
|
| 570 |
+
const response = await fetch('/api/files', { credentials: 'include' });
|
| 571 |
+
if (!response.ok) throw new Error('ํ์ผ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.');
|
| 572 |
+
|
| 573 |
+
const data = await response.json();
|
| 574 |
+
const file = data.files.find(f => f.id == fileId);
|
| 575 |
+
|
| 576 |
+
if (file && file.tags) {
|
| 577 |
+
try {
|
| 578 |
+
const tagsData = JSON.parse(file.tags);
|
| 579 |
+
const tagType = tagsData.type || 'legacy';
|
| 580 |
+
|
| 581 |
+
// ์ผ๋ฐ ํ๊ทธ์ ์์ธ ํ๊ทธ ๋ถ๋ฆฌ
|
| 582 |
+
let simpleTags = null;
|
| 583 |
+
let detailedTags = null;
|
| 584 |
+
|
| 585 |
+
if (tagType === 'simple') {
|
| 586 |
+
simpleTags = tagsData.tags || tagsData;
|
| 587 |
+
detailedTags = tagsData.detailed_tags || null;
|
| 588 |
+
} else if (tagType === 'detailed') {
|
| 589 |
+
detailedTags = tagsData.tags || tagsData;
|
| 590 |
+
simpleTags = tagsData.simple_tags || null;
|
| 591 |
+
} else {
|
| 592 |
+
detailedTags = tagsData;
|
| 593 |
+
simpleTags = null;
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
// ์ผ๋ฐ ํ๊ทธ ๋ ๋๋ง
|
| 597 |
+
let simpleHtml = '';
|
| 598 |
+
if (simpleTags) {
|
| 599 |
+
simpleHtml = renderAllTagSections(simpleTags, 'simple');
|
| 600 |
+
} else {
|
| 601 |
+
simpleHtml = '<div class="empty">์ผ๋ฐ ํ๊ทธ๊ฐ ์์ฑ๋์ง ์์์ต๋๋ค.</div>';
|
| 602 |
+
}
|
| 603 |
+
simpleContent.className = '';
|
| 604 |
+
simpleContent.innerHTML = simpleHtml;
|
| 605 |
+
|
| 606 |
+
// ์์ธ ํ๊ทธ ๋ ๋๋ง
|
| 607 |
+
let detailedHtml = '';
|
| 608 |
+
if (detailedTags) {
|
| 609 |
+
detailedHtml = renderAllTagSections(detailedTags, 'detailed');
|
| 610 |
+
} else {
|
| 611 |
+
detailedHtml = '<div class="empty">์์ธ ํ๊ทธ๊ฐ ์์ฑ๋์ง ์์์ต๋๋ค.</div>';
|
| 612 |
+
}
|
| 613 |
+
detailedContent.className = '';
|
| 614 |
+
detailedContent.innerHTML = detailedHtml;
|
| 615 |
+
|
| 616 |
+
} catch (e) {
|
| 617 |
+
console.error('ํ๊ทธ ํ์ฑ ์ค๋ฅ:', e);
|
| 618 |
+
simpleContent.className = 'error';
|
| 619 |
+
simpleContent.textContent = 'ํ๊ทธ ๋ฐ์ดํฐ๋ฅผ ํ์ฑํ ์ ์์ต๋๋ค.';
|
| 620 |
+
detailedContent.className = 'error';
|
| 621 |
+
detailedContent.textContent = 'ํ๊ทธ ๋ฐ์ดํฐ๋ฅผ ํ์ฑํ ์ ์์ต๋๋ค.';
|
| 622 |
+
}
|
| 623 |
+
} else {
|
| 624 |
+
simpleContent.className = 'empty';
|
| 625 |
+
simpleContent.textContent = '์์ฑ๋ ํ๊ทธ๊ฐ ์์ต๋๋ค.';
|
| 626 |
+
detailedContent.className = 'empty';
|
| 627 |
+
detailedContent.textContent = '์์ฑ๋ ํ๊ทธ๊ฐ ์์ต๋๋ค.';
|
| 628 |
+
}
|
| 629 |
+
} catch (error) {
|
| 630 |
+
console.error('ํ๊ทธ ๋ก๋ ์ค๋ฅ:', error);
|
| 631 |
+
simpleContent.className = 'error';
|
| 632 |
+
simpleContent.textContent = 'ํ๊ทธ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.';
|
| 633 |
+
detailedContent.className = 'error';
|
| 634 |
+
detailedContent.textContent = 'ํ๊ทธ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.';
|
| 635 |
+
}
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
// ๋ชจ๋ ํ๊ทธ ์น์
๋ ๋๋ง ํจ์
|
| 639 |
+
function renderAllTagSections(tags, tagType) {
|
| 640 |
+
let html = '';
|
| 641 |
+
const tagColor = tagType === 'simple' ? '#4caf50' : '#673ab7';
|
| 642 |
+
const tagBgColor = tagType === 'simple' ? '#e8f5e9' : '#ede7f6';
|
| 643 |
+
const tagBorderColor = tagType === 'simple' ? '#c8e6c9' : '#d1c4e9';
|
| 644 |
+
|
| 645 |
+
// 1. Parent Chunk
|
| 646 |
+
if (tags.parent_chunk) {
|
| 647 |
+
html += createTagSection('Parent Chunk', tags.parent_chunk, {
|
| 648 |
+
'world_view': '์ธ๊ณ๊ด',
|
| 649 |
+
'characters': '์ธ๋ฌผ',
|
| 650 |
+
'story': '์ด์ผ๊ธฐ',
|
| 651 |
+
'others': '๊ธฐํ'
|
| 652 |
+
}, tagColor, tagBgColor, tagBorderColor);
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
// 2. Episodes (ํ์ฐจ๋ณ)
|
| 656 |
+
if (tags.episodes) {
|
| 657 |
+
html += createTagSection('ํ์ฐจ๋ณ', tags.episodes, {
|
| 658 |
+
'story': '์ด์ผ๊ธฐ',
|
| 659 |
+
'events': '์ฌ๊ฑด',
|
| 660 |
+
'characters': '์ธ๋ฌผ',
|
| 661 |
+
'relationships_change': '์ธ๋ฌผ๊ฐ ๋ณํ',
|
| 662 |
+
'appearance': '์ธ๋ชจ',
|
| 663 |
+
'clothing': '์๋ณต',
|
| 664 |
+
'items': '์์ดํ
/์์ฌ',
|
| 665 |
+
'others': '๊ธฐํ'
|
| 666 |
+
}, tagColor, tagBgColor, tagBorderColor);
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
// 3. GraphRAG Total
|
| 670 |
+
if (tags.graph_rag_total) {
|
| 671 |
+
html += createTagSection('GraphRAG (์ ์ฒด)', tags.graph_rag_total, {
|
| 672 |
+
'characters': '์ธ๋ฌผ',
|
| 673 |
+
'locations': '์ฅ์',
|
| 674 |
+
'relationships': '๊ด๊ณ',
|
| 675 |
+
'events': '์ฌ๏ฟฝ๏ฟฝ'
|
| 676 |
+
}, tagColor, tagBgColor, tagBorderColor);
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
// 4. GraphRAG by Episode
|
| 680 |
+
if (tags.graph_rag_by_episode) {
|
| 681 |
+
html += createTagSection('GraphRAG (ํ์ฐจ๋ณ)', tags.graph_rag_by_episode, {
|
| 682 |
+
'characters': '์ธ๋ฌผ',
|
| 683 |
+
'locations': '์ฅ์',
|
| 684 |
+
'relationships': '๊ด๊ณ',
|
| 685 |
+
'events': '์ฌ๊ฑด'
|
| 686 |
+
}, tagColor, tagBgColor, tagBorderColor);
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
// 5. GraphRAG by Character
|
| 690 |
+
if (tags.graph_rag_by_character) {
|
| 691 |
+
html += createTagSection('GraphRAG (์ธ๋ฌผ๋ณ)', tags.graph_rag_by_character, {
|
| 692 |
+
'characters': '์ธ๋ฌผ',
|
| 693 |
+
'locations': '์ฅ์',
|
| 694 |
+
'relationships': '๊ด๊ณ',
|
| 695 |
+
'events': '์ฌ๊ฑด'
|
| 696 |
+
}, tagColor, tagBgColor, tagBorderColor);
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
// 6. GraphRAG by Event
|
| 700 |
+
if (tags.graph_rag_by_event) {
|
| 701 |
+
html += createTagSection('GraphRAG (์ฌ๊ฑด๋ณ)', tags.graph_rag_by_event, {
|
| 702 |
+
'characters': '์ธ๋ฌผ',
|
| 703 |
+
'locations': '์ฅ์',
|
| 704 |
+
'relationships': '๊ด๊ณ',
|
| 705 |
+
'events': '์ฌ๊ฑด'
|
| 706 |
+
}, tagColor, tagBgColor, tagBorderColor);
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
// 7. GraphRAG (์์ธ)
|
| 710 |
+
if (tags.graph_rag_detail) {
|
| 711 |
+
html += createTagSection('GraphRAG (์์ธ)', tags.graph_rag_detail, {
|
| 712 |
+
'person_person_relationships_chronological': '์ธ๋ฌผ-์ธ๋ฌผ ๊ด๊ณ (์๊ฐ ์์ผ๋ก ๋์ด)',
|
| 713 |
+
'event_person_relationships': '์ฌ๊ฑด-์ธ๋ฌผ๋ค ๊ด๊ณ'
|
| 714 |
+
}, tagColor, tagBgColor, tagBorderColor);
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
// ๊ตฌ๋ฒ์ ํ๊ทธ ํธํ์ฑ
|
| 718 |
+
if (tags.graph_rag && !tags.graph_rag_total) {
|
| 719 |
+
html += createTagSection('GraphRAG (๊ตฌ๋ฒ์ )', tags.graph_rag, {
|
| 720 |
+
'characters': '์ธ๋ฌผ',
|
| 721 |
+
'relationships': '๊ด๊ณ',
|
| 722 |
+
'events': '์ฌ๊ฑด'
|
| 723 |
+
}, tagColor, tagBgColor, tagBorderColor);
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
// ๋ฐฐ์ด ํธํ์ฑ (์์ ๊ตฌ๋ฒ์ )
|
| 727 |
+
if (Array.isArray(tags)) {
|
| 728 |
+
html += '<div style="display: flex; flex-wrap: wrap; gap: 8px;">';
|
| 729 |
+
tags.forEach(tag => {
|
| 730 |
+
html += `<span style="background: ${tagBgColor}; color: ${tagColor}; padding: 6px 12px; border-radius: 16px; font-size: 13px; border: 1px solid ${tagBorderColor};">#${escapeHtml(tag)}</span>`;
|
| 731 |
+
});
|
| 732 |
+
html += '</div>';
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
if (!html) {
|
| 736 |
+
html = '<div class="empty">ํ๊ทธ ํ์์ด ์ฌ๋ฐ๋ฅด์ง ์๊ฑฐ๋ ๋น์ด์์ต๋๋ค.</div>';
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
return html;
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
// ํ๊ทธ ์น์
์์ฑ ํฌํผ ํจ์
|
| 743 |
+
function createTagSection(title, groupData, keyMap, titleColor = '#1a73e8', bgColor = '#e8f0fe', borderColor = '#d2e3fc') {
|
| 744 |
+
// ๋น ์น์
์ฒดํฌ
|
| 745 |
+
let hasContent = false;
|
| 746 |
+
for (const key of Object.keys(groupData)) {
|
| 747 |
+
if (groupData[key] && groupData[key].length > 0) {
|
| 748 |
+
hasContent = true;
|
| 749 |
+
break;
|
| 750 |
+
}
|
| 751 |
+
}
|
| 752 |
+
if (!hasContent) return '';
|
| 753 |
+
|
| 754 |
+
let html = `<div style="margin-bottom: 32px;">`;
|
| 755 |
+
html += `<h3 style="font-size: 16px; font-weight: 700; color: ${titleColor}; margin-bottom: 16px; border-bottom: 2px solid ${borderColor}; padding-bottom: 8px; display: flex; align-items: center;">`;
|
| 756 |
+
html += `<span style="margin-right: 8px;">๐</span> ${escapeHtml(title)}`;
|
| 757 |
+
html += `</h3>`;
|
| 758 |
+
|
| 759 |
+
html += `<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px;">`;
|
| 760 |
+
|
| 761 |
+
// ์ ์๋ ์์๋๋ก ํ์ํ๊ธฐ ์ํด keyMap์ ํค ์์๋ฅผ ๋ฐ๋ฆ (์กด์ฌํ๋ ๊ฒฝ์ฐ)
|
| 762 |
+
// keyMap์ ์๋ ํค๋ ๋ง์ง๋ง์ ์ถ๊ฐ
|
| 763 |
+
const orderedKeys = Object.keys(keyMap);
|
| 764 |
+
const extraKeys = Object.keys(groupData).filter(k => !orderedKeys.includes(k));
|
| 765 |
+
const allKeys = [...orderedKeys, ...extraKeys];
|
| 766 |
+
|
| 767 |
+
for (const key of allKeys) {
|
| 768 |
+
const values = groupData[key];
|
| 769 |
+
if (values && values.length > 0) {
|
| 770 |
+
const label = keyMap[key] || key;
|
| 771 |
+
html += `<div style="background: #f8f9fa; padding: 12px; border-radius: 8px; border: 1px solid #e8eaed;">`;
|
| 772 |
+
html += `<div style="font-size: 13px; font-weight: 700; color: #5f6368; margin-bottom: 8px;">${escapeHtml(label)}</div>`;
|
| 773 |
+
html += `<div style="display: flex; flex-wrap: wrap; gap: 6px;">`;
|
| 774 |
+
values.forEach(tag => {
|
| 775 |
+
// ๊ด๊ณ ํ๊ทธ์ธ ๊ฒฝ์ฐ ํ์ดํ ํ์์ผ๋ก ํ์
|
| 776 |
+
let displayTag = tag;
|
| 777 |
+
const isRelationshipKey = key === 'relationships' ||
|
| 778 |
+
key === 'person_person_relationships_chronological' ||
|
| 779 |
+
key === 'event_person_relationships' ||
|
| 780 |
+
key === 'relationships_change';
|
| 781 |
+
|
| 782 |
+
if (isRelationshipKey) {
|
| 783 |
+
// "์ธ๋ฌผA โ ์ธ๋ฌผB: ๊ด๊ณ์ ํ" ํ์์ด ์ด๋ฏธ ํฌํจ๋์ด ์์ผ๋ฉด ๊ทธ๋๋ก ์ฌ์ฉ
|
| 784 |
+
// "์ธ๋ฌผA-์ธ๋ฌผB(๊ด๊ณ์ ํ)" ํ์์ "์ธ๋ฌผA โ ์ธ๋ฌผB: ๊ด๊ณ์ ํ"์ผ๋ก ๋ณํ
|
| 785 |
+
if (tag.includes('-') && tag.includes('(') && tag.includes(')')) {
|
| 786 |
+
const match = tag.match(/^(.+?)-(.+?)\((.+?)\)/);
|
| 787 |
+
if (match) {
|
| 788 |
+
displayTag = `${match[1]} โ ${match[2]}: ${match[3]}`;
|
| 789 |
+
}
|
| 790 |
+
}
|
| 791 |
+
// ๊ด๊ณ ํ๊ทธ๋ ํน๋ณํ ์คํ์ผ ์ ์ฉ
|
| 792 |
+
html += `<span style="background: #fff3cd; border: 1px solid #ffc107; padding: 4px 8px; border-radius: 4px; font-size: 12px; color: #856404; font-weight: 500;">${escapeHtml(displayTag)}</span>`;
|
| 793 |
+
} else {
|
| 794 |
+
html += `<span style="background: white; border: 1px solid #dadce0; padding: 4px 8px; border-radius: 4px; font-size: 12px; color: #3c4043;">${escapeHtml(displayTag)}</span>`;
|
| 795 |
+
}
|
| 796 |
+
});
|
| 797 |
+
html += `</div></div>`;
|
| 798 |
+
}
|
| 799 |
+
}
|
| 800 |
+
html += '</div></div>';
|
| 801 |
+
return html;
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
// ํ์ด์ง ๋ก๋ ์ ์ด๊ธฐํ
|
| 805 |
+
window.addEventListener('load', () => {
|
| 806 |
+
loadFiles().then(() => {
|
| 807 |
+
// URL ํ๋ผ๋ฏธํฐ์์ file_id ๊ฐ์ ธ์ค๊ธฐ
|
| 808 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 809 |
+
const fileId = urlParams.get('file_id');
|
| 810 |
+
if (fileId) {
|
| 811 |
+
const select = document.getElementById('fileSelect');
|
| 812 |
+
select.value = fileId;
|
| 813 |
+
loadTags();
|
| 814 |
+
}
|
| 815 |
+
});
|
| 816 |
+
});
|
| 817 |
+
</script>
|
| 818 |
+
</body>
|
| 819 |
+
</html>
|
| 820 |
+
|
templates/admin_tokens.html
CHANGED
|
@@ -35,6 +35,9 @@
|
|
| 35 |
align-items: center;
|
| 36 |
justify-content: space-between;
|
| 37 |
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
.header-title {
|
|
@@ -47,8 +50,109 @@
|
|
| 47 |
|
| 48 |
.header-actions {
|
| 49 |
display: flex;
|
| 50 |
-
gap:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
align-items: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
.menu-toggle {
|
|
@@ -348,16 +452,53 @@
|
|
| 348 |
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ๋ด ์ด๊ธฐ">โฐ</button>
|
| 349 |
<div class="header-actions">
|
| 350 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
<
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
</div>
|
| 362 |
</div>
|
| 363 |
|
|
@@ -370,14 +511,25 @@
|
|
| 370 |
</div>
|
| 371 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 372 |
<div class="mobile-menu-items">
|
|
|
|
| 373 |
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฌ์ฉ์ ๊ด๋ฆฌ</a>
|
| 374 |
-
<a href="{{ url_for('main.
|
| 375 |
-
|
|
|
|
|
|
|
| 376 |
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์์ง ํ์ธ</a>
|
| 377 |
-
|
|
|
|
| 378 |
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์ค์ </a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
| 380 |
-
|
|
|
|
| 381 |
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 382 |
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 383 |
</div>
|
|
|
|
| 35 |
align-items: center;
|
| 36 |
justify-content: space-between;
|
| 37 |
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
| 38 |
+
overflow: visible;
|
| 39 |
+
position: relative;
|
| 40 |
+
z-index: 100;
|
| 41 |
}
|
| 42 |
|
| 43 |
.header-title {
|
|
|
|
| 50 |
|
| 51 |
.header-actions {
|
| 52 |
display: flex;
|
| 53 |
+
gap: 8px;
|
| 54 |
+
align-items: center;
|
| 55 |
+
flex-wrap: wrap;
|
| 56 |
+
position: relative;
|
| 57 |
+
z-index: 101;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* ๋๋กญ๋ค์ด ๋ฉ๋ด ์คํ์ผ */
|
| 61 |
+
.dropdown {
|
| 62 |
+
position: relative;
|
| 63 |
+
display: inline-block;
|
| 64 |
+
z-index: 10001;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/* ๋ฒํผ๊ณผ ๋ฉ๋ด ์ฌ์ด 'ํ'์์ hover๊ฐ ๋๊ฒจ ๋ฉ๋ด๊ฐ ๋ซํ๋ ํ์ ๋ฐฉ์ง */
|
| 68 |
+
.dropdown::after {
|
| 69 |
+
content: '';
|
| 70 |
+
position: absolute;
|
| 71 |
+
left: 0;
|
| 72 |
+
right: 0;
|
| 73 |
+
top: 100%;
|
| 74 |
+
height: 8px;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.dropdown-toggle {
|
| 78 |
+
padding: 8px 16px;
|
| 79 |
+
background: #f1f3f4;
|
| 80 |
+
color: #202124;
|
| 81 |
+
border: none;
|
| 82 |
+
border-radius: 6px;
|
| 83 |
+
font-size: 14px;
|
| 84 |
+
font-weight: 500;
|
| 85 |
+
cursor: pointer;
|
| 86 |
+
transition: all 0.2s;
|
| 87 |
+
display: flex;
|
| 88 |
align-items: center;
|
| 89 |
+
gap: 6px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.dropdown-toggle:hover {
|
| 93 |
+
background: #e8eaed;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.dropdown-toggle::after {
|
| 97 |
+
content: 'โผ';
|
| 98 |
+
font-size: 10px;
|
| 99 |
+
transition: transform 0.2s;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.dropdown:hover .dropdown-toggle::after {
|
| 103 |
+
transform: rotate(180deg);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.dropdown-menu {
|
| 107 |
+
position: absolute;
|
| 108 |
+
top: calc(100% + 4px);
|
| 109 |
+
left: 0;
|
| 110 |
+
margin-top: 0;
|
| 111 |
+
background: white;
|
| 112 |
+
border: 1px solid #dadce0;
|
| 113 |
+
border-radius: 6px;
|
| 114 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 115 |
+
min-width: 200px;
|
| 116 |
+
opacity: 0;
|
| 117 |
+
visibility: hidden;
|
| 118 |
+
transform: translateY(-8px);
|
| 119 |
+
transition: all 0.2s ease;
|
| 120 |
+
z-index: 10002;
|
| 121 |
+
padding: 4px 0;
|
| 122 |
+
pointer-events: none;
|
| 123 |
+
white-space: nowrap;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.dropdown:hover .dropdown-menu {
|
| 127 |
+
opacity: 1;
|
| 128 |
+
visibility: visible;
|
| 129 |
+
transform: translateY(0);
|
| 130 |
+
pointer-events: auto;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
.dropdown-item {
|
| 136 |
+
display: block;
|
| 137 |
+
padding: 10px 16px;
|
| 138 |
+
color: #202124;
|
| 139 |
+
text-decoration: none;
|
| 140 |
+
font-size: 14px;
|
| 141 |
+
transition: background 0.2s;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.dropdown-item:hover {
|
| 145 |
+
background: #f8f9fa;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.dropdown-item:first-child {
|
| 149 |
+
border-top-left-radius: 6px;
|
| 150 |
+
border-top-right-radius: 6px;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.dropdown-item:last-child {
|
| 154 |
+
border-bottom-left-radius: 6px;
|
| 155 |
+
border-bottom-right-radius: 6px;
|
| 156 |
}
|
| 157 |
|
| 158 |
.menu-toggle {
|
|
|
|
| 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 |
+
</div>
|
| 489 |
+
</div>
|
| 490 |
+
|
| 491 |
+
{# ํธ์๊ธฐ๋ฅ #}
|
| 492 |
+
<div class="dropdown">
|
| 493 |
+
<button type="button" class="dropdown-toggle">ํธ์๊ธฐ๋ฅ</button>
|
| 494 |
+
<div class="dropdown-menu">
|
| 495 |
+
<a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์ ํธ</a>
|
| 496 |
+
</div>
|
| 497 |
+
</div>
|
| 498 |
+
|
| 499 |
+
{# ๋ฉ์ธ์ผ๋ก #}
|
| 500 |
+
<a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ์ธ์ผ๋ก</a>
|
| 501 |
+
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋ก๊ทธ์์</a>
|
| 502 |
</div>
|
| 503 |
</div>
|
| 504 |
|
|
|
|
| 511 |
</div>
|
| 512 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 513 |
<div class="mobile-menu-items">
|
| 514 |
+
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์ฌ์ดํธ ๊ด๋ฆฌ</div>
|
| 515 |
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฌ์ฉ์ ๊ด๋ฆฌ</a>
|
| 516 |
+
<a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ ํฐ ํต๊ณ</a>
|
| 517 |
+
|
| 518 |
+
<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>
|
| 519 |
+
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์น์์ค ์
๋ก๋ ๋ฐ ๊ด๋ฆฌ</a>
|
| 520 |
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์์ง ํ์ธ</a>
|
| 521 |
+
|
| 522 |
+
<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>
|
| 523 |
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์ค์ </a>
|
| 524 |
+
<a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๋กฌํํธ ๊ด๋ฆฌ</a>
|
| 525 |
+
|
| 526 |
+
<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>
|
| 527 |
+
<a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๊ทธ/ํ๋กฌํํธ</a>
|
| 528 |
+
|
| 529 |
+
<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>
|
| 530 |
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
| 531 |
+
|
| 532 |
+
<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>
|
| 533 |
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 534 |
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 535 |
</div>
|
templates/admin_utils.html
CHANGED
|
@@ -44,10 +44,107 @@
|
|
| 44 |
gap: 12px;
|
| 45 |
}
|
| 46 |
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
display: flex;
|
| 49 |
-
gap: 12px;
|
| 50 |
align-items: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
.menu-toggle {
|
|
@@ -376,16 +473,53 @@
|
|
| 376 |
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ๋ด ์ด๊ธฐ">โฐ</button>
|
| 377 |
<div class="header-actions">
|
| 378 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
<
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
</div>
|
| 390 |
</div>
|
| 391 |
|
|
@@ -398,14 +532,25 @@
|
|
| 398 |
</div>
|
| 399 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 400 |
<div class="mobile-menu-items">
|
|
|
|
| 401 |
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฌ์ฉ์ ๊ด๋ฆฌ</a>
|
| 402 |
-
<a href="{{ url_for('main.
|
| 403 |
-
|
|
|
|
|
|
|
| 404 |
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์์ง ํ์ธ</a>
|
| 405 |
-
|
|
|
|
| 406 |
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์ค์ </a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
| 408 |
-
|
|
|
|
| 409 |
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 410 |
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 411 |
</div>
|
|
|
|
| 44 |
gap: 12px;
|
| 45 |
}
|
| 46 |
|
| 47 |
+
.header-actions {
|
| 48 |
+
display: flex;
|
| 49 |
+
gap: 8px;
|
| 50 |
+
align-items: center;
|
| 51 |
+
flex-wrap: wrap;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* ๋๋กญ๋ค์ด ๋ฉ๋ด ์คํ์ผ */
|
| 55 |
+
.dropdown {
|
| 56 |
+
position: relative;
|
| 57 |
+
display: inline-block;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* ๋ฒํผ๊ณผ ๋ฉ๋ด ์ฌ์ด 'ํ'์์ hover๊ฐ ๋๊ฒจ ๋ฉ๋ด๊ฐ ๋ซํ๋ ํ์ ๋ฐฉ์ง */
|
| 61 |
+
.dropdown::after {
|
| 62 |
+
content: '';
|
| 63 |
+
position: absolute;
|
| 64 |
+
left: 0;
|
| 65 |
+
right: 0;
|
| 66 |
+
top: 100%;
|
| 67 |
+
height: 8px;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.dropdown-toggle {
|
| 71 |
+
padding: 8px 16px;
|
| 72 |
+
background: #f1f3f4;
|
| 73 |
+
color: #202124;
|
| 74 |
+
border: none;
|
| 75 |
+
border-radius: 6px;
|
| 76 |
+
font-size: 14px;
|
| 77 |
+
font-weight: 500;
|
| 78 |
+
cursor: pointer;
|
| 79 |
+
transition: all 0.2s;
|
| 80 |
display: flex;
|
|
|
|
| 81 |
align-items: center;
|
| 82 |
+
gap: 6px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.dropdown-toggle:hover {
|
| 86 |
+
background: #e8eaed;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.dropdown-toggle::after {
|
| 90 |
+
content: 'โผ';
|
| 91 |
+
font-size: 10px;
|
| 92 |
+
transition: transform 0.2s;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.dropdown:hover .dropdown-toggle::after {
|
| 96 |
+
transform: rotate(180deg);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.dropdown-menu {
|
| 100 |
+
position: absolute;
|
| 101 |
+
top: calc(100% + 4px);
|
| 102 |
+
left: 0;
|
| 103 |
+
margin-top: 0;
|
| 104 |
+
background: white;
|
| 105 |
+
border: 1px solid #dadce0;
|
| 106 |
+
border-radius: 6px;
|
| 107 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 108 |
+
min-width: 200px;
|
| 109 |
+
opacity: 0;
|
| 110 |
+
visibility: hidden;
|
| 111 |
+
transform: translateY(-8px);
|
| 112 |
+
transition: all 0.2s ease;
|
| 113 |
+
z-index: 10000;
|
| 114 |
+
padding: 4px 0;
|
| 115 |
+
pointer-events: none;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.dropdown:hover .dropdown-menu {
|
| 119 |
+
opacity: 1;
|
| 120 |
+
visibility: visible;
|
| 121 |
+
transform: translateY(0);
|
| 122 |
+
pointer-events: auto;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
.dropdown-item {
|
| 128 |
+
display: block;
|
| 129 |
+
padding: 10px 16px;
|
| 130 |
+
color: #202124;
|
| 131 |
+
text-decoration: none;
|
| 132 |
+
font-size: 14px;
|
| 133 |
+
transition: background 0.2s;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.dropdown-item:hover {
|
| 137 |
+
background: #f8f9fa;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.dropdown-item:first-child {
|
| 141 |
+
border-top-left-radius: 6px;
|
| 142 |
+
border-top-right-radius: 6px;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.dropdown-item:last-child {
|
| 146 |
+
border-bottom-left-radius: 6px;
|
| 147 |
+
border-bottom-right-radius: 6px;
|
| 148 |
}
|
| 149 |
|
| 150 |
.menu-toggle {
|
|
|
|
| 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 |
+
</div>
|
| 510 |
+
</div>
|
| 511 |
+
|
| 512 |
+
{# ํธ์๊ธฐ๋ฅ #}
|
| 513 |
+
<div class="dropdown">
|
| 514 |
+
<button type="button" class="dropdown-toggle">ํธ์๊ธฐ๋ฅ</button>
|
| 515 |
+
<div class="dropdown-menu">
|
| 516 |
+
<a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์ ํธ</a>
|
| 517 |
+
</div>
|
| 518 |
+
</div>
|
| 519 |
+
|
| 520 |
+
{# ๋ฉ์ธ์ผ๋ก #}
|
| 521 |
+
<a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ์ธ์ผ๋ก</a>
|
| 522 |
+
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋ก๊ทธ์์</a>
|
| 523 |
</div>
|
| 524 |
</div>
|
| 525 |
|
|
|
|
| 532 |
</div>
|
| 533 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 534 |
<div class="mobile-menu-items">
|
| 535 |
+
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์ฌ์ดํธ ๊ด๋ฆฌ</div>
|
| 536 |
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฌ์ฉ์ ๊ด๋ฆฌ</a>
|
| 537 |
+
<a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ ํฐ ํต๊ณ</a>
|
| 538 |
+
|
| 539 |
+
<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>
|
| 540 |
+
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์น์์ค ์
๋ก๋ ๋ฐ ๊ด๋ฆฌ</a>
|
| 541 |
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์์ง ํ์ธ</a>
|
| 542 |
+
|
| 543 |
+
<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>
|
| 544 |
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์ค์ </a>
|
| 545 |
+
<a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๋กฌํํธ ๊ด๋ฆฌ</a>
|
| 546 |
+
|
| 547 |
+
<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>
|
| 548 |
+
<a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๊ทธ/ํ๋กฌํํธ</a>
|
| 549 |
+
|
| 550 |
+
<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>
|
| 551 |
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
| 552 |
+
|
| 553 |
+
<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>
|
| 554 |
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 555 |
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 556 |
</div>
|
templates/admin_webnovels.html
CHANGED
|
@@ -35,7 +35,9 @@
|
|
| 35 |
align-items: center;
|
| 36 |
justify-content: space-between;
|
| 37 |
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
| 38 |
-
overflow
|
|
|
|
|
|
|
| 39 |
}
|
| 40 |
|
| 41 |
.header-title {
|
|
@@ -48,9 +50,109 @@
|
|
| 48 |
|
| 49 |
.header-actions {
|
| 50 |
display: flex;
|
| 51 |
-
gap:
|
| 52 |
align-items: center;
|
| 53 |
flex-wrap: wrap;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
}
|
| 55 |
|
| 56 |
.menu-toggle {
|
|
@@ -619,16 +721,53 @@
|
|
| 619 |
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ๋ด ์ด๊ธฐ">โฐ</button>
|
| 620 |
<div class="header-actions">
|
| 621 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
<
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
</div>
|
| 633 |
</div>
|
| 634 |
|
|
@@ -641,14 +780,25 @@
|
|
| 641 |
</div>
|
| 642 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 643 |
<div class="mobile-menu-items">
|
|
|
|
| 644 |
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฌ์ฉ์ ๊ด๋ฆฌ</a>
|
| 645 |
-
<a href="{{ url_for('main.
|
|
|
|
|
|
|
|
|
|
| 646 |
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์์ง ํ์ธ</a>
|
| 647 |
-
|
|
|
|
| 648 |
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์ค์ </a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 649 |
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
| 650 |
-
|
| 651 |
-
<
|
| 652 |
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 653 |
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 654 |
</div>
|
|
@@ -992,44 +1142,87 @@
|
|
| 992 |
const toggleButtonText = isPublic ? '๋น๊ณต๊ฐ๋ก' : '๊ณต๊ฐ๋ก';
|
| 993 |
const toggleButtonClass = isPublic ? 'btn-warning' : 'btn-success';
|
| 994 |
|
| 995 |
-
// ํ๊ทธ ํ์
|
| 996 |
let tagsHtml = '';
|
| 997 |
if (file.tags) {
|
| 998 |
try {
|
| 999 |
-
const
|
| 1000 |
-
|
|
|
|
| 1001 |
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
|
| 1010 |
-
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
}
|
| 1015 |
-
}
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1019 |
});
|
| 1020 |
}
|
| 1021 |
}
|
| 1022 |
|
| 1023 |
-
//
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
|
|
|
|
|
|
|
|
|
| 1029 |
});
|
| 1030 |
-
|
| 1031 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1032 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1033 |
tagsHtml += '</div>';
|
| 1034 |
}
|
| 1035 |
} catch (e) {
|
|
@@ -1058,7 +1251,8 @@
|
|
| 1058 |
`<button class="btn btn-secondary" onclick="createParentChunk(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">Parent Chunk ์์ฑ</button>`
|
| 1059 |
}
|
| 1060 |
<button class="btn btn-info" onclick="createMetadata(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">๋ฉํ๋ฐ์ดํฐ ์์ฑ</button>
|
| 1061 |
-
<button class="btn" style="background: #
|
|
|
|
| 1062 |
<button class="btn btn-primary" onclick="continueUpload(${file.id})" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">์ด์ด์ ์
๋ก๋</button>
|
| 1063 |
<button class="btn btn-secondary" onclick="deleteFile(${file.id})" style="padding: 4px 8px; font-size: 12px;">์ญ์ </button>
|
| 1064 |
</div>
|
|
@@ -1945,9 +2139,9 @@
|
|
| 1945 |
}
|
| 1946 |
}
|
| 1947 |
|
| 1948 |
-
// ํ๊ทธ ์์ฑ
|
| 1949 |
-
async function
|
| 1950 |
-
if (!confirm(`"${fileName}" ํ์ผ์ ํ๊ทธ๋ฅผ ์์ฑํ์๊ฒ ์ต๋๊น?\n\n
|
| 1951 |
return;
|
| 1952 |
}
|
| 1953 |
|
|
@@ -1957,25 +2151,58 @@
|
|
| 1957 |
button.textContent = '์์ฑ ์ค...';
|
| 1958 |
|
| 1959 |
try {
|
| 1960 |
-
const response = await fetch(`/api/files/${fileId}/process/tags`, {
|
| 1961 |
method: 'POST',
|
| 1962 |
credentials: 'include'
|
| 1963 |
});
|
| 1964 |
const data = await response.json();
|
| 1965 |
|
| 1966 |
if (response.ok) {
|
| 1967 |
-
|
| 1968 |
-
|
| 1969 |
-
|
| 1970 |
-
|
| 1971 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1972 |
loadFiles(); // ํ์ผ ๋ชฉ๋ก ์๋ก๊ณ ์นจ
|
| 1973 |
} else {
|
| 1974 |
-
|
|
|
|
| 1975 |
}
|
| 1976 |
} catch (error) {
|
| 1977 |
-
showAlert(
|
| 1978 |
-
console.error('ํ๊ทธ ์์ฑ ์ค๋ฅ:', error);
|
| 1979 |
} finally {
|
| 1980 |
button.disabled = false;
|
| 1981 |
button.textContent = originalText;
|
|
|
|
| 35 |
align-items: center;
|
| 36 |
justify-content: space-between;
|
| 37 |
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
| 38 |
+
overflow: visible;
|
| 39 |
+
position: relative;
|
| 40 |
+
z-index: 100;
|
| 41 |
}
|
| 42 |
|
| 43 |
.header-title {
|
|
|
|
| 50 |
|
| 51 |
.header-actions {
|
| 52 |
display: flex;
|
| 53 |
+
gap: 8px;
|
| 54 |
align-items: center;
|
| 55 |
flex-wrap: wrap;
|
| 56 |
+
position: relative;
|
| 57 |
+
z-index: 101;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* ๋๋กญ๋ค์ด ๋ฉ๋ด ์คํ์ผ */
|
| 61 |
+
.dropdown {
|
| 62 |
+
position: relative;
|
| 63 |
+
display: inline-block;
|
| 64 |
+
z-index: 10001;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/* ๋ฒํผ๊ณผ ๋ฉ๋ด ์ฌ์ด 'ํ'์์ hover๊ฐ ๋๊ฒจ ๋ฉ๋ด๊ฐ ๋ซํ๋ ํ์ ๋ฐฉ์ง */
|
| 68 |
+
.dropdown::after {
|
| 69 |
+
content: '';
|
| 70 |
+
position: absolute;
|
| 71 |
+
left: 0;
|
| 72 |
+
right: 0;
|
| 73 |
+
top: 100%;
|
| 74 |
+
height: 8px;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.dropdown-toggle {
|
| 78 |
+
padding: 8px 16px;
|
| 79 |
+
background: #f1f3f4;
|
| 80 |
+
color: #202124;
|
| 81 |
+
border: none;
|
| 82 |
+
border-radius: 6px;
|
| 83 |
+
font-size: 14px;
|
| 84 |
+
font-weight: 500;
|
| 85 |
+
cursor: pointer;
|
| 86 |
+
transition: all 0.2s;
|
| 87 |
+
display: flex;
|
| 88 |
+
align-items: center;
|
| 89 |
+
gap: 6px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.dropdown-toggle:hover {
|
| 93 |
+
background: #e8eaed;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.dropdown-toggle::after {
|
| 97 |
+
content: 'โผ';
|
| 98 |
+
font-size: 10px;
|
| 99 |
+
transition: transform 0.2s;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.dropdown:hover .dropdown-toggle::after {
|
| 103 |
+
transform: rotate(180deg);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.dropdown-menu {
|
| 107 |
+
position: absolute;
|
| 108 |
+
top: calc(100% + 4px);
|
| 109 |
+
left: 0;
|
| 110 |
+
margin-top: 0;
|
| 111 |
+
background: white;
|
| 112 |
+
border: 1px solid #dadce0;
|
| 113 |
+
border-radius: 6px;
|
| 114 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 115 |
+
min-width: 200px;
|
| 116 |
+
opacity: 0;
|
| 117 |
+
visibility: hidden;
|
| 118 |
+
transform: translateY(-8px);
|
| 119 |
+
transition: all 0.2s ease;
|
| 120 |
+
z-index: 10002;
|
| 121 |
+
padding: 4px 0;
|
| 122 |
+
pointer-events: none;
|
| 123 |
+
white-space: nowrap;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.dropdown:hover .dropdown-menu {
|
| 127 |
+
opacity: 1;
|
| 128 |
+
visibility: visible;
|
| 129 |
+
transform: translateY(0);
|
| 130 |
+
pointer-events: auto;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
.dropdown-item {
|
| 136 |
+
display: block;
|
| 137 |
+
padding: 10px 16px;
|
| 138 |
+
color: #202124;
|
| 139 |
+
text-decoration: none;
|
| 140 |
+
font-size: 14px;
|
| 141 |
+
transition: background 0.2s;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.dropdown-item:hover {
|
| 145 |
+
background: #f8f9fa;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.dropdown-item:first-child {
|
| 149 |
+
border-top-left-radius: 6px;
|
| 150 |
+
border-top-right-radius: 6px;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.dropdown-item:last-child {
|
| 154 |
+
border-bottom-left-radius: 6px;
|
| 155 |
+
border-bottom-right-radius: 6px;
|
| 156 |
}
|
| 157 |
|
| 158 |
.menu-toggle {
|
|
|
|
| 721 |
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ๋ด ์ด๊ธฐ">โฐ</button>
|
| 722 |
<div class="header-actions">
|
| 723 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 724 |
+
|
| 725 |
+
{# ์ฌ์ดํธ ๊ด๋ฆฌ #}
|
| 726 |
+
<div class="dropdown">
|
| 727 |
+
<button type="button" class="dropdown-toggle">์ฌ์ดํธ ๊ด๋ฆฌ</button>
|
| 728 |
+
<div class="dropdown-menu">
|
| 729 |
+
<a href="{{ url_for('main.admin') }}" class="dropdown-item">์ฌ์ฉ์ ๊ด๋ฆฌ</a>
|
| 730 |
+
<a href="{{ url_for('main.admin_tokens') }}" class="dropdown-item">ํ ํฐ ํต๊ณ</a>
|
| 731 |
+
</div>
|
| 732 |
+
</div>
|
| 733 |
+
|
| 734 |
+
{# ์น์์ค ๊ด๋ฆฌ #}
|
| 735 |
+
<div class="dropdown">
|
| 736 |
+
<button type="button" class="dropdown-toggle">์น์์ค ๊ด๋ฆฌ</button>
|
| 737 |
+
<div class="dropdown-menu">
|
| 738 |
+
<a href="{{ url_for('main.admin_webnovels') }}" class="dropdown-item">์น์์ค ์
๋ก๋ ๋ฐ ๊ด๋ฆฌ</a>
|
| 739 |
+
<a href="{{ url_for('main.admin_messages') }}" class="dropdown-item">๋ฉ์์ง ํ์ธ</a>
|
| 740 |
+
</div>
|
| 741 |
+
</div>
|
| 742 |
+
|
| 743 |
+
{# AI ์ค์ #}
|
| 744 |
+
<div class="dropdown">
|
| 745 |
+
<button type="button" class="dropdown-toggle">AI ์ค์ </button>
|
| 746 |
+
<div class="dropdown-menu">
|
| 747 |
+
<a href="{{ url_for('main.admin_settings') }}" class="dropdown-item">AI ์ค์ </a>
|
| 748 |
+
<a href="{{ url_for('main.admin_prompts') }}" class="dropdown-item">ํ๋กฌํํธ ๊ด๋ฆฌ</a>
|
| 749 |
+
</div>
|
| 750 |
+
</div>
|
| 751 |
+
|
| 752 |
+
{# ์ฑ๋ด #}
|
| 753 |
+
<div class="dropdown">
|
| 754 |
+
<button type="button" class="dropdown-toggle">์ฑ๋ด</button>
|
| 755 |
+
<div class="dropdown-menu">
|
| 756 |
+
<a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">ํ๊ทธ/ํ๋กฌํํธ</a>
|
| 757 |
+
</div>
|
| 758 |
+
</div>
|
| 759 |
+
|
| 760 |
+
{# ํธ์๊ธฐ๋ฅ #}
|
| 761 |
+
<div class="dropdown">
|
| 762 |
+
<button type="button" class="dropdown-toggle">ํธ์๊ธฐ๋ฅ</button>
|
| 763 |
+
<div class="dropdown-menu">
|
| 764 |
+
<a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์ ํธ</a>
|
| 765 |
+
</div>
|
| 766 |
+
</div>
|
| 767 |
+
|
| 768 |
+
{# ๋ฉ์ธ์ผ๋ก #}
|
| 769 |
+
<a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ์ธ์ผ๋ก</a>
|
| 770 |
+
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋ก๊ทธ์์</a>
|
| 771 |
</div>
|
| 772 |
</div>
|
| 773 |
|
|
|
|
| 780 |
</div>
|
| 781 |
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 782 |
<div class="mobile-menu-items">
|
| 783 |
+
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์ฌ์ดํธ ๊ด๋ฆฌ</div>
|
| 784 |
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ฌ์ฉ์ ๊ด๋ฆฌ</a>
|
| 785 |
+
<a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ ํฐ ํต๊ณ</a>
|
| 786 |
+
|
| 787 |
+
<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>
|
| 788 |
+
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์น์์ค ์
๋ก๋ ๋ฐ ๊ด๋ฆฌ</a>
|
| 789 |
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์์ง ํ์ธ</a>
|
| 790 |
+
|
| 791 |
+
<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>
|
| 792 |
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์ค์ </a>
|
| 793 |
+
<a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๋กฌํํธ ๊ด๋ฆฌ</a>
|
| 794 |
+
|
| 795 |
+
<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>
|
| 796 |
+
<a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ๊ทธ/ํ๋กฌํํธ</a>
|
| 797 |
+
|
| 798 |
+
<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>
|
| 799 |
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์ ํธ</a>
|
| 800 |
+
|
| 801 |
+
<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>
|
| 802 |
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ์ธ์ผ๋ก</a>
|
| 803 |
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ก๊ทธ์์</a>
|
| 804 |
</div>
|
|
|
|
| 1142 |
const toggleButtonText = isPublic ? '๋น๊ณต๊ฐ๋ก' : '๊ณต๊ฐ๋ก';
|
| 1143 |
const toggleButtonClass = isPublic ? 'btn-warning' : 'btn-success';
|
| 1144 |
|
| 1145 |
+
// ํ๊ทธ ํ์ (์ผ๋ฐ ํ๊ทธ์ ์์ธ ํ๊ทธ ๊ตฌ๋ถ)
|
| 1146 |
let tagsHtml = '';
|
| 1147 |
if (file.tags) {
|
| 1148 |
try {
|
| 1149 |
+
const tagsData = JSON.parse(file.tags);
|
| 1150 |
+
const tagType = tagsData.type || 'legacy'; // legacy๋ ๊ธฐ์กด ํ์
|
| 1151 |
+
const tags = tagsData.tags || tagsData; // tags ํ๋๊ฐ ์์ผ๋ฉด ์ฌ์ฉ, ์์ผ๋ฉด ์ ์ฒด
|
| 1152 |
|
| 1153 |
+
let simpleTags = [];
|
| 1154 |
+
let detailedTags = [];
|
| 1155 |
+
|
| 1156 |
+
// ์ผ๋ฐ ํ๊ทธ ์ถ์ถ
|
| 1157 |
+
if (tagType === 'simple' || tagsData.simple_tags) {
|
| 1158 |
+
const simpleData = tagsData.simple_tags || tags;
|
| 1159 |
+
const topLevelKeys = ['parent_chunk', 'episodes', 'graph_rag_total', 'graph_rag_by_episode', 'graph_rag_by_character', 'graph_rag_by_event', 'graph_rag_detail'];
|
| 1160 |
+
topLevelKeys.forEach(key => {
|
| 1161 |
+
if (simpleData[key]) {
|
| 1162 |
+
Object.values(simpleData[key]).forEach(arr => {
|
| 1163 |
+
if (Array.isArray(arr)) simpleTags.push(...arr);
|
| 1164 |
+
});
|
| 1165 |
+
}
|
| 1166 |
+
});
|
| 1167 |
+
}
|
| 1168 |
+
|
| 1169 |
+
// ์์ธ ํ๊ทธ ์ถ์ถ
|
| 1170 |
+
if (tagType === 'detailed' || tagsData.detailed_tags) {
|
| 1171 |
+
const detailedData = tagsData.detailed_tags || (tagType === 'detailed' ? tags : null);
|
| 1172 |
+
if (detailedData) {
|
| 1173 |
+
const topLevelKeys = ['parent_chunk', 'episodes', 'graph_rag_total', 'graph_rag_by_episode', 'graph_rag_by_character', 'graph_rag_by_event', 'graph_rag_detail'];
|
| 1174 |
+
topLevelKeys.forEach(key => {
|
| 1175 |
+
if (detailedData[key]) {
|
| 1176 |
+
Object.values(detailedData[key]).forEach(arr => {
|
| 1177 |
+
if (Array.isArray(arr)) detailedTags.push(...arr);
|
| 1178 |
+
});
|
| 1179 |
+
}
|
| 1180 |
});
|
| 1181 |
}
|
| 1182 |
}
|
| 1183 |
|
| 1184 |
+
// ๊ธฐ์กด ํ์ (legacy) ๏ฟฝ๏ฟฝ๏ฟฝ๋ฆฌ
|
| 1185 |
+
if (tagType === 'legacy' && !tagsData.tags) {
|
| 1186 |
+
const topLevelKeys = ['parent_chunk', 'episodes', 'graph_rag_total', 'graph_rag_by_episode', 'graph_rag_by_character', 'graph_rag_by_event', 'graph_rag_detail'];
|
| 1187 |
+
topLevelKeys.forEach(key => {
|
| 1188 |
+
if (tags[key]) {
|
| 1189 |
+
Object.values(tags[key]).forEach(arr => {
|
| 1190 |
+
if (Array.isArray(arr)) detailedTags.push(...arr);
|
| 1191 |
+
});
|
| 1192 |
+
}
|
| 1193 |
});
|
| 1194 |
+
}
|
| 1195 |
+
|
| 1196 |
+
// ํ๊ทธ ํ์ HTML ์์ฑ
|
| 1197 |
+
if (simpleTags.length > 0 || detailedTags.length > 0) {
|
| 1198 |
+
tagsHtml = '<div style="margin-top: 6px;">';
|
| 1199 |
+
|
| 1200 |
+
// ์ผ๋ฐ ํ๊ทธ ํ์
|
| 1201 |
+
if (simpleTags.length > 0) {
|
| 1202 |
+
const visibleSimple = simpleTags.slice(0, 3);
|
| 1203 |
+
tagsHtml += '<div style="margin-bottom: 4px;"><span style="color: #4caf50; font-size: 10px; font-weight: 500; margin-right: 4px;">[์ผ๋ฐ ํ๊ทธ]</span>';
|
| 1204 |
+
visibleSimple.forEach(tag => {
|
| 1205 |
+
tagsHtml += `<span style="background: #e8f5e9; color: #2e7d32; padding: 2px 6px; border-radius: 4px; font-size: 11px; border: 1px solid #c8e6c9; margin-right: 4px;">#${escapeHtml(tag)}</span>`;
|
| 1206 |
+
});
|
| 1207 |
+
if (simpleTags.length > 3) {
|
| 1208 |
+
tagsHtml += `<span style="color: #5f6368; font-size: 10px;">+${simpleTags.length - 3}</span>`;
|
| 1209 |
+
}
|
| 1210 |
+
tagsHtml += '</div>';
|
| 1211 |
}
|
| 1212 |
+
|
| 1213 |
+
// ์์ธ ํ๊ทธ ํ์
|
| 1214 |
+
if (detailedTags.length > 0) {
|
| 1215 |
+
const visibleDetailed = detailedTags.slice(0, 3);
|
| 1216 |
+
tagsHtml += '<div><span style="color: #673ab7; font-size: 10px; font-weight: 500; margin-right: 4px;">[์์ธ ํ๊ทธ]</span>';
|
| 1217 |
+
visibleDetailed.forEach(tag => {
|
| 1218 |
+
tagsHtml += `<span style="background: #ede7f6; color: #512da8; padding: 2px 6px; border-radius: 4px; font-size: 11px; border: 1px solid #d1c4e9; margin-right: 4px;">#${escapeHtml(tag)}</span>`;
|
| 1219 |
+
});
|
| 1220 |
+
if (detailedTags.length > 3) {
|
| 1221 |
+
tagsHtml += `<span style="color: #5f6368; font-size: 10px;">+${detailedTags.length - 3}</span>`;
|
| 1222 |
+
}
|
| 1223 |
+
tagsHtml += '</div>';
|
| 1224 |
+
}
|
| 1225 |
+
|
| 1226 |
tagsHtml += '</div>';
|
| 1227 |
}
|
| 1228 |
} catch (e) {
|
|
|
|
| 1251 |
`<button class="btn btn-secondary" onclick="createParentChunk(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">Parent Chunk ์์ฑ</button>`
|
| 1252 |
}
|
| 1253 |
<button class="btn btn-info" onclick="createMetadata(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">๋ฉํ๋ฐ์ดํฐ ์์ฑ</button>
|
| 1254 |
+
<button class="btn" style="background: #4caf50; color: white; padding: 4px 8px; font-size: 12px; margin-right: 4px;" onclick="createSimpleTags(${file.id}, '${escapeHtml(file.original_filename)}')">์ผ๋ฐ ํ๊ทธ ์์ฑ</button>
|
| 1255 |
+
<button class="btn" style="background: #673ab7; color: white; padding: 4px 8px; font-size: 12px; margin-right: 4px;" onclick="createDetailedTags(${file.id}, '${escapeHtml(file.original_filename)}')">์์ธ ํ๊ทธ ์์ฑ</button>
|
| 1256 |
<button class="btn btn-primary" onclick="continueUpload(${file.id})" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">์ด์ด์ ์
๋ก๋</button>
|
| 1257 |
<button class="btn btn-secondary" onclick="deleteFile(${file.id})" style="padding: 4px 8px; font-size: 12px;">์ญ์ </button>
|
| 1258 |
</div>
|
|
|
|
| 2139 |
}
|
| 2140 |
}
|
| 2141 |
|
| 2142 |
+
// ์ผ๋ฐ ํ๊ทธ ์์ฑ
|
| 2143 |
+
async function createSimpleTags(fileId, fileName) {
|
| 2144 |
+
if (!confirm(`"${fileName}" ํ์ผ์ ์ผ๋ฐ ํ๊ทธ๋ฅผ ์์ฑํ์๊ฒ ์ต๋๊น?\n\n๊ฐ๋จํ ํค์๋ ํํ์ ํ๊ทธ๊ฐ ์์ฑ๋ฉ๋๋ค.`)) {
|
| 2145 |
return;
|
| 2146 |
}
|
| 2147 |
|
|
|
|
| 2151 |
button.textContent = '์์ฑ ์ค...';
|
| 2152 |
|
| 2153 |
try {
|
| 2154 |
+
const response = await fetch(`/api/files/${fileId}/process/tags/simple`, {
|
| 2155 |
method: 'POST',
|
| 2156 |
credentials: 'include'
|
| 2157 |
});
|
| 2158 |
const data = await response.json();
|
| 2159 |
|
| 2160 |
if (response.ok) {
|
| 2161 |
+
console.log('[์ผ๋ฐ ํ๊ทธ ์์ฑ] ์ฑ๊ณต ์๋ต:', data);
|
| 2162 |
+
showAlert('์ผ๋ฐ ํ๊ทธ ์์ฑ์ด ์๋ฃ๋์์ต๋๋ค.', 'success');
|
| 2163 |
+
loadFiles(); // ํ์ผ ๋ชฉ๋ก ์๋ก๊ณ ์นจ
|
| 2164 |
+
} else {
|
| 2165 |
+
console.error('[์ผ๋ฐ ํ๊ทธ ์์ฑ] ์คํจ ์๋ต:', data);
|
| 2166 |
+
showAlert(`์ผ๋ฐ ํ๊ทธ ์์ฑ ์คํจ: ${data.error || '์ ์ ์๋ ์ค๋ฅ'}`, 'error');
|
| 2167 |
+
}
|
| 2168 |
+
} catch (error) {
|
| 2169 |
+
showAlert(`์ผ๋ฐ ํ๊ทธ ์์ฑ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: ${error.message}`, 'error');
|
| 2170 |
+
console.error('์ผ๋ฐ ํ๊ทธ ์์ฑ ์ค๋ฅ:', error);
|
| 2171 |
+
} finally {
|
| 2172 |
+
button.disabled = false;
|
| 2173 |
+
button.textContent = originalText;
|
| 2174 |
+
}
|
| 2175 |
+
}
|
| 2176 |
+
|
| 2177 |
+
// ์์ธ ํ๊ทธ ์์ฑ
|
| 2178 |
+
async function createDetailedTags(fileId, fileName) {
|
| 2179 |
+
if (!confirm(`"${fileName}" ํ์ผ์ ์์ธ ํ๊ทธ๋ฅผ ์์ฑํ์๊ฒ ์ต๋๊น?\n\n์ด ์์
์ AI ๋ชจ๋ธ์ ์ฌ์ฉํ์ฌ Parent Chunk์ ํ์ฐจ๋ณ ๋ถ์ ๋ฐ์ดํฐ๋ฅผ ์ข
ํฉํ์ฌ ์์ธํ ํ๊ทธ๋ฅผ ์์ฑํฉ๋๋ค.\n์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆด ์ ์์ต๋๋ค.`)) {
|
| 2180 |
+
return;
|
| 2181 |
+
}
|
| 2182 |
+
|
| 2183 |
+
const button = event.target;
|
| 2184 |
+
const originalText = button.textContent;
|
| 2185 |
+
button.disabled = true;
|
| 2186 |
+
button.textContent = '์์ฑ ์ค...';
|
| 2187 |
+
|
| 2188 |
+
try {
|
| 2189 |
+
const response = await fetch(`/api/files/${fileId}/process/tags/detailed`, {
|
| 2190 |
+
method: 'POST',
|
| 2191 |
+
credentials: 'include'
|
| 2192 |
+
});
|
| 2193 |
+
const data = await response.json();
|
| 2194 |
+
|
| 2195 |
+
if (response.ok) {
|
| 2196 |
+
console.log('[์์ธ ํ๊ทธ ์์ฑ] ์ฑ๊ณต ์๋ต:', data);
|
| 2197 |
+
showAlert('์์ธ ํ๊ทธ ์์ฑ์ด ์๋ฃ๋์์ต๋๋ค.', 'success');
|
| 2198 |
loadFiles(); // ํ์ผ ๋ชฉ๋ก ์๋ก๊ณ ์นจ
|
| 2199 |
} else {
|
| 2200 |
+
console.error('[์์ธ ํ๊ทธ ์์ฑ] ์คํจ ์๋ต:', data);
|
| 2201 |
+
showAlert(`์์ธ ํ๊ทธ ์์ฑ ์คํจ: ${data.error || '์ ์ ์๋ ์ค๋ฅ'}`, 'error');
|
| 2202 |
}
|
| 2203 |
} catch (error) {
|
| 2204 |
+
showAlert(`์์ธ ํ๊ทธ ์์ฑ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: ${error.message}`, 'error');
|
| 2205 |
+
console.error('์์ธ ํ๊ทธ ์์ฑ ์ค๋ฅ:', error);
|
| 2206 |
} finally {
|
| 2207 |
button.disabled = false;
|
| 2208 |
button.textContent = originalText;
|