GitHub Actions commited on
Commit
1995f8f
Β·
1 Parent(s): 5a77b24

Auto-deploy from GitHub Actions - 2025-12-12 16:41:27

Browse files
app/database.py CHANGED
@@ -370,3 +370,27 @@ class GraphEvent(db.Model):
370
  }
371
 
372
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  }
371
 
372
 
373
+ # νŒŒμΌλ³„ 챗봇 ν”„λ‘¬ν”„νŠΈ μ €μž₯ λͺ¨λΈ (일반/상세)
374
+ class ChatbotPrompt(db.Model):
375
+ __tablename__ = 'chatbot_prompt'
376
+ id = db.Column(db.Integer, primary_key=True)
377
+ file_id = db.Column(db.Integer, db.ForeignKey('uploaded_file.id'), nullable=False, unique=True)
378
+ simple_prompt = db.Column(db.Text, nullable=True)
379
+ detailed_prompt = db.Column(db.Text, nullable=True)
380
+ created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
381
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
382
+
383
+ # 관계
384
+ file = db.relationship('UploadedFile', backref='chatbot_prompt')
385
+
386
+ def to_dict(self):
387
+ return {
388
+ 'id': self.id,
389
+ 'file_id': self.file_id,
390
+ 'simple_prompt': self.simple_prompt,
391
+ 'detailed_prompt': self.detailed_prompt,
392
+ 'created_at': self.created_at.isoformat() if self.created_at else None,
393
+ 'updated_at': self.updated_at.isoformat() if self.updated_at else None
394
+ }
395
+
396
+
app/routes.py CHANGED
@@ -1,7 +1,7 @@
1
  from flask import Blueprint, render_template, request, jsonify, send_from_directory, redirect, url_for, flash, send_file
2
  from flask_login import login_user, logout_user, login_required, current_user
3
  from werkzeug.utils import secure_filename
4
- from app.database import db, UploadedFile, User, ChatSession, ChatMessage, DocumentChunk, ParentChunk, SystemConfig, EpisodeAnalysis, GraphEntity, GraphRelationship, GraphEvent
5
  from app.vector_db import get_vector_db
6
  from app.gemini_client import get_gemini_client
7
  import requests
@@ -13,6 +13,156 @@ import json
13
 
14
  main_bp = Blueprint('main', __name__)
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  def admin_required(f):
17
  """κ΄€λ¦¬μž κΆŒν•œμ΄ ν•„μš”ν•œ λ°μ½”λ ˆμ΄ν„°"""
18
  from functools import wraps
@@ -1997,6 +2147,13 @@ 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():
@@ -4412,6 +4569,119 @@ def get_files():
4412
  except Exception as e:
4413
  return jsonify({'error': f'파일 λͺ©λ‘ 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}'}), 500
4414
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4415
  @main_bp.route('/api/files/<int:file_id>/chunks', methods=['GET'])
4416
  @login_required
4417
  def get_file_chunks(file_id):
 
1
  from flask import Blueprint, render_template, request, jsonify, send_from_directory, redirect, url_for, flash, send_file
2
  from flask_login import login_user, logout_user, login_required, current_user
3
  from werkzeug.utils import secure_filename
4
+ from app.database import db, UploadedFile, User, ChatSession, ChatMessage, DocumentChunk, ParentChunk, SystemConfig, EpisodeAnalysis, GraphEntity, GraphRelationship, GraphEvent, ChatbotPrompt
5
  from app.vector_db import get_vector_db
6
  from app.gemini_client import get_gemini_client
7
  import requests
 
13
 
14
  main_bp = Blueprint('main', __name__)
15
 
16
+
17
+ def ensure_chatbot_prompt_table_exists():
18
+ """chatbot_prompt ν…Œμ΄λΈ”μ΄ μ—†μœΌλ©΄ 생성 (운영 ν™˜κ²½μ—μ„œ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ λˆ„λ½ λŒ€λΉ„)"""
19
+ try:
20
+ from sqlalchemy import inspect
21
+ inspector = inspect(db.engine)
22
+ if 'chatbot_prompt' not in inspector.get_table_names():
23
+ print("[ChatbotPrompt] chatbot_prompt ν…Œμ΄λΈ”μ΄ μ—†μ–΄ μƒμ„±ν•©λ‹ˆλ‹€.")
24
+ ChatbotPrompt.__table__.create(db.engine, checkfirst=True)
25
+ except Exception as e:
26
+ print(f"[ChatbotPrompt] ν…Œμ΄λΈ” 생성 확인 쀑 였λ₯˜: {e}")
27
+ # create_all은 이미 app initμ—μ„œ μˆ˜ν–‰λ˜μ§€λ§Œ, μ—¬κΈ°μ„œλ„ μ•ˆμ „ν•˜κ²Œ ν•œ 번 더 μ‹œλ„
28
+ try:
29
+ db.create_all()
30
+ except Exception as e2:
31
+ print(f"[ChatbotPrompt] db.create_all μ‹€νŒ¨: {e2}")
32
+
33
+
34
+ def extract_simple_and_detailed_tags(file_tags_json):
35
+ """UploadedFile.tags(JSON λ¬Έμžμ—΄)μ—μ„œ 일반/상세 νƒœκ·Έλ₯Ό λΆ„λ¦¬ν•˜μ—¬ λ°˜ν™˜"""
36
+ if not file_tags_json:
37
+ return None, None, 'no_tags'
38
+ try:
39
+ tags_data = json.loads(file_tags_json)
40
+ except Exception:
41
+ return None, None, 'invalid_json'
42
+
43
+ tag_type = (tags_data.get('type') if isinstance(tags_data, dict) else None) or 'legacy'
44
+
45
+ simple_tags = None
46
+ detailed_tags = None
47
+
48
+ if isinstance(tags_data, dict) and tag_type == 'simple':
49
+ simple_tags = tags_data.get('tags') or tags_data
50
+ detailed_tags = tags_data.get('detailed_tags')
51
+ elif isinstance(tags_data, dict) and tag_type == 'detailed':
52
+ detailed_tags = tags_data.get('tags') or tags_data
53
+ simple_tags = tags_data.get('simple_tags')
54
+ else:
55
+ # legacy ν˜•μ‹μ€ μƒμ„Έλ‘œ μ·¨κΈ‰(κΈ°μ‘΄ 방식)
56
+ detailed_tags = tags_data if isinstance(tags_data, dict) else None
57
+ simple_tags = None
58
+
59
+ # μ•ˆμ „ν•˜κ²Œ tags/type ν‚€ 제거(legacyμ—μ„œ tags_data μžμ²΄κ°€ λ“€μ–΄μ˜€λŠ” 경우)
60
+ if isinstance(simple_tags, dict) and 'type' in simple_tags:
61
+ simple_tags = {k: v for k, v in simple_tags.items() if k != 'type'}
62
+ if isinstance(detailed_tags, dict) and 'type' in detailed_tags:
63
+ detailed_tags = {k: v for k, v in detailed_tags.items() if k != 'type'}
64
+
65
+ return simple_tags, detailed_tags, tag_type
66
+
67
+
68
+ def build_story_chatbot_prompt(file_obj, tags_obj, prompt_kind='simple'):
69
+ """νƒœκ·Έ 기반으둜 'μŠ€ν† λ¦¬ 챗봇'μ—μ„œ μ‚¬μš©ν•  ν”„λ‘¬ν”„νŠΈ ν…μŠ€νŠΈ 생성"""
70
+ def normalize_tags(value):
71
+ if isinstance(value, list):
72
+ return [str(v).strip() for v in value if str(v).strip()]
73
+ return []
74
+
75
+ def render_section(title, section_dict, key_labels):
76
+ if not isinstance(section_dict, dict):
77
+ return ""
78
+ out = [f"## {title}"]
79
+ any_added = False
80
+ for key, label in key_labels.items():
81
+ tags_list = normalize_tags(section_dict.get(key))
82
+ if tags_list:
83
+ any_added = True
84
+ out.append(f"- {label}: " + " ".join([f"#{t}" for t in tags_list]))
85
+ if not any_added:
86
+ return ""
87
+ return "\n".join(out)
88
+
89
+ prompt_title = "일반 ν”„λ‘¬ν”„νŠΈ" if prompt_kind == 'simple' else "상세 ν”„λ‘¬ν”„νŠΈ"
90
+ lines = [
91
+ "[μŠ€ν† λ¦¬ 챗봇 ν”„λ‘¬ν”„νŠΈ]",
92
+ f"- 파일: {file_obj.original_filename}",
93
+ f"- κΈ°μ€€: {prompt_title} (νƒœκ·Έ 기반)",
94
+ "",
95
+ "[μ‚¬μš© μ§€μΉ¨]",
96
+ "- μ•„λž˜ νƒœκ·Έλ₯Ό 세계관/인물/사건/κ΄€κ³„μ˜ '사싀'둜 κ°„μ£Όν•˜κ³ , μ§ˆλ¬Έμ— μΌκ΄€λ˜κ²Œ λ‹΅λ³€ν•˜μ„Έμš”.",
97
+ "- λͺ¨λ₯΄λŠ” λ‚΄μš©μ€ μΆ”μΈ‘ν•˜μ§€ 말고, ν•„μš”ν•˜λ©΄ μ‚¬μš©μžμ—κ²Œ μΆ”κ°€ 정보λ₯Ό μš”μ²­ν•˜μ„Έμš”.",
98
+ ""
99
+ ]
100
+
101
+ # μ„Ήμ…˜ λ§€ν•‘ (admin_tags의 ꡬ쑰에 맞좀)
102
+ if isinstance(tags_obj, dict):
103
+ parent = tags_obj.get('parent_chunk')
104
+ episodes = tags_obj.get('episodes')
105
+ graph_total = tags_obj.get('graph_rag_total')
106
+ graph_by_episode = tags_obj.get('graph_rag_by_episode')
107
+ graph_by_character = tags_obj.get('graph_rag_by_character')
108
+ graph_by_event = tags_obj.get('graph_rag_by_event')
109
+ graph_detail = tags_obj.get('graph_rag_detail')
110
+
111
+ lines.append(render_section("Parent Chunk", parent, {
112
+ 'world_view': '��계관',
113
+ 'characters': '인물',
114
+ 'story': 'μŠ€ν† λ¦¬',
115
+ 'others': '기타'
116
+ }))
117
+
118
+ lines.append(render_section("νšŒμ°¨λ³„", episodes, {
119
+ 'story': 'μŠ€ν† λ¦¬',
120
+ 'events': '사건',
121
+ 'characters': 'λ“±μž₯ 인물',
122
+ 'relationships_change': '관계 λ³€ν™”',
123
+ 'appearance': 'μ™Έλͺ¨',
124
+ 'clothing': '의볡',
125
+ 'items': 'μ•„μ΄ν…œ/μ†Œν’ˆ',
126
+ 'others': '기타'
127
+ }))
128
+
129
+ lines.append(render_section("GraphRAG (전체)", graph_total, {
130
+ 'characters': '인물',
131
+ 'locations': 'μž₯μ†Œ',
132
+ 'relationships': '관계',
133
+ 'events': '사건'
134
+ }))
135
+
136
+ lines.append(render_section("GraphRAG (νšŒμ°¨λ³„)", graph_by_episode, {
137
+ 'characters': '인물',
138
+ 'locations': 'μž₯μ†Œ',
139
+ 'relationships': '관계',
140
+ 'events': '사건'
141
+ }))
142
+
143
+ lines.append(render_section("GraphRAG (인물별)", graph_by_character, {
144
+ 'characters': '인물',
145
+ 'locations': 'μž₯μ†Œ',
146
+ 'relationships': '관계',
147
+ 'events': '사건'
148
+ }))
149
+
150
+ lines.append(render_section("GraphRAG (사건별)", graph_by_event, {
151
+ 'characters': '인물',
152
+ 'locations': 'μž₯μ†Œ',
153
+ 'relationships': '관계',
154
+ 'events': '사건'
155
+ }))
156
+
157
+ lines.append(render_section("GraphRAG (상세)", graph_detail, {
158
+ 'person_person_relationships_chronological': '인물-인물 관계(μ‹œκ°„μˆœ)',
159
+ 'event_person_relationships': '사건-인물 관계'
160
+ }))
161
+
162
+ # 빈 μ„Ήμ…˜ 제거
163
+ rendered = "\n\n".join([s for s in lines if isinstance(s, str) and s.strip()])
164
+ return rendered.strip() + "\n"
165
+
166
  def admin_required(f):
167
  """κ΄€λ¦¬μž κΆŒν•œμ΄ ν•„μš”ν•œ λ°μ½”λ ˆμ΄ν„°"""
168
  from functools import wraps
 
2147
  """νƒœκ·Έ 보기 νŽ˜μ΄μ§€"""
2148
  return render_template('admin_tags.html')
2149
 
2150
+
2151
+ @main_bp.route('/admin/chatbot-prompts')
2152
+ @admin_required
2153
+ def admin_chatbot_prompts():
2154
+ """챗봇 ν”„λ‘¬ν”„νŠΈ(일반/상세) 보기 νŽ˜μ΄μ§€"""
2155
+ return render_template('admin_chatbot_prompts.html')
2156
+
2157
  @main_bp.route('/admin/utils')
2158
  @admin_required
2159
  def admin_utils():
 
4569
  except Exception as e:
4570
  return jsonify({'error': f'파일 λͺ©λ‘ 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}'}), 500
4571
 
4572
+
4573
+ @main_bp.route('/api/files/<int:file_id>/process/prompts/simple', methods=['POST'])
4574
+ @login_required
4575
+ def generate_simple_chatbot_prompt(file_id):
4576
+ """일반 νƒœκ·Έ 기반 'μŠ€ν† λ¦¬ 챗봇' ν”„λ‘¬ν”„νŠΈ 생성/μ €μž₯"""
4577
+ try:
4578
+ ensure_chatbot_prompt_table_exists()
4579
+ file = UploadedFile.query.get_or_404(file_id)
4580
+
4581
+ if not current_user.is_admin and file.uploaded_by != current_user.id:
4582
+ return jsonify({'error': 'κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€.'}), 403
4583
+
4584
+ simple_tags, detailed_tags, tag_type = extract_simple_and_detailed_tags(file.tags)
4585
+ if not simple_tags:
4586
+ return jsonify({'error': '일반 νƒœκ·Έκ°€ μ—†μ–΄ 일반 ν”„λ‘¬ν”„νŠΈλ₯Ό 생성할 수 μ—†μŠ΅λ‹ˆλ‹€. (일반 νƒœκ·Έ 생성 ν›„ λ‹€μ‹œ μ‹œλ„ν•˜μ„Έμš”)'}), 400
4587
+
4588
+ prompt_text = build_story_chatbot_prompt(file, simple_tags, prompt_kind='simple')
4589
+
4590
+ record = ChatbotPrompt.query.filter_by(file_id=file.id).first()
4591
+ if not record:
4592
+ record = ChatbotPrompt(file_id=file.id, simple_prompt=prompt_text)
4593
+ db.session.add(record)
4594
+ else:
4595
+ record.simple_prompt = prompt_text
4596
+ db.session.commit()
4597
+
4598
+ return jsonify({'message': '일반 ν”„λ‘¬ν”„νŠΈκ°€ 생성/μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€.', 'file_id': file.id, 'simple_prompt': prompt_text}), 200
4599
+ except Exception as e:
4600
+ db.session.rollback()
4601
+ return jsonify({'error': f'일반 ν”„λ‘¬ν”„νŠΈ 생성 쀑 였λ₯˜: {str(e)}'}), 500
4602
+
4603
+
4604
+ @main_bp.route('/api/files/<int:file_id>/process/prompts/detailed', methods=['POST'])
4605
+ @login_required
4606
+ def generate_detailed_chatbot_prompt(file_id):
4607
+ """상세 νƒœκ·Έ 기반 'μŠ€ν† λ¦¬ 챗봇' ν”„λ‘¬ν”„νŠΈ 생성/μ €μž₯"""
4608
+ try:
4609
+ ensure_chatbot_prompt_table_exists()
4610
+ file = UploadedFile.query.get_or_404(file_id)
4611
+
4612
+ if not current_user.is_admin and file.uploaded_by != current_user.id:
4613
+ return jsonify({'error': 'κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€.'}), 403
4614
+
4615
+ simple_tags, detailed_tags, tag_type = extract_simple_and_detailed_tags(file.tags)
4616
+ if not detailed_tags:
4617
+ return jsonify({'error': '상세 νƒœκ·Έκ°€ μ—†μ–΄ 상세 ν”„λ‘¬ν”„νŠΈλ₯Ό 생성할 수 μ—†μŠ΅λ‹ˆλ‹€. (상세 νƒœκ·Έ 생성 ν›„ λ‹€μ‹œ μ‹œλ„ν•˜μ„Έμš”)'}), 400
4618
+
4619
+ prompt_text = build_story_chatbot_prompt(file, detailed_tags, prompt_kind='detailed')
4620
+
4621
+ record = ChatbotPrompt.query.filter_by(file_id=file.id).first()
4622
+ if not record:
4623
+ record = ChatbotPrompt(file_id=file.id, detailed_prompt=prompt_text)
4624
+ db.session.add(record)
4625
+ else:
4626
+ record.detailed_prompt = prompt_text
4627
+ db.session.commit()
4628
+
4629
+ return jsonify({'message': '상세 ν”„λ‘¬ν”„νŠΈκ°€ 생성/μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€.', 'file_id': file.id, 'detailed_prompt': prompt_text}), 200
4630
+ except Exception as e:
4631
+ db.session.rollback()
4632
+ return jsonify({'error': f'상세 ν”„λ‘¬ν”„νŠΈ 생성 쀑 였λ₯˜: {str(e)}'}), 500
4633
+
4634
+
4635
+ @main_bp.route('/api/files/<int:file_id>/chatbot-prompts', methods=['GET'])
4636
+ @login_required
4637
+ def get_chatbot_prompts(file_id):
4638
+ """파일의 μ €μž₯된 챗봇 ν”„λ‘¬ν”„νŠΈ(일반/상세) 쑰회"""
4639
+ try:
4640
+ ensure_chatbot_prompt_table_exists()
4641
+ file = UploadedFile.query.get_or_404(file_id)
4642
+
4643
+ if not current_user.is_admin and file.uploaded_by != current_user.id:
4644
+ return jsonify({'error': 'κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€.'}), 403
4645
+
4646
+ record = ChatbotPrompt.query.filter_by(file_id=file.id).first()
4647
+ if not record:
4648
+ return jsonify({'file_id': file.id, 'simple_prompt': None, 'detailed_prompt': None}), 200
4649
+
4650
+ return jsonify({'file_id': file.id, 'simple_prompt': record.simple_prompt, 'detailed_prompt': record.detailed_prompt}), 200
4651
+ except Exception as e:
4652
+ return jsonify({'error': f'챗봇 ν”„λ‘¬ν”„νŠΈ 쑰회 쀑 였λ₯˜: {str(e)}'}), 500
4653
+
4654
+
4655
+ @main_bp.route('/api/admin/chatbot-prompts/files', methods=['GET'])
4656
+ @admin_required
4657
+ def list_files_with_chatbot_prompts():
4658
+ """챗봇 ν”„λ‘¬ν”„νŠΈκ°€ ν•˜λ‚˜λΌλ„ μ €μž₯된 파일 λͺ©λ‘(κ΄€λ¦¬μžμš©)"""
4659
+ try:
4660
+ ensure_chatbot_prompt_table_exists()
4661
+ # ν”„λ‘¬ν”„νŠΈκ°€ μžˆλŠ” κ²ƒλ§Œ
4662
+ q = (
4663
+ db.session.query(UploadedFile, ChatbotPrompt)
4664
+ .join(ChatbotPrompt, ChatbotPrompt.file_id == UploadedFile.id)
4665
+ .filter(
4666
+ (ChatbotPrompt.simple_prompt.isnot(None)) | (ChatbotPrompt.detailed_prompt.isnot(None))
4667
+ )
4668
+ .order_by(UploadedFile.uploaded_at.desc(), UploadedFile.id.desc())
4669
+ )
4670
+
4671
+ items = []
4672
+ for f, p in q.all():
4673
+ items.append({
4674
+ 'file_id': f.id,
4675
+ 'original_filename': f.original_filename,
4676
+ 'uploaded_at': f.uploaded_at.isoformat() if f.uploaded_at else None,
4677
+ 'has_simple_prompt': bool(p.simple_prompt),
4678
+ 'has_detailed_prompt': bool(p.detailed_prompt),
4679
+ })
4680
+
4681
+ return jsonify({'files': items}), 200
4682
+ except Exception as e:
4683
+ return jsonify({'error': f'ν”„λ‘¬ν”„νŠΈ 파일 λͺ©λ‘ 쑰회 쀑 였λ₯˜: {str(e)}'}), 500
4684
+
4685
  @main_bp.route('/api/files/<int:file_id>/chunks', methods=['GET'])
4686
  @login_required
4687
  def get_file_chunks(file_id):
templates/admin.html CHANGED
@@ -701,6 +701,7 @@
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
 
@@ -741,6 +742,7 @@
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>
 
701
  <button type="button" class="dropdown-toggle">챗봇</button>
702
  <div class="dropdown-menu">
703
  <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
704
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
705
  </div>
706
  </div>
707
 
 
742
 
743
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">챗봇</div>
744
  <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
745
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
746
 
747
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">편의기λŠ₯</div>
748
  <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μœ ν‹Έ</a>
templates/admin_chatbot_prompts.html ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>챗봇 ν”„λ‘¬ν”„νŠΈ - SOY NV AI</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
9
+ <style>
10
+ * { margin: 0; padding: 0; box-sizing: border-box; }
11
+ body {
12
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
13
+ background: #f8f9fa;
14
+ color: #202124;
15
+ overflow-x: hidden;
16
+ }
17
+
18
+ .header {
19
+ background: white;
20
+ border-bottom: 1px solid #dadce0;
21
+ padding: 16px 24px;
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: space-between;
25
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
26
+ }
27
+ .header-title { font-size: 20px; font-weight: 500; display:flex; align-items:center; gap: 12px; }
28
+ .header-actions { display:flex; gap: 8px; align-items:center; flex-wrap: wrap; }
29
+
30
+ .dropdown { position: relative; display: inline-block; z-index: 10001; }
31
+ .dropdown::after { content:''; position:absolute; left:0; right:0; top:100%; height:8px; }
32
+ .dropdown-toggle {
33
+ padding: 8px 16px;
34
+ background: #f1f3f4;
35
+ color: #202124;
36
+ border: none;
37
+ border-radius: 6px;
38
+ font-size: 14px;
39
+ font-weight: 500;
40
+ cursor: pointer;
41
+ transition: all 0.2s;
42
+ display:flex; align-items:center; gap:6px;
43
+ }
44
+ .dropdown-toggle:hover { background: #e8eaed; }
45
+ .dropdown-toggle::after { content:'β–Ό'; font-size: 10px; transition: transform 0.2s; }
46
+ .dropdown:hover .dropdown-toggle::after { transform: rotate(180deg); }
47
+ .dropdown-menu {
48
+ position: absolute;
49
+ top: calc(100% + 4px);
50
+ left: 0;
51
+ background: white;
52
+ border: 1px solid #dadce0;
53
+ border-radius: 6px;
54
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
55
+ min-width: 200px;
56
+ opacity: 0;
57
+ visibility: hidden;
58
+ transform: translateY(-8px);
59
+ transition: all 0.2s ease;
60
+ z-index: 10002;
61
+ padding: 4px 0;
62
+ pointer-events: none;
63
+ white-space: nowrap;
64
+ }
65
+ .dropdown:hover .dropdown-menu {
66
+ opacity: 1; visibility: visible; transform: translateY(0); pointer-events: auto;
67
+ }
68
+ .dropdown-item {
69
+ display:block;
70
+ padding: 10px 16px;
71
+ color: #202124;
72
+ text-decoration: none;
73
+ font-size: 14px;
74
+ transition: background 0.2s;
75
+ }
76
+ .dropdown-item:hover { background: #f8f9fa; }
77
+
78
+ .btn {
79
+ padding: 8px 16px;
80
+ border: none;
81
+ border-radius: 6px;
82
+ font-size: 14px;
83
+ font-weight: 500;
84
+ cursor: pointer;
85
+ transition: all 0.2s;
86
+ text-decoration: none;
87
+ display: inline-flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ }
91
+ .btn-secondary { background: #f1f3f4; color:#202124; }
92
+ .btn-secondary:hover { background: #e8eaed; }
93
+ .btn-primary { background: #1a73e8; color: white; }
94
+ .btn-primary:hover { background: #1557b0; }
95
+
96
+ .container { max-width: 1200px; margin: 24px auto; padding: 0 24px; }
97
+ .file-selector {
98
+ background: white;
99
+ border: 1px solid #dadce0;
100
+ border-radius: 10px;
101
+ padding: 16px;
102
+ margin-bottom: 16px;
103
+ display:flex;
104
+ gap: 12px;
105
+ align-items: center;
106
+ flex-wrap: wrap;
107
+ }
108
+ .file-selector label { font-weight: 500; font-size: 14px; color:#202124; }
109
+ .file-selector select {
110
+ min-width: 320px;
111
+ padding: 10px 12px;
112
+ border: 1px solid #dadce0;
113
+ border-radius: 6px;
114
+ font-size: 14px;
115
+ background: white;
116
+ }
117
+
118
+ .grid {
119
+ display: grid;
120
+ grid-template-columns: 1fr 1fr;
121
+ gap: 16px;
122
+ }
123
+ @media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }
124
+
125
+ .panel {
126
+ background: white;
127
+ border: 1px solid #dadce0;
128
+ border-radius: 10px;
129
+ overflow: hidden;
130
+ display:flex;
131
+ flex-direction: column;
132
+ min-height: 360px;
133
+ }
134
+ .panel-header {
135
+ padding: 12px 14px;
136
+ border-bottom: 1px solid #eee;
137
+ display:flex;
138
+ align-items:center;
139
+ justify-content: space-between;
140
+ gap: 8px;
141
+ }
142
+ .panel-title { font-size: 14px; font-weight: 600; }
143
+ .panel-title.simple { color: #2e7d32; }
144
+ .panel-title.detailed { color: #512da8; }
145
+ .panel-body {
146
+ padding: 12px 14px;
147
+ overflow: auto;
148
+ flex: 1;
149
+ }
150
+ pre {
151
+ white-space: pre-wrap;
152
+ word-break: break-word;
153
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
154
+ font-size: 12.5px;
155
+ line-height: 1.5;
156
+ }
157
+ .empty, .loading, .error { font-size: 13px; color: #5f6368; }
158
+ .error { color: #c5221f; }
159
+ </style>
160
+ </head>
161
+ <body>
162
+ <div class="header">
163
+ <div class="header-title">챗봇 ν”„λ‘¬ν”„νŠΈ</div>
164
+ <div class="header-actions">
165
+ <div class="dropdown">
166
+ <button type="button" class="dropdown-toggle">챗봇</button>
167
+ <div class="dropdown-menu">
168
+ <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
169
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
170
+ </div>
171
+ </div>
172
+ <a href="{{ url_for('main.admin') }}" class="btn btn-secondary">κ΄€λ¦¬μž</a>
173
+ <a href="{{ url_for('main.logout') }}" class="btn btn-secondary">λ‘œκ·Έμ•„μ›ƒ</a>
174
+ </div>
175
+ </div>
176
+
177
+ <div class="container">
178
+ <div class="file-selector">
179
+ <label for="fileSelect">파일 선택:</label>
180
+ <select id="fileSelect">
181
+ <option value="">ν”„λ‘¬ν”„νŠΈκ°€ μ €μž₯된 νŒŒμΌμ„ μ„ νƒν•˜μ„Έμš”...</option>
182
+ </select>
183
+ </div>
184
+
185
+ <div class="grid">
186
+ <div class="panel">
187
+ <div class="panel-header">
188
+ <div class="panel-title simple">일반 ν”„λ‘¬ν”„νŠΈ</div>
189
+ <button class="btn btn-secondary" style="padding:6px 10px; font-size:12px;" onclick="copyPrompt('simplePrompt')">볡사</button>
190
+ </div>
191
+ <div class="panel-body">
192
+ <div id="simplePrompt" class="loading">νŒŒμΌμ„ μ„ νƒν•˜λ©΄ ν”„λ‘¬ν”„νŠΈκ°€ ν‘œμ‹œλ©λ‹ˆλ‹€.</div>
193
+ </div>
194
+ </div>
195
+ <div class="panel">
196
+ <div class="panel-header">
197
+ <div class="panel-title detailed">상세 ν”„λ‘¬ν”„νŠΈ</div>
198
+ <button class="btn btn-secondary" style="padding:6px 10px; font-size:12px;" onclick="copyPrompt('detailedPrompt')">볡사</button>
199
+ </div>
200
+ <div class="panel-body">
201
+ <div id="detailedPrompt" class="loading">νŒŒμΌμ„ μ„ νƒν•˜λ©΄ ν”„λ‘¬ν”„νŠΈκ°€ ν‘œμ‹œλ©λ‹ˆλ‹€.</div>
202
+ </div>
203
+ </div>
204
+ </div>
205
+ </div>
206
+
207
+ <script>
208
+ function escapeHtml(text) {
209
+ const div = document.createElement('div');
210
+ div.textContent = text || '';
211
+ return div.innerHTML;
212
+ }
213
+
214
+ async function copyPrompt(elementId) {
215
+ const el = document.getElementById(elementId);
216
+ const text = el?.dataset?.raw || el?.textContent || '';
217
+ if (!text.trim()) {
218
+ alert('볡사할 λ‚΄μš©μ΄ μ—†μŠ΅λ‹ˆλ‹€.');
219
+ return;
220
+ }
221
+ try {
222
+ await navigator.clipboard.writeText(text);
223
+ alert('ν΄λ¦½λ³΄λ“œμ— λ³΅μ‚¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€.');
224
+ } catch (e) {
225
+ // fallback
226
+ const ta = document.createElement('textarea');
227
+ ta.value = text;
228
+ document.body.appendChild(ta);
229
+ ta.select();
230
+ document.execCommand('copy');
231
+ document.body.removeChild(ta);
232
+ alert('ν΄λ¦½λ³΄λ“œμ— λ³΅μ‚¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€.');
233
+ }
234
+ }
235
+
236
+ async function loadFiles() {
237
+ const select = document.getElementById('fileSelect');
238
+ try {
239
+ const response = await fetch('/api/admin/chatbot-prompts/files', { credentials: 'include' });
240
+ if (!response.ok) throw new Error('파일 λͺ©λ‘μ„ 뢈러올 수 μ—†μŠ΅λ‹ˆλ‹€.');
241
+ const data = await response.json();
242
+ const files = data.files || [];
243
+
244
+ while (select.options.length > 1) select.remove(1);
245
+
246
+ files.forEach(f => {
247
+ const option = document.createElement('option');
248
+ option.value = f.file_id;
249
+ const badge = `${f.has_simple_prompt ? 'S' : '-'}${f.has_detailed_prompt ? 'D' : '-'}`;
250
+ option.textContent = `${f.original_filename} [${badge}]`;
251
+ select.appendChild(option);
252
+ });
253
+ } catch (e) {
254
+ console.error('파일 λͺ©λ‘ λ‘œλ“œ 였λ₯˜:', e);
255
+ }
256
+ }
257
+
258
+ async function loadPrompts(fileId) {
259
+ const simpleEl = document.getElementById('simplePrompt');
260
+ const detailedEl = document.getElementById('detailedPrompt');
261
+
262
+ if (!fileId) {
263
+ simpleEl.className = 'loading';
264
+ simpleEl.textContent = 'νŒŒμΌμ„ μ„ νƒν•˜λ©΄ ν”„λ‘¬ν”„νŠΈκ°€ ν‘œμ‹œλ©λ‹ˆλ‹€.';
265
+ simpleEl.dataset.raw = '';
266
+ detailedEl.className = 'loading';
267
+ detailedEl.textContent = 'νŒŒμΌμ„ μ„ νƒν•˜λ©΄ ν”„λ‘¬ν”„νŠΈκ°€ ν‘œμ‹œλ©λ‹ˆλ‹€.';
268
+ detailedEl.dataset.raw = '';
269
+ return;
270
+ }
271
+
272
+ simpleEl.className = 'loading';
273
+ simpleEl.textContent = 'λΆˆλŸ¬μ˜€λŠ” 쀑...';
274
+ detailedEl.className = 'loading';
275
+ detailedEl.textContent = 'λΆˆλŸ¬μ˜€λŠ” 쀑...';
276
+
277
+ try {
278
+ const response = await fetch(`/api/files/${fileId}/chatbot-prompts`, { credentials: 'include' });
279
+ if (!response.ok) throw new Error('ν”„λ‘¬ν”„νŠΈλ₯Ό 뢈러올 수 μ—†μŠ΅λ‹ˆλ‹€.');
280
+ const data = await response.json();
281
+
282
+ if (data.simple_prompt) {
283
+ simpleEl.className = '';
284
+ simpleEl.innerHTML = `<pre>${escapeHtml(data.simple_prompt)}</pre>`;
285
+ simpleEl.dataset.raw = data.simple_prompt;
286
+ } else {
287
+ simpleEl.className = 'empty';
288
+ simpleEl.textContent = 'μ €μž₯된 일반 ν”„λ‘¬ν”„νŠΈκ°€ μ—†μŠ΅λ‹ˆλ‹€.';
289
+ simpleEl.dataset.raw = '';
290
+ }
291
+
292
+ if (data.detailed_prompt) {
293
+ detailedEl.className = '';
294
+ detailedEl.innerHTML = `<pre>${escapeHtml(data.detailed_prompt)}</pre>`;
295
+ detailedEl.dataset.raw = data.detailed_prompt;
296
+ } else {
297
+ detailedEl.className = 'empty';
298
+ detailedEl.textContent = 'μ €μž₯된 상세 ν”„λ‘¬ν”„νŠΈκ°€ μ—†μŠ΅λ‹ˆλ‹€.';
299
+ detailedEl.dataset.raw = '';
300
+ }
301
+
302
+ } catch (e) {
303
+ console.error('ν”„λ‘¬ν”„νŠΈ λ‘œλ“œ 였λ₯˜:', e);
304
+ simpleEl.className = 'error';
305
+ simpleEl.textContent = 'ν”„λ‘¬ν”„νŠΈλ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.';
306
+ detailedEl.className = 'error';
307
+ detailedEl.textContent = 'ν”„λ‘¬ν”„νŠΈλ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.';
308
+ }
309
+ }
310
+
311
+ document.getElementById('fileSelect').addEventListener('change', (e) => loadPrompts(e.target.value));
312
+ loadFiles();
313
+ </script>
314
+ </body>
315
+ </html>
316
+
317
+
templates/admin_files.html CHANGED
@@ -620,6 +620,7 @@
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
 
@@ -660,6 +661,7 @@
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>
 
620
  <button type="button" class="dropdown-toggle">챗봇</button>
621
  <div class="dropdown-menu">
622
  <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
623
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
624
  </div>
625
  </div>
626
 
 
661
 
662
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">챗봇</div>
663
  <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
664
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
665
 
666
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">편의기λŠ₯</div>
667
  <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μœ ν‹Έ</a>
templates/admin_messages.html CHANGED
@@ -606,6 +606,7 @@
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
 
@@ -646,6 +647,7 @@
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>
 
606
  <button type="button" class="dropdown-toggle">챗봇</button>
607
  <div class="dropdown-menu">
608
  <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
609
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
610
  </div>
611
  </div>
612
 
 
647
 
648
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">챗봇</div>
649
  <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
650
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
651
 
652
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">편의기λŠ₯</div>
653
  <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μœ ν‹Έ</a>
templates/admin_prompts.html CHANGED
@@ -460,6 +460,7 @@
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
 
@@ -500,6 +501,7 @@
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>
 
460
  <button type="button" class="dropdown-toggle">챗봇</button>
461
  <div class="dropdown-menu">
462
  <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
463
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
464
  </div>
465
  </div>
466
 
 
501
 
502
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">챗봇</div>
503
  <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
504
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
505
 
506
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">편의기λŠ₯</div>
507
  <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μœ ν‹Έ</a>
templates/admin_settings.html CHANGED
@@ -423,6 +423,7 @@
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
 
@@ -463,6 +464,7 @@
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>
 
423
  <button type="button" class="dropdown-toggle">챗봇</button>
424
  <div class="dropdown-menu">
425
  <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
426
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
427
  </div>
428
  </div>
429
 
 
464
 
465
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">챗봇</div>
466
  <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
467
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
468
 
469
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">편의기λŠ₯</div>
470
  <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μœ ν‹Έ</a>
templates/admin_tags.html CHANGED
@@ -422,6 +422,7 @@
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
 
@@ -462,6 +463,7 @@
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>
 
422
  <button type="button" class="dropdown-toggle">챗봇</button>
423
  <div class="dropdown-menu">
424
  <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
425
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
426
  </div>
427
  </div>
428
 
 
463
 
464
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">챗봇</div>
465
  <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
466
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
467
 
468
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">편의기λŠ₯</div>
469
  <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μœ ν‹Έ</a>
templates/admin_tokens.html CHANGED
@@ -485,6 +485,7 @@
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
 
@@ -525,6 +526,7 @@
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>
 
485
  <button type="button" class="dropdown-toggle">챗봇</button>
486
  <div class="dropdown-menu">
487
  <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
488
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
489
  </div>
490
  </div>
491
 
 
526
 
527
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">챗봇</div>
528
  <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
529
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
530
 
531
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">편의기λŠ₯</div>
532
  <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μœ ν‹Έ</a>
templates/admin_utils.html CHANGED
@@ -506,6 +506,7 @@
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
 
@@ -546,6 +547,7 @@
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>
 
506
  <button type="button" class="dropdown-toggle">챗봇</button>
507
  <div class="dropdown-menu">
508
  <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
509
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
510
  </div>
511
  </div>
512
 
 
547
 
548
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">챗봇</div>
549
  <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
550
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
551
 
552
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">편의기λŠ₯</div>
553
  <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μœ ν‹Έ</a>
templates/admin_webnovels.html CHANGED
@@ -838,6 +838,7 @@
838
  <button type="button" class="dropdown-toggle">챗봇</button>
839
  <div class="dropdown-menu">
840
  <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
 
841
  </div>
842
  </div>
843
 
@@ -878,6 +879,7 @@
878
 
879
  <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>
880
  <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
 
881
 
882
  <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>
883
  <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μœ ν‹Έ</a>
@@ -1352,6 +1354,15 @@
1352
  <button class="btn btn-info" onclick="createMetadata(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 6px 10px; font-size: 12px;">메타데이터 생성</button>
1353
  <button class="btn" style="background: #4caf50; color: white; padding: 6px 10px; font-size: 12px;" onclick="createSimpleTags(${file.id}, '${escapeHtml(file.original_filename)}')">일반 νƒœκ·Έ 생성</button>
1354
  <button class="btn" style="background: #673ab7; color: white; padding: 6px 10px; font-size: 12px;" onclick="createDetailedTags(${file.id}, '${escapeHtml(file.original_filename)}')">상세 νƒœκ·Έ 생성</button>
 
 
 
 
 
 
 
 
 
1355
  <button class="btn btn-primary" onclick="continueUpload(${file.id})" style="padding: 6px 10px; font-size: 12px;">μ΄μ–΄μ„œ μ—…λ‘œλ“œ</button>
1356
  <button class="btn btn-secondary" onclick="deleteFile(${file.id})" style="padding: 6px 10px; font-size: 12px;">μ‚­μ œ</button>
1357
  </div>
@@ -2333,6 +2344,74 @@
2333
  }
2334
  }
2335
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2336
  // λͺ¨λ‹¬ μ™ΈλΆ€ 클릭 μ‹œ λ‹«κΈ°
2337
  window.addEventListener('click', (event) => {
2338
  const modal = document.getElementById('parentChunkModal');
 
838
  <button type="button" class="dropdown-toggle">챗봇</button>
839
  <div class="dropdown-menu">
840
  <a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
841
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="dropdown-item">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
842
  </div>
843
  </div>
844
 
 
879
 
880
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">챗봇</div>
881
  <a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">νƒœκ·Έ/ν”„λ‘¬ν”„νŠΈ</a>
882
+ <a href="{{ url_for('main.admin_chatbot_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">챗봇 ν”„λ‘¬ν”„νŠΈ</a>
883
 
884
  <div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">편의기λŠ₯</div>
885
  <a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μœ ν‹Έ</a>
 
1354
  <button class="btn btn-info" onclick="createMetadata(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 6px 10px; font-size: 12px;">메타데이터 생성</button>
1355
  <button class="btn" style="background: #4caf50; color: white; padding: 6px 10px; font-size: 12px;" onclick="createSimpleTags(${file.id}, '${escapeHtml(file.original_filename)}')">일반 νƒœκ·Έ 생성</button>
1356
  <button class="btn" style="background: #673ab7; color: white; padding: 6px 10px; font-size: 12px;" onclick="createDetailedTags(${file.id}, '${escapeHtml(file.original_filename)}')">상세 νƒœκ·Έ 생성</button>
1357
+ <div class="dropdown">
1358
+ <button class="dropdown-toggle" style="padding: 6px 10px; font-size: 12px; background: #fff; border: 1px solid #dadce0;">
1359
+ ν”„λ‘¬ν”„νŠΈ 생성
1360
+ </button>
1361
+ <div class="dropdown-menu" style="min-width: 220px;">
1362
+ <a href="#" class="dropdown-item" onclick="createSimplePrompt(${file.id}, '${escapeHtml(file.original_filename)}'); return false;">일반 ν”„λ‘¬ν”„νŠΈ 생성</a>
1363
+ <a href="#" class="dropdown-item" onclick="createDetailedPrompt(${file.id}, '${escapeHtml(file.original_filename)}'); return false;">상세 ν”„λ‘¬ν”„νŠΈ 생성</a>
1364
+ </div>
1365
+ </div>
1366
  <button class="btn btn-primary" onclick="continueUpload(${file.id})" style="padding: 6px 10px; font-size: 12px;">μ΄μ–΄μ„œ μ—…λ‘œλ“œ</button>
1367
  <button class="btn btn-secondary" onclick="deleteFile(${file.id})" style="padding: 6px 10px; font-size: 12px;">μ‚­μ œ</button>
1368
  </div>
 
2344
  }
2345
  }
2346
 
2347
+ // 챗봇 ν”„λ‘¬ν”„νŠΈ 생성 (일반)
2348
+ async function createSimplePrompt(fileId, fileName) {
2349
+ if (!confirm(`"${fileName}" 파일의 일반 ν”„λ‘¬ν”„νŠΈλ₯Ό μƒμ„±ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?\n\n일반 νƒœκ·Έ 기반으둜 'μŠ€ν† λ¦¬ 챗봇'μ—μ„œ μ‚¬μš©ν•  ν”„λ‘¬ν”„νŠΈλ₯Ό 생성/μ €μž₯ν•©λ‹ˆλ‹€.`)) {
2350
+ return;
2351
+ }
2352
+
2353
+ try {
2354
+ const response = await fetch(`/api/files/${fileId}/process/prompts/simple`, {
2355
+ method: 'POST',
2356
+ credentials: 'include'
2357
+ });
2358
+
2359
+ let data;
2360
+ try {
2361
+ data = await response.json();
2362
+ } catch {
2363
+ const text = await response.text();
2364
+ console.error('[일반 ν”„λ‘¬ν”„νŠΈ 생성] JSON νŒŒμ‹± μ‹€νŒ¨, 응닡 ν…μŠ€νŠΈ:', text);
2365
+ showAlert(`일반 ν”„λ‘¬ν”„νŠΈ 생성 μ‹€νŒ¨: μ„œλ²„ 응닡 였λ₯˜ (${response.status})`, 'error');
2366
+ return;
2367
+ }
2368
+
2369
+ if (response.ok) {
2370
+ showAlert('일반 ν”„λ‘¬ν”„νŠΈ 생성이 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. (챗봇 ν”„λ‘¬ν”„νŠΈ λ©”λ‰΄μ—μ„œ 확인)', 'success');
2371
+ } else {
2372
+ const errorMsg = data.error || data.message || `μ„œλ²„ 였λ₯˜ (${response.status})`;
2373
+ showAlert(`일반 ν”„λ‘¬ν”„νŠΈ 생성 μ‹€νŒ¨: ${errorMsg}`, 'error');
2374
+ }
2375
+ } catch (error) {
2376
+ console.error('일반 ν”„λ‘¬ν”„νŠΈ 생성 였λ₯˜:', error);
2377
+ showAlert(`일반 ν”„λ‘¬ν”„νŠΈ 생성 쀑 였λ₯˜: ${error.message}`, 'error');
2378
+ }
2379
+ }
2380
+
2381
+ // 챗봇 οΏ½οΏ½οΏ½λ‘¬ν”„νŠΈ 생성 (상세)
2382
+ async function createDetailedPrompt(fileId, fileName) {
2383
+ if (!confirm(`"${fileName}" 파일의 상세 ν”„λ‘¬ν”„νŠΈλ₯Ό μƒμ„±ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?\n\n상세 νƒœκ·Έ 기반으둜 'μŠ€ν† λ¦¬ 챗봇'μ—μ„œ μ‚¬μš©ν•  ν”„λ‘¬ν”„νŠΈλ₯Ό 생성/μ €μž₯ν•©λ‹ˆλ‹€.`)) {
2384
+ return;
2385
+ }
2386
+
2387
+ try {
2388
+ const response = await fetch(`/api/files/${fileId}/process/prompts/detailed`, {
2389
+ method: 'POST',
2390
+ credentials: 'include'
2391
+ });
2392
+
2393
+ let data;
2394
+ try {
2395
+ data = await response.json();
2396
+ } catch {
2397
+ const text = await response.text();
2398
+ console.error('[상세 ν”„λ‘¬ν”„νŠΈ 생성] JSON νŒŒμ‹± μ‹€νŒ¨, 응닡 ν…μŠ€νŠΈ:', text);
2399
+ showAlert(`상세 ν”„λ‘¬ν”„νŠΈ 생성 μ‹€νŒ¨: μ„œλ²„ 응닡 였λ₯˜ (${response.status})`, 'error');
2400
+ return;
2401
+ }
2402
+
2403
+ if (response.ok) {
2404
+ showAlert('상세 ν”„λ‘¬ν”„νŠΈ 생성이 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. (챗봇 ν”„λ‘¬ν”„νŠΈ λ©”λ‰΄μ—μ„œ 확인)', 'success');
2405
+ } else {
2406
+ const errorMsg = data.error || data.message || `μ„œλ²„ 였λ₯˜ (${response.status})`;
2407
+ showAlert(`상세 ν”„λ‘¬ν”„νŠΈ 생성 μ‹€νŒ¨: ${errorMsg}`, 'error');
2408
+ }
2409
+ } catch (error) {
2410
+ console.error('상세 ν”„λ‘¬ν”„νŠΈ 생성 였λ₯˜:', error);
2411
+ showAlert(`상세 ν”„λ‘¬ν”„νŠΈ 생성 쀑 였λ₯˜: ${error.message}`, 'error');
2412
+ }
2413
+ }
2414
+
2415
  // λͺ¨λ‹¬ μ™ΈλΆ€ 클릭 μ‹œ λ‹«κΈ°
2416
  window.addEventListener('click', (event) => {
2417
  const modal = document.getElementById('parentChunkModal');