Spaces:
Sleeping
Sleeping
Commit
·
3f85869
0
Parent(s):
"feat: Phase 1 완료 - 데이터베이스 및 백엔드 기초 구축
Browse files- SQLAlchemy ORM 모델 (12개 테이블)
- Pydantic 스키마 (요청/응답 검증)
- CRUD 헬퍼 함수
- FastAPI 메인 애플리케이션
- 데이터베이스 연결 설정
- 환경 변수 템플릿 (.env.example)
- 팀 협업 가이드 (SETUP.md)
- .env.example +55 -0
- README.md +263 -0
- app/__init__.py +8 -0
- app/crud.py +537 -0
- app/database.py +173 -0
- app/main.py +209 -0
- app/models.py +367 -0
- app/routers/__init__.py +17 -0
- app/schemas.py +457 -0
- requirements.txt +69 -0
- start_server.bat +48 -0
.env.example
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================================
|
| 2 |
+
# SmartEyeSsen Backend Environment Variables
|
| 3 |
+
# ============================================================================
|
| 4 |
+
# 이 파일을 복사하여 .env 파일을 생성하고 실제 값으로 변경하세요.
|
| 5 |
+
# 명령어: cp .env.example .env
|
| 6 |
+
|
| 7 |
+
# ============================================================================
|
| 8 |
+
# 데이터베이스 설정 (MySQL)
|
| 9 |
+
# ============================================================================
|
| 10 |
+
DB_HOST=localhost
|
| 11 |
+
DB_PORT=3306
|
| 12 |
+
DB_USER=root
|
| 13 |
+
DB_PASSWORD=your_password_here
|
| 14 |
+
DB_NAME=smarteyessen_db
|
| 15 |
+
|
| 16 |
+
# 데이터베이스 URL (자동 생성, 수동 변경 불필요)
|
| 17 |
+
DATABASE_URL=mysql+pymysql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
|
| 18 |
+
|
| 19 |
+
# ============================================================================
|
| 20 |
+
# FastAPI 설정
|
| 21 |
+
# ============================================================================
|
| 22 |
+
API_HOST=0.0.0.0
|
| 23 |
+
API_PORT=8000
|
| 24 |
+
API_RELOAD=True
|
| 25 |
+
API_LOG_LEVEL=info
|
| 26 |
+
|
| 27 |
+
# ============================================================================
|
| 28 |
+
# OpenAI API 설정 (선택사항)
|
| 29 |
+
# ============================================================================
|
| 30 |
+
OPENAI_API_KEY=your_openai_api_key_here
|
| 31 |
+
|
| 32 |
+
# ============================================================================
|
| 33 |
+
# 파일 업로드 설정
|
| 34 |
+
# ============================================================================
|
| 35 |
+
UPLOAD_DIR=uploads
|
| 36 |
+
MAX_FILE_SIZE=10485760 # 10MB (바이트 단위)
|
| 37 |
+
ALLOWED_EXTENSIONS=jpg,jpeg,png,pdf
|
| 38 |
+
|
| 39 |
+
# ============================================================================
|
| 40 |
+
# 보안 설정 (JWT - 선택사항)
|
| 41 |
+
# ============================================================================
|
| 42 |
+
SECRET_KEY=your_secret_key_here_change_in_production
|
| 43 |
+
ALGORITHM=HS256
|
| 44 |
+
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
| 45 |
+
|
| 46 |
+
# ============================================================================
|
| 47 |
+
# CORS 설정
|
| 48 |
+
# ============================================================================
|
| 49 |
+
CORS_ORIGINS=http://localhost:3000,http://localhost:8080
|
| 50 |
+
|
| 51 |
+
# ============================================================================
|
| 52 |
+
# 기타 설정
|
| 53 |
+
# ============================================================================
|
| 54 |
+
DEBUG=True
|
| 55 |
+
ENVIRONMENT=development
|
README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SmartEyeSsen Backend
|
| 2 |
+
|
| 3 |
+
시각장애 학생을 위한 AI 기반 학습 자료 분석 시스템 - 백엔드 서버
|
| 4 |
+
|
| 5 |
+
## 🚀 빠른 시작
|
| 6 |
+
|
| 7 |
+
### 1. 환경 설정
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
# .env 파일 생성
|
| 11 |
+
cp .env.example .env
|
| 12 |
+
|
| 13 |
+
# .env 파일 편집 (DB 비밀번호 등 설정)
|
| 14 |
+
notepad .env
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
### 2. 가상환경 생성 및 활성화
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
# 가상환경 생성
|
| 21 |
+
python -m venv venv
|
| 22 |
+
|
| 23 |
+
# 가상환경 활성화 (Windows)
|
| 24 |
+
venv\Scripts\activate
|
| 25 |
+
|
| 26 |
+
# 가상환경 활성화 (Linux/Mac)
|
| 27 |
+
source venv/bin/activate
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
### 3. 의존성 설치
|
| 31 |
+
|
| 32 |
+
```bash
|
| 33 |
+
pip install -r requirements.txt
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
### 4. 데이터베이스 설정
|
| 37 |
+
|
| 38 |
+
MySQL 8.0 이상 필요:
|
| 39 |
+
|
| 40 |
+
```sql
|
| 41 |
+
-- MySQL에 데이터베이스 생성
|
| 42 |
+
CREATE DATABASE smarteyessen_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
| 43 |
+
|
| 44 |
+
-- 사용자 권한 설정 (선택사항)
|
| 45 |
+
GRANT ALL PRIVILEGES ON smarteyessen_db.* TO 'your_user'@'localhost';
|
| 46 |
+
FLUSH PRIVILEGES;
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
### 5. 서버 실행
|
| 50 |
+
|
| 51 |
+
**방법 1: 배치 파일 사용 (Windows)**
|
| 52 |
+
```bash
|
| 53 |
+
start_server.bat
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
**방법 2: 직접 실행**
|
| 57 |
+
```bash
|
| 58 |
+
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
**방법 3: Python 스크립트 실행**
|
| 62 |
+
```bash
|
| 63 |
+
python -m app.main
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
### 6. API 문서 확인
|
| 67 |
+
|
| 68 |
+
서버 실행 후 브라우저에서 접속:
|
| 69 |
+
|
| 70 |
+
- **Swagger UI**: http://localhost:8000/docs
|
| 71 |
+
- **ReDoc**: http://localhost:8000/redoc
|
| 72 |
+
- **Health Check**: http://localhost:8000/health
|
| 73 |
+
|
| 74 |
+
---
|
| 75 |
+
|
| 76 |
+
## 📁 프로젝트 구조
|
| 77 |
+
|
| 78 |
+
```
|
| 79 |
+
Backend/
|
| 80 |
+
├── .env.example # 환경 변수 템플릿
|
| 81 |
+
├── .env # 환경 변수 (git에서 제외)
|
| 82 |
+
├── requirements.txt # Python 의존성
|
| 83 |
+
├── start_server.bat # 서버 실행 스크립트 (Windows)
|
| 84 |
+
├── README.md # 이 파일
|
| 85 |
+
│
|
| 86 |
+
├── app/ # 메인 애플리케이션
|
| 87 |
+
│ ├── __init__.py # 패키지 초기화
|
| 88 |
+
│ ├── main.py # FastAPI 앱 설정
|
| 89 |
+
│ ├── database.py # 데이터베이스 연결
|
| 90 |
+
│ ├── models.py # SQLAlchemy ORM 모델
|
| 91 |
+
│ ├── schemas.py # Pydantic 스키마
|
| 92 |
+
│ ├── crud.py # CRUD 헬퍼 함수
|
| 93 |
+
│ │
|
| 94 |
+
│ └── routers/ # API 라우터
|
| 95 |
+
│ └── __init__.py
|
| 96 |
+
│
|
| 97 |
+
└── uploads/ # 업로드된 파일 저장
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
## 🗄️ 데이터베이스 스키마
|
| 103 |
+
|
| 104 |
+
### 12개 테이블 구조
|
| 105 |
+
|
| 106 |
+
| # | 테이블명 | 설명 |
|
| 107 |
+
|---|---------|------|
|
| 108 |
+
| 1 | `users` | 사용자 정보 |
|
| 109 |
+
| 2 | `document_types` | 문서 유형 (worksheet/document) |
|
| 110 |
+
| 3 | `projects` | 프로젝트 (다중 페이지 문서) |
|
| 111 |
+
| 4 | `pages` | 페이지 정보 |
|
| 112 |
+
| 5 | `layout_elements` | 레이아웃 요소 (DocLayout-YOLO) |
|
| 113 |
+
| 6 | `text_contents` | 텍스트 내용 (OCR 결과) |
|
| 114 |
+
| 7 | `ai_descriptions` | AI 생성 설명 (figure/table) |
|
| 115 |
+
| 8 | `question_groups` | 문제 그룹 (worksheet 전용) |
|
| 116 |
+
| 9 | `question_elements` | 문제 요소 (worksheet 전용) |
|
| 117 |
+
| 10 | `text_versions` | 텍스트 버전 관리 |
|
| 118 |
+
| 11 | `formatting_rules` | 서식 규칙 |
|
| 119 |
+
| 12 | `combined_results` | 통합 결과 (최종 문서) |
|
| 120 |
+
|
| 121 |
+
### 주요 관계
|
| 122 |
+
|
| 123 |
+
- **User (1) → (N) Project**: 사용자는 여러 프로젝트 소유
|
| 124 |
+
- **Project (1) → (N) Page**: 프로젝트는 여러 페이지 포함
|
| 125 |
+
- **Page (1) → (N) LayoutElement**: 페이지는 여러 레이아웃 요소 포함
|
| 126 |
+
- **LayoutElement (1) → (1) TextContent**: 1:1 관계
|
| 127 |
+
- **LayoutElement (1) → (1) AIDescription**: 1:1 관계
|
| 128 |
+
- **TextContent (1) → (N) TextVersion**: 버전 관리
|
| 129 |
+
|
| 130 |
+
---
|
| 131 |
+
|
| 132 |
+
## 🔧 API 엔드포인트 (Phase 2에서 추가 예정)
|
| 133 |
+
|
| 134 |
+
### 사용자 관리
|
| 135 |
+
- `POST /api/v1/users` - 사용자 생성
|
| 136 |
+
- `GET /api/v1/users/{user_id}` - 사용자 조회
|
| 137 |
+
- `PUT /api/v1/users/{user_id}` - 사용자 수정
|
| 138 |
+
- `DELETE /api/v1/users/{user_id}` - 사용자 삭제
|
| 139 |
+
|
| 140 |
+
### 프로젝트 관리
|
| 141 |
+
- `POST /api/v1/projects` - 프로젝트 생성
|
| 142 |
+
- `GET /api/v1/projects` - 프로젝트 목록
|
| 143 |
+
- `GET /api/v1/projects/{project_id}` - 프로젝트 상세
|
| 144 |
+
- `PUT /api/v1/projects/{project_id}` - 프로젝트 수정
|
| 145 |
+
- `DELETE /api/v1/projects/{project_id}` - 프로젝트 삭제
|
| 146 |
+
|
| 147 |
+
### 페이지 관리
|
| 148 |
+
- `POST /api/v1/pages` - 페이지 생성 (이미지 업로드)
|
| 149 |
+
- `GET /api/v1/pages/{page_id}` - 페이지 조회
|
| 150 |
+
- `DELETE /api/v1/pages/{page_id}` - 페이지 삭제
|
| 151 |
+
|
| 152 |
+
### 레이아웃 분석
|
| 153 |
+
- `POST /api/v1/analyze/layout` - 레이아웃 분석 (DocLayout-YOLO)
|
| 154 |
+
- `POST /api/v1/analyze/ocr` - OCR 실행 (PaddleOCR)
|
| 155 |
+
- `POST /api/v1/analyze/describe` - AI 설명 생성 (GPT-4o-mini)
|
| 156 |
+
|
| 157 |
+
### 텍스트 편집
|
| 158 |
+
- `PUT /api/v1/text/{content_id}` - 텍스트 수정
|
| 159 |
+
- `GET /api/v1/text/{content_id}/versions` - 버전 히스토리
|
| 160 |
+
|
| 161 |
+
### 문서 생성
|
| 162 |
+
- `POST /api/v1/export/docx` - DOCX 문서 생성
|
| 163 |
+
- `POST /api/v1/export/pdf` - PDF 문서 생성
|
| 164 |
+
|
| 165 |
+
---
|
| 166 |
+
|
| 167 |
+
## 🧪 개발 도구
|
| 168 |
+
|
| 169 |
+
### 데이터베이스 연결 테스트
|
| 170 |
+
|
| 171 |
+
```bash
|
| 172 |
+
python app/database.py
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
### 데이터베이스 마이그레이션 (Alembic)
|
| 176 |
+
|
| 177 |
+
```bash
|
| 178 |
+
# 초기화
|
| 179 |
+
alembic init alembic
|
| 180 |
+
|
| 181 |
+
# 마이그레���션 파일 생성
|
| 182 |
+
alembic revision --autogenerate -m "Initial migration"
|
| 183 |
+
|
| 184 |
+
# 마이그레이션 적용
|
| 185 |
+
alembic upgrade head
|
| 186 |
+
|
| 187 |
+
# 롤백
|
| 188 |
+
alembic downgrade -1
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
---
|
| 192 |
+
|
| 193 |
+
## 📝 환경 변수 설명
|
| 194 |
+
|
| 195 |
+
| 변수명 | 설명 | 기본값 |
|
| 196 |
+
|--------|------|--------|
|
| 197 |
+
| `DB_HOST` | MySQL 호스트 | `localhost` |
|
| 198 |
+
| `DB_PORT` | MySQL 포트 | `3306` |
|
| 199 |
+
| `DB_USER` | MySQL 사용자명 | `root` |
|
| 200 |
+
| `DB_PASSWORD` | MySQL 비밀번호 | - |
|
| 201 |
+
| `DB_NAME` | 데이터베이스 이름 | `smarteyessen_db` |
|
| 202 |
+
| `API_HOST` | API 서버 호스트 | `0.0.0.0` |
|
| 203 |
+
| `API_PORT` | API 서버 포트 | `8000` |
|
| 204 |
+
| `CORS_ORIGINS` | CORS 허용 출처 | `http://localhost:3000` |
|
| 205 |
+
| `OPENAI_API_KEY` | OpenAI API 키 | - |
|
| 206 |
+
| `ENVIRONMENT` | 환경 (development/production) | `development` |
|
| 207 |
+
|
| 208 |
+
---
|
| 209 |
+
|
| 210 |
+
## 🐛 문제 해결
|
| 211 |
+
|
| 212 |
+
### 1. 데이터베이스 연결 실패
|
| 213 |
+
|
| 214 |
+
```bash
|
| 215 |
+
# MySQL 서비스 확인
|
| 216 |
+
net start MySQL80
|
| 217 |
+
|
| 218 |
+
# .env 파일의 DB 설정 확인
|
| 219 |
+
DB_PASSWORD=your_actual_password
|
| 220 |
+
```
|
| 221 |
+
|
| 222 |
+
### 2. 포트 충돌
|
| 223 |
+
|
| 224 |
+
```bash
|
| 225 |
+
# 다른 포트로 실행
|
| 226 |
+
uvicorn app.main:app --reload --port 8001
|
| 227 |
+
```
|
| 228 |
+
|
| 229 |
+
### 3. 의존성 설치 오류
|
| 230 |
+
|
| 231 |
+
```bash
|
| 232 |
+
# pip 업그레이드
|
| 233 |
+
python -m pip install --upgrade pip
|
| 234 |
+
|
| 235 |
+
# 캐시 삭제 후 재설치
|
| 236 |
+
pip cache purge
|
| 237 |
+
pip install -r requirements.txt
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
---
|
| 241 |
+
|
| 242 |
+
## 📚 참고 자료
|
| 243 |
+
|
| 244 |
+
- [FastAPI 공식 문서](https://fastapi.tiangolo.com/)
|
| 245 |
+
- [SQLAlchemy 공식 문서](https://www.sqlalchemy.org/)
|
| 246 |
+
- [Pydantic 공식 문서](https://docs.pydantic.dev/)
|
| 247 |
+
- [MySQL 8.0 문서](https://dev.mysql.com/doc/refman/8.0/en/)
|
| 248 |
+
|
| 249 |
+
---
|
| 250 |
+
|
| 251 |
+
## 📄 라이선스
|
| 252 |
+
|
| 253 |
+
MIT License
|
| 254 |
+
|
| 255 |
+
---
|
| 256 |
+
|
| 257 |
+
## 👥 개발팀
|
| 258 |
+
|
| 259 |
+
SmartEyeSsen Team - 시각장애 학생을 위한 AI 학습 도구
|
| 260 |
+
|
| 261 |
+
---
|
| 262 |
+
|
| 263 |
+
**Phase 1 완료**: 데이터베이스 및 백엔드 기반 구축 ✅
|
app/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SmartEyeSsen Backend - Application Package
|
| 3 |
+
==========================================
|
| 4 |
+
FastAPI 백엔드 애플리케이션 패키지 초기화
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
__version__ = "1.0.0"
|
| 8 |
+
__author__ = "SmartEyeSsen Team"
|
app/crud.py
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SmartEyeSsen Backend - CRUD Helper Functions
|
| 3 |
+
=============================================
|
| 4 |
+
데이터베이스 CRUD 작업을 위한 헬퍼 함수
|
| 5 |
+
|
| 6 |
+
주요 기능:
|
| 7 |
+
- Create: 새 레코드 생성
|
| 8 |
+
- Read: 단일/다중 레코드 조회
|
| 9 |
+
- Update: 기존 레코드 수정
|
| 10 |
+
- Delete: 레코드 삭제
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from sqlalchemy.orm import Session, joinedload
|
| 14 |
+
from sqlalchemy import desc, asc
|
| 15 |
+
from typing import Optional, List, Type, TypeVar
|
| 16 |
+
from . import models, schemas
|
| 17 |
+
from passlib.context import CryptContext
|
| 18 |
+
|
| 19 |
+
# 비밀번호 해싱 설정
|
| 20 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 21 |
+
|
| 22 |
+
# TypeVar for generic CRUD operations
|
| 23 |
+
ModelType = TypeVar("ModelType")
|
| 24 |
+
CreateSchemaType = TypeVar("CreateSchemaType")
|
| 25 |
+
UpdateSchemaType = TypeVar("UpdateSchemaType")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# ============================================================================
|
| 29 |
+
# 1. User CRUD
|
| 30 |
+
# ============================================================================
|
| 31 |
+
def get_user(db: Session, user_id: int) -> Optional[models.User]:
|
| 32 |
+
"""사용자 ID로 조회"""
|
| 33 |
+
return db.query(models.User).filter(models.User.user_id == user_id).first()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def get_user_by_username(db: Session, username: str) -> Optional[models.User]:
|
| 37 |
+
"""사용자명으로 조회"""
|
| 38 |
+
return db.query(models.User).filter(models.User.username == username).first()
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def get_user_by_email(db: Session, email: str) -> Optional[models.User]:
|
| 42 |
+
"""이메일로 조회"""
|
| 43 |
+
return db.query(models.User).filter(models.User.email == email).first()
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def get_users(db: Session, skip: int = 0, limit: int = 100) -> List[models.User]:
|
| 47 |
+
"""사용자 목록 조회"""
|
| 48 |
+
return db.query(models.User).offset(skip).limit(limit).all()
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def create_user(db: Session, user: schemas.UserCreate) -> models.User:
|
| 52 |
+
"""사용자 생성"""
|
| 53 |
+
hashed_password = pwd_context.hash(user.password)
|
| 54 |
+
db_user = models.User(
|
| 55 |
+
username=user.username,
|
| 56 |
+
email=user.email,
|
| 57 |
+
password_hash=hashed_password
|
| 58 |
+
)
|
| 59 |
+
db.add(db_user)
|
| 60 |
+
db.commit()
|
| 61 |
+
db.refresh(db_user)
|
| 62 |
+
return db_user
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def update_user(db: Session, user_id: int, user_update: schemas.UserUpdate) -> Optional[models.User]:
|
| 66 |
+
"""사용자 정보 수정"""
|
| 67 |
+
db_user = get_user(db, user_id)
|
| 68 |
+
if not db_user:
|
| 69 |
+
return None
|
| 70 |
+
|
| 71 |
+
update_data = user_update.model_dump(exclude_unset=True)
|
| 72 |
+
if "password" in update_data:
|
| 73 |
+
update_data["password_hash"] = pwd_context.hash(update_data.pop("password"))
|
| 74 |
+
|
| 75 |
+
for key, value in update_data.items():
|
| 76 |
+
setattr(db_user, key, value)
|
| 77 |
+
|
| 78 |
+
db.commit()
|
| 79 |
+
db.refresh(db_user)
|
| 80 |
+
return db_user
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def delete_user(db: Session, user_id: int) -> bool:
|
| 84 |
+
"""사용자 삭제"""
|
| 85 |
+
db_user = get_user(db, user_id)
|
| 86 |
+
if not db_user:
|
| 87 |
+
return False
|
| 88 |
+
db.delete(db_user)
|
| 89 |
+
db.commit()
|
| 90 |
+
return True
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ============================================================================
|
| 94 |
+
# 2. DocumentType CRUD
|
| 95 |
+
# ============================================================================
|
| 96 |
+
def get_document_type(db: Session, type_id: int) -> Optional[models.DocumentType]:
|
| 97 |
+
"""문서 유형 ID로 조회"""
|
| 98 |
+
return db.query(models.DocumentType).filter(models.DocumentType.type_id == type_id).first()
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def get_document_type_by_name(db: Session, type_name: str) -> Optional[models.DocumentType]:
|
| 102 |
+
"""문서 유형명으로 조회"""
|
| 103 |
+
return db.query(models.DocumentType).filter(models.DocumentType.type_name == type_name).first()
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def get_document_types(db: Session) -> List[models.DocumentType]:
|
| 107 |
+
"""모든 문서 유형 조회"""
|
| 108 |
+
return db.query(models.DocumentType).all()
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def create_document_type(db: Session, doc_type: schemas.DocumentTypeCreate) -> models.DocumentType:
|
| 112 |
+
"""문서 유형 생성"""
|
| 113 |
+
db_doc_type = models.DocumentType(**doc_type.model_dump())
|
| 114 |
+
db.add(db_doc_type)
|
| 115 |
+
db.commit()
|
| 116 |
+
db.refresh(db_doc_type)
|
| 117 |
+
return db_doc_type
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
# ============================================================================
|
| 121 |
+
# 3. Project CRUD
|
| 122 |
+
# ============================================================================
|
| 123 |
+
def get_project(db: Session, project_id: int) -> Optional[models.Project]:
|
| 124 |
+
"""프로젝트 ID로 조회"""
|
| 125 |
+
return db.query(models.Project).filter(models.Project.project_id == project_id).first()
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def get_project_with_pages(db: Session, project_id: int) -> Optional[models.Project]:
|
| 129 |
+
"""페이지 포함 프로젝트 조회"""
|
| 130 |
+
return db.query(models.Project).options(
|
| 131 |
+
joinedload(models.Project.pages)
|
| 132 |
+
).filter(models.Project.project_id == project_id).first()
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def get_projects_by_user(
|
| 136 |
+
db: Session,
|
| 137 |
+
user_id: int,
|
| 138 |
+
skip: int = 0,
|
| 139 |
+
limit: int = 100
|
| 140 |
+
) -> List[models.Project]:
|
| 141 |
+
"""사용자별 프로젝트 목록 조회"""
|
| 142 |
+
return db.query(models.Project).filter(
|
| 143 |
+
models.Project.user_id == user_id
|
| 144 |
+
).order_by(desc(models.Project.created_at)).offset(skip).limit(limit).all()
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def create_project(db: Session, project: schemas.ProjectCreate, user_id: int) -> models.Project:
|
| 148 |
+
"""프로젝트 생성"""
|
| 149 |
+
db_project = models.Project(**project.model_dump(), user_id=user_id)
|
| 150 |
+
db.add(db_project)
|
| 151 |
+
db.commit()
|
| 152 |
+
db.refresh(db_project)
|
| 153 |
+
return db_project
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def update_project(
|
| 157 |
+
db: Session,
|
| 158 |
+
project_id: int,
|
| 159 |
+
project_update: schemas.ProjectUpdate
|
| 160 |
+
) -> Optional[models.Project]:
|
| 161 |
+
"""프로젝트 수정"""
|
| 162 |
+
db_project = get_project(db, project_id)
|
| 163 |
+
if not db_project:
|
| 164 |
+
return None
|
| 165 |
+
|
| 166 |
+
update_data = project_update.model_dump(exclude_unset=True)
|
| 167 |
+
for key, value in update_data.items():
|
| 168 |
+
setattr(db_project, key, value)
|
| 169 |
+
|
| 170 |
+
db.commit()
|
| 171 |
+
db.refresh(db_project)
|
| 172 |
+
return db_project
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def delete_project(db: Session, project_id: int) -> bool:
|
| 176 |
+
"""프로젝트 삭제 (CASCADE로 관련 데이터 자동 삭제)"""
|
| 177 |
+
db_project = get_project(db, project_id)
|
| 178 |
+
if not db_project:
|
| 179 |
+
return False
|
| 180 |
+
db.delete(db_project)
|
| 181 |
+
db.commit()
|
| 182 |
+
return True
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
# ============================================================================
|
| 186 |
+
# 4. Page CRUD
|
| 187 |
+
# ============================================================================
|
| 188 |
+
def get_page(db: Session, page_id: int) -> Optional[models.Page]:
|
| 189 |
+
"""페이지 ID로 조회"""
|
| 190 |
+
return db.query(models.Page).filter(models.Page.page_id == page_id).first()
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def get_page_with_elements(db: Session, page_id: int) -> Optional[models.Page]:
|
| 194 |
+
"""레이아웃 요소 포함 페이지 조회"""
|
| 195 |
+
return db.query(models.Page).options(
|
| 196 |
+
joinedload(models.Page.layout_elements)
|
| 197 |
+
).filter(models.Page.page_id == page_id).first()
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def get_pages_by_project(db: Session, project_id: int) -> List[models.Page]:
|
| 201 |
+
"""프로젝트별 페이지 목록 조회 (페이지 번호 순)"""
|
| 202 |
+
return db.query(models.Page).filter(
|
| 203 |
+
models.Page.project_id == project_id
|
| 204 |
+
).order_by(asc(models.Page.page_number)).all()
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def create_page(db: Session, page: schemas.PageCreate) -> models.Page:
|
| 208 |
+
"""페이지 생성"""
|
| 209 |
+
db_page = models.Page(**page.model_dump())
|
| 210 |
+
db.add(db_page)
|
| 211 |
+
db.commit()
|
| 212 |
+
db.refresh(db_page)
|
| 213 |
+
return db_page
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def update_page(db: Session, page_id: int, page_update: schemas.PageUpdate) -> Optional[models.Page]:
|
| 217 |
+
"""페이지 수정"""
|
| 218 |
+
db_page = get_page(db, page_id)
|
| 219 |
+
if not db_page:
|
| 220 |
+
return None
|
| 221 |
+
|
| 222 |
+
update_data = page_update.model_dump(exclude_unset=True)
|
| 223 |
+
for key, value in update_data.items():
|
| 224 |
+
setattr(db_page, key, value)
|
| 225 |
+
|
| 226 |
+
db.commit()
|
| 227 |
+
db.refresh(db_page)
|
| 228 |
+
return db_page
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def delete_page(db: Session, page_id: int) -> bool:
|
| 232 |
+
"""페이지 삭제"""
|
| 233 |
+
db_page = get_page(db, page_id)
|
| 234 |
+
if not db_page:
|
| 235 |
+
return False
|
| 236 |
+
db.delete(db_page)
|
| 237 |
+
db.commit()
|
| 238 |
+
return True
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
# ============================================================================
|
| 242 |
+
# 5. LayoutElement CRUD
|
| 243 |
+
# ============================================================================
|
| 244 |
+
def get_layout_element(db: Session, element_id: int) -> Optional[models.LayoutElement]:
|
| 245 |
+
"""레이아웃 요소 ID로 조회"""
|
| 246 |
+
return db.query(models.LayoutElement).filter(
|
| 247 |
+
models.LayoutElement.element_id == element_id
|
| 248 |
+
).first()
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def get_layout_element_with_content(db: Session, element_id: int) -> Optional[models.LayoutElement]:
|
| 252 |
+
"""텍스트 및 AI 설명 포함 레이아웃 요소 조회"""
|
| 253 |
+
return db.query(models.LayoutElement).options(
|
| 254 |
+
joinedload(models.LayoutElement.text_content),
|
| 255 |
+
joinedload(models.LayoutElement.ai_description)
|
| 256 |
+
).filter(models.LayoutElement.element_id == element_id).first()
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def get_layout_elements_by_page(db: Session, page_id: int) -> List[models.LayoutElement]:
|
| 260 |
+
"""페이지별 레이아웃 요소 목록 조회 (Y 좌표 순)"""
|
| 261 |
+
return db.query(models.LayoutElement).filter(
|
| 262 |
+
models.LayoutElement.page_id == page_id
|
| 263 |
+
).order_by(asc(models.LayoutElement.y_position)).all()
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def create_layout_element(db: Session, element: schemas.LayoutElementCreate) -> models.LayoutElement:
|
| 267 |
+
"""레이아웃 요소 생성"""
|
| 268 |
+
db_element = models.LayoutElement(**element.model_dump())
|
| 269 |
+
db.add(db_element)
|
| 270 |
+
db.commit()
|
| 271 |
+
db.refresh(db_element)
|
| 272 |
+
return db_element
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def create_layout_elements_bulk(
|
| 276 |
+
db: Session,
|
| 277 |
+
elements: List[schemas.LayoutElementCreate]
|
| 278 |
+
) -> List[models.LayoutElement]:
|
| 279 |
+
"""레이아웃 요소 일괄 생성"""
|
| 280 |
+
db_elements = [models.LayoutElement(**elem.model_dump()) for elem in elements]
|
| 281 |
+
db.add_all(db_elements)
|
| 282 |
+
db.commit()
|
| 283 |
+
for elem in db_elements:
|
| 284 |
+
db.refresh(elem)
|
| 285 |
+
return db_elements
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def update_layout_element(
|
| 289 |
+
db: Session,
|
| 290 |
+
element_id: int,
|
| 291 |
+
element_update: schemas.LayoutElementUpdate
|
| 292 |
+
) -> Optional[models.LayoutElement]:
|
| 293 |
+
"""레이아웃 요소 수정"""
|
| 294 |
+
db_element = get_layout_element(db, element_id)
|
| 295 |
+
if not db_element:
|
| 296 |
+
return None
|
| 297 |
+
|
| 298 |
+
update_data = element_update.model_dump(exclude_unset=True)
|
| 299 |
+
for key, value in update_data.items():
|
| 300 |
+
setattr(db_element, key, value)
|
| 301 |
+
|
| 302 |
+
db.commit()
|
| 303 |
+
db.refresh(db_element)
|
| 304 |
+
return db_element
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def delete_layout_element(db: Session, element_id: int) -> bool:
|
| 308 |
+
"""레이아웃 요소 삭제"""
|
| 309 |
+
db_element = get_layout_element(db, element_id)
|
| 310 |
+
if not db_element:
|
| 311 |
+
return False
|
| 312 |
+
db.delete(db_element)
|
| 313 |
+
db.commit()
|
| 314 |
+
return True
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
# ============================================================================
|
| 318 |
+
# 6. TextContent CRUD
|
| 319 |
+
# ============================================================================
|
| 320 |
+
def get_text_content(db: Session, content_id: int) -> Optional[models.TextContent]:
|
| 321 |
+
"""텍스트 내용 ID로 조회"""
|
| 322 |
+
return db.query(models.TextContent).filter(
|
| 323 |
+
models.TextContent.content_id == content_id
|
| 324 |
+
).first()
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def get_text_content_by_element(db: Session, element_id: int) -> Optional[models.TextContent]:
|
| 328 |
+
"""요소 ID로 텍스트 내용 조회"""
|
| 329 |
+
return db.query(models.TextContent).filter(
|
| 330 |
+
models.TextContent.element_id == element_id
|
| 331 |
+
).first()
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def create_text_content(db: Session, content: schemas.TextContentCreate) -> models.TextContent:
|
| 335 |
+
"""텍스트 내용 생성"""
|
| 336 |
+
db_content = models.TextContent(**content.model_dump())
|
| 337 |
+
db.add(db_content)
|
| 338 |
+
db.commit()
|
| 339 |
+
db.refresh(db_content)
|
| 340 |
+
return db_content
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def update_text_content(
|
| 344 |
+
db: Session,
|
| 345 |
+
content_id: int,
|
| 346 |
+
content_update: schemas.TextContentUpdate
|
| 347 |
+
) -> Optional[models.TextContent]:
|
| 348 |
+
"""텍스트 내용 수정 (버전 자동 증가)"""
|
| 349 |
+
db_content = get_text_content(db, content_id)
|
| 350 |
+
if not db_content:
|
| 351 |
+
return None
|
| 352 |
+
|
| 353 |
+
# 이전 버전 저장
|
| 354 |
+
if db_content.edited_text:
|
| 355 |
+
version = models.TextVersion(
|
| 356 |
+
content_id=content_id,
|
| 357 |
+
version_number=db_content.version,
|
| 358 |
+
version_text=db_content.edited_text
|
| 359 |
+
)
|
| 360 |
+
db.add(version)
|
| 361 |
+
|
| 362 |
+
# 새 버전으로 업데이트
|
| 363 |
+
update_data = content_update.model_dump(exclude_unset=True)
|
| 364 |
+
for key, value in update_data.items():
|
| 365 |
+
setattr(db_content, key, value)
|
| 366 |
+
|
| 367 |
+
db_content.version += 1
|
| 368 |
+
from datetime import datetime
|
| 369 |
+
db_content.last_edited_at = datetime.now()
|
| 370 |
+
|
| 371 |
+
db.commit()
|
| 372 |
+
db.refresh(db_content)
|
| 373 |
+
return db_content
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
# ============================================================================
|
| 377 |
+
# 7. AIDescription CRUD
|
| 378 |
+
# ============================================================================
|
| 379 |
+
def get_ai_description(db: Session, description_id: int) -> Optional[models.AIDescription]:
|
| 380 |
+
"""AI 설명 ID로 조회"""
|
| 381 |
+
return db.query(models.AIDescription).filter(
|
| 382 |
+
models.AIDescription.description_id == description_id
|
| 383 |
+
).first()
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def get_ai_description_by_element(db: Session, element_id: int) -> Optional[models.AIDescription]:
|
| 387 |
+
"""요소 ID로 AI 설명 조회"""
|
| 388 |
+
return db.query(models.AIDescription).filter(
|
| 389 |
+
models.AIDescription.element_id == element_id
|
| 390 |
+
).first()
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
def create_ai_description(db: Session, description: schemas.AIDescriptionCreate) -> models.AIDescription:
|
| 394 |
+
"""AI 설명 생성"""
|
| 395 |
+
db_description = models.AIDescription(**description.model_dump())
|
| 396 |
+
db.add(db_description)
|
| 397 |
+
db.commit()
|
| 398 |
+
db.refresh(db_description)
|
| 399 |
+
return db_description
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
# ============================================================================
|
| 403 |
+
# 8. QuestionGroup CRUD
|
| 404 |
+
# ============================================================================
|
| 405 |
+
def get_question_group(db: Session, group_id: int) -> Optional[models.QuestionGroup]:
|
| 406 |
+
"""문제 그룹 ID로 조회"""
|
| 407 |
+
return db.query(models.QuestionGroup).filter(
|
| 408 |
+
models.QuestionGroup.group_id == group_id
|
| 409 |
+
).first()
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
def get_question_groups_by_page(db: Session, page_id: int) -> List[models.QuestionGroup]:
|
| 413 |
+
"""페이지별 문제 그룹 목록 조회"""
|
| 414 |
+
return db.query(models.QuestionGroup).filter(
|
| 415 |
+
models.QuestionGroup.page_id == page_id
|
| 416 |
+
).order_by(asc(models.QuestionGroup.group_number)).all()
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def create_question_group(db: Session, group: schemas.QuestionGroupCreate) -> models.QuestionGroup:
|
| 420 |
+
"""문제 그룹 생성"""
|
| 421 |
+
db_group = models.QuestionGroup(**group.model_dump())
|
| 422 |
+
db.add(db_group)
|
| 423 |
+
db.commit()
|
| 424 |
+
db.refresh(db_group)
|
| 425 |
+
return db_group
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
# ============================================================================
|
| 429 |
+
# 9. QuestionElement CRUD
|
| 430 |
+
# ============================================================================
|
| 431 |
+
def get_question_element(db: Session, question_element_id: int) -> Optional[models.QuestionElement]:
|
| 432 |
+
"""문제 요소 ID로 조회"""
|
| 433 |
+
return db.query(models.QuestionElement).filter(
|
| 434 |
+
models.QuestionElement.question_element_id == question_element_id
|
| 435 |
+
).first()
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
def get_question_elements_by_group(db: Session, group_id: int) -> List[models.QuestionElement]:
|
| 439 |
+
"""그룹별 문제 요소 목록 조회"""
|
| 440 |
+
return db.query(models.QuestionElement).filter(
|
| 441 |
+
models.QuestionElement.group_id == group_id
|
| 442 |
+
).order_by(asc(models.QuestionElement.element_order)).all()
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
def create_question_element(db: Session, element: schemas.QuestionElementCreate) -> models.QuestionElement:
|
| 446 |
+
"""문제 요소 생성"""
|
| 447 |
+
db_element = models.QuestionElement(**element.model_dump())
|
| 448 |
+
db.add(db_element)
|
| 449 |
+
db.commit()
|
| 450 |
+
db.refresh(db_element)
|
| 451 |
+
return db_element
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
# ============================================================================
|
| 455 |
+
# 10. FormattingRule CRUD
|
| 456 |
+
# ============================================================================
|
| 457 |
+
def get_formatting_rule(db: Session, rule_id: int) -> Optional[models.FormattingRule]:
|
| 458 |
+
"""서식 규칙 ID로 조회"""
|
| 459 |
+
return db.query(models.FormattingRule).filter(
|
| 460 |
+
models.FormattingRule.rule_id == rule_id
|
| 461 |
+
).first()
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def get_formatting_rule_by_type(db: Session, element_type: str) -> Optional[models.FormattingRule]:
|
| 465 |
+
"""요소 유형으로 서식 규칙 조회"""
|
| 466 |
+
return db.query(models.FormattingRule).filter(
|
| 467 |
+
models.FormattingRule.element_type == element_type
|
| 468 |
+
).first()
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
def get_all_formatting_rules(db: Session) -> List[models.FormattingRule]:
|
| 472 |
+
"""모든 서식 규칙 조회"""
|
| 473 |
+
return db.query(models.FormattingRule).all()
|
| 474 |
+
|
| 475 |
+
|
| 476 |
+
def create_formatting_rule(db: Session, rule: schemas.FormattingRuleCreate) -> models.FormattingRule:
|
| 477 |
+
"""서식 규칙 생성"""
|
| 478 |
+
db_rule = models.FormattingRule(**rule.model_dump())
|
| 479 |
+
db.add(db_rule)
|
| 480 |
+
db.commit()
|
| 481 |
+
db.refresh(db_rule)
|
| 482 |
+
return db_rule
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
def update_formatting_rule(
|
| 486 |
+
db: Session,
|
| 487 |
+
rule_id: int,
|
| 488 |
+
rule_update: schemas.FormattingRuleUpdate
|
| 489 |
+
) -> Optional[models.FormattingRule]:
|
| 490 |
+
"""서식 규칙 수정"""
|
| 491 |
+
db_rule = get_formatting_rule(db, rule_id)
|
| 492 |
+
if not db_rule:
|
| 493 |
+
return None
|
| 494 |
+
|
| 495 |
+
update_data = rule_update.model_dump(exclude_unset=True)
|
| 496 |
+
for key, value in update_data.items():
|
| 497 |
+
setattr(db_rule, key, value)
|
| 498 |
+
|
| 499 |
+
db.commit()
|
| 500 |
+
db.refresh(db_rule)
|
| 501 |
+
return db_rule
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
# ============================================================================
|
| 505 |
+
# 11. CombinedResult CRUD
|
| 506 |
+
# ============================================================================
|
| 507 |
+
def get_combined_result(db: Session, result_id: int) -> Optional[models.CombinedResult]:
|
| 508 |
+
"""통합 결과 ID로 조회"""
|
| 509 |
+
return db.query(models.CombinedResult).filter(
|
| 510 |
+
models.CombinedResult.result_id == result_id
|
| 511 |
+
).first()
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
def get_combined_results_by_project(db: Session, project_id: int) -> List[models.CombinedResult]:
|
| 515 |
+
"""프로젝트별 통합 결과 목록 조회"""
|
| 516 |
+
return db.query(models.CombinedResult).filter(
|
| 517 |
+
models.CombinedResult.project_id == project_id
|
| 518 |
+
).order_by(desc(models.CombinedResult.generated_at)).all()
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
def create_combined_result(db: Session, result: schemas.CombinedResultCreate) -> models.CombinedResult:
|
| 522 |
+
"""통합 결과 생성"""
|
| 523 |
+
db_result = models.CombinedResult(**result.model_dump())
|
| 524 |
+
db.add(db_result)
|
| 525 |
+
db.commit()
|
| 526 |
+
db.refresh(db_result)
|
| 527 |
+
return db_result
|
| 528 |
+
|
| 529 |
+
|
| 530 |
+
def delete_combined_result(db: Session, result_id: int) -> bool:
|
| 531 |
+
"""통합 결과 삭제"""
|
| 532 |
+
db_result = get_combined_result(db, result_id)
|
| 533 |
+
if not db_result:
|
| 534 |
+
return False
|
| 535 |
+
db.delete(db_result)
|
| 536 |
+
db.commit()
|
| 537 |
+
return True
|
app/database.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SmartEyeSsen Backend - Database Connection Configuration
|
| 3 |
+
=========================================================
|
| 4 |
+
SQLAlchemy 엔진, 세션 관리 및 Base 클래스 정의
|
| 5 |
+
|
| 6 |
+
주요 기능:
|
| 7 |
+
- MySQL 데이터베이스 연결 설정
|
| 8 |
+
- 세션 생성 및 의존성 주입
|
| 9 |
+
- 비동기 컨텍스트 매니저 지원
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from sqlalchemy import create_engine, event, text
|
| 13 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 14 |
+
from sqlalchemy.orm import sessionmaker, Session
|
| 15 |
+
from typing import Generator
|
| 16 |
+
import os
|
| 17 |
+
from dotenv import load_dotenv
|
| 18 |
+
|
| 19 |
+
# ============================================================================
|
| 20 |
+
# 환경 변수 로드
|
| 21 |
+
# ============================================================================
|
| 22 |
+
load_dotenv()
|
| 23 |
+
|
| 24 |
+
# ============================================================================
|
| 25 |
+
# 데이터베이스 설정
|
| 26 |
+
# ============================================================================
|
| 27 |
+
DB_HOST = os.getenv("DB_HOST", "localhost")
|
| 28 |
+
DB_PORT = os.getenv("DB_PORT", "3306")
|
| 29 |
+
DB_USER = os.getenv("DB_USER", "root")
|
| 30 |
+
DB_PASSWORD = os.getenv("DB_PASSWORD", "")
|
| 31 |
+
DB_NAME = os.getenv("DB_NAME", "smarteyessen_db")
|
| 32 |
+
|
| 33 |
+
# MySQL 연결 URL 생성
|
| 34 |
+
# pymysql 드라이버 사용, charset=utf8mb4 설정
|
| 35 |
+
SQLALCHEMY_DATABASE_URL = (
|
| 36 |
+
f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
| 37 |
+
f"?charset=utf8mb4"
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# ============================================================================
|
| 41 |
+
# SQLAlchemy Engine 생성
|
| 42 |
+
# ============================================================================
|
| 43 |
+
engine = create_engine(
|
| 44 |
+
SQLALCHEMY_DATABASE_URL,
|
| 45 |
+
# 연결 풀 설정
|
| 46 |
+
pool_size=10, # 기본 연결 수
|
| 47 |
+
max_overflow=20, # 추가 가능한 최대 연결 수
|
| 48 |
+
pool_pre_ping=True, # 연결 유효성 자동 체크
|
| 49 |
+
pool_recycle=3600, # 1시간마다 연결 재생성
|
| 50 |
+
echo=False, # SQL 로그 출력 (개발 시 True로 변경 가능)
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
# ============================================================================
|
| 54 |
+
# SessionLocal 클래스 생성
|
| 55 |
+
# ============================================================================
|
| 56 |
+
SessionLocal = sessionmaker(
|
| 57 |
+
autocommit=False, # 자동 커밋 비활성화
|
| 58 |
+
autoflush=False, # 자동 플러시 비활성화
|
| 59 |
+
bind=engine,
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
# ============================================================================
|
| 63 |
+
# Base 클래스 정의
|
| 64 |
+
# ============================================================================
|
| 65 |
+
Base = declarative_base()
|
| 66 |
+
|
| 67 |
+
# ============================================================================
|
| 68 |
+
# 데이터베이스 세션 의존성 함수
|
| 69 |
+
# ============================================================================
|
| 70 |
+
def get_db() -> Generator[Session, None, None]:
|
| 71 |
+
"""
|
| 72 |
+
FastAPI 의존성 주입용 데이터베이스 세션 생성
|
| 73 |
+
|
| 74 |
+
사용 예시:
|
| 75 |
+
```python
|
| 76 |
+
@app.get("/users")
|
| 77 |
+
def read_users(db: Session = Depends(get_db)):
|
| 78 |
+
users = db.query(User).all()
|
| 79 |
+
return users
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
Yields:
|
| 83 |
+
Session: SQLAlchemy 데이터베이스 세션
|
| 84 |
+
"""
|
| 85 |
+
db = SessionLocal()
|
| 86 |
+
try:
|
| 87 |
+
yield db
|
| 88 |
+
finally:
|
| 89 |
+
db.close()
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# ============================================================================
|
| 93 |
+
# 데이터베이스 초기화 함수
|
| 94 |
+
# ============================================================================
|
| 95 |
+
def init_db():
|
| 96 |
+
"""
|
| 97 |
+
데이터베이스 테이블 생성 (개발 환경용)
|
| 98 |
+
|
| 99 |
+
주의: 운영 환경에서는 Alembic 마이그레이션 사용 권장
|
| 100 |
+
"""
|
| 101 |
+
# models.py에서 정의한 모든 테이블 생성
|
| 102 |
+
Base.metadata.create_all(bind=engine)
|
| 103 |
+
print("✅ Database tables created successfully!")
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def drop_all_tables():
|
| 107 |
+
"""
|
| 108 |
+
모든 테이블 삭제 (개발/테스트 환경용)
|
| 109 |
+
|
| 110 |
+
⚠️ 주의: 모든 데이터가 삭제됩니다!
|
| 111 |
+
"""
|
| 112 |
+
Base.metadata.drop_all(bind=engine)
|
| 113 |
+
print("⚠️ All database tables dropped!")
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
# ============================================================================
|
| 117 |
+
# 데이터베이스 연결 테스트 함수
|
| 118 |
+
# ============================================================================
|
| 119 |
+
def test_connection():
|
| 120 |
+
"""
|
| 121 |
+
데이터베이스 연결 테스트
|
| 122 |
+
|
| 123 |
+
Returns:
|
| 124 |
+
bool: 연결 성공 여부
|
| 125 |
+
"""
|
| 126 |
+
try:
|
| 127 |
+
# 간단한 쿼리 실행하여 연결 확인
|
| 128 |
+
with engine.connect() as connection:
|
| 129 |
+
result = connection.execute(text("SELECT 1"))
|
| 130 |
+
print("✅ Database connection successful!")
|
| 131 |
+
return True
|
| 132 |
+
except Exception as e:
|
| 133 |
+
print(f"❌ Database connection failed: {e}")
|
| 134 |
+
return False
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
# ============================================================================
|
| 138 |
+
# SQLAlchemy Event Listeners (선택사항)
|
| 139 |
+
# ============================================================================
|
| 140 |
+
@event.listens_for(engine, "connect")
|
| 141 |
+
def set_sqlite_pragma(dbapi_conn, connection_record):
|
| 142 |
+
"""
|
| 143 |
+
MySQL 연결 시 추가 설정
|
| 144 |
+
- 타임존 설정
|
| 145 |
+
- 문자셋 ���인
|
| 146 |
+
"""
|
| 147 |
+
cursor = dbapi_conn.cursor()
|
| 148 |
+
# UTF-8 문자셋 강제 설정
|
| 149 |
+
cursor.execute("SET NAMES utf8mb4")
|
| 150 |
+
cursor.execute("SET CHARACTER SET utf8mb4")
|
| 151 |
+
cursor.execute("SET character_set_connection=utf8mb4")
|
| 152 |
+
cursor.close()
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
# ============================================================================
|
| 156 |
+
# 개발용 유틸리티
|
| 157 |
+
# ============================================================================
|
| 158 |
+
if __name__ == "__main__":
|
| 159 |
+
"""
|
| 160 |
+
직접 실행 시 데이터베이스 연결 테스트
|
| 161 |
+
|
| 162 |
+
실행 방법:
|
| 163 |
+
```bash
|
| 164 |
+
python app/database.py
|
| 165 |
+
```
|
| 166 |
+
"""
|
| 167 |
+
print("=" * 60)
|
| 168 |
+
print("SmartEyeSsen Database Connection Test")
|
| 169 |
+
print("=" * 60)
|
| 170 |
+
print(f"Database URL: {SQLALCHEMY_DATABASE_URL.replace(DB_PASSWORD, '***')}")
|
| 171 |
+
print("-" * 60)
|
| 172 |
+
test_connection()
|
| 173 |
+
print("=" * 60)
|
app/main.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SmartEyeSsen Backend - FastAPI Main Application
|
| 3 |
+
================================================
|
| 4 |
+
FastAPI 메인 애플리케이션 및 라우터 설정
|
| 5 |
+
|
| 6 |
+
주요 기능:
|
| 7 |
+
- FastAPI 앱 초기화
|
| 8 |
+
- CORS 설정
|
| 9 |
+
- 라우터 등록
|
| 10 |
+
- 데이터베이스 초기화
|
| 11 |
+
- API 문서화
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from fastapi import FastAPI, Depends, HTTPException, status
|
| 15 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 16 |
+
from fastapi.responses import JSONResponse
|
| 17 |
+
from sqlalchemy.orm import Session
|
| 18 |
+
import os
|
| 19 |
+
from dotenv import load_dotenv
|
| 20 |
+
|
| 21 |
+
from .database import engine, get_db, init_db, test_connection
|
| 22 |
+
from . import models
|
| 23 |
+
|
| 24 |
+
# 환경 변수 로드
|
| 25 |
+
load_dotenv()
|
| 26 |
+
|
| 27 |
+
# ============================================================================
|
| 28 |
+
# FastAPI 앱 초기화
|
| 29 |
+
# ============================================================================
|
| 30 |
+
app = FastAPI(
|
| 31 |
+
title="SmartEyeSsen API",
|
| 32 |
+
description="""
|
| 33 |
+
## SmartEyeSsen Backend API
|
| 34 |
+
|
| 35 |
+
시각장애 학생을 위한 AI 기반 학습 자료 분석 시스템
|
| 36 |
+
|
| 37 |
+
### 주요 기능
|
| 38 |
+
* 📄 **다중 페이지 문서 처리**: Worksheet 및 Document 유형 지원
|
| 39 |
+
* 🤖 **AI 레이아웃 분석**: DocLayout-YOLO 기반 레이아웃 감지
|
| 40 |
+
* 🔍 **OCR 텍스트 추출**: PaddleOCR 기반 텍스트 인식
|
| 41 |
+
* ✏️ **텍스트 편집 및 버전 관리**: TinyMCE 편집기 지원
|
| 42 |
+
* 🖼️ **AI 설명 생성**: GPT-4o-mini 기반 figure/table 설명
|
| 43 |
+
* 📊 **문제 기반 정렬**: Worksheet 전용 문제 번호 기반 정렬
|
| 44 |
+
* 📐 **좌표 기반 정렬**: Document 전용 좌표 기반 정렬
|
| 45 |
+
* 📥 **통합 문서 다운로드**: DOCX/PDF/TXT 형식 지원
|
| 46 |
+
|
| 47 |
+
### 기술 스택
|
| 48 |
+
* **Backend**: FastAPI + SQLAlchemy
|
| 49 |
+
* **Database**: MySQL 8.0
|
| 50 |
+
* **AI Models**: DocLayout-YOLO, PaddleOCR, GPT-4o-mini
|
| 51 |
+
* **Document**: python-docx
|
| 52 |
+
""",
|
| 53 |
+
version="1.0.0",
|
| 54 |
+
docs_url="/docs",
|
| 55 |
+
redoc_url="/redoc",
|
| 56 |
+
openapi_url="/openapi.json",
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# ============================================================================
|
| 60 |
+
# CORS 설정
|
| 61 |
+
# ============================================================================
|
| 62 |
+
CORS_ORIGINS = os.getenv("CORS_ORIGINS", "http://localhost:3000,http://localhost:8080").split(",")
|
| 63 |
+
|
| 64 |
+
app.add_middleware(
|
| 65 |
+
CORSMiddleware,
|
| 66 |
+
allow_origins=CORS_ORIGINS, # 허용할 출처
|
| 67 |
+
allow_credentials=True,
|
| 68 |
+
allow_methods=["*"], # 모든 HTTP 메소드 허용
|
| 69 |
+
allow_headers=["*"], # 모든 헤더 허용
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
# ============================================================================
|
| 73 |
+
# 시작 이벤트
|
| 74 |
+
# ============================================================================
|
| 75 |
+
@app.on_event("startup")
|
| 76 |
+
async def startup_event():
|
| 77 |
+
"""
|
| 78 |
+
애플리케이션 시작 시 실행
|
| 79 |
+
- 데이터베이스 연결 테스트
|
| 80 |
+
- 테이블 생성 (개발 환경)
|
| 81 |
+
"""
|
| 82 |
+
print("=" * 60)
|
| 83 |
+
print("🚀 SmartEyeSsen Backend Starting...")
|
| 84 |
+
print("=" * 60)
|
| 85 |
+
|
| 86 |
+
# 데이터베이스 연결 테스트
|
| 87 |
+
if test_connection():
|
| 88 |
+
print("✅ Database connection successful")
|
| 89 |
+
else:
|
| 90 |
+
print("❌ Database connection failed")
|
| 91 |
+
print("⚠️ Please check your database configuration")
|
| 92 |
+
|
| 93 |
+
# 테이블 생성 (개발 환경에서만)
|
| 94 |
+
if os.getenv("ENVIRONMENT", "development") == "development":
|
| 95 |
+
try:
|
| 96 |
+
init_db()
|
| 97 |
+
print("✅ Database tables initialized")
|
| 98 |
+
except Exception as e:
|
| 99 |
+
print(f"⚠️ Table initialization warning: {e}")
|
| 100 |
+
|
| 101 |
+
print("=" * 60)
|
| 102 |
+
print("✅ SmartEyeSsen Backend Ready!")
|
| 103 |
+
print(f"📖 API Docs: http://localhost:{os.getenv('API_PORT', 8000)}/docs")
|
| 104 |
+
print("=" * 60)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
@app.on_event("shutdown")
|
| 108 |
+
async def shutdown_event():
|
| 109 |
+
"""애플리케이션 종료 시 실행"""
|
| 110 |
+
print("\n" + "=" * 60)
|
| 111 |
+
print("👋 SmartEyeSsen Backend Shutting down...")
|
| 112 |
+
print("=" * 60)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# ============================================================================
|
| 116 |
+
# 루트 엔드포인트
|
| 117 |
+
# ============================================================================
|
| 118 |
+
@app.get("/", tags=["Root"])
|
| 119 |
+
async def root():
|
| 120 |
+
"""
|
| 121 |
+
루트 엔드포인트
|
| 122 |
+
|
| 123 |
+
서버 상태 및 기본 정보 반환
|
| 124 |
+
"""
|
| 125 |
+
return {
|
| 126 |
+
"message": "Welcome to SmartEyeSsen API",
|
| 127 |
+
"version": "1.0.0",
|
| 128 |
+
"status": "running",
|
| 129 |
+
"docs": "/docs",
|
| 130 |
+
"redoc": "/redoc"
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
@app.get("/health", tags=["Root"])
|
| 135 |
+
async def health_check(db: Session = Depends(get_db)):
|
| 136 |
+
"""
|
| 137 |
+
헬스 체크 엔드포인트
|
| 138 |
+
|
| 139 |
+
서버 및 데이터베이스 상태 확인
|
| 140 |
+
"""
|
| 141 |
+
try:
|
| 142 |
+
# 간단한 쿼리로 DB 연결 확인
|
| 143 |
+
db.execute("SELECT 1")
|
| 144 |
+
db_status = "connected"
|
| 145 |
+
except Exception as e:
|
| 146 |
+
db_status = f"error: {str(e)}"
|
| 147 |
+
|
| 148 |
+
return {
|
| 149 |
+
"status": "healthy",
|
| 150 |
+
"database": db_status,
|
| 151 |
+
"api_version": "1.0.0"
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
# ============================================================================
|
| 156 |
+
# 예외 핸들러
|
| 157 |
+
# ============================================================================
|
| 158 |
+
@app.exception_handler(HTTPException)
|
| 159 |
+
async def http_exception_handler(request, exc):
|
| 160 |
+
"""HTTP 예외 핸들러"""
|
| 161 |
+
return JSONResponse(
|
| 162 |
+
status_code=exc.status_code,
|
| 163 |
+
content={
|
| 164 |
+
"error": exc.detail,
|
| 165 |
+
"status_code": exc.status_code
|
| 166 |
+
}
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
@app.exception_handler(Exception)
|
| 171 |
+
async def general_exception_handler(request, exc):
|
| 172 |
+
"""일반 예외 핸들러"""
|
| 173 |
+
return JSONResponse(
|
| 174 |
+
status_code=500,
|
| 175 |
+
content={
|
| 176 |
+
"error": "Internal Server Error",
|
| 177 |
+
"detail": str(exc),
|
| 178 |
+
"status_code": 500
|
| 179 |
+
}
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
# ============================================================================
|
| 184 |
+
# 라우터 등록 (Phase 2에서 추가 예정)
|
| 185 |
+
# ============================================================================
|
| 186 |
+
# from .routers import users, projects, pages, layout_elements
|
| 187 |
+
# app.include_router(users.router, prefix="/api/v1/users", tags=["Users"])
|
| 188 |
+
# app.include_router(projects.router, prefix="/api/v1/projects", tags=["Projects"])
|
| 189 |
+
# app.include_router(pages.router, prefix="/api/v1/pages", tags=["Pages"])
|
| 190 |
+
# app.include_router(layout_elements.router, prefix="/api/v1/elements", tags=["Layout Elements"])
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
# ============================================================================
|
| 194 |
+
# 개발 서버 실행 (직접 실행 시)
|
| 195 |
+
# ============================================================================
|
| 196 |
+
if __name__ == "__main__":
|
| 197 |
+
import uvicorn
|
| 198 |
+
|
| 199 |
+
HOST = os.getenv("API_HOST", "0.0.0.0")
|
| 200 |
+
PORT = int(os.getenv("API_PORT", 8000))
|
| 201 |
+
RELOAD = os.getenv("API_RELOAD", "True").lower() == "true"
|
| 202 |
+
|
| 203 |
+
uvicorn.run(
|
| 204 |
+
"main:app",
|
| 205 |
+
host=HOST,
|
| 206 |
+
port=PORT,
|
| 207 |
+
reload=RELOAD,
|
| 208 |
+
log_level="info"
|
| 209 |
+
)
|
app/models.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SmartEyeSsen Backend - SQLAlchemy ORM Models
|
| 3 |
+
============================================
|
| 4 |
+
12개 테이블에 대한 SQLAlchemy 모델 정의
|
| 5 |
+
|
| 6 |
+
테이블 목록:
|
| 7 |
+
1. users - 사용자 정보
|
| 8 |
+
2. document_types - 문서 유형 (worksheet/document)
|
| 9 |
+
3. projects - 프로젝트 (문서 단위)
|
| 10 |
+
4. pages - 페이지 정보
|
| 11 |
+
5. layout_elements - 레이아웃 요소
|
| 12 |
+
6. text_contents - 텍스트 내용
|
| 13 |
+
7. ai_descriptions - AI 생성 설명
|
| 14 |
+
8. question_groups - 문제 그룹
|
| 15 |
+
9. question_elements - 문제 요소
|
| 16 |
+
10. text_versions - 텍스트 버전 관리
|
| 17 |
+
11. formatting_rules - 서식 규칙
|
| 18 |
+
12. combined_results - 통합 결과
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from sqlalchemy import (
|
| 22 |
+
Column, Integer, String, Text, DateTime, Enum, Numeric,
|
| 23 |
+
ForeignKey, Boolean, JSON, DECIMAL, Index
|
| 24 |
+
)
|
| 25 |
+
from sqlalchemy.orm import relationship
|
| 26 |
+
from sqlalchemy.sql import func
|
| 27 |
+
from datetime import datetime
|
| 28 |
+
from .database import Base
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# ============================================================================
|
| 32 |
+
# 1. Users - 사용자 정보
|
| 33 |
+
# ============================================================================
|
| 34 |
+
class User(Base):
|
| 35 |
+
"""사용자 정보 테이블"""
|
| 36 |
+
__tablename__ = "users"
|
| 37 |
+
|
| 38 |
+
user_id = Column(Integer, primary_key=True, autoincrement=True, comment="사용자 ID")
|
| 39 |
+
username = Column(String(50), unique=True, nullable=False, comment="사용자명")
|
| 40 |
+
email = Column(String(100), unique=True, nullable=False, comment="이메일")
|
| 41 |
+
password_hash = Column(String(255), nullable=False, comment="비밀번호 해시")
|
| 42 |
+
created_at = Column(DateTime, default=func.now(), comment="생성일시")
|
| 43 |
+
last_login = Column(DateTime, nullable=True, comment="마지막 로그인")
|
| 44 |
+
|
| 45 |
+
# 관계 설정
|
| 46 |
+
projects = relationship("Project", back_populates="user", cascade="all, delete-orphan")
|
| 47 |
+
|
| 48 |
+
def __repr__(self):
|
| 49 |
+
return f"<User(id={self.user_id}, username='{self.username}')>"
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# ============================================================================
|
| 53 |
+
# 2. Document Types - 문서 유형
|
| 54 |
+
# ============================================================================
|
| 55 |
+
class DocumentType(Base):
|
| 56 |
+
"""문서 유형 테이블 (worksheet/document)"""
|
| 57 |
+
__tablename__ = "document_types"
|
| 58 |
+
|
| 59 |
+
type_id = Column(Integer, primary_key=True, autoincrement=True, comment="유형 ID")
|
| 60 |
+
type_name = Column(
|
| 61 |
+
Enum('worksheet', 'document', name='document_type_enum'),
|
| 62 |
+
unique=True,
|
| 63 |
+
nullable=False,
|
| 64 |
+
comment="문서 유형"
|
| 65 |
+
)
|
| 66 |
+
description = Column(String(255), nullable=True, comment="유형 설명")
|
| 67 |
+
|
| 68 |
+
# 관계 설정
|
| 69 |
+
projects = relationship("Project", back_populates="document_type")
|
| 70 |
+
|
| 71 |
+
def __repr__(self):
|
| 72 |
+
return f"<DocumentType(id={self.type_id}, name='{self.type_name}')>"
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# ============================================================================
|
| 76 |
+
# 3. Projects - 프로젝트 (문서 단위)
|
| 77 |
+
# ============================================================================
|
| 78 |
+
class Project(Base):
|
| 79 |
+
"""프로젝트 테이블 (다중 페이지 문서)"""
|
| 80 |
+
__tablename__ = "projects"
|
| 81 |
+
|
| 82 |
+
project_id = Column(Integer, primary_key=True, autoincrement=True, comment="프로젝트 ID")
|
| 83 |
+
user_id = Column(Integer, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=False, comment="사용자 ID")
|
| 84 |
+
project_name = Column(String(255), nullable=False, comment="프로젝트명")
|
| 85 |
+
type_id = Column(Integer, ForeignKey("document_types.type_id", ondelete="SET NULL"), nullable=True, comment="문서 유형 ID")
|
| 86 |
+
created_at = Column(DateTime, default=func.now(), comment="생성일시")
|
| 87 |
+
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), comment="수정일시")
|
| 88 |
+
|
| 89 |
+
# 관계 설정
|
| 90 |
+
user = relationship("User", back_populates="projects")
|
| 91 |
+
document_type = relationship("DocumentType", back_populates="projects")
|
| 92 |
+
pages = relationship("Page", back_populates="project", cascade="all, delete-orphan")
|
| 93 |
+
|
| 94 |
+
# 인덱스
|
| 95 |
+
__table_args__ = (
|
| 96 |
+
Index("idx_user_created", "user_id", "created_at"),
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
def __repr__(self):
|
| 100 |
+
return f"<Project(id={self.project_id}, name='{self.project_name}')>"
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
# ============================================================================
|
| 104 |
+
# 4. Pages - 페이지 정보
|
| 105 |
+
# ============================================================================
|
| 106 |
+
class Page(Base):
|
| 107 |
+
"""페이지 정보 테이블"""
|
| 108 |
+
__tablename__ = "pages"
|
| 109 |
+
|
| 110 |
+
page_id = Column(Integer, primary_key=True, autoincrement=True, comment="페이지 ID")
|
| 111 |
+
project_id = Column(Integer, ForeignKey("projects.project_id", ondelete="CASCADE"), nullable=False, comment="프로젝트 ID")
|
| 112 |
+
page_number = Column(Integer, nullable=False, comment="페이지 번호")
|
| 113 |
+
image_path = Column(String(512), nullable=True, comment="이미지 경로")
|
| 114 |
+
image_width = Column(Integer, nullable=True, comment="이미지 너비")
|
| 115 |
+
image_height = Column(Integer, nullable=True, comment="이미지 높이")
|
| 116 |
+
uploaded_at = Column(DateTime, default=func.now(), comment="업로드일시")
|
| 117 |
+
|
| 118 |
+
# 관계 설정
|
| 119 |
+
project = relationship("Project", back_populates="pages")
|
| 120 |
+
layout_elements = relationship("LayoutElement", back_populates="page", cascade="all, delete-orphan")
|
| 121 |
+
|
| 122 |
+
# 인덱스 및 제약조건
|
| 123 |
+
__table_args__ = (
|
| 124 |
+
Index("idx_project_page", "project_id", "page_number", unique=True),
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
def __repr__(self):
|
| 128 |
+
return f"<Page(id={self.page_id}, project={self.project_id}, page_num={self.page_number})>"
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
# ============================================================================
|
| 132 |
+
# 5. Layout Elements - 레이아웃 요소
|
| 133 |
+
# ============================================================================
|
| 134 |
+
class LayoutElement(Base):
|
| 135 |
+
"""레이아웃 요소 테이블 (DocLayout-YOLO 감지 결과)"""
|
| 136 |
+
__tablename__ = "layout_elements"
|
| 137 |
+
|
| 138 |
+
element_id = Column(Integer, primary_key=True, autoincrement=True, comment="요소 ID")
|
| 139 |
+
page_id = Column(Integer, ForeignKey("pages.page_id", ondelete="CASCADE"), nullable=False, comment="페이지 ID")
|
| 140 |
+
element_type = Column(
|
| 141 |
+
Enum('text', 'title', 'figure', 'figure_caption', 'table', 'table_caption',
|
| 142 |
+
'header', 'footer', 'reference', 'equation', name='element_type_enum'),
|
| 143 |
+
nullable=False,
|
| 144 |
+
comment="요소 유형"
|
| 145 |
+
)
|
| 146 |
+
x_min = Column(Integer, nullable=False, comment="좌측 상단 X 좌표")
|
| 147 |
+
y_min = Column(Integer, nullable=False, comment="좌측 상단 Y 좌표")
|
| 148 |
+
x_max = Column(Integer, nullable=False, comment="우측 하단 X 좌표")
|
| 149 |
+
y_max = Column(Integer, nullable=False, comment="우측 하단 Y 좌표")
|
| 150 |
+
confidence = Column(DECIMAL(5, 4), nullable=True, comment="신뢰도")
|
| 151 |
+
area = Column(Integer, nullable=True, comment="영역 크기 (자동 계산)")
|
| 152 |
+
x_position = Column(Integer, nullable=True, comment="X 중심 좌표 (자동 계산)")
|
| 153 |
+
y_position = Column(Integer, nullable=True, comment="Y 중심 좌표 (자동 계산)")
|
| 154 |
+
|
| 155 |
+
# 관계 설정
|
| 156 |
+
page = relationship("Page", back_populates="layout_elements")
|
| 157 |
+
text_content = relationship("TextContent", back_populates="layout_element", uselist=False, cascade="all, delete-orphan")
|
| 158 |
+
ai_description = relationship("AIDescription", back_populates="layout_element", uselist=False, cascade="all, delete-orphan")
|
| 159 |
+
question_elements = relationship("QuestionElement", back_populates="layout_element", cascade="all, delete-orphan")
|
| 160 |
+
|
| 161 |
+
# 인덱스
|
| 162 |
+
__table_args__ = (
|
| 163 |
+
Index("idx_page_element", "page_id", "element_id"),
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
def __repr__(self):
|
| 167 |
+
return f"<LayoutElement(id={self.element_id}, type='{self.element_type}')>"
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
# ============================================================================
|
| 171 |
+
# 6. Text Contents - 텍스트 내용
|
| 172 |
+
# ============================================================================
|
| 173 |
+
class TextContent(Base):
|
| 174 |
+
"""텍스트 내용 테이블 (OCR 결과)"""
|
| 175 |
+
__tablename__ = "text_contents"
|
| 176 |
+
|
| 177 |
+
content_id = Column(Integer, primary_key=True, autoincrement=True, comment="내용 ID")
|
| 178 |
+
element_id = Column(Integer, ForeignKey("layout_elements.element_id", ondelete="CASCADE"), unique=True, nullable=False, comment="요소 ID")
|
| 179 |
+
raw_text = Column(Text, nullable=True, comment="원본 OCR 텍스트")
|
| 180 |
+
edited_text = Column(Text, nullable=True, comment="편집된 텍스트")
|
| 181 |
+
version = Column(Integer, default=1, comment="버전 번호")
|
| 182 |
+
last_edited_at = Column(DateTime, nullable=True, comment="마지막 편집일시")
|
| 183 |
+
|
| 184 |
+
# 관계 설정
|
| 185 |
+
layout_element = relationship("LayoutElement", back_populates="text_content")
|
| 186 |
+
text_versions = relationship("TextVersion", back_populates="text_content", cascade="all, delete-orphan")
|
| 187 |
+
|
| 188 |
+
def __repr__(self):
|
| 189 |
+
return f"<TextContent(id={self.content_id}, element={self.element_id}, v={self.version})>"
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
# ============================================================================
|
| 193 |
+
# 7. AI Descriptions - AI 생성 설명
|
| 194 |
+
# ============================================================================
|
| 195 |
+
class AIDescription(Base):
|
| 196 |
+
"""AI 생성 설명 테이블 (figure/table 설명)"""
|
| 197 |
+
__tablename__ = "ai_descriptions"
|
| 198 |
+
|
| 199 |
+
description_id = Column(Integer, primary_key=True, autoincrement=True, comment="설명 ID")
|
| 200 |
+
element_id = Column(Integer, ForeignKey("layout_elements.element_id", ondelete="CASCADE"), unique=True, nullable=False, comment="요소 ID")
|
| 201 |
+
description_text = Column(Text, nullable=True, comment="AI 생성 설명")
|
| 202 |
+
model_name = Column(String(100), nullable=True, comment="사용된 AI 모델명")
|
| 203 |
+
generated_at = Column(DateTime, default=func.now(), comment="생성일시")
|
| 204 |
+
|
| 205 |
+
# 관계 설정
|
| 206 |
+
layout_element = relationship("LayoutElement", back_populates="ai_description")
|
| 207 |
+
|
| 208 |
+
def __repr__(self):
|
| 209 |
+
return f"<AIDescription(id={self.description_id}, element={self.element_id})>"
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
# ============================================================================
|
| 213 |
+
# 8. Question Groups - 문제 그룹
|
| 214 |
+
# ============================================================================
|
| 215 |
+
class QuestionGroup(Base):
|
| 216 |
+
"""문제 그룹 테이블 (worksheet 전용)"""
|
| 217 |
+
__tablename__ = "question_groups"
|
| 218 |
+
|
| 219 |
+
group_id = Column(Integer, primary_key=True, autoincrement=True, comment="그룹 ID")
|
| 220 |
+
group_number = Column(Integer, nullable=False, comment="문제 번호")
|
| 221 |
+
page_id = Column(Integer, ForeignKey("pages.page_id", ondelete="CASCADE"), nullable=False, comment="페이지 ID")
|
| 222 |
+
|
| 223 |
+
# 관계 설정
|
| 224 |
+
question_elements = relationship("QuestionElement", back_populates="question_group", cascade="all, delete-orphan")
|
| 225 |
+
|
| 226 |
+
# 인덱스 및 제약조건
|
| 227 |
+
__table_args__ = (
|
| 228 |
+
Index("idx_page_group", "page_id", "group_number", unique=True),
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
def __repr__(self):
|
| 232 |
+
return f"<QuestionGroup(id={self.group_id}, num={self.group_number})>"
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
# ============================================================================
|
| 236 |
+
# 9. Question Elements - 문제 요소
|
| 237 |
+
# ============================================================================
|
| 238 |
+
class QuestionElement(Base):
|
| 239 |
+
"""문제 요소 테이블 (worksheet 전용)"""
|
| 240 |
+
__tablename__ = "question_elements"
|
| 241 |
+
|
| 242 |
+
question_element_id = Column(Integer, primary_key=True, autoincrement=True, comment="문제 요소 ID")
|
| 243 |
+
group_id = Column(Integer, ForeignKey("question_groups.group_id", ondelete="CASCADE"), nullable=False, comment="그룹 ID")
|
| 244 |
+
element_id = Column(Integer, ForeignKey("layout_elements.element_id", ondelete="CASCADE"), nullable=False, comment="요소 ID")
|
| 245 |
+
element_order = Column(Integer, nullable=False, comment="요소 순서")
|
| 246 |
+
|
| 247 |
+
# 관계 설정
|
| 248 |
+
question_group = relationship("QuestionGroup", back_populates="question_elements")
|
| 249 |
+
layout_element = relationship("LayoutElement", back_populates="question_elements")
|
| 250 |
+
|
| 251 |
+
# 인덱스 및 제약조건
|
| 252 |
+
__table_args__ = (
|
| 253 |
+
Index("idx_group_order", "group_id", "element_order"),
|
| 254 |
+
Index("idx_element_unique", "element_id", unique=True),
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
def __repr__(self):
|
| 258 |
+
return f"<QuestionElement(id={self.question_element_id}, group={self.group_id})>"
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
# ============================================================================
|
| 262 |
+
# 10. Text Versions - 텍스트 버전 관리
|
| 263 |
+
# ============================================================================
|
| 264 |
+
class TextVersion(Base):
|
| 265 |
+
"""텍스트 버전 관리 테이블"""
|
| 266 |
+
__tablename__ = "text_versions"
|
| 267 |
+
|
| 268 |
+
version_id = Column(Integer, primary_key=True, autoincrement=True, comment="버전 ID")
|
| 269 |
+
content_id = Column(Integer, ForeignKey("text_contents.content_id", ondelete="CASCADE"), nullable=False, comment="내용 ID")
|
| 270 |
+
version_number = Column(Integer, nullable=False, comment="버전 번호")
|
| 271 |
+
version_text = Column(Text, nullable=False, comment="버전 텍스트")
|
| 272 |
+
edited_at = Column(DateTime, default=func.now(), comment="편집일시")
|
| 273 |
+
|
| 274 |
+
# 관계 설정
|
| 275 |
+
text_content = relationship("TextContent", back_populates="text_versions")
|
| 276 |
+
|
| 277 |
+
# 인덱스 및 제약조건
|
| 278 |
+
__table_args__ = (
|
| 279 |
+
Index("idx_content_version", "content_id", "version_number", unique=True),
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
def __repr__(self):
|
| 283 |
+
return f"<TextVersion(id={self.version_id}, content={self.content_id}, v={self.version_number})>"
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
# ============================================================================
|
| 287 |
+
# 11. Formatting Rules - 서식 규칙
|
| 288 |
+
# ============================================================================
|
| 289 |
+
class FormattingRule(Base):
|
| 290 |
+
"""서식 규칙 테이블"""
|
| 291 |
+
__tablename__ = "formatting_rules"
|
| 292 |
+
|
| 293 |
+
rule_id = Column(Integer, primary_key=True, autoincrement=True, comment="규칙 ID")
|
| 294 |
+
element_type = Column(
|
| 295 |
+
Enum('text', 'title', 'figure', 'figure_caption', 'table', 'table_caption',
|
| 296 |
+
'header', 'footer', 'reference', 'equation', name='element_type_enum'),
|
| 297 |
+
nullable=False,
|
| 298 |
+
comment="요소 유형"
|
| 299 |
+
)
|
| 300 |
+
font_name = Column(String(100), nullable=True, comment="폰트명")
|
| 301 |
+
font_size = Column(Integer, nullable=True, comment="폰트 크기")
|
| 302 |
+
font_bold = Column(Boolean, default=False, comment="굵게")
|
| 303 |
+
font_italic = Column(Boolean, default=False, comment="기울임")
|
| 304 |
+
font_underline = Column(Boolean, default=False, comment="밑줄")
|
| 305 |
+
alignment = Column(
|
| 306 |
+
Enum('left', 'center', 'right', 'justify', name='alignment_enum'),
|
| 307 |
+
default='left',
|
| 308 |
+
comment="정렬"
|
| 309 |
+
)
|
| 310 |
+
line_spacing = Column(DECIMAL(3, 1), nullable=True, comment="줄 간격")
|
| 311 |
+
space_before = Column(Integer, nullable=True, comment="앞 여백 (pt)")
|
| 312 |
+
space_after = Column(Integer, nullable=True, comment="뒤 여백 (pt)")
|
| 313 |
+
|
| 314 |
+
# 인덱스
|
| 315 |
+
__table_args__ = (
|
| 316 |
+
Index("idx_element_type", "element_type", unique=True),
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
def __repr__(self):
|
| 320 |
+
return f"<FormattingRule(id={self.rule_id}, type='{self.element_type}')>"
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
# ============================================================================
|
| 324 |
+
# 12. Combined Results - 통합 결과
|
| 325 |
+
# ============================================================================
|
| 326 |
+
class CombinedResult(Base):
|
| 327 |
+
"""통합 결과 테이블 (최종 문서 생성용)"""
|
| 328 |
+
__tablename__ = "combined_results"
|
| 329 |
+
|
| 330 |
+
result_id = Column(Integer, primary_key=True, autoincrement=True, comment="결과 ID")
|
| 331 |
+
project_id = Column(Integer, ForeignKey("projects.project_id", ondelete="CASCADE"), nullable=False, comment="프로젝트 ID")
|
| 332 |
+
combined_text = Column(Text, nullable=True, comment="통합 텍스트")
|
| 333 |
+
output_format = Column(
|
| 334 |
+
Enum('docx', 'pdf', 'txt', name='output_format_enum'),
|
| 335 |
+
default='docx',
|
| 336 |
+
comment="출력 형식"
|
| 337 |
+
)
|
| 338 |
+
file_path = Column(String(512), nullable=True, comment="파일 경로")
|
| 339 |
+
generated_at = Column(DateTime, default=func.now(), comment="생성일시")
|
| 340 |
+
|
| 341 |
+
# 인덱스
|
| 342 |
+
__table_args__ = (
|
| 343 |
+
Index("idx_project_generated", "project_id", "generated_at"),
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
def __repr__(self):
|
| 347 |
+
return f"<CombinedResult(id={self.result_id}, project={self.project_id})>"
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
# ============================================================================
|
| 351 |
+
# 모델 초기화 순서 (참고용)
|
| 352 |
+
# ============================================================================
|
| 353 |
+
"""
|
| 354 |
+
외래 키 의존성 순서:
|
| 355 |
+
1. User (독립)
|
| 356 |
+
2. DocumentType (독립)
|
| 357 |
+
3. FormattingRule (독립)
|
| 358 |
+
4. Project (User, DocumentType 의존)
|
| 359 |
+
5. Page (Project 의존)
|
| 360 |
+
6. LayoutElement (Page 의존)
|
| 361 |
+
7. TextContent (LayoutElement 의존)
|
| 362 |
+
8. AIDescription (LayoutElement 의존)
|
| 363 |
+
9. QuestionGroup (Page 의존)
|
| 364 |
+
10. QuestionElement (QuestionGroup, LayoutElement 의존)
|
| 365 |
+
11. TextVersion (TextContent 의존)
|
| 366 |
+
12. CombinedResult (Project 의존)
|
| 367 |
+
"""
|
app/routers/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SmartEyeSsen Backend - Routers Package
|
| 3 |
+
=======================================
|
| 4 |
+
API 라우터 패키지 초기화
|
| 5 |
+
|
| 6 |
+
Phase 2에서 개별 라우터 모듈이 추가될 예정:
|
| 7 |
+
- users.py: 사용자 관리 API
|
| 8 |
+
- projects.py: 프로젝트 관리 API
|
| 9 |
+
- pages.py: 페이지 관리 API
|
| 10 |
+
- layout_elements.py: 레이아웃 요소 API
|
| 11 |
+
- text_contents.py: 텍스트 내용 API
|
| 12 |
+
- ai_descriptions.py: AI 설명 API
|
| 13 |
+
- formatting_rules.py: 서식 규칙 API
|
| 14 |
+
- combined_results.py: 통합 결과 API
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
__version__ = "1.0.0"
|
app/schemas.py
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SmartEyeSsen Backend - Pydantic Schemas
|
| 3 |
+
=======================================
|
| 4 |
+
API 요청/응답 검증을 위한 Pydantic 스키마 정의
|
| 5 |
+
|
| 6 |
+
주요 기능:
|
| 7 |
+
- 요청 본문 검증 (Create, Update)
|
| 8 |
+
- 응답 데이터 직렬화 (Read)
|
| 9 |
+
- 타입 힌팅 및 자동 문서화
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from pydantic import BaseModel, EmailStr, Field, ConfigDict
|
| 13 |
+
from typing import Optional, List
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
from enum import Enum
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# ============================================================================
|
| 19 |
+
# Enum 정의
|
| 20 |
+
# ============================================================================
|
| 21 |
+
class DocumentTypeEnum(str, Enum):
|
| 22 |
+
"""문서 유형"""
|
| 23 |
+
WORKSHEET = "worksheet"
|
| 24 |
+
DOCUMENT = "document"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class ElementTypeEnum(str, Enum):
|
| 28 |
+
"""레이아웃 요소 유형"""
|
| 29 |
+
TEXT = "text"
|
| 30 |
+
TITLE = "title"
|
| 31 |
+
FIGURE = "figure"
|
| 32 |
+
FIGURE_CAPTION = "figure_caption"
|
| 33 |
+
TABLE = "table"
|
| 34 |
+
TABLE_CAPTION = "table_caption"
|
| 35 |
+
HEADER = "header"
|
| 36 |
+
FOOTER = "footer"
|
| 37 |
+
REFERENCE = "reference"
|
| 38 |
+
EQUATION = "equation"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class AlignmentEnum(str, Enum):
|
| 42 |
+
"""텍스트 정렬"""
|
| 43 |
+
LEFT = "left"
|
| 44 |
+
CENTER = "center"
|
| 45 |
+
RIGHT = "right"
|
| 46 |
+
JUSTIFY = "justify"
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class OutputFormatEnum(str, Enum):
|
| 50 |
+
"""출력 형식"""
|
| 51 |
+
DOCX = "docx"
|
| 52 |
+
PDF = "pdf"
|
| 53 |
+
TXT = "txt"
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# ============================================================================
|
| 57 |
+
# 1. User Schemas
|
| 58 |
+
# ============================================================================
|
| 59 |
+
class UserBase(BaseModel):
|
| 60 |
+
"""사용자 기본 스키마"""
|
| 61 |
+
username: str = Field(..., min_length=3, max_length=50, description="사용자명")
|
| 62 |
+
email: EmailStr = Field(..., description="이메일")
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class UserCreate(UserBase):
|
| 66 |
+
"""사용자 생성 스키마"""
|
| 67 |
+
password: str = Field(..., min_length=8, max_length=100, description="비밀번호")
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class UserUpdate(BaseModel):
|
| 71 |
+
"""사용자 수정 스키마"""
|
| 72 |
+
username: Optional[str] = Field(None, min_length=3, max_length=50)
|
| 73 |
+
email: Optional[EmailStr] = None
|
| 74 |
+
password: Optional[str] = Field(None, min_length=8, max_length=100)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class UserResponse(UserBase):
|
| 78 |
+
"""사용자 응답 스키마"""
|
| 79 |
+
user_id: int
|
| 80 |
+
created_at: datetime
|
| 81 |
+
last_login: Optional[datetime] = None
|
| 82 |
+
|
| 83 |
+
model_config = ConfigDict(from_attributes=True)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# ============================================================================
|
| 87 |
+
# 2. DocumentType Schemas
|
| 88 |
+
# ============================================================================
|
| 89 |
+
class DocumentTypeBase(BaseModel):
|
| 90 |
+
"""문서 유형 기본 스키마"""
|
| 91 |
+
type_name: DocumentTypeEnum = Field(..., description="문서 유형")
|
| 92 |
+
description: Optional[str] = Field(None, max_length=255, description="유형 설명")
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class DocumentTypeCreate(DocumentTypeBase):
|
| 96 |
+
"""문서 유형 생성 스키마"""
|
| 97 |
+
pass
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class DocumentTypeResponse(DocumentTypeBase):
|
| 101 |
+
"""문서 유형 응답 스키마"""
|
| 102 |
+
type_id: int
|
| 103 |
+
|
| 104 |
+
model_config = ConfigDict(from_attributes=True)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
# ============================================================================
|
| 108 |
+
# 3. Project Schemas
|
| 109 |
+
# ============================================================================
|
| 110 |
+
class ProjectBase(BaseModel):
|
| 111 |
+
"""프로젝트 기본 스키마"""
|
| 112 |
+
project_name: str = Field(..., min_length=1, max_length=255, description="프로젝트명")
|
| 113 |
+
type_id: Optional[int] = Field(None, description="문서 유형 ID")
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
class ProjectCreate(ProjectBase):
|
| 117 |
+
"""프로젝트 생성 스키마"""
|
| 118 |
+
pass
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class ProjectUpdate(BaseModel):
|
| 122 |
+
"""프로젝트 수정 스키마"""
|
| 123 |
+
project_name: Optional[str] = Field(None, min_length=1, max_length=255)
|
| 124 |
+
type_id: Optional[int] = None
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class ProjectResponse(ProjectBase):
|
| 128 |
+
"""프로젝트 응답 스키마"""
|
| 129 |
+
project_id: int
|
| 130 |
+
user_id: int
|
| 131 |
+
created_at: datetime
|
| 132 |
+
updated_at: datetime
|
| 133 |
+
|
| 134 |
+
model_config = ConfigDict(from_attributes=True)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
class ProjectWithPagesResponse(ProjectResponse):
|
| 138 |
+
"""페이지 포함 프로젝트 응답"""
|
| 139 |
+
pages: List["PageResponse"] = []
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
# ============================================================================
|
| 143 |
+
# 4. Page Schemas
|
| 144 |
+
# ============================================================================
|
| 145 |
+
class PageBase(BaseModel):
|
| 146 |
+
"""페이지 기본 스키마"""
|
| 147 |
+
page_number: int = Field(..., ge=1, description="페이지 번호")
|
| 148 |
+
image_path: Optional[str] = Field(None, max_length=512, description="이미지 경로")
|
| 149 |
+
image_width: Optional[int] = Field(None, ge=1, description="이미지 너비")
|
| 150 |
+
image_height: Optional[int] = Field(None, ge=1, description="이미지 높이")
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
class PageCreate(PageBase):
|
| 154 |
+
"""페이지 생성 스키마"""
|
| 155 |
+
project_id: int = Field(..., description="프로젝트 ID")
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class PageUpdate(BaseModel):
|
| 159 |
+
"""페이지 수정 스키마"""
|
| 160 |
+
image_path: Optional[str] = Field(None, max_length=512)
|
| 161 |
+
image_width: Optional[int] = Field(None, ge=1)
|
| 162 |
+
image_height: Optional[int] = Field(None, ge=1)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
class PageResponse(PageBase):
|
| 166 |
+
"""페이지 응답 스키마"""
|
| 167 |
+
page_id: int
|
| 168 |
+
project_id: int
|
| 169 |
+
uploaded_at: datetime
|
| 170 |
+
|
| 171 |
+
model_config = ConfigDict(from_attributes=True)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
class PageWithElementsResponse(PageResponse):
|
| 175 |
+
"""레이아웃 요소 포함 페이지 응답"""
|
| 176 |
+
layout_elements: List["LayoutElementResponse"] = []
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
# ============================================================================
|
| 180 |
+
# 5. LayoutElement Schemas
|
| 181 |
+
# ============================================================================
|
| 182 |
+
class LayoutElementBase(BaseModel):
|
| 183 |
+
"""레이아웃 요소 기본 스키마"""
|
| 184 |
+
element_type: ElementTypeEnum = Field(..., description="요소 유형")
|
| 185 |
+
x_min: int = Field(..., description="좌측 상단 X 좌표")
|
| 186 |
+
y_min: int = Field(..., description="좌측 상단 Y 좌표")
|
| 187 |
+
x_max: int = Field(..., description="우측 하단 X 좌표")
|
| 188 |
+
y_max: int = Field(..., description="우측 하단 Y 좌표")
|
| 189 |
+
confidence: Optional[float] = Field(None, ge=0.0, le=1.0, description="신뢰도")
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
class LayoutElementCreate(LayoutElementBase):
|
| 193 |
+
"""레이아웃 요소 생성 스키마"""
|
| 194 |
+
page_id: int = Field(..., description="페이지 ID")
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
class LayoutElementUpdate(BaseModel):
|
| 198 |
+
"""레이아웃 요소 수정 스키마"""
|
| 199 |
+
element_type: Optional[ElementTypeEnum] = None
|
| 200 |
+
x_min: Optional[int] = None
|
| 201 |
+
y_min: Optional[int] = None
|
| 202 |
+
x_max: Optional[int] = None
|
| 203 |
+
y_max: Optional[int] = None
|
| 204 |
+
confidence: Optional[float] = Field(None, ge=0.0, le=1.0)
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
class LayoutElementResponse(LayoutElementBase):
|
| 208 |
+
"""레이아웃 요소 응답 스키마"""
|
| 209 |
+
element_id: int
|
| 210 |
+
page_id: int
|
| 211 |
+
area: Optional[int] = None
|
| 212 |
+
x_position: Optional[int] = None
|
| 213 |
+
y_position: Optional[int] = None
|
| 214 |
+
|
| 215 |
+
model_config = ConfigDict(from_attributes=True)
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
# ============================================================================
|
| 219 |
+
# 6. TextContent Schemas
|
| 220 |
+
# ============================================================================
|
| 221 |
+
class TextContentBase(BaseModel):
|
| 222 |
+
"""텍스트 내용 기본 스키마"""
|
| 223 |
+
raw_text: Optional[str] = Field(None, description="원본 OCR 텍스트")
|
| 224 |
+
edited_text: Optional[str] = Field(None, description="편집된 텍스트")
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
class TextContentCreate(TextContentBase):
|
| 228 |
+
"""텍스트 내용 생성 스키마"""
|
| 229 |
+
element_id: int = Field(..., description="요소 ID")
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
class TextContentUpdate(BaseModel):
|
| 233 |
+
"""텍스트 내용 수정 스키마"""
|
| 234 |
+
edited_text: Optional[str] = Field(None, description="편집된 텍스트")
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
class TextContentResponse(TextContentBase):
|
| 238 |
+
"""텍스트 내용 응답 스키마"""
|
| 239 |
+
content_id: int
|
| 240 |
+
element_id: int
|
| 241 |
+
version: int
|
| 242 |
+
last_edited_at: Optional[datetime] = None
|
| 243 |
+
|
| 244 |
+
model_config = ConfigDict(from_attributes=True)
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
# ============================================================================
|
| 248 |
+
# 7. AIDescription Schemas
|
| 249 |
+
# ============================================================================
|
| 250 |
+
class AIDescriptionBase(BaseModel):
|
| 251 |
+
"""AI 설명 기본 스키마"""
|
| 252 |
+
description_text: Optional[str] = Field(None, description="AI 생성 설명")
|
| 253 |
+
model_name: Optional[str] = Field(None, max_length=100, description="AI 모델명")
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
class AIDescriptionCreate(AIDescriptionBase):
|
| 257 |
+
"""AI 설명 생성 스키마"""
|
| 258 |
+
element_id: int = Field(..., description="요소 ID")
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
class AIDescriptionResponse(AIDescriptionBase):
|
| 262 |
+
"""AI 설명 응답 스키마"""
|
| 263 |
+
description_id: int
|
| 264 |
+
element_id: int
|
| 265 |
+
generated_at: datetime
|
| 266 |
+
|
| 267 |
+
model_config = ConfigDict(from_attributes=True)
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
# ============================================================================
|
| 271 |
+
# 8. QuestionGroup Schemas
|
| 272 |
+
# ============================================================================
|
| 273 |
+
class QuestionGroupBase(BaseModel):
|
| 274 |
+
"""문제 그룹 기본 스키마"""
|
| 275 |
+
group_number: int = Field(..., ge=1, description="문제 번호")
|
| 276 |
+
page_id: int = Field(..., description="페이지 ID")
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
class QuestionGroupCreate(QuestionGroupBase):
|
| 280 |
+
"""문제 그룹 생성 스키마"""
|
| 281 |
+
pass
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
class QuestionGroupResponse(QuestionGroupBase):
|
| 285 |
+
"""문제 그룹 응답 스키마"""
|
| 286 |
+
group_id: int
|
| 287 |
+
|
| 288 |
+
model_config = ConfigDict(from_attributes=True)
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
# ============================================================================
|
| 292 |
+
# 9. QuestionElement Schemas
|
| 293 |
+
# ============================================================================
|
| 294 |
+
class QuestionElementBase(BaseModel):
|
| 295 |
+
"""문제 요소 기본 스키마"""
|
| 296 |
+
group_id: int = Field(..., description="그룹 ID")
|
| 297 |
+
element_id: int = Field(..., description="요소 ID")
|
| 298 |
+
element_order: int = Field(..., ge=1, description="요소 순서")
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
class QuestionElementCreate(QuestionElementBase):
|
| 302 |
+
"""문제 요소 생성 스키마"""
|
| 303 |
+
pass
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
class QuestionElementResponse(QuestionElementBase):
|
| 307 |
+
"""문제 요소 응답 스키마"""
|
| 308 |
+
question_element_id: int
|
| 309 |
+
|
| 310 |
+
model_config = ConfigDict(from_attributes=True)
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
# ============================================================================
|
| 314 |
+
# 10. TextVersion Schemas
|
| 315 |
+
# ============================================================================
|
| 316 |
+
class TextVersionBase(BaseModel):
|
| 317 |
+
"""텍스트 버전 기본 스키마"""
|
| 318 |
+
version_number: int = Field(..., ge=1, description="버전 번호")
|
| 319 |
+
version_text: str = Field(..., description="버전 텍스트")
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
class TextVersionCreate(TextVersionBase):
|
| 323 |
+
"""텍스트 버전 생성 스키마"""
|
| 324 |
+
content_id: int = Field(..., description="내용 ID")
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
class TextVersionResponse(TextVersionBase):
|
| 328 |
+
"""텍스트 버전 응답 스키마"""
|
| 329 |
+
version_id: int
|
| 330 |
+
content_id: int
|
| 331 |
+
edited_at: datetime
|
| 332 |
+
|
| 333 |
+
model_config = ConfigDict(from_attributes=True)
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
# ============================================================================
|
| 337 |
+
# 11. FormattingRule Schemas
|
| 338 |
+
# ============================================================================
|
| 339 |
+
class FormattingRuleBase(BaseModel):
|
| 340 |
+
"""서식 규칙 기본 스키마"""
|
| 341 |
+
element_type: ElementTypeEnum = Field(..., description="요소 유형")
|
| 342 |
+
font_name: Optional[str] = Field(None, max_length=100, description="폰트명")
|
| 343 |
+
font_size: Optional[int] = Field(None, ge=1, le=72, description="폰트 크기")
|
| 344 |
+
font_bold: bool = Field(False, description="굵게")
|
| 345 |
+
font_italic: bool = Field(False, description="기울임")
|
| 346 |
+
font_underline: bool = Field(False, description="밑줄")
|
| 347 |
+
alignment: AlignmentEnum = Field(AlignmentEnum.LEFT, description="정렬")
|
| 348 |
+
line_spacing: Optional[float] = Field(None, ge=0.5, le=3.0, description="줄 간격")
|
| 349 |
+
space_before: Optional[int] = Field(None, ge=0, description="앞 여백 (pt)")
|
| 350 |
+
space_after: Optional[int] = Field(None, ge=0, description="뒤 여백 (pt)")
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
class FormattingRuleCreate(FormattingRuleBase):
|
| 354 |
+
"""서식 규칙 생성 스키마"""
|
| 355 |
+
pass
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
class FormattingRuleUpdate(BaseModel):
|
| 359 |
+
"""서식 규칙 수정 스키마"""
|
| 360 |
+
font_name: Optional[str] = Field(None, max_length=100)
|
| 361 |
+
font_size: Optional[int] = Field(None, ge=1, le=72)
|
| 362 |
+
font_bold: Optional[bool] = None
|
| 363 |
+
font_italic: Optional[bool] = None
|
| 364 |
+
font_underline: Optional[bool] = None
|
| 365 |
+
alignment: Optional[AlignmentEnum] = None
|
| 366 |
+
line_spacing: Optional[float] = Field(None, ge=0.5, le=3.0)
|
| 367 |
+
space_before: Optional[int] = Field(None, ge=0)
|
| 368 |
+
space_after: Optional[int] = Field(None, ge=0)
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
class FormattingRuleResponse(FormattingRuleBase):
|
| 372 |
+
"""서식 규칙 응답 스키마"""
|
| 373 |
+
rule_id: int
|
| 374 |
+
|
| 375 |
+
model_config = ConfigDict(from_attributes=True)
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
# ============================================================================
|
| 379 |
+
# 12. CombinedResult Schemas
|
| 380 |
+
# ============================================================================
|
| 381 |
+
class CombinedResultBase(BaseModel):
|
| 382 |
+
"""통합 결과 기본 스키마"""
|
| 383 |
+
combined_text: Optional[str] = Field(None, description="통합 텍스트")
|
| 384 |
+
output_format: OutputFormatEnum = Field(OutputFormatEnum.DOCX, description="출력 형식")
|
| 385 |
+
file_path: Optional[str] = Field(None, max_length=512, description="파일 경로")
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
class CombinedResultCreate(CombinedResultBase):
|
| 389 |
+
"""통합 결과 생성 스키마"""
|
| 390 |
+
project_id: int = Field(..., description="프로젝트 ID")
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
class CombinedResultResponse(CombinedResultBase):
|
| 394 |
+
"""통합 결과 응답 스키마"""
|
| 395 |
+
result_id: int
|
| 396 |
+
project_id: int
|
| 397 |
+
generated_at: datetime
|
| 398 |
+
|
| 399 |
+
model_config = ConfigDict(from_attributes=True)
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
# ============================================================================
|
| 403 |
+
# 복합 응답 스키마 (관계 포함)
|
| 404 |
+
# ============================================================================
|
| 405 |
+
class LayoutElementWithContentResponse(LayoutElementResponse):
|
| 406 |
+
"""텍스트 및 AI 설명 포함 레이아웃 요소"""
|
| 407 |
+
text_content: Optional[TextContentResponse] = None
|
| 408 |
+
ai_description: Optional[AIDescriptionResponse] = None
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
class PageDetailResponse(PageResponse):
|
| 412 |
+
"""상세 페이지 정보 (모든 관계 포함)"""
|
| 413 |
+
layout_elements: List[LayoutElementWithContentResponse] = []
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
class ProjectDetailResponse(ProjectResponse):
|
| 417 |
+
"""상세 프로젝트 정보 (페이지 및 결과 포함)"""
|
| 418 |
+
pages: List[PageDetailResponse] = []
|
| 419 |
+
combined_results: List[CombinedResultResponse] = []
|
| 420 |
+
document_type: Optional[DocumentTypeResponse] = None
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
# ============================================================================
|
| 424 |
+
# 유틸리티 스키마
|
| 425 |
+
# ============================================================================
|
| 426 |
+
class MessageResponse(BaseModel):
|
| 427 |
+
"""일반 메시지 응답"""
|
| 428 |
+
message: str
|
| 429 |
+
detail: Optional[str] = None
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
class ErrorResponse(BaseModel):
|
| 433 |
+
"""에러 응답"""
|
| 434 |
+
error: str
|
| 435 |
+
detail: Optional[str] = None
|
| 436 |
+
status_code: int
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
class PaginationParams(BaseModel):
|
| 440 |
+
"""페이지네이션 파라미터"""
|
| 441 |
+
skip: int = Field(0, ge=0, description="건너뛸 개수")
|
| 442 |
+
limit: int = Field(100, ge=1, le=1000, description="조회 개수")
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
class PaginatedResponse(BaseModel):
|
| 446 |
+
"""페이지네이션 응답"""
|
| 447 |
+
total: int = Field(..., description="전체 개수")
|
| 448 |
+
skip: int = Field(..., description="건너뛴 개수")
|
| 449 |
+
limit: int = Field(..., description="조회 개수")
|
| 450 |
+
items: List = Field(..., description="데이터 목록")
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
# ============================================================================
|
| 454 |
+
# Forward Reference 해결
|
| 455 |
+
# ============================================================================
|
| 456 |
+
ProjectWithPagesResponse.model_rebuild()
|
| 457 |
+
PageWithElementsResponse.model_rebuild()
|
requirements.txt
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================================
|
| 2 |
+
# SmartEyeSsen Backend - Python Dependencies
|
| 3 |
+
# ============================================================================
|
| 4 |
+
# FastAPI 프레임워크 및 웹 서버
|
| 5 |
+
# ============================================================================
|
| 6 |
+
fastapi==0.109.0
|
| 7 |
+
uvicorn[standard]==0.27.0
|
| 8 |
+
python-multipart==0.0.6
|
| 9 |
+
|
| 10 |
+
# ============================================================================
|
| 11 |
+
# 데이터베이스 및 ORM
|
| 12 |
+
# ============================================================================
|
| 13 |
+
sqlalchemy==2.0.25
|
| 14 |
+
pymysql==1.1.0
|
| 15 |
+
cryptography==42.0.0 # pymysql 암호화 지원
|
| 16 |
+
alembic==1.13.1 # 데이터베이스 마이그레이션 도구
|
| 17 |
+
|
| 18 |
+
# ============================================================================
|
| 19 |
+
# 데이터 검증 및 설정
|
| 20 |
+
# ============================================================================
|
| 21 |
+
pydantic==2.5.3
|
| 22 |
+
pydantic-settings==2.1.0
|
| 23 |
+
python-dotenv==1.0.0
|
| 24 |
+
|
| 25 |
+
# ============================================================================
|
| 26 |
+
# AI/ML 라이브러리
|
| 27 |
+
# ============================================================================
|
| 28 |
+
# DocLayout-YOLO 및 OCR
|
| 29 |
+
ultralytics==8.1.0
|
| 30 |
+
paddlepaddle==2.6.0
|
| 31 |
+
paddleocr==2.7.0
|
| 32 |
+
# OpenAI API (AI 설명 생성)
|
| 33 |
+
openai==1.10.0
|
| 34 |
+
|
| 35 |
+
# ============================================================================
|
| 36 |
+
# 이미지 처리
|
| 37 |
+
# ============================================================================
|
| 38 |
+
pillow==10.2.0
|
| 39 |
+
opencv-python==4.9.0.80
|
| 40 |
+
|
| 41 |
+
# ============================================================================
|
| 42 |
+
# 문서 생성
|
| 43 |
+
# ============================================================================
|
| 44 |
+
python-docx==1.1.0
|
| 45 |
+
|
| 46 |
+
# ============================================================================
|
| 47 |
+
# 유틸리티
|
| 48 |
+
# ============================================================================
|
| 49 |
+
# 날짜/시간 처리
|
| 50 |
+
python-dateutil==2.8.2
|
| 51 |
+
# HTTP 클라이언트
|
| 52 |
+
httpx==0.26.0
|
| 53 |
+
# 로깅
|
| 54 |
+
loguru==0.7.2
|
| 55 |
+
|
| 56 |
+
# ============================================================================
|
| 57 |
+
# 보안 (선택사항)
|
| 58 |
+
# ============================================================================
|
| 59 |
+
# JWT 토큰 인증
|
| 60 |
+
python-jose[cryptography]==3.3.0
|
| 61 |
+
passlib[bcrypt]==1.7.4
|
| 62 |
+
|
| 63 |
+
# ============================================================================
|
| 64 |
+
# 개발 도구 (선택사항)
|
| 65 |
+
# ============================================================================
|
| 66 |
+
# pytest==7.4.4
|
| 67 |
+
# pytest-asyncio==0.23.3
|
| 68 |
+
# black==23.12.1
|
| 69 |
+
# flake8==7.0.0
|
start_server.bat
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
REM ============================================================================
|
| 3 |
+
REM SmartEyeSsen Backend - 서버 시작 스크립트 (Windows)
|
| 4 |
+
REM ============================================================================
|
| 5 |
+
|
| 6 |
+
echo ============================================================
|
| 7 |
+
echo SmartEyeSsen Backend Server
|
| 8 |
+
echo ============================================================
|
| 9 |
+
echo.
|
| 10 |
+
|
| 11 |
+
REM 가상환경 확인
|
| 12 |
+
if exist "venv\Scripts\activate.bat" (
|
| 13 |
+
echo [1/3] Activating virtual environment...
|
| 14 |
+
call venv\Scripts\activate.bat
|
| 15 |
+
) else (
|
| 16 |
+
echo [WARNING] Virtual environment not found!
|
| 17 |
+
echo Please create one with: python -m venv venv
|
| 18 |
+
echo.
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
REM 환경 변수 파일 확인
|
| 22 |
+
if not exist ".env" (
|
| 23 |
+
echo [WARNING] .env file not found!
|
| 24 |
+
echo Please copy .env.example to .env and configure it.
|
| 25 |
+
echo.
|
| 26 |
+
pause
|
| 27 |
+
exit /b 1
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
echo [2/3] Checking dependencies...
|
| 31 |
+
pip list | findstr "fastapi" >nul
|
| 32 |
+
if errorlevel 1 (
|
| 33 |
+
echo [INFO] Installing dependencies...
|
| 34 |
+
pip install -r requirements.txt
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
echo [3/3] Starting FastAPI server...
|
| 38 |
+
echo.
|
| 39 |
+
echo ============================================================
|
| 40 |
+
echo Server will start at: http://localhost:8000
|
| 41 |
+
echo API Documentation: http://localhost:8000/docs
|
| 42 |
+
echo ============================================================
|
| 43 |
+
echo.
|
| 44 |
+
|
| 45 |
+
REM FastAPI 서버 실행
|
| 46 |
+
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
| 47 |
+
|
| 48 |
+
pause
|