This commit is contained in:
엔큐 2025-10-10 14:26:46 +09:00
parent 94e8857f44
commit b2eca41af3
4 changed files with 261 additions and 73 deletions

View File

@ -107,6 +107,8 @@ class LangChainRAGService:
8. 중요한 정보나 주의사항이 있다면 강조하세요
9. 문서에 없는 내용은 추측하지 말고 "문서에 명시되지 않음"이라고 하세요
10. 마크다운 형식을 사용하여 구조화된 답변을 제공하세요
11. 영어로 사고 과정이나 내부 대화를 절대 포함하지 마세요
12. "part. but the user", "wait, the initial", "let me check" 같은 영어 표현을 절대 사용하지 마세요
**참조 문서:**
{context}
@ -217,24 +219,27 @@ class LangChainRAGService:
korean_found = False
for line in lines:
line = line.strip()
if not line:
filtered_lines.append(line)
original_line = line # 원본 줄 보존 (들여쓰기, 공백 등)
line_stripped = line.strip()
# 빈 줄은 그대로 유지 (마크다운 구조 보존)
if not line_stripped:
filtered_lines.append(original_line)
continue
# 한국어가 포함된 줄이 나오면 korean_found = True
if re.search(r'[가-힣]', line):
if re.search(r'[가-힣]', line_stripped):
korean_found = True
# 한국어가 발견되기 전까지는 모든 영어 줄 무조건 제거
if not korean_found:
if re.match(r'^[A-Za-z]', line):
if re.match(r'^[A-Za-z]', line_stripped):
continue
# 한국어가 발견된 후에도 영어 사고 과정 제거
if re.match(r'^[A-Za-z]', line):
# 한국어가 발견된 후에도 영어 사고 과정 제거 (극강화)
if re.match(r'^[A-Za-z]', line_stripped):
# 모든 영어 패턴 제거 (더 많은 패턴 추가)
if any(phrase in line.lower() for phrase in [
if any(phrase in line_stripped.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',
@ -243,24 +248,90 @@ class LangChainRAGService:
'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'
'documents', 'thinking', 'process', 'internal',
'part. but the user', 'wait, the initial', 'let me check',
'user also wants', 'answer in markdown', 'instructions say',
'use markdown for', 'check again', 'but the user also',
'wants the answer in', 'markdown. wait', 'the initial instructions',
'say to use markdown', 'for the answer', 'let me check again',
'part. but the user also wants the answer in markdown',
'wait, the initial instructions say to use markdown',
'let me check again', 'but the user also wants',
'wants the answer in markdown', 'markdown. wait',
'the initial instructions', 'say to use markdown for the answer'
]):
continue
# 영어 문장이지만 중요한 내용이 아닌 경우 제거
if len(line) > 10 and any(word in line.lower() for word in [
if len(line_stripped) > 10 and any(word in line_stripped.lower() for word in [
'thinking', 'process', 'structure', 'format', 'markdown',
'tag', 'focus', 'content', 'documents', 'internal',
'answer', 'should', 'make', 'sure', 'avoid'
'answer', 'should', 'make', 'sure', 'avoid',
'user', 'wants', 'instructions', 'check', 'again',
'part', 'but', 'let', 'me', 'say', 'use', 'for',
'wait', 'initial', 'also', 'the', 'in', 'to'
]):
continue
# 짧은 영어 문장도 제거 (사고 과정일 가능성)
if len(line) < 200 and not re.search(r'[가-힣]', line):
if len(line_stripped) < 200 and not re.search(r'[가-힣]', line_stripped):
continue
filtered_lines.append(line)
# 원본 줄 유지 (들여쓰기, 공백 등 마크다운 구조 보존)
filtered_lines.append(original_line)
answer = '\n'.join(filtered_lines).strip()
# 마크다운 구조 보존을 위한 추가 처리
# 연속된 빈 줄을 하나로 정리하되, 마크다운 구조는 유지
answer = re.sub(r'\n\s*\n\s*\n+', '\n\n', answer)
# 마크다운 문법이 있는지 확인하고, 없다면 강제로 마크다운 형식 적용
if not re.search(r'^#{1,6}\s|^\*\*|^[-*]\s|^\d+\.\s|^\|', answer, re.MULTILINE):
# 마크다운 문법이 없으면 첫 번째 줄을 제목으로 만들기
lines = answer.split('\n')
if lines and lines[0].strip():
# 첫 번째 줄이 제목이 될 수 있는 내용인지 확인
first_line = lines[0].strip()
if len(first_line) > 5 and not first_line.endswith('.'):
lines[0] = f"# {first_line}"
answer = '\n'.join(lines)
# 추가 영어 사고 과정 제거 (전체 텍스트 레벨)
english_phrases_to_remove = [
"part. but the user also wants the answer in markdown. wait, the initial instructions say to use markdown for the answer. let me check again.",
"part. but the user also wants the answer in markdown",
"wait, the initial instructions say to use markdown",
"let me check again",
"but the user also wants",
"wants the answer in markdown",
"markdown. wait",
"the initial instructions",
"say to use markdown for the answer",
"part. but the user",
"user also wants",
"answer in markdown",
"initial instructions",
"use markdown",
"check again"
]
for phrase in english_phrases_to_remove:
answer = answer.replace(phrase, '').strip()
answer = answer.replace(phrase.lower(), '').strip()
answer = answer.replace(phrase.upper(), '').strip()
answer = answer.replace(phrase.capitalize(), '').strip()
# 정규식으로 영어 사고 과정 제거
import re
english_thinking_patterns = [
r'^[A-Za-z].*user.*wants.*answer.*markdown.*$',
r'^[A-Za-z].*part\.\s*but.*user.*also.*wants.*$',
r'^[A-Za-z].*wait.*initial.*instructions.*$',
r'^[A-Za-z].*let.*me.*check.*again.*$'
]
for pattern in english_thinking_patterns:
answer = re.sub(pattern, '', answer, flags=re.MULTILINE | re.IGNORECASE).strip()
response = {
"answer": answer,
"references": references,

View File

@ -96,7 +96,7 @@ const ChatInterface: React.FC = () => {
};
return (
<div className="flex-1 flex flex-col h-full">
<div className="flex-1 flex flex-col min-h-0">
{/* 메시지 영역 */}
<div className="flex-1 overflow-y-auto p-4 space-y-16 bg-white">
{messages.length === 0 ? (
@ -140,7 +140,19 @@ const ChatInterface: React.FC = () => {
</div>
{/* 입력 영역 */}
<div className="bg-white border-t border-gray-200 p-4">
<div
className="bg-white p-4 input-area"
style={{
position: 'relative',
zIndex: 1000,
display: 'block',
visibility: 'visible',
opacity: 1,
minHeight: '80px',
backgroundColor: 'white',
borderTop: 'none'
}}
>
<div className="w-full max-w-none mx-auto px-4" style={{ maxWidth: '80vw' }}>
<div className="flex space-x-3">
<div className="flex-1 relative">
@ -153,6 +165,18 @@ const ChatInterface: React.FC = () => {
placeholder="질문을 입력하세요..."
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
disabled={isLoading}
style={{
border: '1px solid #d1d5db',
backgroundColor: 'white',
color: '#374151',
display: 'block',
visibility: 'visible',
opacity: 1,
width: '100%',
minHeight: '48px',
paddingLeft: '20px',
paddingRight: '16px'
}}
/>
</div>
<motion.button
@ -161,6 +185,13 @@ const ChatInterface: React.FC = () => {
onClick={handleSendMessage}
disabled={!inputMessage.trim() || isLoading}
className="px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-xl font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-all btn-animate"
style={{
display: 'block',
visibility: 'visible',
opacity: 1,
backgroundColor: 'linear-gradient(to right, #2563eb, #9333ea)',
color: 'white'
}}
>
<Send className="w-5 h-5" />
</motion.button>

View File

@ -1,11 +1,9 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
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';
import SimpleMarkdownRenderer from './SimpleMarkdownRenderer';
interface MessageBubbleProps {
message: Message;
@ -115,45 +113,7 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
{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>
<SimpleMarkdownRenderer content={message.content || ''} />
)}
{/* 소스 정보 */}

View File

@ -16,7 +16,6 @@ body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
@ -310,18 +309,8 @@ code {
border: none !important;
}
/* 모든 div 요소의 상단 테두리 제거 */
div {
border-top: none !important;
}
/* 특정 클래스의 테두리 제거 */
.border-t {
border-top: none !important;
}
/* 모든 가능한 선과 배경 제거 */
* {
/* 특정 클래스의 테두리 제거 (입력 영역 제외) */
.border-t:not(input):not(.input-area) {
border-top: none !important;
}
@ -437,6 +426,42 @@ div {
line-height: inherit !important;
}
/* 마크다운 pre 래퍼 스타일 */
.markdown-pre-wrapper {
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;
display: block !important;
overflow: visible !important;
}
/* react-markdown이 생성하는 모든 pre 태그에 대한 강력한 리셋 */
.markdown-content pre,
.markdown-content > pre,
.markdown-content pre[class*="language-"],
.markdown-content pre:not([class]),
.markdown-content pre code,
.markdown-content > pre code {
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;
display: block !important;
overflow: visible !important;
color: inherit !important;
}
/* 실제 코드 블록에만 적용되는 pre 태그 스타일 */
.markdown-content pre {
background-color: #F3F4F6 !important;
@ -491,9 +516,49 @@ div {
}
.markdown-content hr {
display: none !important;
visibility: hidden !important;
height: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
border-top: 1px solid #E5E7EB !important;
margin: 2rem 0 !important;
border-top: none !important;
border-bottom: none !important;
border-left: none !important;
border-right: none !important;
outline: none !important;
box-shadow: none !important;
}
/* 모든 hr 요소 강제 숨김 */
hr {
display: none !important;
visibility: hidden !important;
height: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
outline: none !important;
box-shadow: none !important;
}
/* 모든 가능한 가로줄 요소 강제 숨김 */
hr,
.hr,
.horizontal-line,
.border-t,
.border-top,
[class*="border-t"],
[class*="border-top"] {
display: none !important;
visibility: hidden !important;
height: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
border-top: none !important;
outline: none !important;
box-shadow: none !important;
}
/* 참조 문서 섹션의 모든 선과 배경 제거 */
@ -505,4 +570,65 @@ div {
border-right: none !important;
background: transparent !important;
background-color: transparent !important;
outline: none !important;
box-shadow: none !important;
}
/* 모든 hr 태그 완전 제거 */
hr {
display: none !important;
visibility: hidden !important;
height: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
background: none !important;
}
/* 참조 문서 위의 모든 선 제거 */
.mt-3.pt-3::before,
.mt-3.pt-3::after {
display: none !important;
content: none !important;
}
/* 입력 영역 스타일 보호 */
.input-area {
border-top: none !important;
background: white !important;
position: relative !important;
z-index: 10 !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
height: auto !important;
min-height: 80px !important;
}
.input-area input {
border: 1px solid #d1d5db !important;
background: white !important;
color: #374151 !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
width: 100% !important;
height: auto !important;
min-height: 48px !important;
padding-left: 20px !important;
padding-right: 16px !important;
}
.input-area input:focus {
border: 1px solid #3b82f6 !important;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
}
/* 입력 영역 버튼 스타일 */
.input-area button {
display: block !important;
visibility: visible !important;
opacity: 1 !important;
background: linear-gradient(to right, #2563eb, #9333ea) !important;
color: white !important;
}