🎉 Initial commit: Document Server MVP
✨ Features implemented: - FastAPI backend with JWT authentication - PostgreSQL database with async SQLAlchemy - HTML document viewer with smart highlighting - Note system connected to highlights (1:1 relationship) - Bookmark system for quick navigation - Integrated search (documents + notes) - Tag system for document organization - Docker containerization with Nginx 🔧 Technical stack: - Backend: FastAPI + PostgreSQL + Redis - Frontend: Alpine.js + Tailwind CSS - Authentication: JWT tokens - File handling: HTML + PDF support - Search: Full-text search with relevance scoring 📋 Core functionality: - Text selection → Highlight creation - Highlight → Note attachment - Note management with search/filtering - Bookmark creation at scroll positions - Document upload with metadata - User management (admin creates accounts)
This commit is contained in:
17
backend/src/models/__init__.py
Normal file
17
backend/src/models/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
모델 패키지 초기화
|
||||
"""
|
||||
from src.models.user import User
|
||||
from src.models.document import Document, Tag
|
||||
from src.models.highlight import Highlight
|
||||
from src.models.note import Note
|
||||
from src.models.bookmark import Bookmark
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
"Document",
|
||||
"Tag",
|
||||
"Highlight",
|
||||
"Note",
|
||||
"Bookmark",
|
||||
]
|
||||
42
backend/src/models/bookmark.py
Normal file
42
backend/src/models/bookmark.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
책갈피 모델
|
||||
"""
|
||||
from sqlalchemy import Column, String, DateTime, Text, Integer, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
|
||||
from src.core.database import Base
|
||||
|
||||
|
||||
class Bookmark(Base):
|
||||
"""책갈피 테이블"""
|
||||
__tablename__ = "bookmarks"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
|
||||
# 연결 정보
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
||||
document_id = Column(UUID(as_uuid=True), ForeignKey("documents.id"), nullable=False)
|
||||
|
||||
# 책갈피 정보
|
||||
title = Column(String(200), nullable=False) # 책갈피 제목
|
||||
description = Column(Text, nullable=True) # 설명
|
||||
|
||||
# 위치 정보
|
||||
page_number = Column(Integer, nullable=True) # 페이지 번호 (추정)
|
||||
scroll_position = Column(Integer, default=0) # 스크롤 위치 (픽셀)
|
||||
element_id = Column(String(100), nullable=True) # 특정 요소 ID
|
||||
element_selector = Column(Text, nullable=True) # CSS 선택자
|
||||
|
||||
# 메타데이터
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# 관계
|
||||
user = relationship("User", backref="bookmarks")
|
||||
document = relationship("Document", back_populates="bookmarks")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Bookmark(title='{self.title}', document='{self.document_id}')>"
|
||||
81
backend/src/models/document.py
Normal file
81
backend/src/models/document.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
문서 모델
|
||||
"""
|
||||
from sqlalchemy import Column, String, DateTime, Text, Integer, Boolean, ForeignKey, Table
|
||||
from sqlalchemy.dialects.postgresql import UUID, ARRAY
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
|
||||
from src.core.database import Base
|
||||
|
||||
|
||||
# 문서-태그 다대다 관계 테이블
|
||||
document_tags = Table(
|
||||
'document_tags',
|
||||
Base.metadata,
|
||||
Column('document_id', UUID(as_uuid=True), ForeignKey('documents.id'), primary_key=True),
|
||||
Column('tag_id', UUID(as_uuid=True), ForeignKey('tags.id'), primary_key=True)
|
||||
)
|
||||
|
||||
|
||||
class Document(Base):
|
||||
"""문서 테이블"""
|
||||
__tablename__ = "documents"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
title = Column(String(500), nullable=False, index=True)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
# 파일 정보
|
||||
html_path = Column(String(1000), nullable=False) # HTML 파일 경로
|
||||
pdf_path = Column(String(1000), nullable=True) # PDF 원본 경로 (선택)
|
||||
thumbnail_path = Column(String(1000), nullable=True) # 썸네일 경로
|
||||
|
||||
# 메타데이터
|
||||
file_size = Column(Integer, nullable=True) # 바이트 단위
|
||||
page_count = Column(Integer, nullable=True) # 페이지 수 (추정)
|
||||
language = Column(String(10), default="ko") # 문서 언어
|
||||
|
||||
# 업로드 정보
|
||||
uploaded_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
||||
original_filename = Column(String(500), nullable=True)
|
||||
|
||||
# 상태
|
||||
is_public = Column(Boolean, default=False) # 공개 여부
|
||||
is_processed = Column(Boolean, default=True) # 처리 완료 여부
|
||||
|
||||
# 시간 정보
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
document_date = Column(DateTime(timezone=True), nullable=True) # 문서 작성일 (사용자 입력)
|
||||
|
||||
# 관계
|
||||
uploader = relationship("User", backref="uploaded_documents")
|
||||
tags = relationship("Tag", secondary=document_tags, back_populates="documents")
|
||||
highlights = relationship("Highlight", back_populates="document", cascade="all, delete-orphan")
|
||||
bookmarks = relationship("Bookmark", back_populates="document", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Document(title='{self.title}', id='{self.id}')>"
|
||||
|
||||
|
||||
class Tag(Base):
|
||||
"""태그 테이블"""
|
||||
__tablename__ = "tags"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
name = Column(String(100), unique=True, nullable=False, index=True)
|
||||
color = Column(String(7), default="#3B82F6") # HEX 색상 코드
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
# 메타데이터
|
||||
created_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# 관계
|
||||
creator = relationship("User", backref="created_tags")
|
||||
documents = relationship("Document", secondary=document_tags, back_populates="tags")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Tag(name='{self.name}', color='{self.color}')>"
|
||||
47
backend/src/models/highlight.py
Normal file
47
backend/src/models/highlight.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
하이라이트 모델
|
||||
"""
|
||||
from sqlalchemy import Column, String, DateTime, Text, Integer, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
|
||||
from src.core.database import Base
|
||||
|
||||
|
||||
class Highlight(Base):
|
||||
"""하이라이트 테이블"""
|
||||
__tablename__ = "highlights"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
|
||||
# 연결 정보
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
||||
document_id = Column(UUID(as_uuid=True), ForeignKey("documents.id"), nullable=False)
|
||||
|
||||
# 텍스트 위치 정보
|
||||
start_offset = Column(Integer, nullable=False) # 시작 위치
|
||||
end_offset = Column(Integer, nullable=False) # 끝 위치
|
||||
selected_text = Column(Text, nullable=False) # 선택된 텍스트 (검색용)
|
||||
|
||||
# DOM 위치 정보 (정확한 복원을 위해)
|
||||
element_selector = Column(Text, nullable=True) # CSS 선택자
|
||||
start_container_xpath = Column(Text, nullable=True) # 시작 컨테이너 XPath
|
||||
end_container_xpath = Column(Text, nullable=True) # 끝 컨테이너 XPath
|
||||
|
||||
# 스타일 정보
|
||||
highlight_color = Column(String(7), default="#FFFF00") # HEX 색상 코드
|
||||
highlight_type = Column(String(20), default="highlight") # highlight, underline, etc.
|
||||
|
||||
# 메타데이터
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# 관계
|
||||
user = relationship("User", backref="highlights")
|
||||
document = relationship("Document", back_populates="highlights")
|
||||
note = relationship("Note", back_populates="highlight", uselist=False, cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Highlight(id='{self.id}', text='{self.selected_text[:50]}...')>"
|
||||
47
backend/src/models/note.py
Normal file
47
backend/src/models/note.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
메모 모델
|
||||
"""
|
||||
from sqlalchemy import Column, String, DateTime, Text, Boolean, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID, ARRAY
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
|
||||
from src.core.database import Base
|
||||
|
||||
|
||||
class Note(Base):
|
||||
"""메모 테이블 (하이라이트와 1:1 관계)"""
|
||||
__tablename__ = "notes"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
|
||||
# 연결 정보
|
||||
highlight_id = Column(UUID(as_uuid=True), ForeignKey("highlights.id"), nullable=False, unique=True)
|
||||
|
||||
# 메모 내용
|
||||
content = Column(Text, nullable=False)
|
||||
is_private = Column(Boolean, default=True) # 개인 메모 여부
|
||||
|
||||
# 태그 (메모 분류용)
|
||||
tags = Column(ARRAY(String), nullable=True) # ["중요", "질문", "아이디어"]
|
||||
|
||||
# 메타데이터
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# 관계
|
||||
highlight = relationship("Highlight", back_populates="note")
|
||||
|
||||
@property
|
||||
def user_id(self):
|
||||
"""하이라이트를 통해 사용자 ID 가져오기"""
|
||||
return self.highlight.user_id if self.highlight else None
|
||||
|
||||
@property
|
||||
def document_id(self):
|
||||
"""하이라이트를 통해 문서 ID 가져오기"""
|
||||
return self.highlight.document_id if self.highlight else None
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Note(id='{self.id}', content='{self.content[:50]}...')>"
|
||||
34
backend/src/models/user.py
Normal file
34
backend/src/models/user.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
사용자 모델
|
||||
"""
|
||||
from sqlalchemy import Column, String, Boolean, DateTime, Text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
|
||||
from src.core.database import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""사용자 테이블"""
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
email = Column(String(255), unique=True, index=True, nullable=False)
|
||||
hashed_password = Column(String(255), nullable=False)
|
||||
full_name = Column(String(255), nullable=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_admin = Column(Boolean, default=False)
|
||||
|
||||
# 메타데이터
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
last_login = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# 사용자 설정
|
||||
theme = Column(String(50), default="light") # light, dark
|
||||
language = Column(String(10), default="ko") # ko, en
|
||||
timezone = Column(String(50), default="Asia/Seoul")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User(email='{self.email}', full_name='{self.full_name}')>"
|
||||
Reference in New Issue
Block a user