Naran 데이터 모델 (v4)
상태: 확정 (설계하자에서 검토 완료) zeroclaw 위에 얹는 도메인 레이어. zeroclaw DB는 건드리지 않음.
핵심 확정 사항
Naran 구조:
별도 MCP 서버로 독립 운영 (zeroclaw 포크 없음)
zeroclaw upstream 업데이트를 그대로 받아먹음
DB 구조:
naran.db → MySQL (어느 프로세스에서든 연결 가능)
cron/jobs.db → zeroclaw SQLite (sequential 접근, WAL 충돌 없음)
접근 방식:
Naran MCP Tool → MySQL naran.db 직접 SQL
Naran MCP Tool → zeroclaw cron/jobs.db 직접 SQL (sequential)
REST API 레이어 없음
핵심 개념
Task = 인간의 work 구조체 (상태 없음)
Event = Task의 상태 (시간 블록 배정)
rules = rule 정의 저장소 (재사용 or 일회성 무관)
task_rules = Task ↔ Rule 관계
event_rules = Event ↔ Rule 관계
Event 생성 시점에:
task_rules + event_rules → zeroclaw cron_jobs 변환 (sequential)
Task 상태 파생
Task 자체는 상태를 들고 있지 않는다. events에서 파생:
events 없음 → unscheduled
미래 event 있음 → scheduled
due_date 지났는데 done event 없음 → overdue
done event 있음 → complete
재사용 vs 일회성 Rule
모든 rule은 rules 테이블을 거친다.
재사용이냐 일회성이냐는 얼마나 많은 곳에서 참조하느냐의 차이:
재사용 rule:
rules row 1개 ← task_rules에서 여러 Task가 참조
예: "spotify 아무노래" → 손씻기, 운동, 공부 Task 모두 연결
일회성 rule:
rules row 1개 ← event_rules에서 딱 1번만 참조
예: "오늘만 youtube 인기동영상" → 이번 Event에만 연결
전체 흐름 예시
"손씻기 Task 만들고 spotify 규칙 만들어줘"
→ tasks INSERT (MySQL)
→ rules INSERT (MySQL)
→ task_rules INSERT (MySQL)
"손씻기 1분 전 알람도 추가해줘"
→ rules INSERT (MySQL)
→ task_rules INSERT (MySQL, trigger=prepare, offset=1)
← spotify rule은 그대로 유지 (append)
"손씻기를 10분 뒤 Event로 등록하고 youtube도 틀어줘"
→ events INSERT (MySQL)
→ rules INSERT (MySQL, youtube)
→ event_rules INSERT (MySQL)
→ task_rules 읽어서 → cron_jobs INSERT (zeroclaw SQLite, sequential)
→ event_rules 읽어서 → cron_jobs INSERT (zeroclaw SQLite, sequential)
Generative UI 연동
DB 스키마 변경 없음. Tool 반환값에 render_hint 포함:
{
"render_hint": "calendar",
"data": [{"id": "...", "title": "손씻기", "start": "..."}]
}
| render_hint | 설명 |
|------------|------|
| "calendar" | CalendarView 컴포넌트 |
| "task_list" | TaskListView 컴포넌트 |
| "rule_list" | RuleListView 컴포넌트 |
| "rule_flow" | RuleFlowChart 컴포넌트 |
| 없음 | 일반 chat card |
소유권 분리
MySQL naran.db (Naran MCP 서버 소유):
tasks
rules
task_rules
event_rules
events
event_series
event_series_exceptions
zeroclaw cron/jobs.db (zeroclaw 소유, 건드리지 않음):
cron_jobs ← Event 생성 시점에 변환되어 등록 (sequential)
cron_runs ← 실행 이력
스키마
1. tasks
CREATE TABLE tasks (
id VARCHAR(36) PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
due_date DATE,
priority TINYINT DEFAULT 2,
-- 1(high) 2(normal) 3(low)
energy_level VARCHAR(20),
-- 'deep' | 'shallow' | 'admin'
tags JSON,
project_id VARCHAR(36),
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
INDEX idx_tasks_due_date (due_date)
);
2. rules
CREATE TABLE rules (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(255),
-- 재사용 rule에 유용한 사람이 읽기 좋은 이름
content_type VARCHAR(20) NOT NULL,
-- 'prompt' | 'shell'
content JSON NOT NULL,
-- prompt: {"text": "spotify에서 아무노래 틀기", "allowed_tools": ["shell"]}
-- shell: {"commands": {"macos": "...", "windows": "...", "linux": "..."}}
created_at DATETIME NOT NULL
);
3. task_rules
CREATE TABLE task_rules (
id VARCHAR(36) PRIMARY KEY,
task_id VARCHAR(36) NOT NULL,
rule_id VARCHAR(36) NOT NULL,
trigger VARCHAR(10) NOT NULL,
-- 'prepare' | 'doing' | 'end'
offset_minutes INT DEFAULT 0,
-- trigger='prepare'일 때 몇 분 전
condition JSON,
-- null이면 항상 실행
-- {"type": "date_range", "from": "2024-05-20", "to": "2024-06-15"}
-- {"type": "weekday", "days": ["monday", "wednesday"]}
enabled TINYINT(1) DEFAULT 1,
priority INT DEFAULT 0,
-- 같은 trigger 내 실행 순서
created_at DATETIME NOT NULL,
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY (rule_id) REFERENCES rules(id) ON DELETE CASCADE,
INDEX idx_task_rules_task_id (task_id),
INDEX idx_task_rules_rule_id (rule_id)
);
4. event_rules
CREATE TABLE event_rules (
id VARCHAR(36) PRIMARY KEY,
event_id VARCHAR(36) NOT NULL,
rule_id VARCHAR(36) NOT NULL,
trigger VARCHAR(10) NOT NULL,
-- 'prepare' | 'doing' | 'end'
offset_minutes INT DEFAULT 0,
condition JSON,
enabled TINYINT(1) DEFAULT 1,
priority INT DEFAULT 0,
created_at DATETIME NOT NULL,
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE,
FOREIGN KEY (rule_id) REFERENCES rules(id) ON DELETE CASCADE,
INDEX idx_event_rules_event_id (event_id),
INDEX idx_event_rules_rule_id (rule_id)
);
5. event_series
CREATE TABLE event_series (
id VARCHAR(36) PRIMARY KEY,
title VARCHAR(255) NOT NULL,
rrule VARCHAR(500) NOT NULL,
-- "FREQ=WEEKLY;BYDAY=MO;COUNT=15"
-- "FREQ=WEEKLY;BYDAY=MO;UNTIL=20240630"
timezone VARCHAR(50) DEFAULT 'Asia/Seoul',
materialized_until DATE,
-- 여기까지 eager row 생성됨 (1년 기준)
created_at DATETIME NOT NULL
);
6. event_series_exceptions
CREATE TABLE event_series_exceptions (
id VARCHAR(36) PRIMARY KEY,
series_id VARCHAR(36) NOT NULL,
original_date DATE NOT NULL,
exception_type VARCHAR(10) NOT NULL,
-- 'deleted' | 'modified'
event_id VARCHAR(36),
-- modified이면 수정된 events.id 참조
FOREIGN KEY (series_id) REFERENCES event_series(id) ON DELETE CASCADE,
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE SET NULL,
INDEX idx_exceptions_series (series_id)
);
7. events
CREATE TABLE events (
id VARCHAR(36) PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
start_datetime DATETIME NOT NULL,
end_datetime DATETIME NOT NULL,
timezone VARCHAR(50) DEFAULT 'Asia/Seoul',
is_all_day TINYINT(1) DEFAULT 0,
task_id VARCHAR(36),
series_id VARCHAR(36),
series_index INT,
status VARCHAR(20) DEFAULT 'scheduled',
-- 'scheduled' | 'done' | 'cancelled'
completed_at DATETIME,
priority TINYINT DEFAULT 2,
energy_level VARCHAR(20),
tags JSON,
project_id VARCHAR(36),
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (task_id) REFERENCES tasks(id),
FOREIGN KEY (series_id) REFERENCES event_series(id) ON DELETE CASCADE,
INDEX idx_events_start (start_datetime),
INDEX idx_events_task_id (task_id),
INDEX idx_events_series_id (series_id)
);
task_rules + event_rules → zeroclaw cron_jobs 변환
Event 생성 시점에 sequential하게 처리:
name 형식: "naran:rule:{source}:{source_id}:{event_id}:{trigger}"
task_rules → "naran:rule:task:task-001:event-001:doing"
event_rules → "naran:rule:event:event-001:event-001:doing"
{
"name": "naran:rule:task:task-001:event-001:doing",
"schedule": {"kind": "at", "at": "2024-04-01T09:00:00Z"},
"job_type": "agent",
"prompt": "spotify에서 아무노래 틀기",
"delete_after_run": true
}
삭제 쿼리:
-- Event 삭제 시 관련 cron_jobs 정리
DELETE FROM cron_jobs WHERE name LIKE 'naran:rule:%:event-001:%'
-- Task 삭제 시 관련 cron_jobs 정리
DELETE FROM cron_jobs WHERE name LIKE 'naran:rule:task:task-001:%'
반복 Event 전략 — Hybrid Materialization
1년 이내 인스턴스 → MySQL row 즉시 생성 (eager)
1년 초과 인스턴스 → 조회 시 rrule 계산 (lazy, DB row 없음)
lazy → eager 전환 트리거:
1. rule을 붙일 때
2. Event 시작 30분 전 (자동)
3. 사용자가 캘린더에서 해당 날짜 탭
시리즈 삭제 옵션:
"이 일정만" → event_series_exceptions deleted 기록
"이후 모두" → rrule에 UNTIL 추가
"모든 일정" → event_series 삭제 (CASCADE)
Naran MCP Tool 목록
| Tool | 동작 | render_hint |
|------|------|------------|
| task_add | tasks INSERT | - |
| task_list | tasks SELECT + 상태 파생 | "task_list" |
| task_update | tasks UPDATE | - |
| rule_add | rules INSERT | - |
| rule_list | rules SELECT | "rule_list" |
| task_rule_add | task_rules INSERT | - |
| task_rule_list | task_rules + rules JOIN | "rule_flow" |
| task_rule_remove | task_rules DELETE | - |
| event_add | events INSERT + task_rules/event_rules → cron_jobs (sequential) | - |
| event_rule_add | event_rules INSERT + (rules INSERT) → cron_jobs | - |
| event_rule_remove | event_rules DELETE + cron_jobs DELETE | - |
| event_list | events SELECT + lazy 머지 | "calendar" |
| event_update | events UPDATE + exception 처리 | - |
| event_delete | exceptions or CASCADE + cron_jobs 정리 | - |
ERD
세부 미결 사항
- [ ] 반복 시리즈에서 task_rules 변환 시점 — 시리즈 단위 한 번 vs 인스턴스마다
- [ ] lazy 인스턴스 rrule 계산 위치 (Naran MCP Tool vs Tauri invoke)
- [ ] event_delete 시 zeroclaw에서 실행 중인 cron_jobs 처리 방식
- [ ] rules 삭제 정책 — task_rules/event_rules 참조 중인 rule 삭제 가능 여부