Add application file
Browse files- .gitignore +38 -0
- Procfile +5 -0
- __init__.py +1 -0
- app.py +10 -0
- app/__init__.py +1 -0
- app/app.py +467 -0
- app/static/css/style.css +460 -0
- app/static/js/app.js +504 -0
- app/templates/index.html +117 -0
- app/templates/loading.html +75 -0
- app_gradio.py +129 -0
- data/DatasetForRag.csv +104 -0
- huggingface-space.yml +9 -0
- requirements.txt +11 -0
- retrieval/__init__.py +1 -0
- retrieval/base_retriever.py +48 -0
- retrieval/reranker.py +127 -0
- retrieval/vector_retriever.py +202 -0
- run.py +24 -0
- runtime.txt +1 -0
- utils/__init__.py +1 -0
- utils/document_processor.py +252 -0
- utils/llm_client.py +199 -0
- utils/openai_client.py +179 -0
- utils/vito_stt.py +253 -0
.gitignore
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
env/
|
| 8 |
+
build/
|
| 9 |
+
develop-eggs/
|
| 10 |
+
dist/
|
| 11 |
+
downloads/
|
| 12 |
+
eggs/
|
| 13 |
+
.eggs/
|
| 14 |
+
lib/
|
| 15 |
+
lib64/
|
| 16 |
+
parts/
|
| 17 |
+
sdist/
|
| 18 |
+
var/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
.installed.cfg
|
| 21 |
+
*.egg
|
| 22 |
+
|
| 23 |
+
# Virtual Environment
|
| 24 |
+
venv/
|
| 25 |
+
ENV/
|
| 26 |
+
|
| 27 |
+
# IDE
|
| 28 |
+
.idea/
|
| 29 |
+
.vscode/
|
| 30 |
+
*.swp
|
| 31 |
+
*.swo
|
| 32 |
+
|
| 33 |
+
# 프로젝트 특화
|
| 34 |
+
.env
|
| 35 |
+
*.log
|
| 36 |
+
data/index/
|
| 37 |
+
data/*.json
|
| 38 |
+
app/uploads/
|
Procfile
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<<<<<<< HEAD
|
| 2 |
+
web: gunicorn -b 0.0.0.0:$PORT app:app
|
| 3 |
+
=======
|
| 4 |
+
web: gunicorn app:app
|
| 5 |
+
>>>>>>> 342cb1bea06d143718684d40b8f294967c3bbcae
|
__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# RAG 검색 챗봇 패키지
|
app.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
RAG 검색 챗봇 메인 실행 파일
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
# 앱 모듈에서 Flask 앱 가져오기
|
| 6 |
+
from app.app import app
|
| 7 |
+
|
| 8 |
+
if __name__ == '__main__':
|
| 9 |
+
port = int(os.environ.get("PORT", 7860))
|
| 10 |
+
app.run(debug=False, host='0.0.0.0', port=port)
|
app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# 애플리케이션 패키지
|
app/app.py
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
RAG 검색 챗봇 웹 애플리케이션
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
import tempfile
|
| 9 |
+
import threading
|
| 10 |
+
from flask import Flask, request, jsonify, render_template, send_from_directory
|
| 11 |
+
from werkzeug.utils import secure_filename
|
| 12 |
+
from dotenv import load_dotenv
|
| 13 |
+
|
| 14 |
+
# 로거 설정
|
| 15 |
+
logging.basicConfig(
|
| 16 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 17 |
+
level=logging.INFO
|
| 18 |
+
)
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
# 환경 변수 로드
|
| 22 |
+
load_dotenv()
|
| 23 |
+
|
| 24 |
+
# 로컬 모듈 임포트
|
| 25 |
+
from utils.vito_stt import VitoSTT
|
| 26 |
+
from utils.openai_client import OpenAILLM
|
| 27 |
+
from utils.document_processor import DocumentProcessor
|
| 28 |
+
from retrieval.vector_retriever import VectorRetriever
|
| 29 |
+
from retrieval.reranker import ReRanker
|
| 30 |
+
|
| 31 |
+
# Flask 앱 초기화
|
| 32 |
+
app = Flask(__name__)
|
| 33 |
+
|
| 34 |
+
# 최대 파일 크기 설정 (10MB)
|
| 35 |
+
app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024
|
| 36 |
+
app.config['UPLOAD_FOLDER'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
|
| 37 |
+
app.config['DATA_FOLDER'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'data')
|
| 38 |
+
app.config['INDEX_PATH'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'data', 'index')
|
| 39 |
+
|
| 40 |
+
# 업로드 폴더가 없으면 생성
|
| 41 |
+
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
| 42 |
+
os.makedirs(app.config['DATA_FOLDER'], exist_ok=True)
|
| 43 |
+
os.makedirs(app.config['INDEX_PATH'], exist_ok=True)
|
| 44 |
+
|
| 45 |
+
# 허용되는 오디오 파일 확장자
|
| 46 |
+
ALLOWED_AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'm4a'}
|
| 47 |
+
|
| 48 |
+
# 허용되는 문서 파일 확장자
|
| 49 |
+
ALLOWED_DOC_EXTENSIONS = {'txt', 'md', 'pdf', 'docx', 'csv'}
|
| 50 |
+
|
| 51 |
+
# OpenAI LLM 클라이언트 초기화
|
| 52 |
+
llm_client = OpenAILLM()
|
| 53 |
+
|
| 54 |
+
# VITO STT 클라이언트 초기화
|
| 55 |
+
stt_client = VitoSTT()
|
| 56 |
+
|
| 57 |
+
# 전역 검색기 객체와 재순위화 검색기 객체
|
| 58 |
+
base_retriever = None
|
| 59 |
+
retriever = None
|
| 60 |
+
|
| 61 |
+
# 앱 초기화 상태
|
| 62 |
+
app_ready = False
|
| 63 |
+
|
| 64 |
+
def allowed_audio_file(filename):
|
| 65 |
+
"""파일이 허용된 오디오 확장자를 가지는지 확인"""
|
| 66 |
+
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_AUDIO_EXTENSIONS
|
| 67 |
+
|
| 68 |
+
def allowed_doc_file(filename):
|
| 69 |
+
"""파일이 허용된 문서 확장자를 가지는지 확인"""
|
| 70 |
+
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_DOC_EXTENSIONS
|
| 71 |
+
|
| 72 |
+
def init_retriever():
|
| 73 |
+
"""검색기 객체 초기화 또는 로드"""
|
| 74 |
+
global base_retriever, retriever
|
| 75 |
+
|
| 76 |
+
index_path = app.config['INDEX_PATH']
|
| 77 |
+
|
| 78 |
+
# 기존 인덱스가 있는지 확인
|
| 79 |
+
if os.path.exists(os.path.join(index_path, "documents.json")):
|
| 80 |
+
try:
|
| 81 |
+
logger.info(f"기존 벡터 인덱스를 '{index_path}'에서 로드합니다...")
|
| 82 |
+
base_retriever = VectorRetriever.load(index_path)
|
| 83 |
+
logger.info(f"{len(base_retriever.documents)}개 문서가 로드되었습니다.")
|
| 84 |
+
except Exception as e:
|
| 85 |
+
logger.error(f"인덱스 로드 중 오류 발생: {e}")
|
| 86 |
+
logger.info("새 검색기를 초기화합니다...")
|
| 87 |
+
base_retriever = VectorRetriever()
|
| 88 |
+
else:
|
| 89 |
+
logger.info("기존 인덱스를 찾을 수 없어 새 검색기를 초기화합니다...")
|
| 90 |
+
base_retriever = VectorRetriever()
|
| 91 |
+
|
| 92 |
+
# 데이터 폴더의 문서 로드
|
| 93 |
+
data_path = app.config['DATA_FOLDER']
|
| 94 |
+
if not base_retriever.documents and os.path.exists(data_path):
|
| 95 |
+
logger.info(f"{data_path}에서 문서를 로드합니다...")
|
| 96 |
+
docs = DocumentProcessor.load_documents_from_directory(
|
| 97 |
+
data_path,
|
| 98 |
+
extensions=[".txt", ".md", ".csv"],
|
| 99 |
+
recursive=True
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
if docs:
|
| 103 |
+
logger.info(f"{len(docs)}개 문서를 검색기에 추가합니다...")
|
| 104 |
+
base_retriever.add_documents(docs)
|
| 105 |
+
|
| 106 |
+
# 인덱스 저장
|
| 107 |
+
logger.info(f"검색기 상태를 '{index_path}'에 저장합니다...")
|
| 108 |
+
try:
|
| 109 |
+
base_retriever.save(index_path)
|
| 110 |
+
logger.info("인덱스 저장 완료")
|
| 111 |
+
except Exception as e:
|
| 112 |
+
logger.error(f"인덱스 저장 중 오류 발생: {e}")
|
| 113 |
+
|
| 114 |
+
# 재순위화 검색기 초기화
|
| 115 |
+
logger.info("재순위화 검색기를 초기화합니다...")
|
| 116 |
+
|
| 117 |
+
# 자체 구현된 재순위화 함수 - 간단한 TF-IDF 기반 점수 재계산
|
| 118 |
+
def custom_rerank_fn(query, results):
|
| 119 |
+
"""간단한 자체 구현 재순위화 함수"""
|
| 120 |
+
# 쿼리 단어 분석
|
| 121 |
+
query_terms = set(query.lower().split())
|
| 122 |
+
|
| 123 |
+
# 결과 재점수화
|
| 124 |
+
for result in results:
|
| 125 |
+
if "text" in result:
|
| 126 |
+
text = result["text"].lower()
|
| 127 |
+
|
| 128 |
+
# 간단한 TF 기반 점수 계산
|
| 129 |
+
term_freq = sum(1 for term in query_terms if term in text)
|
| 130 |
+
|
| 131 |
+
# 길이 정규화
|
| 132 |
+
normalized_score = term_freq / (len(text.split()) + 1) * 10
|
| 133 |
+
|
| 134 |
+
# 기존 임베딩 점수와 새 점수 결합
|
| 135 |
+
result["rerank_score"] = result.get("score", 0) * 0.7 + normalized_score * 0.3
|
| 136 |
+
else:
|
| 137 |
+
# 텍스트가 없는 경우 원래 점수 유지
|
| 138 |
+
result["rerank_score"] = result.get("score", 0)
|
| 139 |
+
|
| 140 |
+
# 재점수화된 결과 정렬
|
| 141 |
+
results.sort(key=lambda x: x.get("rerank_score", 0), reverse=True)
|
| 142 |
+
return results
|
| 143 |
+
|
| 144 |
+
# 재순위화 검색기 객체 생성 (CrossEncoder 대신 사용자 정의 함수 사용)
|
| 145 |
+
retriever = ReRanker(
|
| 146 |
+
base_retriever=base_retriever,
|
| 147 |
+
rerank_fn=custom_rerank_fn,
|
| 148 |
+
rerank_field="text"
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
logger.info("재순위화 검색기 초기화 완료")
|
| 152 |
+
|
| 153 |
+
return retriever
|
| 154 |
+
|
| 155 |
+
# 비동기 초기화 함수
|
| 156 |
+
def background_init():
|
| 157 |
+
"""백그라운드에서 검색기 초기화 수행"""
|
| 158 |
+
global app_ready, retriever
|
| 159 |
+
try:
|
| 160 |
+
logger.info("백그라운드 초기화 시작")
|
| 161 |
+
retriever = init_retriever()
|
| 162 |
+
app_ready = True
|
| 163 |
+
logger.info("앱 초기화 완료")
|
| 164 |
+
except Exception as e:
|
| 165 |
+
logger.error(f"앱 초기화 중 오류 발생: {e}", exc_info=True)
|
| 166 |
+
app_ready = False
|
| 167 |
+
|
| 168 |
+
# 백그라운드 스레드에서 초기화 시작
|
| 169 |
+
init_thread = threading.Thread(target=background_init)
|
| 170 |
+
init_thread.daemon = True
|
| 171 |
+
init_thread.start()
|
| 172 |
+
|
| 173 |
+
@app.route('/')
|
| 174 |
+
def index():
|
| 175 |
+
"""메인 페이지"""
|
| 176 |
+
if not app_ready:
|
| 177 |
+
return render_template('loading.html')
|
| 178 |
+
return render_template('index.html')
|
| 179 |
+
|
| 180 |
+
@app.route('/api/status')
|
| 181 |
+
def app_status():
|
| 182 |
+
"""앱 초기화 상태 확인 API"""
|
| 183 |
+
return jsonify({"ready": app_ready})
|
| 184 |
+
|
| 185 |
+
@app.route('/api/chat', methods=['POST'])
|
| 186 |
+
def chat():
|
| 187 |
+
"""텍스트 기반 챗봇 API"""
|
| 188 |
+
global retriever, app_ready
|
| 189 |
+
|
| 190 |
+
# 앱 준비 상태 확인
|
| 191 |
+
if not app_ready:
|
| 192 |
+
return jsonify({"error": "앱이 아직 초기화 중입니다. 잠시 후 다시 시도해주세요."}), 503
|
| 193 |
+
|
| 194 |
+
try:
|
| 195 |
+
data = request.get_json()
|
| 196 |
+
if not data or 'query' not in data:
|
| 197 |
+
return jsonify({"error": "쿼리가 제공되지 않았습니다."}), 400
|
| 198 |
+
|
| 199 |
+
query = data['query']
|
| 200 |
+
logger.info(f"쿼리 수신: {query}")
|
| 201 |
+
|
| 202 |
+
# RAG 검색 수행 (재순위화 적용)
|
| 203 |
+
search_results = retriever.search(query, top_k=5, first_stage_k=20)
|
| 204 |
+
|
| 205 |
+
# 검색 결과에서 컨텍스트 추출
|
| 206 |
+
context = DocumentProcessor.prepare_rag_context(search_results, field="text")
|
| 207 |
+
|
| 208 |
+
if not context:
|
| 209 |
+
logger.warning("검색 결과가 없습니다.")
|
| 210 |
+
return jsonify({
|
| 211 |
+
"answer": "죄송합니다. 관련 정보를 찾을 수 없습니다.",
|
| 212 |
+
"sources": []
|
| 213 |
+
})
|
| 214 |
+
|
| 215 |
+
# LLM에 질의
|
| 216 |
+
answer = llm_client.rag_generate(query, context)
|
| 217 |
+
|
| 218 |
+
# 소스 정보 추출
|
| 219 |
+
sources = []
|
| 220 |
+
for result in search_results:
|
| 221 |
+
if "source" in result:
|
| 222 |
+
source_info = {
|
| 223 |
+
"source": result.get("source", "Unknown"),
|
| 224 |
+
"score": result.get("rerank_score", result.get("score", 0))
|
| 225 |
+
}
|
| 226 |
+
sources.append(source_info)
|
| 227 |
+
|
| 228 |
+
return jsonify({
|
| 229 |
+
"answer": answer,
|
| 230 |
+
"sources": sources
|
| 231 |
+
})
|
| 232 |
+
|
| 233 |
+
except Exception as e:
|
| 234 |
+
logger.error(f"채팅 처리 중 오류 발생: {e}", exc_info=True)
|
| 235 |
+
return jsonify({"error": f"처리 중 오류가 발생했습니다: {str(e)}"}), 500
|
| 236 |
+
|
| 237 |
+
@app.route('/api/voice', methods=['POST'])
|
| 238 |
+
def voice_chat():
|
| 239 |
+
"""
|
| 240 |
+
음성 챗 API 엔드포인트: 오디오 파일을 받아 텍스트로 변환하고, 질문에 대한 응답과 소스를 반환
|
| 241 |
+
|
| 242 |
+
Returns:
|
| 243 |
+
JSON 응답:
|
| 244 |
+
- transcription: 인식된 텍스트
|
| 245 |
+
- answer: LLM에서 생성한 응답
|
| 246 |
+
- sources: 검색된 문서 소스 (리스트)
|
| 247 |
+
- error: 오류 발생 시 오류 메시지
|
| 248 |
+
- details: 오류 상세 정보 (선택적)
|
| 249 |
+
"""
|
| 250 |
+
logger.info("음성 챗 요청 수신")
|
| 251 |
+
|
| 252 |
+
# 오디오 파일 확인
|
| 253 |
+
if 'audio' not in request.files:
|
| 254 |
+
logger.error("오디오 파일이 제공되지 않음")
|
| 255 |
+
return jsonify({"error": "오디오 파일이 제공되지 않았습니다."}), 400
|
| 256 |
+
|
| 257 |
+
audio_file = request.files['audio']
|
| 258 |
+
logger.info(f"수신된 파일: {audio_file.filename}")
|
| 259 |
+
|
| 260 |
+
try:
|
| 261 |
+
# 오디오 파일 읽기
|
| 262 |
+
with audio_file.stream as f:
|
| 263 |
+
audio_bytes = f.read()
|
| 264 |
+
|
| 265 |
+
# 음성인식 (VitoSTT)
|
| 266 |
+
stt = VitoSTT()
|
| 267 |
+
stt_result = stt.transcribe_audio(audio_bytes, language="ko")
|
| 268 |
+
|
| 269 |
+
if not stt_result["success"]:
|
| 270 |
+
logger.error(f"음성인식 실패: {stt_result['error']}")
|
| 271 |
+
return jsonify({
|
| 272 |
+
"error": stt_result["error"],
|
| 273 |
+
"details": stt_result.get("details", "")
|
| 274 |
+
}), 500
|
| 275 |
+
|
| 276 |
+
transcription = stt_result["text"]
|
| 277 |
+
if not transcription:
|
| 278 |
+
logger.warning("음성인식 결과가 비어있습니다.")
|
| 279 |
+
return jsonify({"error": "음성에서 텍스트를 인식하지 못했습니다."}), 400
|
| 280 |
+
|
| 281 |
+
logger.info(f"음성인식 성공: {transcription[:50]}...")
|
| 282 |
+
|
| 283 |
+
# 검색기 호출: 인식된 텍스트를 쿼리로 사용
|
| 284 |
+
sources = retriever.search(transcription)
|
| 285 |
+
if not sources:
|
| 286 |
+
logger.warning("검색된 소스가 없습니다.")
|
| 287 |
+
sources = []
|
| 288 |
+
|
| 289 |
+
# 소스 문서 내용을 컨텍스트로 준비
|
| 290 |
+
context = "\n".join([doc["text"] for doc in sources])
|
| 291 |
+
logger.info(f"검색된 소스 수: {len(sources)}")
|
| 292 |
+
|
| 293 |
+
# LLM 호출: 질문과 컨텍스트를 바탕으로 응답 생성
|
| 294 |
+
prompt = f"질문: {transcription}\n\n컨텍스트:\n{context}\n\n답변:"
|
| 295 |
+
answer = llm_client.generate(prompt)
|
| 296 |
+
|
| 297 |
+
if not answer:
|
| 298 |
+
logger.error("LLM 응답 생성 실패")
|
| 299 |
+
return jsonify({"error": "응답 생성에 실패했습니다."}), 500
|
| 300 |
+
|
| 301 |
+
logger.info(f"LLM 응답 생성 성공: {answer[:50]}...")
|
| 302 |
+
|
| 303 |
+
# 응답 반환
|
| 304 |
+
return jsonify({
|
| 305 |
+
"transcription": transcription,
|
| 306 |
+
"answer": answer,
|
| 307 |
+
"sources": sources # [{ "text": "...", "metadata": {...} }, ...]
|
| 308 |
+
})
|
| 309 |
+
|
| 310 |
+
except Exception as e:
|
| 311 |
+
logger.error(f"음성 챗 처리 중 오류 발생: {str(e)}", exc_info=True)
|
| 312 |
+
return jsonify({
|
| 313 |
+
"error": "음성 처리 중 내부 오류 발생",
|
| 314 |
+
"details": str(e)
|
| 315 |
+
}), 500
|
| 316 |
+
|
| 317 |
+
@app.route('/api/upload', methods=['POST'])
|
| 318 |
+
def upload_document():
|
| 319 |
+
"""지식베이스 문서 업로드 API"""
|
| 320 |
+
global base_retriever, retriever, app_ready
|
| 321 |
+
|
| 322 |
+
# 앱 준비 상태 확인
|
| 323 |
+
if not app_ready:
|
| 324 |
+
return jsonify({"error": "앱이 아직 초기화 중입니다. 잠시 후 다시 시도해주세요."}), 503
|
| 325 |
+
|
| 326 |
+
try:
|
| 327 |
+
# 파일이 요청에 포함되어 있는지 확인
|
| 328 |
+
if 'document' not in request.files:
|
| 329 |
+
return jsonify({"error": "문서 파일이 제공되지 않았습니다."}), 400
|
| 330 |
+
|
| 331 |
+
doc_file = request.files['document']
|
| 332 |
+
logger.info(f"받은 파일명: {doc_file.filename}")
|
| 333 |
+
|
| 334 |
+
# 파일명이 비어있는지 확인
|
| 335 |
+
if doc_file.filename == '':
|
| 336 |
+
return jsonify({"error": "선택된 파일이 없습니다."}), 400
|
| 337 |
+
|
| 338 |
+
# 파일 형식 확인
|
| 339 |
+
if not allowed_doc_file(doc_file.filename):
|
| 340 |
+
logger.error(f"허용되지 않는 파일 형식: {doc_file.filename}")
|
| 341 |
+
return jsonify({"error": "허용되지 않는 파일 형식입니다. 현재 허용된 파일 형식: {}".format(', '.join(ALLOWED_DOC_EXTENSIONS))}), 400
|
| 342 |
+
|
| 343 |
+
# 파일명 보안 처리
|
| 344 |
+
filename = secure_filename(doc_file.filename)
|
| 345 |
+
|
| 346 |
+
# 데이터 폴더에 저장
|
| 347 |
+
filepath = os.path.join(app.config['DATA_FOLDER'], filename)
|
| 348 |
+
doc_file.save(filepath)
|
| 349 |
+
|
| 350 |
+
logger.info(f"문서가 저장되었습니다: {filepath}")
|
| 351 |
+
|
| 352 |
+
# 문서 처리
|
| 353 |
+
try:
|
| 354 |
+
# 먼저 UTF-8로 시도
|
| 355 |
+
try:
|
| 356 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 357 |
+
content = f.read()
|
| 358 |
+
except UnicodeDecodeError:
|
| 359 |
+
# UTF-8로 실패하면 CP949(한국어 Windows 기본 인코딩)로 시도
|
| 360 |
+
logger.info(f"UTF-8 디코딩 실패, CP949로 시도: {filename}")
|
| 361 |
+
with open(filepath, 'r', encoding='cp949') as f:
|
| 362 |
+
content = f.read()
|
| 363 |
+
|
| 364 |
+
# 메타데이터 생성
|
| 365 |
+
metadata = {
|
| 366 |
+
"source": filename,
|
| 367 |
+
"filename": filename,
|
| 368 |
+
"filetype": filename.rsplit('.', 1)[1].lower(),
|
| 369 |
+
"filepath": filepath
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
# 문서 청크 생성
|
| 373 |
+
docs = DocumentProcessor.text_to_documents(
|
| 374 |
+
content,
|
| 375 |
+
metadata=metadata,
|
| 376 |
+
chunk_size=512,
|
| 377 |
+
chunk_overlap=50
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
if docs:
|
| 381 |
+
logger.info(f"{len(docs)}개 문서 청크를 검색기에 추가합니다...")
|
| 382 |
+
base_retriever.add_documents(docs)
|
| 383 |
+
|
| 384 |
+
# 인덱스 저장
|
| 385 |
+
logger.info(f"검색기 상태를 저장합니다...")
|
| 386 |
+
index_path = app.config['INDEX_PATH']
|
| 387 |
+
try:
|
| 388 |
+
base_retriever.save(index_path)
|
| 389 |
+
logger.info("인덱스 저장 완료")
|
| 390 |
+
except Exception as e:
|
| 391 |
+
logger.error(f"인덱스 저장 중 오류 발생: {e}")
|
| 392 |
+
return jsonify({"error": f"인덱스 저장 중 오류: {str(e)}"}), 500
|
| 393 |
+
|
| 394 |
+
return jsonify({
|
| 395 |
+
"success": True,
|
| 396 |
+
"message": f"파일 '{filename}'가 성공적으로 업로드되고 {len(docs)}개 청크가 ���가되었습니다."
|
| 397 |
+
})
|
| 398 |
+
else:
|
| 399 |
+
logger.warning(f"파일 '{filename}'에서 처리할 문서가 없습니다.")
|
| 400 |
+
return jsonify({
|
| 401 |
+
"warning": True,
|
| 402 |
+
"message": f"파일 '{filename}'이 저장되었지만 처리할 내용이 없습니다."
|
| 403 |
+
})
|
| 404 |
+
|
| 405 |
+
except Exception as e:
|
| 406 |
+
logger.error(f"문서 '{filename}' 처리 중 오류 발생: {e}", exc_info=True)
|
| 407 |
+
return jsonify({"error": f"문서 처리 중 오류: {str(e)}"}), 500
|
| 408 |
+
|
| 409 |
+
except Exception as e:
|
| 410 |
+
logger.error(f"파일 업로드 중 오류 발생: {e}", exc_info=True)
|
| 411 |
+
return jsonify({"error": f"파일 업로드 중 오류: {str(e)}"}), 500
|
| 412 |
+
|
| 413 |
+
@app.route('/api/documents', methods=['GET'])
|
| 414 |
+
def list_documents():
|
| 415 |
+
"""지식베이스 문서 목록 API"""
|
| 416 |
+
global base_retriever, retriever, app_ready
|
| 417 |
+
|
| 418 |
+
# 앱 준비 상태 확인
|
| 419 |
+
if not app_ready:
|
| 420 |
+
return jsonify({"error": "앱이 아직 초기화 중입니다. 잠시 후 다시 시도해주세요."}), 503
|
| 421 |
+
|
| 422 |
+
try:
|
| 423 |
+
# 문서 소스 목록 생성
|
| 424 |
+
sources = {}
|
| 425 |
+
|
| 426 |
+
if base_retriever and base_retriever.documents:
|
| 427 |
+
for doc in base_retriever.documents:
|
| 428 |
+
source = doc.get("source", "unknown")
|
| 429 |
+
if source in sources:
|
| 430 |
+
sources[source]["chunks"] += 1
|
| 431 |
+
else:
|
| 432 |
+
sources[source] = {
|
| 433 |
+
"filename": doc.get("filename", source),
|
| 434 |
+
"chunks": 1,
|
| 435 |
+
"filetype": doc.get("filetype", "unknown")
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
# 목록 형식으로 변환
|
| 439 |
+
documents = []
|
| 440 |
+
for source, info in sources.items():
|
| 441 |
+
documents.append({
|
| 442 |
+
"source": source,
|
| 443 |
+
"filename": info["filename"],
|
| 444 |
+
"chunks": info["chunks"],
|
| 445 |
+
"filetype": info["filetype"]
|
| 446 |
+
})
|
| 447 |
+
|
| 448 |
+
# 청크 수로 정렬
|
| 449 |
+
documents.sort(key=lambda x: x["chunks"], reverse=True)
|
| 450 |
+
|
| 451 |
+
return jsonify({
|
| 452 |
+
"documents": documents,
|
| 453 |
+
"total_documents": len(documents),
|
| 454 |
+
"total_chunks": sum(doc["chunks"] for doc in documents)
|
| 455 |
+
})
|
| 456 |
+
|
| 457 |
+
except Exception as e:
|
| 458 |
+
logger.error(f"문서 목록 조회 중 오류 발생: {e}", exc_info=True)
|
| 459 |
+
return jsonify({"error": f"문서 목록 조회 중 오류: {str(e)}"}), 500
|
| 460 |
+
|
| 461 |
+
# 정적 파일 서빙
|
| 462 |
+
@app.route('/static/<path:path>')
|
| 463 |
+
def send_static(path):
|
| 464 |
+
return send_from_directory('static', path)
|
| 465 |
+
|
| 466 |
+
if __name__ == '__main__':
|
| 467 |
+
app.run(debug=False, host='0.0.0.0', port=5000)
|
app/static/css/style.css
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* 기본 스타일 */
|
| 2 |
+
:root {
|
| 3 |
+
--primary-color: #4a6da7;
|
| 4 |
+
--primary-dark: #345089;
|
| 5 |
+
--secondary-color: #f59e0b;
|
| 6 |
+
--text-color: #333;
|
| 7 |
+
--light-text: #666;
|
| 8 |
+
--bg-color: #f8f9fa;
|
| 9 |
+
--card-bg: #fff;
|
| 10 |
+
--border-color: #e0e0e0;
|
| 11 |
+
--error-color: #ef4444;
|
| 12 |
+
--success-color: #10b981;
|
| 13 |
+
--hover-color: #f1f5f9;
|
| 14 |
+
--transition: all 0.3s ease;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
* {
|
| 18 |
+
box-sizing: border-box;
|
| 19 |
+
margin: 0;
|
| 20 |
+
padding: 0;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
body {
|
| 24 |
+
font-family: 'Pretendard', 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif;
|
| 25 |
+
line-height: 1.6;
|
| 26 |
+
color: var(--text-color);
|
| 27 |
+
background-color: var(--bg-color);
|
| 28 |
+
margin: 0;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.container {
|
| 32 |
+
max-width: 1000px;
|
| 33 |
+
margin: 0 auto;
|
| 34 |
+
padding: 20px;
|
| 35 |
+
min-height: 100vh;
|
| 36 |
+
display: flex;
|
| 37 |
+
flex-direction: column;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* 헤더 스타일 */
|
| 41 |
+
header {
|
| 42 |
+
text-align: center;
|
| 43 |
+
margin-bottom: 20px;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
header h1 {
|
| 47 |
+
color: var(--primary-color);
|
| 48 |
+
margin-bottom: 20px;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.tabs {
|
| 52 |
+
display: flex;
|
| 53 |
+
justify-content: center;
|
| 54 |
+
margin-bottom: 20px;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.tab {
|
| 58 |
+
padding: 10px 20px;
|
| 59 |
+
background-color: var(--card-bg);
|
| 60 |
+
border: 1px solid var(--border-color);
|
| 61 |
+
border-radius: 4px;
|
| 62 |
+
margin: 0 5px;
|
| 63 |
+
cursor: pointer;
|
| 64 |
+
transition: var(--transition);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.tab:hover {
|
| 68 |
+
background-color: var(--hover-color);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.tab.active {
|
| 72 |
+
background-color: var(--primary-color);
|
| 73 |
+
color: white;
|
| 74 |
+
border-color: var(--primary-color);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* 메인 컨텐츠 */
|
| 78 |
+
main {
|
| 79 |
+
flex-grow: 1;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.tab-content {
|
| 83 |
+
display: none;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.tab-content.active {
|
| 87 |
+
display: block;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
/* 채팅 섹션 */
|
| 91 |
+
.chat-container {
|
| 92 |
+
background-color: var(--card-bg);
|
| 93 |
+
border-radius: 8px;
|
| 94 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
| 95 |
+
overflow: hidden;
|
| 96 |
+
height: 70vh;
|
| 97 |
+
display: flex;
|
| 98 |
+
flex-direction: column;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.chat-messages {
|
| 102 |
+
flex-grow: 1;
|
| 103 |
+
overflow-y: auto;
|
| 104 |
+
padding: 20px;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.message {
|
| 108 |
+
margin-bottom: 20px;
|
| 109 |
+
display: flex;
|
| 110 |
+
align-items: flex-start;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.message.user {
|
| 114 |
+
justify-content: flex-end;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.message-content {
|
| 118 |
+
padding: 12px 16px;
|
| 119 |
+
border-radius: 12px;
|
| 120 |
+
max-width: 80%;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.message.system .message-content {
|
| 124 |
+
background-color: #f0f7ff;
|
| 125 |
+
color: var(--primary-dark);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.message.user .message-content {
|
| 129 |
+
background-color: var(--primary-color);
|
| 130 |
+
color: white;
|
| 131 |
+
border-top-right-radius: 4px;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.message.bot .message-content {
|
| 135 |
+
background-color: #f1f5f9;
|
| 136 |
+
color: var(--text-color);
|
| 137 |
+
border-top-left-radius: 4px;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.message p {
|
| 141 |
+
margin-bottom: 8px;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.message p:last-child {
|
| 145 |
+
margin-bottom: 0;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.message .sources {
|
| 149 |
+
font-size: 0.85em;
|
| 150 |
+
color: var(--light-text);
|
| 151 |
+
margin-top: 5px;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.message .source-item {
|
| 155 |
+
margin-right: 10px;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.message .transcription {
|
| 159 |
+
font-style: italic;
|
| 160 |
+
opacity: 0.8;
|
| 161 |
+
font-size: 0.9em;
|
| 162 |
+
margin-bottom: 8px;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.chat-input-container {
|
| 166 |
+
display: flex;
|
| 167 |
+
padding: 15px;
|
| 168 |
+
border-top: 1px solid var(--border-color);
|
| 169 |
+
background-color: var(--card-bg);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
#userInput {
|
| 173 |
+
flex-grow: 1;
|
| 174 |
+
border: 1px solid var(--border-color);
|
| 175 |
+
border-radius: 20px;
|
| 176 |
+
padding: 10px 15px;
|
| 177 |
+
font-size: 16px;
|
| 178 |
+
resize: none;
|
| 179 |
+
outline: none;
|
| 180 |
+
transition: var(--transition);
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
#userInput:focus {
|
| 184 |
+
border-color: var(--primary-color);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.mic-button, .send-button, .stop-recording-button {
|
| 188 |
+
border: none;
|
| 189 |
+
background-color: transparent;
|
| 190 |
+
color: var(--primary-color);
|
| 191 |
+
font-size: 20px;
|
| 192 |
+
margin-left: 10px;
|
| 193 |
+
cursor: pointer;
|
| 194 |
+
transition: var(--transition);
|
| 195 |
+
width: 40px;
|
| 196 |
+
height: 40px;
|
| 197 |
+
border-radius: 50%;
|
| 198 |
+
display: flex;
|
| 199 |
+
align-items: center;
|
| 200 |
+
justify-content: center;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.mic-button:hover, .send-button:hover, .stop-recording-button:hover {
|
| 204 |
+
background-color: var(--hover-color);
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.stop-recording-button {
|
| 208 |
+
background-color: var(--error-color);
|
| 209 |
+
color: white;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.stop-recording-button:hover {
|
| 213 |
+
background-color: #dc2626; /* 더 어두운 빨간색 */
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.recording-status {
|
| 217 |
+
display: flex;
|
| 218 |
+
align-items: center;
|
| 219 |
+
padding: 10px 15px;
|
| 220 |
+
background-color: rgba(239, 68, 68, 0.1);
|
| 221 |
+
border-top: 1px solid var(--border-color);
|
| 222 |
+
color: var(--error-color);
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.recording-indicator {
|
| 226 |
+
position: relative;
|
| 227 |
+
width: 12px;
|
| 228 |
+
height: 12px;
|
| 229 |
+
margin-right: 10px;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.recording-pulse {
|
| 233 |
+
position: absolute;
|
| 234 |
+
width: 100%;
|
| 235 |
+
height: 100%;
|
| 236 |
+
background-color: var(--error-color);
|
| 237 |
+
border-radius: 50%;
|
| 238 |
+
animation: pulse 1.5s infinite;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
@keyframes pulse {
|
| 242 |
+
0% {
|
| 243 |
+
transform: scale(0.8);
|
| 244 |
+
opacity: 1;
|
| 245 |
+
}
|
| 246 |
+
70% {
|
| 247 |
+
transform: scale(1.5);
|
| 248 |
+
opacity: 0;
|
| 249 |
+
}
|
| 250 |
+
100% {
|
| 251 |
+
transform: scale(0.8);
|
| 252 |
+
opacity: 0;
|
| 253 |
+
}
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.hidden {
|
| 257 |
+
display: none;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
/* 문서 관리 섹션 */
|
| 261 |
+
.docs-container {
|
| 262 |
+
background-color: var(--card-bg);
|
| 263 |
+
border-radius: 8px;
|
| 264 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
| 265 |
+
overflow: hidden;
|
| 266 |
+
padding: 20px;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.upload-section {
|
| 270 |
+
margin-bottom: 30px;
|
| 271 |
+
padding-bottom: 20px;
|
| 272 |
+
border-bottom: 1px solid var(--border-color);
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.upload-section h2, .docs-list-section h2 {
|
| 276 |
+
margin-bottom: 15px;
|
| 277 |
+
color: var(--primary-color);
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
.file-upload {
|
| 281 |
+
display: flex;
|
| 282 |
+
align-items: center;
|
| 283 |
+
margin-bottom: 20px;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.file-upload input[type="file"] {
|
| 287 |
+
display: none;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.file-upload label {
|
| 291 |
+
padding: 10px 20px;
|
| 292 |
+
background-color: var(--primary-color);
|
| 293 |
+
color: white;
|
| 294 |
+
border-radius: 4px;
|
| 295 |
+
cursor: pointer;
|
| 296 |
+
transition: var(--transition);
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.file-upload label:hover {
|
| 300 |
+
background-color: var(--primary-dark);
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
#fileName {
|
| 304 |
+
margin-left: 15px;
|
| 305 |
+
color: var(--light-text);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.upload-button {
|
| 309 |
+
padding: 10px 20px;
|
| 310 |
+
background-color: var(--secondary-color);
|
| 311 |
+
color: white;
|
| 312 |
+
border: none;
|
| 313 |
+
border-radius: 4px;
|
| 314 |
+
cursor: pointer;
|
| 315 |
+
transition: var(--transition);
|
| 316 |
+
display: flex;
|
| 317 |
+
align-items: center;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.upload-button i {
|
| 321 |
+
margin-right: 8px;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.upload-button:hover {
|
| 325 |
+
background-color: #d97706; /* 더 어두운 주황색 */
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.upload-status {
|
| 329 |
+
margin-top: 15px;
|
| 330 |
+
padding: 10px 15px;
|
| 331 |
+
border-radius: 4px;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
.upload-status.success {
|
| 335 |
+
background-color: rgba(16, 185, 129, 0.1);
|
| 336 |
+
color: var(--success-color);
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.upload-status.error {
|
| 340 |
+
background-color: rgba(239, 68, 68, 0.1);
|
| 341 |
+
color: var(--error-color);
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.docs-list-section {
|
| 345 |
+
position: relative;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.refresh-button {
|
| 349 |
+
position: absolute;
|
| 350 |
+
top: 0;
|
| 351 |
+
right: 0;
|
| 352 |
+
padding: 5px 10px;
|
| 353 |
+
background-color: transparent;
|
| 354 |
+
color: var(--primary-color);
|
| 355 |
+
border: 1px solid var(--primary-color);
|
| 356 |
+
border-radius: 4px;
|
| 357 |
+
cursor: pointer;
|
| 358 |
+
transition: var(--transition);
|
| 359 |
+
display: flex;
|
| 360 |
+
align-items: center;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.refresh-button i {
|
| 364 |
+
margin-right: 5px;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.refresh-button:hover {
|
| 368 |
+
background-color: var(--hover-color);
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.docs-list {
|
| 372 |
+
width: 100%;
|
| 373 |
+
border-collapse: collapse;
|
| 374 |
+
margin-top: 20px;
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.docs-list th, .docs-list td {
|
| 378 |
+
padding: 12px 15px;
|
| 379 |
+
text-align: left;
|
| 380 |
+
border-bottom: 1px solid var(--border-color);
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
.docs-list th {
|
| 384 |
+
background-color: #f1f5f9;
|
| 385 |
+
font-weight: 600;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.docs-list tr:hover {
|
| 389 |
+
background-color: var(--hover-color);
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
.loading-indicator {
|
| 393 |
+
display: flex;
|
| 394 |
+
flex-direction: column;
|
| 395 |
+
align-items: center;
|
| 396 |
+
justify-content: center;
|
| 397 |
+
padding: 30px;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.spinner {
|
| 401 |
+
width: 40px;
|
| 402 |
+
height: 40px;
|
| 403 |
+
border: 4px solid #f3f3f3;
|
| 404 |
+
border-top: 4px solid var(--primary-color);
|
| 405 |
+
border-radius: 50%;
|
| 406 |
+
animation: spin 1s linear infinite;
|
| 407 |
+
margin-bottom: 15px;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
@keyframes spin {
|
| 411 |
+
0% { transform: rotate(0deg); }
|
| 412 |
+
100% { transform: rotate(360deg); }
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.no-docs-message {
|
| 416 |
+
text-align: center;
|
| 417 |
+
padding: 30px;
|
| 418 |
+
color: var(--light-text);
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
/* 푸터 */
|
| 422 |
+
footer {
|
| 423 |
+
text-align: center;
|
| 424 |
+
margin-top: 30px;
|
| 425 |
+
padding-top: 20px;
|
| 426 |
+
border-top: 1px solid var(--border-color);
|
| 427 |
+
color: var(--light-text);
|
| 428 |
+
font-size: 0.9em;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
/* 반응형 스타일 */
|
| 432 |
+
@media (max-width: 768px) {
|
| 433 |
+
.container {
|
| 434 |
+
padding: 10px;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
.chat-container {
|
| 438 |
+
height: 65vh;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
.message-content {
|
| 442 |
+
max-width: 90%;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
.file-upload {
|
| 446 |
+
flex-direction: column;
|
| 447 |
+
align-items: flex-start;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
#fileName {
|
| 451 |
+
margin-left: 0;
|
| 452 |
+
margin-top: 10px;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
.refresh-button {
|
| 456 |
+
position: static;
|
| 457 |
+
margin-top: 10px;
|
| 458 |
+
margin-bottom: 10px;
|
| 459 |
+
}
|
| 460 |
+
}
|
app/static/js/app.js
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* RAG 검색 챗봇 UI JavaScript
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
// DOM 요소
|
| 6 |
+
const chatTab = document.getElementById('chatTab');
|
| 7 |
+
const docsTab = document.getElementById('docsTab');
|
| 8 |
+
const chatSection = document.getElementById('chatSection');
|
| 9 |
+
const docsSection = document.getElementById('docsSection');
|
| 10 |
+
const chatMessages = document.getElementById('chatMessages');
|
| 11 |
+
const userInput = document.getElementById('userInput');
|
| 12 |
+
const sendButton = document.getElementById('sendButton');
|
| 13 |
+
const micButton = document.getElementById('micButton');
|
| 14 |
+
const stopRecordingButton = document.getElementById('stopRecordingButton');
|
| 15 |
+
const recordingStatus = document.getElementById('recordingStatus');
|
| 16 |
+
const uploadForm = document.getElementById('uploadForm');
|
| 17 |
+
const documentFile = document.getElementById('documentFile');
|
| 18 |
+
const fileName = document.getElementById('fileName');
|
| 19 |
+
const uploadButton = document.getElementById('uploadButton');
|
| 20 |
+
const uploadStatus = document.getElementById('uploadStatus');
|
| 21 |
+
const refreshDocsButton = document.getElementById('refreshDocsButton');
|
| 22 |
+
const docsList = document.getElementById('docsList');
|
| 23 |
+
const docsLoading = document.getElementById('docsLoading');
|
| 24 |
+
const noDocsMessage = document.getElementById('noDocsMessage');
|
| 25 |
+
|
| 26 |
+
// 녹음 관련 변수
|
| 27 |
+
let mediaRecorder = null;
|
| 28 |
+
let audioChunks = [];
|
| 29 |
+
let isRecording = false;
|
| 30 |
+
|
| 31 |
+
// 앱 초기화 상태 확인 함수
|
| 32 |
+
async function checkAppStatus() {
|
| 33 |
+
try {
|
| 34 |
+
const response = await fetch('/api/status');
|
| 35 |
+
if (!response.ok) {
|
| 36 |
+
return false;
|
| 37 |
+
}
|
| 38 |
+
const data = await response.json();
|
| 39 |
+
return data.ready;
|
| 40 |
+
} catch (error) {
|
| 41 |
+
console.error('Status check failed:', error);
|
| 42 |
+
return false;
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// 페이지 로드 시 초기화
|
| 47 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 48 |
+
// 앱 상태 확인 (로딩 페이지가 아닌 경우에만)
|
| 49 |
+
if (window.location.pathname === '/' && !document.getElementById('app-loading-indicator')) {
|
| 50 |
+
// 앱 상태 주기적으로 확인
|
| 51 |
+
const statusInterval = setInterval(async () => {
|
| 52 |
+
const isReady = await checkAppStatus();
|
| 53 |
+
if (isReady) {
|
| 54 |
+
clearInterval(statusInterval);
|
| 55 |
+
console.log('앱이 준비되었습니다.');
|
| 56 |
+
}
|
| 57 |
+
}, 5000);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// 탭 전환 이벤트 리스너
|
| 61 |
+
chatTab.addEventListener('click', () => {
|
| 62 |
+
switchTab('chat');
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
docsTab.addEventListener('click', () => {
|
| 66 |
+
switchTab('docs');
|
| 67 |
+
loadDocuments();
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
// 메시지 전송 이벤트 리스너
|
| 71 |
+
sendButton.addEventListener('click', sendMessage);
|
| 72 |
+
userInput.addEventListener('keydown', (event) => {
|
| 73 |
+
if (event.key === 'Enter' && !event.shiftKey) {
|
| 74 |
+
event.preventDefault();
|
| 75 |
+
sendMessage();
|
| 76 |
+
}
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
// 음성 인식 이벤트 리스너
|
| 80 |
+
micButton.addEventListener('click', startRecording);
|
| 81 |
+
stopRecordingButton.addEventListener('click', stopRecording);
|
| 82 |
+
|
| 83 |
+
// 문서 업로드 이벤트 리스너
|
| 84 |
+
documentFile.addEventListener('change', (event) => {
|
| 85 |
+
if (event.target.files.length > 0) {
|
| 86 |
+
fileName.textContent = event.target.files[0].name;
|
| 87 |
+
} else {
|
| 88 |
+
fileName.textContent = '선택된 파일 없음';
|
| 89 |
+
}
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
+
uploadForm.addEventListener('submit', (event) => {
|
| 93 |
+
event.preventDefault();
|
| 94 |
+
uploadDocument();
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
// 문서 목록 새로고침 이벤트 리스너
|
| 98 |
+
refreshDocsButton.addEventListener('click', loadDocuments);
|
| 99 |
+
|
| 100 |
+
// 자동 입력 필드 크기 조정
|
| 101 |
+
userInput.addEventListener('input', adjustTextareaHeight);
|
| 102 |
+
|
| 103 |
+
// 초기 문서 목록 로드
|
| 104 |
+
if (docsSection.classList.contains('active')) {
|
| 105 |
+
loadDocuments();
|
| 106 |
+
}
|
| 107 |
+
});
|
| 108 |
+
|
| 109 |
+
/**
|
| 110 |
+
* 탭 전환 함수
|
| 111 |
+
* @param {string} tabName - 활성화할 탭 이름 ('chat' 또는 'docs')
|
| 112 |
+
*/
|
| 113 |
+
function switchTab(tabName) {
|
| 114 |
+
if (tabName === 'chat') {
|
| 115 |
+
chatTab.classList.add('active');
|
| 116 |
+
docsTab.classList.remove('active');
|
| 117 |
+
chatSection.classList.add('active');
|
| 118 |
+
docsSection.classList.remove('active');
|
| 119 |
+
} else if (tabName === 'docs') {
|
| 120 |
+
chatTab.classList.remove('active');
|
| 121 |
+
docsTab.classList.add('active');
|
| 122 |
+
chatSection.classList.remove('active');
|
| 123 |
+
docsSection.classList.add('active');
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/**
|
| 128 |
+
* 채팅 메시지 전송 함수
|
| 129 |
+
*/
|
| 130 |
+
async function sendMessage() {
|
| 131 |
+
const message = userInput.value.trim();
|
| 132 |
+
if (!message) return;
|
| 133 |
+
|
| 134 |
+
// UI 업데이트
|
| 135 |
+
addMessage(message, 'user');
|
| 136 |
+
userInput.value = '';
|
| 137 |
+
adjustTextareaHeight();
|
| 138 |
+
|
| 139 |
+
// 로딩 메시지 추가
|
| 140 |
+
const loadingMessageId = addLoadingMessage();
|
| 141 |
+
|
| 142 |
+
try {
|
| 143 |
+
// API 요청
|
| 144 |
+
const response = await fetch('/api/chat', {
|
| 145 |
+
method: 'POST',
|
| 146 |
+
headers: {
|
| 147 |
+
'Content-Type': 'application/json'
|
| 148 |
+
},
|
| 149 |
+
body: JSON.stringify({ query: message })
|
| 150 |
+
});
|
| 151 |
+
|
| 152 |
+
if (!response.ok) {
|
| 153 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
const data = await response.json();
|
| 157 |
+
|
| 158 |
+
// 로딩 메시지 제거
|
| 159 |
+
removeLoadingMessage(loadingMessageId);
|
| 160 |
+
|
| 161 |
+
// 응답 표시
|
| 162 |
+
if (data.error) {
|
| 163 |
+
addErrorMessage(data.error);
|
| 164 |
+
} else {
|
| 165 |
+
addMessage(data.answer, 'bot', null, data.sources);
|
| 166 |
+
}
|
| 167 |
+
} catch (error) {
|
| 168 |
+
console.error('Error:', error);
|
| 169 |
+
removeLoadingMessage(loadingMessageId);
|
| 170 |
+
addErrorMessage('오류가 발생했습니다. 다시 시도해 주세요.');
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
/**
|
| 175 |
+
* 음성 녹음 시작 함수
|
| 176 |
+
*/
|
| 177 |
+
async function startRecording() {
|
| 178 |
+
if (isRecording) return;
|
| 179 |
+
|
| 180 |
+
try {
|
| 181 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
| 182 |
+
isRecording = true;
|
| 183 |
+
audioChunks = [];
|
| 184 |
+
|
| 185 |
+
mediaRecorder = new MediaRecorder(stream);
|
| 186 |
+
|
| 187 |
+
mediaRecorder.addEventListener('dataavailable', (event) => {
|
| 188 |
+
if (event.data.size > 0) audioChunks.push(event.data);
|
| 189 |
+
});
|
| 190 |
+
|
| 191 |
+
mediaRecorder.addEventListener('stop', sendAudioMessage);
|
| 192 |
+
|
| 193 |
+
// 녹음 시작
|
| 194 |
+
mediaRecorder.start();
|
| 195 |
+
|
| 196 |
+
// UI 업데이트
|
| 197 |
+
micButton.style.display = 'none';
|
| 198 |
+
recordingStatus.classList.remove('hidden');
|
| 199 |
+
|
| 200 |
+
console.log('녹음 시작됨');
|
| 201 |
+
} catch (error) {
|
| 202 |
+
console.error('음성 녹음 권한을 얻을 수 없습니다:', error);
|
| 203 |
+
alert('마이크 접근 권한이 필요합니다.');
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/**
|
| 208 |
+
* 음성 녹음 중지 함수
|
| 209 |
+
*/
|
| 210 |
+
function stopRecording() {
|
| 211 |
+
if (!isRecording || !mediaRecorder) return;
|
| 212 |
+
|
| 213 |
+
mediaRecorder.stop();
|
| 214 |
+
isRecording = false;
|
| 215 |
+
|
| 216 |
+
// UI 업데이트
|
| 217 |
+
micButton.style.display = 'flex';
|
| 218 |
+
recordingStatus.classList.add('hidden');
|
| 219 |
+
|
| 220 |
+
console.log('녹음 중지됨');
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
/**
|
| 224 |
+
* 녹음된 오디오 메시지 전송 함수
|
| 225 |
+
*/
|
| 226 |
+
async function sendAudioMessage() {
|
| 227 |
+
if (audioChunks.length === 0) return;
|
| 228 |
+
|
| 229 |
+
// 오디오 Blob 생성
|
| 230 |
+
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
| 231 |
+
|
| 232 |
+
// 로딩 메시지 추가
|
| 233 |
+
const loadingMessageId = addLoadingMessage();
|
| 234 |
+
|
| 235 |
+
try {
|
| 236 |
+
// FormData에 오디오 추가
|
| 237 |
+
const formData = new FormData();
|
| 238 |
+
formData.append('audio', audioBlob, 'recording.wav');
|
| 239 |
+
|
| 240 |
+
// API 요청
|
| 241 |
+
const response = await fetch('/api/voice', {
|
| 242 |
+
method: 'POST',
|
| 243 |
+
body: formData
|
| 244 |
+
});
|
| 245 |
+
|
| 246 |
+
if (!response.ok) {
|
| 247 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
const data = await response.json();
|
| 251 |
+
|
| 252 |
+
// 로딩 메시지 제거
|
| 253 |
+
removeLoadingMessage(loadingMessageId);
|
| 254 |
+
|
| 255 |
+
// 응답 표시
|
| 256 |
+
if (data.error) {
|
| 257 |
+
addErrorMessage(data.error);
|
| 258 |
+
} else {
|
| 259 |
+
// 사용자 메시지(음성 텍스트) 추가
|
| 260 |
+
if (data.transcription) {
|
| 261 |
+
addMessage(data.transcription, 'user');
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
// 봇 응답 추가
|
| 265 |
+
addMessage(data.answer, 'bot', data.transcription, data.sources);
|
| 266 |
+
}
|
| 267 |
+
} catch (error) {
|
| 268 |
+
console.error('Error:', error);
|
| 269 |
+
removeLoadingMessage(loadingMessageId);
|
| 270 |
+
addErrorMessage('오디오 처리 중 오류가 발생했습니다. 다시 시도해 주세요.');
|
| 271 |
+
}
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
/**
|
| 275 |
+
* 문서 업로드 함수
|
| 276 |
+
*/
|
| 277 |
+
async function uploadDocument() {
|
| 278 |
+
if (documentFile.files.length === 0) {
|
| 279 |
+
alert('파일을 선택해 주세요.');
|
| 280 |
+
return;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
// UI 업데이트
|
| 284 |
+
uploadStatus.classList.remove('hidden');
|
| 285 |
+
uploadStatus.className = 'upload-status';
|
| 286 |
+
uploadStatus.innerHTML = '<div class="spinner"></div><p>업로드 중...</p>';
|
| 287 |
+
uploadButton.disabled = true;
|
| 288 |
+
|
| 289 |
+
try {
|
| 290 |
+
const formData = new FormData();
|
| 291 |
+
formData.append('document', documentFile.files[0]);
|
| 292 |
+
|
| 293 |
+
// API 요청
|
| 294 |
+
const response = await fetch('/api/upload', {
|
| 295 |
+
method: 'POST',
|
| 296 |
+
body: formData
|
| 297 |
+
});
|
| 298 |
+
|
| 299 |
+
const data = await response.json();
|
| 300 |
+
|
| 301 |
+
// 응답 처리
|
| 302 |
+
if (data.error) {
|
| 303 |
+
uploadStatus.className = 'upload-status error';
|
| 304 |
+
uploadStatus.textContent = `오류: ${data.error}`;
|
| 305 |
+
} else if (data.warning) {
|
| 306 |
+
uploadStatus.className = 'upload-status warning';
|
| 307 |
+
uploadStatus.textContent = data.message;
|
| 308 |
+
} else {
|
| 309 |
+
uploadStatus.className = 'upload-status success';
|
| 310 |
+
uploadStatus.textContent = data.message;
|
| 311 |
+
|
| 312 |
+
// 문서 목록 새로고침
|
| 313 |
+
loadDocuments();
|
| 314 |
+
|
| 315 |
+
// 입력 필드 초기화
|
| 316 |
+
documentFile.value = '';
|
| 317 |
+
fileName.textContent = '선택된 파일 없음';
|
| 318 |
+
}
|
| 319 |
+
} catch (error) {
|
| 320 |
+
console.error('Error:', error);
|
| 321 |
+
uploadStatus.className = 'upload-status error';
|
| 322 |
+
uploadStatus.textContent = '업로드 중 오류가 발생했습니다. 다시 시도해 주세요.';
|
| 323 |
+
} finally {
|
| 324 |
+
uploadButton.disabled = false;
|
| 325 |
+
}
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
/**
|
| 329 |
+
* 문서 목록 로드 함수
|
| 330 |
+
*/
|
| 331 |
+
async function loadDocuments() {
|
| 332 |
+
// UI 업데이트
|
| 333 |
+
docsList.querySelector('tbody').innerHTML = '';
|
| 334 |
+
docsLoading.classList.remove('hidden');
|
| 335 |
+
noDocsMessage.classList.add('hidden');
|
| 336 |
+
|
| 337 |
+
try {
|
| 338 |
+
// API 요청
|
| 339 |
+
const response = await fetch('/api/documents');
|
| 340 |
+
|
| 341 |
+
if (!response.ok) {
|
| 342 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
const data = await response.json();
|
| 346 |
+
|
| 347 |
+
// 응답 처리
|
| 348 |
+
docsLoading.classList.add('hidden');
|
| 349 |
+
|
| 350 |
+
if (!data.documents || data.documents.length === 0) {
|
| 351 |
+
noDocsMessage.classList.remove('hidden');
|
| 352 |
+
return;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
// 문서 목록 업데이트
|
| 356 |
+
const tbody = docsList.querySelector('tbody');
|
| 357 |
+
data.documents.forEach(doc => {
|
| 358 |
+
const row = document.createElement('tr');
|
| 359 |
+
|
| 360 |
+
const fileNameCell = document.createElement('td');
|
| 361 |
+
fileNameCell.textContent = doc.filename || doc.source;
|
| 362 |
+
row.appendChild(fileNameCell);
|
| 363 |
+
|
| 364 |
+
const chunksCell = document.createElement('td');
|
| 365 |
+
chunksCell.textContent = doc.chunks;
|
| 366 |
+
row.appendChild(chunksCell);
|
| 367 |
+
|
| 368 |
+
const typeCell = document.createElement('td');
|
| 369 |
+
typeCell.textContent = doc.filetype || '-';
|
| 370 |
+
row.appendChild(typeCell);
|
| 371 |
+
|
| 372 |
+
tbody.appendChild(row);
|
| 373 |
+
});
|
| 374 |
+
} catch (error) {
|
| 375 |
+
console.error('Error:', error);
|
| 376 |
+
docsLoading.classList.add('hidden');
|
| 377 |
+
noDocsMessage.classList.remove('hidden');
|
| 378 |
+
noDocsMessage.querySelector('p').textContent = '문서 목록을 불러오는 중 오류가 발생했습니다.';
|
| 379 |
+
}
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
/**
|
| 383 |
+
* 메시지 추가 함수
|
| 384 |
+
* @param {string} text - 메시지 내용
|
| 385 |
+
* @param {string} sender - 메시지 발신자 ('user' 또는 'bot' 또는 'system')
|
| 386 |
+
* @param {string|null} transcription - 음성 인식 텍스트 (선택 사항)
|
| 387 |
+
* @param {Array|null} sources - 소스 정보 배열 (선택 사항)
|
| 388 |
+
*/
|
| 389 |
+
function addMessage(text, sender, transcription = null, sources = null) {
|
| 390 |
+
const messageDiv = document.createElement('div');
|
| 391 |
+
messageDiv.classList.add('message', sender);
|
| 392 |
+
|
| 393 |
+
const contentDiv = document.createElement('div');
|
| 394 |
+
contentDiv.classList.add('message-content');
|
| 395 |
+
|
| 396 |
+
// 음성 인식 텍스트 추가 (있는 경우)
|
| 397 |
+
if (transcription && sender === 'bot') {
|
| 398 |
+
const transcriptionP = document.createElement('p');
|
| 399 |
+
transcriptionP.classList.add('transcription');
|
| 400 |
+
transcriptionP.textContent = `"${transcription}"`;
|
| 401 |
+
contentDiv.appendChild(transcriptionP);
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
// 메시지 텍스트 추가
|
| 405 |
+
const textP = document.createElement('p');
|
| 406 |
+
textP.textContent = text;
|
| 407 |
+
contentDiv.appendChild(textP);
|
| 408 |
+
|
| 409 |
+
// 소스 정보 추가 (있는 경우)
|
| 410 |
+
if (sources && sources.length > 0 && sender === 'bot') {
|
| 411 |
+
const sourcesDiv = document.createElement('div');
|
| 412 |
+
sourcesDiv.classList.add('sources');
|
| 413 |
+
|
| 414 |
+
const sourcesTitle = document.createElement('strong');
|
| 415 |
+
sourcesTitle.textContent = '출처: ';
|
| 416 |
+
sourcesDiv.appendChild(sourcesTitle);
|
| 417 |
+
|
| 418 |
+
sources.forEach((source, index) => {
|
| 419 |
+
if (index < 3) { // 최대 3개까지만 표시
|
| 420 |
+
const sourceSpan = document.createElement('span');
|
| 421 |
+
sourceSpan.classList.add('source-item');
|
| 422 |
+
sourceSpan.textContent = source.source;
|
| 423 |
+
sourcesDiv.appendChild(sourceSpan);
|
| 424 |
+
}
|
| 425 |
+
});
|
| 426 |
+
|
| 427 |
+
contentDiv.appendChild(sourcesDiv);
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
messageDiv.appendChild(contentDiv);
|
| 431 |
+
chatMessages.appendChild(messageDiv);
|
| 432 |
+
|
| 433 |
+
// 스크롤을 가장 아래로 이동
|
| 434 |
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
/**
|
| 438 |
+
* 로딩 메시지 추가 함수
|
| 439 |
+
* @returns {string} 로딩 메시지 ID
|
| 440 |
+
*/
|
| 441 |
+
function addLoadingMessage() {
|
| 442 |
+
const id = 'loading-' + Date.now();
|
| 443 |
+
const messageDiv = document.createElement('div');
|
| 444 |
+
messageDiv.classList.add('message', 'bot');
|
| 445 |
+
messageDiv.id = id;
|
| 446 |
+
|
| 447 |
+
const contentDiv = document.createElement('div');
|
| 448 |
+
contentDiv.classList.add('message-content');
|
| 449 |
+
|
| 450 |
+
const loadingP = document.createElement('p');
|
| 451 |
+
loadingP.innerHTML = '<div class="spinner" style="width: 20px; height: 20px; display: inline-block; margin-right: 10px;"></div> 생각 중...';
|
| 452 |
+
contentDiv.appendChild(loadingP);
|
| 453 |
+
|
| 454 |
+
messageDiv.appendChild(contentDiv);
|
| 455 |
+
chatMessages.appendChild(messageDiv);
|
| 456 |
+
|
| 457 |
+
// 스크롤을 가장 아래로 이동
|
| 458 |
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
| 459 |
+
|
| 460 |
+
return id;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
/**
|
| 464 |
+
* 로딩 메시지 제거 함수
|
| 465 |
+
* @param {string} id - 로딩 메시지 ID
|
| 466 |
+
*/
|
| 467 |
+
function removeLoadingMessage(id) {
|
| 468 |
+
const loadingMessage = document.getElementById(id);
|
| 469 |
+
if (loadingMessage) {
|
| 470 |
+
loadingMessage.remove();
|
| 471 |
+
}
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
/**
|
| 475 |
+
* 오류 메시지 추가 함수
|
| 476 |
+
* @param {string} errorText - 오류 메시지 내용
|
| 477 |
+
*/
|
| 478 |
+
function addErrorMessage(errorText) {
|
| 479 |
+
const messageDiv = document.createElement('div');
|
| 480 |
+
messageDiv.classList.add('message', 'system');
|
| 481 |
+
|
| 482 |
+
const contentDiv = document.createElement('div');
|
| 483 |
+
contentDiv.classList.add('message-content');
|
| 484 |
+
contentDiv.style.backgroundColor = 'rgba(239, 68, 68, 0.1)';
|
| 485 |
+
contentDiv.style.color = 'var(--error-color)';
|
| 486 |
+
|
| 487 |
+
const errorP = document.createElement('p');
|
| 488 |
+
errorP.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${errorText}`;
|
| 489 |
+
contentDiv.appendChild(errorP);
|
| 490 |
+
|
| 491 |
+
messageDiv.appendChild(contentDiv);
|
| 492 |
+
chatMessages.appendChild(messageDiv);
|
| 493 |
+
|
| 494 |
+
// 스크롤을 가장 아래로 이동
|
| 495 |
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
/**
|
| 499 |
+
* textarea 높이 자동 조정 함수
|
| 500 |
+
*/
|
| 501 |
+
function adjustTextareaHeight() {
|
| 502 |
+
userInput.style.height = 'auto';
|
| 503 |
+
userInput.style.height = Math.min(userInput.scrollHeight, 100) + 'px';
|
| 504 |
+
}
|
app/templates/index.html
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>RAG 검색 챗봇</title>
|
| 7 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
| 8 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div class="container">
|
| 12 |
+
<header>
|
| 13 |
+
<h1>RAG 검색 챗봇</h1>
|
| 14 |
+
<div class="tabs">
|
| 15 |
+
<button id="chatTab" class="tab active">대화</button>
|
| 16 |
+
<button id="docsTab" class="tab">문서관리</button>
|
| 17 |
+
</div>
|
| 18 |
+
</header>
|
| 19 |
+
|
| 20 |
+
<main>
|
| 21 |
+
<!-- 대화 탭 -->
|
| 22 |
+
<section id="chatSection" class="tab-content active">
|
| 23 |
+
<div class="chat-container">
|
| 24 |
+
<div class="chat-messages" id="chatMessages">
|
| 25 |
+
<div class="message system">
|
| 26 |
+
<div class="message-content">
|
| 27 |
+
<p>안녕하세요! 지식베이스에 대해 궁금한 점을 물어보세요. 음성으로 질문하시려면 마이크 버튼을 누르세요.</p>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
|
| 32 |
+
<div class="chat-input-container">
|
| 33 |
+
<textarea id="userInput" placeholder="메시지를 입력하세요..." rows="1"></textarea>
|
| 34 |
+
<button id="micButton" class="mic-button">
|
| 35 |
+
<i class="fas fa-microphone"></i>
|
| 36 |
+
</button>
|
| 37 |
+
<button id="sendButton" class="send-button">
|
| 38 |
+
<i class="fas fa-paper-plane"></i>
|
| 39 |
+
</button>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<div id="recordingStatus" class="recording-status hidden">
|
| 43 |
+
<div class="recording-indicator">
|
| 44 |
+
<div class="recording-pulse"></div>
|
| 45 |
+
</div>
|
| 46 |
+
<span>녹음 중...</span>
|
| 47 |
+
<button id="stopRecordingButton" class="stop-recording-button">
|
| 48 |
+
<i class="fas fa-stop"></i>
|
| 49 |
+
</button>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
</section>
|
| 53 |
+
|
| 54 |
+
<!-- 문서관리 탭 -->
|
| 55 |
+
<section id="docsSection" class="tab-content">
|
| 56 |
+
<div class="docs-container">
|
| 57 |
+
<div class="upload-section">
|
| 58 |
+
<h2>문서 업로드</h2>
|
| 59 |
+
<p>지식베이스에 추가할 문서를 업로드하세요. (지원 형식: .txt, .md, .csv)</p>
|
| 60 |
+
|
| 61 |
+
<form id="uploadForm" enctype="multipart/form-data">
|
| 62 |
+
<div class="file-upload">
|
| 63 |
+
<input type="file" id="documentFile" name="document" accept=".txt,.md,.csv">
|
| 64 |
+
<label for="documentFile">파일 선택</label>
|
| 65 |
+
<span id="fileName">선택된 파일 없음</span>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<button type="submit" id="uploadButton" class="upload-button">
|
| 69 |
+
<i class="fas fa-upload"></i> 업로드
|
| 70 |
+
</button>
|
| 71 |
+
</form>
|
| 72 |
+
|
| 73 |
+
<div id="uploadStatus" class="upload-status hidden"></div>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<div class="docs-list-section">
|
| 77 |
+
<h2>문서 목록</h2>
|
| 78 |
+
<button id="refreshDocsButton" class="refresh-button">
|
| 79 |
+
<i class="fas fa-sync-alt"></i> 새로고침
|
| 80 |
+
</button>
|
| 81 |
+
|
| 82 |
+
<div class="docs-list-container">
|
| 83 |
+
<table id="docsList" class="docs-list">
|
| 84 |
+
<thead>
|
| 85 |
+
<tr>
|
| 86 |
+
<th>파일명</th>
|
| 87 |
+
<th>청크 수</th>
|
| 88 |
+
<th>유형</th>
|
| 89 |
+
</tr>
|
| 90 |
+
</thead>
|
| 91 |
+
<tbody>
|
| 92 |
+
<!-- 문서 목록이 여기에 동적으로 추가됩니다 -->
|
| 93 |
+
</tbody>
|
| 94 |
+
</table>
|
| 95 |
+
|
| 96 |
+
<div id="docsLoading" class="loading-indicator">
|
| 97 |
+
<div class="spinner"></div>
|
| 98 |
+
<p>문서 로딩 중...</p>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<div id="noDocsMessage" class="no-docs-message hidden">
|
| 102 |
+
<p>지식베이스에 등록된 문서가 없습니다. 문서를 업로드해 주세요.</p>
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
</section>
|
| 108 |
+
</main>
|
| 109 |
+
|
| 110 |
+
<footer>
|
| 111 |
+
<p>© 2025 RAG 검색 챗봇 | DeepSeek LLM & VITO STT 활용</p>
|
| 112 |
+
</footer>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
| 116 |
+
</body>
|
| 117 |
+
</html>
|
app/templates/loading.html
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
<meta http-equiv="refresh" content="5"> <!-- 5초마다 새로고침 -->
|
| 7 |
+
<title>RAG 검색 챗봇 - 초기화 중</title>
|
| 8 |
+
<style>
|
| 9 |
+
body {
|
| 10 |
+
font-family: 'Pretendard', 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif;
|
| 11 |
+
line-height: 1.6;
|
| 12 |
+
color: #333;
|
| 13 |
+
background-color: #f8f9fa;
|
| 14 |
+
text-align: center;
|
| 15 |
+
padding-top: 100px;
|
| 16 |
+
margin: 0;
|
| 17 |
+
}
|
| 18 |
+
.container {
|
| 19 |
+
max-width: 800px;
|
| 20 |
+
margin: 0 auto;
|
| 21 |
+
padding: 20px;
|
| 22 |
+
}
|
| 23 |
+
h1 {
|
| 24 |
+
color: #4a6da7;
|
| 25 |
+
margin-bottom: 30px;
|
| 26 |
+
}
|
| 27 |
+
.loader {
|
| 28 |
+
border: 16px solid #f3f3f3;
|
| 29 |
+
border-top: 16px solid #4a6da7;
|
| 30 |
+
border-radius: 50%;
|
| 31 |
+
width: 80px;
|
| 32 |
+
height: 80px;
|
| 33 |
+
animation: spin 2s linear infinite;
|
| 34 |
+
margin: 0 auto 30px;
|
| 35 |
+
}
|
| 36 |
+
@keyframes spin {
|
| 37 |
+
0% { transform: rotate(0deg); }
|
| 38 |
+
100% { transform: rotate(360deg); }
|
| 39 |
+
}
|
| 40 |
+
p {
|
| 41 |
+
font-size: 18px;
|
| 42 |
+
margin-bottom: 20px;
|
| 43 |
+
}
|
| 44 |
+
.info {
|
| 45 |
+
background-color: #e7f0ff;
|
| 46 |
+
border-radius: 8px;
|
| 47 |
+
padding: 20px;
|
| 48 |
+
margin-top: 30px;
|
| 49 |
+
text-align: left;
|
| 50 |
+
}
|
| 51 |
+
.info h2 {
|
| 52 |
+
color: #4a6da7;
|
| 53 |
+
margin-top: 0;
|
| 54 |
+
}
|
| 55 |
+
</style>
|
| 56 |
+
</head>
|
| 57 |
+
<body>
|
| 58 |
+
<div class="container">
|
| 59 |
+
<h1>RAG 검색 챗봇 초기화 중...</h1>
|
| 60 |
+
<div class="loader"></div>
|
| 61 |
+
<p>첫 실행 시 데이터 준비에 시간이 소요됩니다. 잠시만 기다려주세요.</p>
|
| 62 |
+
<p>페이지는 5초마다 자동으로 새로고침됩니다.</p>
|
| 63 |
+
|
| 64 |
+
<div class="info">
|
| 65 |
+
<h2>초기화 작업</h2>
|
| 66 |
+
<ul>
|
| 67 |
+
<li>벡터 인덱스 생성 중...</li>
|
| 68 |
+
<li>문서 처리 및 임베딩 생성 중...</li>
|
| 69 |
+
<li>검색 엔진 준비 중...</li>
|
| 70 |
+
</ul>
|
| 71 |
+
<p>이 과정은 최대 1-2분이 소요될 수 있습니다.</p>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
</body>
|
| 75 |
+
</html>
|
app_gradio.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import gradio as gr
|
| 3 |
+
import json
|
| 4 |
+
import logging
|
| 5 |
+
from app.app import app as flask_app # Flask 앱 가져오기
|
| 6 |
+
from flask import json as flask_json
|
| 7 |
+
|
| 8 |
+
# 로거 설정
|
| 9 |
+
logging.basicConfig(
|
| 10 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 11 |
+
level=logging.INFO
|
| 12 |
+
)
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
# Flask 테스트 클라이언트 초기화
|
| 16 |
+
flask_client = flask_app.test_client()
|
| 17 |
+
|
| 18 |
+
# Gradio 인터페이스 생성
|
| 19 |
+
with gr.Blocks(title="RAG 검색 챗봇 with 음성인식") as demo:
|
| 20 |
+
gr.HTML("""
|
| 21 |
+
<div style="text-align: center; max-width: 800px; margin: 0 auto;">
|
| 22 |
+
<h1>RAG 검색 챗봇 with 음성인식</h1>
|
| 23 |
+
<p>텍스트 또는 음성으로 질문을 입력하세요.</p>
|
| 24 |
+
</div>
|
| 25 |
+
""")
|
| 26 |
+
|
| 27 |
+
with gr.Tab("텍스트 챗"):
|
| 28 |
+
text_input = gr.Textbox(label="질문 입력", placeholder="여기에 질문을 입력하세요...")
|
| 29 |
+
text_output = gr.Textbox(label="응답", interactive=False)
|
| 30 |
+
text_button = gr.Button("질문 제출")
|
| 31 |
+
|
| 32 |
+
with gr.Tab("음성 챗"):
|
| 33 |
+
# 권장: source="microphone"으로 설정하여 녹음 기능을 활성화
|
| 34 |
+
audio_input = gr.Audio(label="음성 입력", type="filepath")
|
| 35 |
+
audio_transcription = gr.Textbox(label="인식된 텍스트", interactive=False)
|
| 36 |
+
audio_output = gr.Textbox(label="응답", interactive=False)
|
| 37 |
+
gr.Markdown("""
|
| 38 |
+
<div style="text-align: center; margin: 10px 0;">
|
| 39 |
+
<p>녹음 정지 버튼을 누르면 자동으로 음성이 전송됩니다.</p>
|
| 40 |
+
</div>
|
| 41 |
+
""")
|
| 42 |
+
|
| 43 |
+
with gr.Tab("문서 업로드"):
|
| 44 |
+
doc_input = gr.File(label="문서 업로드", file_types=[".txt", ".md", ".pdf", ".docx", ".csv"])
|
| 45 |
+
doc_output = gr.Textbox(label="업로드 결과", interactive=False)
|
| 46 |
+
doc_button = gr.Button("문서 업로드")
|
| 47 |
+
|
| 48 |
+
# 텍스트 챗 기능
|
| 49 |
+
def handle_text_chat(query):
|
| 50 |
+
if not query:
|
| 51 |
+
return "질문을 입력하세요."
|
| 52 |
+
try:
|
| 53 |
+
logger.info("텍스트 챗 요청: /api/chat")
|
| 54 |
+
response = flask_client.post("/api/chat", json={"query": query})
|
| 55 |
+
data = flask_json.loads(response.data)
|
| 56 |
+
if "error" in data:
|
| 57 |
+
logger.error(f"텍스트 챗 오류: {data['error']}")
|
| 58 |
+
return data["error"]
|
| 59 |
+
return data["answer"]
|
| 60 |
+
except Exception as e:
|
| 61 |
+
logger.error(f"텍스트 챗 처리 실패: {str(e)}")
|
| 62 |
+
return f"처리 중 오류 발생: {str(e)}"
|
| 63 |
+
|
| 64 |
+
# 음성 챗 기능
|
| 65 |
+
def handle_voice_chat(audio_file):
|
| 66 |
+
if not audio_file:
|
| 67 |
+
return "", "음성을 업로드하세요."
|
| 68 |
+
try:
|
| 69 |
+
logger.info("음성 챗 요청: /api/voice")
|
| 70 |
+
with open(audio_file, "rb") as f:
|
| 71 |
+
# Flask 테스트 클라이언트는 files 직접 지원 안 하므로, 데이터를 읽어 전달
|
| 72 |
+
response = flask_client.post(
|
| 73 |
+
"/api/voice",
|
| 74 |
+
data={"audio": (f, "audio_file")}
|
| 75 |
+
)
|
| 76 |
+
data = flask_json.loads(response.data)
|
| 77 |
+
if "error" in data:
|
| 78 |
+
logger.error(f"음성 챗 오류: {data['error']}")
|
| 79 |
+
return "", data["error"]
|
| 80 |
+
return data["transcription"], data["answer"]
|
| 81 |
+
except Exception as e:
|
| 82 |
+
logger.error(f"음성 챗 처리 실패: {str(e)}")
|
| 83 |
+
return "", f"처리 중 오류 발생: {str(e)}"
|
| 84 |
+
|
| 85 |
+
# 문서 업로드 기능
|
| 86 |
+
def handle_doc_upload(doc_file):
|
| 87 |
+
if not doc_file:
|
| 88 |
+
return "문서를 업로드하세요."
|
| 89 |
+
try:
|
| 90 |
+
logger.info(f"문서 업로드 요청: /api/upload, 파일명: {doc_file.name}")
|
| 91 |
+
file_extension = os.path.splitext(doc_file.name)[1].lower()
|
| 92 |
+
logger.info(f"파일 확장자: {file_extension}")
|
| 93 |
+
|
| 94 |
+
with open(doc_file, "rb") as f:
|
| 95 |
+
response = flask_client.post(
|
| 96 |
+
"/api/upload",
|
| 97 |
+
data={"document": (f, doc_file.name)}
|
| 98 |
+
)
|
| 99 |
+
data = flask_json.loads(response.data)
|
| 100 |
+
if "error" in data:
|
| 101 |
+
logger.error(f"문서 업로드 오류: {data['error']}")
|
| 102 |
+
return data["error"]
|
| 103 |
+
return data["message"]
|
| 104 |
+
except Exception as e:
|
| 105 |
+
logger.error(f"문서 업로드 처리 실패: {str(e)}")
|
| 106 |
+
return f"처리 중 오류 발생: {str(e)}"
|
| 107 |
+
|
| 108 |
+
# 이벤트 핸들러 연결
|
| 109 |
+
text_button.click(
|
| 110 |
+
fn=handle_text_chat,
|
| 111 |
+
inputs=text_input,
|
| 112 |
+
outputs=text_output
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
# 음성 입력 값이 변경될 때 자동으로 전송
|
| 116 |
+
audio_input.change(
|
| 117 |
+
fn=handle_voice_chat,
|
| 118 |
+
inputs=audio_input,
|
| 119 |
+
outputs=[audio_transcription, audio_output]
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
doc_button.click(
|
| 123 |
+
fn=handle_doc_upload,
|
| 124 |
+
inputs=doc_input,
|
| 125 |
+
outputs=doc_output
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
if __name__ == "__main__":
|
| 129 |
+
demo.launch(server_port=7860)
|
data/DatasetForRag.csv
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ID,���� ����,���� (Question),���� (Answer),���� ����/�ƶ� (Reference/Context)
|
| 2 |
+
IT001,��� Ȯ��,������ 16 Pro Max ���� ȭ�� ũ��� �� ��ġ�ΰ���?,������ 16 Pro Max�� 6.9��ġ Super Retina XDR ���÷��̸� ž���߽��ϴ�.,"(2024-09-15) Apple's latest flagship, the iPhone 16 Pro Max, boasts an expansive 6.9-inch Super Retina XDR display, offering an immersive viewing experience with ProMotion technology and always-on capability."
|
| 3 |
+
IT002,��� Ȯ��,�Z ������ S25 Ultra�� ���� ī�� ȭ�Ҵ� ���ΰ���?,�Z ������ S25 Ultra�� 2�� ȭ��(200MP)�� ���� ī�� ������ ž���� ������ ����˴ϴ�. (����: ��� �� �����̹Ƿ� ���� ����),"(2025-01-10) Leaks suggest the upcoming Samsung Galaxy S25 Ultra will retain a 200-megapixel primary camera sensor, likely with further improvements in sensor technology and image processing for enhanced low-light performance and detail capture."
|
| 4 |
+
IT003,���� ����,����Ʈ���� '�ֻ���(Refresh Rate)'�̶� �����ΰ���?,"�ֻ����� ���÷��̰� 1�ʿ� ȭ���� �� ���̳� ���ΰ�ħ�ϴ����� ��Ÿ���� ��ġ�Դϴ�. ������ �츣��(Hz)�� ����ϸ�, ���� �ֻ���(��: 120Hz)�� ��ũ���̳� ȭ�� ��ȯ �� �� �ε巯�� �������� �����ݴϴ�.","Refresh rate, measured in Hertz (Hz), refers to how many times per second the display updates the image shown on the screen. A standard display might have a 60Hz refresh rate, while high-end smartphones often feature 90Hz, 120Hz, or even higher rates for smoother motion clarity, particularly noticeable in fast-paced games or while scrolling through content."
|
| 5 |
+
IT004,�� �м�,������ 16�� ������ S25�� �ֿ� �������� �����ΰ���?,"�ü��(iOS vs Android), ������ ö��, ī�� �ý����� Ư¡(����, �� ���� ��), ���°� ������, ���� Ĩ�� ���� ��� �ֿ� ���̰� �ֽ��ϴ�. ������� ��ȣ���� ��� ȯ�濡 ���� ������� ���� �� �ֽ��ϴ�.","Key differences between the iPhone 16 and Galaxy S25 lie in their core operating systems (Apple's iOS vs. Google's Android), distinct design languages, camera philosophies (e.g., natural tones vs. vibrant processing, zoom capabilities), ecosystem integration (iMessage/AirDrop vs. Google/Samsung services), and the underlying chipset performance (A-series vs. Snapdragon/Exynos)."
|
| 6 |
+
IT005,���� �ذ�,����Ʈ�� ������ �ʹ� ���� ��ƿ�. ��� �ذ��� �� �ֳ���?,"ȭ�� ��� ����, ������� �ʴ� �� ����, ����� �� Ȱ�� ����, ��ġ ���� ���� ����, ���� ��� Ȱ��ȭ, ���ʿ��� �˸� ���� ���� �õ��� �� �� �ֽ��ϴ�. ���� ��ü�� ����ȭ ���ɼ��� �����ؾ� �մϴ�.","To address rapid battery drain on your smartphone, try reducing screen brightness, closing apps you aren't actively using, limiting background app refresh and location services for non-essential apps, enabling power-saving or low-power modes, disabling unnecessary notifications, and checking battery usage statistics to identify power-hungry apps. If the issue persists, battery degradation might be the cause."
|
| 7 |
+
IT006,��� ���,�ȵ���̵� ����Ʈ������ ��ũ������ ��� �ﳪ��?,"�Ϲ������� ���� ��ư�� ���� �ٿ� ��ư�� ���ÿ� ª�� ������ ��ũ������ �Կ��˴ϴ�. ��� �����糪 �� ���� �ճ��� ȭ�� ����, �� �հ������� ȭ�� ������ �� �ٸ� ����� ���� ���� �ֽ��ϴ�.","Taking a screenshot on most Android phones typically involves pressing and holding the power button and the volume down button simultaneously for a moment. Some manufacturers offer alternative methods, such as palm swipe gestures (Samsung) or three-finger swipes (various brands). The captured screenshot is usually saved to the gallery or a dedicated screenshots folder."
|
| 8 |
+
IT007,��� Ȯ��,���� �ȼ� 8a�� ���� ��õǾ�����?,���� �ȼ� 8a�� 2024�� 5���� ���� ��ǥ �� ��õǾ����ϴ�.,"(2024-05-07) Google officially unveiled the Pixel 8a during its Google I/O event keynote in May 2024, making it available for purchase shortly thereafter. It serves as the more affordable alternative within the Pixel 8 lineup."
|
| 9 |
+
IT008,���� ����,����Ʈ���� 'IP ���'�� ������ �ǹ��ϳ���? (��: IP68),"IP ����� 'Ingress Protection'�� ���ڷ�, ����� ����(ù ��° ����) �� ���(�� ��° ����) ������ ��Ÿ���ϴ�. IP68�� �ְ� ������ ����(6)�� ���� ���� �Ͽ����� ���(8, ���� 1.5m ���ɿ��� 30��)�� �ǹ��մϴ�.","The IP rating (Ingress Protection rating) classifies the level of protection an enclosure provides against intrusion from solid objects (like dust; indicated by the first digit) and liquids (water; indicated by the second digit). For example, an IP68 rating means the device is dust-tight (6) and protected against continuous immersion in water under conditions specified by the manufacturer (8, often up to 1.5 meters for 30 minutes)."
|
| 10 |
+
IT009,���� �߷�,�ֽ� ����Ʈ���� AI ����� ���� ǰ�� ��� ��� ��ϳ���?,"AI�� ��� �ν�, �ǻ�ü �ĺ�, ������ ����, ���� ������ �ռ�(HDR ��), �ι� ����� �ɵ� ȿ�� ����ȭ, ������ ȯ�濡���� ��� �� ������ ���� �� �پ��� ������� �����Ͽ� ���� ���� ������� ǰ���� ����ŵ�ϴ�.","AI algorithms in modern smartphones significantly enhance photo quality through various computational photography techniques. These include scene recognition for automatic setting optimization, subject detection for focus and exposure, advanced noise reduction, multi-frame synthesis for improved dynamic range (HDR) and low-light performance, sophisticated depth mapping for portrait mode bokeh, and semantic segmentation for targeted adjustments."
|
| 11 |
+
IT010,�� �м�,OLED ���÷��̿� LCD ���÷����� ������� �����ΰ���?,"OLED�� ��ü �߱� ���ڸ� ����Ͽ� �Ϻ��� ������ ǥ��, ���� ���Ϻ�, ���� ���� �ӵ��� ����������, ���� ���� ���ɼ��� ��������� ���� ������ �����Դϴ�. LCD�� ���� ������ ���� ��� ǥ���� ������ ������, ���Ϻ� ���� ������ ǥ���� �Ѱ谡 �ֽ��ϴ�.","OLED (Organic Light-Emitting Diode) displays offer advantages like perfect blacks (pixels can turn off completely), infinite contrast ratios, wider viewing angles, and faster response times. Downsides can include potential burn-in over time and higher manufacturing costs. LCD (Liquid Crystal Display) panels are typically brighter, less prone to burn-in, and cheaper to produce, but they suffer from lower contrast ratios ('black' is dark grey due to the backlight) and potential backlight bleeding."
|
| 12 |
+
IT011,��� Ȯ��,�ֽ� �����е� ���� �� ž��� Ĩ�� �̸��� �����ΰ���?,�ֽ� �����е� ���� (M4 Ĩ ž�� ��)���� Apple M4 Ĩ�� ž��Ǿ����ϴ�.,"(2024-05-07) Apple introduced the new iPad Pro lineup featuring the powerful, next-generation Apple M4 chip, delivering significant performance gains and enabling features like the Tandem OLED display and enhanced AI capabilities."
|
| 13 |
+
IT012,���� ����,5G ��Ʈ��ũ�� �ֿ� Ư¡�� �����ΰ���?,"5G�� �ʰ���(eMBB), ��������(URLLC), �ʿ���(mMTC)�̶�� �� ���� �ֿ� Ư¡�� �����ϴ�. �̸� ���� �� ���� ������ �ӵ�, �ǽð��� ����� ���伺, ���� ������ ��⸦ ���ÿ� �����ϴ� ���� ���������ϴ�.","The key characteristics of 5G networks are Enhanced Mobile Broadband (eMBB) for significantly faster data speeds, Ultra-Reliable Low-Latency Communication (URLLC) for near-instantaneous responsiveness crucial for applications like autonomous driving and remote surgery, and Massive Machine-Type Communications (mMTC) enabling connectivity for a vast number of IoT devices simultaneously."
|
| 14 |
+
IT013,���� �ذ�,����Ʈ�� ȭ���� ���ڱ� ���߰� ������ �����. ��� �ؾ� �ϳ���?,"��κ��� ���, ���� ��ư�� ���(10-30��) ���� ���� ������� �õ��ϸ� �ذ�� �� �ֽ��ϴ�. �ȵ���̵��� ��� '���� ��ư + ���� �ٿ� ��ư'�� ���ÿ� ��� ������ ���յ� ���˴ϴ�. ��⺰ ���� ����� ����� Ȯ���غ�����.","If your smartphone screen freezes and becomes unresponsive, the first troubleshooting step is usually a forced restart. For most iPhones, quickly press and release volume up, then volume down, then press and hold the side button. For many Android devices, press and hold the power button for 10-30 seconds, or press and hold the power button and volume down button together until the device restarts. Consult your device's manual for specific instructions."
|
| 15 |
+
IT014,��� ���,���������� AirDrop(������) ����� ����ϴ� ����� �����ΰ���?,"���� ���� ���ų� ���� �ۿ��� AirDrop�� Ȱ��ȭ�ϰ� '���� ��', '����ó��', '��� ���' �� ���� �ɼ��� �����մϴ�. ������ ������ ���� ���� ��Ʈ���� AirDrop �������� ���ϰ� ������ ����̳� ��⸦ �����մϴ�.","To use AirDrop on an iPhone, ensure Wi-Fi and Bluetooth are turned on. Open Control Center or Settings > General > AirDrop, and choose your receiving preference ('Receiving Off', 'Contacts Only', or 'Everyone'). To share a file, tap the Share icon, select AirDrop, and then tap the user or device you want to share with. The recipient will receive a notification to accept or decline the file."
|
| 16 |
+
IT015,���� �߷�,������ ����Ʈ�� ���� �� �����ؾ� �� ������� �����ϱ��?,"�������δ� ��ȭ���� ���ϱ� ���ϴٴ� ��, ��Ƽ�½�ŷ�� ���̼� ���� �ֽ��ϴ�. �������δ� �Ϲ� ����Ʈ�� ��� ���� ����, ������(Ư�� ������ ���� ���÷���)�� ���� ���, ��������� �β��� ���ſ� �� ���� �ֽ��ϴ�.","When considering purchasing a foldable smartphone, weigh the advantages, such as having a large tablet-like screen in a portable form factor and enhanced multitasking capabilities. However, also consider the disadvantages: higher price compared to traditional smartphones, potential concerns about long-term durability (especially the hinge mechanism and the flexible inner display), increased thickness and weight, and sometimes compromises in other specs (like battery size or camera modules) to accommodate the folding design."
|
| 17 |
+
IT016,��� Ȯ��,������ 14 Ultra ���� ī�� ���� ������ ��� �dz���?,"������ 14 Ultra�� ����ī(Leica)�� ������ ���� ī�� �ý����� ���߰� ������, 1��ġ ������ ž���� ���� ī��, �ʱ��� ī��, ���� �� ���� ���� ī��(3.2��, 5��)�� �����Ǿ� �ֽ��ϴ�.","(2024-02-22) The Xiaomi 14 Ultra features a sophisticated quad-camera system co-engineered with Leica. It includes a main camera with a large 1-inch LYT-900 sensor, an ultra-wide camera, a 3.2x telephoto camera with a floating lens element, and a 5x periscope telephoto camera, offering versatile focal lengths."
|
| 18 |
+
IT017,���� ����,����Ʈ���� 'SoC(System on Chip)'�� �����ΰ���?,"SoC�� ����Ʈ���� �γ� ������ �ϴ� �ٽ� ��ǰ����, �߾�ó����ġ(CPU), ����ó����ġ(GPU), ��� ��(5G, LTE ��), AI ó�� ��ġ(NPU), �� ��Ʈ�ѷ� �� �پ��� ����� ȸ�θ� �ϳ��� Ĩ�� ������ ���Դϴ�.","A System on Chip (SoC) is an integrated circuit that combines multiple essential computing components onto a single silicon chip. In a smartphone, the SoC typically includes the Central Processing Unit (CPU), Graphics Processing Unit (GPU), memory controllers, communication modems (like 5G/LTE), Neural Processing Unit (NPU) for AI tasks, Image Signal Processor (ISP) for camera functions, and other necessary hardware accelerators, acting as the device's 'brain'."
|
| 19 |
+
IT018,�� �м�,���� ������ ���� ���� �� � ����� �� ��������? �Ϲ������� �� ������?,�Ϲ������� ���� ������ ���� �������� �� �����ϴ�. �̴� ���� ������ ���� �ս��� ���� �� ���� ������ ���������� ������ �� �ֱ� �����Դϴ�. ���� ������ ������ ���� �������� �����ϴ� �������� ������ �ս��� ���Ͽ� ���� ȿ���� �������� ������ �ֽ��ϴ�.,"Wired charging is generally faster than wireless charging because it offers a more direct and efficient transfer of electrical power with less energy loss. Wireless charging relies on electromagnetic induction between coils in the charger and the device, which inherently involves some energy loss as heat, limiting the maximum achievable charging speed and efficiency compared to a direct cable connection that can handle higher wattage more stably."
|
| 20 |
+
IT019,���� �ذ�,��������(Wi-Fi)�� ����ƴٰ� �����µ� ���ͳ��� �� �ſ�. ��� �ؾ� �ϳ���?,1. ������(�����) ����� 2. ����Ʈ�� ����� 3. ����Ʈ���� Wi-Fi �������� �ش� ��Ʈ��ũ '�ر�' �� �翬�� 4. �ٸ� ������ ������ ������ ���ϴ��� Ȯ�� (������/���ͳ� ȸ�� ���� Ȯ��) 5. DNS ���� ���� ���� �õ��� �� �� �ֽ��ϴ�.,"If your phone shows it's connected to Wi-Fi but has no internet access, try these steps: 1. Restart your Wi-Fi router/modem. 2. Restart your smartphone. 3. Forget the Wi-Fi network on your phone (Settings > Wi-Fi > select network > Forget) and then reconnect. 4. Check if other devices on the same network have internet access (this helps isolate if the issue is with your phone or the network/ISP). 5. Try changing DNS settings on your phone (e.g., to Google DNS 8.8.8.8)."
|
| 21 |
+
IT020,��� ���,������ ����Ʈ������ '�Z ����(Samsung Pay)' ���� �� ������ �˷��ּ���.,"�Z ����(�� �Z ����) ���� ���� �Z �������� �α����� ��, �ȳ��� ���� ���� ī��(�ſ�/üũ)�� ī�� ��ĵ �Ǵ� ���� �Է����� ����մϴ�. ���� �ÿ��� ȭ�� �ϴܿ��� ���� ����÷� ī�带 �����ϰ� ����/��й�ȣ ���� �� ���� �ܸ�� ����Ʈ�� ���� ������ ��� �˴ϴ�.","To set up and use Samsung Pay (now part of Samsung Wallet) on a Galaxy phone: 1. Open the Samsung Wallet app and sign in with your Samsung account. 2. Follow the prompts to add a payment card by scanning it with the camera or entering details manually. 3. To pay, swipe up from the bottom of the screen (even when locked), select the desired card, authenticate with your fingerprint, PIN, or iris scan, and then tap the back of your phone near the payment terminal (NFC or MST)."
|
| 22 |
+
IT021,���� �߷�,"����Ʈ�� ���� ����� ��ġ ���� ���� ������ �䱸�ϴ� ������ �����̸�, ��� �� � ���� �����ؾ� �ϳ���?","���� ��ġ ������ �䱸�ϴ� ������ ����/������̼�, �ֺ� ��� �˻�, ���� ���� ����, ������ ���� ǥ��, Ȱ�� ����(� ��) �� �پ��մϴ�. ��� �ÿ��� '�� ��� �߿��� ���' �ɼ��� �����ϰ�, ���ʿ��� �ۿ��� ������ �ο����� ������, �ֱ������� ���� ������ �����Ͽ� ���� ���� ���� ������ �ּ�ȭ�ؾ� �մϴ�.","Apps request location access for various functions: navigation (maps), finding nearby places, providing localized weather forecasts, delivering location-based ads or content, tracking workouts, geotagging photos, etc. When granting permission, it's crucial to: 1. Prefer the 'Allow only while using the app' option if available. 2. Deny permission to apps that don't genuinely need location data for their core function. 3. Regularly review app permissions in settings to minimize privacy risks and unnecessary data exposure."
|
| 23 |
+
IT022,��� Ȯ��,���� ��ġ �ø��� 9�� �ֿ� �ǰ� ���� ����� �����ΰ���?,"������(ECG) ����, ���� ��� ��ȭ�� ����, �Ѿ��� ����, �浹 ����, �ո� �µ� ������ ���� ���� �ֱ� ����, �ɹڼ� �����(��/�� �ɹڼ� �˸� ����) ���� �ֿ� �ǰ� ���� ����Դϴ�.","Key health-related features of the Apple Watch Series 9 include the ECG app for taking electrocardiograms, blood oxygen saturation (SpO2) measurement, fall detection, crash detection, wrist temperature sensing for retrospective ovulation estimates and cycle tracking, and continuous heart rate monitoring with notifications for high/low heart rates and irregular rhythms."
|
| 24 |
+
IT023,���� ����,NFC(Near Field Communication)' ����� ����Ʈ������ ��� Ȱ��dz���?,"NFC�� ����� �Ÿ�(�� 10cm �̳�)���� ��� �� ���� ������ ����� �����ϰ� �ϴ� ����Դϴ�. ����Ʈ�������� ����� ����(�Z����, ��������, �������� ��), ����ī�� ���, ������ ����(Android Beam ��), ����Ʈ ��� ���� �� ���� � Ȱ��˴ϴ�.","NFC (Near Field Communication) is a short-range wireless technology enabling communication between devices within close proximity (typically around 4 inches or 10 cm). On smartphones, it's used for various applications, including contactless mobile payments (like Google Pay, Apple Pay, Samsung Pay), transit passes, quick data transfer between phones (e.g., Android Beam - now deprecated but NFC still used for sharing), pairing with accessories (like headphones or speakers), and reading NFC tags for information or automation tasks."
|
| 25 |
+
IT024,�� �м�,"����Ʈ�� �ü�� ������Ʈ�� �� �߿��ϸ�, ������Ʈ ���ķ� ������ ���� �����ΰ���?","OS ������Ʈ�� ���ο� ��� �߰�, ���� ����, ���� ����� ��ġ ���� �����ϹǷ� �߿��մϴ�. ������Ʈ ������ �߿��� ������ ����ϰ�, ����� ���� ������ ���� �ܷ��� Ȯ���ϸ�, �������� Wi-Fi ȯ�濡�� �����ϴ� ���� �����ϴ�. ������Ʈ �Ŀ��� ��Ȥ ���� �Ҹ� ������ �� ȣȯ�� ������ ���� �� �����Ƿ� ��ȭ�� ������ �ʿ䰡 �ֽ��ϴ�.","Smartphone OS updates are crucial as they often include new features, performance enhancements, bug fixes, and critical security patches to protect against vulnerabilities. Before updating, it's vital to back up important data, ensure sufficient storage space and battery level (or connect to power), and use a stable Wi-Fi connection. After updating, monitor for any changes, such as altered battery life or app compatibility issues, which can sometimes occur initially."
|
| 26 |
+
IT025,���� �ذ�,����Ʈ�� �߿��� ���� �� ��� ��ó�ؾ� �ϳ���?,"����� ����/�� ��� �ߴ�, ���� �� ��� ����, ���籤�� ���� ���ϱ�, ����Ʈ�� ���̽� ��� ����, ���ʿ��� ���(GPS, �������� ��) ����, ��� ����� ���� �õ��� �� �� �ֽ��ϴ�. �߿��� ������������ ���ӵǸ� ���� ���� ������ �ʿ��� �� �ֽ��ϴ�.","If your smartphone overheats, try the following: Stop using demanding apps or games, avoid using the phone while it's charging, keep it out of direct sunlight, temporarily remove the case, turn off unnecessary features like GPS and Bluetooth, and restart the device. If excessive heating persists or seems abnormal, contact customer support or visit a service center, as it could indicate a hardware issue."
|
| 27 |
+
IT026,���� ����,����Ʈ���� 'eSIM(�̽�)'�̶� �����ΰ���? ��� ����ϳ���?,"eSIM�� 'embedded SIM'�� ���ڷ�, �������� SIM ī�� ���� ��� ���ο� ����� Ĩ�� ���� ��Ż� ������ �ٿ�ε��Ͽ� ����ϴ� ������ SIM�Դϴ�. ��Ż� ���̳� QR �ڵ� ��ĵ ���� ���� �����Ͽ� ����� �� �ֽ��ϴ�.","An eSIM (embedded Subscriber Identity Module) is a digital SIM that allows users to activate a cellular plan from a carrier without having to use a physical nano-SIM card. The information is downloaded and programmed directly onto a dedicated chip inside the device. Activation is typically done through a carrier's app, a QR code, or manual entry of activation details provided by the carrier."
|
| 28 |
+
IT027,��� Ȯ��,UWB(�ʱ��뿪) ����� ����Ʈ������ �ַ� � �뵵�� ���dz���?,"UWB ����� �ſ� ������ �Ÿ� �� ���� ������ �����Ͽ�, ����Ʈ�������� �ַ� ������ Ű(�ڵ��� �� ���� ��), �нǹ� ������(��: �����±�, ����Ʈ�±�)�� ���� Ž�� ���, ��� �� ���� ���� �� ���� �ν� � Ȱ��˴ϴ�.","Ultra-Wideband (UWB) technology enables highly accurate spatial awareness and precise distance measurement between devices. In smartphones, it's primarily used for features like secure digital car keys, precision finding of nearby tracker tags (e.g., Apple AirTag, Samsung SmartTag+), directional file sharing (like enhancing AirDrop), and seamless smart home device interactions."
|
| 29 |
+
IT028,�� �м�,������ �۷��� ���ͽ� 2�� ������ �۷��� �Ƹ��� �������� �����ΰ���?,"������ �۷��� ���ͽ� 2�� ���� ��� ��ȣ�� ������ �� ��ȭ�����Դϴ�. �ݸ�, ������ S24 Ultra � ����� ������ �۷��� �ƸӴ� �پ ��ũ��ġ ������ �Բ� ȭ�� �ݻ����� ũ�� ���� �������� ���� ���� Ư¡�Դϴ�.","Corning's Gorilla Glass Victus 2 primarily focuses on improved drop protection, particularly on rough surfaces like concrete. Gorilla Glass Armor, introduced on the Galaxy S24 Ultra, maintains strong scratch resistance while significantly reducing screen reflections (by up to 75%), enhancing outdoor visibility and perceived clarity."
|
| 30 |
+
IT029,���� ����,����Ʈ�� ���� ��� �� 'GaN(��ȭ����)' ������� �����̸� ������ �����ΰ���?,"GaN ������� ���� �Ǹ��� ��� ��ȭ�����̶�� �ż��縦 ����Ͽ� ���� �������Դϴ�. GaN ����� ���� ȿ���� ���� ������ ���� ����� �� ���� ũ��� ���� �� ������, �߿��� ���ٴ� ������ �ֽ��ϴ�. �̸� ���� �� �۰� ������鼭�� ����� ������ ���������ϴ�.","GaN (Gallium Nitride) chargers utilize Gallium Nitride semiconductor material instead of traditional silicon. GaN is more power-efficient, allowing manufacturers to build smaller, lighter chargers that can deliver the same or higher power output compared to silicon-based ones. They also tend to generate less heat, contributing to their compact size and efficiency."
|
| 31 |
+
IT030,��� Ȯ��,�Z ������ ����Ʈ���� ������ �� �Ϲ������� � ���� ����Ʈ���� ������Ʈ ���� �Ⱓ�� �� �䰡��? (2025�� 4�� ����),"2025�� 4�� ��������, �÷��� ���� ��� �Z�� ����(�ȼ�)�� �ִ� 7�Ⱓ�� OS �� ���� ������Ʈ�� ����ϸ� ���� �� ���� �Ⱓ�� �����ϴ� ������ �ֽ��ϴ�. ����(������) ���� �������� ������Ʈ�� ����������, �������� ���� �Ⱓ�� �������� �ʴ� ���Դϴ�.","(As of April 2025) For flagship devices, Samsung and Google (with its Pixel line) currently tend to offer the longest official software support durations, promising up to 7 years of both OS upgrades and security updates for recent models. Apple has a strong track record of providing long-term iOS updates for iPhones, often exceeding 5-6 years, but typically doesn't state a specific guaranteed timeframe upfront."
|
| 32 |
+
IT031,���� �ذ�,�������� �̾����� ����Ʈ���� ������� �ʰų� �ڲ� ���ܿ�. �ذ� �����?,"1. �̾����� ����Ʈ�� ��� ����� 2. ����Ʈ�� �������� �������� �ش� �̾��� ��� ��� ���� �� ���� 3. �̾��� ���� ���� Ȯ�� 4. �ֺ� ���� ���� Ȯ��(�ٸ� �������� ���, Wi-Fi ������ ��) 5. ����Ʈ�� �� �̾��� �߿��� �ֽ� ������Ʈ Ȯ�� ���� �õ��� �� �� �ֽ��ϴ�.","If your Bluetooth earbuds won't connect or keep disconnecting: 1. Restart both the earbuds and your smartphone. 2. Unpair/forget the earbuds in your phone's Bluetooth settings and then re-pair them. 3. Ensure the earbuds are sufficiently charged. 4. Check for interference from other Bluetooth devices, Wi-Fi routers, or microwave ovens. 5. Make sure both your phone's OS and the earbuds' firmware are up-to-date."
|
| 33 |
+
IT032,��� ���,�ȵ���̵� ����Ʈ������ Ȩ ȭ���� �ٸ� ��ó(Launcher) ������ �ٲٷ��� ��� �ϳ���?,"���� �÷��� ������ ���ϴ� ��ó ��(��: Nova Launcher, Microsoft Launcher ��)�� ��ġ�մϴ�. ��ġ ��, ����Ʈ�� ���� ���� '���ø����̼�' �Ǵ� 'Ȩ ȭ��' ���� �������� '�⺻ Ȩ ��' �Ǵ� '��ó' ������ ã�� ��ġ�� ��ó ������ �����ϸ� �˴ϴ�.","To change the home screen launcher on an Android phone: 1. Download a launcher app (e.g., Nova Launcher, Microsoft Launcher, Niagara Launcher) from the Google Play Store. 2. After installation, go to your phone's Settings app. 3. Navigate to 'Apps', 'Default apps', or 'Home screen' settings (the exact path varies by manufacturer). 4. Find the 'Default home app' or 'Launcher' setting and select the newly installed launcher you wish to use."
|
| 34 |
+
IT033,�� �м�,"����Ʈ�� ���� ��, �÷��� �� ��� �߱ޱ�(�̵巹����)�� �����ص� ������ ���� �����ΰ���?","����� ���� �÷��̳� ������ ������ ī�� ������ �ʼ��� �ƴϸ�, �ַ� �� ����, SNS, ������ ��û, �⺻���� �� ��� �� �ϻ����� �뵵�� ����Ʈ���� ����Ѵٸ� ���� ��� ������ ���� �߱ޱ� �ε� ����� ���������� ��� ������ ���� �� �ֽ��ϴ�. ������ �������� ��쿡�� ���� ����Դϴ�.","Choosing a mid-range smartphone instead of a flagship is often suitable when: 1. You don't require top-tier gaming performance or professional-level camera capabilities. 2. Your primary usage involves everyday tasks like web Browse, social media, video streaming, messaging, and running standard apps. 3. Budget is a significant consideration, as mid-range phones offer good value for money. 4. You prioritize battery life, as some mid-range models excel in this area due to less power-hungry processors."
|
| 35 |
+
IT034,���� ����,����� ���� �� MST(���׳�ƽ ���� ����) ��İ� NFC(�ٰŸ� ���� ���) ����� ���̴� �����ΰ���?,"NFC ����� ���� �ܸ���� ����Ʈ�� ���� ���� ���(RF ��ȣ)�� ���� ���� ������ ��ȯ�մϴ�. �ݸ�, MST ����� ����Ʈ������ �ڱ��� ��ȣ�� ������ ���� ���׳�ƽ ī�� �����Ⱑ �ִ� �ܸ������ ī�� ������ ���� �� �ֵ��� �ϴ� ����Դϴ�. (�Z ���̿��� �ַ� ���Ǿ����� ���� NFC �߽����� ��ȯ ��)","NFC (Near Field Communication) payments work by establishing radio communication between the phone and an NFC-enabled payment terminal. MST (Magnetic Secure Transmission), primarily used by early Samsung Pay, generates a magnetic signal that mimics a physical card swipe, allowing payments on older terminals that only have a magnetic stripe reader. NFC requires specific NFC readers, while MST had broader (but declining) compatibility with older terminals."
|
| 36 |
+
IT035,��� ���,���������� '���� ���(Focus Mode)'�� ��� �����ϰ� Ȱ���ϳ���?,"���� > ���� ��忡�� '���ر��� ���', '����', '����', '���� �ð�' �� �⺻ ��带 �����ϰų� '+' ��ư�� ���� ����� ���� ��带 ����ϴ�. �� ��庰�� �˸��� ����� ����� ���� �����ϰ�, Ư�� ��� ȭ�� �� Ȩ ȭ�� �������� �����Ͽ� �ش� ��忡 �´� ȯ���� ������ �� �ֽ��ϴ�.","To set up and use Focus Modes on an iPhone: Go to Settings > Focus. Choose a provided Focus (like Do Not Disturb, Work, Sleep, Personal) or tap the '+' button to create a custom one. For each Focus, specify which contacts and apps are allowed to send notifications. You can also link specific Lock Screen and Home Screen pages to a Focus, automatically activate it based on time, location, or app usage, and share your Focus status with others."
|
| 37 |
+
IT036,���� �߷�,����Ʈ�� ���÷����� 'Always On Display(AOD)' ����� ���� �Ҹ� ��� ���� ������ ��ġ����? ��������� ��� �ּ�ȭ�ϳ���?,"AOD�� ȭ�� ��ü�� �ƴ� �Ϻ� �ȼ��� �Ѽ� �ð�, �˸� ���� ǥ���ϹǷ� �Ϲ����� ȭ�� �������ٴ� ���� �Ҹ� �����ϴ�. OLED ���÷��̴� ������ ǥ�� �� �ȼ��� ������ ���Ƿ� LCD���� AOD�� �����մϴ�. ����, ������ ���, ���� �ֻ���(��: 1Hz) ����, �ֺ� �� ���� ��� �ڵ� ���� ���� ���� ���� �Ҹ� �ּ�ȭ�մϴ�.","Always On Display (AOD) consumes less battery than having the full screen active because it only illuminates a small number of pixels to show information like time and notifications. OLED displays are particularly efficient for AOD as black pixels are completely turned off, consuming no power. Manufacturers further minimize battery drain by using low-power co-processors, drastically reducing the refresh rate (sometimes down to 1Hz) for the AOD, and automatically adjusting brightness based on ambient light or turning off AOD when the phone is in a pocket or face down."
|
| 38 |
+
IT037,��� Ȯ��,Qi2(ġ��)' ���� ���� ǥ���� ���� Qi ǥ�ذ� ������ �� � ���� �����Ǿ�����?,"Qi2 ǥ���� ������ MagSafe ����� ������� �� MPP(Magnetic Power Profile)�� �����Ͽ�, ������� ��� ���� �ڼ� ������ ���� ������ ��ġ���� �������� ���� ������ �����ϰ� �մϴ�. �̸� ���� ���� ȿ���� ���̰�, ���������� �� ���� ���� ���� �ӵ��� �����ϸ�, ��ġ�� �� �پ��� ������ ������ ������ �����ϰ� �մϴ�.","The Qi2 wireless charging standard improves upon the original Qi standard primarily by incorporating the Magnetic Power Profile (MPP), based on Apple's MagSafe technology. This ensures perfect alignment between the charger and the device using magnets, leading to improved energy efficiency, potentially faster charging speeds (initially up to 15W, similar to MagSafe), and greater convenience, while also opening possibilities for new types of magnetic accessories like stands and mounts."
|
| 39 |
+
IT038,���� ����,Ʈ�� ���̾�� �̾���(TWS)�� '��Ƽ�� ������ ĵ����(ANC)' ����� � ������ �۵��ϳ���?,"ANC�� �̾��� �ܺ� ����ũ�� �ֺ� ����(�ַ� �����ļ��� �ݺ����� ����)�� ������ ��, �� ������ �ݴ�Ǵ� ����(������)�� ���ĸ� �����Ͽ� ���� ����Ŀ�� ����մϴ�. �� �� ���İ� ���� �����ϸ鼭 ������ ����Ű�� �����Դϴ�.","Active Noise Cancellation (ANC) in True Wireless Stereo (TWS) earbuds works by using external microphones to pick up ambient noise (particularly low-frequency, consistent sounds like engine hum or AC noise). The ANC processor then generates an 'anti-noise' sound wave that is exactly out of phase with the incoming noise. This anti-noise is played through the earbud's internal speakers, and when the two sound waves meet, they interfere destructively, effectively cancelling out the unwanted noise before it reaches the listener's ear."
|
| 40 |
+
IT039,���� �ذ�,����Ʈ�� ���� ������ �����ϴٴ� �˸��� ��� ����. ���� Ȯ�� �����?,"1. ������� �ʴ� �� ���� 2. ����/������ Ŭ����(���� ����, iCloud ��) ��� �� ���� ���� �Ǵ� �뷮 ����ȭ ��� ��� 3. ij�� ������ ���� (�� ���� �Ǵ� ��� ���� ��) 4. �ٿ�ε� ���� ���� 5. ��뷮 ����(������, ���� ������ ��) Ȯ�� �� ���� �Ǵ� ���� ��/PC�� �̵� (���� ��)","To free up storage space on your smartphone when you get 'storage full' warnings: 1. Uninstall apps you no longer use. 2. Back up photos and videos to a cloud service (like Google Photos, iCloud Photos) and then use the 'Free up space' option or delete them from the device. 3. Clear cached data for apps (via individual app settings or a device care/storage management tool). 4. Clean out the Downloads folder. 5. Identify and delete large files (videos, offline maps, game data) or move them to external storage (microSD card if supported) or a computer."
|
| 41 |
+
IT040,��� ���,���������� �ٸ� ���������� ������ �ű�� ���� ���� ����� �����ΰ���?,"���� ����(Quick Start)' ����� ����ϴ� ���� ���� �����ϴ�. �� ������ ������ �Ѱ� ���� ������ ������ �θ�, ȭ�� �ȳ��� ���� ���� �������� ����, ��, ������ ��κ��� �� ���������� �������� ���� ������ �� �ֽ��ϴ�. iCloud ����� �̿��� ���� ����� �ֽ��ϴ�.","The easiest way to transfer data from an old iPhone to a new one is using 'Quick Start'. Turn on the new iPhone and place it near your old iPhone. Follow the on-screen prompts that appear on the old iPhone to use your Apple ID to set up the new device. You can then choose to transfer data directly (iPhone to iPhone wireless transfer) or restore from an iCloud backup. Direct transfer usually moves most settings, apps, and data seamlessly."
|
| 42 |
+
IT041,�� �м�,����Ʈ�� ī���� '���� ��'�� '������ ��'�� �ٺ����� ���̴� �����ΰ���?,"���� ���� ���� ���� ��� ���������� �̵����� ���� �Ÿ��� ���������ν� �̹����� Ȯ���ϴ� ����Դϴ�. ȭ�� ���� ���� �ǻ�ü�� ������ �Կ��� �� �ֽ��ϴ�. ������ ���� �Կ��� �̹����� Ư�� ������ ����Ʈ���������� Ȯ��(ũ��)�ϴ� ����̹Ƿ�, Ȯ�� ������ ���������� ȭ���� ����(�ȼ� ���� ����)�˴ϴ�.","Optical zoom uses the camera's lens elements physically moving to change the focal length and magnify the subject without losing image quality. It's like using the zoom lens on a traditional camera. Digital zoom, however, simply crops into the existing image captured by the sensor and enlarges that portion digitally (interpolation). This results in a loss of detail and pixelation as you increase the zoom level because no new image information is actually being captured."
|
| 43 |
+
IT042,���� �߷�,����Ʈ���� '������ �Ǹ�(Right to Repair)' ���ǰ� ����ڿ��� ��ġ�� ������ ������ �����ϱ��?,"������ �Ǹ��� ����Ǹ� ����ڴ� �������� ���� ������ �ܿ��� ���� �������� �̿��ϰų� �ڰ� ������ �� ���� �� �� �ְ� �˴ϴ�. �̴� ���� ��� ����, ���� �Ⱓ ����, ��� ���� ����(���� �� ����) ���� �������� ȿ���� �̾��� �� ������, ��ǰ �� ���� �Ŵ��� ���ټ� ����� ����� �� �ֽ��ϴ�.","The ""Right to Repair"" movement, when implemented through legislation or manufacturer policy changes, positively impacts smartphone users by: 1. Providing more choices for repairs beyond the original manufacturer (independent repair shops). 2. Potentially lowering repair costs due to increased competition. 3. Enabling easier self-repair through better access to genuine parts, tools, and repair manuals. 4. Extending the usable lifespan of devices, thus reducing electronic waste and saving users money on replacements."
|
| 44 |
+
IT043,��� Ȯ��,�ֽ� ������ ��ġ(��: ������ ��ġ 7)���� ���� ���� ����� ž��Ǿ�����? (2025�� 4�� ����),"2025�� 4�� ����, ���ȭ�� ������ ��ġ ���� ��ħ���� ����� ���� ���� ���� ����� ���� ž����� �ʾҽ��ϴ�. ���� ����� ���� ���� ���̶�� �ҽ��� ������, ���� ���� �� ����� ������ ���ȭ������ �ð��� �� �ɸ� ������ ����˴ϴ�.","(As of April 2025) No, commercially available Galaxy Watch models, including the latest anticipated versions like the Galaxy Watch 7 series, do not yet feature non-invasive continuous blood glucose monitoring. While research and development in this area are ongoing by Samsung and others, the technology faces significant technical and regulatory hurdles before it can be reliably implemented and approved for consumer smartwatches."
|
| 45 |
+
IT044,���� ����,"����Ʈ�� ī���� 'Pro ���' �Ǵ� '������ ���'�� � ����̸�, ���� ����ϸ� ��������?","Pro ���� ISO(����), ���� �ӵ�, ȭ��Ʈ �뷱��, ����, ���� ���� �� ī���� �ֿ� ������ ����ڰ� �������� ������ �� �ְ� ���ִ� ����Դϴ�. �ڵ� ��� ������� ���������� �ʰų�, Ư�� ȿ��(�����, Ư�� ���� ��)�� �ǵ������� �����ϰ� ���� �� ����ϸ� �����ϴ�.","The 'Pro' or 'Expert' mode in a smartphone camera app allows users to manually control key photographic settings, such as ISO (sensitivity), shutter speed, white balance, manual focus, and exposure compensation, much like on a DSLR or mirrorless camera. It's useful when you want more creative control over the final image than the automatic mode provides, for specific effects (like long exposures for light trails), or in tricky lighting situations where the auto mode struggles to produce the desired result."
|
| 46 |
+
IT045,���� �ذ�,"Ư�� ��(��: īī����, �ν�Ÿ��)���� �˸��� ���� �ʾƿ�. Ȯ���ؾ� �� ������?",1. ����Ʈ�� ��ü �˸� ���� Ȯ�� (�ش� �� �˸� ��� ����) 2. �� ���� �˸� ���� Ȯ�� (�� ���� ���� �˸� �ѱ�/���� ����) 3. ���ر��� ��� �Ǵ� ���� ��� Ȱ��ȭ ���� Ȯ�� 4. ���� ��� ���� Ȯ�� (����� ������ �Ǵ� �˸� ���� ����) 5. �� ij�� ���� �Ǵ� �缳ġ �õ�,"If you're not receiving notifications from a specific app (e.g., KakaoTalk, Instagram): 1. Check the phone's main notification settings to ensure notifications are allowed for that app. 2. Check the in-app notification settings within the app itself. 3. Make sure Do Not Disturb or a Focus Mode isn't active and blocking notifications. 4. Verify that power-saving modes aren't restricting background data or notifications for the app. 5. Try clearing the app's cache or, as a last resort, reinstalling the app."
|
| 47 |
+
IT046,��� ���,���� ����(Google Maps) �ۿ��� �������� ������ �ٿ�ε��ϰ� ����ϴ� �����?,���� ���� �� ���� > ������ ���� �� > '�������� ����' ���� > '�� ���� ����' �� > �ٿ�ε��� ������ �簢�� �ȿ� ���Խ�Ű�� '�ٿ�ε�' ��ư ������. �ٿ�ε�� ������ ���ͳ� ������ ���� �� �ڵ����� ���˴ϴ�.,To download and use offline maps in the Google Maps app: Open Google Maps > Tap your profile picture > Select 'Offline maps' > Tap 'Select your own map' > Pan and zoom the map to frame the area you want to download within the blue rectangle > Tap 'Download'. The downloaded map will be used automatically when you have a poor or no internet connection in that area.
|
| 48 |
+
IT047,�� �м�,"����Ʈ�� ���� ��� �� ����, PIN, ���� �ν�, �� �ν� ������ �������?","����: �������� ���ȼ� ����(������ ����). PIN: ���Ϻ��� ���ȼ� ������ ���� ���� ���� ����. ���� �ν�: �����ϰ� ���ȼ� ������, �հ��� ����(����, ��ó)�� ���� �νķ� ���� ����. �� �ν�: �ſ� ����(���� ��� ��� ����)������, ����ũ ���� ��/��ο� ������ �νķ� ����, ���ȼ�(�ֵ���, ���� ��) �̽� ���� �� ���� (��� ��Ŀ� ���� �ٸ�).","Pattern: Fast, easy to remember, but low security (easily observed). PIN: More secure than pattern, but combinations can sometimes be guessed. Fingerprint: Convenient and generally secure, but recognition can fail with wet/dirty/injured fingers. Face Recognition: Very convenient (unlocks upon looking), but can struggle with masks or in low light, and security varies greatly by type (basic 2D vs. secure 3D like Face ID; 2D can sometimes be fooled by photos)."
|
| 49 |
+
IT048,���� �߷�,����Ʈ�� ��ü �ֱⰡ ���� ������� ������ �ֵ� ������ �����̶�� �����ϳ���?,1. ����Ʈ�� ���� ���� ����ȭ: �ֽ����� ������ ���� ü�� ���� ���� ����. 2. ��� ���� ���: �÷��� �� ���� �δ� ����. 3. ����Ʈ���� ���� �Ⱓ ����: �������� �ֽ� OS ��� ����. 4. ���� ��ȭ: �ų� ���� ��� �������� ��� ��ȭ ����. 5. ȯ�� ���� �� ���Ӱ��ɼ��� ���� �ν� ����.,"The trend of longer smartphone replacement cycles can be attributed to several key factors: 1. Performance Plateau: Modern smartphones are powerful enough for most users' needs, reducing the perceived performance gap between new and older models. 2. Rising Device Prices: Flagship phone costs have increased significantly, making upgrades less affordable. 3. Longer Software Support: Manufacturers are providing OS and security updates for longer periods, keeping older devices functional and secure. 4. Slower Pace of Innovation: Year-over-year hardware innovations feel less revolutionary than in the past. 5. Increased Environmental Awareness: Users are more conscious of e-waste and the sustainability of frequent upgrades."
|
| 50 |
+
IT049,��� Ȯ��,���� ���� ����(Apple Vision Pro)�� � ������ ����ΰ���?,"���� ���� ���δ� ������ ����� '���� ��ǻ��(Spatial Computer)'��, ȥ�� ����(MR) �����Դϴ�. ������� ���� ������ ������ �������� ���������Ͽ� �����ָ�, ��, ��, �������� ��ȣ�ۿ��մϴ�. VR(��������)�� AR(��������) ������ ��� �����մϴ�.","Apple Vision Pro is categorized by Apple as a ""spatial computer."" It is a mixed reality (MR) headset that blends digital content with the physical world. It allows users to interact with apps and media overlaid onto their surroundings using their eyes, hands, and voice. It provides both virtual reality (VR) and augmented reality (AR) experiences."
|
| 51 |
+
IT050,���� ����,"Ŭ���� ���丮�� ����(iCloud, Google Drive, OneDrive ��)�� ����ϴ� �ֵ� ������ �����ΰ���?","1. ������ ��� �� ����: ��� �н�/���� �� ������ ��ȣ. 2. ���� ��� �� ���� ����ȭ: ����Ʈ��, �º���, PC ��� ���� ���� ����. 3. ���� ���� Ȯ��: ��� ���� ���� �뷮 �δ� ����. 4. ���� ���� �� ���� ���̼�.","The main advantages of using cloud storage services (like iCloud Drive, Google Drive, OneDrive, Dropbox) include: 1. Data Backup and Recovery: Protects files against device loss, theft, or failure. 2. Cross-Device Synchronization: Access the same files seamlessly across multiple devices (phone, tablet, computer). 3. Storage Expansion: Offloads files from local device storage, freeing up space. 4. Easy File Sharing and Collaboration: Simplifies sharing large files or working on documents with others."
|
| 52 |
+
IT051,���� ����,"����Ʈ�� ���÷��� ��� �� 'LTPO'�� �����̸�, � ������ �ֳ���?","LTPO(Low-Temperature Polycrystalline Oxide)�� ���÷��� ���÷��� �����, ȭ�� �ֻ���(Refresh Rate)�� ��Ȳ�� �°� �ſ� ���� ����(��: 1Hz)���� �������� ������ �� �ְ� ���ݴϴ�. ������ ȭ�鿡���� �ֻ����� ���� ���� �Ҹ� ũ�� ���� �� �ִٴ� ���� ���� ū �����Դϴ�.","LTPO (Low-Temperature Polycrystalline Oxide) is an advanced display backplane technology primarily used in OLED panels. Its key advantage is enabling a variable refresh rate (VRR) that can dynamically adjust from very high (e.g., 120Hz) down to very low (e.g., 1Hz) depending on the content being displayed. This significantly reduces power consumption, especially for static content or Always-On Displays, thus improving battery life."
|
| 53 |
+
IT052,���� ����,USB-PD ���� ǥ�ؿ��� 'PPS(Programmable Power Supply)'�� �����ΰ���?,"PPS�� USB Power Delivery(PD) 3.0 �̻� ���Ե� ���α��Ӻ� ���� ���� ǥ���Դϴ�. ���� �����Ⱑ �ǽð����� ����ϸ� ���а� ������ �̼��ϰ� �����Ͽ� ���� ȿ���� ���̰� �߿��� �ٿ��ָ�, Ư�� ���� ���� �� ���� �δ��� �����ִ� ������ �մϴ�.","PPS (Programmable Power Supply) is an optional standard within the USB Power Delivery (PD) 3.0 and later specifications. It allows for dynamic adjustment of voltage and current in small increments between the charger and the device being charged. This real-time negotiation leads to more efficient charging, reduced heat generation, and potentially faster charging speeds while being gentler on the battery's health compared to fixed voltage steps."
|
| 54 |
+
IT053,�� �м�,����Ʈ�� RAM ���� �� LPDDR5�� LPDDR5X�� �ֿ� �������� �����ΰ���?,"LPDDR5X�� LPDDR5 ��� �� ���� ������ ���� �ӵ�(�뿪��)�� �����ϸ�, ���� ȿ������ �����Ǿ����ϴ�. �̴� ����� ����, ���ػ� ������ ó��, ��Ƽ�½�ŷ ��� �� ������ �ε巯�� ������ �����ϰ� �ϸ�, ���� �Ҹ�� ���̴� �� ��մϴ�.","LPDDR5X is an evolution of the LPDDR5 mobile RAM standard. The primary difference is increased data transfer speed (bandwidth), with LPDDR5X typically offering speeds up to 8533 Mbps compared to LPDDR5's typical max of 6400 Mbps. It also features further improvements in power efficiency. This results in faster performance for demanding tasks like gaming, high-res video processing, and heavy multitasking, while potentially consuming less power."
|
| 55 |
+
IT054,��� Ȯ��,��� ���÷��� ī��(UDC) ����� 2025�� ���� ��� ���ر��� �����߳���? ������ ������ �ֳ���?,"2025�� ���� UDC ����� �ʱ� �� ��� ���� �����Ǿ�, ī�� Ȧ�� ���� ������ �����鼭�� ������ ǰ���� ��ī �Կ��� �����������ϴ�. ������ ������ �Ϲ� ��ġȦ ī�� ��� �� ������ ������ ������ �����̳� ���������� �ణ�� �ս��� ������, ���÷��� �ش� ������ ȭ�� ���ϼ� ������ ������ �ذ������ �ʾҽ��ϴ�.","(As of April 2025) Under-display camera (UDC) technology has matured significantly since its initial iterations. Current implementations (seen in some foldables and concept phones) offer a more seamless full-screen experience with the camera being less visible. Image quality has improved but generally still lags slightly behind traditional punch-hole cameras, especially in challenging low-light conditions or sharpness, due to light diffraction passing through the display layers. Minor display uniformity issues over the camera area can also still be perceptible."
|
| 56 |
+
IT055,���� ����,����Ʈ���� '��ũ�� ����(Screen Reader)' ����� �����̸� �������� �ʿ��Ѱ���?,"��ũ�� ������ ȭ�鿡 ǥ�õ� �ؽ�Ʈ, ��ư, �̹��� ���� �� ��� ��Ҹ� �������� �о��ִ� ���ټ� ����Դϴ�. �ַ� �ð� ��ְ� �ִ� ����ڰ� ����Ʈ���� �����ϰ� ������ ���� �� �ֵ��� ���� ���� ���˴ϴ�. (��: iOS�� VoiceOver, Android�� TalkBack)","A Screen Reader is an accessibility feature that converts text, buttons, image descriptions, and other elements displayed on the screen into synthesized speech or braille output. It is primarily designed for users who are blind or have significant visual impairments, enabling them to navigate the interface, read content, and interact with their smartphone effectively. Examples include VoiceOver on iOS and TalkBack on Android."
|
| 57 |
+
IT056,��� ���,"����� VPN�� � ��쿡 ����ϴ� ���� ������, ��� �� ������ ���� �����ΰ���?","���� Wi-Fi ��� �� ���� ��ȭ, ���� ���� ������ ����, ���� ���� ��ȣ(IP �ּ� ����) ���� ���� ����ϴ� ���� �����ϴ�. ������ �����δ� ���� VPN�� ��� �ӵ� ����, ������ �α�, ���� ���� ���� ���� �� �����Ƿ� �ŷ��� �� �ִ� ���� VPN ���� �����ϴ� ���� ����˴ϴ�. ���� VPN ����� �ҹ��� ������ �ֽ��ϴ�.","Using a mobile VPN is beneficial when: 1. Connecting to public Wi-Fi networks (enhances security). 2. Accessing geo-restricted content or services. 3. Protecting your privacy by masking your IP address. Points to consider: Free VPNs may have limitations like slow speeds, data caps, potential logging of your activity, or even security risks. Choosing a reputable paid VPN service is generally recommended for better performance and privacy. Also, be aware of the legality of VPN use in certain countries."
|
| 58 |
+
IT057,��� Ȯ��,����Ʈ�� ��������� �������� �߰��� ���� �Ǹ�(Trade-in) ���α��� ��� �̿��ϳ���?,"�Ϲ������� ������ ������Ʈ�� ���� ������ �� ��� ���� �� '���� �Ǹ�' �ɼ��� �����մϴ�. ������ ����ϴ� ����� ��, ���� ���� �Է��ϸ� ���� ���� �ݾ��� Ȯ���� �� �ְ�, �� ��� ���� �� ���� ��⸦ ������ ������� �ݳ��ϸ� ���� �˼� �� ���� �ݾ��� Ȯ���Ǿ� ȯ�� �Ǵ� ���� ����˴ϴ�. (��: Apple Trade In, �Z Ʈ���̵���)","Official trade-in programs from manufacturers like Apple (Apple Trade In) or Samsung (Samsung Trade-in) typically work as follows: When purchasing a new device online or in-store, select the trade-in option. You'll provide details about your old device (model, condition) to get an estimated trade-in value. After receiving your new device, you'll ship your old device back using provided instructions/materials. Once inspected, the final trade-in value is confirmed and applied as a credit or refund."
|
| 59 |
+
IT058,���� �ذ�,����Ʈ�� GPS�� ���� ��ġ�� �� �� ��ų� ��Ȯ���� ��������. �ذ� �����?,1. GPS(��ġ ����) ���� Ȯ�� (���� ��Ȯ�� ��� Ȱ��ȭ) 2. �ǿܿ��� ��� ��� (���� ��ȣ ����) 3. ��� ����� 4. ���� �� ij��/������ ���� 5. A-GPS ������ ������Ʈ (Ư�� �� ��� �Ǵ� �ڵ� ������Ʈ Ȯ��) 6. ����Ʈ�� ���̽� ���� �� ��Ʈ (�ݼ� �� ���� Ȯ��) 7. OS ������Ʈ Ȯ��.,"If your smartphone's GPS is inaccurate or struggles to get a location fix: 1. Check location settings and ensure 'High accuracy' mode (using Wi-Fi, mobile networks, and GPS) is enabled. 2. Go outdoors with a clear view of the sky to allow better satellite signal reception. 3. Restart your device. 4. Clear the cache and data for your map app (e.g., Google Maps). 5. Reset/update A-GPS (Assisted GPS) data (some apps can help, or it happens automatically). 6. Temporarily remove the phone case to check for interference. 7. Check for OS updates."
|
| 60 |
+
IT059,��� ���,�ȵ���̵� ������ ����� �ֽ���(�״���) ����� ��� �ѳ���?,���� > '��Ʈ��ũ �� ���ͳ�' �Ǵ� '����' > '����� �ֽ��� �� �״���' > '����� �ֽ���' ����ġ�� �մϴ�. �ֽ��� �̸�(SSID)�� ��й�ȣ�� Ȯ���ϰų� ������ �� �ֽ��ϴ�. ��� �����縶�� �� �̸��� �ణ �ٸ� �� �ֽ��ϴ�.,To turn on the mobile hotspot (tethering) feature on an Android phone: Go to Settings > Tap 'Network & internet' or 'Connections' > Select 'Hotspot & tethering' > Toggle the switch for 'Mobile Hotspot' (or Wi-Fi hotspot) to On. You can usually configure the hotspot name (SSID) and password within this menu. The exact menu names might vary slightly depending on the phone manufacturer and Android version.
|
| 61 |
+
IT060,�� �м�,"�ֽ� ���� �ȼ� ��ġ�� �Z ������ ��ġ�� ������ ��, ������ ������ �����ΰ���? (2025�� �� ����)","(���� �ó����� ���) �ȼ� ��ġ(��: �ȼ� ��ġ 3)�� ���� Wear OS ����, ���� ��ý���Ʈ ����, Fitbit �ǰ� ���� ����� ���� ������ �����Դϴ�. ������ ��ġ(��: ������ ��ġ 7)�� �پ��� ������/ũ�� �ɼ�, �Z ���°� ������(SmartThings ��), ���� ������ Ȱ���� ��Ư�� ����� �������̽�(�Ϻ� ��) �� �� �� ���� ������ �����ϴ� ������ �ֽ��ϴ�.","(Based on typical trends, hypothetical 2025 models) The Pixel Watch (e.g., Pixel Watch 3) likely excels with its stock Wear OS experience, deep Google Assistant integration, and seamless Fitbit health tracking features. The Galaxy Watch (e.g., Galaxy Watch 7) often offers more hardware variety (sizes, rotating bezel option on some models), strong integration with the Samsung ecosystem (SmartThings, Samsung Health), potentially longer battery life on certain models, and a slightly different UI approach (One UI Watch)."
|
| 62 |
+
IT061,���� �߷�,����Ʈ�� �����ο��� ���� ������� ���(IP68)�� Ȯ���ϴ� �Ͱ� ���� ���̼��� ���̴� �� ���̿��� � ���� ����(Trade-off)�� ���� �� �ֳ���?,"���� ������� ����� ���ϱ� ���� ��������� �������� �������ϰ� ����ϰ� ��ǰ���� �ſ� �����ϰ� ��ġ�ϸ�, ���� ����(��Ʈ ��)�� �ּ�ȭ�ϴ� ������ �ֽ��ϴ�. �̷��� ����� ���� ��ǰ ������ ��ư� ����� ���� �� ���� ������ �����ϰ� �ϰ� �ð��� ����� ������ų �� �ֽ��ϴ�. ��, ��� ���� ��ȭ�� ���� ���̼��� �����ϴ� ������ �� �� �ֽ��ϴ�.","Achieving a high IP rating (like IP68) often involves design choices that can hinder repairability. Manufacturers may use strong adhesives to seal the chassis, tightly pack components, use non-modular parts, and minimize ports, all of which make water and dust ingress harder. However, these same techniques make disassembly difficult and risky, increasing the complexity, time, and cost associated with repairs, thus creating a trade-off between water resistance and ease of repair."
|
| 63 |
+
IT062,��� Ȯ��,"����Ʈ�� ī�� ���� ũ��(��: 1��ġ, 1/1.3��ġ)�� ���� ǰ���� � ������ �ֳ���?","�Ϲ������� ���� ũ�Ⱑ Ŭ���� �� �ȼ��� �� ���� ���� �Ƶ��� �� �ֽ��ϴ�. �̴� Ư�� ������ ȯ�濡�� ����� ���� �������� dz���ϸ�, ���̳��� ������(��� ��ο� �κ� ǥ�� �ɷ�)�� ���� ������ ��� �� �����մϴ�. ����, �� ���� �ɵ�(�ڿ������� ��� �帲) ǥ������ ������ �˴ϴ�.","A larger camera sensor size (e.g., 1-inch type vs. 1/1.3-inch type) generally allows each pixel on the sensor to be larger and capture more light. This translates to several advantages, particularly: better low-light performance (less noise, more detail), improved dynamic range (capturing detail in both bright highlights and dark shadows), and a shallower natural depth of field (more background blur or bokeh) without relying solely on software processing."
|
| 64 |
+
IT063,���� ����,"��ƽ �ǵ��(Haptic Feedback)'�̶� �����̸�, ����Ʈ�� ����� ���迡 ��� ��ϳ���?","��ƽ �ǵ���� ��ġ �Է��̳� Ư�� ���ۿ� �����Ͽ� ����ڿ��� �����̳� ������ ���� �˰��� �ǵ���� �����ϴ� ����Դϴ�. ����Ʈ�������� Ű���� Ÿ���� �� �̼��� ����, ��ư ���� ȿ��, ���ӿ����� Ÿ�ݰ� ���� �����Ͽ� ����� �������̽��� �� �������̰� ���� �ְ� �����, ������ ��ư�� ������ �ùķ��̼��ϴ� �� ��մϴ�.","Haptic feedback refers to the use of touch sensations, like vibrations or textures, to provide feedback to the user in response to an action or event. On smartphones, sophisticated haptics (like Apple's Taptic Engine or similar Android implementations) enhance the user experience by providing subtle tactile confirmation for keyboard typing, simulating button presses, creating immersive effects in games, and making interactions feel more tangible and intuitive."
|
| 65 |
+
IT064,���� �ذ�,����Ʈ�� ��ȭ �� ���� ��Ҹ��� �� �� �鸮�ų� �� ��Ҹ��� �� ������ �ʾƿ�. ���ΰ� �ذ�å��?,"����: ��ȭ��(����Ŀ)/����ũ ���� �̹��� ����, ��� ��ȣ �ҷ�, ����Ʈ���� ����, �������� ��� ���� ����, VoLTE ���� ���� ��. �ذ�å: 1. ��ȭ��/����ũ ���� û�� 2. ��ȭ ���� �̵�(��ȣ ���� ��) 3. ��� ����� 4. �������� ���� 5. VoLTE �ѱ�/���� �õ� 6. OS �� ��Ż� ���� ������Ʈ Ȯ��.","Poor call quality (can't hear others well / they can't hear you) can be caused by: blocked earpiece/microphone holes (dust/debris), weak cellular signal, software glitches, issues with connected Bluetooth devices, VoLTE setting problems. Solutions: 1. Gently clean the earpiece speaker and microphone holes. 2. Move to an area with better cell reception. 3. Restart your phone. 4. Turn off Bluetooth temporarily. 5. Toggle VoLTE (Voice over LTE) settings on/off. 6. Check for OS and carrier settings updates."
|
| 66 |
+
IT065,��� ���,����Ʈ�� ȭ�� ��ȭ ����� ��� ����ϳ���? (iOS / Android �Ϲ����� ���),"iOS: ���� ���Ϳ� 'ȭ�� ���' ��ư �߰� ��, ���� ���� ���� �ش� ��ư �� (����ũ ����� ���� ���� ���� ����). Android: ���� ���� ��(��� �� ����)�� 'ȭ�� ��ȭ' �Ǵ� '��ũ�� ���ڴ�' Ÿ�� �߰�/���� �� ��ȭ ���� (����� ���� �ɼ� ���� ����). �����纰�� �ణ�� ���� ����.","iOS: Add the 'Screen Recording' button to Control Center (Settings > Control Center). Open Control Center, tap the Screen Recording button (long-press to toggle microphone audio). Android (stock/common method): Swipe down to open the Quick Settings panel, find and tap the 'Screen record' or 'Screen recorder' tile (you might need to edit the panel to add it). Choose audio recording options and tap Start. Specific steps might vary slightly by manufacturer."
|
| 67 |
+
IT066,�� �м�,"����Ʈ�� ���� �� ���� ���丮�� �뷮(128GB, 256GB, 512GB ��)�� � �������� �����ؾ� �ұ��?","����/������(Ư�� 4K) �Կ� ��, ����� ���� ��ġ ����, �������� ���� ������(����, ��ȭ) �뷮, Ŭ���� ���丮�� Ȱ�� ���� ���� �����ؾ� �մϴ�. �Ϲ������� ����/������ ���� ����ڴ� 256GB �̻�, ����/�� ���� ����ڴ� 512GB �̻�, Ŭ���� Ȱ�뵵�� ���� �⺻���� ��븸 �Ѵٸ� 128GB�� ������ �� �ֽ��ϴ�. microSD ī�� Ȯ�� ���� ���ε� �߿��� ���� �����Դϴ�.","Choosing internal storage capacity (128GB, 256GB, 512GB, etc.) depends on: How often you take photos/videos (especially high-resolution like 4K), how many large games you install, how much offline media (music, movies) you store, and how heavily you rely on cloud storage. General guidance: Casual users relying on cloud might be okay with 128GB. Moderate users/photographers often find 256GB a good balance. Heavy users, gamers, or videographers might need 512GB or more. Also consider if the phone supports microSD card expansion."
|
| 68 |
+
IT067,���� �߷�,"����Ʈ�� �� ����(�۽����, �÷��̽����)�� �ξ� ���� ������ ��å ������ �����ڿ� �Һ��ڿ��� ��ġ�� ������ �����ϱ��?","������: ���� ������(15~30%)�� ���� ���ҷ� �̾��� �� ���� �λ� �Ǵ� ���� ��� ������ �� �� ������, �ܺ� ���� �ý��� ���� �� �� ���� ���� ���� ����. �Һ���: ������ �δ��� �� ����/������ �λ����� ������ �� ����. �ܺ� ���� ��� �� ���� ���Ǽ�/���ȼ� ��ȭ ���ɼ�. �� ���� ���°� ���� �� ���ſ� ������ �� �� ����.","Debates over in-app purchase commission rates (typically 15-30%) on major app stores impact developers and consumers: Developers: High fees reduce revenue, potentially leading to higher app prices, reduced investment, or exploring alternative payment systems (which can risk app removal). Consumers: Developer costs might be passed on as higher prices or subscription fees. Allowing external payments could offer choice but might fragment the payment experience or raise security concerns for some. The policies influence competition and innovation within the app ecosystem."
|
| 69 |
+
IT068,��� Ȯ��,"�Z '����(DeX)' ����� �����̸�, � ���� ����� �� �ֳ���?","�Z DeX�� �Z ������ ����Ʈ���̳� �º����� �����, TV, PC � �����Ͽ� ����ũ�� ��ǻ�Ϳ� ������ ��� ȯ���� �����ϴ� ����Դϴ�. Ű����, ���콺 ������ ���� ��Ƽ�½�ŷ, ���� �۾�, ���������̼� ���� ū ȭ�鿡�� �����ϰ� ������ �� �ֽ��ϴ�. �ַ� ������ S �ø���, ��Ʈ �ø���(����), Z ���� �ø���, �� S �ø��� �� �÷��� ���� �����˴ϴ�.","Samsung DeX (Desktop Experience) is a feature that allows users to connect their Samsung Galaxy smartphone or tablet to an external display (monitor, TV, or PC) to get a desktop-like computing environment. With a keyboard and mouse connected, users can multitask with multiple windows, work on documents, give presentations, and use apps on a larger screen. It's primarily available on flagship Galaxy devices, including recent Galaxy S series, Z Fold series, and Tab S series models."
|
| 70 |
+
IT069,���� ����,"����Ʈ�� ��� ��� ��Ʈ �� '���(Freshwater)' ������ ������ �ǹ��ϸ�, �� �ٴ幰�̳� �ٸ� ��ü�� ���ؾ� �ϳ���?","����Ʈ�� ��� ��Ʈ�� ������ ����� ȯ�濡�� ������ ��(���)�� �������� ����˴ϴ�. �ٴ幰�� ���� ������ �νļ��� ���ϰ�, ������� ȭ�� ��ǰ �� �ٸ� ��ü�� ������, ����, �ν� ���� �����Ͽ� ��� ���� �ջ��̳� ������ ����ų �� �ֽ��ϴ�. ���� ��� ����� ��� ȯ�濡 �����Ǹ�, �ٸ� ��ü�� ����Ǵ� ���� ���ؾ� �մϴ�.","Smartphone water resistance tests (IP ratings) are conducted under controlled lab conditions using fresh water. Saltwater (seawater) is highly corrosive due to salt content and can damage seals and internal components quickly. Other liquids like sugary drinks, coffee, or chemicals can cause stickiness, residue buildup, corrosion, and short circuits. Therefore, the advertised water resistance rating applies specifically to fresh water exposure, and contact with other liquids should be avoided."
|
| 71 |
+
IT070,���� �ذ�,����Ʈ�� ȭ�� ��ġ�� ���������� �� �� �ǰų� Ư�� ������ �� �ſ�. ������ �����ϱ��?,"����: 1. ȭ�� ��ȣ �ʸ�/��ȭ ���� ���� (���, ����, �β�) 2. ȭ�� ���� (����, ����) 3. �Ͻ����� ����Ʈ���� ���� (�� �浹 ��) 4. Ư�� �۰��� ȣȯ�� ���� 5. �ϵ����(��ġ��ũ�� �г�) �ջ� ���ɼ�. �ذ� �õ�: �ʸ� ���� �� ��Ʈ, ȭ�� û��, ��� �����, �ǽ� �� ����, ������ �ɼǿ��� ��ġ ��Ʈ, ���Ŀ��� ������ ����.","Intermittent or localized touch screen issues can be caused by: 1. Problems with the screen protector/tempered glass (poor fit, dirt underneath, thickness). 2. A dirty screen (water droplets, oil). 3. Temporary software glitches (app freezes). 4. Compatibility issues with a specific app. 5. Potential hardware damage to the digitizer (touch panel). Troubleshooting: Test without the screen protector, clean the screen thoroughly, restart the device, uninstall recently added apps, use developer options for touch diagnostics, and if problems persist, seek professional repair."
|
| 72 |
+
IT071,���� �ذ�,Wi-Fi ������ �Ǵµ� �ڲ� ���ܿ�. ��� �ؾ� �ϳ���?,1. ������(�����) �� ����Ʈ�� ����� 2. ������� ����Ʈ�� �Ÿ� Ȯ�� �� ��ֹ� ���� 3. ����Ʈ�� Wi-Fi �������� '��Ʈ��ũ ���� �ʱ�ȭ' �õ� 4. Ư�� Wi-Fi ��Ʈ��ũ������ ���ϴ��� Ȯ�� (�ٸ� Wi-Fi ��Ʈ) 5. ������ �߿��� ������Ʈ Ȯ�� 6. �ٸ� �� ���� ���� �� ������ ���� �Ǵ� ���ͳ� ȸ�� ���� �ʿ�.,"(Forum Post Snippet) ""My phone keeps disconnecting from Wi-Fi even though it shows connected initially. I've tried restarting everything. Usually, checking router placement, resetting network settings on the phone, or even checking the router's channel settings can help. If it happens everywhere, might be the phone; if only at home and affects other devices, likely the router or ISP."""
|
| 73 |
+
IT072,���� �ذ�,����� ������(LTE/5G)�� ���ڱ� �� �ſ�. ��Ż� �ΰ��� �ߴµ� ���ͳ� ������ �� �˴ϴ�.,1. ����� ��� �״ٰ� ���� 2. ����Ʈ�� ����� 3. ���� > '����� ��Ʈ��ũ' > '���� ����Ʈ �̸�(APN)' �ʱ�ȭ �Ǵ� Ȯ�� 4. ������ ��뷮 �ѵ� �ʰ� ���� Ȯ�� (��Ż� ��/������Ʈ) 5. SIM ī�� ������ 6. ��Ʈ��ũ ���� �ʱ�ȭ �õ� 7. Ư�� ���� ������ �� ������ �ٸ� ��ҿ��� Ȯ��.,"(Carrier Help Article) ""If mobile data isn't working despite showing a signal: 1. Toggle Airplane mode. 2. Restart your device. 3. Check APN settings (Settings > Mobile Networks > Access Point Names > Reset to default). 4. Verify you haven't exceeded your data limit. 5. Reseat your SIM card. 6. Try resetting network settings. If the issue persists, contact customer support."""
|
| 74 |
+
IT073,���� �ذ�,����Ʈ�� ���� ���� �ӵ��� �ʹ� ������. �������� �ξ� ���� �ɸ��ϴ�.,1. ��ǰ �Ǵ� ������ ���� ������ �� ���̺� ��� ���� Ȯ�� 2. ���� ���̺� �ջ� ���� Ȯ�� (�ٸ� ���̺��� ��Ʈ) 3. ���� ��Ʈ(����) �̹��� Ȯ�� �� û�� (�ε巯�� �� ���) 4. ���� �� ����� �� ��� ���� 5. ��� ����� 6. ����� �� Ȱ�� Ȯ�� (���� �Ҹ� ���� �� Ȯ��) 7. ���� ���� �����Ͽ� ���� �ӵ� Ȯ�� (����Ʈ���� ���� ����).,"(Troubleshooting Guide) ""Slow charging can be due to a faulty cable, adapter, or dirty charging port. Always use certified chargers/cables. Check the port for lint/debris. Avoid heavy usage while charging. Background apps can also consume power. Testing in Safe Mode can help rule out third-party app issues. If the problem continues, the battery itself might be degrading."""
|
| 75 |
+
IT074,���� �ذ�,����Ʈ�� ������ ���ڱ� �� ������. ȭ���� �׳� ��İ� ���ɴϴ�.,"1. ���� ���� ���� ���ɼ�: �ּ� 30�� �̻� ���� �õ� 2. ���� ����� �õ� (������ ��� Ȯ��: ��) ����+�����ٿ� ��� ������) 3. �ٸ� ������ �� ���̺��� ���� �õ� 4. ���� ���� ���� ���� Ȯ�� 5. ȭ�� ��Ⱑ ������ �Ǿ� �ְų� �ܺ� ���÷��� ���� �������� Ȯ�� (���� ��) 6. �� ������� �ذ� �� �Ǹ� ���� ���� ���� �ʿ� (����, ���κ��� �� �ϵ���� ���� ���ɼ�).","(Device Manual Excerpt) ""If your phone doesn't turn on (black screen): 1. Charge the device for at least 30 minutes as the battery might be fully depleted. 2. Attempt a forced restart (e.g., Press and hold Power + Volume Down buttons for 10-15 seconds). 3. Try a different charger and cable. 4. Check the charging port connection. If unresponsive, it might require professional service."""
|
| 76 |
+
IT075,���� �ذ�,����Ʈ�� ī�� ���� �����ϸ� ȭ���� ��İ� �����ų� ���� �ٷ� ����˴ϴ�.,"1. ī�� ���� ���� ���� Ȯ�� 2. ��� ����� 3. ���� > ���ø����̼� > ī�� �� > ���� ���� > 'ij�� ����' �� '������ ����' �õ� (������ ���� �� ���� �ʱ�ȭ��) 4. �ٸ� ī�� ��(��: �ν�Ÿ��, ����� ��) ���� �� ���� ���� Ȯ�� (�ϵ���� ���� ����) 5. ���� ��忡�� ī�� �� ���� (����Ʈ���� �浹 Ȯ��) 6. OS ������Ʈ Ȯ�� 7. ���� �ʱ�ȭ (���� ����, ������ ��� �ʼ�).","(Tech Support Forum) ""Camera app showing black screen or crashing? First, restart the phone. Then try clearing the camera app's cache and data (Settings > Apps > Camera > Storage). Check if other apps using the camera work. Test in Safe Mode. If it still fails, it could be a hardware issue or might need a factory reset after backing up data."""
|
| 77 |
+
IT076,���� �ذ�,SIM ī�带 ã�� �� ����' �Ǵ� 'SIM ī�� ����' ���� ������ ����.,1. ����� ��� �״ٰ� ���� 2. ��� ����� 3. SIM ī�� Ʈ���̸� ���� SIM ī�尡 �ùٸ��� �����Ǿ����� Ȯ�� �� ������ (�ݼ� ���˸� �ε巴�� �۱�) 4. �ٸ� �۵��ϴ� SIM ī�带 �־� ��Ʈ (SIM ī�� ��ü �ҷ� Ȯ��) 5. ���� SIM ī�带 �ٸ� �� �־� ��Ʈ (��� ���� Ȯ��) 6. ��Ʈ��ũ ���� �ʱ�ȭ 7. ��Ż翡 ���� (SIM ī�� Ȱ��ȭ/��ü �ʿ� ���ɼ�).,"(Mobile Network Provider FAQ) ""Seeing a 'No SIM card' error? 1. Toggle Airplane mode. 2. Restart your device. 3. Remove and reinsert the SIM card, ensuring it's seated correctly and the contacts are clean. 4. Try the SIM in another phone, or another working SIM in your phone to isolate the issue (SIM vs. phone). 5. Reset network settings. 6. Contact us if the problem persists, as the SIM might need replacement or reactivation."""
|
| 78 |
+
IT077,���� �ذ�,����Ʈ���� ���� ���� �ڲ� ����õſ�.,"1. �ֱ� ��ġ�� �� Ȯ�� �� ���� (Ư�� �� �浹 ���ɼ�) 2. SD ī�� ��� �� ���� �� ���� Ȯ�� (SD ī�� ���� ���ɼ�) 3. ���� ���� ���� Ȯ�� (��ü�� �ƴ� ���) �Ǵ� ���� ����ȭ �ǽ� 4. ���� ���� ���� ���� Ȯ�� 5. OS ������Ʈ Ȯ�� �� ��ġ 6. ���� ��� ���� �� ���� Ȯ�� (�ý��� �� ��������, ��ġ�� �� �������� ����) 7. ���� �ʱ�ȭ (������ ��� �ʼ�).","(Android Help Center) ""Random reboots can be caused by problematic apps, faulty hardware (like battery or SD card), insufficient storage, or OS issues. Try uninstalling recently added apps, removing the SD card, checking for OS updates, and testing in Safe Mode. If frequent reboots continue, a factory reset (after backup) or hardware inspection might be necessary."""
|
| 79 |
+
IT078,���� �ذ�,Ư�� ���� �����ϸ� �ٷ� '���� �����Ǿ����ϴ�' ������ �߸鼭 ����˴ϴ�.,"1. �� ���� ���� �� �ٽ� ���� 2. ��� ����� 3. ���� > ���ø����̼� > �ش� �� > ���� ���� > 'ij�� ����' �õ� 4. �� ������Ʈ Ȯ�� (�� ����) 5. �� ���� �� �缳ġ 6. OS ������ �� ȣȯ�� Ȯ�� (Ư�� OS ������Ʈ ���� �� ��) 7. '������ ����' �õ� (�� ���� �� ������ �ʱ�ȭ��, ������ ����) 8. �����ڿ��� ����.","(App Troubleshooting Guide) ""If an app crashes immediately upon opening: 1. Force stop the app and relaunch. 2. Restart your phone. 3. Clear the app's cache (Settings > Apps > [App Name] > Storage). 4. Check for app updates in the Play Store/App Store. 5. Uninstall and reinstall the app. 6. Ensure app compatibility with your current OS version. Clearing app data (use cautiously) might also help."""
|
| 80 |
+
IT079,���� �ذ�,����Ʈ�� ���� �߿� ��Ⱑ �ʹ� �߰ſ�����. ������ �ǰ���? ��� �ؾ� �ϳ���?,"���� �� �ణ�� �߿��� ����������, ������ ����� ������ �̴߰ٸ� ���ǰ� �ʿ��մϴ�. 1. ���� �� ����� ����/�� ��� �ߴ� 2. ��dz�� �� �Ǵ� ������ ���� (�̺� �� �� ���ϱ�) 3. ��ǰ/���� ������ �� ���̺� ��� Ȯ�� 4. ����Ʈ�� ���̽� ��� ���� 5. ���� ���� ��� ��Ȱ��ȭ (���� ���� ��) 6. ��� ����� �� ����. ���������� ������ �߿� �� ���� �Ǵ� ���� ȸ�� ������ �� ������ ���� �ʿ�.","(Battery Safety Information) ""It's normal for your phone to get slightly warm while charging, especially during fast charging. However, if it becomes uncomfortably hot: Stop using demanding apps while charging, ensure good ventilation (avoid charging under pillows), use the original or certified charger/cable, remove the case, and consider disabling fast charging temporarily if the option exists. Persistent excessive heat could indicate an issue requiring service."""
|
| 81 |
+
IT080,���� �ذ�,����Ʈ�� ȭ���� �̼��ϰ� �����ŷ���. ������ �����?,1. ȭ�� ��� ���� Ȯ�� (�ڵ� ��� ���� �Ǵ� Ư�� ��� ���� ����) 2. Ư�� �� ��� �ÿ��� ���ϴ��� Ȯ�� (�� ȣȯ�� ����) 3. ��� ����� 4. ������ �ɼ� Ȯ�� (�ϵ���� �������� ��� �� �� �� ���� ���� ���� �õ� - ���� �ʿ�) 5. ���� ��� ���� �� ���� Ȯ�� 6. OS ������Ʈ Ȯ�� 7. ȭ�� ��ü�� �ϵ���� ���� ���ɼ� (���ΰ��� �ٸ�). ���� �� ���� ���� ����.,"(Display Troubleshooting Tips) ""Screen flickering can be caused by software glitches, app incompatibility, or hardware issues. Check if it happens at specific brightness levels or only in certain apps. Try restarting the phone, disabling adaptive brightness temporarily, checking developer options (use caution), and testing in Safe Mode. If flickering persists across all apps and conditions, it might be a display hardware problem."""
|
| 82 |
+
IT081,���� �ذ�,����Ʈ�� ����Ŀ �Ҹ��� �۰ų� �������ŷ���.,"1. ����Ŀ ��(����) �̹��� Ȯ�� �� �ε巯�� �ַ� û�� 2. ���� ���� Ȯ�� (�̵�� ����, ��ȭ ���� ��) 3. ���ر��� ��� �� ���Ұ� ���� ���� Ȯ�� 4. Ư�� �ۿ����� ���� ���ϴ��� Ȯ�� 5. �������� ����Ŀ/�̾��� ���� ���� Ȯ�� �� ���� �� ��Ʈ 6. ��� ����� 7. ���� ��忡�� ��Ʈ (����Ʈ���� ���� Ȯ��) 8. ����Ŀ ��ü�� �ϵ���� ���� ���ɼ�.","(Audio Problem Fixes) ""Low or distorted speaker sound? 1. Clean the speaker grille gently. 2. Double-check all volume settings (media, call, ringtone). 3. Ensure Do Not Disturb isn't silencing media. 4. Test sound in multiple apps. 5. Disconnect any Bluetooth audio devices. 6. Restart the phone. 7. Test in Safe Mode. If the issue remains, the speaker hardware might be faulty."""
|
| 83 |
+
IT082,���� �ذ�,����Ʈ�� ���� ��ư�̳� ���� ��ư�� �����ϰų� ������ ������ �����.,1. ��ư �ֺ� �̹��� Ȯ�� �� û�� (���� ���� ��� ��) 2. ���̽��� ��ư�� ������ �ְų� �����ϴ��� Ȯ�� �� �����ϰ� ��Ʈ 3. ��� ����� (ȭ�� ��ġ�� ���� ��) 4. ������ �ջ�(���� ��) ���� Ȯ�� 5. ����Ʈ������ ��ư ��� Ȱ�� (���ټ� �� �� - �ӽù���) 6. �������� ���� �� ������ ��ư ��ǰ ���� ���ɼ� ���� ���� ���� �湮 �ʿ�.,"(Hardware Button Issues) ""If power or volume buttons are stuck or unresponsive: 1. Check for debris around the button and try cleaning gently (e.g., with compressed air). 2. Remove the case to ensure it's not interfering. 3. Restart the device if possible using on-screen options. 4. Consider physical damage history. 5. You might be able to use software alternatives temporarily (e.g., accessibility menus). Persistent issues likely require hardware repair."""
|
| 84 |
+
IT083,���� �ذ�,���� ���� �е忡 ����Ʈ���� �÷����Ƶ� ������ ���۵��� �ʾƿ�.,"1. ����Ʈ�� ���� ���� ������ �����ϴ��� Ȯ�� 2. ���� �е� ���� ���� Ȯ�� 3. ����Ʈ���� ���� �е� ���� ��ġ Ȯ�� (��Ȯ�� ���� ��ġ�� ���߱�) 4. ����Ʈ�� ���̽� ���� �� �õ� (�β��� ���̽�, �ݼ� ������ �� ���� ����) 5. ����Ʈ�� �� ���� �е� �����(���� ��) 6. �ٸ� ���� ���� ���� ���� ���� �е� ��Ʈ 7. �ٸ� ���� ������� ����Ʈ�� ��Ʈ.","(Wireless Charging Not Working) ""Phone not charging wirelessly? 1. Confirm your phone supports Qi wireless charging. 2. Ensure the charging pad is powered on. 3. Adjust phone placement on the pad (coil alignment is key). 4. Remove thick cases or metal attachments. 5. Restart both phone and pad (if possible). 6. Test the pad with another compatible device, or your phone on another pad to isolate the problem."""
|
| 85 |
+
IT084,���� �ذ�,"����Ʈ��ġ(������ ��ġ, ���� ��ġ ��)�� ����Ʈ�����κ��� �˸��� ���� ���ؿ�.",1. ����Ʈ���� ��ġ �������� ���� ���� Ȯ�� 2. ��ġ�� ����� ��� �Ǵ� ���ر��� ���� �����Ǿ� �ִ��� Ȯ�� 3. ����Ʈ�� ��(��ġ ���� ��)���� �˸� ���� Ȯ�� (�˸� ���� �� ���� ��) 4. ����Ʈ�� ��ü �˸� ���� Ȯ�� (�ش� �� �˸� ��� ����) 5. ����Ʈ���� ��ġ ��� ����� 6. ��ġ ���� �� ������Ʈ Ȯ�� 7. ��ġ �ʱ�ȭ �� �翬�� (���� ����).,"(Smartwatch Notification Sync) ""Not getting phone notifications on your watch? 1. Check Bluetooth connection between phone and watch. 2. Ensure the watch isn't in Airplane or Do Not Disturb mode. 3. Verify notification settings in the watch companion app on your phone (which apps are allowed). 4. Check phone's notification settings for those apps. 5. Restart both devices. 6. Update the watch app. 7. As a last resort, unpair and reset the watch, then re-pair."""
|
| 86 |
+
IT085,���� �ذ�,���� ����(�Ǵ� iCloud) ����ó�� �� ����Ʈ������ ����ȭ���� �ʾƿ�.,1. �� ����Ʈ�� ���ͳ� ���� ���� Ȯ�� (Wi-Fi/����� ������) 2. ����Ʈ�� ���� > ���� ������ �ش� ����/iCloud ���� �α��� �� ����ȭ ���� Ȯ�� ('����ó' ����ȭ ���� �ִ���) 3. ���� ����ȭ �õ� (���� ���� �� '���� ����ȭ' ��ư) 4. ����ó �� ��ü ���� Ȯ�� (ǥ���� ���� ���� ��) 5. ��� ����� 6. ���� ���� �� �ٽ� �߰� 7. ��(contacts.google.com / icloud.com)���� ����ó ���������� ���̴��� Ȯ��.,"(Account Sync Issues) ""Contacts not syncing to new phone? 1. Ensure stable internet connection. 2. Go to Settings > Accounts > [Your Google/iCloud Account] and check if 'Contacts' sync is enabled. 3. Try initiating a manual sync ('Sync now'). 4. Check settings within the Contacts app itself (accounts to display). 5. Restart the phone. 6. Remove and re-add the account. 7. Verify contacts appear correctly on the web version (contacts.google.com or icloud.com)."""
|
| 87 |
+
IT086,���� �ذ�,�ȵ���̵� ������ 'System UI�� �������� �ʽ��ϴ�' �Ǵ� 'System UI�� �����Ǿ����ϴ�' ������ ����.,"1. ��� ����� 2. �ֱ� ��ġ/������Ʈ�� �� Ȯ�� �� ���� (Ư�� ��ó, ���� �� �ý��� UI�� ���� �ִ� ��) 3. ���� > ���ø����̼� > �� ��� ���� '������' > '�ý��� �� ǥ��' > 'System UI' �� ���� > ���� ���� > 'ij�� ����' �õ� 4. Google �� �� Google Play ���� ������Ʈ Ȯ�� �� ij�� ���� 5. ���� ���� �����Ͽ� ���� Ȯ�� 6. ���� �ʱ�ȭ (������ ��� �ʼ�).","(Android Common Issues Forum) ""Getting 'System UI isn't responding'? This often relates to a conflicting app (like a third-party launcher or widget), outdated Google apps, or corrupted cache. Try restarting, uninstalling recent apps, clearing System UI cache (Settings > Apps > Show system apps > System UI > Storage > Clear cache), updating Google apps, and testing in Safe Mode. A factory reset is the last resort."""
|
| 88 |
+
IT087,���� �ذ�,���� �÷��� ����(Google Play Services) ���� ���� ������ ��� ���Ϳ�.,1. ��� ����� 2. ���� > ���ø����̼� > (�ý��� �� ǥ��) > Google Play ���� > ���� ���� > 'ij�� ����' > '��� ������ �����' �õ� (���� ��α��� �ʿ��� �� ����) 3. Google Play ���� �� ������Ʈ Ȯ�� (�÷��� ����� �Ǵ� APK Mirror �� �ŷ��� �� �ִ� ��ó) 4. Google Play ����� �� ij�� �� ������ ���� 5. Google ���� ���� �� ���� 6. ��¥ �� �ð� ���� �ڵ� ����ȭ Ȯ��.,"(Google Support Page Snippet) ""Errors related to Google Play Services can often be resolved by: 1. Restarting the device. 2. Clearing the cache and data for Google Play Services (Settings > Apps > Google Play Services > Storage > Manage space > Clear All Data). 3. Ensuring Play Services and Play Store apps are up-to-date. 4. Removing and re-adding your Google account. 5. Checking that date and time are set automatically."""
|
| 89 |
+
IT088,���� �ذ�,"���� �Է��Ϸ��� �ϴµ� Ű���尡 ��Ÿ���� �ʰų�, ��Ÿ���ٰ� �ٷ� �������.","1. ��� ����� 2. ���� ��� ���� Ű���� �� ���� ���� (���� > ���ø����̼� > �ش� Ű���� ��) 3. �ش� Ű���� �� ij�� ���� (���� ���� ��) 4. �ٸ� Ű���� ������ ���� �� ��Ʈ (��: Gboard, �Z Ű���� ��) 5. �ش� Ű���� �� ������Ʈ Ȯ�� �Ǵ� �缳ġ 6. ���� ��忡�� ��Ʈ (Ÿ�� �� �浹 Ȯ��).","(Keyboard Troubleshooting Steps) ""Keyboard not appearing or disappearing? 1. Restart your phone. 2. Force stop the keyboard app you are using (via Settings > Apps). 3. Clear the cache for that keyboard app. 4. Try switching to a different keyboard app (like Gboard or the default manufacturer keyboard). 5. Update or reinstall the problematic keyboard app. 6. Test in Safe Mode to rule out conflicts."""
|
| 90 |
+
IT089,���� �ذ�,"����� �ֽ��̿� �ٸ� ���� ����Ǵµ�, ���ͳ� ������ �Ҿ����ϰų� �ڲ� ����ϴ�.","1. �ֽ��� �� ����Ʈ�� ����� �� ����� ��� ����� 2. �ֽ��� ���� Ȯ�� (���� ���� ��� �� ����, ���� ��� ��) 3. ����Ʈ���� ����� ������ ��ȣ ���� Ȯ�� 4. �ֽ��� ���ļ� �뿪 ���� �õ� (2.4GHz <-> 5GHz) 5. �ֺ� ȯ���� ���� ���� Ȯ�� 6. ����Ʈ�� ����� ������ ��� ���� ���� Ȯ�� 7. ����Ʈ�� OS ������Ʈ Ȯ��.","(Mobile Hotspot Guide) ""Unstable hotspot connection for connected devices? 1. Restart both the hotspot phone and the connected device. 2. Check hotspot settings (max connections, power saving options). 3. Ensure the hotspot phone has a strong mobile data signal. 4. Try switching the hotspot band (2.4GHz vs 5GHz). 5. Minimize potential interference. 6. Check for background data restrictions on the hotspot phone. 7. Keep the OS updated."""
|
| 91 |
+
IT090,���� �ذ�,�������� �̾������� ���� ���� �� �Ҹ��� �ڲ� ����ų� �����ſ�.,"1. �̾����� ����Ʈ�� ����� �� ���� 2. �̾��� ���� �ܷ� Ȯ�� 3. ����Ʈ���� �̾��� �Ÿ� Ȯ�� (������ ����) 4. �ٸ� �������� ��� �Ǵ� Wi-Fi ��ȣ���� ���� �ּ�ȭ (�ֺ� ��� ����) 5. ����Ʈ�� ���̽� ���� �� ��Ʈ 6. ����Ʈ�� �� �̾��� �߿��� �ֽ� ������Ʈ 7. ������ �ɼǿ��� �������� �ڵ� ���� �õ� (SBC, AAC, aptX �� - ȣȯ�� Ȯ�� �ʿ�) 8. �ٸ� ����Ʈ���� �����Ͽ� ��Ʈ (���� ���� ��� �ľ�).","(Bluetooth Audio Stuttering Fixes) ""Bluetooth audio skipping? 1. Restart & re-pair devices. 2. Ensure earbuds are charged. 3. Keep phone and earbuds close. 4. Minimize interference (turn off other Bluetooth devices, move away from routers). 5. Remove phone case. 6. Update firmware for both phone and earbuds. 7. Try changing Bluetooth codecs in Developer Options (if applicable). 8. Test earbuds with another phone to isolate the issue."""
|
| 92 |
+
IT091,���� �ذ�,"��ȭ�� �� ȭ���� ������ �ʰų�, ��ȭ ���� �Ŀ��� ȭ���� �ٷ� ������ �ʾƿ�. (���� ���� ����)",1. ȭ�� ��� ���� ���� �κ�(��ȭ�� ��ó) �̹��� Ȯ�� �� û�� 2. ȭ�� ��ȣ �ʸ�/��ȭ ������ ������ �����ų� �����ϴ��� Ȯ�� �� �����ϰ� ��Ʈ 3. ��� ����� 4. ���� ���� ��Ʈ ���(���� ��� �Ǵ� ������Ƽ ��) ��� 5. OS ������Ʈ Ȯ�� 6. ���� ��ü�� �ϵ���� ���� ���ɼ� (���� �� ���� ���� ����).,"(Proximity Sensor Issues) ""Screen not turning off during calls or staying off after? This is likely a proximity sensor issue. 1. Clean the sensor area near the earpiece. 2. Check if your screen protector is blocking the sensor; test without it. 3. Restart the phone. 4. Use a diagnostic tool/app to test the sensor. 5. Check for OS updates. If it persists, the sensor hardware might be faulty and require service."""
|
| 93 |
+
IT092,���� �ذ�,����Ʈ�� ȭ�� �ڵ� ȸ��(����/���� ��ȯ) ����� �۵����� �ʾƿ�.,"1. ���� ���� ������ '�ڵ� ȸ��' ����� ���� �ִ��� Ȯ�� (���� �ִٸ� ���Ͽ� �ѱ�) 2. ��� ����� 3. Ư�� �ۿ����� ȸ���� �� �Ǵ��� Ȯ�� (�� ��ü���� ȸ�� ������ ���ɼ�) 4. ����(���ӵ���, ���̷ν�����) ���� (���� ��� �Ǵ� ���� �� ���) 5. ���� ��忡�� ��Ʈ (����Ʈ���� �浹 Ȯ��) 6. OS ������Ʈ Ȯ��. ���� �� ���� �ϵ���� ���� ���ɼ�.","(Auto-Rotate Not Working) ""Screen not auto-rotating? 1. Check the Quick Settings panel and ensure 'Auto-rotate' (or Portrait/Landscape lock) is enabled. 2. Restart your device. 3. Verify if rotation works in other apps (some apps don't support rotation). 4. Use a sensor testing app to check the accelerometer/gyroscope. 5. Test in Safe Mode. 6. Check for OS updates. Persistent issues might point to a sensor hardware failure."""
|
| 94 |
+
IT093,���� �ذ�,���� �ν� ������ �� �۵����� �ʰų� �ʹ� ������.,1. ���� ǥ�� �� �հ��� û�� ���� Ȯ�� (�����ϰ� �����ϰ�) 2. ��ϵ� ���� ���� �� ���� (���� ������ ���� �� ��ĵ) 3. ȭ�� ��ȣ �ʸ�/��ȭ ������ ����(Ư�� ���÷��� ������)�� �����ϴ��� Ȯ�� (ȣȯ �ʸ� ���) 4. ��� ����� 5. OS �� ���� ��ü �ν� ����Ʈ���� ������Ʈ Ȯ�� 6. �������� ���� �ν� ���� ����ȭ �ɼ� Ȯ�� (�ִϸ��̼� ȿ�� ���� ��) 7. ���� ��� ��Ʈ.,"(Fingerprint Sensor Troubleshooting) ""Slow or unreliable fingerprint sensor? 1. Clean the sensor surface and your finger (ensure dry). 2. Delete registered fingerprints and re-register them carefully from multiple angles. 3. Ensure your screen protector is compatible (especially for under-display sensors). 4. Restart the phone. 5. Check for OS/biometric software updates. 6. Look for fingerprint optimization settings (e.g., disabling animations). 7. Test in Safe Mode."""
|
| 95 |
+
IT094,���� �ذ�,������ 20~30% ���Ҵٰ� ǥ�õǴµ� ���ڱ� ������ ����������.,1. ���� ����(Ķ���극�̼�) �õ�: ���� ���� -> ���� ����(�ڵ� ����) -> �ٽ� ���� ����. (��Ȯ�� ����� ������ ������ �� ����) 2. ���� ��� ��� Ȯ�� (Ư�� ���� �������� �Ҹ� �����ϴ���) 3. �߿� ȯ�� ���� ���� Ȯ�� (���¿��� ���� ���� ����) 4. OS ������Ʈ Ȯ�� (���� ���� ���� ���� ���ɼ�) 5. ���� ����ȭ ���ɼ� ���� (���� ���� ����). ���� �� ���� ��ü ���� �ʿ�.,"(Phone Shutting Off Unexpectedly) ""Phone dying suddenly even with 20-30% battery left? 1. Try battery calibration (fully charge -> use until auto-off -> fully charge again). 2. Check battery usage stats for rogue apps. 3. Avoid extreme cold temperatures. 4. Check for OS updates. 5. This often indicates battery degradation (wear and tear). Consider battery replacement if the issue persists."""
|
| 96 |
+
IT095,���� �ذ�,������ ���� '���� ���� ����(Wireless PowerShare)' ����� �۵����� �ʾƿ�.,1. ���� ���� ���� ��� Ȱ��ȭ Ȯ�� (���� ���� �� �Ǵ� ���� > ����) 2. �����ϴ� ���� ���� �ܷ��� ���� ����(���� 30%) �̻����� Ȯ�� 3. ���� ���� ��Ⱑ Qi ���� ���� ǥ���� �����ϴ��� Ȯ�� 4. �� ��� �� �߾��� ��Ȯ�� �´�� �ִ��� Ȯ�� 5. �� ��� ��� ���̽� ���� �� �õ� 6. �����ϴ� ���� ����� ����Ǿ� ���� ������ Ȯ�� (�Ϻ� �� ����) 7. ��� �����.,"(Wireless PowerShare Issues) ""Wireless PowerShare not working? 1. Ensure the feature is enabled (Quick Settings or Battery settings). 2. Check if the sharing phone's battery is above the minimum threshold (usually 30%). 3. Confirm the receiving device supports Qi wireless charging. 4. Align the backs of the devices carefully (center-to-center). 5. Remove cases from both devices. 6. Disconnect the sharing phone from any wired charger. 7. Restart the sharing phone."""
|
| 97 |
+
IT096,���� �ذ�,"��ǻ�Ϳ��� ����Ʈ������ ����/������ ������ �����ߴµ�, ������ �ۿ��� ���� ���̰ų� ������ �ʾƿ�.","1. ���� ���� �� ���� �Ҿ��� ���ɼ�: ���̺� ���� ���� Ȯ�� �� �ٸ� ���̺�/��Ʈ ���, ������ �õ� 2. ����Ʈ�� ���� ���� ���� ���� Ȯ�� 3. ���� ���� ȣȯ�� Ȯ�� (����Ʈ������ �����ϴ� �ڵ�/��������) 4. ���� ���� ��ü �ջ� ���� Ȯ�� (��ǻ�Ϳ��� ���� ����Ǵ���) 5. ����Ʈ�� �̵�� ��ij�� ����� (��� ����� �Ǵ� ���� �� ���) 6. ���� Ž���� ������ ���� ���� ���� �� ���� �õ�.","(File Corruption After Transfer) ""Files appear corrupted or won't open after copying from PC? 1. Ensure a stable connection during transfer; try a different cable/port and re-transfer. 2. Check for sufficient storage space on the phone. 3. Verify file format compatibility (supported codecs). 4. Confirm the original file isn't corrupted (check on PC). 5. Trigger media rescan (restarting phone usually does this). 6. Try opening the file directly using a file manager app."""
|
| 98 |
+
IT097,���� �ذ�,����Ʈ������ '�� ��� ã��(Find My Device / Find My Mobile)'�� ��ġ ��ȸ�� �� �ſ�.,1. �ش� ��� Ȱ��ȭ ���� Ȯ�� (���� > ����/���� > �� ��� ã��) 2. ����Ʈ�� ���� ���� �ְ� ���ͳ�(����� ������ �Ǵ� Wi-Fi)�� ����Ǿ� �ִ��� Ȯ�� 3. ��ġ(GPS) ���� ���� �ִ��� Ȯ�� 4. �ش� ����/�Z ���� �α��� ���� Ȯ�� 5. ����� ������ ���� ���� Ȯ�� (�� ��� ã�� ���� ����) 6. ��� �����.,"(Find My Device Not Working) ""Unable to locate phone using Find My Device/Mobile? 1. Ensure the feature is enabled in settings (Security/Account > Find My Device). 2. The phone must be powered on and connected to the internet (mobile data or Wi-Fi). 3. Location/GPS services must be turned on. 4. Verify you're signed into the correct Google/Samsung account. 5. Check for background data restrictions for related services. 6. Restart the phone."""
|
| 99 |
+
IT098,���� �ذ�,S��(�Ǵ� ���� �潽)�� �º���/���� ������� �ʰų� �ʱ� �ν��� ����Ȯ�ؿ�.,1. S��/�����潽 ���� ���� Ȯ�� �� ���� (��� ���� �Ǵ� ���� ����) 2. �������� ���� Ȯ�� �� �翬�� (S�� ���� �� �� ��� ��� ��) 3. ���� ����/�ջ� ���� Ȯ�� �� ��ü 4. ȭ�� ��ȣ �ʸ�/��ȭ ������ �����ϴ��� Ȯ�� (Ư�� �β��� �ʸ�) 5. ��� ����� 6. S��/�����潽 ���� �ʱ�ȭ (����Ʈ���� ���� ��) 7. �ٸ� ȣȯ �ۿ��� ��Ʈ.,"(Stylus Connection/Accuracy Problems) ""S Pen/Apple Pencil not connecting or writing inaccurately? 1. Ensure the stylus is charged (attach to device or charge separately). 2. Check Bluetooth settings and try re-pairing (needed for air actions/features). 3. Inspect the nib/tip for wear or damage and replace if needed. 4. Check if the screen protector is interfering (especially thick ones). 5. Restart the host device. 6. Reset stylus settings if available. 7. Test in different compatible apps."""
|
| 100 |
+
IT099,���� �ذ�,"����Ʈ���� USB-C ���̺��� ����Ϳ� �����ߴµ� ȭ���� ������ �ʾƿ�. (DeX, �̷��� ��)",1. ����Ʈ�� ���� USB-C ���� ���� ���(DisplayPort Alt Mode �Ǵ� �ش� ���(DeX ��)) �����ϴ��� Ȯ�� 2. ����ϴ� USB-C ���̺��� ���� ���(DP Alt Mode) �����ϴ��� Ȯ�� (���� ���� ���̺� �Ұ�) 3. ����� �Է� �ҽ� ���� Ȯ�� (����� HDMI/DP ��Ʈ ����) 4. �ٸ� ����� �Ǵ� �ٸ� ���̺��� ��Ʈ 5. ����Ʈ�� �� ����� ����� 6. ����Ʈ�� ���� Ȯ�� (DeX/ȭ�� �̷��� ���� ����).,"(USB-C to Monitor No Signal) ""No display when connecting phone to monitor via USB-C? 1. Verify your phone model supports video output over USB-C (DisplayPort Alt Mode, DeX, etc.). 2. Ensure you're using a USB-C cable that supports video output (not just charging). 3. Check the monitor's input source setting (select the correct HDMI/DP port). 4. Test with a different monitor or cable. 5. Restart both the phone and monitor. 6. Check relevant phone settings (DeX, Screen Mirroring)."""
|
| 101 |
+
IT100,���� �ذ�,��Ʃ�� �ۿ��� �������� ���µ� ȭ���� �������� �ڲ� ���۸��� �ɸ��ų� ���ܿ�. ���ͳ� �ӵ��� �����.,1. ��Ʃ�� �� ij�� ���� (���� > ���ø����̼� > YouTube > ���� ����) 2. ��Ʃ�� �� ������Ʈ Ȯ�� �Ǵ� �缳ġ 3. ��� ����� 4. �����(������) ����� 5. �ٸ� ������ ���� ���� Ȯ�� (��Ʈ��ũ ���� ����) 6. DNS ���� ���� �õ� (��: Google DNS 8.8.8.8) 7. ����忡�� ������ ���� ����ϴ� �� Ȯ�� �� ���� 8. �ð��� ���� �� �õ� (��Ʈ��ũ ȥ�� �ð���).,"(YouTube Buffering Despite Fast Internet) ""YouTube videos keep buffering/stuttering even with good internet speed? 1. Clear YouTube app cache. 2. Update or reinstall the YouTube app. 3. Restart your device. 4. Restart your router. 5. Check if other devices on the network have the same issue. 6. Try changing DNS settings (e.g., to Google DNS). 7. Close background apps consuming bandwidth. 8. Test during different times of day (off-peak hours)."""
|
| 102 |
+
��ó,,,,
|
| 103 |
+
https://blog.nus.edu.sg/esim/for-travel/the-best-esim-card-for-sweden-travel-in-2024.html,,,,
|
| 104 |
+
https://www.windowtonews.com/news.php?id=558541&cat_id=15,,,,
|
huggingface-space.yml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
title: RAG 검색 챗봇 with 음성인식
|
| 2 |
+
emoji: 🤖
|
| 3 |
+
colorFrom: indigo
|
| 4 |
+
colorTo: blue
|
| 5 |
+
sdk: gradio
|
| 6 |
+
sdk_version: 3.44.4
|
| 7 |
+
app_file: app.py
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask>=2.0.1
|
| 2 |
+
python-dotenv>=1.0.0
|
| 3 |
+
requests>=2.25.1
|
| 4 |
+
numpy>=1.20.0
|
| 5 |
+
scikit-learn>=1.0.2
|
| 6 |
+
gunicorn>=20.1.0
|
| 7 |
+
werkzeug>=2.0.1
|
| 8 |
+
nltk>=3.6.5
|
| 9 |
+
sentence-transformers>=2.2.2
|
| 10 |
+
gradio>=3.50.0,<4.0.0
|
| 11 |
+
openai>=1.0.0
|
retrieval/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# 검색 모듈 패키지
|
retrieval/base_retriever.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
기본 검색기 인터페이스
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from abc import ABC, abstractmethod
|
| 6 |
+
from typing import List, Dict, Any, Union, Tuple
|
| 7 |
+
|
| 8 |
+
class BaseRetriever(ABC):
|
| 9 |
+
"""검색 인터페이스를 정의하는 추상 기본 클래스"""
|
| 10 |
+
|
| 11 |
+
@abstractmethod
|
| 12 |
+
def search(self, query: str, top_k: int = 5, **kwargs) -> List[Dict[str, Any]]:
|
| 13 |
+
"""
|
| 14 |
+
주어진 쿼리에 대해 검색을 수행하고 결과를 반환합니다.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
query: 검색 쿼리
|
| 18 |
+
top_k: 반환할 상위 결과 수
|
| 19 |
+
**kwargs: 추가 검색 매개변수
|
| 20 |
+
|
| 21 |
+
Returns:
|
| 22 |
+
검색 결과 목록 (각 결과는 딕셔너리 형태)
|
| 23 |
+
"""
|
| 24 |
+
pass
|
| 25 |
+
|
| 26 |
+
@abstractmethod
|
| 27 |
+
def add_documents(self, documents: List[Dict[str, Any]]) -> None:
|
| 28 |
+
"""
|
| 29 |
+
검색기에 문서를 추가합니다.
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
documents: 추가할 문서 목록 (각 문서는 딕셔너리 형태)
|
| 33 |
+
"""
|
| 34 |
+
pass
|
| 35 |
+
|
| 36 |
+
def get_relevant_documents(self, query: str, top_k: int = 5, **kwargs) -> List[Dict[str, Any]]:
|
| 37 |
+
"""
|
| 38 |
+
search 메서드의 별칭
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
query: 검색 쿼리
|
| 42 |
+
top_k: 반환할 상위 결과 수
|
| 43 |
+
**kwargs: 추가 검색 매개변수
|
| 44 |
+
|
| 45 |
+
Returns:
|
| 46 |
+
검색 결과 목록
|
| 47 |
+
"""
|
| 48 |
+
return self.search(query, top_k, **kwargs)
|
retrieval/reranker.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
재순위화 검색 구현 모듈
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
from typing import List, Dict, Any, Optional, Union, Callable
|
| 7 |
+
from .base_retriever import BaseRetriever
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
class ReRanker(BaseRetriever):
|
| 12 |
+
"""
|
| 13 |
+
검색 결과 재순위화 검색기
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
def __init__(
|
| 17 |
+
self,
|
| 18 |
+
base_retriever: BaseRetriever,
|
| 19 |
+
rerank_model: Optional[Union[str, Any]] = None,
|
| 20 |
+
rerank_fn: Optional[Callable] = None,
|
| 21 |
+
rerank_field: str = "text",
|
| 22 |
+
rerank_batch_size: int = 32
|
| 23 |
+
):
|
| 24 |
+
"""
|
| 25 |
+
ReRanker 초기화
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
base_retriever: 기본 검색기 인스턴스
|
| 29 |
+
rerank_model: 재순위화 모델 (Cross-Encoder) 이름 또는 인스턴스
|
| 30 |
+
rerank_fn: 사용자 정의 재순위화 함수 (제공된 경우 rerank_model 대신 사용)
|
| 31 |
+
rerank_field: 재순위화에 사용할 문서 필드
|
| 32 |
+
rerank_batch_size: 재순위화 모델 배치 크기
|
| 33 |
+
"""
|
| 34 |
+
self.base_retriever = base_retriever
|
| 35 |
+
self.rerank_field = rerank_field
|
| 36 |
+
self.rerank_batch_size = rerank_batch_size
|
| 37 |
+
self.rerank_fn = rerank_fn
|
| 38 |
+
|
| 39 |
+
# 재순위화 모델 로드 (사용자 정의 함수가 제공되지 않은 경우)
|
| 40 |
+
if rerank_fn is None and rerank_model is not None:
|
| 41 |
+
try:
|
| 42 |
+
from sentence_transformers import CrossEncoder
|
| 43 |
+
if isinstance(rerank_model, str):
|
| 44 |
+
logger.info(f"재순위화 모델 '{rerank_model}' 로드 중...")
|
| 45 |
+
self.rerank_model = CrossEncoder(rerank_model)
|
| 46 |
+
else:
|
| 47 |
+
self.rerank_model = rerank_model
|
| 48 |
+
except ImportError:
|
| 49 |
+
logger.warning("sentence-transformers 패키지가 설치되지 않았습니다. pip install sentence-transformers 명령으로 설치하세요.")
|
| 50 |
+
raise
|
| 51 |
+
else:
|
| 52 |
+
self.rerank_model = None
|
| 53 |
+
|
| 54 |
+
def add_documents(self, documents: List[Dict[str, Any]]) -> None:
|
| 55 |
+
"""
|
| 56 |
+
기본 검색기에 문서 추가
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
documents: 추가할 문서 목록
|
| 60 |
+
"""
|
| 61 |
+
self.base_retriever.add_documents(documents)
|
| 62 |
+
|
| 63 |
+
def search(self, query: str, top_k: int = 5, first_stage_k: int = 30, **kwargs) -> List[Dict[str, Any]]:
|
| 64 |
+
"""
|
| 65 |
+
2단계 검색 수행: 기본 검색 + 재순위화
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
query: 검색 쿼리
|
| 69 |
+
top_k: 최종적으로 반환할 상위 결과 수
|
| 70 |
+
first_stage_k: 첫 번째 단계에서 검색할 결과 수
|
| 71 |
+
**kwargs: 추가 검색 매개변수
|
| 72 |
+
|
| 73 |
+
Returns:
|
| 74 |
+
재순위화된 검색 결과 목록
|
| 75 |
+
"""
|
| 76 |
+
# 첫 번째 단계: 기본 검색기로 more_k 문서 검색
|
| 77 |
+
logger.info(f"기본 검색기로 {first_stage_k}개 문서 검색 중...")
|
| 78 |
+
initial_results = self.base_retriever.search(query, top_k=first_stage_k, **kwargs)
|
| 79 |
+
|
| 80 |
+
if not initial_results:
|
| 81 |
+
logger.warning("첫 번째 단계 검색 결과가 없습니다.")
|
| 82 |
+
return []
|
| 83 |
+
|
| 84 |
+
if len(initial_results) < first_stage_k:
|
| 85 |
+
logger.info(f"요청한 {first_stage_k}개보다 적은 {len(initial_results)}개 결과를 검색했습니다.")
|
| 86 |
+
|
| 87 |
+
# 사용자 정의 재순위화 함수가 제공된 경우
|
| 88 |
+
if self.rerank_fn is not None:
|
| 89 |
+
logger.info("사용자 정의 함수로 재순위화 중...")
|
| 90 |
+
reranked_results = self.rerank_fn(query, initial_results)
|
| 91 |
+
return reranked_results[:top_k]
|
| 92 |
+
|
| 93 |
+
# 재순위화 모델이 로드된 경우
|
| 94 |
+
elif self.rerank_model is not None:
|
| 95 |
+
logger.info(f"CrossEncoder 모델로 재순위화 중...")
|
| 96 |
+
|
| 97 |
+
# 텍스트 쌍 생성
|
| 98 |
+
text_pairs = []
|
| 99 |
+
for doc in initial_results:
|
| 100 |
+
if self.rerank_field not in doc:
|
| 101 |
+
logger.warning(f"문서에 필드 '{self.rerank_field}'가 없습니다.")
|
| 102 |
+
continue
|
| 103 |
+
text_pairs.append([query, doc[self.rerank_field]])
|
| 104 |
+
|
| 105 |
+
# 모델로 점수 계산
|
| 106 |
+
scores = self.rerank_model.predict(
|
| 107 |
+
text_pairs,
|
| 108 |
+
batch_size=self.rerank_batch_size,
|
| 109 |
+
show_progress_bar=True if len(text_pairs) > 10 else False
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
# 결과 재정렬
|
| 113 |
+
for idx, doc in enumerate(initial_results[:len(scores)]):
|
| 114 |
+
doc["rerank_score"] = float(scores[idx])
|
| 115 |
+
|
| 116 |
+
reranked_results = sorted(
|
| 117 |
+
initial_results[:len(scores)],
|
| 118 |
+
key=lambda x: x.get("rerank_score", 0),
|
| 119 |
+
reverse=True
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
return reranked_results[:top_k]
|
| 123 |
+
|
| 124 |
+
# 재순위화 없이 초기 결과 반환
|
| 125 |
+
else:
|
| 126 |
+
logger.info("재순위화 모델/함수가 없어 초기 검색 결과를 그대로 반환합니다.")
|
| 127 |
+
return initial_results[:top_k]
|
retrieval/vector_retriever.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
벡터 검색 구현 모듈
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import numpy as np
|
| 7 |
+
from typing import List, Dict, Any, Optional, Union, Tuple
|
| 8 |
+
import logging
|
| 9 |
+
from sentence_transformers import SentenceTransformer
|
| 10 |
+
from .base_retriever import BaseRetriever
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
class VectorRetriever(BaseRetriever):
|
| 15 |
+
"""
|
| 16 |
+
임베딩 기반 벡터 검색 구현
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
def __init__(
|
| 20 |
+
self,
|
| 21 |
+
embedding_model: Optional[Union[str, SentenceTransformer]] = "paraphrase-multilingual-MiniLM-L12-v2",
|
| 22 |
+
documents: Optional[List[Dict[str, Any]]] = None,
|
| 23 |
+
embedding_field: str = "text",
|
| 24 |
+
embedding_device: str = "cpu"
|
| 25 |
+
):
|
| 26 |
+
"""
|
| 27 |
+
VectorRetriever 초기화
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
embedding_model: 임베딩 모델 이름 또는 SentenceTransformer 인스턴스
|
| 31 |
+
documents: 초기 문서 목록 (선택 사항)
|
| 32 |
+
embedding_field: 임베딩할 문서 필드 이름
|
| 33 |
+
embedding_device: 임베딩 모델 실행 장치 ('cpu' 또는 'cuda')
|
| 34 |
+
"""
|
| 35 |
+
self.embedding_field = embedding_field
|
| 36 |
+
self.model_name = None
|
| 37 |
+
|
| 38 |
+
# 임베딩 모델 로드
|
| 39 |
+
if isinstance(embedding_model, str):
|
| 40 |
+
logger.info(f"임베딩 모델 '{embedding_model}' 로드 중...")
|
| 41 |
+
self.model_name = embedding_model
|
| 42 |
+
self.embedding_model = SentenceTransformer(embedding_model, device=embedding_device)
|
| 43 |
+
else:
|
| 44 |
+
self.embedding_model = embedding_model
|
| 45 |
+
# 모델이 이미 로드된 인스턴스일 경우 이름 추출
|
| 46 |
+
if hasattr(embedding_model, '_modules') and 'modules' in embedding_model._modules:
|
| 47 |
+
self.model_name = "loaded_sentence_transformer"
|
| 48 |
+
|
| 49 |
+
# 문서 저장소 초기화
|
| 50 |
+
self.documents = []
|
| 51 |
+
self.document_embeddings = None
|
| 52 |
+
|
| 53 |
+
# 초기 문서가 제공된 경우 추가
|
| 54 |
+
if documents:
|
| 55 |
+
self.add_documents(documents)
|
| 56 |
+
|
| 57 |
+
def add_documents(self, documents: List[Dict[str, Any]]) -> None:
|
| 58 |
+
"""
|
| 59 |
+
검색기에 문서를 추가하고 임베딩 생성
|
| 60 |
+
|
| 61 |
+
Args:
|
| 62 |
+
documents: 추가할 문서 목록
|
| 63 |
+
"""
|
| 64 |
+
if not documents:
|
| 65 |
+
logger.warning("추가할 문서가 없습니다.")
|
| 66 |
+
return
|
| 67 |
+
|
| 68 |
+
# 문서 추가
|
| 69 |
+
document_texts = []
|
| 70 |
+
for doc in documents:
|
| 71 |
+
if self.embedding_field not in doc:
|
| 72 |
+
logger.warning(f"문서에 필드 '{self.embedding_field}'가 없습니다. 건너뜁니다.")
|
| 73 |
+
continue
|
| 74 |
+
|
| 75 |
+
self.documents.append(doc)
|
| 76 |
+
document_texts.append(doc[self.embedding_field])
|
| 77 |
+
|
| 78 |
+
if not document_texts:
|
| 79 |
+
logger.warning(f"임베딩할 텍스트가 없습니다. 모든 문서에 '{self.embedding_field}' 필드가 있는지 확인하세요.")
|
| 80 |
+
return
|
| 81 |
+
|
| 82 |
+
# 문서 임베딩 생성
|
| 83 |
+
logger.info(f"{len(document_texts)}개 문서의 임베딩 생성 중...")
|
| 84 |
+
new_embeddings = self.embedding_model.encode(document_texts, show_progress_bar=True)
|
| 85 |
+
|
| 86 |
+
# 기존 임베딩과 병합
|
| 87 |
+
if self.document_embeddings is None:
|
| 88 |
+
self.document_embeddings = new_embeddings
|
| 89 |
+
else:
|
| 90 |
+
self.document_embeddings = np.vstack([self.document_embeddings, new_embeddings])
|
| 91 |
+
|
| 92 |
+
logger.info(f"총 {len(self.documents)}개 문서, {self.document_embeddings.shape[0]}개 임베딩 저장됨")
|
| 93 |
+
|
| 94 |
+
def search(self, query: str, top_k: int = 5, **kwargs) -> List[Dict[str, Any]]:
|
| 95 |
+
"""
|
| 96 |
+
쿼리에 대한 벡터 검색 수행
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
query: 검색 쿼리
|
| 100 |
+
top_k: 반환할 상위 결과 수
|
| 101 |
+
**kwargs: 추가 검색 매개변수
|
| 102 |
+
|
| 103 |
+
Returns:
|
| 104 |
+
관련성 점수와 함께 검색된 문서 목록
|
| 105 |
+
"""
|
| 106 |
+
if not self.documents or self.document_embeddings is None:
|
| 107 |
+
logger.warning("검색할 문서가 없습니다.")
|
| 108 |
+
return []
|
| 109 |
+
|
| 110 |
+
# 쿼리 임베딩 생성
|
| 111 |
+
query_embedding = self.embedding_model.encode(query)
|
| 112 |
+
|
| 113 |
+
# 코사인 유사도 계산
|
| 114 |
+
scores = np.dot(self.document_embeddings, query_embedding) / (
|
| 115 |
+
np.linalg.norm(self.document_embeddings, axis=1) * np.linalg.norm(query_embedding)
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
# 상위 결과 선택
|
| 119 |
+
top_indices = np.argsort(scores)[-top_k:][::-1]
|
| 120 |
+
|
| 121 |
+
# 결과 형식화
|
| 122 |
+
results = []
|
| 123 |
+
for idx in top_indices:
|
| 124 |
+
doc = self.documents[idx].copy()
|
| 125 |
+
doc["score"] = float(scores[idx])
|
| 126 |
+
results.append(doc)
|
| 127 |
+
|
| 128 |
+
return results
|
| 129 |
+
|
| 130 |
+
def save(self, directory: str) -> None:
|
| 131 |
+
"""
|
| 132 |
+
검색기 상태를 디스크에 저장
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
directory: 저장할 디렉토리 경로
|
| 136 |
+
"""
|
| 137 |
+
import pickle
|
| 138 |
+
import json
|
| 139 |
+
|
| 140 |
+
os.makedirs(directory, exist_ok=True)
|
| 141 |
+
|
| 142 |
+
# 문서 저장
|
| 143 |
+
with open(os.path.join(directory, "documents.json"), "w", encoding="utf-8") as f:
|
| 144 |
+
json.dump(self.documents, f, ensure_ascii=False, indent=2)
|
| 145 |
+
|
| 146 |
+
# 임베딩 저장
|
| 147 |
+
if self.document_embeddings is not None:
|
| 148 |
+
np.save(os.path.join(directory, "embeddings.npy"), self.document_embeddings)
|
| 149 |
+
|
| 150 |
+
# 모델 정보 저장
|
| 151 |
+
model_info = {
|
| 152 |
+
"model_name": self.model_name or "paraphrase-multilingual-MiniLM-L12-v2", # 기본값 설정
|
| 153 |
+
"embedding_dim": self.embedding_model.get_sentence_embedding_dimension() if hasattr(self.embedding_model, 'get_sentence_embedding_dimension') else 384
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
with open(os.path.join(directory, "model_info.json"), "w") as f:
|
| 157 |
+
json.dump(model_info, f)
|
| 158 |
+
|
| 159 |
+
logger.info(f"검색기 상태를 '{directory}'에 저장했습니다.")
|
| 160 |
+
|
| 161 |
+
@classmethod
|
| 162 |
+
def load(cls, directory: str, embedding_model: Optional[Union[str, SentenceTransformer]] = None) -> "VectorRetriever":
|
| 163 |
+
"""
|
| 164 |
+
디스크에서 검색기 상태를 로드
|
| 165 |
+
|
| 166 |
+
Args:
|
| 167 |
+
directory: 로드할 디렉토리 경로
|
| 168 |
+
embedding_model: 사용할 임베딩 모델 (제공되지 않으면 저장된 정보 사용)
|
| 169 |
+
|
| 170 |
+
Returns:
|
| 171 |
+
로드된 VectorRetriever 인스턴스
|
| 172 |
+
"""
|
| 173 |
+
import json
|
| 174 |
+
|
| 175 |
+
# 모델 정보 로드
|
| 176 |
+
with open(os.path.join(directory, "model_info.json"), "r") as f:
|
| 177 |
+
model_info = json.load(f)
|
| 178 |
+
|
| 179 |
+
# 임베딩 모델 인스턴스화
|
| 180 |
+
if embedding_model is None:
|
| 181 |
+
# 모델 이름을 사용하여 모델 인스턴스화
|
| 182 |
+
if "model_name" in model_info and isinstance(model_info["model_name"], str):
|
| 183 |
+
embedding_model = model_info["model_name"]
|
| 184 |
+
else:
|
| 185 |
+
# 안전장치: 모델 이름이 없거나 정수인 경우(이전 버전 호환성) 기본 모델 사용
|
| 186 |
+
logger.warning("유효한 모델 이름을 찾을 수 없습니다. 기본 모델을 사용합니다.")
|
| 187 |
+
embedding_model = "paraphrase-multilingual-MiniLM-L12-v2"
|
| 188 |
+
|
| 189 |
+
# 검색기 인스턴스 생성 (문서 없이)
|
| 190 |
+
retriever = cls(embedding_model=embedding_model)
|
| 191 |
+
|
| 192 |
+
# 문서 로드
|
| 193 |
+
with open(os.path.join(directory, "documents.json"), "r", encoding="utf-8") as f:
|
| 194 |
+
retriever.documents = json.load(f)
|
| 195 |
+
|
| 196 |
+
# 임베딩 로드
|
| 197 |
+
embeddings_path = os.path.join(directory, "embeddings.npy")
|
| 198 |
+
if os.path.exists(embeddings_path):
|
| 199 |
+
retriever.document_embeddings = np.load(embeddings_path)
|
| 200 |
+
|
| 201 |
+
logger.info(f"검색기 상태를 '{directory}'에서 로드했습니다.")
|
| 202 |
+
return retriever
|
run.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
개발 환경에서 빠르게 실행할 수 있는 스크립트
|
| 3 |
+
"""
|
| 4 |
+
import os
|
| 5 |
+
import logging
|
| 6 |
+
import webbrowser
|
| 7 |
+
from app.app import app
|
| 8 |
+
|
| 9 |
+
# 로거 설정
|
| 10 |
+
logging.basicConfig(
|
| 11 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 12 |
+
level=logging.INFO
|
| 13 |
+
)
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
if __name__ == "__main__":
|
| 17 |
+
port = int(os.environ.get("PORT", 5000))
|
| 18 |
+
logger.info(f"서버를 http://localhost:{port}에서 시작합니다...")
|
| 19 |
+
|
| 20 |
+
# 브라우저 자동 실행 (선택 사항)
|
| 21 |
+
webbrowser.open_new(f"http://localhost:{port}")
|
| 22 |
+
|
| 23 |
+
# 플라스크 서버 실행
|
| 24 |
+
app.run(debug=True, host="0.0.0.0", port=port)
|
runtime.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
python-3.10.12
|
utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# 유틸리티 모듈 패키지
|
utils/document_processor.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
문서 처리 유틸리티 모듈
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import re
|
| 7 |
+
import logging
|
| 8 |
+
from typing import List, Dict, Any, Optional, Tuple, Union
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger("DocProcessor")
|
| 12 |
+
if not logger.hasHandlers():
|
| 13 |
+
handler = logging.StreamHandler()
|
| 14 |
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 15 |
+
handler.setFormatter(formatter)
|
| 16 |
+
logger.addHandler(handler)
|
| 17 |
+
logger.setLevel(logging.INFO)
|
| 18 |
+
|
| 19 |
+
class DocumentProcessor:
|
| 20 |
+
"""문서 처리 유틸리티 클래스"""
|
| 21 |
+
|
| 22 |
+
@staticmethod
|
| 23 |
+
def split_text(
|
| 24 |
+
text: str,
|
| 25 |
+
chunk_size: int = 512,
|
| 26 |
+
chunk_overlap: int = 50,
|
| 27 |
+
separator: str = "\n"
|
| 28 |
+
) -> List[str]:
|
| 29 |
+
"""
|
| 30 |
+
텍스트를 더 작은 청크로 분할
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
text: 분할할 텍스트
|
| 34 |
+
chunk_size: 각 청크의 최대 문자 수
|
| 35 |
+
chunk_overlap: 청크 간 중첩되는 문자 수
|
| 36 |
+
separator: 분할 시 사용할 구분자
|
| 37 |
+
|
| 38 |
+
Returns:
|
| 39 |
+
분할된 텍스트 청크 목록
|
| 40 |
+
"""
|
| 41 |
+
if not text or chunk_size <= 0:
|
| 42 |
+
return []
|
| 43 |
+
|
| 44 |
+
# 구분자로 분할
|
| 45 |
+
parts = text.split(separator)
|
| 46 |
+
chunks = []
|
| 47 |
+
current_chunk = []
|
| 48 |
+
current_size = 0
|
| 49 |
+
|
| 50 |
+
for part in parts:
|
| 51 |
+
part_size = len(part)
|
| 52 |
+
|
| 53 |
+
if current_size + part_size + len(current_chunk) > chunk_size and current_chunk:
|
| 54 |
+
# 현재 청크가 최대 크기를 초과하면 저장
|
| 55 |
+
chunks.append(separator.join(current_chunk))
|
| 56 |
+
|
| 57 |
+
# 중첩을 위해 일부 청크 유지
|
| 58 |
+
overlap_tokens = []
|
| 59 |
+
overlap_size = 0
|
| 60 |
+
for token in reversed(current_chunk):
|
| 61 |
+
if overlap_size + len(token) <= chunk_overlap:
|
| 62 |
+
overlap_tokens.insert(0, token)
|
| 63 |
+
overlap_size += len(token) + 1 # separator 길이 포함
|
| 64 |
+
else:
|
| 65 |
+
break
|
| 66 |
+
|
| 67 |
+
current_chunk = overlap_tokens
|
| 68 |
+
current_size = overlap_size - len(current_chunk) # separator 길이 제외
|
| 69 |
+
|
| 70 |
+
current_chunk.append(part)
|
| 71 |
+
current_size += part_size
|
| 72 |
+
|
| 73 |
+
# 마지막 청크 추가
|
| 74 |
+
if current_chunk:
|
| 75 |
+
chunks.append(separator.join(current_chunk))
|
| 76 |
+
|
| 77 |
+
return chunks
|
| 78 |
+
|
| 79 |
+
@staticmethod
|
| 80 |
+
def clean_text(text: str, remove_urls: bool = True, remove_extra_whitespace: bool = True) -> str:
|
| 81 |
+
"""
|
| 82 |
+
텍스트 정제
|
| 83 |
+
|
| 84 |
+
Args:
|
| 85 |
+
text: 정제할 텍스트
|
| 86 |
+
remove_urls: URL 제거 여부
|
| 87 |
+
remove_extra_whitespace: 여분의 공백 제거 여부
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
정제된 텍스트
|
| 91 |
+
"""
|
| 92 |
+
if not text:
|
| 93 |
+
return ""
|
| 94 |
+
|
| 95 |
+
# URL 제거
|
| 96 |
+
if remove_urls:
|
| 97 |
+
text = re.sub(r'https?://\S+|www\.\S+', '', text)
|
| 98 |
+
|
| 99 |
+
# 특수 문자 및 HTML 태그 정제
|
| 100 |
+
text = re.sub(r'<.*?>', '', text) # HTML 태그 제거
|
| 101 |
+
|
| 102 |
+
# 여분의 공백 제거
|
| 103 |
+
if remove_extra_whitespace:
|
| 104 |
+
text = re.sub(r'\s+', ' ', text).strip()
|
| 105 |
+
|
| 106 |
+
return text
|
| 107 |
+
|
| 108 |
+
@staticmethod
|
| 109 |
+
def text_to_documents(
|
| 110 |
+
text: str,
|
| 111 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 112 |
+
chunk_size: int = 512,
|
| 113 |
+
chunk_overlap: int = 50
|
| 114 |
+
) -> List[Dict[str, Any]]:
|
| 115 |
+
"""
|
| 116 |
+
텍스트를 문서 객체 목록으로 변환
|
| 117 |
+
|
| 118 |
+
Args:
|
| 119 |
+
text: 변환할 텍스트
|
| 120 |
+
metadata: 문서에 추가할 메타데이터
|
| 121 |
+
chunk_size: 각 청크의 최대 문자 수
|
| 122 |
+
chunk_overlap: 청크 간 중첩되는 문자 수
|
| 123 |
+
|
| 124 |
+
Returns:
|
| 125 |
+
문서 객체 목록
|
| 126 |
+
"""
|
| 127 |
+
if not text:
|
| 128 |
+
return []
|
| 129 |
+
|
| 130 |
+
# 텍스트 정제
|
| 131 |
+
clean = DocumentProcessor.clean_text(text)
|
| 132 |
+
|
| 133 |
+
# 텍스트 분할
|
| 134 |
+
chunks = DocumentProcessor.split_text(
|
| 135 |
+
clean,
|
| 136 |
+
chunk_size=chunk_size,
|
| 137 |
+
chunk_overlap=chunk_overlap
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
# 문서 객체 생성
|
| 141 |
+
documents = []
|
| 142 |
+
for i, chunk in enumerate(chunks):
|
| 143 |
+
doc = {
|
| 144 |
+
"text": chunk,
|
| 145 |
+
"index": i,
|
| 146 |
+
"chunk_count": len(chunks)
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
# 메타데이터 추가
|
| 150 |
+
if metadata:
|
| 151 |
+
doc.update(metadata)
|
| 152 |
+
|
| 153 |
+
documents.append(doc)
|
| 154 |
+
|
| 155 |
+
return documents
|
| 156 |
+
|
| 157 |
+
@staticmethod
|
| 158 |
+
def load_documents_from_directory(
|
| 159 |
+
directory: str,
|
| 160 |
+
extensions: List[str] = [".txt", ".md", ".csv"],
|
| 161 |
+
recursive: bool = True,
|
| 162 |
+
chunk_size: int = 512,
|
| 163 |
+
chunk_overlap: int = 50
|
| 164 |
+
) -> List[Dict[str, Any]]:
|
| 165 |
+
"""
|
| 166 |
+
디렉토리에서 문서 로드 및 처리
|
| 167 |
+
|
| 168 |
+
Args:
|
| 169 |
+
directory: 로드할 디렉토리 경로
|
| 170 |
+
extensions: 처리할 파일 확장자 목록
|
| 171 |
+
recursive: 하위 디렉토리 검색 여부
|
| 172 |
+
chunk_size: 각 청크의 최대 문자 수
|
| 173 |
+
chunk_overlap: 청크 간 중첩되는 문자 수
|
| 174 |
+
|
| 175 |
+
Returns:
|
| 176 |
+
문서 객체 목록
|
| 177 |
+
"""
|
| 178 |
+
if not os.path.isdir(directory):
|
| 179 |
+
logger.error(f"디렉토리를 찾을 수 없습니다: {directory}")
|
| 180 |
+
return []
|
| 181 |
+
|
| 182 |
+
documents = []
|
| 183 |
+
|
| 184 |
+
for root, dirs, files in os.walk(directory):
|
| 185 |
+
if not recursive and root != directory:
|
| 186 |
+
continue
|
| 187 |
+
|
| 188 |
+
for file in files:
|
| 189 |
+
_, ext = os.path.splitext(file)
|
| 190 |
+
if ext.lower() not in extensions:
|
| 191 |
+
continue
|
| 192 |
+
|
| 193 |
+
file_path = os.path.join(root, file)
|
| 194 |
+
rel_path = os.path.relpath(file_path, directory)
|
| 195 |
+
|
| 196 |
+
try:
|
| 197 |
+
logger.info(f"파일 로드 중: {rel_path}")
|
| 198 |
+
# 먼저 UTF-8로 시도
|
| 199 |
+
try:
|
| 200 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 201 |
+
content = f.read()
|
| 202 |
+
except UnicodeDecodeError:
|
| 203 |
+
# UTF-8로 실패하면 CP949(한국어 Windows 기본 인코딩)로 시도
|
| 204 |
+
logger.info(f"UTF-8 디코딩 실패, CP949로 시도: {rel_path}")
|
| 205 |
+
with open(file_path, 'r', encoding='cp949') as f:
|
| 206 |
+
content = f.read()
|
| 207 |
+
|
| 208 |
+
# 메타데이터 생성
|
| 209 |
+
metadata = {
|
| 210 |
+
"source": rel_path,
|
| 211 |
+
"filename": file,
|
| 212 |
+
"filetype": ext.lower()[1:],
|
| 213 |
+
"filepath": file_path
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
# 문서 처리
|
| 217 |
+
file_docs = DocumentProcessor.text_to_documents(
|
| 218 |
+
content,
|
| 219 |
+
metadata=metadata,
|
| 220 |
+
chunk_size=chunk_size,
|
| 221 |
+
chunk_overlap=chunk_overlap
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
documents.extend(file_docs)
|
| 225 |
+
logger.info(f"{len(file_docs)}개 청크 추출: {rel_path}")
|
| 226 |
+
|
| 227 |
+
except Exception as e:
|
| 228 |
+
logger.error(f"파일 '{rel_path}' 처리 중 오류 발생: {e}")
|
| 229 |
+
continue
|
| 230 |
+
|
| 231 |
+
logger.info(f"총 {len(documents)}개 문서 청크를 로드했습니다.")
|
| 232 |
+
return documents
|
| 233 |
+
|
| 234 |
+
@staticmethod
|
| 235 |
+
def prepare_rag_context(results: List[Dict[str, Any]], field: str = "text") -> List[str]:
|
| 236 |
+
"""
|
| 237 |
+
검색 결과에서 RAG에 사용할 컨텍스트 추출
|
| 238 |
+
|
| 239 |
+
Args:
|
| 240 |
+
results: 검색 결과 목록
|
| 241 |
+
field: 텍스트 내용이 있는 필드 이름
|
| 242 |
+
|
| 243 |
+
Returns:
|
| 244 |
+
컨텍스트 텍스트 목록
|
| 245 |
+
"""
|
| 246 |
+
context = []
|
| 247 |
+
|
| 248 |
+
for result in results:
|
| 249 |
+
if field in result:
|
| 250 |
+
context.append(result[field])
|
| 251 |
+
|
| 252 |
+
return context
|
utils/llm_client.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
DeepSeek LLM API 클라이언트 모듈
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
import requests
|
| 9 |
+
from typing import List, Dict, Any, Optional, Union
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
|
| 12 |
+
# 환경 변수 로드
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
# 로거 설정
|
| 16 |
+
logger = logging.getLogger("DeepSeekLLM")
|
| 17 |
+
if not logger.hasHandlers():
|
| 18 |
+
handler = logging.StreamHandler()
|
| 19 |
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 20 |
+
handler.setFormatter(formatter)
|
| 21 |
+
logger.addHandler(handler)
|
| 22 |
+
logger.setLevel(logging.INFO)
|
| 23 |
+
|
| 24 |
+
class DeepSeekLLM:
|
| 25 |
+
"""DeepSeek LLM API 래퍼 클래스"""
|
| 26 |
+
|
| 27 |
+
def __init__(self):
|
| 28 |
+
"""DeepSeek LLM 클래스 초기화"""
|
| 29 |
+
self.api_key = os.getenv("DEEPSEEK_API_KEY")
|
| 30 |
+
self.endpoint = os.getenv("DEEPSEEK_ENDPOINT", "https://api.deepseek.com/v1/chat/completions")
|
| 31 |
+
self.model = os.getenv("DEEPSEEK_MODEL", "deepseek-chat")
|
| 32 |
+
|
| 33 |
+
if not self.api_key:
|
| 34 |
+
logger.warning("DeepSeek API 키가 .env 파일에 설정되지 않았습니다.")
|
| 35 |
+
logger.warning("DEEPSEEK_API_KEY를 확인하세요.")
|
| 36 |
+
else:
|
| 37 |
+
logger.info("DeepSeek LLM API 키 로드 완료.")
|
| 38 |
+
|
| 39 |
+
def chat_completion(
|
| 40 |
+
self,
|
| 41 |
+
messages: List[Dict[str, str]],
|
| 42 |
+
temperature: float = 0.7,
|
| 43 |
+
max_tokens: int = 1000,
|
| 44 |
+
stream: bool = False,
|
| 45 |
+
**kwargs
|
| 46 |
+
) -> Dict[str, Any]:
|
| 47 |
+
"""
|
| 48 |
+
DeepSeek 채팅 완성 API 호출
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
messages: 채팅 메시지 목록
|
| 52 |
+
temperature: 생성 온도 (낮을수록 결정적)
|
| 53 |
+
max_tokens: 생성할 최대 토큰 수
|
| 54 |
+
stream: 스트리밍 응답 활성화 여부
|
| 55 |
+
**kwargs: 추가 API 매개변수
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
API 응답 (딕셔너리)
|
| 59 |
+
"""
|
| 60 |
+
if not self.api_key:
|
| 61 |
+
logger.error("API 키가 설정되지 않아 DeepSeek API를 호출할 수 없습니다.")
|
| 62 |
+
raise ValueError("DeepSeek API 키가 설정되지 않았습니다.")
|
| 63 |
+
|
| 64 |
+
headers = {
|
| 65 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 66 |
+
"Content-Type": "application/json"
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
payload = {
|
| 70 |
+
"model": self.model,
|
| 71 |
+
"messages": messages,
|
| 72 |
+
"temperature": temperature,
|
| 73 |
+
"max_tokens": max_tokens,
|
| 74 |
+
"stream": stream,
|
| 75 |
+
**kwargs
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
try:
|
| 79 |
+
logger.info(f"DeepSeek API 요청 전송 중: {self.endpoint}")
|
| 80 |
+
response = requests.post(
|
| 81 |
+
self.endpoint,
|
| 82 |
+
headers=headers,
|
| 83 |
+
json=payload,
|
| 84 |
+
timeout=60 # 타임아웃 설정
|
| 85 |
+
)
|
| 86 |
+
response.raise_for_status()
|
| 87 |
+
|
| 88 |
+
if stream:
|
| 89 |
+
return response # 스트리밍 응답은 원시 응답 객체 반환
|
| 90 |
+
else:
|
| 91 |
+
return response.json()
|
| 92 |
+
|
| 93 |
+
except requests.exceptions.Timeout:
|
| 94 |
+
logger.error("DeepSeek API 요청 시간 초과")
|
| 95 |
+
raise TimeoutError("DeepSeek API 요청 시간 초과")
|
| 96 |
+
except requests.exceptions.RequestException as e:
|
| 97 |
+
logger.error(f"DeepSeek API 요청 실패: {e}")
|
| 98 |
+
if hasattr(e, 'response') and e.response is not None:
|
| 99 |
+
logger.error(f"응답 코드: {e.response.status_code}, 내용: {e.response.text}")
|
| 100 |
+
raise ConnectionError(f"DeepSeek API 요청 실패: {e}")
|
| 101 |
+
|
| 102 |
+
def generate(
|
| 103 |
+
self,
|
| 104 |
+
prompt: str,
|
| 105 |
+
system_prompt: Optional[str] = None,
|
| 106 |
+
temperature: float = 0.7,
|
| 107 |
+
max_tokens: int = 1000,
|
| 108 |
+
**kwargs
|
| 109 |
+
) -> str:
|
| 110 |
+
"""
|
| 111 |
+
간단한 텍스트 생성 인터페이스
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
prompt: 사용자 프롬프트
|
| 115 |
+
system_prompt: 시스템 프롬프트 (선택 사항)
|
| 116 |
+
temperature: 생성 온도
|
| 117 |
+
max_tokens: 생성할 최대 토큰 수
|
| 118 |
+
**kwargs: 추가 API 매개변수
|
| 119 |
+
|
| 120 |
+
Returns:
|
| 121 |
+
생성된 텍스트
|
| 122 |
+
"""
|
| 123 |
+
messages = []
|
| 124 |
+
|
| 125 |
+
if system_prompt:
|
| 126 |
+
messages.append({"role": "system", "content": system_prompt})
|
| 127 |
+
|
| 128 |
+
messages.append({"role": "user", "content": prompt})
|
| 129 |
+
|
| 130 |
+
try:
|
| 131 |
+
response = self.chat_completion(
|
| 132 |
+
messages=messages,
|
| 133 |
+
temperature=temperature,
|
| 134 |
+
max_tokens=max_tokens,
|
| 135 |
+
**kwargs
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
if not response or "choices" not in response or not response["choices"]:
|
| 139 |
+
logger.error("DeepSeek API 응답에서 생성된 텍스트를 찾을 수 없습니다.")
|
| 140 |
+
return ""
|
| 141 |
+
|
| 142 |
+
return response["choices"][0]["message"]["content"].strip()
|
| 143 |
+
|
| 144 |
+
except Exception as e:
|
| 145 |
+
logger.error(f"텍스트 생성 중 오류 발생: {e}")
|
| 146 |
+
return f"오류: {str(e)}"
|
| 147 |
+
|
| 148 |
+
def rag_generate(
|
| 149 |
+
self,
|
| 150 |
+
query: str,
|
| 151 |
+
context: List[str],
|
| 152 |
+
system_prompt: Optional[str] = None,
|
| 153 |
+
temperature: float = 0.3,
|
| 154 |
+
max_tokens: int = 1000,
|
| 155 |
+
**kwargs
|
| 156 |
+
) -> str:
|
| 157 |
+
"""
|
| 158 |
+
RAG 검색 결과를 활용한 텍스트 생성
|
| 159 |
+
|
| 160 |
+
Args:
|
| 161 |
+
query: 사용자 질의
|
| 162 |
+
context: 검색된 문맥 목록
|
| 163 |
+
system_prompt: 시스템 프롬프트 (선택 사항)
|
| 164 |
+
temperature: 생성 온도
|
| 165 |
+
max_tokens: 생성할 최대 토큰 수
|
| 166 |
+
**kwargs: 추가 API 매개변수
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
생성된 텍스트
|
| 170 |
+
"""
|
| 171 |
+
if not system_prompt:
|
| 172 |
+
system_prompt = """당신은 검색 결과를 기반으로 질문에 답변하는 도우미입니다.
|
| 173 |
+
- 검색 결과는 <context> 태그 안에 제공됩니다.
|
| 174 |
+
- 검색 결과에 답변이 있으면 해당 정보를 사용하여 명확하게 답변하세요.
|
| 175 |
+
- 검색 결과에 답변이 없으면 "검색 결과에 관련 정보가 없습니다"라고 말하세요.
|
| 176 |
+
- 검색 내용을 그대로 복사하지 말고, 자연스러운 한국어로 답변을 작성하세요.
|
| 177 |
+
- 답변은 간결하고 정확하게 제공하세요."""
|
| 178 |
+
|
| 179 |
+
context_text = "\n\n".join([f"문서 {i+1}: {doc}" for i, doc in enumerate(context)])
|
| 180 |
+
|
| 181 |
+
prompt = f"""질문: {query}
|
| 182 |
+
|
| 183 |
+
<context>
|
| 184 |
+
{context_text}
|
| 185 |
+
</context>
|
| 186 |
+
|
| 187 |
+
위 검색 결과를 참고하여 질문에 답변해 주세요."""
|
| 188 |
+
|
| 189 |
+
try:
|
| 190 |
+
return self.generate(
|
| 191 |
+
prompt=prompt,
|
| 192 |
+
system_prompt=system_prompt,
|
| 193 |
+
temperature=temperature,
|
| 194 |
+
max_tokens=max_tokens,
|
| 195 |
+
**kwargs
|
| 196 |
+
)
|
| 197 |
+
except Exception as e:
|
| 198 |
+
logger.error(f"RAG 텍스트 생성 중 오류 발생: {e}")
|
| 199 |
+
return f"오류: {str(e)}"
|
utils/openai_client.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
OpenAI API 클라이언트 모듈
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
from typing import List, Dict, Any, Optional, Union
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
from openai import OpenAI
|
| 11 |
+
|
| 12 |
+
# 환경 변수 로드
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
# 로거 설정
|
| 16 |
+
logger = logging.getLogger("OpenAILLM")
|
| 17 |
+
if not logger.hasHandlers():
|
| 18 |
+
handler = logging.StreamHandler()
|
| 19 |
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 20 |
+
handler.setFormatter(formatter)
|
| 21 |
+
logger.addHandler(handler)
|
| 22 |
+
logger.setLevel(logging.INFO)
|
| 23 |
+
|
| 24 |
+
class OpenAILLM:
|
| 25 |
+
"""OpenAI API 래퍼 클래스"""
|
| 26 |
+
|
| 27 |
+
def __init__(self):
|
| 28 |
+
"""OpenAI LLM 클래스 초기화"""
|
| 29 |
+
self.api_key = os.getenv("OPENAI_API_KEY")
|
| 30 |
+
self.model = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo")
|
| 31 |
+
|
| 32 |
+
if not self.api_key:
|
| 33 |
+
logger.warning("OpenAI API 키가 .env 파일에 설정되지 않았습니다.")
|
| 34 |
+
logger.warning("OPENAI_API_KEY를 확인하세요.")
|
| 35 |
+
else:
|
| 36 |
+
# OpenAI 클라이언트 초기화
|
| 37 |
+
self.client = OpenAI(api_key=self.api_key)
|
| 38 |
+
logger.info("OpenAI API 키 로드 완료.")
|
| 39 |
+
|
| 40 |
+
def chat_completion(
|
| 41 |
+
self,
|
| 42 |
+
messages: List[Dict[str, str]],
|
| 43 |
+
temperature: float = 0.7,
|
| 44 |
+
max_tokens: int = 1000,
|
| 45 |
+
**kwargs
|
| 46 |
+
) -> Dict[str, Any]:
|
| 47 |
+
"""
|
| 48 |
+
OpenAI 채팅 완성 API 호출
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
messages: 채팅 메시지 목록
|
| 52 |
+
temperature: 생성 온도 (낮을수록 결정적)
|
| 53 |
+
max_tokens: 생성할 최대 토큰 수
|
| 54 |
+
**kwargs: 추가 API 매개변수
|
| 55 |
+
|
| 56 |
+
Returns:
|
| 57 |
+
API 응답 (딕셔너리)
|
| 58 |
+
"""
|
| 59 |
+
if not self.api_key:
|
| 60 |
+
logger.error("API 키가 설정되지 않아 OpenAI API를 호출할 수 없습니다.")
|
| 61 |
+
raise ValueError("OpenAI API 키가 설정되지 않았습니다.")
|
| 62 |
+
|
| 63 |
+
try:
|
| 64 |
+
logger.info(f"OpenAI API 요청 전송 중 (모델: {self.model})")
|
| 65 |
+
|
| 66 |
+
# 새로운 OpenAI SDK를 사용하여 API 호출
|
| 67 |
+
response = self.client.chat.completions.create(
|
| 68 |
+
model=self.model,
|
| 69 |
+
messages=messages,
|
| 70 |
+
temperature=temperature,
|
| 71 |
+
max_tokens=max_tokens,
|
| 72 |
+
**kwargs
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
return response
|
| 76 |
+
|
| 77 |
+
except Exception as e:
|
| 78 |
+
logger.error(f"OpenAI API 요청 실패: {e}")
|
| 79 |
+
raise Exception(f"OpenAI API 요청 실패: {e}")
|
| 80 |
+
|
| 81 |
+
def generate(
|
| 82 |
+
self,
|
| 83 |
+
prompt: str,
|
| 84 |
+
system_prompt: Optional[str] = None,
|
| 85 |
+
temperature: float = 0.7,
|
| 86 |
+
max_tokens: int = 1000,
|
| 87 |
+
**kwargs
|
| 88 |
+
) -> str:
|
| 89 |
+
"""
|
| 90 |
+
간단한 텍스트 생성 인터페이스
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
prompt: 사용자 프롬프트
|
| 94 |
+
system_prompt: 시스템 프롬프트 (선택 사항)
|
| 95 |
+
temperature: 생성 온도
|
| 96 |
+
max_tokens: 생성할 최대 토큰 수
|
| 97 |
+
**kwargs: 추가 API 매개변수
|
| 98 |
+
|
| 99 |
+
Returns:
|
| 100 |
+
생성된 텍스트
|
| 101 |
+
"""
|
| 102 |
+
messages = []
|
| 103 |
+
|
| 104 |
+
if system_prompt:
|
| 105 |
+
messages.append({"role": "system", "content": system_prompt})
|
| 106 |
+
|
| 107 |
+
messages.append({"role": "user", "content": prompt})
|
| 108 |
+
|
| 109 |
+
try:
|
| 110 |
+
response = self.chat_completion(
|
| 111 |
+
messages=messages,
|
| 112 |
+
temperature=temperature,
|
| 113 |
+
max_tokens=max_tokens,
|
| 114 |
+
**kwargs
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
# 새로운 OpenAI SDK 응답 구조에 맞게 처리
|
| 118 |
+
if not response or not hasattr(response, 'choices') or not response.choices:
|
| 119 |
+
logger.error("OpenAI API 응답에서 생성된 텍스트를 찾을 수 없습니다.")
|
| 120 |
+
return ""
|
| 121 |
+
|
| 122 |
+
return response.choices[0].message.content.strip()
|
| 123 |
+
|
| 124 |
+
except Exception as e:
|
| 125 |
+
logger.error(f"텍스트 생성 중 오류 발생: {e}")
|
| 126 |
+
return f"오류: {str(e)}"
|
| 127 |
+
|
| 128 |
+
def rag_generate(
|
| 129 |
+
self,
|
| 130 |
+
query: str,
|
| 131 |
+
context: List[str],
|
| 132 |
+
system_prompt: Optional[str] = None,
|
| 133 |
+
temperature: float = 0.3,
|
| 134 |
+
max_tokens: int = 1000,
|
| 135 |
+
**kwargs
|
| 136 |
+
) -> str:
|
| 137 |
+
"""
|
| 138 |
+
RAG 검색 결과를 활용한 텍스트 생성
|
| 139 |
+
|
| 140 |
+
Args:
|
| 141 |
+
query: 사용자 질의
|
| 142 |
+
context: 검색된 문맥 목록
|
| 143 |
+
system_prompt: 시스템 프롬프트 (선택 사항)
|
| 144 |
+
temperature: 생성 온도
|
| 145 |
+
max_tokens: 생성할 최대 토큰 수
|
| 146 |
+
**kwargs: 추가 API 매개변수
|
| 147 |
+
|
| 148 |
+
Returns:
|
| 149 |
+
생성된 텍스트
|
| 150 |
+
"""
|
| 151 |
+
if not system_prompt:
|
| 152 |
+
system_prompt = """당신은 검색 결과를 기반으로 질문에 답변하는 도우미입니다.
|
| 153 |
+
- 검색 결과는 <context> 태그 안에 제공됩니다.
|
| 154 |
+
- 검색 결과에 답변이 있으면 해당 정보를 사용하여 명확하게 답변하세요.
|
| 155 |
+
- 검색 결과에 답변이 없으면 "검색 결과에 관련 정보가 없습니다"라고 말하세요.
|
| 156 |
+
- 검색 내용을 그대로 복사하지 말고, 자연스러운 한국어로 답변을 작성하세요.
|
| 157 |
+
- 답변은 간결하고 정확하게 제공하세요."""
|
| 158 |
+
|
| 159 |
+
context_text = "\n\n".join([f"문서 {i+1}: {doc}" for i, doc in enumerate(context)])
|
| 160 |
+
|
| 161 |
+
prompt = f"""질문: {query}
|
| 162 |
+
|
| 163 |
+
<context>
|
| 164 |
+
{context_text}
|
| 165 |
+
</context>
|
| 166 |
+
|
| 167 |
+
위 검색 결과를 참고하여 질문에 답변해 주세요."""
|
| 168 |
+
|
| 169 |
+
try:
|
| 170 |
+
return self.generate(
|
| 171 |
+
prompt=prompt,
|
| 172 |
+
system_prompt=system_prompt,
|
| 173 |
+
temperature=temperature,
|
| 174 |
+
max_tokens=max_tokens,
|
| 175 |
+
**kwargs
|
| 176 |
+
)
|
| 177 |
+
except Exception as e:
|
| 178 |
+
logger.error(f"RAG 텍스트 생성 중 오류 발생: {e}")
|
| 179 |
+
return f"오류: {str(e)}"
|
utils/vito_stt.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
VITO API를 사용한 음성 인식(STT) 모듈
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import logging
|
| 8 |
+
import requests
|
| 9 |
+
import json
|
| 10 |
+
import time # time import 추가
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
|
| 13 |
+
# 환경 변수 로드
|
| 14 |
+
load_dotenv()
|
| 15 |
+
|
| 16 |
+
# 로거 설정 (app.py와 공유하거나 독립적으로 설정 가능)
|
| 17 |
+
# 여기서는 독립적인 로거를 사용합니다. 필요시 app.py의 로거를 사용하도록 수정할 수 있습니다.
|
| 18 |
+
logger = logging.getLogger("VitoSTT")
|
| 19 |
+
# 기본 로깅 레벨 설정 (핸들러가 없으면 출력이 안될 수 있으므로 기본 핸들러 추가 고려)
|
| 20 |
+
if not logger.hasHandlers():
|
| 21 |
+
handler = logging.StreamHandler()
|
| 22 |
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 23 |
+
handler.setFormatter(formatter)
|
| 24 |
+
logger.addHandler(handler)
|
| 25 |
+
logger.setLevel(logging.INFO) # 기본 레벨 INFO로 설정
|
| 26 |
+
|
| 27 |
+
class VitoSTT:
|
| 28 |
+
"""VITO STT API 래퍼 클래스"""
|
| 29 |
+
|
| 30 |
+
def __init__(self):
|
| 31 |
+
"""VITO STT 클래스 초기화"""
|
| 32 |
+
self.client_id = os.getenv("VITO_CLIENT_ID")
|
| 33 |
+
self.client_secret = os.getenv("VITO_CLIENT_SECRET")
|
| 34 |
+
|
| 35 |
+
if not self.client_id or not self.client_secret:
|
| 36 |
+
logger.warning("VITO API 인증 정보가 .env 파일에 설정되지 않았습니다.")
|
| 37 |
+
logger.warning("VITO_CLIENT_ID와 VITO_CLIENT_SECRET를 확인하세요.")
|
| 38 |
+
# 에러를 발생시키거나, 기능 사용 시점에 체크하도록 둘 수 있습니다.
|
| 39 |
+
# 여기서는 경고만 하고 넘어갑니다.
|
| 40 |
+
else:
|
| 41 |
+
logger.info("VITO STT API 클라이언트 ID/Secret 로드 완료.")
|
| 42 |
+
|
| 43 |
+
# API 엔드포인트
|
| 44 |
+
self.token_url = "https://openapi.vito.ai/v1/authenticate"
|
| 45 |
+
self.stt_url = "https://openapi.vito.ai/v1/transcribe"
|
| 46 |
+
|
| 47 |
+
# 액세스 토큰
|
| 48 |
+
self.access_token = None
|
| 49 |
+
self._token_expires_at = 0 # 토큰 만료 시간 추적 (선택적 개선)
|
| 50 |
+
|
| 51 |
+
def get_access_token(self):
|
| 52 |
+
"""VITO API 액세스 토큰 획득"""
|
| 53 |
+
# 현재 시간을 가져와 토큰 만료 여부 확인 (선택적 개선)
|
| 54 |
+
# now = time.time()
|
| 55 |
+
# if self.access_token and now < self._token_expires_at:
|
| 56 |
+
# logger.debug("기존 VITO API 토큰 사용")
|
| 57 |
+
# return self.access_token
|
| 58 |
+
|
| 59 |
+
if not self.client_id or not self.client_secret:
|
| 60 |
+
logger.error("API 키가 설정되지 않아 토큰을 획득할 수 없습니다.")
|
| 61 |
+
raise ValueError("VITO API 인증 정보가 설정되지 않았습니다.")
|
| 62 |
+
|
| 63 |
+
logger.info("VITO API 액세스 토큰 요청 중...")
|
| 64 |
+
try:
|
| 65 |
+
response = requests.post(
|
| 66 |
+
self.token_url,
|
| 67 |
+
data={"client_id": self.client_id, "client_secret": self.client_secret},
|
| 68 |
+
timeout=10 # 타임아웃 설정
|
| 69 |
+
)
|
| 70 |
+
response.raise_for_status() # HTTP 오류 발생 시 예외 발생
|
| 71 |
+
|
| 72 |
+
result = response.json()
|
| 73 |
+
self.access_token = result.get("access_token")
|
| 74 |
+
expires_in = result.get("expires_in", 3600) # 만료 시간 (초), 기본값 1시간
|
| 75 |
+
self._token_expires_at = time.time() + expires_in - 60 # 60초 여유
|
| 76 |
+
|
| 77 |
+
if not self.access_token:
|
| 78 |
+
logger.error("VITO API 응답에서 토큰을 찾을 수 없습니다.")
|
| 79 |
+
raise ValueError("VITO API 토큰을 받아오지 못했습니다.")
|
| 80 |
+
|
| 81 |
+
logger.info("VITO API 액세스 토큰 획득 성공")
|
| 82 |
+
return self.access_token
|
| 83 |
+
except requests.exceptions.Timeout:
|
| 84 |
+
logger.error(f"VITO API 토큰 획득 시간 초과: {self.token_url}")
|
| 85 |
+
raise TimeoutError("VITO API 토큰 획득 시간 초과")
|
| 86 |
+
except requests.exceptions.RequestException as e:
|
| 87 |
+
logger.error(f"VITO API 토큰 획득 실패: {e}")
|
| 88 |
+
if hasattr(e, 'response') and e.response is not None:
|
| 89 |
+
logger.error(f"응답 코드: {e.response.status_code}, 내용: {e.response.text}")
|
| 90 |
+
raise ConnectionError(f"VITO API 토큰 획득 실패: {e}")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def transcribe_audio(self, audio_bytes, language="ko"):
|
| 94 |
+
"""
|
| 95 |
+
오디오 바이트 데이터를 텍스트로 변환
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
audio_bytes: 오디오 파일 바이트 데이터
|
| 99 |
+
language: 언어 코드 (기본값: 'ko')
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
인식된 텍스트 또는 오류 메시지를 포함한 딕셔너리
|
| 103 |
+
{'success': True, 'text': '인식된 텍스트'}
|
| 104 |
+
{'success': False, 'error': '오류 메시지', 'details': '상세 내용'}
|
| 105 |
+
"""
|
| 106 |
+
if not self.client_id or not self.client_secret:
|
| 107 |
+
logger.error("API 키가 설정되지 않았습니다.")
|
| 108 |
+
return {"success": False, "error": "API 키가 설정되지 않았습니다."}
|
| 109 |
+
|
| 110 |
+
try:
|
| 111 |
+
# 토큰 획득 또는 갱신
|
| 112 |
+
# (선택적 개선: 만료 시간 체크 로직 추가 시 self._token_expires_at 사용)
|
| 113 |
+
if not self.access_token: # or time.time() >= self._token_expires_at:
|
| 114 |
+
logger.info("VITO API 토큰 획득/갱신 시도...")
|
| 115 |
+
self.get_access_token()
|
| 116 |
+
|
| 117 |
+
headers = {
|
| 118 |
+
"Authorization": f"Bearer {self.access_token}"
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
files = {
|
| 122 |
+
"file": ("audio_file", audio_bytes) # 파일명 튜플로 전달
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
# API 설정값 (필요에 따라 수정)
|
| 126 |
+
config = {
|
| 127 |
+
"use_multi_channel": False,
|
| 128 |
+
"use_itn": True, # Inverse Text Normalization (숫자, 날짜 등 변환)
|
| 129 |
+
"use_disfluency_filter": True, # 필러 (음, 아...) 제거
|
| 130 |
+
"use_profanity_filter": False, # 비속어 필터링
|
| 131 |
+
"language": language,
|
| 132 |
+
# "type": "audio" # type 파라미터는 VITO 문서상 필수 아님 (자동 감지)
|
| 133 |
+
}
|
| 134 |
+
data = {"config": json.dumps(config)}
|
| 135 |
+
|
| 136 |
+
logger.info(f"VITO STT API ({self.stt_url}) 요청 전송 중...")
|
| 137 |
+
response = requests.post(
|
| 138 |
+
self.stt_url,
|
| 139 |
+
headers=headers,
|
| 140 |
+
files=files,
|
| 141 |
+
data=data,
|
| 142 |
+
timeout=20 # 업로드 타임아웃
|
| 143 |
+
)
|
| 144 |
+
response.raise_for_status()
|
| 145 |
+
|
| 146 |
+
result = response.json()
|
| 147 |
+
job_id = result.get("id")
|
| 148 |
+
|
| 149 |
+
if not job_id:
|
| 150 |
+
logger.error("VITO API 작업 ID를 받아오지 못했습니다.")
|
| 151 |
+
return {"success": False, "error": "VITO API 작업 ID를 받아오지 못했습니다."}
|
| 152 |
+
|
| 153 |
+
logger.info(f"VITO STT 작업 ID: {job_id}, 결과 확인 시작...")
|
| 154 |
+
|
| 155 |
+
# 결과 확인 URL
|
| 156 |
+
transcript_url = f"{self.stt_url}/{job_id}"
|
| 157 |
+
max_tries = 15 # 최대 시도 횟수 증가
|
| 158 |
+
wait_time = 2 # 대기 시간 증가 (초)
|
| 159 |
+
|
| 160 |
+
for try_count in range(max_tries):
|
| 161 |
+
time.sleep(wait_time) # API 부하 감소 위해 대기
|
| 162 |
+
logger.debug(f"결과 확인 시도 ({try_count + 1}/{max_tries}) - URL: {transcript_url}")
|
| 163 |
+
get_response = requests.get(
|
| 164 |
+
transcript_url,
|
| 165 |
+
headers=headers,
|
| 166 |
+
timeout=10 # 결과 확인 타임아웃
|
| 167 |
+
)
|
| 168 |
+
get_response.raise_for_status()
|
| 169 |
+
|
| 170 |
+
result = get_response.json()
|
| 171 |
+
status = result.get("status")
|
| 172 |
+
logger.debug(f"현재 상태: {status}")
|
| 173 |
+
|
| 174 |
+
if status == "completed":
|
| 175 |
+
# 결과 추출 (utterances 구조 확인 필요)
|
| 176 |
+
utterances = result.get("results", {}).get("utterances", [])
|
| 177 |
+
if utterances:
|
| 178 |
+
# 전체 텍스트를 하나로 합침
|
| 179 |
+
transcript = " ".join([seg.get("msg", "") for seg in utterances if seg.get("msg")]).strip()
|
| 180 |
+
logger.info(f"VITO STT 인식 성공 (일부): {transcript[:50]}...")
|
| 181 |
+
return {
|
| 182 |
+
"success": True,
|
| 183 |
+
"text": transcript
|
| 184 |
+
# "raw_result": result # 필요시 전체 결과 반환
|
| 185 |
+
}
|
| 186 |
+
else:
|
| 187 |
+
logger.warning("VITO STT 완료되었으나 결과 utterances가 비어있습니다.")
|
| 188 |
+
return {"success": True, "text": ""} # 성공이지만 텍스트 없음
|
| 189 |
+
|
| 190 |
+
elif status == "failed":
|
| 191 |
+
error_msg = f"VITO API 변환 실패: {result.get('message', '알 수 없는 오류')}"
|
| 192 |
+
logger.error(error_msg)
|
| 193 |
+
return {"success": False, "error": error_msg, "details": result}
|
| 194 |
+
|
| 195 |
+
elif status == "transcribing":
|
| 196 |
+
logger.info(f"VITO API 처리 중... ({try_count + 1}/{max_tries})")
|
| 197 |
+
else: # registered, waiting 등 다른 상태
|
| 198 |
+
logger.info(f"VITO API 상태 '{status}', 대기 중... ({try_count + 1}/{max_tries})")
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
logger.error(f"VITO API 응답 타임아웃 ({max_tries * wait_time}초 초과)")
|
| 202 |
+
return {"success": False, "error": "VITO API 응답 타임아웃"}
|
| 203 |
+
|
| 204 |
+
except requests.exceptions.HTTPError as e:
|
| 205 |
+
# 토큰 만료 오류 처리 (401 Unauthorized)
|
| 206 |
+
if e.response.status_code == 401:
|
| 207 |
+
logger.warning("VITO API 토큰이 만료되었거나 유효하지 않습니다. 토큰 재발급 시도...")
|
| 208 |
+
self.access_token = None # 기존 토큰 무효화
|
| 209 |
+
try:
|
| 210 |
+
# 재귀 호출 대신, 토큰 재발급 후 다시 시도하는 로직 구성
|
| 211 |
+
self.get_access_token()
|
| 212 |
+
logger.info("새 토큰으로 재시도합니다.")
|
| 213 |
+
# 재시도는 이 함수를 다시 호출하는 대신, 호출하는 쪽에서 처리하는 것이 더 안전할 수 있음
|
| 214 |
+
# 여기서는 한 번 더 시도하는 로직 추가 (무한 루프 방지 필요)
|
| 215 |
+
# return self.transcribe_audio(audio_bytes, language) # 재귀 호출 방식
|
| 216 |
+
# --- 비재귀 방식 ---
|
| 217 |
+
headers["Authorization"] = f"Bearer {self.access_token}" # 헤더 업데이트
|
| 218 |
+
# POST 요청부터 다시 시작 (코드 중복 발생 가능성 있음)
|
| 219 |
+
# ... (POST 요청 및 결과 폴링 로직 반복) ...
|
| 220 |
+
# 간단하게는 그냥 실패 처리하고 상위에서 재시도 유도
|
| 221 |
+
return {"success": False, "error": "토큰 만료 후 재시도 필요", "details": "토큰 재발급 성공"}
|
| 222 |
+
|
| 223 |
+
except Exception as token_e:
|
| 224 |
+
logger.error(f"토큰 재획득 실패: {token_e}")
|
| 225 |
+
return {"success": False, "error": f"토큰 재획득 실패: {str(token_e)}"}
|
| 226 |
+
|
| 227 |
+
else:
|
| 228 |
+
# 401 외 다른 HTTP 오류
|
| 229 |
+
error_body = ""
|
| 230 |
+
try:
|
| 231 |
+
error_body = e.response.text
|
| 232 |
+
except Exception:
|
| 233 |
+
pass
|
| 234 |
+
logger.error(f"VITO API HTTP 오류: {e.response.status_code}, 응답: {error_body}")
|
| 235 |
+
return {
|
| 236 |
+
"success": False,
|
| 237 |
+
"error": f"API HTTP 오류: {e.response.status_code}",
|
| 238 |
+
"details": error_body
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
except requests.exceptions.Timeout:
|
| 242 |
+
logger.error("VITO API 요청 시간 초과")
|
| 243 |
+
return {"success": False, "error": "API 요청 시간 초과"}
|
| 244 |
+
except requests.exceptions.RequestException as e:
|
| 245 |
+
logger.error(f"VITO API 요청 중 네트워크 오류 발생: {str(e)}")
|
| 246 |
+
return {"success": False, "error": "API 요청 네트워크 오류", "details": str(e)}
|
| 247 |
+
except Exception as e:
|
| 248 |
+
logger.error(f"음성인식 처리 중 예상치 못한 오류 발생: {str(e)}", exc_info=True)
|
| 249 |
+
return {
|
| 250 |
+
"success": False,
|
| 251 |
+
"error": "음성인식 내부 처리 실패",
|
| 252 |
+
"details": str(e)
|
| 253 |
+
}
|