jeongsoo commited on
Commit
9acece8
Β·
1 Parent(s): 6539bc3

Add application file

Browse files
Files changed (1) hide show
  1. app/app.py +193 -294
app/app.py CHANGED
@@ -1,201 +1,4 @@
1
- @app.route('/api/upload', methods=['POST'])
2
- @login_required
3
- def upload_document():
4
- """μ§€μ‹λ² μ΄μŠ€ λ¬Έμ„œ μ—…λ‘œλ“œ API"""
5
- global base_retriever, retriever, app_ready
6
-
7
- # μ•± μ€€λΉ„ μƒνƒœ 확인
8
- if not app_ready:
9
- return jsonify({"error": "앱이 아직 μ΄ˆκΈ°ν™” μ€‘μž…λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”."}), 503
10
-
11
- try:
12
- # 파일이 μš”μ²­μ— ν¬ν•¨λ˜μ–΄ μžˆλŠ”μ§€ 확인
13
- if 'document' not in request.files:
14
- return jsonify({"error": "λ¬Έμ„œ 파일이 μ œκ³΅λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}), 400
15
-
16
- doc_file = request.files['document']
17
- logger.info(f"받은 파일λͺ…: {doc_file.filename}")
18
-
19
- # 파일λͺ…이 λΉ„μ–΄μžˆλŠ”μ§€ 확인
20
- if doc_file.filename == '':
21
- return jsonify({"error": "μ„ νƒλœ 파일이 μ—†μŠ΅λ‹ˆλ‹€."}), 400
22
-
23
- # 파일 ν˜•μ‹ 확인
24
- if not allowed_doc_file(doc_file.filename):
25
- logger.error(f"ν—ˆμš©λ˜μ§€ μ•ŠλŠ” 파일 ν˜•μ‹: {doc_file.filename}")
26
- return jsonify({"error": "ν—ˆμš©λ˜μ§€ μ•ŠλŠ” 파일 ν˜•μ‹μž…λ‹ˆλ‹€. ν˜„μž¬ ν—ˆμš©λœ 파일 ν˜•μ‹: {}".format(', '.join(ALLOWED_DOC_EXTENSIONS))}), 400
27
-
28
- # 파일λͺ… λ³΄μ•ˆ 처리
29
- filename = secure_filename(doc_file.filename)
30
-
31
- # 데이터 폴더에 μ €μž₯
32
- filepath = os.path.join(app.config['DATA_FOLDER'], filename)
33
- doc_file.save(filepath)
34
-
35
- logger.info(f"λ¬Έμ„œκ°€ μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€: {filepath}")
36
-
37
- # λ¬Έμ„œ 처리
38
- try:
39
- # λ¨Όμ € UTF-8둜 μ‹œλ„
40
- try:
41
- with open(filepath, 'r', encoding='utf-8') as f:
42
- content = f.read()
43
- except UnicodeDecodeError:
44
- # UTF-8둜 μ‹€νŒ¨ν•˜λ©΄ CP949(ν•œκ΅­μ–΄ Windows κΈ°λ³Έ 인코딩)둜 μ‹œλ„
45
- logger.info(f"UTF-8 λ””μ½”λ”© μ‹€νŒ¨, CP949둜 μ‹œλ„: {filename}")
46
- with open(filepath, 'r', encoding='cp949') as f:
47
- content = f.read()
48
-
49
- # 메타데이터 생성
50
- metadata = {
51
- "source": filename,
52
- "filename": filename,
53
- "filetype": filename.rsplit('.', 1)[1].lower(),
54
- "filepath": filepath
55
- }
56
-
57
- # 파일 ν˜•μ‹μ— 따라 λ‹€λ₯Έ 처리 적용
58
- file_ext = filename.rsplit('.', 1)[1].lower()
59
-
60
- # CSV νŒŒμΌμ€ ν–‰ λ‹¨μœ„λ‘œ 처리
61
- if file_ext == 'csv':
62
- logger.info(f"CSV 파일 μ—…λ‘œλ“œ 감지, ν–‰ λ‹¨μœ„λ‘œ λΆ„ν•  처리: {filename}")
63
- docs = DocumentProcessor.csv_to_documents(content, metadata)
64
- else:
65
- # 일반 ν…μŠ€νŠΈ λ¬Έμ„œ 처리
66
- docs = DocumentProcessor.text_to_documents(
67
- content,
68
- metadata=metadata,
69
- chunk_size=512,
70
- chunk_overlap=50
71
- )
72
-
73
- if docs:
74
- logger.info(f"{len(docs)}개 λ¬Έμ„œ 청크λ₯Ό 검색기에 μΆ”κ°€ν•©λ‹ˆλ‹€...")
75
- base_retriever.add_documents(docs)
76
-
77
- # 인덱슀 μ €μž₯
78
- logger.info(f"검색기 μƒνƒœλ₯Ό μ €μž₯ν•©λ‹ˆλ‹€...")
79
- index_path = app.config['INDEX_PATH']
80
- try:
81
- base_retriever.save(index_path)
82
- logger.info("인덱슀 μ €μž₯ μ™„λ£Œ")
83
- except Exception as e:
84
- logger.error(f"인덱슀 μ €μž₯ 쀑 였λ₯˜ λ°œμƒ: {e}")
85
- return jsonify({"error": f"인덱슀 μ €μž₯ 쀑 였λ₯˜: {str(e)}"}), 500
86
-
87
- return jsonify({
88
- "success": True,
89
- "message": f"파일 '{filename}'κ°€ μ„±κ³΅μ μœΌλ‘œ μ—…λ‘œλ“œλ˜κ³  {len(docs)}개 청크가 μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€."
90
- })
91
- else:
92
- logger.warning(f"파일 '{filename}'μ—μ„œ μ²˜λ¦¬ν•  λ¬Έμ„œκ°€ μ—†μŠ΅λ‹ˆλ‹€.")
93
- return jsonify({
94
- "warning": True,
95
- "message": f"파일 '{filename}'이 μ €μž₯λ˜μ—ˆμ§€λ§Œ μ²˜λ¦¬ν•  λ‚΄μš©μ΄ μ—†μŠ΅λ‹ˆλ‹€."
96
- })
97
-
98
- except Exception as e:
99
- logger.error(f"λ¬Έμ„œ '{filename}' 처리 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
100
- return jsonify({"error": f"λ¬Έμ„œ 처리 쀑 였λ₯˜: {str(e)}"}), 500
101
-
102
- except Exception as e:
103
- logger.error(f"파일 μ—…λ‘œλ“œ 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
104
- return jsonify({"error": f"파일 μ—…λ‘œλ“œ 쀑 였λ₯˜: {str(e)}"}), 500
105
-
106
- @app.route('/api/documents', methods=['GET'])
107
- @login_required
108
- def list_documents():
109
- """μ§€μ‹λ² μ΄μŠ€ λ¬Έμ„œ λͺ©λ‘ API"""
110
- global base_retriever, retriever, app_ready
111
-
112
- # μ•± μ€€λΉ„ μƒνƒœ 확인
113
- if not app_ready:
114
- return jsonify({"error": "앱이 아직 μ΄ˆκΈ°ν™” μ€‘μž…λ‹ˆλ‹€. μž μ‹œ ν›„ οΏ½οΏ½μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”."}), 503
115
-
116
- try:
117
- # λ¬Έμ„œ μ†ŒμŠ€ λͺ©λ‘ 생성
118
- sources = {}
119
-
120
- if base_retriever and base_retriever.documents:
121
- for doc in base_retriever.documents:
122
- source = doc.get("source", "unknown")
123
- if source in sources:
124
- sources[source]["chunks"] += 1
125
- else:
126
- sources[source] = {
127
- "filename": doc.get("filename", source),
128
- "chunks": 1,
129
- "filetype": doc.get("filetype", "unknown")
130
- }
131
-
132
- # λͺ©λ‘ ν˜•μ‹μœΌλ‘œ λ³€ν™˜
133
- documents = []
134
- for source, info in sources.items():
135
- documents.append({
136
- "source": source,
137
- "filename": info["filename"],
138
- "chunks": info["chunks"],
139
- "filetype": info["filetype"]
140
- })
141
-
142
- # 청크 수둜 μ •λ ¬
143
- documents.sort(key=lambda x: x["chunks"], reverse=True)
144
-
145
- return jsonify({
146
- "documents": documents,
147
- "total_documents": len(documents),
148
- "total_chunks": sum(doc["chunks"] for doc in documents)
149
- })
150
-
151
- except Exception as e:
152
- logger.error(f"λ¬Έμ„œ λͺ©λ‘ 쑰회 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
153
- return jsonify({"error": f"λ¬Έμ„œ λͺ©λ‘ 쑰회 쀑 였λ₯˜: {str(e)}"}), 500
154
-
155
- # 정적 파일 μ„œλΉ™
156
- @app.route('/static/<path:path>')
157
- def send_static(path):
158
- return send_from_directory('static', path)# μ„Έμ…˜ μΏ ν‚€ 처리 확인 및 μˆ˜μ •
159
- @app.before_request
160
- def process_cookies():
161
- # μˆ˜λ™ μΏ ν‚€ 처리 확인
162
- if 'session_data' in request.cookies and 'logged_in' not in session:
163
- try:
164
- cookie_data = json.loads(request.cookies.get('session_data'))
165
- logger.info(f"\n[Before Request] μˆ˜λ™ μΏ ν‚€ κ°’ 발견: {cookie_data}")
166
- if cookie_data.get('logged_in'):
167
- # μ„Έμ…˜ μž¬κ΅¬μ„±
168
- session['logged_in'] = True
169
- session['username'] = cookie_data.get('username')
170
- logger.info(f"\n[Before Request] μˆ˜λ™ μΏ ν‚€μ—μ„œ μ„Έμ…˜ 볡원: {session}")
171
- except Exception as e:
172
- logger.error(f"\n[Before Request] μΏ ν‚€ 처리 였λ₯˜: {e}")# ν—ˆκΉ…νŽ˜μ΄μŠ€ ν™˜κ²½μ„ μœ„ν•œ μ„Έμ…˜ 처리 ν–₯상
173
- @app.after_request
174
- def after_request_func(response):
175
- # μ„Έμ…˜μ΄ μˆ˜μ •λ˜μ—ˆλŠ”μ§€ 확인
176
- if session.modified:
177
- logger.info("\n[After Request] μ„Έμ…˜μ΄ μˆ˜μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
178
- logger.info(f"\ud604재 μ„Έμ…˜ λ‚΄μš©: {session}")
179
-
180
- # 응닡 헀더 λ‘œκΉ…
181
- logger.info("\n[Response Headers]")
182
- for header, value in response.headers:
183
- logger.info(f" {header}: {value}")
184
-
185
- # μΏ ν‚€ μ„€μ •
186
- if 'Set-Cookie' in response.headers:
187
- logger.info(f"Set-Cookie 헀더 있음: {response.headers['Set-Cookie']}")
188
- else:
189
- # 둜그인 ν›„ μ„Έμ…˜ μΏ ν‚€κ°€ μ—†μœΌλ©΄ μ„Έμ…˜ 값을 확인
190
- if 'logged_in' in session and request.path != '/login':
191
- logger.info("μ„Έμ…˜μ— logged_in이 μžˆμ§€λ§Œ μΏ ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
192
-
193
- # ν—ˆκΉ…νŽ˜μ΄μŠ€ ν”„λ‘μ‹œ κ΄€λ ¨ 헀더 처리
194
- response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
195
- response.headers['Pragma'] = 'no-cache'
196
- response.headers['Expires'] = '0'
197
-
198
- return response"""
199
  RAG 검색 챗봇 μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜
200
  """
201
 
@@ -751,7 +554,7 @@ def voice_chat():
751
  logger.info(f"[μŒμ„±μ±—] CSV 파일 처리: {doc['source']}")
752
  logger.info(f"[μŒμ„±μ±—] CSV λ‚΄μš© 처음 λΆ€λΆ„: {doc['text'][:100]}...")
753
 
754
- # 첫 번째 λΌμΈμ΄λ‚˜ λ‚΄μš©μ—μ„œ 콀럼 κ°’ μΆ”μΆœ μ‹œλ„
755
  try:
756
  # ν…μŠ€νŠΈμ˜ 처음 뢀뢄을 μΆ”μΆœ
757
  text_lines = doc["text"].strip().split('\n')
@@ -763,10 +566,10 @@ def voice_chat():
763
 
764
  if ',' in first_line: # CSV ν˜•μ‹μ΄λ©΄
765
  first_columns = first_line.split(',')
766
- logger.info(f"[μŒμ„±μ±—] CSV 콀럼 개수: {len(first_columns)}")
767
 
768
  first_column = first_columns[0].strip()
769
- logger.info(f"[μŒμ„±μ±—] CSV 첫 번째 콀럼 κ°’: '{first_column}'")
770
  source_info["id"] = first_column
771
  logger.info(f"[μŒμ„±μ±—] source_info에 id μΆ”κ°€: {source_info}")
772
  else:
@@ -774,7 +577,7 @@ def voice_chat():
774
  else:
775
  logger.warning(f"[μŒμ„±μ±—] CSV νŒŒμΌμ΄μ§€λ§Œ 라인이 μ—†μŒ: {doc['source']}")
776
  except Exception as e:
777
- logger.warning(f"[μŒμ„±μ±—] CSV 첫 번째 콀럼 μΆ”μΆœ μ‹€νŒ¨: {e}")
778
 
779
  enhanced_sources.append(source_info)
780
 
@@ -795,110 +598,206 @@ def voice_chat():
795
  "error": "μŒμ„± 처리 쀑 λ‚΄λΆ€ 였λ₯˜ λ°œμƒ",
796
  "details": str(e)
797
  }), 500
798
- """
799
- logger.info("μŒμ„± μ±— μš”μ²­ μˆ˜μ‹ ")
800
-
801
- # μ˜€λ””μ˜€ 파일 확인
802
- if 'audio' not in request.files:
803
- logger.error("μ˜€λ””μ˜€ 파일이 μ œκ³΅λ˜μ§€ μ•ŠμŒ")
804
- return jsonify({"error": "μ˜€λ””μ˜€ 파일이 μ œκ³΅λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}), 400
805
 
806
- audio_file = request.files['audio']
807
- logger.info(f"μˆ˜μ‹ λœ 파일: {audio_file.filename}")
 
808
 
809
  try:
810
- # μ˜€λ””μ˜€ 파일 읽기
811
- with audio_file.stream as f:
812
- audio_bytes = f.read()
813
-
814
- # μŒμ„±μΈμ‹ (VitoSTT)
815
- stt = VitoSTT()
816
- stt_result = stt.transcribe_audio(audio_bytes, language="ko")
817
 
818
- if not stt_result["success"]:
819
- logger.error(f"μŒμ„±μΈμ‹ μ‹€νŒ¨: {stt_result['error']}")
820
- return jsonify({
821
- "error": stt_result["error"],
822
- "details": stt_result.get("details", "")
823
- }), 500
824
 
825
- transcription = stt_result["text"]
826
- if not transcription:
827
- logger.warning("μŒμ„±μΈμ‹ κ²°κ³Όκ°€ λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€.")
828
- return jsonify({"error": "μŒμ„±μ—μ„œ ν…μŠ€νŠΈλ₯Ό μΈμ‹ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."}), 400
829
 
830
- logger.info(f"μŒμ„±μΈμ‹ 성곡: {transcription[:50]}...")
 
 
 
831
 
832
- # 검색기 호좜: μΈμ‹λœ ν…μŠ€νŠΈλ₯Ό 쿼리둜 μ‚¬μš©
833
- sources = retriever.search(transcription, top_k=5, first_stage_k=6)
834
- if not sources:
835
- logger.warning("κ²€μƒ‰λœ μ†ŒμŠ€κ°€ μ—†μŠ΅λ‹ˆλ‹€.")
836
- sources = []
837
 
838
- # μ†ŒμŠ€ λ¬Έμ„œ λ‚΄μš©μ„ μ»¨ν…μŠ€νŠΈλ‘œ μ€€λΉ„
839
- context = DocumentProcessor.prepare_rag_context(sources, field="text")
840
- logger.info(f"κ²€μƒ‰λœ μ†ŒμŠ€ 수: {len(sources)}")
841
 
842
- # LLM 호좜: 질문과 μ»¨ν…μŠ€νŠΈλ₯Ό λ°”νƒ•μœΌλ‘œ 응닡 생성
843
- llm_id = request.form.get('llm_id', None) # ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ LLM 선택이 제곡되면 μ‚¬μš©
844
- answer = llm_interface.rag_generate(transcription, context, llm_id=llm_id)
845
 
846
- # μ†ŒμŠ€ 정보 μΆ”μΆœ
847
- enhanced_sources = []
848
- for doc in sources:
849
- if "source" in doc:
850
- source_info = {
851
- "source": doc.get("source", "Unknown"),
852
- "score": doc.get("rerank_score", doc.get("score", 0))
853
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
854
 
855
- # CSV 파일인 경우 첫 번째 컨텐츠 데이터λ₯Ό μΆ”μΆœν•˜μ—¬ ν‘œμ‹œ
856
- if "text" in doc and "filetype" in doc and doc["filetype"] == "csv":
857
- # 디버깅 둜그 μΆ”κ°€
858
- logger.info(f"[μŒμ„±μ±—] CSV 파일 처리: {doc['source']}")
859
- logger.info(f"[μŒμ„±μ±—] CSV λ‚΄μš© 처음 λΆ€λΆ„: {doc['text'][:100]}...")
860
-
861
- # 첫 번째 λΌμΈμ΄λ‚˜ λ‚΄μš©μ—μ„œ 컬럼 κ°’ μΆ”μΆœ μ‹œλ„
862
- try:
863
- # ν…μŠ€νŠΈμ˜ 처음 뢀뢄을 μΆ”μΆœ
864
- text_lines = doc["text"].strip().split('\n')
865
- logger.info(f"[μŒμ„±μ±—] CSV 라인 개수: {len(text_lines)}")
866
-
867
- if len(text_lines) > 0:
868
- first_line = text_lines[0].strip()
869
- logger.info(f"[μŒμ„±μ±—] CSV 첫 쀄: {first_line}")
870
-
871
- if ',' in first_line: # CSV ν˜•μ‹μ΄λ©΄
872
- first_columns = first_line.split(',')
873
- logger.info(f"[μŒμ„±μ±—] CSV 컬럼 개수: {len(first_columns)}")
874
-
875
- first_column = first_columns[0].strip()
876
- logger.info(f"[μŒμ„±μ±—] CSV 첫 번째 컬럼 κ°’: '{first_column}'")
877
- source_info["id"] = first_column
878
- logger.info(f"[μŒμ„±μ±—] source_info에 id μΆ”κ°€: {source_info}")
879
- else:
880
- logger.warning(f"[μŒμ„±μ±—] CSV νŒŒμΌμ΄μ§€λ§Œ μ½”λ§ˆκ°€ μ—†μŒ: {first_line}")
881
- else:
882
- logger.warning(f"[μŒμ„±μ±—] CSV νŒŒμΌμ΄μ§€λ§Œ 라인이 μ—†μŒ: {doc['source']}")
883
- except Exception as e:
884
- logger.warning(f"[μŒμ„±μ±—] CSV 첫 번째 컬럼 μΆ”μΆœ μ‹€νŒ¨: {e}")
885
 
886
- enhanced_sources.append(source_info)
887
-
888
- # μ΅œμ’… 응닡 ꡬ쑰 λ‘œκΉ…
889
- response_data = {
890
- "transcription": transcription,
891
- "answer": answer,
892
- "sources": enhanced_sources,
893
- "llm": llm_interface.get_current_llm_details()
894
- }
895
- logger.debug(f"[μŒμ„±μ±—] μ΅œμ’… API 응닡 ꡬ쑰: {json.dumps(response_data, ensure_ascii=False, indent=2)[:500]}...")
 
 
 
 
896
 
897
- return jsonify(response_data)
898
-
899
  except Exception as e:
900
- logger.error(f"μŒμ„± μ±— 처리 쀑 였λ₯˜ λ°œμƒ: {str(e)}", exc_info=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
901
  return jsonify({
902
- "error": "μŒμ„± 처리 쀑 λ‚΄λΆ€ 였λ₯˜ λ°œμƒ",
903
- "details": str(e)
904
- }), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  RAG 검색 챗봇 μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜
3
  """
4
 
 
554
  logger.info(f"[μŒμ„±μ±—] CSV 파일 처리: {doc['source']}")
555
  logger.info(f"[μŒμ„±μ±—] CSV λ‚΄μš© 처음 λΆ€λΆ„: {doc['text'][:100]}...")
556
 
557
+ # 첫 번째 λΌμΈμ΄λ‚˜ λ‚΄μš©μ—μ„œ 컬럼 κ°’ μΆ”μΆœ μ‹œλ„
558
  try:
559
  # ν…μŠ€νŠΈμ˜ 처음 뢀뢄을 μΆ”μΆœ
560
  text_lines = doc["text"].strip().split('\n')
 
566
 
567
  if ',' in first_line: # CSV ν˜•μ‹μ΄λ©΄
568
  first_columns = first_line.split(',')
569
+ logger.info(f"[μŒμ„±μ±—] CSV 컬럼 개수: {len(first_columns)}")
570
 
571
  first_column = first_columns[0].strip()
572
+ logger.info(f"[μŒμ„±μ±—] CSV 첫 번째 컬럼 κ°’: '{first_column}'")
573
  source_info["id"] = first_column
574
  logger.info(f"[μŒμ„±μ±—] source_info에 id μΆ”κ°€: {source_info}")
575
  else:
 
577
  else:
578
  logger.warning(f"[μŒμ„±μ±—] CSV νŒŒμΌμ΄μ§€λ§Œ 라인이 μ—†μŒ: {doc['source']}")
579
  except Exception as e:
580
+ logger.warning(f"[μŒμ„±μ±—] CSV 첫 번째 컬럼 μΆ”μΆœ μ‹€νŒ¨: {e}")
581
 
582
  enhanced_sources.append(source_info)
583
 
 
598
  "error": "μŒμ„± 처리 쀑 λ‚΄λΆ€ 였λ₯˜ λ°œμƒ",
599
  "details": str(e)
600
  }), 500
601
+
602
+ @app.route('/api/upload', methods=['POST'])
603
+ @login_required
604
+ def upload_document():
605
+ """μ§€μ‹λ² μ΄μŠ€ λ¬Έμ„œ μ—…λ‘œλ“œ API"""
606
+ global base_retriever, retriever, app_ready
 
607
 
608
+ # μ•± μ€€λΉ„ μƒνƒœ 확인
609
+ if not app_ready:
610
+ return jsonify({"error": "앱이 아직 μ΄ˆκΈ°ν™” μ€‘μž…λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”."}), 503
611
 
612
  try:
613
+ # 파일이 μš”μ²­μ— ν¬ν•¨λ˜μ–΄ μžˆλŠ”μ§€ 확인
614
+ if 'document' not in request.files:
615
+ return jsonify({"error": "λ¬Έμ„œ 파일이 μ œκ³΅λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}), 400
 
 
 
 
616
 
617
+ doc_file = request.files['document']
618
+ logger.info(f"받은 파일λͺ…: {doc_file.filename}")
 
 
 
 
619
 
620
+ # 파일λͺ…이 λΉ„μ–΄μžˆλŠ”μ§€ 확인
621
+ if doc_file.filename == '':
622
+ return jsonify({"error": "μ„ νƒλœ 파일이 μ—†μŠ΅λ‹ˆλ‹€."}), 400
 
623
 
624
+ # 파일 ν˜•μ‹ 확인
625
+ if not allowed_doc_file(doc_file.filename):
626
+ logger.error(f"ν—ˆμš©λ˜μ§€ μ•ŠλŠ” 파일 ν˜•μ‹: {doc_file.filename}")
627
+ return jsonify({"error": "ν—ˆμš©λ˜μ§€ μ•ŠλŠ” 파일 ν˜•μ‹μž…λ‹ˆλ‹€. ν˜„μž¬ ν—ˆμš©λœ 파일 ν˜•μ‹: {}".format(', '.join(ALLOWED_DOC_EXTENSIONS))}), 400
628
 
629
+ # 파일λͺ… λ³΄μ•ˆ 처리
630
+ filename = secure_filename(doc_file.filename)
 
 
 
631
 
632
+ # 데이터 폴더에 μ €μž₯
633
+ filepath = os.path.join(app.config['DATA_FOLDER'], filename)
634
+ doc_file.save(filepath)
635
 
636
+ logger.info(f"λ¬Έμ„œκ°€ μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€: {filepath}")
 
 
637
 
638
+ # λ¬Έμ„œ 처리
639
+ try:
640
+ # λ¨Όμ € UTF-8둜 μ‹œλ„
641
+ try:
642
+ with open(filepath, 'r', encoding='utf-8') as f:
643
+ content = f.read()
644
+ except UnicodeDecodeError:
645
+ # UTF-8둜 μ‹€νŒ¨ν•˜λ©΄ CP949(ν•œκ΅­μ–΄ Windows κΈ°λ³Έ 인코딩)둜 μ‹œλ„
646
+ logger.info(f"UTF-8 λ””μ½”λ”© μ‹€νŒ¨, CP949둜 μ‹œλ„: {filename}")
647
+ with open(filepath, 'r', encoding='cp949') as f:
648
+ content = f.read()
649
+
650
+ # 메타데이터 생성
651
+ metadata = {
652
+ "source": filename,
653
+ "filename": filename,
654
+ "filetype": filename.rsplit('.', 1)[1].lower(),
655
+ "filepath": filepath
656
+ }
657
+
658
+ # 파일 ν˜•μ‹μ— 따라 λ‹€λ₯Έ 처리 적용
659
+ file_ext = filename.rsplit('.', 1)[1].lower()
660
+
661
+ # CSV νŒŒμΌμ€ ν–‰ λ‹¨μœ„λ‘œ 처리
662
+ if file_ext == 'csv':
663
+ logger.info(f"CSV 파일 μ—…λ‘œλ“œ 감지, ν–‰ λ‹¨μœ„λ‘œ λΆ„ν•  처리: {filename}")
664
+ docs = DocumentProcessor.csv_to_documents(content, metadata)
665
+ else:
666
+ # 일반 ν…μŠ€νŠΈ λ¬Έμ„œ 처리
667
+ docs = DocumentProcessor.text_to_documents(
668
+ content,
669
+ metadata=metadata,
670
+ chunk_size=512,
671
+ chunk_overlap=50
672
+ )
673
+
674
+ if docs:
675
+ logger.info(f"{len(docs)}개 λ¬Έμ„œ 청크λ₯Ό 검색기에 μΆ”κ°€ν•©λ‹ˆλ‹€...")
676
+ base_retriever.add_documents(docs)
677
 
678
+ # 인덱슀 μ €μž₯
679
+ logger.info(f"검색기 μƒνƒœλ₯Ό μ €μž₯ν•©λ‹ˆλ‹€...")
680
+ index_path = app.config['INDEX_PATH']
681
+ try:
682
+ base_retriever.save(index_path)
683
+ logger.info("인덱슀 μ €μž₯ μ™„λ£Œ")
684
+ except Exception as e:
685
+ logger.error(f"인덱슀 μ €μž₯ 쀑 였λ₯˜ λ°œμƒ: {e}")
686
+ return jsonify({"error": f"인덱슀 μ €μž₯ 쀑 였λ₯˜: {str(e)}"}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
687
 
688
+ return jsonify({
689
+ "success": True,
690
+ "message": f"파일 '{filename}'κ°€ μ„±κ³΅μ μœΌλ‘œ μ—…λ‘œλ“œλ˜κ³  {len(docs)}개 청크가 μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€."
691
+ })
692
+ else:
693
+ logger.warning(f"파일 '{filename}'μ—μ„œ μ²˜λ¦¬ν•  λ¬Έμ„œκ°€ μ—†μŠ΅λ‹ˆλ‹€.")
694
+ return jsonify({
695
+ "warning": True,
696
+ "message": f"파일 '{filename}'이 μ €μž₯λ˜μ—ˆμ§€λ§Œ μ²˜λ¦¬ν•  λ‚΄μš©μ΄ μ—†μŠ΅λ‹ˆλ‹€."
697
+ })
698
+
699
+ except Exception as e:
700
+ logger.error(f"λ¬Έμ„œ '{filename}' 처리 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
701
+ return jsonify({"error": f"λ¬Έμ„œ 처리 쀑 였λ₯˜: {str(e)}"}), 500
702
 
 
 
703
  except Exception as e:
704
+ logger.error(f"파일 μ—…λ‘œλ“œ 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
705
+ return jsonify({"error": f"파일 μ—…λ‘œλ“œ 쀑 였λ₯˜: {str(e)}"}), 500
706
+
707
+ @app.route('/api/documents', methods=['GET'])
708
+ @login_required
709
+ def list_documents():
710
+ """μ§€μ‹λ² μ΄μŠ€ λ¬Έμ„œ λͺ©λ‘ API"""
711
+ global base_retriever, retriever, app_ready
712
+
713
+ # μ•± μ€€λΉ„ μƒνƒœ 확인
714
+ if not app_ready:
715
+ return jsonify({"error": "앱이 아직 μ΄ˆκΈ°ν™” μ€‘μž…λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”."}), 503
716
+
717
+ try:
718
+ # λ¬Έμ„œ μ†ŒμŠ€ λͺ©λ‘ 생성
719
+ sources = {}
720
+
721
+ if base_retriever and base_retriever.documents:
722
+ for doc in base_retriever.documents:
723
+ source = doc.get("source", "unknown")
724
+ if source in sources:
725
+ sources[source]["chunks"] += 1
726
+ else:
727
+ sources[source] = {
728
+ "filename": doc.get("filename", source),
729
+ "chunks": 1,
730
+ "filetype": doc.get("filetype", "unknown")
731
+ }
732
+
733
+ # λͺ©λ‘ ν˜•μ‹μœΌλ‘œ λ³€ν™˜
734
+ documents = []
735
+ for source, info in sources.items():
736
+ documents.append({
737
+ "source": source,
738
+ "filename": info["filename"],
739
+ "chunks": info["chunks"],
740
+ "filetype": info["filetype"]
741
+ })
742
+
743
+ # 청크 수둜 μ •λ ¬
744
+ documents.sort(key=lambda x: x["chunks"], reverse=True)
745
+
746
  return jsonify({
747
+ "documents": documents,
748
+ "total_documents": len(documents),
749
+ "total_chunks": sum(doc["chunks"] for doc in documents)
750
+ })
751
+
752
+ except Exception as e:
753
+ logger.error(f"λ¬Έμ„œ λͺ©λ‘ 쑰회 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
754
+ return jsonify({"error": f"λ¬Έμ„œ λͺ©λ‘ 쑰회 쀑 였λ₯˜: {str(e)}"}), 500
755
+
756
+ # 정적 파일 μ„œλΉ™
757
+ @app.route('/static/<path:path>')
758
+ def send_static(path):
759
+ return send_from_directory('static', path)
760
+
761
+ # μ„Έμ…˜ μΏ ν‚€ 처리 확인 및 μˆ˜μ •
762
+ @app.before_request
763
+ def process_cookies():
764
+ # μˆ˜λ™ μΏ ν‚€ 처리 확인
765
+ if 'session_data' in request.cookies and 'logged_in' not in session:
766
+ try:
767
+ cookie_data = json.loads(request.cookies.get('session_data'))
768
+ logger.info(f"\n[Before Request] μˆ˜λ™ μΏ ν‚€ κ°’ 발견: {cookie_data}")
769
+ if cookie_data.get('logged_in'):
770
+ # μ„Έμ…˜ μž¬κ΅¬μ„±
771
+ session['logged_in'] = True
772
+ session['username'] = cookie_data.get('username')
773
+ logger.info(f"\n[Before Request] μˆ˜λ™ μΏ ν‚€μ—μ„œ μ„Έμ…˜ 볡원: {session}")
774
+ except Exception as e:
775
+ logger.error(f"\n[Before Request] μΏ ν‚€ 처리 였λ₯˜: {e}")
776
+
777
+ # ν—ˆκΉ…νŽ˜μ΄μŠ€ ν™˜κ²½μ„ μœ„ν•œ μ„Έμ…˜ 처리 ν–₯상
778
+ @app.after_request
779
+ def after_request_func(response):
780
+ # μ„Έμ…˜μ΄ μˆ˜μ •λ˜μ—ˆλŠ”μ§€ 확인
781
+ if session.modified:
782
+ logger.info("\n[After Request] μ„Έμ…˜μ΄ μˆ˜μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
783
+ logger.info(f"ν˜„μž¬ μ„Έμ…˜ λ‚΄μš©: {session}")
784
+
785
+ # 응닡 헀더 λ‘œκΉ…
786
+ logger.info("\n[Response Headers]")
787
+ for header, value in response.headers:
788
+ logger.info(f" {header}: {value}")
789
+
790
+ # μΏ ν‚€ μ„€μ •
791
+ if 'Set-Cookie' in response.headers:
792
+ logger.info(f"Set-Cookie 헀더 있음: {response.headers['Set-Cookie']}")
793
+ else:
794
+ # 둜그인 ν›„ μ„Έμ…˜ μΏ ν‚€κ°€ μ—†μœΌλ©΄ μ„Έμ…˜ 값을 확인
795
+ if 'logged_in' in session and request.path != '/login':
796
+ logger.info("μ„Έμ…˜μ— logged_in이 μžˆμ§€λ§Œ μΏ ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
797
+
798
+ # ν—ˆκΉ…νŽ˜μ΄μŠ€ ν”„λ‘μ‹œ κ΄€λ ¨ 헀더 처리
799
+ response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
800
+ response.headers['Pragma'] = 'no-cache'
801
+ response.headers['Expires'] = '0'
802
+
803
+ return response