This commit is contained in:
dsyoon
2025-12-27 14:06:26 +09:00
parent 23f5388c56
commit 46460b77f8
33 changed files with 4600 additions and 1 deletions

View File

@@ -0,0 +1,57 @@
// AIService.js
// -----------------------------------------------------------------------------
// 백엔드 `/chat` 엔드포인트 호출 전용 모듈.
// - question : 사용자가 보낸 텍스트
// - toolId : 엔진 ID (ex: dev_chatbot)
// - sessionId : 세션 지속용 ID (null이면 서버가 새로 발급)
// - files : 첨부 파일 배열
// 응답 형태 { response, sessionId, toolName }
// 프론트엔드의 ChatHandler / ChatInput 등에서 재사용한다.
// -----------------------------------------------------------------------------
// 실제 운영환경에서는 REACT_APP_* 형태의 .env 값을 사용하도록 권장.
const OPENAI_API_KEY = process.env.REACT_APP_OPENAI_API_KEY;
// FastAPI 서버 주소 필요시 프록시 / env 로 분리.
const API_BASE_URL = 'http://localhost:8010';
/**
* 챗봇 API 호출 함수 (multipart/form-data)
* @param {string} question - 사용자 질문
* @param {string} toolId - 엔진 ID
* @param {string|null} sessionId- 세션 ID (옵션)
* @param {File[]} files - 첨부 파일 배열
* @returns {Promise<{response:string, sessionId:string, toolName:string}>}
*/
const AIService = async (question, toolId, sessionId = null, files = []) => {
try {
// ------------ FormData 구성 -------------
const formData = new FormData();
formData.append('message', question);
formData.append('tool_id', toolId);
if (sessionId) formData.append('session_id', sessionId);
files.forEach(file => formData.append('image', file));
// ------------- Fetch -------------------
const response = await fetch(`${API_BASE_URL}/chat`, {
method: 'POST',
body: formData,
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
return {
response : data.response?.trim() || 'AI 응답을 생성하지 못했습니다.',
sessionId: data.session_id,
toolName : data.tool_name,
};
} catch (e) {
console.error('AI 서비스 오류:', e);
return { response: 'AI 응답을 생성하지 못했습니다.', sessionId: null, toolName: '' };
}
};
export default AIService;

View File

@@ -0,0 +1,40 @@
// ChatHandler.js
// -----------------------------------------------------------------------------
// 1) 이미지가 있을 경우 OCRService 로 텍스트 추출
// 2) 최종 question(원문 + OCR 텍스트) 을 AIService 로 전달
// 3) AIService 응답을 그대로 반환하여 상위 컴포넌트(ChatInput)에서 처리
// -----------------------------------------------------------------------------
import OCRService from './OCRService';
import AIService from './AIService';
/**
* 사용자의 입력(text + image)을 받아 AIService 로 요청하는 헬퍼.
* @param {Object} params
* @param {string} params.text - 사용자가 입력한 텍스트
* @param {File[]} params.files - 첨부 파일 배열(선택)
* @param {string} params.toolId - 호출할 엔진 ID
* @param {string=} params.sessionId - 세션 ID (선택)
* @returns {Promise<{response:string, sessionId:string, toolName:string}>}
*/
const ChatHandler = async ({ text, files = [], toolId, sessionId = null }) => {
let question = text;
// 이미지 파일이 있으면 OCR 실행 후 question 에 병합
const imageFiles = files.filter(f => f.type.startsWith('image/'));
if (imageFiles.length) {
const ocrTexts = [];
for (const img of imageFiles) {
const ocrText = await OCRService(img);
if (ocrText) ocrTexts.push(ocrText);
}
if (ocrTexts.length) {
question = `${text}\n\n이미지에서 추출한 텍스트:\n${ocrTexts.join('\n')}`;
}
}
// AI 서비스 호출
return AIService(question, toolId, sessionId, files);
};
export default ChatHandler;

View File

@@ -0,0 +1,93 @@
// ChatInput.jsx (간단한 상태 기반 데모 컴포넌트)
// -----------------------------------------------------------------------------
// • inputText / inputImage 상태 관리
// • ChatHandler 호출 후 messages 배열에 push
// • very minimal UI (실제 서비스에서는 스타일·UX 개선 필요)
// -----------------------------------------------------------------------------
import React, { useState } from 'react';
import ChatHandler from './ChatHandler';
const ChatInput = () => {
// ---------------- state ----------------
const [inputText, setInputText] = useState('');
const [selectedFiles, setSelectedFiles] = useState([]); // File[]
const [messages, setMessages] = useState([]); // {text, files, sender}
const [loading, setLoading] = useState(false);
// ------------- handlers ---------------
const handleTextChange = e => setInputText(e.target.value);
const handleFilesChange = e => {
const files = Array.from(e.target.files || []);
if (files.length) {
setSelectedFiles(prev => [...prev, ...files]);
}
};
const handleFileRemove = idx => {
setSelectedFiles(prev => prev.filter((_, i) => i !== idx));
};
// 채팅 보내기
const handleSend = async () => {
if (!inputText && selectedFiles.length === 0) return;
setLoading(true);
// 1) 사용자 메시지 화면에 표시
const userMessage = { text: inputText, files: selectedFiles, sender: 'user' };
setMessages(prev => [...prev, userMessage]);
// 2) ChatHandler 로 AI 답변 요청
const { response } = await ChatHandler({
text : inputText,
files : selectedFiles,
toolId : 'dev_chatbot', // 데모용 고정
});
// 3) AI 응답 메시지 push
setMessages(prev => [...prev, userMessage, { text: response, sender: 'ai' }]);
// 4) 입력 초기화
setInputText('');
setSelectedFiles([]);
setLoading(false);
};
// ---------------- render --------------
return (
<div>
<div style={{ minHeight: 200, border: '1px solid #ccc', marginBottom: 10, padding: 10 }}>
{messages.map((msg, idx) => (
<div key={idx} style={{ marginBottom: 8 }}>
<b>{msg.sender === 'user' ? '나' : 'AI'}:</b> {msg.text}
{msg.files && msg.files.length > 0 && (
<ul style={{ marginTop: 4 }}>
{msg.files.map((file, fIdx) => (
<li key={fIdx}>{file.name}</li>
))}
</ul>
)}
</div>
))}
{loading && <div>AI가 응답을 생성하고 있습니다...</div>}
</div>
{/* 입력 영역 */}
<input type="text" value={inputText} onChange={handleTextChange} placeholder="메시지를 입력하세요" style={{ width: 300 }} />
<input type="file" multiple onChange={handleFilesChange} />
{selectedFiles.length > 0 && (
<ul style={{ marginTop: 4 }}>
{selectedFiles.map((file, idx) => (
<li key={idx}>
{file.name} <button type="button" onClick={() => handleFileRemove(idx)}>삭제</button>
</li>
))}
</ul>
)}
<button onClick={handleSend} disabled={loading}>전송</button>
</div>
);
};
export default ChatInput;

View File

@@ -0,0 +1,22 @@
// OCRService.js 브라우저에서 동작하는 간단 OCR 래퍼(Tesseract.js)
// -----------------------------------------------------------------------------
// 이미지 File 객체를 받아 한국어+영어 텍스트를 추출하여 반환한다.
// 주의: 브라우저 워커 기반이므로 큰 이미지·다중 호출 시 성능 이슈가 있을 수 있다.
// -----------------------------------------------------------------------------
import Tesseract from 'tesseract.js';
/**
* @param {File} imageFile - 이미지 파일 객체
* @returns {Promise<string>} 추출 텍스트 (trim 처리)
*/
const OCRService = async (imageFile) => {
try {
const { data: { text } } = await Tesseract.recognize(imageFile, 'kor+eng');
return text.trim();
} catch (e) {
return '';
}
};
export default OCRService;