diff --git a/backend/services/langchain_service.py b/backend/services/langchain_service.py index 006a51b..1fd9bcd 100644 --- a/backend/services/langchain_service.py +++ b/backend/services/langchain_service.py @@ -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, diff --git a/frontend/src/components/ChatInterface.tsx b/frontend/src/components/ChatInterface.tsx index 86d716c..1d1309c 100644 --- a/frontend/src/components/ChatInterface.tsx +++ b/frontend/src/components/ChatInterface.tsx @@ -96,7 +96,7 @@ const ChatInterface: React.FC = () => { }; return ( -
+
{/* 메시지 영역 */}
{messages.length === 0 ? ( @@ -140,7 +140,19 @@ const ChatInterface: React.FC = () => {
{/* 입력 영역 */} -
+
@@ -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' + }} />
{ 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' + }} > diff --git a/frontend/src/components/MessageBubble.tsx b/frontend/src/components/MessageBubble.tsx index 908e3ee..9de90a2 100644 --- a/frontend/src/components/MessageBubble.tsx +++ b/frontend/src/components/MessageBubble.tsx @@ -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 = ({ message }) => { {message.content}
) : ( -
-
{children}
, - // code 태그 처리 - code: ({ children, className }) => { - const isInline = !className; - return isInline ? ( - {children} - ) : ( -
{children}
- ); - }, - // 모든 마크다운 요소에 대한 커스텀 스타일링 - h1: ({ children }) =>

{children}

, - h2: ({ children }) =>

{children}

, - h3: ({ children }) =>

{children}

, - h4: ({ children }) =>

{children}

, - p: ({ children }) =>

{children}

, - ul: ({ children }) =>
    {children}
, - ol: ({ children }) =>
    {children}
, - li: ({ children }) =>
  • {children}
  • , - strong: ({ children }) => {children}, - em: ({ children }) => {children}, - blockquote: ({ children }) =>
    {children}
    , - table: ({ children }) =>
    {children}
    , - thead: ({ children }) => {children}, - tbody: ({ children }) => {children}, - tr: ({ children }) => {children}, - th: ({ children }) => {children}, - td: ({ children }) => {children}, - hr: () =>
    , - }} - > - {message.content} -
    -
    + )} {/* 소스 정보 */} @@ -214,4 +174,4 @@ const MessageBubble: React.FC = ({ message }) => { ); }; -export default MessageBubble; +export default MessageBubble; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index 3eb7cfc..dc3618a 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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; }