feat: 소설 분기 시스템 및 트리 메모장 구현
🌟 주요 기능: - 트리 구조 메모장 시스템 - 소설 분기 관리 (정사 경로 설정) - 중앙 배치 트리 다이어그램 - 정사 경로 목차 뷰 - 인라인 편집 기능 📚 백엔드: - MemoTree, MemoNode 모델 추가 - 정사 경로 자동 순서 관리 - 분기점에서 하나만 선택 가능한 로직 - RESTful API 엔드포인트 🎨 프론트엔드: - memo-tree.html: 트리 다이어그램 에디터 - story-view.html: 정사 경로 목차 뷰 - SVG 연결선으로 시각적 트리 표현 - Alpine.js 기반 반응형 UI - Monaco Editor 통합 ✨ 특별 기능: - 정사 경로 황금색 배지 표시 - 확대/축소 및 패닝 지원 - 드래그 앤 드롭 준비 - 내보내기 및 인쇄 기능 - 인라인 편집 모달
This commit is contained in:
205
backend/src/schemas/memo_tree.py
Normal file
205
backend/src/schemas/memo_tree.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""
|
||||
트리 구조 메모장 Pydantic 스키마
|
||||
"""
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
# 기본 스키마들
|
||||
class MemoTreeBase(BaseModel):
|
||||
"""메모 트리 기본 스키마"""
|
||||
title: str = Field(..., min_length=1, max_length=255)
|
||||
description: Optional[str] = None
|
||||
tree_type: str = Field(default="general", pattern="^(general|novel|research|project)$")
|
||||
template_data: Optional[Dict[str, Any]] = None
|
||||
settings: Optional[Dict[str, Any]] = None
|
||||
is_public: bool = False
|
||||
|
||||
|
||||
class MemoTreeCreate(MemoTreeBase):
|
||||
"""메모 트리 생성 요청"""
|
||||
pass
|
||||
|
||||
|
||||
class MemoTreeUpdate(BaseModel):
|
||||
"""메모 트리 업데이트 요청"""
|
||||
title: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
description: Optional[str] = None
|
||||
tree_type: Optional[str] = Field(None, pattern="^(general|novel|research|project)$")
|
||||
template_data: Optional[Dict[str, Any]] = None
|
||||
settings: Optional[Dict[str, Any]] = None
|
||||
is_public: Optional[bool] = None
|
||||
is_archived: Optional[bool] = None
|
||||
|
||||
|
||||
class MemoTreeResponse(MemoTreeBase):
|
||||
"""메모 트리 응답"""
|
||||
id: str
|
||||
user_id: str
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime]
|
||||
is_archived: bool
|
||||
node_count: Optional[int] = 0 # 노드 개수
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# 메모 노드 스키마들
|
||||
class MemoNodeBase(BaseModel):
|
||||
"""메모 노드 기본 스키마"""
|
||||
title: str = Field(..., min_length=1, max_length=500)
|
||||
content: Optional[str] = None
|
||||
node_type: str = Field(default="memo", pattern="^(folder|memo|chapter|character|plot)$")
|
||||
tags: Optional[List[str]] = None
|
||||
node_metadata: Optional[Dict[str, Any]] = None
|
||||
status: str = Field(default="draft", pattern="^(draft|writing|review|complete)$")
|
||||
|
||||
# 정사 경로 관련 필드
|
||||
is_canonical: Optional[bool] = False
|
||||
canonical_order: Optional[int] = None
|
||||
|
||||
|
||||
class MemoNodeCreate(MemoNodeBase):
|
||||
"""메모 노드 생성 요청"""
|
||||
tree_id: str
|
||||
parent_id: Optional[str] = None
|
||||
sort_order: Optional[int] = 0
|
||||
|
||||
|
||||
class MemoNodeUpdate(BaseModel):
|
||||
"""메모 노드 업데이트 요청"""
|
||||
title: Optional[str] = Field(None, min_length=1, max_length=500)
|
||||
content: Optional[str] = None
|
||||
node_type: Optional[str] = Field(None, pattern="^(folder|memo|chapter|character|plot)$")
|
||||
parent_id: Optional[str] = None
|
||||
sort_order: Optional[int] = None
|
||||
tags: Optional[List[str]] = None
|
||||
node_metadata: Optional[Dict[str, Any]] = None
|
||||
status: Optional[str] = Field(None, pattern="^(draft|writing|review|complete)$")
|
||||
|
||||
# 정사 경로 관련 필드
|
||||
is_canonical: Optional[bool] = None
|
||||
canonical_order: Optional[int] = None
|
||||
|
||||
|
||||
class MemoNodeMove(BaseModel):
|
||||
"""메모 노드 이동 요청"""
|
||||
parent_id: Optional[str] = None
|
||||
sort_order: int = 0
|
||||
|
||||
|
||||
class MemoNodeResponse(MemoNodeBase):
|
||||
"""메모 노드 응답"""
|
||||
id: str
|
||||
tree_id: str
|
||||
parent_id: Optional[str]
|
||||
user_id: str
|
||||
sort_order: int
|
||||
depth_level: int
|
||||
path: Optional[str]
|
||||
word_count: int
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime]
|
||||
|
||||
# 정사 경로 관련 필드
|
||||
is_canonical: bool
|
||||
canonical_order: Optional[int]
|
||||
story_path: Optional[str]
|
||||
|
||||
# 관계 데이터
|
||||
children_count: Optional[int] = 0
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# 트리 구조 응답
|
||||
class MemoTreeWithNodes(MemoTreeResponse):
|
||||
"""노드가 포함된 메모 트리 응답"""
|
||||
nodes: List[MemoNodeResponse] = []
|
||||
|
||||
|
||||
# 노드 버전 스키마들
|
||||
class MemoNodeVersionResponse(BaseModel):
|
||||
"""메모 노드 버전 응답"""
|
||||
id: str
|
||||
node_id: str
|
||||
version_number: int
|
||||
title: str
|
||||
content: Optional[str]
|
||||
node_metadata: Optional[Dict[str, Any]]
|
||||
created_at: datetime
|
||||
created_by: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# 공유 스키마들
|
||||
class MemoTreeShareCreate(BaseModel):
|
||||
"""메모 트리 공유 생성 요청"""
|
||||
shared_with_user_email: str
|
||||
permission_level: str = Field(default="read", pattern="^(read|write|admin)$")
|
||||
|
||||
|
||||
class MemoTreeShareResponse(BaseModel):
|
||||
"""메모 트리 공유 응답"""
|
||||
id: str
|
||||
tree_id: str
|
||||
shared_with_user_id: str
|
||||
shared_with_user_email: str
|
||||
shared_with_user_name: str
|
||||
permission_level: str
|
||||
created_at: datetime
|
||||
created_by: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# 검색 및 필터링
|
||||
class MemoSearchRequest(BaseModel):
|
||||
"""메모 검색 요청"""
|
||||
query: str = Field(..., min_length=1)
|
||||
tree_id: Optional[str] = None
|
||||
node_types: Optional[List[str]] = None
|
||||
tags: Optional[List[str]] = None
|
||||
status: Optional[List[str]] = None
|
||||
|
||||
|
||||
class MemoSearchResult(BaseModel):
|
||||
"""메모 검색 결과"""
|
||||
node: MemoNodeResponse
|
||||
tree: MemoTreeResponse
|
||||
matches: List[Dict[str, Any]] # 매치된 부분들
|
||||
relevance_score: float
|
||||
|
||||
|
||||
# 통계 스키마
|
||||
class MemoTreeStats(BaseModel):
|
||||
"""메모 트리 통계"""
|
||||
total_nodes: int
|
||||
nodes_by_type: Dict[str, int]
|
||||
nodes_by_status: Dict[str, int]
|
||||
total_words: int
|
||||
last_updated: Optional[datetime]
|
||||
|
||||
|
||||
# 내보내기 스키마
|
||||
class ExportRequest(BaseModel):
|
||||
"""내보내기 요청"""
|
||||
tree_id: str
|
||||
format: str = Field(..., pattern="^(markdown|html|pdf|docx)$")
|
||||
include_metadata: bool = True
|
||||
node_types: Optional[List[str]] = None
|
||||
|
||||
|
||||
class ExportResponse(BaseModel):
|
||||
"""내보내기 응답"""
|
||||
file_url: str
|
||||
file_name: str
|
||||
file_size: int
|
||||
created_at: datetime
|
||||
Reference in New Issue
Block a user