# backend/app/models.py # 주의: `from __future__ import annotations` 를 쓰지 않는다 — # SQLAlchemy 2.x 가 Relationship 의 list["Project"] 문자열 주석을 해석하지 못해 # 매퍼 초기화에 실패한다(ForwardRef 로 평가되도록 둔다). from datetime import UTC, date, datetime from enum import Enum from typing import Optional from sqlmodel import Field, Relationship, SQLModel def now() -> datetime: return datetime.now(UTC) # ---------- enums (값은 CONTRACT 고정) ---------- class TaskStatus(str, Enum): todo = "todo" doing = "doing" waiting = "waiting" review = "review" done = "done" class Prio(str, Enum): high = "높음" normal = "보통" low = "낮음" class InboxKind(str, Enum): text = "text" voice = "voice" image = "image" class InboxStatus(str, Enum): new = "new" classified = "classified" confirmed = "confirmed" dismissed = "dismissed" class ClsType(str, Enum): task = "task" event = "event" idea = "idea" class Sphere(str, Enum): work = "work" life = "life" # ---------- core ---------- class Person(SQLModel, table=True): __tablename__ = "person" id: str = Field(primary_key=True) # 예: "jiwoo" name: str initial: str color: str # 예: "var(--blue)" / "oklch(0.66 0.13 200)" is_me: bool = False class Folder(SQLModel, table=True): __tablename__ = "folder" id: str = Field(primary_key=True) # "work" / "life" name: str # "업무" / "개인" tone: str = "ink" # tone 집합: blue|violet|coral|green|amber|ink|faint icon: str = "folder" # "folder" / "heart" sort_order: int = 0 is_system: bool = True projects: list["Project"] = Relationship(back_populates="folder") class Project(SQLModel, table=True): __tablename__ = "project" id: str = Field(primary_key=True) # "biz", "biz-okr", ... folder_id: str = Field(foreign_key="folder.id") parent_id: Optional[str] = Field(default=None, foreign_key="project.id") # 무한 중첩 name: str tone: str = "ink" sort_order: int = 0 pinned: bool = False # 즐겨찾기 folder: Optional[Folder] = Relationship(back_populates="projects") parent: Optional["Project"] = Relationship( back_populates="children", sa_relationship_kwargs={"remote_side": "Project.id"}, ) children: list["Project"] = Relationship(back_populates="parent") tasks: list["Task"] = Relationship(back_populates="project") class Task(SQLModel, table=True): __tablename__ = "task" id: str = Field(primary_key=True) # "k1", "kx3", ... project_id: str = Field(foreign_key="project.id") parent_id: Optional[str] = Field(default=None, foreign_key="task.id") # 무한 중첩 하위작업 title: str status: TaskStatus = TaskStatus.todo assignee_id: Optional[str] = Field(default=None, foreign_key="person.id") due: Optional[date] = None # 시드는 "06-08" → 2026-06-08 로 적재 prio: Prio = Prio.normal notes: str = "" # HTML 허용 est: str = "" delegated: bool = False sort_order: int = 0 created_at: datetime = Field(default_factory=now) updated_at: datetime = Field(default_factory=now) project: Optional[Project] = Relationship(back_populates="tasks") parent: Optional["Task"] = Relationship( back_populates="children", sa_relationship_kwargs={"remote_side": "Task.id"}, ) children: list["Task"] = Relationship(back_populates="parent") comments: list["TaskComment"] = Relationship(back_populates="task") assignee: Optional[Person] = Relationship() class TaskComment(SQLModel, table=True): __tablename__ = "task_comment" id: str = Field(primary_key=True) # TEXT PK 예: "c1" task_id: str = Field(foreign_key="task.id") person_id: str = Field(foreign_key="person.id") text: str created_at: datetime = Field(default_factory=now) task: Optional[Task] = Relationship(back_populates="comments") person: Optional[Person] = Relationship() # ---------- inbox (연합의 핵심) ---------- class InboxItem(SQLModel, table=True): __tablename__ = "inbox_item" id: str = Field(primary_key=True) # "s1", ... kind: InboxKind = InboxKind.text raw: str status: InboxStatus = InboxStatus.new created_at: datetime = Field(default_factory=now) materialized_task_id: Optional[str] = Field(default=None, foreign_key="task.id") classifications: list["InboxClassification"] = Relationship(back_populates="item") class InboxClassification(SQLModel, table=True): __tablename__ = "inbox_classification" id: str = Field(primary_key=True) # TEXT PK 예: "cls1" inbox_item_id: str = Field(foreign_key="inbox_item.id") # 1:1 최신 type: ClsType sphere: Sphere project_id: Optional[str] = Field(default=None, foreign_key="project.id") proj_label: str = "" # 예: "개인 › 여행 — 한국" tone: str = "ink" due_text: str = "" # 예: "출발 전 · ~6/14" when_text: str = "" # 예: "오늘 21:00 빈 시간 추천" extra: str = "" # 예: "가격 추적 알림 켜둠" reason: str = "" # 사람이 읽는 한국어 설명 confidence: float = 0.0 model: str = "" # 사용된 provider/model 표기 created_at: datetime = Field(default_factory=now) item: Optional[InboxItem] = Relationship(back_populates="classifications") # ---------- 대시보드 읽기전용 시드 ---------- class Event(SQLModel, table=True): __tablename__ = "event" id: str = Field(primary_key=True) # TEXT PK 예: "e1" time: str title: str tag: str = "" dur: str = "" tone: str = "ink" # tone 키 값('blue' 등). 'var(--blue)' 저장 금지 soon: bool = False sort_order: int = 0 class Approval(SQLModel, table=True): __tablename__ = "approval" id: str = Field(primary_key=True) # "a1", ... icon: str tone: str risk: str # "low" | "high" time: str title: str detail: str = "" cta: str = "" alt: str = "" undo_label: str = "" sort_order: int = 0 class Goal(SQLModel, table=True): __tablename__ = "goal" id: str = Field(primary_key=True) # TEXT PK 예: "g1" title: str pct: int sub: str = "" tone: str = "blue" # tone 키 값('blue' 등). 'var(--blue)' 저장 금지 sort_order: int = 0 class Briefing(SQLModel, table=True): """단일 row(id=1). 아침 브리핑 + 날씨/출근/수면 + saved_today/today_routed.""" __tablename__ = "briefing" id: Optional[int] = Field(default=None, primary_key=True) # 단일 row id=1 today: str = "" # 히어로 날짜 라벨 (예: "6월 7일 일요일") weather_temp: int = 0 # 현재 기온 (예: 24) weather_cond: str = "" # "맑음 · 한낮 28°" — "한낮 28°"는 일 최고기온 weather_icon: str = "cloudSun" # 'cloudSun' 저장 → 표시 시 'sun' 으로 매핑 commute: str = "" sleep: str = "" note: str = "" # HTML 허용 saved_today: str = "" # "47분" today_routed: int = 0 # 7