smarteye-backend / app /models.py
AkJeond's picture
refactor(database): combined_text ์ปฌ๋Ÿผ์„ LONGTEXT๋กœ ํ™•์žฅ ๋ฐ DB ์ดˆ๊ธฐํ™” ํ†ตํ•ฉ
dc8b42c
"""
SmartEyeSsen Backend - SQLAlchemy ORM Models (v2)
================================================
ERD v2 ๊ธฐ์ค€ 12๊ฐœ ํ…Œ์ด๋ธ” SQLAlchemy ๋ชจ๋ธ ์ •์˜
ํ…Œ์ด๋ธ” ๋ชฉ๋ก:
1. users - ์‚ฌ์šฉ์ž ์ •๋ณด
2. document_types - ๋ฌธ์„œ ํƒ€์ž… ์ •์˜ (worksheet/document)
3. projects - ํ”„๋กœ์ ํŠธ (๋ฌธ์„œ ๋‹จ์œ„)
4. pages - ํŽ˜์ด์ง€ ์ •๋ณด
5. layout_elements - ๋ ˆ์ด์•„์›ƒ ์š”์†Œ
6. text_contents - OCR ๊ฒฐ๊ณผ
7. ai_descriptions - AI ์ƒ์„ฑ ์„ค๋ช…
8. question_groups - ๋ฌธ์ œ ๊ทธ๋ฃน (v2: ์•ต์ปค ์š”์†Œ ๊ธฐ๋ฐ˜)
9. question_elements - ๋ฌธ์ œ-์š”์†Œ ๋งคํ•‘
10. text_versions - ํ…์ŠคํŠธ ๋ฒ„์ „ ๊ด€๋ฆฌ
11. formatting_rules - ํฌ๋งทํŒ… ๊ทœ์น™
12. combined_results - ํ†ตํ•ฉ ๋ฌธ์„œ ์บ์‹œ
์ตœ์ข… ์ˆ˜์ •์ผ: 2025-01-22 (v2)
์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ: ERD v2 ๊ธฐ์ค€ ์™„์ „ ์žฌ์ž‘์„ฑ (์•ต์ปค/์ž์‹ ๊ฐœ๋… ๋ฐ˜์˜)
"""
from sqlalchemy import (
Column, Integer, String, Text, DateTime, Enum, Float,
ForeignKey, Boolean, JSON, Index, UniqueConstraint, Computed
)
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from datetime import datetime
from .database import Base
import enum
# ============================================================================
# Enums - ์—ด๊ฑฐํ˜• ์ •์˜
# ============================================================================
class SortingMethodEnum(str, enum.Enum):
"""์ •๋ ฌ ๋ฐฉ์‹"""
QUESTION_BASED = "question_based" # ๋ฌธ์ œ์ง€: ์•ต์ปค-์ž์‹ ์žฌ๊ท€
READING_ORDER = "reading_order" # ์ผ๋ฐ˜๋ฌธ์„œ: Y/X ์ขŒํ‘œ
class AnalysisModeEnum(str, enum.Enum):
"""๋ถ„์„ ๋ชจ๋“œ"""
AUTO = "auto"
MANUAL = "manual"
HYBRID = "hybrid"
class ProjectStatusEnum(str, enum.Enum):
"""ํ”„๋กœ์ ํŠธ ์ƒํƒœ"""
CREATED = "created"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
ERROR = "error"
class AnalysisStatusEnum(str, enum.Enum):
"""๋ถ„์„ ์ƒํƒœ"""
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
ERROR = "error"
class VersionTypeEnum(str, enum.Enum):
"""๋ฒ„์ „ ์œ ํ˜•"""
ORIGINAL = "original"
AUTO_FORMATTED = "auto_formatted"
USER_EDITED = "user_edited"
# ============================================================================
# 1. Users - ์‚ฌ์šฉ์ž ์ •๋ณด
# ============================================================================
class User(Base):
"""์‚ฌ์šฉ์ž ์ •๋ณด ํ…Œ์ด๋ธ”"""
__tablename__ = "users"
user_id = Column(Integer, primary_key=True, autoincrement=True, comment="์‚ฌ์šฉ์ž ๊ณ ์œ  ID")
email = Column(String(255), unique=True, nullable=False, comment="์ด๋ฉ”์ผ (๋กœ๊ทธ์ธ ID)")
name = Column(String(100), nullable=False, comment="์‚ฌ์šฉ์ž ์ด๋ฆ„")
role = Column(String(50), nullable=False, default="user", comment="์—ญํ•  (admin/teacher/student/user)")
password_hash = Column(String(255), nullable=False, comment="bcrypt ํ•ด์‹œ๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ")
api_key = Column(String(255), nullable=True, comment="OpenAI API ํ‚ค (AES-256 ์•”ํ˜ธํ™” ์ €์žฅ)")
created_at = Column(DateTime, default=func.now(), comment="๊ณ„์ • ์ƒ์„ฑ์ผ")
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), comment="๋งˆ์ง€๋ง‰ ์ˆ˜์ •์ผ")
# ๊ด€๊ณ„ ์„ค์ •
projects = relationship("Project", back_populates="user", cascade="all, delete-orphan")
text_versions = relationship("TextVersion", back_populates="user")
# ์ธ๋ฑ์Šค
__table_args__ = (
Index("idx_email", "email"),
Index("idx_role", "role"),
)
def __repr__(self):
return f"<User(user_id={self.user_id}, email='{self.email}', role='{self.role}')>"
# ============================================================================
# 2. Document Types - ๋ฌธ์„œ ํƒ€์ž… ์ •์˜
# ============================================================================
class DocumentType(Base):
"""๋ฌธ์„œ ํƒ€์ž… ์ •์˜ ํ…Œ์ด๋ธ” (worksheet/document)"""
__tablename__ = "document_types"
doc_type_id = Column(Integer, primary_key=True, autoincrement=True, comment="๋ฌธ์„œ ํƒ€์ž… ๊ณ ์œ  ID")
type_name = Column(String(100), unique=True, nullable=False, comment="ํƒ€์ž…๋ช… (worksheet/document/form)")
model_name = Column(String(100), nullable=False, comment="AI ๋ชจ๋ธ๋ช… (SmartEyeSsen/DocLayout-YOLO)")
sorting_method = Column(
Enum(SortingMethodEnum),
nullable=False,
comment="์ •๋ ฌ ๋ฐฉ์‹: question_based(๋ฌธ์ œ์ง€), reading_order(์ผ๋ฐ˜๋ฌธ์„œ)"
)
description = Column(Text, nullable=True, comment="ํƒ€์ž… ์„ค๋ช…")
created_at = Column(DateTime, default=func.now(), comment="์ƒ์„ฑ์ผ")
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), comment="์ˆ˜์ •์ผ")
# ๊ด€๊ณ„ ์„ค์ •
projects = relationship("Project", back_populates="document_type")
formatting_rules = relationship("FormattingRule", back_populates="document_type", cascade="all, delete-orphan")
# ์ธ๋ฑ์Šค
__table_args__ = (
Index("idx_type_name", "type_name"),
)
def __repr__(self):
return f"<DocumentType(doc_type_id={self.doc_type_id}, type_name='{self.type_name}', sorting='{self.sorting_method.value}')>"
# ============================================================================
# 3. Projects - ํ”„๋กœ์ ํŠธ (๋ฌธ์„œ ๋‹จ์œ„)
# ============================================================================
class Project(Base):
"""ํ”„๋กœ์ ํŠธ ํ…Œ์ด๋ธ” (๋‹ค์ค‘ ํŽ˜์ด์ง€ ๋ฌธ์„œ)"""
__tablename__ = "projects"
project_id = Column(Integer, primary_key=True, autoincrement=True, comment="ํ”„๋กœ์ ํŠธ ๊ณ ์œ  ID")
user_id = Column(Integer, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=False, comment="์†Œ์œ ์ž ID")
doc_type_id = Column(Integer, ForeignKey("document_types.doc_type_id", ondelete="RESTRICT"), nullable=False, comment="๋ฌธ์„œ ํƒ€์ž… ID")
project_name = Column(String(255), nullable=False, comment="ํ”„๋กœ์ ํŠธ ์ด๋ฆ„")
total_pages = Column(Integer, default=0, comment="์ด ํŽ˜์ด์ง€ ์ˆ˜ (ํŠธ๋ฆฌ๊ฑฐ๋กœ ์ž๋™ ๊ณ„์‚ฐ)")
analysis_mode = Column(
Enum(AnalysisModeEnum),
default=AnalysisModeEnum.AUTO,
comment="๋ถ„์„ ๋ชจ๋“œ: auto/manual/hybrid"
)
status = Column(
Enum(ProjectStatusEnum),
default=ProjectStatusEnum.CREATED,
comment="ํ”„๋กœ์ ํŠธ ์ƒํƒœ"
)
created_at = Column(DateTime, default=func.now(), comment="ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ์ผ")
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), comment="๋งˆ์ง€๋ง‰ ์ˆ˜์ •์ผ")
# ๊ด€๊ณ„ ์„ค์ •
user = relationship("User", back_populates="projects")
document_type = relationship("DocumentType", back_populates="projects")
pages = relationship("Page", back_populates="project", cascade="all, delete-orphan")
combined_result = relationship("CombinedResult", back_populates="project", uselist=False, cascade="all, delete-orphan")
# ์ธ๋ฑ์Šค
__table_args__ = (
Index("idx_user_id", "user_id"),
Index("idx_doc_type_id", "doc_type_id"),
Index("idx_status", "status"),
)
def __repr__(self):
return f"<Project(project_id={self.project_id}, name='{self.project_name}', status='{self.status.value}')>"
# ============================================================================
# 4. Pages - ํŽ˜์ด์ง€ ์ •๋ณด
# ============================================================================
class Page(Base):
"""ํŽ˜์ด์ง€ ์ •๋ณด ํ…Œ์ด๋ธ”"""
__tablename__ = "pages"
page_id = Column(Integer, primary_key=True, autoincrement=True, comment="ํŽ˜์ด์ง€ ๊ณ ์œ  ID")
project_id = Column(Integer, ForeignKey("projects.project_id", ondelete="CASCADE"), nullable=False, comment="์†Œ์† ํ”„๋กœ์ ํŠธ ID")
page_number = Column(Integer, nullable=False, comment="ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (1๋ถ€ํ„ฐ ์‹œ์ž‘)")
image_path = Column(String(500), nullable=False, comment="์ด๋ฏธ์ง€ ํŒŒ์ผ ๊ฒฝ๋กœ")
image_width = Column(Integer, nullable=True, comment="์ด๋ฏธ์ง€ ๋„ˆ๋น„ (ํ”ฝ์…€)")
image_height = Column(Integer, nullable=True, comment="์ด๋ฏธ์ง€ ๋†’์ด (ํ”ฝ์…€)")
analysis_status = Column(
Enum(AnalysisStatusEnum),
default=AnalysisStatusEnum.PENDING,
comment="๋ถ„์„ ์ƒํƒœ"
)
processing_time = Column(Float, nullable=True, comment="์ฒ˜๋ฆฌ ์‹œ๊ฐ„ (์ดˆ)")
created_at = Column(DateTime, default=func.now(), comment="ํŽ˜์ด์ง€ ์ถ”๊ฐ€์ผ")
analyzed_at = Column(DateTime, nullable=True, comment="๋ถ„์„ ์™„๋ฃŒ์ผ")
# ๊ด€๊ณ„ ์„ค์ •
project = relationship("Project", back_populates="pages")
layout_elements = relationship("LayoutElement", back_populates="page", cascade="all, delete-orphan")
question_groups = relationship("QuestionGroup", back_populates="page", cascade="all, delete-orphan")
text_versions = relationship("TextVersion", back_populates="page", cascade="all, delete-orphan")
# ์ธ๋ฑ์Šค ๋ฐ ์ œ์•ฝ์กฐ๊ฑด
__table_args__ = (
UniqueConstraint("project_id", "page_number", name="uk_project_page"),
Index("idx_project_id", "project_id"),
Index("idx_analysis_status", "analysis_status"),
)
def __repr__(self):
return f"<Page(page_id={self.page_id}, project={self.project_id}, page_num={self.page_number}, status='{self.analysis_status.value}')>"
# ============================================================================
# 5. Layout Elements - ๋ ˆ์ด์•„์›ƒ ์š”์†Œ (v2: order_index ์‚ญ์ œ)
# ============================================================================
class LayoutElement(Base):
"""๋ ˆ์ด์•„์›ƒ ์š”์†Œ ํ…Œ์ด๋ธ” (AI ๋ชจ๋ธ ๊ฒ€์ถœ ๊ฒฐ๊ณผ)"""
__tablename__ = "layout_elements"
element_id = Column(Integer, primary_key=True, autoincrement=True, comment="์š”์†Œ ๊ณ ์œ  ID")
page_id = Column(Integer, ForeignKey("pages.page_id", ondelete="CASCADE"), nullable=False, comment="์†Œ์† ํŽ˜์ด์ง€ ID")
class_name = Column(String(100), nullable=False, comment="ํด๋ž˜์Šค๋ช… (question_number/figure/table/text ๋“ฑ)")
confidence = Column(Float, nullable=False, comment="์‹ ๋ขฐ๋„ (0.0~1.0)")
# ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์ขŒํ‘œ
bbox_x = Column(Integer, nullable=False, comment="X ์ขŒํ‘œ (์™ผ์ชฝ ์ƒ๋‹จ)")
bbox_y = Column(Integer, nullable=False, comment="Y ์ขŒํ‘œ (์™ผ์ชฝ ์ƒ๋‹จ)")
bbox_width = Column(Integer, nullable=False, comment="๋„ˆ๋น„ (ํ”ฝ์…€)")
bbox_height = Column(Integer, nullable=False, comment="๋†’์ด (ํ”ฝ์…€)")
# ์ž๋™ ๊ณ„์‚ฐ ์ปฌ๋Ÿผ (GENERATED COLUMN) - SQLAlchemy์—์„œ๋Š” Computed ์‚ฌ์šฉ
area = Column(Integer, Computed("bbox_width * bbox_height"), comment="๋ฉด์  (์ž๋™ ๊ณ„์‚ฐ)")
y_position = Column(Integer, Computed("bbox_y"), comment="Y ์ •๋ ฌ์šฉ ์ขŒํ‘œ (์ž๋™ ๊ณ„์‚ฐ)")
x_position = Column(Integer, Computed("bbox_x"), comment="X ์ •๋ ฌ์šฉ ์ขŒํ‘œ (์ž๋™ ๊ณ„์‚ฐ)")
created_at = Column(DateTime, default=func.now(), comment="์ƒ์„ฑ์ผ")
# ๊ด€๊ณ„ ์„ค์ •
page = relationship("Page", back_populates="layout_elements")
text_content = relationship("TextContent", back_populates="layout_element", uselist=False, cascade="all, delete-orphan")
ai_description = relationship("AIDescription", back_populates="layout_element", uselist=False, cascade="all, delete-orphan")
question_group = relationship("QuestionGroup", back_populates="anchor_element", uselist=False, cascade="all, delete-orphan")
question_elements = relationship("QuestionElement", back_populates="layout_element", cascade="all, delete-orphan")
# ์ธ๋ฑ์Šค
__table_args__ = (
Index("idx_page_id", "page_id"),
Index("idx_class_name", "class_name"),
Index("idx_position", "page_id", "y_position", "x_position"), # ๋ณตํ•ฉ ์ธ๋ฑ์Šค
)
def __repr__(self):
return f"<LayoutElement(element_id={self.element_id}, class='{self.class_name}', conf={self.confidence:.3f})>"
# ============================================================================
# 6. Text Contents - OCR ๊ฒฐ๊ณผ
# ============================================================================
class TextContent(Base):
"""OCR ๊ฒฐ๊ณผ ํ…Œ์ด๋ธ”"""
__tablename__ = "text_contents"
text_id = Column(Integer, primary_key=True, autoincrement=True, comment="OCR ๊ฒฐ๊ณผ ๊ณ ์œ  ID")
element_id = Column(Integer, ForeignKey("layout_elements.element_id", ondelete="CASCADE"), unique=True, nullable=False, comment="๋ ˆ์ด์•„์›ƒ ์š”์†Œ ID (1:1 ๋งคํ•‘)")
ocr_text = Column(Text, nullable=False, comment="OCR ์ถ”์ถœ ํ…์ŠคํŠธ")
ocr_engine = Column(String(50), default="PaddleOCR", comment="์‚ฌ์šฉํ•œ OCR ์—”์ง„")
ocr_confidence = Column(Float, nullable=True, comment="OCR ์‹ ๋ขฐ๋„ (0.0~1.0)")
language = Column(String(10), default="ko", comment="์–ธ์–ด ์ฝ”๋“œ (ko/en/ja/zh)")
created_at = Column(DateTime, default=func.now(), comment="์ƒ์„ฑ์ผ")
# ๊ด€๊ณ„ ์„ค์ •
layout_element = relationship("LayoutElement", back_populates="text_content")
# ์ธ๋ฑ์Šค
__table_args__ = (
UniqueConstraint("element_id", name="uk_element"),
Index("idx_language", "language"),
# FULLTEXT ์ธ๋ฑ์Šค๋Š” MySQL ํŠน์ • ๊ธฐ๋Šฅ์ด๋ฏ€๋กœ ์ƒ๋žต (ํ•„์š”์‹œ Raw SQL๋กœ ์ถ”๊ฐ€)
)
def __repr__(self):
return f"<TextContent(text_id={self.text_id}, element={self.element_id}, engine='{self.ocr_engine}')>"
# ============================================================================
# 7. AI Descriptions - AI ์ƒ์„ฑ ์„ค๋ช…
# ============================================================================
class AIDescription(Base):
"""AI ์ƒ์„ฑ ์„ค๋ช… ํ…Œ์ด๋ธ” (figure/table ์„ค๋ช…)"""
__tablename__ = "ai_descriptions"
ai_desc_id = Column(Integer, primary_key=True, autoincrement=True, comment="AI ์„ค๋ช… ๊ณ ์œ  ID")
element_id = Column(Integer, ForeignKey("layout_elements.element_id", ondelete="CASCADE"), unique=True, nullable=False, comment="๋ ˆ์ด์•„์›ƒ ์š”์†Œ ID (1:1 ๋งคํ•‘)")
description = Column(Text, nullable=False, comment="AI๊ฐ€ ์ƒ์„ฑํ•œ ์„ค๋ช… ํ…์ŠคํŠธ")
ai_model = Column(String(100), default="gpt-4o-mini", comment="์‚ฌ์šฉํ•œ AI ๋ชจ๋ธ๋ช…")
prompt_used = Column(Text, nullable=True, comment="์‚ฌ์šฉํ•œ ํ”„๋กฌํ”„ํŠธ (๋””๋ฒ„๊น…์šฉ)")
created_at = Column(DateTime, default=func.now(), comment="์ƒ์„ฑ์ผ")
# ๊ด€๊ณ„ ์„ค์ •
layout_element = relationship("LayoutElement", back_populates="ai_description")
# ์ธ๋ฑ์Šค
__table_args__ = (
UniqueConstraint("element_id", name="uk_element"),
Index("idx_ai_model", "ai_model"),
)
def __repr__(self):
return f"<AIDescription(ai_desc_id={self.ai_desc_id}, element={self.element_id}, model='{self.ai_model}')>"
# ============================================================================
# 8. Question Groups - ๋ฌธ์ œ ๊ทธ๋ฃน (v2: ์•ต์ปค ์š”์†Œ ๊ธฐ๋ฐ˜)
# ============================================================================
class QuestionGroup(Base):
"""๋ฌธ์ œ ๊ทธ๋ฃน ํ…Œ์ด๋ธ” (์•ต์ปค ์š”์†Œ ๊ธฐ์ค€)"""
__tablename__ = "question_groups"
question_group_id = Column(Integer, primary_key=True, autoincrement=True, comment="๋ฌธ์ œ ๊ทธ๋ฃน ๊ณ ์œ  ID")
page_id = Column(Integer, ForeignKey("pages.page_id", ondelete="CASCADE"), nullable=False, comment="์†Œ์† ํŽ˜์ด์ง€ ID")
anchor_element_id = Column(Integer, ForeignKey("layout_elements.element_id", ondelete="CASCADE"), unique=True, nullable=False, comment="์•ต์ปค ์š”์†Œ ID (FK: layout_elements)")
# Y์ขŒํ‘œ ๋ฒ”์œ„
start_y = Column(Integer, nullable=False, comment="๋ฌธ์ œ ์‹œ์ž‘ Y์ขŒํ‘œ")
end_y = Column(Integer, nullable=False, comment="๋ฌธ์ œ ์ข…๋ฃŒ Y์ขŒํ‘œ")
# ํ†ต๊ณ„ ์ •๋ณด
element_count = Column(Integer, default=0, comment="๋ฌธ์ œ์— ์†ํ•œ ์š”์†Œ ๊ฐœ์ˆ˜ (์ž์‹ ์š”์†Œ ์ˆ˜)")
created_at = Column(DateTime, default=func.now(), comment="์ƒ์„ฑ์ผ")
# ๊ด€๊ณ„ ์„ค์ •
page = relationship("Page", back_populates="question_groups")
anchor_element = relationship("LayoutElement", back_populates="question_group")
question_elements = relationship("QuestionElement", back_populates="question_group", cascade="all, delete-orphan")
# ์ธ๋ฑ์Šค ๋ฐ ์ œ์•ฝ์กฐ๊ฑด
__table_args__ = (
UniqueConstraint("anchor_element_id", name="uk_anchor_element"),
Index("idx_page_id", "page_id"),
)
def __repr__(self):
return f"<QuestionGroup(group_id={self.question_group_id}, anchor={self.anchor_element_id}, Y={self.start_y}-{self.end_y})>"
# ============================================================================
# 9. Question Elements - ๋ฌธ์ œ-์š”์†Œ ๋งคํ•‘
# ============================================================================
class QuestionElement(Base):
"""๋ฌธ์ œ-์š”์†Œ ๋งคํ•‘ ํ…Œ์ด๋ธ” (์ž์‹ ์š”์†Œ ๊ด€๋ฆฌ)"""
__tablename__ = "question_elements"
qe_id = Column(Integer, primary_key=True, autoincrement=True, comment="๋งคํ•‘ ๋ ˆ์ฝ”๋“œ ๊ณ ์œ  ID")
question_group_id = Column(Integer, ForeignKey("question_groups.question_group_id", ondelete="CASCADE"), nullable=False, comment="๋ฌธ์ œ ๊ทธ๋ฃน ID")
element_id = Column(Integer, ForeignKey("layout_elements.element_id", ondelete="CASCADE"), nullable=False, comment="์ž์‹ ์š”์†Œ ID")
order_in_question = Column(Integer, nullable=False, comment="๋ฌธ์ œ ๋‚ด ์š”์†Œ ์ˆœ์„œ (1, 2, 3, ...) - Y์ขŒํ‘œ ๊ธฐ์ค€")
created_at = Column(DateTime, default=func.now(), comment="์ƒ์„ฑ์ผ")
# ๊ด€๊ณ„ ์„ค์ •
question_group = relationship("QuestionGroup", back_populates="question_elements")
layout_element = relationship("LayoutElement", back_populates="question_elements")
# ์ธ๋ฑ์Šค ๋ฐ ์ œ์•ฝ์กฐ๊ฑด
__table_args__ = (
UniqueConstraint("question_group_id", "element_id", name="uk_question_element"),
Index("idx_order", "question_group_id", "order_in_question"),
)
def __repr__(self):
return f"<QuestionElement(qe_id={self.qe_id}, group={self.question_group_id}, element={self.element_id}, order={self.order_in_question})>"
# ============================================================================
# 10. Text Versions - ํ…์ŠคํŠธ ๋ฒ„์ „ ๊ด€๋ฆฌ
# ============================================================================
class TextVersion(Base):
"""ํ…์ŠคํŠธ ๋ฒ„์ „ ๊ด€๋ฆฌ ํ…Œ์ด๋ธ”"""
__tablename__ = "text_versions"
version_id = Column(Integer, primary_key=True, autoincrement=True, comment="๋ฒ„์ „ ๊ณ ์œ  ID")
page_id = Column(Integer, ForeignKey("pages.page_id", ondelete="CASCADE"), nullable=False, comment="์†Œ์† ํŽ˜์ด์ง€ ID")
user_id = Column(Integer, ForeignKey("users.user_id", ondelete="SET NULL"), nullable=True, comment="์ˆ˜์ •ํ•œ ์‚ฌ์šฉ์ž ID (์‚ฌ์šฉ์ž ์ˆ˜์ • ์‹œ)")
content = Column(Text, nullable=False, comment="ํ…์ŠคํŠธ ๋‚ด์šฉ")
version_number = Column(Integer, nullable=False, comment="๋ฒ„์ „ ๋ฒˆํ˜ธ (1, 2, 3, ...)")
version_type = Column(
Enum(VersionTypeEnum),
nullable=False,
comment="๋ฒ„์ „ ์œ ํ˜•: original/auto_formatted/user_edited"
)
is_current = Column(Boolean, default=False, comment="ํ˜„์žฌ ๋ฒ„์ „ ์—ฌ๋ถ€")
created_at = Column(DateTime, default=func.now(), comment="๋ฒ„์ „ ์ƒ์„ฑ์ผ")
# ๊ด€๊ณ„ ์„ค์ •
page = relationship("Page", back_populates="text_versions")
user = relationship("User", back_populates="text_versions")
# ์ธ๋ฑ์Šค ๋ฐ ์ œ์•ฝ์กฐ๊ฑด
__table_args__ = (
UniqueConstraint("page_id", "version_number", name="uk_page_version"),
Index("idx_page_id", "page_id"),
Index("idx_is_current", "is_current"),
)
def __repr__(self):
return f"<TextVersion(version_id={self.version_id}, page={self.page_id}, v={self.version_number}, type='{self.version_type.value}')>"
# ============================================================================
# 11. Formatting Rules - ํฌ๋งทํŒ… ๊ทœ์น™ (v2: ์•ต์ปค/์ž์‹ ํด๋ž˜์Šค ๊ทœ์น™)
# ============================================================================
class FormattingRule(Base):
"""ํฌ๋งทํŒ… ๊ทœ์น™ ํ…Œ์ด๋ธ”"""
__tablename__ = "formatting_rules"
rule_id = Column(Integer, primary_key=True, autoincrement=True, comment="๊ทœ์น™ ๊ณ ์œ  ID")
doc_type_id = Column(Integer, ForeignKey("document_types.doc_type_id", ondelete="CASCADE"), nullable=False, comment="๋ฌธ์„œ ํƒ€์ž… ID")
class_name = Column(String(100), nullable=False, comment="์ ์šฉ ํด๋ž˜์Šค๋ช… (question_number/figure/text ๋“ฑ)")
# ํฌ๋งทํŒ… ์„ค์ •
prefix = Column(String(50), default="", comment="์ ‘๋‘์‚ฌ (์˜ˆ: '\\n\\n', ' ')")
suffix = Column(String(50), default="", comment="์ ‘๋ฏธ์‚ฌ (์˜ˆ: '. ', '\\n')")
indent_level = Column(Integer, default=0, comment="๋“ค์—ฌ์“ฐ๊ธฐ ๋ ˆ๋ฒจ (0~10)")
# ์Šคํƒ€์ผ ์„ค์ • (์„ ํƒ ์‚ฌํ•ญ)
font_size = Column(String(20), nullable=True, comment="ํฐํŠธ ํฌ๊ธฐ (์˜ˆ: '14pt')")
font_weight = Column(String(20), nullable=True, comment="ํฐํŠธ ๋‘๊ป˜ (์˜ˆ: 'bold')")
created_at = Column(DateTime, default=func.now(), comment="๊ทœ์น™ ์ƒ์„ฑ์ผ")
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), comment="๊ทœ์น™ ์ˆ˜์ •์ผ")
# ๊ด€๊ณ„ ์„ค์ •
document_type = relationship("DocumentType", back_populates="formatting_rules")
# ์ธ๋ฑ์Šค ๋ฐ ์ œ์•ฝ์กฐ๊ฑด
__table_args__ = (
UniqueConstraint("doc_type_id", "class_name", name="uk_type_class"),
Index("idx_doc_type_id", "doc_type_id"),
)
def __repr__(self):
return f"<FormattingRule(rule_id={self.rule_id}, doc_type={self.doc_type_id}, class='{self.class_name}')>"
# ============================================================================
# 12. Combined Results - ํ†ตํ•ฉ ๋ฌธ์„œ ์บ์‹œ
# ============================================================================
class CombinedResult(Base):
"""ํ†ตํ•ฉ ๋ฌธ์„œ ์บ์‹œ ํ…Œ์ด๋ธ”"""
__tablename__ = "combined_results"
combined_id = Column(Integer, primary_key=True, autoincrement=True, comment="ํ†ตํ•ฉ ๊ฒฐ๊ณผ ๊ณ ์œ  ID")
project_id = Column(Integer, ForeignKey("projects.project_id", ondelete="CASCADE"), unique=True, nullable=False, comment="ํ”„๋กœ์ ํŠธ ID (1:1 ๋งคํ•‘)")
combined_text = Column(Text(4294967295), nullable=False, comment="ํ†ตํ•ฉ๋œ ์ „์ฒด ํ…์ŠคํŠธ (ํŽ˜์ด์ง€๋ณ„ ๊ฒฐ๊ณผ ํ•ฉ์นจ) - LONGTEXT")
combined_stats = Column(JSON, nullable=True, comment="ํ†ต๊ณ„ ์ •๋ณด (JSON ํ˜•์‹: ํŽ˜์ด์ง€์ˆ˜, ๋‹จ์–ด์ˆ˜, ๋ฌธ์ œ์ˆ˜ ๋“ฑ)")
generated_at = Column(DateTime, default=func.now(), comment="์ตœ์ดˆ ์ƒ์„ฑ์ผ")
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), comment="๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ์ผ")
# ๊ด€๊ณ„ ์„ค์ •
project = relationship("Project", back_populates="combined_result")
# ์ธ๋ฑ์Šค ๋ฐ ์ œ์•ฝ์กฐ๊ฑด
__table_args__ = (
UniqueConstraint("project_id", name="uk_project"),
Index("idx_project_id", "project_id"),
)
def __repr__(self):
return f"<CombinedResult(combined_id={self.combined_id}, project={self.project_id})>"
# ============================================================================
# ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ์ˆœ์„œ (์ฐธ๊ณ ์šฉ)
# ============================================================================
"""
์™ธ๋ž˜ ํ‚ค ์˜์กด์„ฑ ์ˆœ์„œ:
1. User (๋…๋ฆฝ)
2. DocumentType (๋…๋ฆฝ)
3. Project (User, DocumentType ์˜์กด)
4. Page (Project ์˜์กด)
5. LayoutElement (Page ์˜์กด)
6. TextContent (LayoutElement ์˜์กด)
7. AIDescription (LayoutElement ์˜์กด)
8. QuestionGroup (Page, LayoutElement ์˜์กด) - ์•ต์ปค ๊ด€๊ณ„
9. QuestionElement (QuestionGroup, LayoutElement ์˜์กด)
10. TextVersion (Page, User ์˜์กด)
11. FormattingRule (DocumentType ์˜์กด)
12. CombinedResult (Project ์˜์กด)
"""