9d9b3359b0
개인 운영 로그 / 일정 / 할 일 / 회고용 1차 컨테이너 도메인 신설.
plan: ~/.claude/plans/beszel-tingly-sloth.md (라운드 12 v6).
Schema:
- enum 5종 (event_kind / event_status / event_source / event_actor / history_change_kind)
- events 테이블: kind(task|calendar_event|activity_log) + lifecycle 7-state status
- events_history: lifecycle op 자동 기록, FK RESTRICT (이력은 시점 사실)
- CHECK: calendar_event → start_at NOT NULL / activity_log → started_at|ended_at NOT NULL
- partial unique (source, source_ref) — 외부 source dedup (PR-4 활용)
- partial index (active status / activity_log timeline)
API:
- POST /api/events (kind=activity_log shortcut: status=done + ended_at=now() default)
- GET /api/events/{id} | /api/events?kind&status&from&to&project_tag&source
- PATCH /api/events/{id} (extra=forbid + 시간 필드 변경 시 reschedule history)
- POST /api/events/{id}/{complete,cancel,defer,reactivate} (history 자동)
- GET /api/events/today (Asia/Seoul default, deferred 는 defer_until<=now() 만)
- GET /api/events/inbox | /api/events/activity?from&to
제외 (PR-2~5 또는 백로그):
- DELETE (회고 데이터 → /cancel 일관화)
- log shortcut / upcoming endpoint (POST + GET ?from&to 로 흡수)
- /ingest (PR-4 MailPlus forward 시 정확한 요구로 추가)
- iCal export / ntfy 알림 / recurrence / 일반 edit history
114 lines
3.7 KiB
Python
114 lines
3.7 KiB
Python
"""events 1차 컨테이너 ORM (개인 운영 로그 / 일정 / 할 일 / 회고)
|
|
|
|
PR-1 (migrations 239~247) 의 본체. kind enum 으로 task/calendar_event/activity_log
|
|
세 변형을 통합 관리. memo_document_id 는 메모 link (optional).
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from sqlalchemy import (
|
|
BigInteger,
|
|
Boolean,
|
|
DateTime,
|
|
ForeignKey,
|
|
SmallInteger,
|
|
String,
|
|
Text,
|
|
)
|
|
from sqlalchemy.dialects.postgresql import ENUM as PgEnum
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from core.database import Base
|
|
|
|
# Postgres enum 재선언 X (create_type=False) — migration 239~243 이 권위.
|
|
EventKindEnum = PgEnum(
|
|
"task",
|
|
"calendar_event",
|
|
"activity_log",
|
|
name="event_kind",
|
|
create_type=False,
|
|
)
|
|
EventStatusEnum = PgEnum(
|
|
"inbox",
|
|
"next",
|
|
"scheduled",
|
|
"in_progress",
|
|
"done",
|
|
"cancelled",
|
|
"deferred",
|
|
name="event_status",
|
|
create_type=False,
|
|
)
|
|
EventSourceEnum = PgEnum(
|
|
"manual",
|
|
"memo",
|
|
"email",
|
|
"chat",
|
|
"webhook",
|
|
"git_commit",
|
|
"claude_code",
|
|
name="event_source",
|
|
create_type=False,
|
|
)
|
|
EventActorEnum = PgEnum(
|
|
"manual",
|
|
"eid",
|
|
"email_ingest",
|
|
"system",
|
|
name="event_actor",
|
|
create_type=False,
|
|
)
|
|
|
|
|
|
class Event(Base):
|
|
__tablename__ = "events"
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
|
title: Mapped[str] = mapped_column(Text, nullable=False)
|
|
description: Mapped[str | None] = mapped_column(Text)
|
|
kind: Mapped[str] = mapped_column(EventKindEnum, nullable=False)
|
|
status: Mapped[str] = mapped_column(EventStatusEnum, nullable=False, default="inbox")
|
|
|
|
# 시간 필드 — kind 별 의미가 다름 (CHECK 제약은 migration 244)
|
|
due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
start_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
end_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
all_day: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
timezone: Mapped[str | None] = mapped_column(Text)
|
|
|
|
# lifecycle
|
|
defer_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
cancelled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
|
|
priority: Mapped[int | None] = mapped_column(SmallInteger)
|
|
project_tag: Mapped[str | None] = mapped_column(String(64))
|
|
tags: Mapped[list[Any]] = mapped_column(JSONB, nullable=False, default=list)
|
|
|
|
# 출처 / 외부 식별자
|
|
source: Mapped[str] = mapped_column(EventSourceEnum, nullable=False, default="manual")
|
|
source_ref: Mapped[str | None] = mapped_column(Text)
|
|
raw_metadata: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
|
|
|
# 메모 link (optional, ON DELETE SET NULL)
|
|
memo_document_id: Mapped[int | None] = mapped_column(
|
|
BigInteger, ForeignKey("documents.id", ondelete="SET NULL")
|
|
)
|
|
|
|
# 인증 / actor
|
|
user_id: Mapped[int] = mapped_column(
|
|
BigInteger, ForeignKey("users.id"), nullable=False
|
|
)
|
|
created_by: Mapped[str] = mapped_column(EventActorEnum, nullable=False, default="manual")
|
|
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), default=datetime.now, nullable=False
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), default=datetime.now, onupdate=datetime.now, nullable=False
|
|
)
|