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.

1095 lines
34 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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:0011: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:0011: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 완료")