GitHub Actions commited on
Commit
d2a4597
ยท
1 Parent(s): 9ed5c17

Auto-deploy from GitHub Actions - 2025-12-12 09:16:33

Browse files
Files changed (2) hide show
  1. app/routes.py +66 -46
  2. templates/admin_files.html +121 -0
app/routes.py CHANGED
@@ -4946,7 +4946,7 @@ def process_graph(file_id):
4946
  @main_bp.route('/api/files/<int:file_id>/process/tags', methods=['POST'])
4947
  @login_required
4948
  def generate_tags(file_id):
4949
- """ํƒœ๊ทธ ์ƒ์„ฑ (Parent Chunk, ํšŒ์ฐจ ๋ถ„์„, GraphRAG ํ™œ์šฉ)"""
4950
  try:
4951
  file = UploadedFile.query.get_or_404(file_id)
4952
 
@@ -4964,47 +4964,79 @@ def generate_tags(file_id):
4964
  parent_chunk_text = ""
4965
  if parent_chunk:
4966
  parent_chunk_text = f"""
4967
- [์ž‘ํ’ˆ ์ „์ฒด ์ •๋ณด]
4968
  ์„ธ๊ณ„๊ด€: {parent_chunk.world_view or '์—†์Œ'}
4969
  ์ฃผ์š” ์บ๋ฆญํ„ฐ: {parent_chunk.characters or '์—†์Œ'}
4970
  ์ฃผ์š” ์Šคํ† ๋ฆฌ: {parent_chunk.story or '์—†์Œ'}
 
 
4971
  """
4972
 
4973
  # 2. ํšŒ์ฐจ ๋ถ„์„ (์š”์•ฝ)
4974
- analyses = EpisodeAnalysis.query.filter_by(file_id=file_id).order_by(EpisodeAnalysis.id).limit(10).all()
4975
  analysis_text = ""
4976
  if analyses:
4977
- analysis_text = "[์ฃผ์š” ํšŒ์ฐจ ๋ถ„์„ ๋‚ด์šฉ]\n"
4978
  for analysis in analyses:
4979
  # ๋‚ด์šฉ์ด ๋„ˆ๋ฌด ๊ธธ๋ฉด ์•ž๋ถ€๋ถ„๋งŒ ์‚ฌ์šฉ
4980
- content_preview = analysis.analysis_content[:500] + "..." if len(analysis.analysis_content) > 500 else analysis.analysis_content
4981
  analysis_text += f"- {analysis.episode_title}: {content_preview}\n"
4982
 
4983
- # 3. GraphRAG (์ฃผ์š” ์ธ๋ฌผ, ํ‚ค์›Œ๋“œ)
4984
- entities = GraphEntity.query.filter_by(file_id=file_id).limit(20).all()
4985
- entity_text = ""
4986
- if entities:
4987
- entity_names = [e.entity_name for e in entities]
4988
- entity_text = f"[์ฃผ์š” ๋“ฑ์žฅ ์š”์†Œ]\n{', '.join(entity_names)}"
 
 
 
 
 
 
 
 
 
 
 
4989
 
4990
  # ๋ฐ์ดํ„ฐ๊ฐ€ ๋„ˆ๋ฌด ์—†์œผ๋ฉด ํƒœ๊ทธ ์ƒ์„ฑ ๋ถˆ๊ฐ€
4991
- if not parent_chunk_text and not analysis_text and not entity_text:
4992
  return jsonify({'error': 'ํƒœ๊ทธ๋ฅผ ์ƒ์„ฑํ•  ์ถฉ๋ถ„ํ•œ ๋ฐ์ดํ„ฐ(Parent Chunk, ํšŒ์ฐจ ๋ถ„์„ ๋“ฑ)๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.'}), 400
4993
 
4994
  # ํ”„๋กฌํ”„ํŠธ ๊ตฌ์„ฑ
4995
  prompt = f"""
4996
- ๋‹ค์Œ์€ ์›น์†Œ์„ค์˜ ๋ถ„์„ ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค. ์ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ด ์ž‘ํ’ˆ์„ ์ž˜ ์„ค๋ช…ํ•˜๋Š” ํƒœ๊ทธ 10๊ฐœ~20๊ฐœ๋ฅผ ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”.
4997
 
4998
  {parent_chunk_text}
4999
 
5000
  {analysis_text}
5001
 
5002
- {entity_text}
5003
 
5004
- ์š”๊ตฌ์‚ฌํ•ญ:
5005
- 1. ์žฅ๋ฅด, ๋ถ„์œ„๊ธฐ, ์ฃผ์š” ์†Œ์žฌ, ์บ๋ฆญํ„ฐ ํŠน์„ฑ ๋“ฑ์„ ํฌํ•จํ•˜๋Š” ํƒœ๊ทธ๋ฅผ ์ƒ์„ฑํ•˜์„ธ์š”.
5006
- 2. ๋ฐ˜๋“œ์‹œ JSON ๋ฐฐ์—ด ํ˜•์‹์œผ๋กœ๋งŒ ์‘๋‹ตํ•˜์„ธ์š”. (์˜ˆ: ["ํŒํƒ€์ง€", "๋กœ๋งจ์Šค", "์„ฑ์žฅ๋ฌผ", "๋งˆ๋ฒ•"])
5007
- 3. ๋‹ค๋ฅธ ์„ค๋ช… ์—†์ด JSON ๋ฐฐ์—ด๋งŒ ์ถœ๋ ฅํ•˜์„ธ์š”.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5008
  """
5009
 
5010
  # AI ํ˜ธ์ถœ
@@ -5024,8 +5056,8 @@ def generate_tags(file_id):
5024
  result = gemini_client.generate_response(
5025
  prompt=prompt,
5026
  model_name=gemini_model_name,
5027
- temperature=0.7,
5028
- max_output_tokens=1024
5029
  )
5030
  if not result['error'] and result.get('response'):
5031
  response_text = result['response'].strip()
@@ -5035,15 +5067,7 @@ def generate_tags(file_id):
5035
  # Ollama ๋ชจ๋ธ์ธ ๊ฒฝ์šฐ (๋˜๋Š” Gemini ์‹คํŒจ ์‹œ)
5036
  if not response_text:
5037
  try:
5038
- ollama_model_name = file.model_name
5039
- if ollama_model_name.lower().startswith('gemini:'): # Gemini ๋ชจ๋ธ๋ช…์ด์ง€๋งŒ Ollama๋กœ ์‹œ๋„ํ•˜๋Š” ๊ฒฝ์šฐ (์„ค์ • ์˜ค๋ฅ˜ ๋“ฑ)
5040
- # ๊ธฐ๋ณธ Ollama ๋ชจ๋ธ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
5041
- pass
5042
-
5043
- # Ollama ํ˜ธ์ถœ ๋กœ์ง (generate_response_with_ollama ์‚ฌ์šฉ)
5044
- # ์—ฌ๊ธฐ์„œ๋Š” ์ง์ ‘ ํ˜ธ์ถœ ๋Œ€์‹  ๊ธฐ์กด ํ•จ์ˆ˜ ํ™œ์šฉ
5045
- # app/routes.py์— ollama ๊ด€๋ จ ํ•จ์ˆ˜๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ ํ•„์š”ํ•˜์ง€๋งŒ,
5046
- # ๊ฐ„๋‹จํ•˜๊ฒŒ requests๋กœ ํ˜ธ์ถœ ๊ตฌํ˜„
5047
  import requests
5048
  from app.core.config import get_config
5049
  config = get_config()
@@ -5054,9 +5078,10 @@ def generate_tags(file_id):
5054
  'model': file.model_name,
5055
  'prompt': prompt,
5056
  'stream': False,
5057
- 'options': {'temperature': 0.7}
 
5058
  },
5059
- timeout=120
5060
  )
5061
 
5062
  if ollama_response.status_code == 200:
@@ -5072,33 +5097,28 @@ def generate_tags(file_id):
5072
  import json
5073
  import re
5074
 
5075
- tags = []
5076
  try:
5077
  # JSON ์ถ”์ถœ ์‹œ๋„
5078
- json_match = re.search(r'\[.*\]', response_text, re.DOTALL)
5079
  if json_match:
5080
- tags = json.loads(json_match.group(0))
 
5081
  else:
5082
- # JSON ํ˜•์‹์ด ์•„๋‹ˆ๋ฉด ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ์ฒ˜๋ฆฌ ์‹œ๋„
5083
- tags = [t.strip() for t in response_text.replace('[', '').replace(']', '').replace('"', '').split(',')]
 
5084
  except Exception as e:
5085
  print(f"[ํƒœ๊ทธ ์ƒ์„ฑ] ํŒŒ์‹ฑ ์˜ค๋ฅ˜: {str(e)}")
5086
- # ์‰ผํ‘œ๋กœ ๋‹จ์ˆœ ๋ถ„๋ฆฌ ์‹œ๋„
5087
- tags = [t.strip() for t in response_text.split(',')]
5088
-
5089
- # ๋นˆ ํƒœ๊ทธ ์ œ๊ฑฐ ๋ฐ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅ
5090
- tags = [t for t in tags if t]
5091
-
5092
- if not tags:
5093
- return jsonify({'error': 'ํƒœ๊ทธ๋ฅผ ์ถ”์ถœํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.'}), 500
5094
 
5095
  # DB ์ €์žฅ (JSON ๋ฌธ์ž์—ด๋กœ ์ €์žฅ)
5096
- file.tags = json.dumps(tags, ensure_ascii=False)
5097
  db.session.commit()
5098
 
5099
  return jsonify({
5100
  'message': 'ํƒœ๊ทธ๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
5101
- 'tags': tags,
5102
  'file_id': file.id
5103
  }), 200
5104
 
 
4946
  @main_bp.route('/api/files/<int:file_id>/process/tags', methods=['POST'])
4947
  @login_required
4948
  def generate_tags(file_id):
4949
+ """ํƒœ๊ทธ ์ƒ์„ฑ (Parent Chunk, ํšŒ์ฐจ ๋ถ„์„, GraphRAG ํ™œ์šฉ) - ๊ณ„์ธต์  ๊ตฌ์กฐ"""
4950
  try:
4951
  file = UploadedFile.query.get_or_404(file_id)
4952
 
 
4964
  parent_chunk_text = ""
4965
  if parent_chunk:
4966
  parent_chunk_text = f"""
4967
+ [Parent Chunk]
4968
  ์„ธ๊ณ„๊ด€: {parent_chunk.world_view or '์—†์Œ'}
4969
  ์ฃผ์š” ์บ๋ฆญํ„ฐ: {parent_chunk.characters or '์—†์Œ'}
4970
  ์ฃผ์š” ์Šคํ† ๋ฆฌ: {parent_chunk.story or '์—†์Œ'}
4971
+ ์—ํ”ผ์†Œ๋“œ ์š”์•ฝ: {parent_chunk.episodes or '์—†์Œ'}
4972
+ ๊ธฐํƒ€: {parent_chunk.others or '์—†์Œ'}
4973
  """
4974
 
4975
  # 2. ํšŒ์ฐจ ๋ถ„์„ (์š”์•ฝ)
4976
+ analyses = EpisodeAnalysis.query.filter_by(file_id=file_id).order_by(EpisodeAnalysis.id).limit(20).all()
4977
  analysis_text = ""
4978
  if analyses:
4979
+ analysis_text = "[ํšŒ์ฐจ๋ณ„ ๋ถ„์„ (์š”์•ฝ)]\n"
4980
  for analysis in analyses:
4981
  # ๋‚ด์šฉ์ด ๋„ˆ๋ฌด ๊ธธ๋ฉด ์•ž๋ถ€๋ถ„๋งŒ ์‚ฌ์šฉ
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
+ # 3. GraphRAG (์ฃผ์š” ์ธ๋ฌผ, ๊ด€๊ณ„, ์‚ฌ๊ฑด)
4986
+ entities = GraphEntity.query.filter_by(file_id=file_id).limit(30).all()
4987
+ relationships = GraphRelationship.query.filter_by(file_id=file_id).limit(30).all()
4988
+ events = GraphEvent.query.filter_by(file_id=file_id).limit(30).all()
4989
+
4990
+ graph_text = ""
4991
+ if entities or relationships or events:
4992
+ graph_text = "[GraphRAG ๋ฐ์ดํ„ฐ]\n"
4993
+ if entities:
4994
+ entity_names = [f"{e.entity_name}({e.entity_type})" for e in entities]
4995
+ graph_text += f"์—”ํ‹ฐํ‹ฐ: {', '.join(entity_names)}\n"
4996
+ if relationships:
4997
+ rel_strs = [f"{r.source}->{r.target}({r.relationship_type})" for r in relationships]
4998
+ graph_text += f"๊ด€๊ณ„: {', '.join(rel_strs)}\n"
4999
+ if events:
5000
+ event_names = [e.event_name for e in events]
5001
+ graph_text += f"์‚ฌ๊ฑด: {', '.join(event_names)}\n"
5002
 
5003
  # ๋ฐ์ดํ„ฐ๊ฐ€ ๋„ˆ๋ฌด ์—†์œผ๋ฉด ํƒœ๊ทธ ์ƒ์„ฑ ๋ถˆ๊ฐ€
5004
+ if not parent_chunk_text and not analysis_text and not graph_text:
5005
  return jsonify({'error': 'ํƒœ๊ทธ๋ฅผ ์ƒ์„ฑํ•  ์ถฉ๋ถ„ํ•œ ๋ฐ์ดํ„ฐ(Parent Chunk, ํšŒ์ฐจ ๋ถ„์„ ๋“ฑ)๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.'}), 400
5006
 
5007
  # ํ”„๋กฌํ”„ํŠธ ๊ตฌ์„ฑ
5008
  prompt = f"""
5009
+ ๋‹ค์Œ์€ ์›น์†Œ์„ค์˜ ๋ถ„์„ ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค. ์ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๊ฐ ์„น์…˜๋ณ„๋กœ ํ•ต์‹ฌ ํƒœ๊ทธ๋ฅผ ์ถ”์ถœํ•ด์ฃผ์„ธ์š”.
5010
 
5011
  {parent_chunk_text}
5012
 
5013
  {analysis_text}
5014
 
5015
+ {graph_text}
5016
 
5017
+ [์š”๊ตฌ์‚ฌํ•ญ]
5018
+ ๋‹ค์Œ JSON ํ˜•์‹์œผ๋กœ ํƒœ๊ทธ๋ฅผ ์ƒ์„ฑํ•˜์„ธ์š”. ๊ฐ ํ•ญ๋ชฉ๋‹น 5~10๊ฐœ์˜ ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ๋ฅผ ์ถ”์ถœํ•˜์„ธ์š”. ์—†๋Š” ํ•ญ๋ชฉ์€ ๋นˆ ๋ฐฐ์—ด๋กœ ๋‘์„ธ์š”.
5019
+
5020
+ {{
5021
+ "parent_chunk": {{
5022
+ "world_view": ["ํƒœ๊ทธ1", "ํƒœ๊ทธ2"],
5023
+ "characters": ["ํƒœ๊ทธ1", "ํƒœ๊ทธ2"],
5024
+ "story": ["ํƒœ๊ทธ1", "ํƒœ๊ทธ2"],
5025
+ "others": ["ํƒœ๊ทธ1", "ํƒœ๊ทธ2"]
5026
+ }},
5027
+ "episodes": {{
5028
+ "world_view": ["ํƒœ๊ทธ1", "ํƒœ๊ทธ2"],
5029
+ "characters": ["ํƒœ๊ทธ1", "ํƒœ๊ทธ2"],
5030
+ "items": ["ํƒœ๊ทธ1", "ํƒœ๊ทธ2"]
5031
+ }},
5032
+ "graph_rag": {{
5033
+ "characters": ["ํƒœ๊ทธ1", "ํƒœ๊ทธ2"],
5034
+ "relationships": ["ํƒœ๊ทธ1", "ํƒœ๊ทธ2"],
5035
+ "events": ["ํƒœ๊ทธ1", "ํƒœ๊ทธ2"]
5036
+ }}
5037
+ }}
5038
+
5039
+ ๋ฐ˜๋“œ์‹œ JSON ํ˜•์‹์œผ๋กœ๋งŒ ์‘๋‹ตํ•˜์„ธ์š”. ๋‹ค๋ฅธ ์„ค๋ช…์€ ํฌํ•จํ•˜์ง€ ๋งˆ์„ธ์š”. JSON ๊ตฌ๋ฌธ์„ ์ •ํ™•ํžˆ ์ง€์ผœ์ฃผ์„ธ์š”.
5040
  """
5041
 
5042
  # AI ํ˜ธ์ถœ
 
5056
  result = gemini_client.generate_response(
5057
  prompt=prompt,
5058
  model_name=gemini_model_name,
5059
+ temperature=0.5,
5060
+ max_output_tokens=2048
5061
  )
5062
  if not result['error'] and result.get('response'):
5063
  response_text = result['response'].strip()
 
5067
  # Ollama ๋ชจ๋ธ์ธ ๊ฒฝ์šฐ (๋˜๋Š” Gemini ์‹คํŒจ ์‹œ)
5068
  if not response_text:
5069
  try:
5070
+ # Ollama ํ˜ธ์ถœ ๋กœ์ง
 
 
 
 
 
 
 
 
5071
  import requests
5072
  from app.core.config import get_config
5073
  config = get_config()
 
5078
  'model': file.model_name,
5079
  'prompt': prompt,
5080
  'stream': False,
5081
+ 'options': {'temperature': 0.5},
5082
+ 'format': 'json' # Ollama JSON ๋ชจ๋“œ ํ™œ์„ฑํ™”
5083
  },
5084
+ timeout=180
5085
  )
5086
 
5087
  if ollama_response.status_code == 200:
 
5097
  import json
5098
  import re
5099
 
5100
+ tags_data = {}
5101
  try:
5102
  # JSON ์ถ”์ถœ ์‹œ๋„
5103
+ json_match = re.search(r'\{.*\}', response_text, re.DOTALL)
5104
  if json_match:
5105
+ json_str = json_match.group(0)
5106
+ tags_data = json.loads(json_str)
5107
  else:
5108
+ # JSON ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ
5109
+ print(f"[ํƒœ๊ทธ ์ƒ์„ฑ] JSON ํ˜•์‹์ด ์•„๋‹˜: {response_text[:100]}...")
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 ์ €์žฅ (JSON ๋ฌธ์ž์—ด๋กœ ์ €์žฅ)
5116
+ file.tags = json.dumps(tags_data, ensure_ascii=False)
5117
  db.session.commit()
5118
 
5119
  return jsonify({
5120
  'message': 'ํƒœ๊ทธ๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
5121
+ 'tags': tags_data,
5122
  'file_id': file.id
5123
  }), 200
5124
 
templates/admin_files.html CHANGED
@@ -674,6 +674,19 @@
674
  </div>
675
  </div>
676
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  <!-- vis-network ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ -->
678
  <script type="text/javascript" src="https://unpkg.com/vis-network@latest/standalone/umd/vis-network.min.js"></script>
679
 
@@ -798,6 +811,7 @@
798
  <div class="file-actions">
799
  <button class="btn btn-secondary" onclick="viewSummary(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">์š”์•ฝ ๋‚ด์šฉ ๋ณด๊ธฐ</button>
800
  <button class="btn btn-info" onclick="viewChunks(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">์ฒญํฌ ๋ณด๊ธฐ</button>
 
801
  <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>
802
  <button class="btn btn-success" onclick="viewGraphRAGVisualization(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px;">๊ทธ๋ž˜ํ”„ ์‹œ๊ฐํ™”</button>
803
  </div>
@@ -1835,6 +1849,113 @@
1835
  }
1836
  });
1837
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1838
  // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ดˆ๊ธฐํ™”
1839
  window.addEventListener('load', () => {
1840
  loadModelFilter();
 
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()">&times;</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>
692
 
 
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
  }
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', () => {
1961
  loadModelFilter();