You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1192 lines
26 KiB
Python
1192 lines
26 KiB
Python
# backend/app/schemas.py
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime
|
|
from typing import Any, Optional
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
from .models import ClsType, InboxKind, InboxStatus, Prio, Sphere, TaskStatus
|
|
|
|
|
|
# ---------- people ----------
|
|
class PersonOut(BaseModel):
|
|
id: str
|
|
name: str
|
|
initial: str
|
|
color: str
|
|
is_me: bool
|
|
|
|
|
|
# ---------- tree ----------
|
|
class ProjectNode(BaseModel):
|
|
id: str
|
|
folder_id: str
|
|
parent_id: Optional[str] = None
|
|
name: str
|
|
tone: str
|
|
sort_order: int
|
|
pinned: bool
|
|
task_count: int = 0 # 자신+하위 프로젝트의 작업 수 합
|
|
children: list[ProjectNode] = []
|
|
|
|
|
|
class FolderOut(BaseModel):
|
|
id: str
|
|
name: str
|
|
tone: str
|
|
icon: str
|
|
sort_order: int
|
|
is_system: bool
|
|
projects: list[ProjectNode] = [] # 최상위 프로젝트(중첩 children 포함)
|
|
|
|
|
|
class FolderCreate(BaseModel):
|
|
name: str
|
|
tone: Optional[str] = "ink"
|
|
icon: Optional[str] = "folder"
|
|
|
|
|
|
class FolderPatch(BaseModel):
|
|
name: Optional[str] = None
|
|
tone: Optional[str] = None
|
|
icon: Optional[str] = None
|
|
sort_order: Optional[int] = None
|
|
|
|
|
|
class ProjectCreate(BaseModel):
|
|
folder_id: str
|
|
parent_id: Optional[str] = None
|
|
name: str
|
|
tone: Optional[str] = "ink"
|
|
|
|
|
|
class ProjectPatch(BaseModel):
|
|
name: Optional[str] = None
|
|
tone: Optional[str] = None
|
|
parent_id: Optional[str] = None
|
|
sort_order: Optional[int] = None
|
|
|
|
|
|
# ---------- tasks ----------
|
|
class CommentOut(BaseModel):
|
|
id: str
|
|
task_id: str
|
|
person_id: str
|
|
text: str
|
|
created_at: datetime
|
|
edited_at: Optional[datetime] = None
|
|
|
|
|
|
class CommentPatch(BaseModel):
|
|
text: str
|
|
|
|
|
|
class TaskNode(BaseModel):
|
|
id: str
|
|
project_id: str
|
|
parent_id: Optional[str] = None
|
|
title: str
|
|
status: TaskStatus
|
|
assignee_id: Optional[str] = None
|
|
due: Optional[date] = None
|
|
prio: Prio
|
|
notes: str
|
|
est: str
|
|
delegated: bool
|
|
sort_order: int
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
comments: list[CommentOut] = []
|
|
children: list[TaskNode] = []
|
|
|
|
|
|
class TaskCreate(BaseModel):
|
|
title: str
|
|
project_id: str
|
|
parent_id: Optional[str] = None
|
|
status: Optional[TaskStatus] = TaskStatus.todo
|
|
assignee_id: Optional[str] = None
|
|
due: Optional[date] = None
|
|
prio: Optional[Prio] = Prio.normal
|
|
notes: Optional[str] = ""
|
|
est: Optional[str] = ""
|
|
|
|
|
|
class TaskPatch(BaseModel):
|
|
title: Optional[str] = None
|
|
project_id: Optional[str] = None
|
|
parent_id: Optional[str] = None
|
|
status: Optional[TaskStatus] = None
|
|
assignee_id: Optional[str] = None
|
|
due: Optional[date] = None
|
|
prio: Optional[Prio] = None
|
|
notes: Optional[str] = None
|
|
est: Optional[str] = None
|
|
delegated: Optional[bool] = None
|
|
sort_order: Optional[int] = None
|
|
|
|
|
|
class CommentCreate(BaseModel):
|
|
person_id: str
|
|
text: str
|
|
|
|
|
|
class TaskReorderRequest(BaseModel):
|
|
ids: list[str] # 같은 컬럼 내 새 순서대로 정렬된 task id 목록
|
|
|
|
|
|
# ---------- scaffold ----------
|
|
class ScaffoldItem(BaseModel):
|
|
title: str
|
|
est: str = ""
|
|
|
|
|
|
class ScaffoldOut(BaseModel):
|
|
kind: str # "연구 · 논문 작성" 등
|
|
icon: str # "brain"|"branch"|"file"|"list"
|
|
items: list[ScaffoldItem]
|
|
created: bool = False # True면 실제 생성됨, False면 미리보기
|
|
created_task_ids: list[str] = []
|
|
|
|
|
|
class ScaffoldRequest(BaseModel):
|
|
create: bool = False # True면 하위작업 실제 생성, False면 미리보기
|
|
use_llm: bool = False # LLM 보강 사용 여부
|
|
|
|
|
|
# ---------- risks ----------
|
|
class RiskOut(BaseModel):
|
|
kind: str # "지연 위험" | "업무 쏠림" | "의존성"
|
|
icon: str # "clock" | "scale" | "link"
|
|
tone: str # "coral" | "amber" | "violet"
|
|
task_id: Optional[str] = None # 관련 task id (cta 대상)
|
|
text: str # 한국어. 강조어를 ** ** 마크다운으로 감싼다
|
|
cta: Optional[str] = None # "작업 열기" | "후속 작업 보기"
|
|
|
|
|
|
# ---------- inbox ----------
|
|
class ClassificationOut(BaseModel):
|
|
id: Optional[str] = None
|
|
inbox_item_id: str
|
|
type: ClsType
|
|
sphere: Sphere
|
|
project_id: Optional[str] = None
|
|
proj_label: str
|
|
tone: str
|
|
due_text: str
|
|
when_text: str
|
|
extra: str
|
|
reason: str
|
|
confidence: float
|
|
model: str
|
|
created_at: Optional[datetime] = None
|
|
|
|
|
|
class InboxItemOut(BaseModel):
|
|
id: str
|
|
kind: InboxKind
|
|
raw: str
|
|
status: InboxStatus
|
|
created_at: datetime
|
|
materialized_task_id: Optional[str] = None
|
|
classification: Optional[ClassificationOut] = None # 최신 1건
|
|
|
|
|
|
class CaptureRequest(BaseModel):
|
|
kind: InboxKind = InboxKind.text
|
|
raw: str
|
|
|
|
|
|
class CaptureResponse(BaseModel):
|
|
item: InboxItemOut
|
|
classification: ClassificationOut
|
|
|
|
|
|
class ReclassifyRequest(BaseModel):
|
|
type: Optional[ClsType] = None # 사용자가 타입 강제 가능
|
|
|
|
|
|
class ConfirmResponse(BaseModel):
|
|
item: InboxItemOut
|
|
task: Optional[TaskNode] = None # type=task 면 생성된 작업
|
|
|
|
|
|
# ---------- llm ----------
|
|
class LLMHealth(BaseModel):
|
|
reachable: bool
|
|
provider: str # "ollama" | "heuristic"
|
|
model: str
|
|
host: Optional[str] = None
|
|
detail: str = ""
|
|
|
|
|
|
# ---------- dashboard ----------
|
|
class UserOut(BaseModel):
|
|
name: str # 예: "지우"
|
|
initial: str # 예: "지"
|
|
|
|
|
|
class WeatherOut(BaseModel):
|
|
temp: int # 현재 기온 (예: 24)
|
|
cond: str # "맑음 · 한낮 28°"
|
|
icon: str # 'cloudSun' 저장 → 표시용 'sun' 으로 매핑
|
|
|
|
|
|
class BriefingOut(BaseModel):
|
|
today: str # 히어로 날짜 라벨 (예: "6월 7일 일요일")
|
|
weather: WeatherOut
|
|
commute: str
|
|
sleep: str
|
|
note: str # HTML 허용
|
|
|
|
|
|
class EventOut(BaseModel):
|
|
id: str
|
|
time: str
|
|
title: str
|
|
tag: str
|
|
dur: str
|
|
tone: str
|
|
soon: bool
|
|
|
|
|
|
class ApprovalSummaryOut(BaseModel):
|
|
id: str
|
|
icon: str
|
|
tone: str
|
|
title: str
|
|
time: str
|
|
# risk=="high" 만 포함, 최대 3건. detail/cta/alt/undo_label 미포함.
|
|
|
|
|
|
class GoalOut(BaseModel):
|
|
id: str
|
|
title: str
|
|
pct: int
|
|
sub: str
|
|
tone: str # tone=키('blue' 등)
|
|
|
|
|
|
class InboxRecentOut(BaseModel):
|
|
id: str
|
|
kind: str
|
|
raw: str
|
|
type: str
|
|
proj_label: str
|
|
tone: str
|
|
# 평탄화 형태: 최신 classification 에서 type/proj_label/tone 을 끌어와 채운다.
|
|
|
|
|
|
class TaskSummaryItem(BaseModel):
|
|
id: str
|
|
title: str
|
|
prio: str
|
|
project: str
|
|
|
|
|
|
class TaskSummaryOut(BaseModel):
|
|
open_count: int # 미완료 작업 수
|
|
items: list[TaskSummaryItem]
|
|
|
|
|
|
class BadgesOut(BaseModel):
|
|
appr: int # 결재함 대기(=high risk 건수)
|
|
task: int # 미완료 작업 수(상단 작업 배지)
|
|
noti: int # 알림 배지 (시드 상수 6)
|
|
|
|
|
|
class DashboardOut(BaseModel):
|
|
user: UserOut
|
|
briefing: BriefingOut
|
|
saved_today: str # "47분"
|
|
today_routed: int # 7
|
|
schedule: list[EventOut]
|
|
task_summary: TaskSummaryOut
|
|
goals: list[GoalOut]
|
|
approvals_summary: list[ApprovalSummaryOut] # high-risk 만, 최대 3건
|
|
inbox_recent: list[InboxRecentOut] # 최근 3 (평탄화 형태)
|
|
badges: BadgesOut
|
|
|
|
|
|
# 재귀 모델 전방참조 해소
|
|
ProjectNode.model_rebuild()
|
|
TaskNode.model_rebuild()
|
|
|
|
|
|
# ==================== phase-7: 결재함 + 자동화 ====================
|
|
|
|
|
|
# ---------- 결재함 ----------
|
|
class ApprovalOut(BaseModel):
|
|
id: str
|
|
icon: str
|
|
tone: str
|
|
risk: str # "low" | "high"
|
|
time: str
|
|
title: str
|
|
detail: str
|
|
cta: str
|
|
alt: str
|
|
undo_label: str
|
|
status: str # pending|approved|executed|undone
|
|
source: str
|
|
rule_id: Optional[str] = None
|
|
sort_order: int
|
|
|
|
|
|
class ApprovalLogOut(BaseModel):
|
|
id: str
|
|
time: str
|
|
text: str
|
|
approval_id: Optional[str] = None
|
|
|
|
|
|
class AutonomyOut(BaseModel):
|
|
level: str # approval_first|mixed|full_auto
|
|
|
|
|
|
class AutonomyPatch(BaseModel):
|
|
level: str # 셋 중 하나만 허용(검증)
|
|
|
|
|
|
class ApprovalQueueOut(BaseModel):
|
|
"""결재함 페이지 응답 정본. 자율성 레벨로 큐를 분기한 결과."""
|
|
|
|
autonomy: str
|
|
saved_today: str # "47분"
|
|
auto_count_night: int # 7
|
|
pending: list[ApprovalOut]
|
|
done: list[ApprovalOut]
|
|
log: list[ApprovalLogOut]
|
|
badges_appr: int # 상단 결재함 배지 = pending 수
|
|
|
|
|
|
# ---------- 자동화 ----------
|
|
class FlowOut(BaseModel):
|
|
trigger: str
|
|
cond: Optional[str] = None
|
|
action: str
|
|
|
|
|
|
class RuleOut(BaseModel):
|
|
id: str
|
|
name: str
|
|
cat: str # mail|cal|focus|life
|
|
trigger: str
|
|
cond: Optional[str] = None
|
|
action: str
|
|
on: bool
|
|
last: str
|
|
runs: int
|
|
fresh: bool
|
|
source: str
|
|
|
|
|
|
class RuleCreate(BaseModel):
|
|
name: str
|
|
cat: str
|
|
trigger: str
|
|
cond: Optional[str] = None
|
|
action: str
|
|
source: Optional[str] = "user"
|
|
|
|
|
|
class RulePatch(BaseModel):
|
|
name: Optional[str] = None
|
|
cat: Optional[str] = None
|
|
trigger: Optional[str] = None
|
|
cond: Optional[str] = None
|
|
action: Optional[str] = None
|
|
on: Optional[bool] = None
|
|
|
|
|
|
class ParseRequest(BaseModel):
|
|
text: str
|
|
|
|
|
|
class ParsePreviewOut(BaseModel):
|
|
matched: bool
|
|
name: str
|
|
cat: str # mail|cal|focus|life
|
|
parse: FlowOut
|
|
model: str # "llm:<model>" | "heuristic" | "example"
|
|
confidence: float
|
|
|
|
|
|
class SuggestionOut(BaseModel):
|
|
id: str
|
|
pattern: str
|
|
offer: FlowOut
|
|
offer_name: str
|
|
offer_cat: str
|
|
status: str
|
|
|
|
|
|
class RunLogOut(BaseModel):
|
|
id: str
|
|
time: str
|
|
rule: str
|
|
text: str
|
|
undone: bool
|
|
|
|
|
|
class AutomationStatsOut(BaseModel):
|
|
active: int
|
|
runs_week: int
|
|
saved: str
|
|
|
|
|
|
class AutomationPageOut(BaseModel):
|
|
"""자동화 페이지 응답 정본(한 번에 모든 뷰 데이터)."""
|
|
|
|
stats: AutomationStatsOut
|
|
rules: list[RuleOut]
|
|
suggests: list[SuggestionOut]
|
|
log: list[RunLogOut]
|
|
examples: list[ParsePreviewOut]
|
|
|
|
|
|
# ==================== phase-8: 일정 + 회의 도우미 ====================
|
|
|
|
|
|
class CalendarOut(BaseModel):
|
|
id: str
|
|
name: str
|
|
tone: str
|
|
on: bool
|
|
count: int = 0
|
|
|
|
|
|
class EventActionOut(BaseModel):
|
|
text: str
|
|
when: str
|
|
|
|
|
|
class AttendeeOut(BaseModel):
|
|
email: str
|
|
name: str = ""
|
|
status: str = "" # accepted|declined|tentative|needsAction
|
|
|
|
|
|
class CalEventOut(BaseModel):
|
|
id: str
|
|
day: int
|
|
date: str = "" # 실제 날짜 ISO "YYYY-MM-DD"
|
|
start: str
|
|
end: str
|
|
title: str
|
|
cal: str
|
|
loc: str = ""
|
|
note: str = ""
|
|
soon: bool = False
|
|
people: list[str] = []
|
|
rrule: str = "" # 반복 규칙(iCal RRULE)
|
|
reminders: list[int] = [] # 시작 N분 전 알림
|
|
attendees: list[AttendeeOut] = []
|
|
response_status: str = "" # 내 RSVP
|
|
actions: list[EventActionOut] = []
|
|
has_meeting: bool = False
|
|
meet_label: str = ""
|
|
|
|
|
|
class EventWriteRequest(BaseModel):
|
|
"""새 일정 생성/수정 요청. start 빈값 = 종일."""
|
|
|
|
title: str
|
|
date: str # ISO "YYYY-MM-DD"
|
|
start: str = "" # "HH:MM"
|
|
end: str = "" # "HH:MM"
|
|
loc: str = ""
|
|
note: str = ""
|
|
people: list[str] = [] # 참석자 이메일
|
|
cal: str = "" # 대상 캘린더 id(빈값=기본 연결 캘린더 또는 로컬)
|
|
rrule: str = "" # 반복 규칙(빈값=단일). 예 "FREQ=WEEKLY;BYDAY=MO"
|
|
reminders: list[int] = [] # 시작 N분 전 알림(분 단위)
|
|
|
|
|
|
class EventRsvpRequest(BaseModel):
|
|
status: str # accepted | declined | tentative
|
|
|
|
|
|
class FocusBlockOut(BaseModel):
|
|
id: str
|
|
day: int
|
|
date: str = "" # 실제 날짜 ISO(데모는 6월로 매핑)
|
|
start: str
|
|
end: str
|
|
title: str
|
|
type: str
|
|
tag: str
|
|
task_id: Optional[str] = None
|
|
auto: bool = True
|
|
sort_order: int = 0
|
|
|
|
|
|
class MeetingActionOut(BaseModel):
|
|
id: str
|
|
idx: int
|
|
text: str
|
|
who: str
|
|
when: str = ""
|
|
added: bool
|
|
materialized_task_id: Optional[str] = None
|
|
|
|
|
|
class MeetingOut(BaseModel):
|
|
id: str
|
|
event_id: str
|
|
phase: str
|
|
one_on_one: bool = False
|
|
meta: str = ""
|
|
starts_in: str = ""
|
|
summary: list[str] = []
|
|
decisions: list[str] = []
|
|
agenda: list[str] = []
|
|
last_meeting: dict[str, Any] = {}
|
|
last_actions: list[dict[str, Any]] = []
|
|
insights: list[str] = []
|
|
docs: list[str] = []
|
|
person: dict[str, Any] = {}
|
|
promises: list[dict[str, Any]] = []
|
|
signals: list[dict[str, Any]] = []
|
|
talking_points: list[str] = []
|
|
actions: list[MeetingActionOut] = []
|
|
|
|
|
|
class DayBundleOut(BaseModel):
|
|
day: int
|
|
date: str = ""
|
|
events: list[CalEventOut]
|
|
focus_blocks: list[FocusBlockOut]
|
|
|
|
|
|
class WeekBundleOut(BaseModel):
|
|
today: int
|
|
today_date: str = "" # 실제 오늘 ISO(real 연결 시 실날짜, 아니면 데모 2026-06-08)
|
|
mode: str = "demo" # "real" | "demo"
|
|
week: list[int]
|
|
weekdays: list[str]
|
|
calendars: list[CalendarOut]
|
|
events: list[CalEventOut]
|
|
focus_blocks: list[FocusBlockOut]
|
|
|
|
|
|
class MaterializeResponse(BaseModel):
|
|
action: MeetingActionOut
|
|
task: Optional[TaskNode] = None
|
|
all_added: bool = False
|
|
|
|
|
|
class FocusSuggestRequest(BaseModel):
|
|
day: int
|
|
min_minutes: int = 20
|
|
create: bool = False
|
|
|
|
|
|
class FocusSuggestResponse(BaseModel):
|
|
day: int
|
|
suggestions: list[FocusBlockOut]
|
|
created: bool = False
|
|
|
|
|
|
MaterializeResponse.model_rebuild()
|
|
|
|
|
|
# ==================== phase-9: 메일 + 알림 ====================
|
|
|
|
|
|
class MailAccountOut(BaseModel):
|
|
id: str
|
|
name: str
|
|
email: str
|
|
tone: str
|
|
kind: str
|
|
unread: int # 집중(focused) 받은편지함 안읽음
|
|
others: int = 0 # 기타(Outlook other) 받은편지함 안읽음
|
|
|
|
|
|
class MailFolderOut(BaseModel):
|
|
id: str
|
|
name: str
|
|
icon: str
|
|
|
|
|
|
class AccountFolderOut(BaseModel):
|
|
"""계정별 실제 폴더/라벨(사이드바 트리). slug 가 list_mail folder 필터 키."""
|
|
|
|
account: str
|
|
slug: str
|
|
name: str
|
|
kind: str # system | category | label | custom
|
|
icon: str
|
|
unread: int = 0
|
|
total: int = 0
|
|
sort_order: int = 0
|
|
|
|
|
|
class AiTaskOut(BaseModel):
|
|
text: str
|
|
project: str
|
|
due: str = ""
|
|
prio: str = ""
|
|
|
|
|
|
class AiEventOut(BaseModel):
|
|
title: str
|
|
date: str = ""
|
|
day: str = ""
|
|
time: str = ""
|
|
dur: str = ""
|
|
place: str = ""
|
|
|
|
|
|
class AiReplyOut(BaseModel):
|
|
tone: str
|
|
preview: str
|
|
body: str
|
|
|
|
|
|
class AiFileOut(BaseModel):
|
|
project: str
|
|
reason: str
|
|
|
|
|
|
class AiAnalysisOut(BaseModel):
|
|
summary: str = ""
|
|
priority: str = "보통"
|
|
category: str = ""
|
|
tasks: list[AiTaskOut] = []
|
|
events: list[AiEventOut] = []
|
|
replies: list[AiReplyOut] = []
|
|
file: Optional[AiFileOut] = None
|
|
|
|
|
|
class MailAttachmentOut(BaseModel):
|
|
id: str = "" # 프로바이더 attachment id(다운로드용)
|
|
name: str
|
|
mime: str = ""
|
|
size: int = 0
|
|
|
|
|
|
class EmailRowOut(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
id: str
|
|
account: str
|
|
from_: str = Field(alias="from")
|
|
from_name: str = "" # 표시 이름(이름 없으면 도메인 추정) — 목록/리더에서 노출
|
|
from_email: str = "" # 실제 메일 주소(리더 보조 표시)
|
|
to: str
|
|
cc: str = ""
|
|
thread_id: str = ""
|
|
subject: str
|
|
time: str
|
|
read: bool
|
|
starred: bool
|
|
has_attach: bool
|
|
labels: list[str] = []
|
|
preview: str
|
|
category: str = "primary" # 카테고리 탭(primary/promotions/social/updates) — 라벨에서 산출
|
|
ai: Optional[AiAnalysisOut] = None
|
|
|
|
|
|
class EmailDetailOut(EmailRowOut):
|
|
date: str
|
|
body: list[str] = []
|
|
body_html: str = "" # 원본 HTML 본문(샌드박스 iframe 렌더용; cid 는 첨부 URL 로 치환됨)
|
|
attachments: list[MailAttachmentOut] = []
|
|
|
|
|
|
class ConversationOut(BaseModel):
|
|
"""대화(스레드) 묶음 — 같은 thread_id 메일들."""
|
|
|
|
thread_id: str
|
|
subject: str
|
|
count: int
|
|
messages: list[EmailRowOut] = []
|
|
|
|
|
|
class ExtractRequest(BaseModel):
|
|
kind: str # task | event | file
|
|
index: int = 0
|
|
|
|
|
|
class ExtractResponse(BaseModel):
|
|
kind: str
|
|
task: Optional[TaskNode] = None
|
|
event_id: Optional[str] = None
|
|
project: Optional[str] = None
|
|
message: str
|
|
|
|
|
|
class ExtractAllResponse(BaseModel):
|
|
created_task_ids: list[str] = []
|
|
created_event_ids: list[str] = []
|
|
filed_project: Optional[str] = None
|
|
message: str
|
|
|
|
|
|
class ReplyDraftRequest(BaseModel):
|
|
reply_index: Optional[int] = None
|
|
tone: Optional[str] = None
|
|
|
|
|
|
class ReplyDraftOut(BaseModel):
|
|
to: str
|
|
from_account: str
|
|
subject: str
|
|
body: str
|
|
|
|
|
|
class ComposeAttachment(BaseModel):
|
|
name: str
|
|
mime: str = "application/octet-stream"
|
|
data_b64: str # base64 인코딩 본문
|
|
|
|
|
|
class SendRequest(BaseModel):
|
|
from_account: str
|
|
to: str
|
|
cc: str = ""
|
|
bcc: str = ""
|
|
subject: str
|
|
body: str
|
|
in_reply_to: Optional[str] = None
|
|
attachments: list[ComposeAttachment] = []
|
|
|
|
|
|
class SendResponse(BaseModel):
|
|
approval_id: str
|
|
status: str
|
|
message: str
|
|
|
|
|
|
class MailStarRequest(BaseModel):
|
|
starred: bool
|
|
|
|
|
|
class MailReadRequest(BaseModel):
|
|
read: bool
|
|
|
|
|
|
class MailLabelRequest(BaseModel):
|
|
"""라벨/카테고리 추가·제거(폴더 이동 포함)."""
|
|
|
|
add: list[str] = []
|
|
remove: list[str] = []
|
|
|
|
|
|
class BulkMailRequest(BaseModel):
|
|
ids: list[str]
|
|
action: str # read | unread | archive | unarchive | trash | star | unstar
|
|
|
|
|
|
# ---------- 알림 ----------
|
|
class NotificationOut(BaseModel):
|
|
id: str
|
|
bucket: str
|
|
src: str
|
|
app: str
|
|
title: str
|
|
sum: str
|
|
time: str
|
|
why: str
|
|
tone: str
|
|
act: str = ""
|
|
|
|
|
|
class TriageOut(BaseModel):
|
|
now: list[NotificationOut]
|
|
later: list[NotificationOut]
|
|
held: list[NotificationOut]
|
|
counts: dict
|
|
stats: dict
|
|
|
|
|
|
class ReclassifyNotifRequest(BaseModel):
|
|
to_bucket: str
|
|
|
|
|
|
class GuardOut(BaseModel):
|
|
on: bool
|
|
until: str
|
|
held: int
|
|
quiet: str
|
|
|
|
|
|
class GuardPatch(BaseModel):
|
|
on: bool
|
|
|
|
|
|
class DigestOut(BaseModel):
|
|
id: str
|
|
time: str
|
|
name: str
|
|
desc: str
|
|
count: int
|
|
done: bool
|
|
|
|
|
|
class SenderRuleOut(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
id: str
|
|
who: str
|
|
from_label: str = Field(serialization_alias="from")
|
|
rule: str
|
|
tone: str
|
|
bucket: str
|
|
|
|
|
|
class SenderRuleCreate(BaseModel):
|
|
who: str
|
|
from_label: str
|
|
rule: str
|
|
bucket: str = "now"
|
|
tone: str = "ink"
|
|
|
|
|
|
ExtractResponse.model_rebuild()
|
|
|
|
|
|
# ==================== phase-12: 여정 + 하루 마감 ====================
|
|
|
|
|
|
class JourneyPersonOut(BaseModel):
|
|
id: str
|
|
name: str
|
|
initial: str
|
|
color: str
|
|
me: bool = False
|
|
tasks: int = 0
|
|
role: str = ""
|
|
|
|
|
|
class JourneyStageOut(BaseModel):
|
|
id: str
|
|
title: str
|
|
time: str
|
|
tone: str
|
|
n: str
|
|
|
|
|
|
class RoutineCardOut(BaseModel):
|
|
node: str
|
|
icon: str
|
|
title: str
|
|
meta: str
|
|
tag: str
|
|
tool: str
|
|
toolTone: str = ""
|
|
|
|
|
|
class TaskCardOut(BaseModel):
|
|
node: str
|
|
title: str
|
|
meta: str
|
|
who: str
|
|
done: bool = False
|
|
|
|
|
|
class MeetCardOut(BaseModel):
|
|
node: str
|
|
time: str
|
|
title: str
|
|
meta: str
|
|
who: str
|
|
soon: bool = False
|
|
|
|
|
|
class TileCardOut(BaseModel):
|
|
node: str
|
|
hi: bool = False
|
|
icon: str
|
|
tt: str
|
|
ts: str
|
|
|
|
|
|
class JourneyCardsOut(BaseModel):
|
|
s1: list[RoutineCardOut]
|
|
s2: list[TaskCardOut]
|
|
s3: list[MeetCardOut]
|
|
s4: list[TileCardOut]
|
|
|
|
|
|
class JourneyLinkOut(BaseModel):
|
|
from_node: str
|
|
to_node: str
|
|
tone: str
|
|
dash: bool = False
|
|
|
|
|
|
class JourneyRowOut(BaseModel):
|
|
node: str
|
|
title: str
|
|
star: bool = False
|
|
statusKey: str
|
|
statusLabel: str
|
|
startTime: str
|
|
who: str
|
|
|
|
|
|
class GaugeOut(BaseModel):
|
|
pct: int
|
|
tone: str
|
|
val: str
|
|
unit: str
|
|
label: str
|
|
sub: str
|
|
|
|
|
|
class JourneyMetaOut(BaseModel):
|
|
stageCount: int
|
|
itemCount: int
|
|
collabCount: int
|
|
peopleMore: str = "+3"
|
|
|
|
|
|
class JourneyHeaderOut(BaseModel):
|
|
dateLabel: str
|
|
weather: str
|
|
|
|
|
|
class JourneyTableOut(BaseModel):
|
|
subtitle: str
|
|
|
|
|
|
class JourneyOut(BaseModel):
|
|
header: JourneyHeaderOut
|
|
meta: JourneyMetaOut
|
|
people: list[JourneyPersonOut]
|
|
stages: list[JourneyStageOut]
|
|
cards: JourneyCardsOut
|
|
links: list[JourneyLinkOut]
|
|
rows: list[JourneyRowOut]
|
|
table: JourneyTableOut
|
|
gauges: list[GaugeOut]
|
|
gaugeFoot: str
|
|
|
|
|
|
# ==================== phase-13: 외부 연동 & 데이터 커넥터 ====================
|
|
class ConnectorStatusOut(BaseModel):
|
|
id: str
|
|
domain: str
|
|
provider: str
|
|
name: str # "회사" / "우리카드" / "Apple Health"
|
|
kind: str # "Lumi Workspace" / "신용/체크" / "건강 앱"
|
|
tone: str # blue|violet|coral|green|amber|ink|faint
|
|
mode: str # mock | csv | healthkit | real
|
|
env_mode: str # CONNECTOR_<DOMAIN> 값(real 허용 여부)
|
|
state: str # connected|disconnected|syncing|error|token_expired
|
|
on: bool # state==connected (라이프 카드 on 호환)
|
|
last: str # last_label
|
|
last_synced_at: Optional[datetime] = None
|
|
error_detail: str = ""
|
|
email: str = "" # phase-16 연결된 실계정 주소(external_account_id)
|
|
|
|
@classmethod
|
|
def from_account(cls, a, env_mode: str) -> ConnectorStatusOut:
|
|
dom = a.domain.value if hasattr(a.domain, "value") else a.domain
|
|
mode = a.mode.value if hasattr(a.mode, "value") else a.mode
|
|
state = a.state.value if hasattr(a.state, "value") else a.state
|
|
return cls(
|
|
id=a.id,
|
|
domain=dom,
|
|
provider=a.provider,
|
|
name=a.name,
|
|
kind=a.kind,
|
|
tone=a.tone,
|
|
mode=mode,
|
|
env_mode=env_mode,
|
|
state=state,
|
|
on=(state == "connected"),
|
|
last=a.last_label,
|
|
last_synced_at=a.last_synced_at,
|
|
error_detail=a.error_detail,
|
|
email=getattr(a, "external_account_id", "") or "",
|
|
)
|
|
|
|
|
|
class OAuthStartOut(BaseModel):
|
|
authorize_url: str
|
|
|
|
|
|
class ConnectorProviderOut(BaseModel):
|
|
"""phase-16 '계정 추가' 버튼이 보여줄 OAuth provider 목록 + 구성 여부."""
|
|
|
|
domain: str # mail | calendar | knowledge ...
|
|
provider: str # gmail | outlook | google_calendar | notion
|
|
label: str # "Gmail" / "Outlook"
|
|
configured: bool # client_id 설정되어 real 연동 가능
|
|
|
|
|
|
class SyncResultOut(BaseModel):
|
|
domain: str
|
|
account_id: str
|
|
seen: int = 0
|
|
upserted: int = 0
|
|
skipped: int = 0
|
|
errors: int = 0
|
|
events_published: list[str] = Field(default_factory=list)
|
|
detail: str = ""
|
|
|
|
|
|
class ImportResultOut(BaseModel):
|
|
entity_type: str
|
|
imported: int = 0
|
|
skipped: int = 0
|
|
errors: int = 0
|
|
detail: str = ""
|
|
|
|
|
|
# ==================== phase-14: 능동 워커 + 멀티모달 ====================
|
|
class ProactiveCardOut(BaseModel):
|
|
id: str
|
|
kind: str
|
|
icon: str
|
|
tone: str
|
|
title: str
|
|
why: str
|
|
cta: Optional[str] = None
|
|
action_kind: Optional[str] = None
|
|
detected_at: str
|
|
status: str
|
|
|
|
|
|
class ProactiveAcceptOut(BaseModel):
|
|
id: str
|
|
action_kind: str
|
|
created_id: Optional[str] = None
|
|
message: str
|
|
|
|
|
|
class WeeklyReviewOut(BaseModel):
|
|
id: str
|
|
week_label: str
|
|
deep_work_h: float
|
|
meeting_h: float
|
|
done_count: int
|
|
auto_count: int
|
|
saved_minutes: int
|
|
patterns: list[dict] = Field(default_factory=list)
|
|
note: Optional[str] = None
|
|
|
|
|
|
class WorkerRunOut(BaseModel):
|
|
job: str
|
|
ok: bool
|
|
detail: str
|
|
|
|
|
|
class TranscribeOut(BaseModel):
|
|
text: str
|
|
seconds: float
|
|
model: str
|
|
confidence: float
|
|
|
|
|
|
class CaptionOut(BaseModel):
|
|
text: str
|
|
ocr: str
|
|
model: str
|
|
confidence: float
|
|
|
|
|
|
# ==================== phase-15: 인증/멀티유저 + 운영 ====================
|
|
class LoginIn(BaseModel):
|
|
email: str
|
|
password: str
|
|
|
|
|
|
class MeOut(BaseModel):
|
|
id: str
|
|
name: str
|
|
initial: str
|
|
email: str
|
|
role: str # member | admin
|
|
|
|
|
|
class TokenOut(BaseModel):
|
|
id: str
|
|
name: str
|
|
token: Optional[str] = None # 발급 시 1회만 평문
|
|
created_at: str
|
|
|
|
|
|
class ReadyOut(BaseModel):
|
|
ready: bool
|
|
checks: dict[str, str]
|
|
|
|
|
|
# ==================== 설정(settings) ====================
|
|
class ProfileUpdateIn(BaseModel):
|
|
"""프로필 편집. 모두 선택 — 주어진 필드만 갱신."""
|
|
|
|
name: Optional[str] = None
|
|
role: Optional[str] = None
|
|
|
|
|
|
class TokenInfoOut(BaseModel):
|
|
"""토큰 목록 항목(평문 미포함)."""
|
|
|
|
id: str
|
|
name: str
|
|
created_at: str
|
|
last_used_at: Optional[str] = None
|
|
revoked: bool = False
|
|
|
|
|
|
class LLMConfigOut(BaseModel):
|
|
"""유효 LLM 연결 설정 + 선택지 + 보조 프로바이더(읽기전용)."""
|
|
|
|
provider: str # 유효(오버레이>env): ollama | openai | gemini
|
|
model: str
|
|
host: str
|
|
timeout: float
|
|
provider_options: list[str] = ["ollama"]
|
|
overridden: bool = False # 런타임 오버레이가 활성인가(env 기본과 다른가)
|
|
api_key_set: bool = False # API 키가 설정돼 있는가(값은 절대 노출하지 않음)
|
|
# 보조 프로바이더(읽기전용, env 관리)
|
|
stt_provider: str = ""
|
|
vision_provider: str = ""
|
|
|
|
|
|
class LLMUpdateIn(BaseModel):
|
|
"""LLM 연결 저장. None=미변경, 빈 문자열은 '기본으로 되돌리기'."""
|
|
|
|
provider: Optional[str] = None
|
|
model: Optional[str] = None
|
|
host: Optional[str] = None
|
|
timeout: Optional[float] = None
|
|
api_key: Optional[str] = None # 암호화 저장. 빈 문자열=키 삭제, None=미변경
|
|
|
|
|
|
class LLMTestIn(BaseModel):
|
|
"""연결 테스트(저장 없이). 미지정 필드는 유효 설정값 사용."""
|
|
|
|
provider: Optional[str] = None
|
|
host: Optional[str] = None
|
|
model: Optional[str] = None
|
|
api_key: Optional[str] = None # 미지정 시 저장된 키 사용
|
|
|
|
|
|
class LLMTestOut(BaseModel):
|
|
reachable: bool
|
|
provider: str
|
|
model: str
|
|
host: Optional[str] = None
|
|
models: list[str] = [] # ollama /api/tags 결과
|
|
detail: str = ""
|
|
|
|
|
|
class SystemConfigOut(BaseModel):
|
|
"""런타임/환경 상태 스냅샷(읽기전용). env(.env)로 관리됨을 명시."""
|
|
|
|
env: str # dev | prod
|
|
auth_enabled: bool
|
|
worker_enabled: bool
|
|
database: str # sqlite | postgres | ...
|
|
sync_interval_minutes: int
|
|
sync_page_size: int
|
|
connector_modes: dict[str, str] # {calendar: mock, mail: mock}
|
|
|