KwanHak commited on
Commit
3f85869
·
0 Parent(s):

"feat: Phase 1 완료 - 데이터베이스 및 백엔드 기초 구축

Browse files

- SQLAlchemy ORM 모델 (12개 테이블)
- Pydantic 스키마 (요청/응답 검증)
- CRUD 헬퍼 함수
- FastAPI 메인 애플리케이션
- 데이터베이스 연결 설정
- 환경 변수 템플릿 (.env.example)
- 팀 협업 가이드 (SETUP.md)

Files changed (11) hide show
  1. .env.example +55 -0
  2. README.md +263 -0
  3. app/__init__.py +8 -0
  4. app/crud.py +537 -0
  5. app/database.py +173 -0
  6. app/main.py +209 -0
  7. app/models.py +367 -0
  8. app/routers/__init__.py +17 -0
  9. app/schemas.py +457 -0
  10. requirements.txt +69 -0
  11. 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