Skip to content

Commit e33656f

Browse files
committed
Drive diagram runtime from YAML
1 parent bea1c52 commit e33656f

11 files changed

Lines changed: 81 additions & 16 deletions

File tree

docs/skills/architecture/learning-yaml-contract.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ intro:
3838
detail: 따라 칠 코드
3939
- label: 실행
4040
detail: 입력과 검증
41+
runtime:
42+
- label: 계약
43+
detail: YAML SSOT
44+
- label: 환경
45+
detail: uv 패키지
46+
- label: 검증
47+
detail: 실행 결과
4148
sections:
4249
- id: dataframe-basics
4350
title: DataFrame 만들기
@@ -70,7 +77,7 @@ sections:
7077
## 렌더링 원칙
7178
7279
- 레슨 상단은 `intro.direction`, `intro.benefits`, `intro.diagram`을 읽어 무엇을 공부하는지, 왜 유용한지, 전체 흐름을 보여준다.
73-
- `intro.diagram`은 제품 화면에서 blueprint 질감, 좌측 overview rail, 번호가 있는 노드, 연결선, 흐름 트랙, 런타임 스트립을 가진 학습 아키텍처 캔버스로 렌더링한다. 단순 목록 박스로 축소하지 않고, 상단 benefit은 작은 박스 반복이 아니라 흐름을 보조하는 체크 리스트로 둔다.
80+
- `intro.diagram`은 제품 화면에서 blueprint 질감, 좌측 overview rail, 번호가 있는 노드, 연결선, 흐름 트랙, 런타임 스트립을 가진 학습 아키텍처 캔버스로 렌더링한다. `diagram.steps`는 상단 학습 흐름, `diagram.runtime`은 계약/환경/검증 같은 런타임 레이어를 정의한다. 단순 목록 박스로 축소하지 않고, 상단 benefit은 작은 박스 반복이 아니라 흐름을 보조하는 체크 리스트로 둔다.
7481
- 섹션 하나가 학습카드 하나다. `sections[].blocks[]`의 작은 카드 반복을 기본 구조로 삼지 않는다.
7582
- 섹션 카드는 `title → subtitle → goal → why → explanation → tips → snippet → exercise → result → check` 순서로 이어진다.
7683
- 카드 내부 정보는 라벨, 구획선, 여백으로 구분한다. 카드 안에 또 카드가 덕지덕지 쌓이는 구조는 피한다.
@@ -80,7 +87,7 @@ sections:
8087
- 섹션 헤더 번호는 타이틀/서브타이틀 묶음과 높이를 맞춘다. 셀 도움 요청 액션은 hover-only가 아니라 항상 보이는 control로 둔다.
8188
- 셀 TOC는 push rail이다. overlay flyout으로 같은 아이콘 목록을 한 번 더 띄우지 않는다.
8289
- structured 섹션의 `exercise` band는 클릭해야 열리는 preview가 아니라 바로 보이는 실제 입력 editor를 가진다. `learning-card-browser` gate는 이 editor가 desktop/mobile에서 보이고 starter code를 렌더링하는지 확인한다.
83-
- 레슨 overview는 `data-learning-overview`, `data-learning-overview-blueprint`, `data-learning-overview-rail`, `data-learning-overview-part`, `data-learning-flow-diagram`, `data-learning-flow-blueprint`, `data-learning-flow-track`, `data-learning-flow-step`, `data-learning-flow-runtime-node` marker를 가진다. `learning-card-browser` gate는 방향, 학습 효과, 플로우 track/step/runtime node가 실제 desktop/mobile 화면에 보이는지 확인한다.
90+
- 레슨 overview는 `data-learning-overview`, `data-learning-overview-blueprint`, `data-learning-overview-rail`, `data-learning-overview-part`, `data-learning-flow-diagram`, `data-learning-flow-blueprint`, `data-learning-flow-track`, `data-learning-flow-step`, `data-learning-flow-runtime-node` marker를 가진다. `learning-card-browser` gate는 방향, 학습 효과, 플로우 track/step/runtime node가 실제 desktop/mobile 화면에 보이는지 확인하고, runtime node 문구가 YAML의 `intro.diagram.runtime`에서 온다는 점을 검증한다.
8491
- structured section card는 브라우저 검증을 위해 `data-learning-section-card`, `data-learning-section-structured`, `data-learning-section-part` marker를 가진다. 검증 대상 part는 `overview`, `snippet`, `exercise`, `result`, `check`다.
8592
- marker 계약과 editor build는 `uv run python -X utf8 tests/run.py gate learning-card-contract`로 확인한다. 실제 데스크톱/모바일 브라우저 렌더링은 `uv run python -X utf8 tests/run.py gate learning-card-browser`로 확인한다.
8693
- `snippet`은 예제 스니펫 셀로, `exercise.starterCode`는 학습자가 직접 입력/수정하는 실습 셀로 materialize한다. 렌더러는 이 영역을 `Python 실습 코드`와 `학습자가 작성` 상태로 명확히 표시하고 `data-learning-exercise-input-role="student-practice"`를 유지한다.

editor/src/components/curriculum/curriculumSurface.tsx

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ function LearningRouteConnector() {
308308

309309
function LearningFlowDiagram({ diagram }: { diagram?: Record<string, unknown> }) {
310310
const customSteps = diagramSteps(diagram);
311+
const customRuntimeNodes = diagramRuntimeNodes(diagram);
311312
const fallbackSteps = [
312313
{ label: "목표", detail: "무슨 공부", Icon: Route, tone: "text-sky-500" },
313314
{ label: "개념", detail: "설명과 팁", Icon: BookOpen, tone: "text-amber-500" },
@@ -324,6 +325,18 @@ function LearningFlowDiagram({ diagram }: { diagram?: Record<string, unknown> })
324325
}))
325326
: fallbackSteps;
326327
const visibleSteps = steps.slice(0, 4);
328+
const runtimeIcons = [GitBranch, Package, CheckCircle2];
329+
const fallbackRuntimeNodes = [
330+
{ label: "계약", detail: "YAML SSOT", Icon: GitBranch },
331+
{ label: "환경", detail: "uv 패키지", Icon: Package },
332+
{ label: "검증", detail: "실행 결과", Icon: CheckCircle2 },
333+
];
334+
const runtimeNodes = customRuntimeNodes.length
335+
? customRuntimeNodes.map((node, index) => ({
336+
...node,
337+
Icon: runtimeIcons[index % runtimeIcons.length],
338+
}))
339+
: fallbackRuntimeNodes;
327340

328341
return (
329342
<div
@@ -417,11 +430,7 @@ function LearningFlowDiagram({ diagram }: { diagram?: Record<string, unknown> })
417430
<div className="mt-4 rounded-md border bg-card/80 px-3 py-3" data-learning-flow-runtime="true">
418431
<div className="mb-2 text-xs font-medium text-muted-foreground">런타임 레이어</div>
419432
<div className="grid gap-2 sm:grid-cols-3">
420-
{[
421-
{ label: "계약", detail: "YAML SSOT", Icon: GitBranch },
422-
{ label: "환경", detail: "uv 패키지", Icon: Wrench },
423-
{ label: "검증", detail: "실행 결과", Icon: CheckCircle2 },
424-
].map(({ Icon, detail, label }) => (
433+
{runtimeNodes.slice(0, 3).map(({ Icon, detail, label }) => (
425434
<div className="flex min-w-0 items-center gap-2 text-xs" data-learning-flow-runtime-node="true" key={label}>
426435
<Icon className="size-3.5 shrink-0 text-muted-foreground" />
427436
<span className="min-w-0">
@@ -1125,6 +1134,29 @@ function diagramSteps(diagram?: Record<string, unknown>) {
11251134
.slice(0, 5);
11261135
}
11271136

1137+
function diagramRuntimeNodes(diagram?: Record<string, unknown>) {
1138+
if (!diagram) return [];
1139+
const source = Array.isArray(diagram.runtime)
1140+
? diagram.runtime
1141+
: Array.isArray(diagram.runtimeLayer)
1142+
? diagram.runtimeLayer
1143+
: Array.isArray(diagram.layers)
1144+
? diagram.layers
1145+
: [];
1146+
return source
1147+
.map((item) => {
1148+
if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
1149+
return { label: String(item), detail: "" };
1150+
}
1151+
if (!isRecord(item)) return null;
1152+
const label = readPayloadText(item.label ?? item.title ?? item.name);
1153+
const detail = readPayloadText(item.detail ?? item.description ?? item.content);
1154+
return label ? { label, detail } : null;
1155+
})
1156+
.filter((item): item is { label: string; detail: string } => Boolean(item))
1157+
.slice(0, 3);
1158+
}
1159+
11281160
function readSectionContract(value: unknown): CurriculumSectionContract | undefined {
11291161
return isRecord(value) ? value as CurriculumSectionContract : undefined;
11301162
}

0 commit comments

Comments
 (0)