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.

490 lines
19 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
from datetime import date
from sqlmodel import Session, select
from .db import engine, init_db
from .models import (
Approval,
Briefing,
ClsType,
Event,
Folder,
Goal,
InboxClassification,
InboxItem,
InboxKind,
InboxStatus,
Person,
Prio,
Project,
Sphere,
Task,
TaskComment,
TaskStatus,
)
YEAR = 2026
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 내부에서만 호출하는 헬퍼."""
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))
for i, row in enumerate(APPROVALS):
aid, icon, tone, risk, time, title, detail, cta, alt, undo = row
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,
)
)
s.add(Briefing(id=1, **BRIEFING)) # 단일 row id=1
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, Approval, Goal, Briefing,
):
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()
for i, node in enumerate(TASKS):
insert_task(s, node, None, node["project"], i)
s.commit()
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) # 대시보드 읽기전용 시드 통합
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 완료")