|
|
# backend/app/seed.py
|
|
|
import json
|
|
|
from datetime import date
|
|
|
|
|
|
from sqlmodel import Session, select
|
|
|
|
|
|
from .db import engine, init_db
|
|
|
from .models import (
|
|
|
ApiToken,
|
|
|
Approval,
|
|
|
ApprovalLog,
|
|
|
ApprovalSource,
|
|
|
ApprovalStatus,
|
|
|
AuditLog,
|
|
|
AuthSession,
|
|
|
AutomationRule,
|
|
|
AutomationRunLog,
|
|
|
AutomationStats,
|
|
|
AutomationSuggestion,
|
|
|
AutonomyLevel,
|
|
|
AutonomySetting,
|
|
|
Briefing,
|
|
|
Calendar,
|
|
|
CalEvent,
|
|
|
ChatMessage,
|
|
|
ClsType,
|
|
|
ConnectorAccount,
|
|
|
ConnectorAccountLog,
|
|
|
ConnectorSource,
|
|
|
ConnectorSyncState,
|
|
|
Digest,
|
|
|
Draft,
|
|
|
Email,
|
|
|
ErrandStep,
|
|
|
ErrandTask,
|
|
|
Event,
|
|
|
ExternalLink,
|
|
|
FinanceBudget,
|
|
|
FinanceCategory,
|
|
|
FinanceInsight,
|
|
|
FinanceSub,
|
|
|
FinanceTx,
|
|
|
FocusBlock,
|
|
|
Folder,
|
|
|
Goal,
|
|
|
HealthHabit,
|
|
|
HealthRing,
|
|
|
HealthSample,
|
|
|
HealthVital,
|
|
|
InboxClassification,
|
|
|
InboxItem,
|
|
|
InboxKind,
|
|
|
InboxStatus,
|
|
|
JourneyLink,
|
|
|
KnowledgeCollection,
|
|
|
KnowledgeItem,
|
|
|
MailAccount,
|
|
|
MailFolder,
|
|
|
Meeting,
|
|
|
MeetingAction,
|
|
|
Notification,
|
|
|
NotificationGuard,
|
|
|
NotifyStats,
|
|
|
OAuthState,
|
|
|
Person,
|
|
|
Prio,
|
|
|
ProactiveCard,
|
|
|
Project,
|
|
|
RagChunk,
|
|
|
ResearchChart,
|
|
|
ResearchCollection,
|
|
|
ResearchQA,
|
|
|
ResearchReport,
|
|
|
ResearchSource,
|
|
|
SavedTrip,
|
|
|
SenderRule,
|
|
|
Sent,
|
|
|
SleepNight,
|
|
|
Sphere,
|
|
|
Task,
|
|
|
TaskComment,
|
|
|
TaskStatus,
|
|
|
Trip,
|
|
|
TripChecklist,
|
|
|
TripDay,
|
|
|
TripPlan,
|
|
|
TripPrep,
|
|
|
TripRoute,
|
|
|
TripStay,
|
|
|
UserCredential,
|
|
|
WeeklyReview,
|
|
|
)
|
|
|
from .seed_mail import _seed_mail, _seed_notify
|
|
|
|
|
|
YEAR = 2026
|
|
|
|
|
|
# 원본 journey-clean-data.js links[] — HEX→tone 키 변환 (from, to, tone, dash)
|
|
|
JOURNEY_LINKS = [
|
|
|
("m-brief", "t-report", "blue", False),
|
|
|
("t-report", "k-strat", "coral", False),
|
|
|
("t-wire", "k-design", "violet", False),
|
|
|
("t-interview", "k-11", "green", True),
|
|
|
("t-welcome", "k-stand", "green", True),
|
|
|
("k-strat", "w-focus", "ink", False),
|
|
|
("k-design", "w-retro", "violet", True),
|
|
|
("k-11", "w-next", "green", True),
|
|
|
]
|
|
|
|
|
|
|
|
|
def _seed_journey(s: Session) -> None:
|
|
|
for i, (frm, to, tone, dash) in enumerate(JOURNEY_LINKS, start=1):
|
|
|
s.add(
|
|
|
JourneyLink(id=f"jl{i}", from_node=frm, to_node=to, tone=tone, dash=dash, sort_order=i)
|
|
|
)
|
|
|
|
|
|
|
|
|
def D(mmdd: str | None):
|
|
|
"""'06-08' -> date(2026,6,8). 빈값/None -> None"""
|
|
|
if not mmdd:
|
|
|
return None
|
|
|
m, d = mmdd.split("-")
|
|
|
return date(YEAR, int(m), int(d))
|
|
|
|
|
|
|
|
|
# ---- people (REF/assets/tasks-data.js people) ----
|
|
|
PEOPLE = [
|
|
|
("jiwoo", "지우", "지", "var(--blue)", True),
|
|
|
("hyunwoo", "현우", "현", "var(--violet)", False),
|
|
|
("minseo", "민서", "민", "var(--green)", False),
|
|
|
("jaeho", "재호", "재", "var(--coral)", False),
|
|
|
("sua", "수아", "수", "oklch(0.66 0.13 200)", False),
|
|
|
]
|
|
|
|
|
|
# ---- folders + projects (REF tree) ----
|
|
|
FOLDERS = [
|
|
|
("work", "업무", "ink", "folder", 0),
|
|
|
("life", "개인", "blue", "heart", 1),
|
|
|
]
|
|
|
# (id, folder_id, parent_id, name, tone, sort)
|
|
|
PROJECTS = [
|
|
|
# 업무
|
|
|
("biz", "work", None, "경영 전략", "coral", 0),
|
|
|
("biz-okr", "work", "biz", "2026 Q2 OKR", "coral", 0),
|
|
|
("biz-report", "work", "biz", "분기 리포트", "coral", 1),
|
|
|
("biz-budget", "work", "biz", "예산 관리", "coral", 2),
|
|
|
("onb", "work", None, "온보딩 리디자인", "violet", 1),
|
|
|
("onb-research", "work", "onb", "사용자 리서치", "violet", 0),
|
|
|
("onb-wire", "work", "onb", "와이어프레임", "violet", 1),
|
|
|
("onb-voice", "work", "onb", "음성 인터페이스", "violet", 2),
|
|
|
("team", "work", None, "팀 운영", "green", 2),
|
|
|
("team-hr", "work", "team", "채용 & 온보딩", "green", 0),
|
|
|
("team-retro", "work", "team", "회고", "green", 1),
|
|
|
# 개인
|
|
|
("me", "life", None, "일상", "blue", 0),
|
|
|
("life-trip", "life", None, "여행 — 한국", "coral", 1),
|
|
|
("life-fam", "life", None, "가족", "green", 2),
|
|
|
]
|
|
|
|
|
|
# ---- tasks (REF tasks; 중첩) ----
|
|
|
TASKS = [
|
|
|
{
|
|
|
"id": "k1",
|
|
|
"title": "분기 리포트 초안 마무리",
|
|
|
"project": "biz-report",
|
|
|
"assignee": "jiwoo",
|
|
|
"due": "06-08",
|
|
|
"prio": "높음",
|
|
|
"status": "doing",
|
|
|
"notes": "<h3>목표</h3><p>경영진 미팅용 Q2 성과 리포트. <b>리텐션·매출·예산</b> 세 섹션으로 구성한다.</p><blockquote>핵심 지표는 시각화해서 한눈에 들어오게.</blockquote>",
|
|
|
"comments": [
|
|
|
{"who": "hyunwoo", "text": "매출 섹션은 제가 오늘 안으로 넘겨드릴게요."},
|
|
|
{"who": "jiwoo", "text": "좋아요, 예산 섹션만 확정되면 취합할게요."},
|
|
|
],
|
|
|
"children": [
|
|
|
{
|
|
|
"title": "리텐션 데이터 취합",
|
|
|
"status": "done",
|
|
|
"assignee": "sua",
|
|
|
"due": "06-08",
|
|
|
"project": "biz-report",
|
|
|
"children": [
|
|
|
{
|
|
|
"title": "코호트 정의 확정",
|
|
|
"status": "done",
|
|
|
"assignee": "sua",
|
|
|
"due": "06-07",
|
|
|
},
|
|
|
{
|
|
|
"title": "주차별 잔존율 추출",
|
|
|
"status": "done",
|
|
|
"assignee": "sua",
|
|
|
"due": "06-08",
|
|
|
},
|
|
|
{
|
|
|
"title": "이탈 사유 태깅",
|
|
|
"status": "done",
|
|
|
"assignee": "jaeho",
|
|
|
"due": "06-08",
|
|
|
"children": [
|
|
|
{"title": "인터뷰 발췌 5건", "status": "done", "assignee": "jaeho"},
|
|
|
{"title": "사유 카테고리 분류", "status": "done", "assignee": "jaeho"},
|
|
|
],
|
|
|
},
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
"title": "매출 섹션 작성",
|
|
|
"status": "doing",
|
|
|
"assignee": "jiwoo",
|
|
|
"due": "06-09",
|
|
|
"prio": "높음",
|
|
|
"project": "biz-report",
|
|
|
"notes": "<p>MRR/ARR 표는 완료. 증감 코멘트 마무리 필요.</p>",
|
|
|
"children": [
|
|
|
{"title": "MRR / ARR 표 정리", "status": "done", "assignee": "jiwoo"},
|
|
|
{"title": "전분기 대비 증감 코멘트", "status": "doing", "assignee": "jiwoo"},
|
|
|
{"title": "예측 시나리오 3종", "status": "todo", "assignee": "jiwoo"},
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
"title": "예산 섹션 작성",
|
|
|
"status": "todo",
|
|
|
"assignee": "jiwoo",
|
|
|
"due": "06-10",
|
|
|
"project": "biz-budget",
|
|
|
},
|
|
|
{
|
|
|
"title": "경영진 검토 요청 메일",
|
|
|
"status": "todo",
|
|
|
"assignee": "jiwoo",
|
|
|
"due": "06-10",
|
|
|
"project": "biz-report",
|
|
|
},
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
"id": "k2",
|
|
|
"title": "온보딩 와이어프레임 피드백 정리",
|
|
|
"project": "onb-wire",
|
|
|
"assignee": "hyunwoo",
|
|
|
"due": "06-09",
|
|
|
"prio": "높음",
|
|
|
"status": "review",
|
|
|
"notes": "<p>디자인 리뷰에서 나온 3개 화면 수정사항을 취합하고 우선순위를 매긴다.</p>",
|
|
|
"comments": [{"who": "sua", "text": "3번 화면 CTA 위치는 아래로 내리는 게 좋겠어요."}],
|
|
|
"children": [
|
|
|
{
|
|
|
"title": "환영 화면 수정안",
|
|
|
"status": "done",
|
|
|
"assignee": "hyunwoo",
|
|
|
"project": "onb-wire",
|
|
|
"children": [
|
|
|
{"title": "카피 톤 조정", "status": "done", "assignee": "hyunwoo"},
|
|
|
{"title": "일러스트 교체 요청", "status": "doing", "assignee": "minseo"},
|
|
|
],
|
|
|
},
|
|
|
{"title": "권한 요청 화면 재배치", "status": "doing", "assignee": "hyunwoo"},
|
|
|
{"title": "음성 안내 추가 검토", "status": "todo", "assignee": "sua"},
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
"id": "k5",
|
|
|
"title": "사용자 인터뷰 5건 정리",
|
|
|
"project": "onb-research",
|
|
|
"assignee": "sua",
|
|
|
"due": "06-10",
|
|
|
"prio": "보통",
|
|
|
"status": "doing",
|
|
|
"children": [
|
|
|
{"title": "녹취 요약 (5건)", "status": "doing", "assignee": "sua"},
|
|
|
{"title": "인사이트 태깅", "status": "todo", "assignee": "sua"},
|
|
|
{"title": "리서치 보드 업데이트", "status": "todo", "assignee": "minseo"},
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
"id": "k6",
|
|
|
"title": "OKR 중간 점검 자료 준비",
|
|
|
"project": "biz-okr",
|
|
|
"assignee": "jiwoo",
|
|
|
"due": "06-11",
|
|
|
"prio": "높음",
|
|
|
"status": "todo",
|
|
|
"children": [
|
|
|
{"title": "Objective별 진척도 집계", "status": "todo", "assignee": "jiwoo"},
|
|
|
{"title": "리스크 항목 표시", "status": "todo", "assignee": "jiwoo"},
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
"id": "k4",
|
|
|
"title": "구독 결제 카드 갱신",
|
|
|
"project": "me",
|
|
|
"assignee": "jiwoo",
|
|
|
"due": "06-12",
|
|
|
"prio": "보통",
|
|
|
"status": "todo",
|
|
|
"children": [],
|
|
|
},
|
|
|
{
|
|
|
"id": "k3",
|
|
|
"title": "신규 입사자 환영 메일 발송",
|
|
|
"project": "team-hr",
|
|
|
"assignee": "minseo",
|
|
|
"due": "06-07",
|
|
|
"prio": "보통",
|
|
|
"status": "done",
|
|
|
"children": [
|
|
|
{"title": "메일 템플릿 작성", "status": "done", "assignee": "minseo"},
|
|
|
{"title": "수신자 명단 확인", "status": "done", "assignee": "minseo"},
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
"id": "k13",
|
|
|
"title": "스프린트 회고 문서 배포",
|
|
|
"project": "team-retro",
|
|
|
"assignee": "minseo",
|
|
|
"due": "06-08",
|
|
|
"prio": "낮음",
|
|
|
"status": "review",
|
|
|
"children": [
|
|
|
{"title": "액션 아이템 담당자 지정", "status": "done", "assignee": "minseo"},
|
|
|
{"title": "다음 스프린트 반영 확인", "status": "doing", "assignee": "jaeho"},
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
"id": "k10",
|
|
|
"title": "모바일 푸시 알림 QA",
|
|
|
"project": "onb-wire",
|
|
|
"assignee": "sua",
|
|
|
"due": "06-09",
|
|
|
"prio": "높음",
|
|
|
"status": "doing",
|
|
|
"children": [
|
|
|
{"title": "iOS 시나리오 3종", "status": "done", "assignee": "sua"},
|
|
|
{"title": "Android 시나리오 3종", "status": "doing", "assignee": "sua"},
|
|
|
{"title": "딥링크 라우팅 확인", "status": "todo", "assignee": "jaeho"},
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
"id": "k14",
|
|
|
"title": "데이터 전처리 파이프라인",
|
|
|
"project": "onb-research",
|
|
|
"assignee": "minseo",
|
|
|
"due": "06-10",
|
|
|
"prio": "보통",
|
|
|
"status": "waiting",
|
|
|
"delegated": True,
|
|
|
"notes": "<p>일정이 빠듯해 같은 프로젝트의 <b>민서님</b>께 위임 — 현재 완료를 기다리는 중입니다.</p>",
|
|
|
"children": [
|
|
|
{"title": "원천 데이터 스키마 정리", "status": "done", "assignee": "minseo"},
|
|
|
{"title": "결측치 처리 규칙 정의", "status": "doing", "assignee": "minseo"},
|
|
|
{"title": "정제 스크립트 작성", "status": "todo", "assignee": "minseo"},
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
"id": "k20",
|
|
|
"title": "한국행 비행기 티켓 구매",
|
|
|
"project": "life-trip",
|
|
|
"assignee": "jiwoo",
|
|
|
"due": "06-14",
|
|
|
"prio": "높음",
|
|
|
"status": "todo",
|
|
|
"notes": "<p>스마트 인박스에서 자동 생성된 작업이에요. 아리가 <b>가격 추적 알림</b>을 켜뒀고, 적정가가 보이면 결재함으로 알려드려요.</p>",
|
|
|
"children": [
|
|
|
{
|
|
|
"title": "날짜 후보 확정",
|
|
|
"status": "doing",
|
|
|
"assignee": "jiwoo",
|
|
|
"project": "life-trip",
|
|
|
},
|
|
|
{
|
|
|
"title": "가격 알림 확인 후 결제",
|
|
|
"status": "todo",
|
|
|
"assignee": "jiwoo",
|
|
|
"project": "life-trip",
|
|
|
},
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
"id": "k21",
|
|
|
"title": "엄마 생신 선물 알아보기",
|
|
|
"project": "life-fam",
|
|
|
"assignee": "jiwoo",
|
|
|
"due": "06-20",
|
|
|
"prio": "보통",
|
|
|
"status": "todo",
|
|
|
"children": [],
|
|
|
},
|
|
|
]
|
|
|
|
|
|
# kx 카운터 (원본 tuid 와 동일하게 kx1,kx2,...)
|
|
|
_kx = 0
|
|
|
|
|
|
|
|
|
def kx() -> str:
|
|
|
global _kx
|
|
|
_kx += 1
|
|
|
return f"kx{_kx}"
|
|
|
|
|
|
|
|
|
# 댓글 TEXT PK 카운터 (c1, c2, ...)
|
|
|
_cidx = 0
|
|
|
|
|
|
|
|
|
def cid() -> str:
|
|
|
global _cidx
|
|
|
_cidx += 1
|
|
|
return f"c{_cidx}"
|
|
|
|
|
|
|
|
|
def insert_task(s: Session, node: dict, parent_id: str | None, project_id: str, order: int):
|
|
|
tid = node.get("id") or kx()
|
|
|
pid = node.get("project") or project_id # 상속 규칙
|
|
|
t = Task(
|
|
|
id=tid,
|
|
|
project_id=pid,
|
|
|
parent_id=parent_id,
|
|
|
title=node["title"],
|
|
|
status=TaskStatus(node.get("status", "todo")),
|
|
|
assignee_id=node.get("assignee", "jiwoo"),
|
|
|
due=D(node.get("due")),
|
|
|
prio=Prio(node.get("prio", "보통")),
|
|
|
notes=node.get("notes", ""),
|
|
|
est=node.get("est", ""),
|
|
|
delegated=node.get("delegated", False),
|
|
|
sort_order=order,
|
|
|
)
|
|
|
s.add(t)
|
|
|
for c in node.get("comments", []):
|
|
|
s.add(TaskComment(id=cid(), task_id=tid, person_id=c["who"], text=c["text"]))
|
|
|
for i, child in enumerate(node.get("children", [])):
|
|
|
insert_task(s, child, tid, pid, i)
|
|
|
|
|
|
|
|
|
# ---- inbox (REF sinbox-data.js) ----
|
|
|
INBOX = [
|
|
|
{
|
|
|
"id": "s1",
|
|
|
"kind": "text",
|
|
|
"status": "new",
|
|
|
"raw": "다음 주에 한국 놀러가는 비행기 티켓 사기",
|
|
|
"cls": {
|
|
|
"type": "task",
|
|
|
"sphere": "life",
|
|
|
"proj_label": "개인 › 여행 — 한국",
|
|
|
"project_id": "life-trip",
|
|
|
"tone": "coral",
|
|
|
"due_text": "출발 전 · ~6/14",
|
|
|
"when_text": "오늘 21:00 빈 시간 추천",
|
|
|
"extra": "가격 추적 알림 켜둠",
|
|
|
"reason": "구매라는 행동이 있으니 '작업' 맞아요. 작업 트리의 '개인' 아래에 '여행 — 한국' 프로젝트를 만들어 넣었어요 — 따로 섹션이 생기는 게 아니라 다른 작업과 똑같이 보여요. 출발까지 일주일이라 가격 알림도 걸어뒀어요.",
|
|
|
"confidence": 0.92,
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
"id": "s2",
|
|
|
"kind": "text",
|
|
|
"status": "confirmed",
|
|
|
"raw": "수요일 11시 자전거 수리 맡기기",
|
|
|
"cls": {
|
|
|
"type": "event",
|
|
|
"sphere": "life",
|
|
|
"proj_label": "개인 캘린더",
|
|
|
"project_id": None,
|
|
|
"tone": "blue",
|
|
|
"due_text": "수 6/10 11:00",
|
|
|
"when_text": "캘린더 등록 완료",
|
|
|
"extra": "",
|
|
|
"reason": "시간이 정해진 일은 작업이 아니라 일정으로 바로 등록해요.",
|
|
|
"confidence": 0.95,
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
"id": "s3",
|
|
|
"kind": "voice",
|
|
|
"status": "confirmed",
|
|
|
"raw": "음성 메모 0:09 — 엄마 생신 선물 미리 알아보기",
|
|
|
"cls": {
|
|
|
"type": "task",
|
|
|
"sphere": "life",
|
|
|
"proj_label": "가족",
|
|
|
"project_id": "life-fam",
|
|
|
"tone": "green",
|
|
|
"due_text": "6/20 전",
|
|
|
"when_text": "주말 오전 블록",
|
|
|
"extra": "",
|
|
|
"reason": "기한이 느슨한 개인 작업이라 주말 블록에 배치했어요.",
|
|
|
"confidence": 0.8,
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
"id": "s4",
|
|
|
"kind": "text",
|
|
|
"status": "confirmed",
|
|
|
"raw": "온보딩 환영 화면에 짧은 애니메이션 넣으면 어떨까",
|
|
|
"cls": {
|
|
|
"type": "idea",
|
|
|
"sphere": "work",
|
|
|
"proj_label": "온보딩 리디자인 · 아이디어 보드",
|
|
|
"project_id": "onb",
|
|
|
"tone": "violet",
|
|
|
"due_text": "",
|
|
|
"when_text": "",
|
|
|
"extra": "",
|
|
|
"reason": "아직 행동이 정해지지 않아 보드에 보관 — 목요일 디자인 싱크 안건으로도 제안해둘게요.",
|
|
|
"confidence": 0.7,
|
|
|
},
|
|
|
},
|
|
|
]
|
|
|
|
|
|
# ---- dashboard 읽기전용 (REF data.js / approve-data.js) ----
|
|
|
# (id, time, title, tag, dur, tone키, soon)
|
|
|
SCHEDULE = [
|
|
|
("e1", "09:30", "팀 데일리 스탠드업", "프로덕트", "15분", "blue", False),
|
|
|
("e2", "11:00", "디자인 리뷰 — 온보딩 플로우", "디자인", "45분", "violet", False),
|
|
|
("e3", "14:00", "분기 전략 미팅", "경영진", "60분", "coral", True),
|
|
|
("e4", "16:30", "1:1 — 민서님", "팀", "30분", "green", False),
|
|
|
]
|
|
|
# (id, title, pct, sub, tone키)
|
|
|
GOALS = [
|
|
|
("g1", "분기 OKR — 사용자 리텐션", 68, "12개 중 8개 달성", "blue"),
|
|
|
("g2", "주 4회 운동", 75, "이번 주 3/4회", "coral"),
|
|
|
("g3", "‘딥 워크’ 책 완독", 40, "320쪽 중 128쪽", "violet"),
|
|
|
]
|
|
|
APPROVALS = [
|
|
|
(
|
|
|
"a1",
|
|
|
"cal",
|
|
|
"coral",
|
|
|
"low",
|
|
|
"07:42",
|
|
|
"치과 예약을 16:00로 옮겼어요",
|
|
|
"14시 분기 전략 미팅과 겹침 · 병원 예약 시스템에서 빈 슬롯 확인 후 변경",
|
|
|
"",
|
|
|
"",
|
|
|
"원래 시간으로",
|
|
|
),
|
|
|
(
|
|
|
"a2",
|
|
|
"mail",
|
|
|
"blue",
|
|
|
"low",
|
|
|
"06:10",
|
|
|
"영수증·뉴스레터 7통을 정리했어요",
|
|
|
"영수증 3통 → 금융 폴더 · 뉴스레터 4통 → 읽을거리, 받은편지함은 중요한 것만 남김",
|
|
|
"",
|
|
|
"",
|
|
|
"되돌리기",
|
|
|
),
|
|
|
(
|
|
|
"a3",
|
|
|
"cal",
|
|
|
"violet",
|
|
|
"low",
|
|
|
"07:40",
|
|
|
"내일 오전 딥 워크 2시간을 예약했어요",
|
|
|
"분기 리포트 마감(내일 18시) 대비 · 9:00–11:00, 방해 금지로 설정",
|
|
|
"",
|
|
|
"",
|
|
|
"블록 해제",
|
|
|
),
|
|
|
(
|
|
|
"a4",
|
|
|
"mail",
|
|
|
"violet",
|
|
|
"high",
|
|
|
"보내기 대기",
|
|
|
"현우님께 회신 초안이 준비됐어요",
|
|
|
"“잘 받았어요! 금요일 오전까지 화면별 코멘트 정리해서 드릴게요.”",
|
|
|
"보내기",
|
|
|
"수정",
|
|
|
"",
|
|
|
),
|
|
|
(
|
|
|
"a5",
|
|
|
"users",
|
|
|
"green",
|
|
|
"high",
|
|
|
"전달 대기",
|
|
|
"민서님께 ‘데이터 전처리’ 위임 요청",
|
|
|
"오늘 일정 과부하 감지 · 맥락 요약과 마감(목)을 담은 요청 메시지 작성 완료",
|
|
|
"전달",
|
|
|
"내가 할게",
|
|
|
"",
|
|
|
),
|
|
|
(
|
|
|
"a6",
|
|
|
"wallet",
|
|
|
"amber",
|
|
|
"high",
|
|
|
"확인 필요",
|
|
|
"Netflix 일시정지를 추천해요",
|
|
|
"최근 2개월 시청 기록 없음 · 모레 17,000원 결제 예정 — 정지 절차는 준비해뒀어요",
|
|
|
"일시정지",
|
|
|
"유지",
|
|
|
"",
|
|
|
),
|
|
|
]
|
|
|
BRIEFING = dict(
|
|
|
today="6월 7일 일요일",
|
|
|
weather_temp=24,
|
|
|
weather_cond="맑음 · 한낮 28°",
|
|
|
weather_icon="cloudSun",
|
|
|
commute="출근 23분 · 평소보다 4분 빠름",
|
|
|
sleep="어젯밤 7시간 12분 · 평소만큼 푹 잤어요",
|
|
|
note="오늘은 오후 미팅이 핵심이에요. 오전을 비워 <b>분기 리포트</b>에 집중하시면 좋겠어요. 14시 전엔 비가 그칠 예정이라 우산은 안 챙기셔도 돼요.",
|
|
|
saved_today="47분",
|
|
|
today_routed=7,
|
|
|
)
|
|
|
|
|
|
|
|
|
def _seed_dashboard(s: Session) -> None:
|
|
|
"""대시보드 읽기전용 시드 — run_seed 내부에서만 호출하는 헬퍼.
|
|
|
Approval 시드는 phase-7 _seed_approvals 로 단일화(중복 적재 금지)."""
|
|
|
for i, (eid, t, title, tag, dur, tone, soon) in enumerate(SCHEDULE):
|
|
|
s.add(
|
|
|
Event(id=eid, time=t, title=title, tag=tag, dur=dur, tone=tone, soon=soon, sort_order=i)
|
|
|
)
|
|
|
for i, (gid, title, pct, sub, tone) in enumerate(GOALS):
|
|
|
s.add(Goal(id=gid, title=title, pct=pct, sub=sub, tone=tone, sort_order=i))
|
|
|
s.add(Briefing(id=1, **BRIEFING)) # 단일 row id=1
|
|
|
|
|
|
|
|
|
# ---- 결재함 (REF approve-data.js) ----
|
|
|
# (time, text) — REF approve-data.js log
|
|
|
APPROVAL_LOG = [
|
|
|
("08:55", "스탠드업 직전 — 어제 진행 요약 노트 생성"),
|
|
|
("07:30", "출근 경로 확인 · 평소보다 4분 빠른 경로로 안내 예약"),
|
|
|
("06:00", "구독 결제 캘린더 동기화 (Netflix·Spotify 6/9)"),
|
|
|
("어제 23:10", "수면 모드 — 알림 음소거 · 내일 브리핑 예약"),
|
|
|
]
|
|
|
# source 매핑(icon 기반): cal→calendar, mail→mail, users→system, wallet→finance
|
|
|
SRC = {
|
|
|
"cal": ApprovalSource.calendar,
|
|
|
"mail": ApprovalSource.mail,
|
|
|
"users": ApprovalSource.system,
|
|
|
"wallet": ApprovalSource.finance,
|
|
|
}
|
|
|
|
|
|
|
|
|
def _seed_approvals(s: Session) -> None:
|
|
|
for i, (aid, icon, tone, risk, time, title, detail, cta, alt, undo) in enumerate(APPROVALS):
|
|
|
# 시드 status 는 pending; 페이지 응답은 derive_queue 가 자율성으로 재파생.
|
|
|
s.add(
|
|
|
Approval(
|
|
|
id=aid,
|
|
|
icon=icon,
|
|
|
tone=tone,
|
|
|
risk=risk,
|
|
|
time=time,
|
|
|
title=title,
|
|
|
detail=detail,
|
|
|
cta=cta,
|
|
|
alt=alt,
|
|
|
undo_label=undo,
|
|
|
sort_order=i,
|
|
|
status=ApprovalStatus.pending.value,
|
|
|
source=SRC.get(icon, ApprovalSource.system).value,
|
|
|
)
|
|
|
)
|
|
|
for i, (time, text) in enumerate(APPROVAL_LOG):
|
|
|
s.add(ApprovalLog(id=f"alog{i + 1}", time=time, text=text, sort_order=i))
|
|
|
s.add(AutonomySetting(id="default", level=AutonomyLevel.mixed.value)) # 기본 '혼합', 단일 row
|
|
|
|
|
|
|
|
|
# ---- 자동화 (REF auto-data.js) ----
|
|
|
# (id, on, cat, name, trigger, cond, action, last, runs)
|
|
|
RULES = [
|
|
|
(
|
|
|
"r1",
|
|
|
True,
|
|
|
"mail",
|
|
|
"수아님 메일은 바로",
|
|
|
"수아님 발신 메일",
|
|
|
None,
|
|
|
"즉시 알림 + 3줄 요약",
|
|
|
"오늘 09:12",
|
|
|
9,
|
|
|
),
|
|
|
(
|
|
|
"r2",
|
|
|
True,
|
|
|
"focus",
|
|
|
"딥 워크 보호막",
|
|
|
"집중 모드 시작",
|
|
|
"긴급 표시가 아니면",
|
|
|
"알림 보류 + 메신저 상태 '집중'",
|
|
|
"오늘 10:00",
|
|
|
5,
|
|
|
),
|
|
|
(
|
|
|
"r3",
|
|
|
True,
|
|
|
"cal",
|
|
|
"30분 빈틈 채우기",
|
|
|
"일정 사이 30분 빈틈",
|
|
|
"이동이 없으면",
|
|
|
"가벼운 할 일 자동 배치",
|
|
|
"어제 15:40",
|
|
|
7,
|
|
|
),
|
|
|
(
|
|
|
"r4",
|
|
|
True,
|
|
|
"mail",
|
|
|
"뉴스레터는 저녁에",
|
|
|
"뉴스레터 도착",
|
|
|
None,
|
|
|
"보관 후 저녁 다이제스트로 묶음",
|
|
|
"어제 18:30",
|
|
|
14,
|
|
|
),
|
|
|
(
|
|
|
"r5",
|
|
|
True,
|
|
|
"life",
|
|
|
"영수증 자동 정리",
|
|
|
"카드 결제 알림",
|
|
|
None,
|
|
|
"지출 기록 + 카테고리 분류",
|
|
|
"오늘 12:48",
|
|
|
11,
|
|
|
),
|
|
|
(
|
|
|
"r6",
|
|
|
False,
|
|
|
"cal",
|
|
|
"금요일 오후 비우기",
|
|
|
"금 13:00 이후 회의 초대",
|
|
|
"내가 필수 참석자가 아니면",
|
|
|
"다음 주 시간 제안",
|
|
|
"지난주 금",
|
|
|
2,
|
|
|
),
|
|
|
(
|
|
|
"r7",
|
|
|
True,
|
|
|
"life",
|
|
|
"수면 모드 연동",
|
|
|
"23:00이 되면",
|
|
|
"내일 첫 일정이 9시 전이면",
|
|
|
"수면 모드 제안 + 기상 알람 조정",
|
|
|
"어제 23:00",
|
|
|
6,
|
|
|
),
|
|
|
]
|
|
|
# (id, pattern, offer:{name,cat,trigger,cond,action})
|
|
|
SUGGESTS = [
|
|
|
(
|
|
|
"s1",
|
|
|
"월요일 아침마다 주간 리포트 초안을 직접 만드시네요. 최근 4주 연속이에요.",
|
|
|
"주간 리포트 미리 준비",
|
|
|
"focus",
|
|
|
"일요일 21:00",
|
|
|
None,
|
|
|
"지난주 작업·일정으로 초안 생성",
|
|
|
),
|
|
|
(
|
|
|
"s2",
|
|
|
"18시 이후 도착한 메일은 다음 날 아침에 읽으세요. 이번 주 밤 알림만 14번 울렸어요.",
|
|
|
"퇴근 후 메일은 아침에",
|
|
|
"mail",
|
|
|
"18:00~08:00 메일 도착",
|
|
|
"VIP가 아니면",
|
|
|
"아침 브리핑으로 묶음",
|
|
|
),
|
|
|
]
|
|
|
# (time, rule, text) — REF auto-data.js log
|
|
|
RUN_LOG = [
|
|
|
("12:48", "영수증 자동 정리", "점심 결제 13,500원 → 식비로 분류"),
|
|
|
("10:00", "딥 워크 보호막", "알림 4건 보류 — 16:00에 묶음 전달 예정"),
|
|
|
("09:12", "수아님 메일은 바로", "분기 리포트 피드백 도착 → 요약과 함께 알림"),
|
|
|
("08:55", "30분 빈틈 채우기", "10:30 빈틈에 '온보딩 문구 검토' 배치"),
|
|
|
("어제 18:30", "뉴스레터는 저녁에", "뉴스레터 5통 → 저녁 다이제스트로 묶음"),
|
|
|
("어제 23:00", "수면 모드 연동", "내일 8:30 첫 회의 → 수면 모드 제안"),
|
|
|
]
|
|
|
|
|
|
|
|
|
def _seed_automation(s: Session) -> None:
|
|
|
rule_by_name = {}
|
|
|
for rid, on, cat, name, trig, cond, action, last, runs in RULES:
|
|
|
s.add(
|
|
|
AutomationRule(
|
|
|
id=rid,
|
|
|
name=name,
|
|
|
cat=cat,
|
|
|
trigger=trig,
|
|
|
cond=cond,
|
|
|
action=action,
|
|
|
on=on,
|
|
|
last=last,
|
|
|
runs=runs,
|
|
|
fresh=False,
|
|
|
source="user",
|
|
|
)
|
|
|
)
|
|
|
rule_by_name[name] = rid
|
|
|
for sid, pat, oname, ocat, otrig, ocond, oaction in SUGGESTS:
|
|
|
s.add(
|
|
|
AutomationSuggestion(
|
|
|
id="asug-" + sid,
|
|
|
pattern=pat,
|
|
|
offer_name=oname,
|
|
|
offer_cat=ocat,
|
|
|
offer_trigger=otrig,
|
|
|
offer_cond=ocond,
|
|
|
offer_action=oaction,
|
|
|
status="open",
|
|
|
)
|
|
|
)
|
|
|
for i, (time, rule, text) in enumerate(RUN_LOG):
|
|
|
s.add(
|
|
|
AutomationRunLog(
|
|
|
id=f"arun{i + 1}",
|
|
|
time=time,
|
|
|
rule_id=rule_by_name.get(rule),
|
|
|
rule=rule,
|
|
|
text=text,
|
|
|
sort_order=i,
|
|
|
)
|
|
|
)
|
|
|
s.add(AutomationStats(id=1, active=7, runs_week=31, saved="1시간 40분"))
|
|
|
|
|
|
|
|
|
def _seed_proactive(s: Session) -> None:
|
|
|
s.add(
|
|
|
ProactiveCard(
|
|
|
id="pc1",
|
|
|
kind="schedule",
|
|
|
icon="cal",
|
|
|
tone="violet",
|
|
|
title="내일 미팅 3연속 — 점심 비워뒀어요",
|
|
|
why="10:00·11:00·13:00 연속, 12시 공백을 보호 블록으로 잡았어요",
|
|
|
cta="그대로 둘게요",
|
|
|
action_kind="focus_block",
|
|
|
action_payload=json.dumps(
|
|
|
{
|
|
|
"day": 9,
|
|
|
"start": "12:00",
|
|
|
"end": "13:00",
|
|
|
"type": "light",
|
|
|
"title": "점심·재충전 보호",
|
|
|
},
|
|
|
ensure_ascii=False,
|
|
|
),
|
|
|
detected_at="방금",
|
|
|
status="active",
|
|
|
)
|
|
|
)
|
|
|
s.add(
|
|
|
ProactiveCard(
|
|
|
id="pc2",
|
|
|
kind="finance",
|
|
|
icon="wallet",
|
|
|
tone="amber",
|
|
|
title="ChatGPT Plus 청구서가 평소보다 6천원 비싸요",
|
|
|
why="평소 29,000원 → 이번 35,000원 · 사유 확인 권장",
|
|
|
cta="자세히",
|
|
|
action_kind="task",
|
|
|
action_payload=json.dumps(
|
|
|
{"title": "ChatGPT Plus 청구 사유 확인", "project_id": "me"}, ensure_ascii=False
|
|
|
),
|
|
|
detected_at="방금",
|
|
|
status="active",
|
|
|
)
|
|
|
)
|
|
|
|
|
|
|
|
|
def _seed_weekly_review(s: Session) -> None:
|
|
|
patterns = [
|
|
|
{
|
|
|
"title": "이번 주 딥워크 8h, 회의 12h",
|
|
|
"suggest": "화요일 오전을 ‘방해 금지’ 딥워크로 보호할까요?",
|
|
|
"action_kind": "automation",
|
|
|
"payload": {
|
|
|
"name": "화요일 오전 딥워크 보호",
|
|
|
"cat": "focus",
|
|
|
"trigger": "매주 화요일 08:00",
|
|
|
"action": "09:00–11:00 방해 금지 블록 예약",
|
|
|
},
|
|
|
}
|
|
|
]
|
|
|
s.add(
|
|
|
WeeklyReview(
|
|
|
id="wr-2026-w24",
|
|
|
week_label="6/7~6/13",
|
|
|
deep_work_h=8.0,
|
|
|
meeting_h=12.0,
|
|
|
done_count=18,
|
|
|
auto_count=12,
|
|
|
saved_minutes=196,
|
|
|
patterns_json=json.dumps(patterns, ensure_ascii=False),
|
|
|
note="이번 주 딥워크 8h, 회의 12h — 화요일 오전을 ‘방해 금지’ 딥워크로 보호할까요?",
|
|
|
)
|
|
|
)
|
|
|
|
|
|
|
|
|
# phase-15: 로그인 자격(AUTH_ENABLED 일 때만 — 데모/CI 회귀 비용 0).
|
|
|
# (user_id, email, role) — 비밀번호는 demo_admin_password 공통(데모).
|
|
|
_USERS = [
|
|
|
("jiwoo", "jiwoo@lumi.co", "admin"),
|
|
|
("hyunwoo", "hyunwoo@lumi.co", "member"),
|
|
|
]
|
|
|
|
|
|
|
|
|
def _seed_users(s: Session) -> None:
|
|
|
from .config import get_settings
|
|
|
|
|
|
if not get_settings().auth_enabled:
|
|
|
return # 데모 모드: 자격 미생성(pbkdf2 비용 회피 + current_user 가 지우 우회)
|
|
|
from .auth.password import hash_password
|
|
|
|
|
|
pw = get_settings().demo_admin_password
|
|
|
pw_hash = hash_password(pw)
|
|
|
for uid, email, role in _USERS:
|
|
|
s.add(
|
|
|
UserCredential(
|
|
|
id=f"uc-{uid}",
|
|
|
user_id=uid,
|
|
|
email=email,
|
|
|
password_hash=pw_hash,
|
|
|
role=role,
|
|
|
)
|
|
|
)
|
|
|
|
|
|
|
|
|
def _reset_counters() -> None:
|
|
|
global _kx, _cidx
|
|
|
_kx = 0
|
|
|
_cidx = 0
|
|
|
|
|
|
|
|
|
def _run(s: Session, reset: bool) -> None:
|
|
|
"""주어진 세션 위에서 시드 적재(트랜잭션 본문)."""
|
|
|
_reset_counters()
|
|
|
if reset:
|
|
|
for tbl in (
|
|
|
TaskComment,
|
|
|
Task,
|
|
|
InboxClassification,
|
|
|
InboxItem,
|
|
|
Project,
|
|
|
Folder,
|
|
|
Person,
|
|
|
Event,
|
|
|
Goal,
|
|
|
Briefing,
|
|
|
# phase-7 (FK 자식 먼저)
|
|
|
ApprovalLog,
|
|
|
AutomationRunLog,
|
|
|
Approval,
|
|
|
AutomationSuggestion,
|
|
|
AutomationRule,
|
|
|
AutonomySetting,
|
|
|
AutomationStats,
|
|
|
# phase-8 (FK 자식 먼저)
|
|
|
MeetingAction,
|
|
|
Meeting,
|
|
|
FocusBlock,
|
|
|
CalEvent,
|
|
|
Calendar,
|
|
|
# phase-9 (FK 자식 먼저)
|
|
|
Email,
|
|
|
Sent,
|
|
|
Draft,
|
|
|
MailFolder,
|
|
|
MailAccount,
|
|
|
Notification,
|
|
|
Digest,
|
|
|
SenderRule,
|
|
|
NotificationGuard,
|
|
|
NotifyStats,
|
|
|
# phase-10 (FK 자식 먼저)
|
|
|
RagChunk,
|
|
|
TripPlan,
|
|
|
SavedTrip,
|
|
|
TripChecklist,
|
|
|
TripDay,
|
|
|
TripPrep,
|
|
|
TripStay,
|
|
|
TripRoute,
|
|
|
Trip,
|
|
|
ResearchChart,
|
|
|
ResearchQA,
|
|
|
ResearchReport,
|
|
|
ResearchSource,
|
|
|
ResearchCollection,
|
|
|
# phase-11 (FK 자식 먼저)
|
|
|
KnowledgeItem,
|
|
|
KnowledgeCollection,
|
|
|
FinanceInsight,
|
|
|
FinanceSub,
|
|
|
FinanceCategory,
|
|
|
FinanceBudget,
|
|
|
HealthHabit,
|
|
|
SleepNight,
|
|
|
HealthVital,
|
|
|
HealthRing,
|
|
|
ConnectorSource,
|
|
|
# phase-12
|
|
|
JourneyLink,
|
|
|
# phase-13 (FK 자식 먼저)
|
|
|
ConnectorAccountLog,
|
|
|
ConnectorSyncState,
|
|
|
ExternalLink,
|
|
|
OAuthState,
|
|
|
FinanceTx,
|
|
|
HealthSample,
|
|
|
ChatMessage,
|
|
|
ConnectorAccount,
|
|
|
# phase-14 (FK 자식 먼저: ErrandStep → ErrandTask)
|
|
|
ErrandStep,
|
|
|
ErrandTask,
|
|
|
ProactiveCard,
|
|
|
WeeklyReview,
|
|
|
# phase-15 (인증/감사)
|
|
|
AuthSession,
|
|
|
ApiToken,
|
|
|
UserCredential,
|
|
|
AuditLog,
|
|
|
):
|
|
|
for row in s.exec(select(tbl)).all():
|
|
|
s.delete(row)
|
|
|
s.commit()
|
|
|
|
|
|
for pid, name, ini, color, me in PEOPLE:
|
|
|
s.add(Person(id=pid, name=name, initial=ini, color=color, is_me=me))
|
|
|
for fid, name, tone, icon, order in FOLDERS:
|
|
|
s.add(Folder(id=fid, name=name, tone=tone, icon=icon, sort_order=order))
|
|
|
for pid, fid, parent, name, tone, order in PROJECTS:
|
|
|
s.add(
|
|
|
Project(id=pid, folder_id=fid, parent_id=parent, name=name, tone=tone, sort_order=order)
|
|
|
)
|
|
|
s.commit()
|
|
|
|
|
|
# phase-16+: 작업(Task) mock 시드 제거 — 빈 상태로 시작(앱에서 직접 생성).
|
|
|
for n, it in enumerate(INBOX, start=1):
|
|
|
s.add(
|
|
|
InboxItem(
|
|
|
id=it["id"],
|
|
|
kind=InboxKind(it["kind"]),
|
|
|
raw=it["raw"],
|
|
|
status=InboxStatus(it["status"]),
|
|
|
)
|
|
|
)
|
|
|
c = it["cls"]
|
|
|
s.add(
|
|
|
InboxClassification(
|
|
|
id=f"cls{n}", # TEXT PK
|
|
|
inbox_item_id=it["id"],
|
|
|
type=ClsType(c["type"]),
|
|
|
sphere=Sphere(c["sphere"]),
|
|
|
project_id=c["project_id"],
|
|
|
proj_label=c["proj_label"],
|
|
|
tone=c["tone"],
|
|
|
due_text=c["due_text"],
|
|
|
when_text=c["when_text"],
|
|
|
extra=c["extra"],
|
|
|
reason=c["reason"],
|
|
|
confidence=c["confidence"],
|
|
|
model="seed",
|
|
|
)
|
|
|
)
|
|
|
s.commit()
|
|
|
|
|
|
_seed_dashboard(s) # 대시보드 읽기전용 시드 통합
|
|
|
_seed_approvals(s) # phase-7 결재함
|
|
|
_seed_automation(s) # phase-7 자동화
|
|
|
# phase-16+: 일정(_seed_calendar) mock 시드 제거 — Google 캘린더 연결로 유입.
|
|
|
_seed_mail(s) # phase-9 메일 — 연락처(Person)만 유지(메일/계정 mock 제거)
|
|
|
_seed_notify(s) # phase-9 알림
|
|
|
_seed_journey(s) # phase-12 여정/하루마감
|
|
|
_seed_proactive(s) # phase-14 능동 카드 2장
|
|
|
_seed_weekly_review(s) # phase-14 주간 리뷰
|
|
|
_seed_users(s) # phase-15 로그인 자격(AUTH_ENABLED 일 때만)
|
|
|
s.commit()
|
|
|
|
|
|
|
|
|
def run_seed(session: Session | None = None, reset: bool = True) -> None:
|
|
|
"""시드 진입 함수 단일 정본.
|
|
|
|
|
|
- CLI: run_seed() → 자체 세션 생성(엔진에서).
|
|
|
- 테스트: run_seed(session=test_session, reset=True) → 주어진 세션 재사용.
|
|
|
"""
|
|
|
init_db()
|
|
|
if session is not None:
|
|
|
_run(session, reset)
|
|
|
else:
|
|
|
with Session(engine) as s:
|
|
|
_run(s, reset)
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
run_seed()
|
|
|
print("✅ seed 완료")
|