From 41a5eb816c90f34f7e4e7770a77dd7f5bd1fd92e Mon Sep 17 00:00:00 2001 From: Sangjoon PARK Date: Fri, 27 Feb 2026 09:21:23 +0900 Subject: [PATCH 1/5] =?UTF-8?q?OCR=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=20=EA=B2=B0=EA=B3=BC=20=EB=B0=8F=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=EB=90=9C=20=EB=AC=B8=EC=84=9C=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20MySQL=20DB=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20Database=20=EB=AA=A8=EB=93=88=20=EA=B5=AC?= =?UTF-8?q?=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 문서 DB용 폴더 database_document 추가 - DB 설계도 schema.sql 추가 - DB 연결 파이썬 파일 database.py 추가 - backend_OCR 내 requirements.txt에 sqlalchemy, pymysql 추가 - ocr_server.py MySQL DB 연동 추가 현재 OCR 동작 과정 1. S3 연동: 프론트엔드가 S3에 이미지를 올리고 백엔드(OCR 서버)로 파일명을 보냅니다. 백엔드는 S3에서 그 파일을 임시로 다운로드합니다. 2. OCR 분석: 다운받은 이미지에서 텍스트를 추출합니다. 3. DB 저장: 파일명, 확장자, S3 주소, 그리고 추출된 텍스트를 묶어 docsinfos 테이블에 INSERT 합니다. 이 때 MySQL이 고유 번호(id)를 자동으로 발급해 줍니다. 4. 프론트엔드 반환: 방금 발급받은 id와 추출된 text를 프론트엔드로 반환합니다. --- backend-OCR/ocr_server.py | 48 +++++++++++++++--- backend-OCR/requirements.txt | Bin 186 -> 228 bytes database_document/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 168 bytes .../__pycache__/database.cpython-314.pyc | Bin 0 -> 2077 bytes database_document/database.py | 42 +++++++++++++++ database_document/schema.sql | 15 ++++++ 7 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 database_document/__init__.py create mode 100644 database_document/__pycache__/__init__.cpython-314.pyc create mode 100644 database_document/__pycache__/database.cpython-314.pyc create mode 100644 database_document/database.py create mode 100644 database_document/schema.sql diff --git a/backend-OCR/ocr_server.py b/backend-OCR/ocr_server.py index c8da17f..a6976e9 100644 --- a/backend-OCR/ocr_server.py +++ b/backend-OCR/ocr_server.py @@ -1,10 +1,20 @@ -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, Depends from fastapi.middleware.cors import CORSMiddleware import boto3 import os +import sys from urllib.parse import unquote from ocr_logic import EasyDocOCR from dotenv import load_dotenv +from sqlalchemy.orm import Session + +# 현재 파일의 상위 폴더(EasyDOC)를 경로를 추가하여 database 폴더에 접근 가능하게 함 +current_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.dirname(current_dir) +sys.path.append(parent_dir) + +from database_document.database import get_db, Document +from sqlalchemy.orm import Session load_dotenv() @@ -39,10 +49,10 @@ region_name=AWS_REGION ) -# 프론트엔드 요청에 맞게 GET 방식으로 주소 변경 +# db: Session = Depends(get_db)를 추가하여 API가 호출될 때마다 DB와 통신할 수 있는 세션 할당 @app.get("/ocr/s3/{filename}") -def run_ocr(filename: str): - #URL에 포함된 암호화된 파일명(예: %ED%95...)을 정상적인 글자로 변환 +def run_ocr(filename: str, db: Session = Depends(get_db)): + # URL에 포함된 암호화된 파일명(예: %ED%95...)을 정상적인 글자로 변환 decoded_filename = unquote(filename) # 로컬에 다운로드할 임시 경로 @@ -58,8 +68,34 @@ def run_ocr(filename: str): text_result = ocr_engine.extract_text(local_path) print(f"분석 완료: {text_result[:30]}...") - #프론트엔드에서 ocrResponse.data.text로 받을 수 있도록 반환형식 수정 - return {"text": text_result} + # ================= DB 저장 로직 추가 ================= + # 1. S3 URL 주소 조립 + s3_url = f"https://{BUCKET_NAME}.s3.{AWS_REGION}.amazonaws.com/{filename}" + + # 2. 파일명에서 확장자(예: png, jpg) 추출 + file_extension = decoded_filename.split('.')[-1] if '.' in decoded_filename else "unknown" + + # 3. DB에 넣을 데이터 포장 (INSERT 문과 동일한 역할) + new_doc = Document( + file_name = decoded_filename, + file_type = file_extension, + s3_url = s3_url, + extracted_text=text_result + ) + + # 4. DB에 추가하고 저장 + db.add(new_doc) + db.commit() + db.refresh(new_doc) #MySQL이 방금 발급해준 고유 ID 번호를 가져온다 + + print(f"DB 저장 성공! (문서 번호: {new_doc.id})") + # =================================================== + + # 프론트엔드에 추출 텍스트와 함께 부여된 문서 번호 반환 + return { + "id": new_doc.id, + "text": text_result + } except Exception as e: print(f"에러 발생: {e}") diff --git a/backend-OCR/requirements.txt b/backend-OCR/requirements.txt index 3ecdd39bdd58d092ac423e97eec576c670ad489b..4c2c28a43fecdc52b47ee10e8ead689804cbfa72 100644 GIT binary patch delta 49 tcmdnR_=Iu7E-hXLE{0-;LWUfML?BLP$Y4li$YrQx0Erg>X%HJC4*({P3Y7o= delta 6 NcmaFDxQlVZE&vKB0_y+( diff --git a/database_document/__init__.py b/database_document/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/database_document/__pycache__/__init__.cpython-314.pyc b/database_document/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e38a4dd754d3b3ce9b2cb4157b94d1706dd78d33 GIT binary patch literal 168 zcmdPqOlWax zQE^OhR&ka=j7xrUX>Mv>NpXyOW{F2>QjBY2aixpDb4*HNNn%oBacX=DR7FgDd}dx| mNqoFsLFFwDo80`A(wtPgB37WOAbX2Jj8DvrjEqIhKo$UM?J4j8 literal 0 HcmV?d00001 diff --git a/database_document/__pycache__/database.cpython-314.pyc b/database_document/__pycache__/database.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94d1445b295f12c01d671671b5b657778ab27adc GIT binary patch literal 2077 zcmah~O>7fK6rT0ZdUyRF$GC(-2+*n)BB7810b1%LPVF{v!rEz|ve~ZfO;}ia!|XU< zxL6gcT1xq8E;(}OA)K01)njhuXep6OrbV>srQE2(p@&NC+g;nCmC~-Y-@G^P^P8DB zpPvr~`~rgK!0iX>j2od}O{e{MTgK*X2B8MJfJ7#ZQVd~I4&q2Ti8JLQu9Ta&Q!HV7 z&A21&NpXZr@r1X0XIe-JM6hgE+Dp8^ai@JLKk@gXg1-+9ww3_TJN0UN;jOU`iR>V( zZayTqEyvU0gtp~y9Zq;#4&UK)ZOailoQUWx1bs;K6+(diLfEpq00V^xVDN~SbQhRD zROs$}CtBd)Od&B;h>GFk&eq9x6u7NgkXRG}rz?iUNFg@Z=DZ?y#}L^my7r@Z^qKj^ zI7|H{tjI)>lQ8pCnGZe#6^=d`Ftl&69xiX|CYDLL_C}*{Qb2nuUsA_^W*{qDyoici6cI--#b-$J-+j+OaKNc4A&D=Ii)= z%MTQSQ+w^qFQe$`tQQtJ(k1e6uL9*p^-`^(RN>fVy=17>S>4FNZF%RhT9I*Gx~$Zx z8>*mO8#Aw{TGKSSs441ipc9zzl$BYzrjdsz6OT|%lB#k=kt8ZeQbjM%q(U@p=^zms`^zb*wQUaGNDmPG6*QK1P%zcSR;xdNw^pIoA4r< z966bvHWX~+jZ4O*!})fVjQo2lN!5z^q-@kjCr9!iS=kgbDAlfGXQqF?j(0*X3wppX zJ_7g@J?`#pO#H$J?|B~NR=U^uV~ydB(2n~HOQppd%hT(jqm8uf7Z)}&3-^YjAi~U2eW;yr6ySV_c5A}j* z^Q{JlI_+>C?Lr@-YpB8W!Z-z2`|oF~af*;`FUtK7OGT%gyOGMg&LYNl)Bl`BUjNdZEwZQ8Q?_0TZlDd{cHR{gWU-3dQkh$y*joY{Gj36@Wj?UI~%SSEXSPwjJ!ZFyx8Gp IBGy6w1}Jatw*UYD literal 0 HcmV?d00001 diff --git a/database_document/database.py b/database_document/database.py new file mode 100644 index 0000000..b426598 --- /dev/null +++ b/database_document/database.py @@ -0,0 +1,42 @@ +from sqlalchemy import create_engine, Column, Integer, String, Text, TIMESTAMP +from sqlalchemy.orm import declarative_base, sessionmaker +from datetime import datetime +import os +from dotenv import load_dotenv + +# .env 파일 로드 +load_dotenv() + +# 환경변수에서 DB 정보 가져오기 +DB_USER = os.getenv("DB_USER") +DB_PASSWORD = os.getenv("DB_PASSWORD") +DB_HOST = os.getenv("DB_HOST") +DB_PORT = os.getenv("DB_PORT") +DB_NAME = os.getenv("DB_NAME") + +# MySQL 연결 주소 만들기 (pymysql) +SQLALCHEMY_DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4" + +# DB 엔진 및 세션 생성 +engine = create_engine(SQLALCHEMY_DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + +# 테이블 구조 정의 (MySQL과 1:1 매칭) +class Document(Base): + __tablename__ = "docsinfos" + + id = Column(Integer, primary_key=True, index=True) + file_name = Column(String(255), nullable=False) + file_type = Column(String(50)) + s3_url = Column(String(1000)) + extracted_text = Column(Text) + created_at = Column(TIMESTAMP, default=datetime.utcnow) + +# DB 연결 세션을 가져오는 함수 (FastAPI에서 사용) +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/database_document/schema.sql b/database_document/schema.sql new file mode 100644 index 0000000..73b945a --- /dev/null +++ b/database_document/schema.sql @@ -0,0 +1,15 @@ +-- 1. 새로운 데이터베이스(easydoc_db) 생성 (한글 깨짐 방지를 위해 utf8mb4 설정) +CREATE DATABASE IF NOT EXISTS easydoc_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- 2. 해당 데이터베이스 사용 선언 +USE easydoc_db; + +-- 3. 통합된 문서를 담을 docsinfos 테이블 생성 +CREATE TABLE IF NOT EXISTS docsinfos ( + id INT AUTO_INCREMENT PRIMARY KEY, -- 고유 번호 (자동 증가) + file_name VARCHAR(255) NOT NULL, -- 파일명 (예: 행정기본법.pdf) + file_type VARCHAR(50), -- 확장자 (예: pdf, png) + s3_url VARCHAR(1000), -- S3에 저장된 경로 + extracted_text LONGTEXT, -- 추출된 아주 긴 텍스트 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 업로드된 시간 (자동 기록) +); \ No newline at end of file From f2b2094ef53ddc92a6074445e1fac424453d63f8 Mon Sep 17 00:00:00 2001 From: Sangjoon PARK Date: Tue, 3 Mar 2026 20:26:17 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=EC=B5=9C=EA=B7=BC=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EA=B0=B1=EC=8B=A0=20+=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=ED=95=9C=20=EB=AC=B8=EC=84=9C=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 최근 문서 목록에 docsinfos에 저장된 문서들을 표시 & 새 문서 업로드 시 목록 갱신 - 사용자가 최근 문서 목록에서 문서를 클릭하면 docsinfos에 저장되어있는 그 문서의 추출된 텍스트를 불러옴 --- EasyDOC-frontend/src/pages/Viewer.jsx | 136 +++++++++++++++----------- backend-OCR/ocr_server.py | 36 ++++++- 2 files changed, 110 insertions(+), 62 deletions(-) diff --git a/EasyDOC-frontend/src/pages/Viewer.jsx b/EasyDOC-frontend/src/pages/Viewer.jsx index 449bbd2..02186fb 100644 --- a/EasyDOC-frontend/src/pages/Viewer.jsx +++ b/EasyDOC-frontend/src/pages/Viewer.jsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from "react"; +import React, { useState, useRef, useEffect } from "react"; import axios from "axios"; import { Upload, Clock, FileText, Settings, X, User, BookOpen, ChevronRight, Lightbulb } from 'lucide-react'; import "./viewer.css"; @@ -28,47 +28,65 @@ function LogoIcon() { } export default function Viewer({ parsedData, ocrData }) { - // 요약 박스 표시 여부 상태 (기본값: true) - const [showSummary, setShowSummary] = useState(true); + // ================= 상태 관리 ================= + // 1. UI 관련 상태 + const [showSummary, setShowSummary] = useState(true); // 요약 박스 표시 여부 상태 (기본값: true) + const [pdfUrl, setPdfUrl] = useState("/sample.pdf"); // 현재 보고 있는 PDF 경로 상태 (기본값: 샘플) - // 현재 보고 있는 PDF 경로 상태 (기본값: 샘플) - const [pdfUrl, setPdfUrl] = useState("/sample.pdf"); + // 2. 데이터 관련 상태 (백엔드 연동) + const [documents, setDocuments] = useState([]); // 사이드바 문서 목록 + const [selectedDoc, setSelectedDoc] = useState(null); // 현재 선택된 문서 상세 정보 + const [loading, setIsLoading] = useState(false); // 로딩 상태 - // 최근 문서 목록 상태 - const [recentDocs, setRecentDocs] = useState([ - {id: 1, title: '행정기본법.pdf', date: '2024.11.14'}, - {id: 2, title: '조세특례제한법.pdf', date: '2024.11.13'}, - {id: 3, title: '도시및주거환경지정비법.pdf', date: '2024.11.13'}, - {id: 4, title: '건축법시행령.pdf', date: '2024.11.12'}, - ]); - - // 파일 선택을 위한 ref + // 3. 파일 업로드 관련 Refs const fileInputRef = useRef(null); + const API_GATEWAY_URL = "https://28d37e8xg3.execute-api.ap-northeast-2.amazonaws.com/upload-url"; // AWS API Gateway 주소 + + // ================= API 연동 ================= + // 1. 화면이 켜지면 DB에서 문서 목록 가져오기 + useEffect(() => { + fetchDocuments(); + }, []); - // AWS API Gateway 주소 - const API_GATEWAY_URL = "https://28d37e8xg3.execute-api.ap-northeast-2.amazonaws.com/upload-url"; + const fetchDocuments = async () => { + try { + const response = await axios.get('http://localhost:8001/api/documents'); + setDocuments(response.data); + } catch (error) { + console.error("문서 목록 로딩 실패:", error); + } + }; - // 버튼 클릭 시 숨겨진 input 실행 + // 2. 사이드바에서 문서 클릭 시 상세 내용 가져오기 + const handleDocClick = async (id) => { + try { + setIsLoading(true); + const response = await axios.get(`http://localhost:8001/api/documents/${id}`); + setSelectedDoc(response.data); // 선택된 문서 상태 업데이트 + setPdfUrl(null); // 텍스트를 보여주기 위해 PDF 뷰어는 숨김 처리 + } catch (error) { + console.error("문서 상세 로딩 실패:", error); + alert("문서 내용을 불러올 수 없습니다."); + } finally { + setIsLoading(false); + } + }; + + // 3. 파일 선택 버튼 핸들러 const handleUploadBtnClick = () => { console.log("버튼 클릭됨! fileInputRef 상태:", fileInputRef.current); //디버깅용 fileInputRef.current?.click(); }; - // 파일 선택 시 업로드 로직 -// 상단에 state 추가 (기존 state들 근처에) -const [parsedText, setParsedText] = useState(parsedData?.text || ""); -const [ocrText, setOcrText] = useState(ocrData ? (ocrData.text || ocrData) : ""); // OCR -const [isLoading, setIsLoading] = useState(false); - - -// handleFileChange 수정 +// 4. 파일 업로드 및 처리 로직 const handleFileChange = async (e) => { const file = e.target.files[0]; if (!file) return; const objectUrl = URL.createObjectURL(file); setPdfUrl(objectUrl); - setIsLoading(true); // 로딩 시작 + setSelectedDoc(null); // 기존 선택된 텍스트 초기화 + setIsLoading(true); // 로딩 시작 try { // AWS Lambda에 업로드 URL 요청 @@ -93,44 +111,37 @@ const handleFileChange = async (e) => { console.log("4. 업로드 성공!"); // 업로드된 파일형에 따라 파싱 혹은 OCR 실행 - + let resultData = null; if (file.type.startsWith("image/")) { // 파일이 이미지일 때 -> OCR 서버 (8001번) 요청 console.log("5. OCR 서버에 분석 요청..."); - const ocrResponse = await axios.get( + const response = await axios.get( `http://localhost:8001/ocr/s3/${encodeURIComponent(file.name)}` ); - console.log("6. OCR 결과 도착!", ocrResponse.data); - setOcrText(ocrResponse.data.text || ocrResponse.data); - setParsedText(""); // 문서 파싱 결과는 비움 + console.log("6. OCR 결과 도착!", response.data); + resultData = response.data; } else { // 파일이 문서일 때 -> 파싱 서버 (8000번) 요청 // 👇 파싱 요청 추가 console.log("5. 파싱 요청 중..."); - const parseResponse = await axios.get( + const response = await axios.get( `http://localhost:8000/parse/s3/${encodeURIComponent(file.name)}` ); - console.log("6. 파싱 완료!", parseResponse.data); - setParsedText(parseResponse.data.text); // 파싱 결과 저장 - setOcrText(""); // OCR 결과는 비움 + console.log("6. 파싱 완료!", response.data); + resultData = response.data; } - // 최근 문서 목록 업데이트 - const newDoc = { - id: Date.now(), - title: file.name, - date: new Date().toLocaleDateString("ko-KR", { - year: "numeric", - month:"2-digit", - day: "2-digit", - }) - .replace(/\. /g, ".") - .replace(".", "") - }; + // 결과 표시 + // 백엔드에서 받은 {id, text} 형식을 selectedDoc 상태에 맞춰서 넣음 + setSelectedDoc({ + id: resultData.id, + file_name: file.name, + text: resultData.text + }); - const filteredDocs = recentDocs.filter((doc) => doc.title != file.name); - setRecentDocs([newDoc, ...filteredDocs].slice(0, 10)); + // 목록 새로고침 (방금 올린 파일을 최근 문서 목록에 등록) + fetchDocuments(); } catch (error) { console.error("파일 처리 오류:", error); @@ -143,6 +154,12 @@ const handleFileChange = async (e) => { } }; + // 날짜 형식 변환 함수 (2024-11-14 -> 2024.11.14) + const formatDate = (dateString) => { + if (!dateString) return ""; + return dateString.substring(0, 10).replace(/-/g, '.'); + }; + return (
@@ -168,22 +185,26 @@ const handleFileChange = async (e) => { 문서 업로드 - {/* 최근 문서 목록 */} + {/* 최근 문서 목록 (DB 데이터 연동 수정) */}
최근 문서
    - {recentDocs.map((doc) => ( -
  • + {documents.map((doc) => ( +
  • handleDocClick(doc.id)} + >
    - {doc.title} - {doc.date} + {doc.file_name} + {formatDate(doc.created_at)}
    @@ -211,11 +232,11 @@ const handleFileChange = async (e) => {
-
+
{/* 텍스트가 있으면 표시, 없으면 PDF 표시 */} - {(ocrText || parsedText) ? ( + {selectedDoc ? (
-
{ocrText ? ocrText: parsedText}
+
{selectedDoc.text}
) : (