This commit is contained in:
엔큐 2025-10-09 14:31:51 +09:00
parent 33e09027b2
commit 94e8857f44
7 changed files with 605 additions and 44 deletions

View File

@ -92,17 +92,29 @@ class LangChainRAGService:
def _setup_rag_chain(self):
"""RAG 체인 설정"""
try:
# 프롬프트 템플릿
# 개선된 프롬프트 템플릿
prompt_template = """
다음 문서들을 참고하여 질문에 답변해주세요.
문서들:
{context}
질문: {input}
답변: 문서의 내용을 바탕으로 정확하고 상세하게 답변해주세요.
"""
당신은 연구 문서 전문가입니다. 주어진 문서들을 바탕으로 사용자의 질문에 정확하고 상세하게 답변해주세요.
**절대 지켜야 답변 규칙:**
1. 답변은 반드시 한국어로만 시작하세요
2. 영어 단어나 문장을 절대 사용하지 마세요
3. <think>부터 </think>까지의 모든 내용을 절대 포함하지 마세요
4. 사고 과정, 분석 과정, 고민 과정을 전혀 보여주지 마세요
5. 바로 최종 답변만 제공하세요
6. 문서의 내용을 정확히 파악하고 요약하여 답변하세요
7. 구체적인 절차나 방법이 있다면 단계별로 설명하세요
8. 중요한 정보나 주의사항이 있다면 강조하세요
9. 문서에 없는 내용은 추측하지 말고 "문서에 명시되지 않음"이라고 하세요
10. 마크다운 형식을 사용하여 구조화된 답변을 제공하세요
**참조 문서:**
{context}
**사용자 질문:** {input}
**답변:** 문서의 내용을 바탕으로 질문에 대한 구체적이고 실용적인 답변을 마크다운 형식으로 바로 제공해주세요. 반드시 한국어로 시작하고, 영어나 <think> 태그는 절대 포함하지 마세요.
"""
prompt = PromptTemplate(
template=prompt_template,
@ -171,33 +183,133 @@ class LangChainRAGService:
def generate_answer(self, question: str) -> Dict[str, Any]:
"""RAG를 통한 답변 생성"""
try:
# 간단한 유사 문서 검색으로 시작
# RAG 체인을 사용하여 답변 생성
if self.qa_chain:
logger.info(f"🤖 LLM을 사용한 RAG 답변 생성 시작: {question}")
result = self.qa_chain.invoke({"input": question})
# 참조 문서 정보 추출
references = []
source_documents = result.get("context", [])
for doc in source_documents:
if hasattr(doc, 'metadata') and doc.metadata:
filename = doc.metadata.get('filename', 'Unknown')
file_id = doc.metadata.get('file_id', 'unknown')
chunk_index = doc.metadata.get('chunk_index', 0)
page_number = chunk_index + 1
references.append(f"{filename}::{file_id} [p{page_number}]")
# <think> 태그 및 영어 사고 과정 제거 (강화된 버전)
answer = result.get("answer", "답변을 생성할 수 없습니다.")
import re
# <think>부터 </think>까지의 모든 내용 제거 (대소문자 구분 없음)
answer = re.sub(r'<think>.*?</think>', '', answer, flags=re.DOTALL | re.IGNORECASE).strip()
# </think> 태그만 있는 경우도 제거
answer = re.sub(r'</think>', '', answer, flags=re.IGNORECASE).strip()
# <think> 태그만 있는 경우도 제거
answer = re.sub(r'<think>', '', answer, flags=re.IGNORECASE).strip()
# 영어 사고 과정 완전 제거 (극강화 버전)
lines = answer.split('\n')
filtered_lines = []
korean_found = False
for line in lines:
line = line.strip()
if not line:
filtered_lines.append(line)
continue
# 한국어가 포함된 줄이 나오면 korean_found = True
if re.search(r'[가-힣]', line):
korean_found = True
# 한국어가 발견되기 전까지는 모든 영어 줄 무조건 제거
if not korean_found:
if re.match(r'^[A-Za-z]', line):
continue
# 한국어가 발견된 후에도 영어 사고 과정 제거
if re.match(r'^[A-Za-z]', line):
# 모든 영어 패턴 제거 (더 많은 패턴 추가)
if any(phrase in line.lower() for phrase in [
'let me', 'i need to', 'i should', 'i will', 'i can',
'the answer should', 'make sure', 'avoid any',
'structure the answer', 'break down', 'organize',
'tag or any thinking', 'so i need to focus',
'focus on the main content', 'documents',
'thinking process', 'internal thought',
'i need to focus', 'main content', 'tag', 'thinking',
'tag or any', 'so i need', 'focus on', 'main content of',
'documents', 'thinking', 'process', 'internal'
]):
continue
# 영어 문장이지만 중요한 내용이 아닌 경우 제거
if len(line) > 10 and any(word in line.lower() for word in [
'thinking', 'process', 'structure', 'format', 'markdown',
'tag', 'focus', 'content', 'documents', 'internal',
'answer', 'should', 'make', 'sure', 'avoid'
]):
continue
# 짧은 영어 문장도 제거 (사고 과정일 가능성)
if len(line) < 200 and not re.search(r'[가-힣]', line):
continue
filtered_lines.append(line)
answer = '\n'.join(filtered_lines).strip()
response = {
"answer": answer,
"references": references,
"source_documents": source_documents
}
logger.info(f"✅ LLM RAG 답변 생성 완료: {len(references)}개 참조")
return response
else:
# RAG 체인이 없는 경우 폴백
logger.warning("⚠️ RAG 체인이 초기화되지 않음. 폴백 모드로 전환")
return self._generate_fallback_answer(question)
except Exception as e:
logger.error(f"❌ RAG 답변 생성 실패: {e}")
# 오류 시 폴백 답변 생성
return self._generate_fallback_answer(question)
def _generate_fallback_answer(self, question: str) -> Dict[str, Any]:
"""폴백 답변 생성 (LLM 없이)"""
try:
# 유사 문서 검색
similar_docs = self.search_similar_documents(question, k=3)
if not similar_docs:
return {
"answer": "죄송합니다. 관련 문서를 찾을 수 없습니다.",
"references": ["문서 없음"],
"answer": "죄송합니다. 관련 문서를 찾을 수 없습니다. 다른 키워드로 검색해보시거나 문서를 업로드해주세요.",
"references": [],
"source_documents": []
}
# 문서 내용을 기반으로 간단한 답변 생성
context_text = ""
# 문서 내용 요약
context_summary = ""
references = []
for i, doc in enumerate(similar_docs):
context_text += f"\n문서 {i+1}:\n{doc.page_content[:500]}...\n"
# 문서 내용의 핵심 부분 추출 (처음 300자)
content_preview = doc.page_content[:300].replace('\n', ' ').strip()
context_summary += f"\n{content_preview}...\n"
if hasattr(doc, 'metadata') and doc.metadata:
filename = doc.metadata.get('filename', 'Unknown')
file_id = doc.metadata.get('file_id', 'unknown')
chunk_index = doc.metadata.get('chunk_index', 0)
# 페이지 번호는 청크 인덱스를 기반으로 추정 (실제로는 더 정확한 방법 필요)
page_number = chunk_index + 1
references.append(f"{filename}::{file_id} [p{page_number}]")
# 간단한 답변 생성 (LLM 없이)
answer = f"질문하신 '{question}'에 대한 관련 문서를 찾았습니다.\n\n참조 문서에서 관련 내용을 확인할 수 있습니다."
# 간단한 답변 생성
answer = f"질문하신 '{question}'에 대한 관련 문서를 찾았습니다.\n\n참조 문서에서 관련 내용을 확인할 수 있습니다:\n{context_summary}"
response = {
"answer": answer,
@ -205,15 +317,14 @@ class LangChainRAGService:
"source_documents": similar_docs
}
logger.info(f"RAG 답변 생성 완료: {len(references)}개 참조")
logger.info(f"폴백 답변 생성 완료: {len(references)}개 참조")
return response
except Exception as e:
logger.error(f"❌ RAG 답변 생성 실패: {e}")
# 오류 시 기본 응답 반환
logger.error(f"❌ 폴백 답변 생성 실패: {e}")
return {
"answer": "죄송합니다. 현재 시스템 오류로 인해 답변을 생성할 수 없습니다.",
"references": ["시스템 오류"],
"answer": "죄송합니다. 현재 시스템 오류로 인해 답변을 생성할 수 없습니다. 잠시 후 다시 시도해주세요.",
"references": [],
"source_documents": []
}

View File

@ -3,20 +3,27 @@
"version": "1.0.0",
"private": true,
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"@types/node": "^20.0.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/node": "^20.0.0",
"autoprefixer": "^10.4.0",
"framer-motion": "^10.16.0",
"lucide-react": "^0.294.0",
"react-pdf": "^10.1.0",
"pdfjs-dist": "^5.3.93",
"postcss": "^8.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^10.1.0",
"react-pdf": "^10.1.0",
"react-scripts": "5.0.1",
"rehype-highlight": "^7.0.2",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"tailwindcss": "^3.3.0",
"autoprefixer": "^10.4.0",
"postcss": "^8.4.0"
"typescript": "^4.9.5"
},
"scripts": {
"start": "react-scripts start",

View File

@ -3,6 +3,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import ChatInterface from './components/ChatInterface';
import LoginModal from './components/LoginModal';
import FileUploadModal from './components/FileUploadModal';
import PDFViewer from './components/PDFViewer';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import { ChatProvider } from './contexts/ChatContext';
import { FileProvider } from './contexts/FileContext';
@ -12,6 +13,12 @@ function AppContent() {
const { isAuthenticated } = useAuth();
const [showLogin, setShowLogin] = useState(false);
const [showFileUpload, setShowFileUpload] = useState(false);
const [showPDFViewer, setShowPDFViewer] = useState(false);
const [pdfViewerData, setPdfViewerData] = useState<{
fileId: string;
filename: string;
pageNumber: number;
} | null>(null);
const handleFileUploadClick = () => {
@ -25,6 +32,22 @@ function AppContent() {
}
};
const handlePDFView = (fileId: string, filename: string) => {
setPdfViewerData({
fileId,
filename,
pageNumber: 1
});
setShowPDFViewer(true);
setShowFileUpload(false); // 파일 업로드 모달 닫기
};
const handlePDFViewerClose = () => {
setShowPDFViewer(false);
setPdfViewerData(null);
setShowFileUpload(true); // 파일 업로드 모달 다시 열기
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 flex flex-col">
{/* 헤더 */}
@ -77,6 +100,16 @@ function AppContent() {
{showFileUpload && (
<FileUploadModal
onClose={() => setShowFileUpload(false)}
onPDFView={handlePDFView}
/>
)}
{showPDFViewer && pdfViewerData && (
<PDFViewer
fileId={pdfViewerData.fileId}
filename={pdfViewerData.filename}
pageNumber={pdfViewerData.pageNumber}
onClose={handlePDFViewerClose}
/>
)}
</AnimatePresence>

View File

@ -5,9 +5,10 @@ import { X, Upload, Trash2, Search } from 'lucide-react';
interface FileUploadModalProps {
onClose: () => void;
onPDFView: (fileId: string, filename: string) => void;
}
const FileUploadModal: React.FC<FileUploadModalProps> = ({ onClose }) => {
const FileUploadModal: React.FC<FileUploadModalProps> = ({ onClose, onPDFView }) => {
const { files, uploadFile, deleteFile, refreshFiles, searchFiles, isLoading } = useFiles();
const [dragActive, setDragActive] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
@ -252,6 +253,10 @@ const FileUploadModal: React.FC<FileUploadModalProps> = ({ onClose }) => {
}
};
const handleDocumentDoubleClick = (fileId: string, filename: string) => {
onPDFView(fileId, filename);
};
const showTooltip = (content: string, e: React.MouseEvent) => {
setTooltip({
show: true,
@ -483,10 +488,12 @@ const FileUploadModal: React.FC<FileUploadModalProps> = ({ onClose }) => {
{file.id}
</div>
<div
className="col-span-5 text-sm text-gray-800 truncate cursor-help"
className="col-span-5 text-sm text-gray-800 truncate cursor-pointer hover:text-blue-600 hover:underline transition-colors"
onMouseEnter={(e) => showTooltip(file.filename, e)}
onMouseLeave={hideTooltip}
onMouseMove={(e) => showTooltip(file.filename, e)}
onDoubleClick={() => handleDocumentDoubleClick(file.id, file.filename)}
title="더블클릭하여 PDF 뷰어로 열기"
>
{file.filename}
</div>

View File

@ -1,6 +1,9 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { User, Bot, FileText } from 'lucide-react';
import { User, FileText } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkBreaks from 'remark-breaks';
import { Message } from '../contexts/ChatContext';
import PDFViewer from './PDFViewer';
@ -88,28 +91,82 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
<div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
isUser
? 'bg-gradient-to-r from-blue-500 to-purple-500'
: 'bg-gradient-to-r from-gray-500 to-gray-600'
: 'bg-transparent'
}`}>
{isUser ? (
<User className="w-4 h-4 text-white" />
) : (
<Bot className="w-4 h-4 text-white" />
<img
src="/images/woongtalk_bgremove.png"
alt="연구QA 챗봇"
className="w-8 h-8"
/>
)}
</div>
{/* 메시지 내용 */}
<div className={`rounded-2xl px-4 py-3 flex-1 ${
<div className={`rounded-2xl px-4 py-3 flex-1 border border-gray-200 ${
isUser
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white'
: 'bg-gray-100 text-gray-800'
}`}>
<div className="whitespace-pre-wrap break-words">
{message.content}
</div>
{isUser ? (
<div className="whitespace-pre-wrap break-words">
{message.content}
</div>
) : (
<div className="markdown-content">
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkBreaks]}
components={{
// pre 태그를 완전히 제거하고 일반 div로 대체
pre: ({ children }) => <div>{children}</div>,
// code 태그 처리
code: ({ children, className }) => {
const isInline = !className;
return isInline ? (
<code className="bg-gray-200 px-2 py-1 rounded text-sm font-mono text-gray-800">{children}</code>
) : (
<div className="bg-gray-100 text-gray-800 p-4 rounded-lg overflow-x-auto mb-4 font-mono text-sm">{children}</div>
);
},
// 모든 마크다운 요소에 대한 커스텀 스타일링
h1: ({ children }) => <h1 className="text-2xl font-bold mb-4 text-gray-800 border-b border-gray-200 pb-2">{children}</h1>,
h2: ({ children }) => <h2 className="text-xl font-bold mb-3 text-gray-800 mt-6">{children}</h2>,
h3: ({ children }) => <h3 className="text-lg font-bold mb-2 text-gray-800 mt-4">{children}</h3>,
h4: ({ children }) => <h4 className="text-base font-bold mb-2 text-gray-800 mt-3">{children}</h4>,
p: ({ children }) => <p className="mb-3 text-gray-700 leading-relaxed">{children}</p>,
ul: ({ children }) => <ul className="list-disc list-inside mb-4 text-gray-700 space-y-1 ml-4">{children}</ul>,
ol: ({ children }) => <ol className="list-decimal list-inside mb-4 text-gray-700 space-y-1 ml-4">{children}</ol>,
li: ({ children }) => <li className="mb-1">{children}</li>,
strong: ({ children }) => <strong className="font-bold text-gray-800">{children}</strong>,
em: ({ children }) => <em className="italic text-gray-700">{children}</em>,
blockquote: ({ children }) => <blockquote className="border-l-4 border-blue-300 pl-4 italic text-gray-600 bg-blue-50 py-2 rounded-r mb-4">{children}</blockquote>,
table: ({ children }) => <div className="overflow-x-auto mb-4"><table className="min-w-full border-collapse border border-gray-300">{children}</table></div>,
thead: ({ children }) => <thead className="bg-gray-50">{children}</thead>,
tbody: ({ children }) => <tbody>{children}</tbody>,
tr: ({ children }) => <tr className="border-b border-gray-200 hover:bg-gray-50">{children}</tr>,
th: ({ children }) => <th className="border border-gray-300 px-4 py-2 text-left font-semibold text-gray-800 bg-gray-100">{children}</th>,
td: ({ children }) => <td className="border border-gray-300 px-4 py-2 text-gray-700">{children}</td>,
hr: () => <hr className="my-6 border-gray-300" />,
}}
>
{message.content}
</ReactMarkdown>
</div>
)}
{/* 소스 정보 */}
{!isUser && message.sources && message.sources.length > 0 && (
<div className="mt-3 pt-3 border-t border-gray-300/30">
<div className="mt-3 pt-3" style={{
borderTop: 'none !important',
border: 'none !important',
borderBottom: 'none !important',
borderLeft: 'none !important',
borderRight: 'none !important',
outline: 'none !important',
boxShadow: 'none !important'
}}>
<div className="flex items-center space-x-1 text-sm opacity-75">
<FileText className="w-4 h-4" />
<span> :</span>

View File

@ -5,8 +5,12 @@ const TypingIndicator: React.FC = () => {
return (
<div className="flex justify-start">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-full bg-gradient-to-r from-gray-500 to-gray-600 flex items-center justify-center">
<div className="w-4 h-4 bg-white rounded-full"></div>
<div className="w-8 h-8 rounded-full flex items-center justify-center">
<img
src="/images/woongtalk_bgremove.png"
alt="연구QA 챗봇"
className="w-8 h-8"
/>
</div>
<div className="bg-gray-100 rounded-2xl px-4 py-3">
<div className="flex space-x-1">

View File

@ -164,3 +164,345 @@ code {
transform: scale(1);
transform-origin: 0% 0%;
}
/* 마크다운 콘텐츠 스타일 */
.markdown-content {
max-width: 100%;
line-height: 1.6;
color: #374151;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.5rem;
color: #1f2937;
}
.markdown-content h1 {
font-size: 1.5rem;
border-bottom: 1px solid #e5e7eb;
padding-bottom: 0.5rem;
}
.markdown-content h2 {
font-size: 1.25rem;
}
.markdown-content h3 {
font-size: 1.125rem;
}
.markdown-content h4 {
font-size: 1rem;
}
.markdown-content p {
margin-bottom: 1rem;
color: #374151;
}
.markdown-content ul,
.markdown-content ol {
margin-bottom: 1rem;
padding-left: 1.5rem;
}
.markdown-content li {
margin-bottom: 0.25rem;
color: #374151;
}
.markdown-content strong {
font-weight: 600;
color: #1f2937;
}
.markdown-content em {
font-style: italic;
color: #4b5563;
}
.markdown-content code {
background-color: #f3f4f6;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-size: 0.875rem;
font-family: 'Courier New', monospace;
}
.markdown-content pre {
background-color: #1f2937;
color: #f9fafb;
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin-bottom: 1rem;
}
.markdown-content pre code {
background-color: transparent;
padding: 0;
color: inherit;
}
.markdown-content blockquote {
border-left: 4px solid #3b82f6;
padding-left: 1rem;
margin: 1rem 0;
font-style: italic;
color: #4b5563;
background-color: #eff6ff;
padding: 0.5rem 1rem;
border-radius: 0 0.25rem 0.25rem 0;
}
.markdown-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
border: 1px solid #d1d5db;
}
.markdown-content th,
.markdown-content td {
border: 1px solid #d1d5db;
padding: 0.5rem;
text-align: left;
}
.markdown-content th {
background-color: #f9fafb;
font-weight: 600;
color: #1f2937;
}
.markdown-content tr:hover {
background-color: #f9fafb;
}
.markdown-content hr {
border: none !important;
border-top: none !important;
margin: 2rem 0;
display: none !important; /* hr 요소 완전 숨김 */
height: 0 !important;
visibility: hidden !important;
}
/* 모든 가능한 선 제거 */
.markdown-content * {
border-top: none !important;
}
.markdown-content > *:first-child {
border-top: none !important;
}
/* 참조 문서 섹션의 모든 선 제거 */
.mt-3.pt-3 {
border-top: none !important;
border: none !important;
}
/* 모든 div 요소의 상단 테두리 제거 */
div {
border-top: none !important;
}
/* 특정 클래스의 테두리 제거 */
.border-t {
border-top: none !important;
}
/* 모든 가능한 선과 배경 제거 */
* {
border-top: none !important;
}
/* 메시지 버블의 모든 테두리 제거 */
.rounded-2xl {
border: none !important;
border-top: none !important;
border-bottom: none !important;
border-left: none !important;
border-right: none !important;
}
/* 마크다운 콘텐츠 스타일 */
.markdown-content {
border: none !important;
background: transparent !important;
color: #374151 !important;
font-family: inherit !important;
line-height: 1.6 !important;
white-space: pre-wrap !important;
word-wrap: break-word !important;
}
/* 마크다운 요소들 기본 스타일 */
.markdown-content h1 {
font-size: 1.5rem !important;
font-weight: bold !important;
color: #374151 !important;
margin: 1rem 0 0.5rem 0 !important;
border-bottom: 2px solid #E5E7EB !important;
padding-bottom: 0.5rem !important;
}
.markdown-content h2 {
font-size: 1.25rem !important;
font-weight: bold !important;
color: #374151 !important;
margin: 1.5rem 0 0.5rem 0 !important;
}
.markdown-content h3 {
font-size: 1.125rem !important;
font-weight: bold !important;
color: #374151 !important;
margin: 1.25rem 0 0.5rem 0 !important;
}
.markdown-content h4 {
font-size: 1rem !important;
font-weight: bold !important;
color: #374151 !important;
margin: 1rem 0 0.5rem 0 !important;
}
.markdown-content p {
margin: 0.5rem 0 !important;
line-height: 1.6 !important;
color: #4B5563 !important;
}
.markdown-content ul {
margin: 0.5rem 0 !important;
padding-left: 1.5rem !important;
list-style-type: disc !important;
}
.markdown-content ol {
margin: 0.5rem 0 !important;
padding-left: 1.5rem !important;
list-style-type: decimal !important;
}
.markdown-content li {
margin: 0.25rem 0 !important;
color: #4B5563 !important;
}
.markdown-content strong {
font-weight: bold !important;
color: #374151 !important;
}
.markdown-content em {
font-style: italic !important;
color: #4B5563 !important;
}
.markdown-content code {
background-color: #F3F4F6 !important;
padding: 0.125rem 0.25rem !important;
border-radius: 0.25rem !important;
font-family: 'Courier New', monospace !important;
font-size: 0.875rem !important;
color: #374151 !important;
}
/* 마크다운 내용이 pre 태그로 감싸지지 않도록 설정 */
.markdown-content {
white-space: normal !important;
word-wrap: break-word !important;
}
/* 전체 마크다운을 감싸는 pre 태그의 스타일을 일반 div처럼 변경 */
.markdown-content > pre {
white-space: normal !important;
word-wrap: break-word !important;
background: transparent !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
font-family: inherit !important;
font-size: inherit !important;
line-height: inherit !important;
}
/* 실제 코드 블록에만 적용되는 pre 태그 스타일 */
.markdown-content pre {
background-color: #F3F4F6 !important;
color: #374151 !important;
padding: 1rem !important;
border-radius: 0.5rem !important;
overflow-x: auto !important;
margin: 1rem 0 !important;
white-space: pre-wrap !important;
word-wrap: break-word !important;
}
/* pre 태그 내부의 코드 블록만 스타일 적용 */
.markdown-content pre code {
background: transparent !important;
padding: 0 !important;
border-radius: 0 !important;
font-size: inherit !important;
}
.markdown-content blockquote {
border-left: 4px solid #3B82F6 !important;
padding-left: 1rem !important;
margin: 1rem 0 !important;
font-style: italic !important;
color: #6B7280 !important;
background-color: #EFF6FF !important;
padding: 0.5rem 1rem !important;
border-radius: 0 0.25rem 0.25rem 0 !important;
}
.markdown-content table {
width: 100% !important;
border-collapse: collapse !important;
margin: 1rem 0 !important;
}
.markdown-content th {
border: 1px solid #D1D5DB !important;
padding: 0.5rem !important;
text-align: left !important;
background-color: #F9FAFB !important;
font-weight: bold !important;
color: #374151 !important;
}
.markdown-content td {
border: 1px solid #D1D5DB !important;
padding: 0.5rem !important;
text-align: left !important;
color: #4B5563 !important;
}
.markdown-content hr {
border: none !important;
border-top: 1px solid #E5E7EB !important;
margin: 2rem 0 !important;
}
/* 참조 문서 섹션의 모든 선과 배경 제거 */
.mt-3.pt-3 {
border: none !important;
border-top: none !important;
border-bottom: none !important;
border-left: none !important;
border-right: none !important;
background: transparent !important;
background-color: transparent !important;
}