Morit Plugin8. 실제 예제
Runtime v3 예제: 날짜별 학교생활
날짜를 고르면 그날의 시간표를 카드로 보여 주는 완성된 Runtime v3 플러그인입니다. 아래 파일을 그대로
만들면 validate, runtime-test, build가 통과합니다. Morit 저장소의 release 회귀 fixture
(test/fixtures/plugins/school-life-runtime-v3)를 줄인 것입니다.
text
school-day/
├─ manifest.json
├─ logic/school.morit
├─ src/school.py
└─ fixtures/day.json # runtime-test 전용, 패키지에는 들어가지 않음
1. manifest.json
logic_modules는 Morit Script 모듈과 export의 입출력 schema를 선언합니다.- data source는 capability의 원본 응답을 받아(
result_mode: "raw")normalizeDay로 변환합니다. depends_on에 따라 날짜를 바꾸면 다시 요청하고, 이전 날짜의 늦은 응답은 버립니다.data_state가 로딩·빈 데이터·오류·내용을 나눠 표시하고,repeat와component가 카드를 반복합니다.
json
{
"schema_version": 2,
"id": "com.example.school_day",
"name": "School Day",
"description": "날짜별 시간표와 급식을 보여 주는 Runtime v3 예제",
"publisher": "example",
"version": "1.0.0",
"min_morit_version": "1.9.0",
"max_morit_version": "1.999.999",
"permissions": [],
"data_policy": "purge",
"capabilities": [
{
"id": "com.example.school_day.lookup",
"kind": "tool",
"title": "학교생활 조회",
"description": "날짜의 시간표와 급식을 조회합니다.",
"permissions": [],
"input_schema": {
"type": "object",
"properties": {"date": {"type": "string"}},
"required": ["date"]
},
"runtime": {"adapter": "sandbox_python", "entrypoint": "src/school.py"}
}
],
"ui_extensions": [
{
"id": "com.example.school_day.screen",
"point": "screen",
"title": "학교생활",
"permissions": [],
"config": {
"ui_schema": 3,
"logic_modules": [
{
"id": "school",
"path": "logic/school.morit",
"exports": {
"normalizeDay": {
"input_schema": {
"type": "object",
"required": ["days"],
"properties": {"days": {"type": "array", "items": {"type": "object"}}}
},
"output_schema": {
"type": "object",
"required": ["days", "lessonCount"],
"properties": {
"days": {"type": "array", "items": {"type": "object"}},
"lessonCount": {"type": "integer"}
}
}
}
}
}
],
"initial_state": {"selected_date": "{{= runtime.today }}"},
"computed_state": {"title": "{{= state.selected_date + \" 학교생활\" }}"},
"data_sources": [
{
"id": "school_day",
"capability": "com.example.school_day.lookup",
"trigger": "load",
"depends_on": ["selected_date"],
"arguments": {"date": "{{= state.selected_date }}"},
"result_mode": "raw",
"transform": {"module": "school", "export": "normalizeDay"},
"cache": {"policy": "stale_while_revalidate", "ttl_seconds": 300},
"retry": {"max_attempts": 2, "delay_ms": 500, "on": ["timeout", "network", "server"]}
}
],
"components": {
"lesson_card": {
"props_schema": {
"type": "object",
"properties": {"lesson": {"type": "object"}},
"required": ["lesson"]
},
"view": {
"type": "card",
"children": [
{"type": "text", "props": {"text": "{{= props.lesson.label }}", "style": "label"}},
{"type": "text", "props": {"text": "{{= props.lesson.subject }}"}}
]
}
}
},
"view": {
"type": "column",
"children": [
{"type": "text", "props": {"text": "{{= computed.title }}", "style": "title"}},
{
"type": "date_field",
"props": {"state_key": "selected_date", "label": "날짜", "required": true}
},
{
"type": "data_state",
"props": {
"source": "school_day",
"empty_when": "{{= data.school_day != null && data.school_day.lessonCount == 0 }}"
},
"slots": {
"loading": {"type": "skeleton", "props": {"height": 60, "variant": "card"}},
"empty": {"type": "empty", "props": {"title": "이 날은 수업이 없어요"}},
"content": {
"type": "repeat",
"props": {
"items": "{{= data.school_day.days[0].lessons }}",
"as": "lesson",
"key": "{{= lesson.period }}",
"layout": "grid",
"columns": {"compact": 1, "medium": 2, "expanded": 3}
},
"slots": {
"item": {"type": "component", "props": {"component": "lesson_card", "props": {"lesson": "{{= lesson }}"}}}
}
}
}
}
]
}
}
}
]
}
2. logic/school.morit
HTML이 섞인 급식 메뉴를 텍스트로 바꾸고 교시 라벨을 붙입니다. 문법과 내장 함수는 Morit Script 레퍼런스를 참고하세요.
text
function cleanMenu(items) {
return items.map(item => html.toText(item)).filter(item => item != "");
}
export function normalizeDay(input) {
const days = input.days.map(day => ({
date: day.date,
lessons: day.lessons.map(lesson => ({
period: lesson.period,
subject: lesson.subject,
label: string.from(lesson.period) + "교시"
})),
meals: day.meals.map(meal => ({
name: meal.name,
menu: cleanMenu(meal.menu)
}))
}));
return {days: days, lessonCount: days.reduce((count, day) => count + day.lessons.length, 0)};
}
3. src/school.py
sandbox_python capability는 handle(request) 함수 하나를 구현합니다. request에는 query와
arguments가 들어 있고, 결과는 summary·data·evidence 객체입니다. 외부 API를 쓰려면 이
capability를 http_json adapter로 바꿔도 화면 설정은 그대로입니다.
python
# A real plugin would call a school API here; this example returns fixed data.
DAYS = [
{
"date": "2026-09-16",
"lessons": [
{"period": 1, "subject": "국어"},
{"period": 2, "subject": "수학"},
],
"meals": [{"name": "점심", "menu": ["<b>현미밥</b>", "된장국<br>두부"]}],
}
]
def handle(request):
"""The Host calls handle() with {"query": ..., "arguments": {...}}."""
date = request.get("arguments", {}).get("date")
days = [day for day in DAYS if day["date"] == date]
return {"summary": f"{date} 학교생활", "data": {"days": days}, "evidence": []}
4. fixtures/day.json
runtime-test가 capability 대신 사용하는 응답입니다.
json
{
"days": [
{
"date": "2026-09-16",
"lessons": [
{
"period": 1,
"subject": "국어"
},
{
"period": 2,
"subject": "수학"
},
{
"period": 3,
"subject": "과학"
}
],
"meals": [
{
"name": "점심",
"menu": [
"<b>현미밥</b>",
"된장국<br>두부",
"사과 & 배",
""
]
}
]
},
{
"date": "2026-09-17",
"lessons": [],
"meals": []
}
]
}
5. 검증과 build
bash
npx -y @morit/cli plugin validate .
npx -y @morit/cli plugin runtime-test . --fixture fixtures/day.json \
--now 2026-09-16T00:30:00Z --timezone Asia/Seoul --json
npx -y @morit/cli plugin preview . --output dist/preview.html
npx -y @morit/cli plugin build . --output dist/school-day.mplg
runtime-test 결과의 output에는 변환된 days와 lessonCount가, inspector.computed_state에는
{"title": "2026-09-16 학교생활"}이 들어 있어야 합니다. 마지막으로 앱에 설치해 날짜 선택, 빈 날짜,
오류 후 다시 시도를 확인합니다.