# 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": "
경영진 미팅용 Q2 성과 리포트. 리텐션·매출·예산 세 섹션으로 구성한다.
핵심 지표는 시각화해서 한눈에 들어오게.", "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": "
MRR/ARR 표는 완료. 증감 코멘트 마무리 필요.
", "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": "디자인 리뷰에서 나온 3개 화면 수정사항을 취합하고 우선순위를 매긴다.
", "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": "일정이 빠듯해 같은 프로젝트의 민서님께 위임 — 현재 완료를 기다리는 중입니다.
", "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": "스마트 인박스에서 자동 생성된 작업이에요. 아리가 가격 추적 알림을 켜뒀고, 적정가가 보이면 결재함으로 알려드려요.
", "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="오늘은 오후 미팅이 핵심이에요. 오전을 비워 분기 리포트에 집중하시면 좋겠어요. 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 완료")