@vibeuniv/mcp-server 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { scanTeachingCriticalFiles } from "../lib/file-scanner.js";
2
3
  export const generateCurriculumSchema = {
3
4
  project_id: z.string().describe("The VibeUniv project ID"),
4
5
  difficulty: z
@@ -10,33 +11,53 @@ export const generateCurriculumSchema = {
10
11
  function formatTechStack(t) {
11
12
  return `- **${t.name}**${t.version ? ` v${t.version}` : ""} (${t.category})`;
12
13
  }
13
- function formatKBHints(kbHints) {
14
+ function formatKBHints(kbHints, locale) {
14
15
  const sections = [];
15
16
  for (const [techName, hints] of Object.entries(kbHints)) {
16
17
  if (hints.length === 0)
17
18
  continue;
18
- const conceptLines = hints.map((h) => `#### ${h.concept_name} (\`${h.concept_key}\`)
19
+ const conceptLines = locale === "en"
20
+ ? hints.map((h) => `#### ${h.concept_name} (\`${h.concept_key}\`)
21
+ - **Key Points:**
22
+ ${h.key_points.map((p) => ` - ${p}`).join("\n")}
23
+ - **Good Quiz Topics:** ${h.common_quiz_topics.join(", ")}
24
+ - **Prerequisites:** ${h.prerequisite_concepts.length > 0 ? h.prerequisite_concepts.join(", ") : "(none)"}`).join("\n\n")
25
+ : hints.map((h) => `#### ${h.concept_name} (\`${h.concept_key}\`)
19
26
  - **핵심 포인트:**
20
27
  ${h.key_points.map((p) => ` - ${p}`).join("\n")}
21
28
  - **좋은 퀴즈 주제:** ${h.common_quiz_topics.join(", ")}
22
29
  - **선행 개념:** ${h.prerequisite_concepts.length > 0 ? h.prerequisite_concepts.join(", ") : "(없음)"}`).join("\n\n");
23
- sections.push(`### ${techName} 핵심 개념 가이드\n\n${conceptLines}`);
30
+ const heading = locale === "en"
31
+ ? `### ${techName} Core Concept Guide`
32
+ : `### ${techName} 핵심 개념 가이드`;
33
+ sections.push(`${heading}\n\n${conceptLines}`);
24
34
  }
25
- return sections.length > 0
26
- ? `## 교육 핵심 포인트 (Knowledge Base)
35
+ if (sections.length === 0)
36
+ return "";
37
+ const header = locale === "en"
38
+ ? `## Educational Key Points (Knowledge Base)
39
+
40
+ Below are **core educational key points** for each technology.
41
+ Include these points in the curriculum, reference quiz topics, and follow prerequisite ordering.`
42
+ : `## 교육 핵심 포인트 (Knowledge Base)
27
43
 
28
44
  아래는 각 기술의 **핵심 교육 포인트**입니다.
29
45
  커리큘럼에 반드시 이 포인트들을 포함하고, 퀴즈 주제를 참고하세요.
30
- 선행 개념 순서에 맞게 모듈을 배치하세요.
31
-
32
- ${sections.join("\n\n")}`
33
- : "";
46
+ 선행 개념 순서에 맞게 모듈을 배치하세요.`;
47
+ return `${header}\n\n${sections.join("\n\n")}`;
34
48
  }
35
- function formatEducationalAnalysis(analysis, difficulty) {
49
+ function formatEducationalAnalysis(analysis, difficulty, locale) {
36
50
  const sections = [];
51
+ const en = locale === "en";
37
52
  // Project Overview
38
53
  const ov = analysis.project_overview;
39
- sections.push(`### 프로젝트 개요 (AI 분석 결과)
54
+ sections.push(en
55
+ ? `### Project Overview (AI Analysis)
56
+ - **App Description:** ${ov.one_liner}
57
+ - **App Type:** ${ov.app_type}
58
+ - **Target Users:** ${ov.target_users}
59
+ - **Core Features:** ${ov.core_features.join(", ")}`
60
+ : `### 프로젝트 개요 (AI 분석 결과)
40
61
  - **앱 설명:** ${ov.one_liner}
41
62
  - **앱 유형:** ${ov.app_type}
42
63
  - **대상 사용자:** ${ov.target_users}
@@ -47,16 +68,24 @@ function formatEducationalAnalysis(analysis, difficulty) {
47
68
  const steps = f.steps
48
69
  .map((s) => ` - ${s.description} (\`${s.file}\`:${s.line_range})`)
49
70
  .join("\n");
50
- return `- **${f.name}** (난이도: ${f.difficulty})\n 트리거: ${f.trigger}\n${steps}`;
71
+ return en
72
+ ? `- **${f.name}** (difficulty: ${f.difficulty})\n Trigger: ${f.trigger}\n${steps}`
73
+ : `- **${f.name}** (난이도: ${f.difficulty})\n 트리거: ${f.trigger}\n${steps}`;
51
74
  });
52
- sections.push(`### 사용자 흐름 (User Flows)\n\n각 흐름을 커리큘럼에서 다뤄야 합니다:\n\n${flowLines.join("\n\n")}`);
75
+ sections.push(en
76
+ ? `### User Flows\n\nEach flow should be covered in the curriculum:\n\n${flowLines.join("\n\n")}`
77
+ : `### 사용자 흐름 (User Flows)\n\n각 흐름을 커리큘럼에서 다뤄야 합니다:\n\n${flowLines.join("\n\n")}`);
53
78
  }
54
79
  // File Difficulty Map
55
80
  if (analysis.file_analysis.length > 0) {
56
81
  const fileLines = analysis.file_analysis
57
82
  .sort((a, b) => a.complexity - b.complexity)
58
- .map((f) => `- \`${f.path}\` — ${f.role} (복잡도: ${f.complexity}/5, ${f.difficulty})`);
59
- sections.push(`### 파일 난이도 맵\n\n쉬운 파일부터 어려운 파일 순서로 정렬했습니다. 모듈 순서를 결정할 때 참고하세요:\n\n${fileLines.join("\n")}`);
83
+ .map((f) => en
84
+ ? `- \`${f.path}\` ${f.role} (complexity: ${f.complexity}/5, ${f.difficulty})`
85
+ : `- \`${f.path}\` — ${f.role} (복잡도: ${f.complexity}/5, ${f.difficulty})`);
86
+ sections.push(en
87
+ ? `### File Difficulty Map\n\nSorted from easiest to hardest. Use this to determine module order:\n\n${fileLines.join("\n")}`
88
+ : `### 파일 난이도 맵\n\n쉬운 파일부터 어려운 파일 순서로 정렬했습니다. 모듈 순서를 결정할 때 참고하세요:\n\n${fileLines.join("\n")}`);
60
89
  }
61
90
  // Learning Priorities
62
91
  const priorities = analysis.learning_priorities;
@@ -65,82 +94,152 @@ function formatEducationalAnalysis(analysis, difficulty) {
65
94
  : difficulty === "intermediate"
66
95
  ? priorities.intermediate
67
96
  : priorities.advanced;
68
- const priorityLines = [
69
- `- **시작:** ${lp.start_with.join(", ")}`,
70
- `- **집중:** ${lp.focus_on.join(", ")}`,
71
- ];
97
+ const priorityLines = en
98
+ ? [
99
+ `- **Start with:** ${lp.start_with.join(", ")}`,
100
+ `- **Focus on:** ${lp.focus_on.join(", ")}`,
101
+ ]
102
+ : [
103
+ `- **시작:** ${lp.start_with.join(", ")}`,
104
+ `- **집중:** ${lp.focus_on.join(", ")}`,
105
+ ];
72
106
  if ("skip_for_now" in lp) {
73
- priorityLines.push(`- **나중에:** ${lp.skip_for_now.join(", ")}`);
107
+ priorityLines.push(en
108
+ ? `- **Skip for now:** ${lp.skip_for_now.join(", ")}`
109
+ : `- **나중에:** ${lp.skip_for_now.join(", ")}`);
74
110
  }
75
111
  if ("deep_dive" in lp) {
76
- priorityLines.push(`- **심화:** ${lp.deep_dive.join(", ")}`);
112
+ priorityLines.push(en
113
+ ? `- **Deep dive:** ${lp.deep_dive.join(", ")}`
114
+ : `- **심화:** ${lp.deep_dive.join(", ")}`);
77
115
  }
78
116
  if ("challenge_topics" in lp) {
79
- priorityLines.push(`- **도전:** ${lp.challenge_topics.join(", ")}`);
117
+ priorityLines.push(en
118
+ ? `- **Challenge:** ${lp.challenge_topics.join(", ")}`
119
+ : `- **도전:** ${lp.challenge_topics.join(", ")}`);
80
120
  }
81
- sections.push(`### ${difficulty} 난이도 학습 우선순위\n\n${priorityLines.join("\n")}`);
121
+ sections.push(en
122
+ ? `### ${difficulty} Level Learning Priorities\n\n${priorityLines.join("\n")}`
123
+ : `### ${difficulty} 난이도 학습 우선순위\n\n${priorityLines.join("\n")}`);
82
124
  // Repeated Patterns
83
125
  if (analysis.repeated_patterns.length > 0) {
84
- const patternLines = analysis.repeated_patterns.map((p) => `- **${p.name}**: ${p.description} (${p.occurrences.length}회 발견) — 교육 가치: ${p.teaching_value}`);
85
- sections.push(`### 반복 패턴\n\n프로젝트에서 반복적으로 사용되는 패턴입니다. 패턴들을 커리큘럼에 포함하면 학습 효과가 높아집니다:\n\n${patternLines.join("\n")}`);
126
+ const patternLines = analysis.repeated_patterns.map((p) => en
127
+ ? `- **${p.name}**: ${p.description} (found ${p.occurrences.length} times) teaching value: ${p.teaching_value}`
128
+ : `- **${p.name}**: ${p.description} (${p.occurrences.length}회 발견) — 교육 가치: ${p.teaching_value}`);
129
+ sections.push(en
130
+ ? `### Repeated Patterns\n\nThese patterns are used repeatedly in the project. Including them in the curriculum enhances learning:\n\n${patternLines.join("\n")}`
131
+ : `### 반복 패턴\n\n프로젝트에서 반복적으로 사용되는 패턴입니다. 이 패턴들을 커리큘럼에 포함하면 학습 효과가 높아집니다:\n\n${patternLines.join("\n")}`);
86
132
  }
87
133
  // Code Quality
88
134
  const cq = analysis.code_quality;
89
135
  if (cq.good_practices.length > 0 || cq.improvement_areas.length > 0) {
90
136
  const lines = [];
91
137
  if (cq.good_practices.length > 0) {
92
- lines.push("**좋은 사례 (교육 포인트):**");
138
+ lines.push(en ? "**Good Practices (Teaching Points):**" : "**좋은 사례 (교육 포인트):**");
93
139
  for (const gp of cq.good_practices) {
94
- lines.push(`- ${gp.description} → 관련 개념: ${gp.concept}`);
140
+ lines.push(en
141
+ ? `- ${gp.description} → Related concept: ${gp.concept}`
142
+ : `- ${gp.description} → 관련 개념: ${gp.concept}`);
95
143
  }
96
144
  }
97
145
  if (cq.improvement_areas.length > 0) {
98
- lines.push("\n**개선 기회 (학습 기회):**");
146
+ lines.push(en ? "\n**Improvement Opportunities (Learning Opportunities):**" : "\n**개선 기회 (학습 기회):**");
99
147
  for (const ia of cq.improvement_areas) {
100
- lines.push(`- [${ia.severity}] ${ia.description} → 교육: ${ia.teaching_opportunity}`);
148
+ lines.push(en
149
+ ? `- [${ia.severity}] ${ia.description} → Teaching: ${ia.teaching_opportunity}`
150
+ : `- [${ia.severity}] ${ia.description} → 교육: ${ia.teaching_opportunity}`);
101
151
  }
102
152
  }
103
- sections.push(`### 코드 품질 관찰\n\n${lines.join("\n")}`);
153
+ sections.push(en
154
+ ? `### Code Quality Observations\n\n${lines.join("\n")}`
155
+ : `### 코드 품질 관찰\n\n${lines.join("\n")}`);
104
156
  }
105
157
  // Tech Stack Metaphors (beginner only)
106
158
  if (difficulty === "beginner" && ov.tech_stack_metaphors.length > 0) {
107
159
  const metaphorLines = ov.tech_stack_metaphors.map((m) => `- **${m.tech_name}** → ${m.metaphor}`);
108
- sections.push(`### 기술 스택 비유 (초보자용)\n\n이 비유들을 커리큘럼에서 적극 활용하세요:\n\n${metaphorLines.join("\n")}`);
160
+ sections.push(en
161
+ ? `### Tech Stack Metaphors (Beginner)\n\nUse these metaphors actively in the curriculum:\n\n${metaphorLines.join("\n")}`
162
+ : `### 기술 스택 비유 (초보자용)\n\n이 비유들을 커리큘럼에서 적극 활용하세요:\n\n${metaphorLines.join("\n")}`);
109
163
  }
110
- return `## 프로젝트 교육 분석 (Educational Analysis)
164
+ const header = en
165
+ ? `## Project Educational Analysis
111
166
 
112
- 아래는 AI 프로젝트를 분석한 교육용 메타데이터입니다.
113
- 정보를 활용해 구체적이고 맞춤화된 커리큘럼을 만드세요.
167
+ Below is AI-analyzed educational metadata for the project.
168
+ Use this information to create a more specific and personalized curriculum.`
169
+ : `## 프로젝트 교육 분석 (Educational Analysis)
114
170
 
115
- ${sections.join("\n\n")}`;
171
+ 아래는 AI가 프로젝트를 분석한 교육용 메타데이터입니다.
172
+ 이 정보를 활용해 더 구체적이고 맞춤화된 커리큘럼을 만드세요.`;
173
+ return `${header}\n\n${sections.join("\n\n")}`;
116
174
  }
117
- function buildLevelGuidance(difficulty) {
175
+ function buildLevelGuidance(difficulty, locale) {
176
+ const en = locale === "en";
118
177
  if (difficulty === "beginner") {
119
- return `- "X가 뭔가요?"부터 시작 — 기술이 왜 존재하는지, 없으면 어떤 문제가 생기는지부터 설명
120
- - 전문 용어를 때는 반드시 바로 뒤에 쉬운 말로 풀어서 설명 (예: "미들웨어(middleware)란, 요청이 들어올 때마다 자동으로 실행되는 '검문소' 같은 코드예요")
121
- - 일상생활 비유를 적극 활용 (예: "컴포넌트는 레고 블록", "API는 식당 주문 창구", "데이터베이스는 엑셀 스프레드시트")
122
- - concept과 quiz 모듈을 많이, practical은 아주 쉬운 것만
123
- - 번에 하나의 개념만 여러 개념을 한꺼번에 설명하지 않기`;
178
+ return en
179
+ ? `**[Core Principle] Explain as if to a 5-6 year old. Assume they know nothing.**
180
+ - 3-step concept breakdown: ①Analogy (food/LEGO/school/play) ②One-sentence definition ③Code connection
181
+ - "What if this didn't exist?" before/after comparison (e.g., "No middleware? → Anyone can access secret pages 😱")
182
+ - Translate key code lines into plain English (e.g., \`const x = 5\` → "Put the number 5 in a box called x 📦")
183
+ - Give friendly nicknames to technical terms: useState→"memory box", props→"delivery package", middleware→"security checkpoint"
184
+ - concept 40%+, quiz 20%+, practical 15%↓(very easy only)
185
+ - explanation 400+ chars, 5-7 sections per module
186
+ - Use emojis actively: 🎯summary, 💡tip, ⚠️warning, 👏praise
187
+ - Tone: like reading a picture book, short sentences, lots of encouragement
188
+ - estimated_minutes: 20-35 min`
189
+ : `**[대원칙] 5~6세 아이에게 설명하듯. 아무것도 모른다고 가정.**
190
+ - 개념 3단계 쪼개기: ①비유(음식/레고/학교/놀이) → ②한 문장 정의 → ③코드 연결
191
+ - "이게 없으면?" before/after 비교 (예: "미들웨어 없으면? → 아무나 비밀 페이지 접근 😱")
192
+ - 주요 코드 라인에 "우리말 번역" (예: \`const x = 5\` → "x 상자에 숫자 5를 넣어요 📦")
193
+ - 기술 용어에 한국어 별명: useState→"기억 상자", props→"택배 상자", middleware→"보안 검문소"
194
+ - concept 40%↑, quiz 20%↑, practical 15%↓(아주 쉬운 것만)
195
+ - explanation 400자↑, 모듈당 5-7섹션
196
+ - 이모지 적극 활용: 🎯한줄정리, 💡팁, ⚠️주의, 👏칭찬
197
+ - 톤: 그림책 읽어주듯, 짧은 문장, 격려·칭찬 대폭
198
+ - estimated_minutes: 20-35분`;
124
199
  }
125
200
  if (difficulty === "intermediate") {
126
- return `- 기본 프로그래밍 지식은 안다고 가정
201
+ return en
202
+ ? `- Assume basic programming knowledge
203
+ - Focus on "how" and "why" — not just usage but how things work and design decisions
204
+ - Increase practical and project_walkthrough module ratio
205
+ - Cover common patterns, best practices, common mistakes`
206
+ : `- 기본 프로그래밍 지식은 안다고 가정
127
207
  - "어떻게"와 "왜"에 집중 — 단순 사용법이 아니라 동작 원리와 설계 이유
128
208
  - practical과 project_walkthrough 모듈 비중 높이기
129
209
  - 일반적인 패턴, 베스트 프랙티스, 흔한 실수 다루기`;
130
210
  }
131
- return `- 탄탄한 프로그래밍 지식 전제
211
+ return en
212
+ ? `- Assume solid programming knowledge
213
+ - Focus on advanced patterns, performance optimization, architecture design
214
+ - Maximize practical and project_walkthrough ratio
215
+ - Cover edge cases, internals, optimization strategies`
216
+ : `- 탄탄한 프로그래밍 지식 전제
132
217
  - 고급 패턴, 성능 최적화, 아키텍처 설계에 집중
133
218
  - practical과 project_walkthrough 비중 극대화
134
219
  - 엣지 케이스, 내부 동작 원리, 최적화 전략 다루기`;
135
220
  }
136
221
  // ─── Tool registration ──────────────────────────────────────────────
137
222
  export function registerGenerateCurriculum(server, client) {
138
- server.tool("vibeuniv_generate_curriculum", "Generate a learning curriculum for the project. Returns tech stack info and a JSON schema — you create the curriculum JSON, then submit it with vibeuniv_submit_curriculum.", generateCurriculumSchema, { readOnlyHint: true, openWorldHint: true }, async ({ project_id, difficulty }) => {
223
+ server.tool("vibeuniv_generate_curriculum", "Generate a learning curriculum for the project. IMPORTANT: Before calling this tool, you MUST ask the user which difficulty level they prefer — beginner (초급), intermediate (중급), or advanced (고급). Do NOT default to beginner without asking. Returns tech stack info and a JSON schema — you create the curriculum JSON, then submit it with vibeuniv_submit_curriculum.", generateCurriculumSchema, { readOnlyHint: true, openWorldHint: true }, async ({ project_id, difficulty }) => {
139
224
  try {
140
225
  console.error(`[vibeuniv] Generating curriculum instructions for project ${project_id}...`);
141
226
  // Fetch all curriculum context in a single API call
142
227
  const curriculumContext = await client.getCurriculumContext(project_id);
228
+ // Read teaching-critical files from local disk (avoids server roundtrip for file decryption)
229
+ let localFiles = [];
230
+ try {
231
+ localFiles = await scanTeachingCriticalFiles(process.cwd());
232
+ }
233
+ catch (err) {
234
+ console.error(`[vibeuniv] Local file scan failed (non-fatal): ${err instanceof Error ? err.message : err}`);
235
+ }
236
+ // Prefer local files; fall back to server files if local scan yields nothing
237
+ const curriculumFiles = localFiles.length > 0
238
+ ? localFiles
239
+ : (curriculumContext.files ?? []);
143
240
  const techStacks = curriculumContext.techStacks;
241
+ const locale = curriculumContext.locale ?? "ko";
242
+ const en = locale === "en";
144
243
  if (techStacks.length === 0) {
145
244
  return {
146
245
  content: [
@@ -162,18 +261,18 @@ export function registerGenerateCurriculum(server, client) {
162
261
  const coreList = coreStacks.map(formatTechStack).join("\n");
163
262
  const supportingList = supportingStacks.length > 0
164
263
  ? supportingStacks.map(formatTechStack).join("\n")
165
- : "(없음)";
166
- const levelGuidance = buildLevelGuidance(difficulty);
264
+ : en ? "(none)" : "(없음)";
265
+ const levelGuidance = buildLevelGuidance(difficulty, locale);
167
266
  // Build KB hints section
168
267
  const kbSection = kbResult && Object.keys(kbResult.techs).length > 0
169
- ? `\n${formatKBHints(kbResult.techs)}\n`
268
+ ? `\n${formatKBHints(kbResult.techs, locale)}\n`
170
269
  : "";
171
270
  // Build educational analysis section (with defensive try/catch for LLM-generated data)
172
271
  let eduSection = "";
173
272
  let hasEduAnalysis = false;
174
273
  if (educationalAnalysis) {
175
274
  try {
176
- eduSection = `\n${formatEducationalAnalysis(educationalAnalysis, difficulty)}\n`;
275
+ eduSection = `\n${formatEducationalAnalysis(educationalAnalysis, difficulty, locale)}\n`;
177
276
  hasEduAnalysis = true;
178
277
  }
179
278
  catch (err) {
@@ -182,146 +281,263 @@ export function registerGenerateCurriculum(server, client) {
182
281
  }
183
282
  // Build educational analysis instruction
184
283
  const eduInstruction = hasEduAnalysis
185
- ? `
186
- ### 10. 교육 분석 데이터 활용
187
- - **프로젝트 개요**의 핵심 기능과 유형을 커리큘럼 소개에 반영
188
- - **사용자 흐름(User Flows)** 을 project_walkthrough 모듈의 기반으로 활용
189
- - **파일 난이도 맵**을 참고해 모듈 순서와 예상 시간을 설정
190
- - **학습 우선순위**에 따라 모듈 배치를 최적화
191
- - **반복 패턴**을 별도 모듈이나 퀴즈로 다뤄 학습 효과 극대화
192
- - **코드 품질 관찰**의 좋은 사례를 교육 포인트로, 개선 기회를 챌린지로 활용
193
- - 초보자 난이도일 경우 **기술 스택 비유**를 explanation 섹션에 적극 활용`
284
+ ? en
285
+ ? `\n**Educational Analysis Usage:** Project overview→intro, User Flows→walkthrough, File difficulty→module order, Learning priorities→arrangement, Repeated patterns→quizzes, Code quality→teaching points${difficulty === "beginner" ? ", Metaphors→explanations" : ""}`
286
+ : `\n**교육 분석 활용:** 프로젝트 개요→소개, User Flows→walkthrough, 파일 난이도→모듈 순서, 학습 우선순위→배치, 반복 패턴→퀴즈, 코드 품질→교육 포인트${difficulty === "beginner" ? ", 비유→explanation" : ""}`
194
287
  : "";
195
288
  // Build KB instruction
196
289
  const kbInstruction = kbResult && Object.keys(kbResult.techs).length > 0
197
- ? `
198
- ### ${educationalAnalysis ? "11" : "10"}. Knowledge Base 활용
199
- - **핵심 포인트**를 반드시 커리큘럼에 포함 빠뜨리지 마세요
200
- - **퀴즈 주제**를 quiz_question 섹션의 주제로 활용
201
- - **선행 개념** 순서에 맞게 모듈을 배치 (선행 개념이 먼저 나와야 함)`
290
+ ? en
291
+ ? `\n**KB Usage:** Include key points, use quiz topics, follow prerequisite ordering.`
292
+ : `\n**KB 활용:** 핵심 포인트 필수 포함, 퀴즈 주제 활용, 선행 개념 순서 준수.`
202
293
  : "";
203
- const instructions = `이 프로젝트의 학습 커리큘럼을 생성해주세요. 아래 지시를 꼼꼼히 따라주세요.
294
+ // Build project source code section (local files preferred, server fallback)
295
+ const filesSection = curriculumFiles.length > 0
296
+ ? en
297
+ ? `\n## Project Source Code
298
+
299
+ Below are the student's actual project files.
300
+ You MUST directly quote this code in code_example and walkthrough sections.
301
+ Do NOT make up code.
302
+
303
+ ${curriculumFiles.map((f) => `#### ${f.file_path}\n\`\`\`\n${f.content}\n\`\`\``).join("\n\n")}\n`
304
+ : `\n## 프로젝트 소스 코드
305
+
306
+ 아래는 학생의 실제 프로젝트 파일입니다.
307
+ 커리큘럼의 code_example, walkthrough 섹션에서 반드시 이 코드를 직접 인용하세요.
308
+ 코드를 창작하지 마세요.
309
+
310
+ ${curriculumFiles.map((f) => `#### ${f.file_path}\n\`\`\`\n${f.content}\n\`\`\``).join("\n\n")}\n`
311
+ : "";
312
+ const learnMoreLabel = en ? "📚 Learn More" : "📚 더 알아보기";
313
+ const minBodyChars = difficulty === "beginner" ? "400" : "200";
314
+ const sectionsPerModule = difficulty === "beginner" ? "5-7" : "3-5";
315
+ const minSections = difficulty === "beginner" ? "5" : "3";
316
+ const paragraphs = difficulty === "beginner" ? "6-8" : "4-6";
317
+ const instructions = en
318
+ ? `Please generate a learning curriculum for this project.
319
+
320
+ ## Target: Vibe Coder (${difficulty})
321
+
322
+ Someone who built an app using AI coding tools but wants to understand **why it works**. Project-based learning, no abstract theory.
323
+
324
+ ## Tech Stack
325
+
326
+ **Core (Required):**
327
+ ${coreList}
328
+
329
+ **Supporting (Optional):**
330
+ ${supportingList}
331
+ ${filesSection}${eduSection}${kbSection}
332
+ ## Design Principles
333
+
334
+ **Module Count (Required — server will reject if below minimum):**
335
+ - Minimum 10 modules total (aim for 10-15)
336
+ - Core technologies: at least 2 modules each
337
+ - Supporting technologies: at least 1 module each
338
+
339
+ **Module Design — Focus on project features:**
340
+ Design modules around the project's actual features/files, not generic tech modules ("React Basics").
341
+
342
+ Good examples:
343
+ - "Dashboard Layout Analysis — Authentication with Server Components" (app/(dashboard)/layout.tsx)
344
+ - "API Routes and Middleware — Auth Checks and Rate Limiting" (middleware.ts, app/api/...)
345
+ - "Supabase Connection — Server vs Browser Client Differences" (lib/supabase/server.ts, client.ts)
346
+
347
+ Bad examples:
348
+ - "React Basics" (too generic)
349
+ - "TypeScript Introduction" (unrelated to project)
350
+
351
+ **Module Order:** Start from project entry points (page.tsx, layout.tsx) → core features → advanced patterns
352
+ **Each module must center around at least one project file**
353
+
354
+ **Module Types:** concept (concept+analogy), practical (code practice), quiz (code-based quiz), project_walkthrough (line-by-line file reading)
355
+ **Difficulty:**
356
+ ${levelGuidance}
357
+
358
+ **Section Design (${sectionsPerModule} per module, minimum ${minSections}):**
359
+ - explanation: Markdown ${paragraphs} paragraphs. Must cite project file paths.
360
+ End with "${learnMoreLabel}" links 2-3 (React→react.dev, Next.js→nextjs.org/docs,
361
+ TypeScript→typescriptlang.org, Supabase→supabase.com/docs, Tailwind→tailwindcss.com/docs)
362
+ - code_example: Copy actual project code + line-by-line comments.
363
+ Below the code block, explain with numbered list "What this code does:"
364
+ - quiz_question: 4-choice based on project code. quiz_explanation with correct/incorrect reasoning
365
+ - challenge: ___BLANK___ fill-in-the-blank. Both starter_code and answer_code required
366
+ - reflection: "Open the X folder in your project. Look for Y." format
367
+
368
+ **Required Placement Rules:**
369
+ - Start each module with explanation
370
+ - Maximum 2 consecutive explanations, 3rd must be quiz/reflection
371
+ - At least 1 code_example per module required
372
+ - At least 1 quiz_question per module required
373
+
374
+ **Tone (Critical — key to learning content quality):**
375
+ - Use casual, friendly "you" language
376
+ - Address the student as "you" or "we"
377
+ - Short sentences, one idea per sentence
378
+ - Keep technical terms in English + follow with a plain explanation in parentheses
379
+ - Start with questions: "Have you ever wondered about this code?", "Why does it work this way?"
380
+ - Encourage: "If you've followed along this far, you already understand half of it!", "It can be confusing at first — don't worry"
381
+ - Use analogies: everyday analogies for new concepts (API→restaurant order window, component→LEGO blocks)
382
+ - Transition phrases: "Alright, now let's...", "Wait a moment!", "Let's check the actual code, shall we?"
383
+ - Forbidden: dry academic tone, filler phrases like "Great question!", emotionless listing
384
+ - Do NOT make up code — only quote actual project code
385
+
386
+ **walkthrough:** Explain a file from import→logic→export order + connections to other files.
387
+ ${eduInstruction}${kbInstruction}
388
+
389
+ ## JSON Schema
204
390
 
205
- ## 학생 프로필
391
+ Follow the structure below exactly. All string values in English. Output ONLY JSON (no code fences/explanations).
206
392
 
207
- 이 커리큘럼은 **"바이브 코더(Vibe Coder)"** 를 위한 것입니다.
208
- 바이브 코더란, AI 코딩 도구(Claude Code, Cursor, Bolt 등) 동작하는 앱을 이미 만든 사람입니다.
209
- 코드가 돌아가게는 했지만, **왜 이렇게 작동하는지** 이해하고 싶어하는 사람이에요.
393
+ {
394
+ "title": "string (required) Curriculum title",
395
+ "description": "string (required) Curriculum description",
396
+ "difficulty": "${difficulty}",
397
+ "estimated_hours": number (optional),
398
+ "modules": [
399
+ {
400
+ "title": "string (required) — Module title",
401
+ "description": "string (required) — Module description",
402
+ "module_type": "concept | practical | quiz | project_walkthrough",
403
+ "estimated_minutes": number (15-45),
404
+ "tech_name": "string (required) — Must exactly match a name from the tech stack list above",
405
+ "content": {
406
+ "sections": [
407
+ {
408
+ "type": "explanation | code_example | quiz_question | challenge | reflection",
409
+ "title": "string (required) — Section title",
410
+ "body": "string (required) — Markdown body. explanation minimum ${minBodyChars} chars",
210
411
 
211
- - **난이도:** ${difficulty}
212
- - **학습 스타일:** 이미 완성된 프로젝트 기반으로 학습 (추상적 이론 X)
213
- - **목표:** 자기 프로젝트의 기술 스택을 이해해서, 혼자서도 디버깅하고 기능을 추가할 수 있게 되는 것
412
+ "code": "string (required for code_example) — Actual project code + line-by-line comments",
214
413
 
215
- ## 감지된 기술 스택
414
+ "quiz_options": ["string", "string", "string", "string"] (required for quiz_question, exactly 4),
415
+ "quiz_answer": number (required for quiz_question, 0-3),
416
+ "quiz_explanation": "string (required for quiz_question) — Correct/incorrect reasoning",
216
417
 
217
- ### 핵심 기술 (Core) — 반드시 다뤄야 함
418
+ "challenge_starter_code": "string (required for challenge) — Contains ___BLANK___",
419
+ "challenge_answer_code": "string (required for challenge) — Completed code"
420
+ }
421
+ ]
422
+ }
423
+ }
424
+ ]
425
+ }
426
+
427
+ **Required Rules:**
428
+ - At least 1 code_example + 1 quiz_question per module
429
+ - Minimum 10 modules, minimum ${minSections} sections per module
430
+ - explanation body must be at least ${minBodyChars} characters${difficulty === "beginner" ? `
431
+
432
+ **[Beginner-only Additional Rules — Must Follow]:**
433
+ - Every concept must have "What if this didn't exist?" before/after comparison
434
+ - Every line of code in code_example must have a plain-English translation
435
+ - Challenges must have only 1-2 blanks with very specific hints
436
+ - All technical terms must have friendly nicknames` : ""}
437
+
438
+ After generating: vibeuniv_submit_curriculum({ project_id: "${project_id}", curriculum: <JSON> })`
439
+ : `이 프로젝트의 학습 커리큘럼을 생성해주세요.
440
+
441
+ ## 대상: 바이브 코더 (${difficulty})
442
+
443
+ AI 코딩 도구로 앱을 만들었지만 **왜 작동하는지** 이해하고 싶은 사람. 프로젝트 기반 학습, 추상적 이론 X.
444
+
445
+ ## 기술 스택
446
+
447
+ **Core (필수):**
218
448
  ${coreList}
219
449
 
220
- ### 보조 기술 (Supporting) — 선택적으로 다루기
450
+ **Supporting (선택):**
221
451
  ${supportingList}
222
- ${eduSection}${kbSection}
223
- ## 사전 준비: 프로젝트 파일 분석
452
+ ${filesSection}${eduSection}${kbSection}
453
+ ## 설계 원칙
224
454
 
225
- **중요:** 커리큘럼 생성 전에 반드시 프로젝트의 소스 코드를 읽어주세요.
226
- 특히 다음 파일들을 확인하세요:
227
- - 진입점 파일 (예: index.ts, app.tsx, main.py, layout.tsx 등)
228
- - 설정 파일 (예: package.json, tsconfig.json, next.config.ts 등)
229
- - 핵심 비즈니스 로직 파일 (가장 많이 수정된 파일들)
230
- - API 라우트, 데이터베이스 접근 코드
455
+ **모듈 수량 (필수 미달 서버에서 거부됨):**
456
+ - 전체 최소 10개 모듈 (10-15개 권장)
457
+ - Core 기술: 최소 2개 모듈
458
+ - Supporting 기술: 최소 1개 모듈
231
459
 
232
- 파일들의 실제 코드를 커리큘럼에 직접 인용해야 합니다.
460
+ **모듈 구성 프로젝트 기능 중심으로 설계:**
461
+ 기술별 일반 모듈("React 기초")이 아닌, 프로젝트의 실제 기능/파일을 중심으로 설계하세요.
233
462
 
234
- ## 커리큘럼 설계 원칙
463
+ 좋은 예:
464
+ - "대시보드 레이아웃 분석 — Server Component로 인증 처리하기" (app/(dashboard)/layout.tsx)
465
+ - "API 라우트와 미들웨어 — 인증 체크와 속도 제한" (middleware.ts, app/api/...)
466
+ - "Supabase 연결 — 서버 vs 브라우저 클라이언트 차이" (lib/supabase/server.ts, client.ts)
235
467
 
236
- ### 1. 모듈 순서 — 의존성 기반
237
- - **가장 중요한 핵심 기술부터** 시작 (core framework → language → supporting tools)
238
- - **선행 지식 → 후행 지식** 순서 (예: JavaScript 기초 → React → Next.js)
239
- - 각 기술당 3-7개 모듈 (중요도와 복잡도에 따라 조절)
468
+ 나쁜 예:
469
+ - "React 기초" (너무 일반적)
470
+ - "TypeScript 입문" (프로젝트와 무관)
240
471
 
241
- ### 2. 모듈 유형 믹스
242
- - \`concept\` 핵심 개념을 비유와 예시로 설명
243
- - \`practical\` — 학생의 실제 코드를 수정하는 실습
244
- - \`quiz\` — 학생의 실제 코드 기반 퀴즈로 이해도 확인
245
- - \`project_walkthrough\` — 학생의 실제 파일을 위에서 아래로 한 줄씩 읽어보기
472
+ **모듈 순서:** 프로젝트의 진입점(page.tsx, layout.tsx)부터 핵심 기능 → 고급 패턴
473
+ **각 모듈은 반드시 1개 이상의 프로젝트 파일을 중심으로 설명**
246
474
 
247
- ### 3. 난이도별 접근법
475
+ **모듈 유형:** concept(개념+비유), practical(코드 실습), quiz(코드 기반 퀴즈), project_walkthrough(파일 라인별 읽기)
476
+ **난이도:**
248
477
  ${levelGuidance}
249
478
 
250
- ### 4. 모듈의 섹션 구성 (5-8개)
251
- 섹션 유형과 순서 배치 규칙:
252
- - \`explanation\` 마크다운으로 개념 설명. **반드시 학생의 실제 파일 경로와 코드를 인용**
253
- - \`code_example\` — 학생의 프로젝트에서 **실제 코드를 복사**해서 한국어 주석으로 설명
254
- - \`quiz_question\` 학생의 실제 코드 기반 4지선다 퀴즈
255
- - \`challenge\` 학생의 실제 프로젝트에서 수정해볼 있는 구체적 과제
256
- - \`reflection\` "생각해보기" (1-3문장, 학생이 스스로 고민해보도록)
257
-
258
- **배치 규칙:** explanation이나 code_example 1-2개 후에 반드시 quiz_question 또는 reflection을 배치.
259
- explanation만 연속 3개 이상 나오면 안 됩니다.
260
-
261
- ### 5. 콘텐츠 작성 톤
262
- - **모든 콘텐츠는 한국어** (기술 용어는 영어 유지하되 괄호 안에 한국어 설명 병기)
263
- - **짧은 문장**, **짧은 문단** — 한 단락에 3문장 이하
264
- - **글머리 기호** 적극 사용 — 장문 대신 핵심만
265
- - **질문으로 시작** — "왜 이렇게 할까요?", "이 코드 없이도 동작할까요?"
266
- - **마이크로러닝 느낌** 섹션이 짧은 카드처럼, 강의처럼 느껴지면 안 됨
267
-
268
- ### 6. 코드 인용 규칙
269
- - explanation: "여러분의 \`middleware.ts\` 파일 5번째 줄을 보면..." 형태로 구체적 파일 + 라인 참조
270
- - code_example: 학생 프로젝트의 **실제 코드를 그대로** 복사하고, 줄에 한국어 주석 추가
271
- 예시:
272
- \`\`\`
273
- // app/layout.tsx에서 가져온 코드
274
- export default function RootLayout({ children }) { // 모든 페이지를 감싸는 최상위 레이아웃
275
- return <html lang="ko"> // 한국어 페이지라는 것을 브라우저에 알려줌
276
- \`\`\`
277
- - quiz_question: "여러분의 \`app/page.tsx\`에서 \`export default\`를 쓰는 이유는?"처럼 학생 코드 기반
278
- - challenge: 수정할 파일, 추가할 내용, 기대 결과를 구체적으로 명시
279
- - **코드를 창작하지 마세요** — 반드시 프로젝트에 실재하는 코드를 인용
280
-
281
- ### 7. 퀴즈 규칙
282
- - 선택지 정확히 4개, 정답은 0-indexed
283
- - \`quiz_explanation\`에 정답 이유 + 오답 이유를 2-4문장으로 설명
284
- - 학생의 실제 코드와 연결된 퀴즈 출제 (추상적 이론 퀴즈 X)
285
-
286
- ### 8. 챌린지 규칙
287
- - 어떤 파일을 수정할지, 무엇을 추가/변경할지, 결과가 어떻게 바뀌는지 구체적으로
288
- - \`challenge_starter_code\`: TODO 주석이 있는 뼈대 코드
289
- - \`challenge_answer_code\`: 완성된 정답 코드
290
-
291
- ### 9. project_walkthrough 규칙
292
- - 학생의 실제 파일 하나를 위에서 아래로 읽기
293
- - import 구문 → 핵심 로직 → export 순서로 설명
294
- - 이 파일이 프로젝트의 다른 파일들과 어떻게 연결되는지 설명
479
+ **섹션 구성 (모듈당 ${sectionsPerModule}개, 최소 ${minSections}개):**
480
+ - explanation: 마크다운 ${paragraphs} 문단. 반드시 프로젝트 파일 경로 인용.
481
+ 끝에 "${learnMoreLabel}" 링크 2-3개 (React→react.dev, Next.js→nextjs.org/docs,
482
+ TypeScript→typescriptlang.org, Supabase→supabase.com/docs, Tailwind→tailwindcss.com/docs)
483
+ - code_example: 프로젝트 실제 코드 복사 + 라인별 한국어 주석.
484
+ 코드 블록 아래에 "이 코드가 하는 일:" 번호 목록으로 설명
485
+ - quiz_question: 프로젝트 코드 기반 4지선다. quiz_explanation에 정답/오답 이유
486
+ - challenge: ___BLANK___ 빈칸 채우기. starter_code + answer_code 모두 필수
487
+ - reflection: "여러분의 프로젝트에서 X 폴더를 열어보세요. Y를 찾아보세요." 형태
488
+
489
+ **필수 배치 규칙:**
490
+ - 모듈 시작은 explanation으로
491
+ - explanation 연속 2개까지만, 3번째는 반드시 quiz/reflection
492
+ - 모듈당 code_example 최소 1개 필수
493
+ - 모듈당 quiz_question 최소 1개 필수
494
+
495
+ **톤 (매우 중요학습 콘텐츠 품질의 핵심):**
496
+ - 해요체 사용 (~이에요, ~거든요, ~잖아요, ~해볼까요?)
497
+ - 학생을 "여러분" 또는 "우리"로 지칭
498
+ - 짧은 문장 위주, 문장에 하나의 아이디어
499
+ - 기술 용어는 영어 유지 + 바로 뒤에 괄호로 쉬운 설명
500
+ - 질문으로 시작: "혹시 이 코드 보면서 궁금하셨죠?", "왜 이렇게 할까요?"
501
+ - 격려 필수: "여기까지 따라오셨으면 벌써 절반은 이해하신 거예요!", "처음엔 헷갈릴 수 있는데 걱정 마세요"
502
+ - 비유 필수: 새 개념마다 일상생활 비유 (API→식당 주문 창구, 컴포넌트→레고 블록)
503
+ - 전환 어구: "자, 그러면 이제...", "여기서 잠깐!", "실제 코드에서 확인해볼까요?"
504
+ - 금지: 교과서체(~이다, ~하라), 감정 없는 나열, 영어 직역투
505
+ - 코드 창작 금지 — 프로젝트 실제 코드만 인용
506
+
507
+ **walkthrough:** 파일 하나를 import→로직→export 순서로 설명 + 다른 파일과의 연결.
295
508
  ${eduInstruction}${kbInstruction}
296
509
 
297
510
  ## JSON 스키마
298
511
 
299
- 아래 스키마에 맞는 JSON 생성하세요.
512
+ 아래 구조를 정확히 따르세요. 모든 string 값은 한국어. JSON 출력 (코드 펜스/설명 없이).
300
513
 
301
514
  {
302
- "title": "string (학습 경로 제목, 한국어)",
303
- "description": "string (2-3문장, 커리큘럼을 마치면 무엇을 할 수 있는지)",
515
+ "title": "string (필수) 커리큘럼 제목",
516
+ "description": "string (필수) 커리큘럼 설명",
304
517
  "difficulty": "${difficulty}",
305
- "estimated_hours": number,
518
+ "estimated_hours": number (선택),
306
519
  "modules": [
307
520
  {
308
- "title": "string (모듈 제목, 한국어)",
309
- "description": "string (1-2문장 모듈 설명, 한국어)",
521
+ "title": "string (필수) — 모듈 제목",
522
+ "description": "string (필수) 모듈 설명",
310
523
  "module_type": "concept | practical | quiz | project_walkthrough",
311
524
  "estimated_minutes": number (15-45),
312
- "tech_name": "string (위 기술 스택 목록의 정확한 이름)",
525
+ "tech_name": "string (필수) — 위 기술 스택 목록의 이름과 정확히 일치",
313
526
  "content": {
314
527
  "sections": [
315
528
  {
316
529
  "type": "explanation | code_example | quiz_question | challenge | reflection",
317
- "title": "string (섹션 제목, 한국어)",
318
- "body": "string (마크다운 콘텐츠, 한국어)",
319
- "code": "string (code_example일 때만, 학생의 실제 코드 + 한국어 주석)",
320
- "quiz_options": ["string", "string", "string", "string"] (quiz_question일 때만),
321
- "quiz_answer": number (0-3, quiz_question일 때만),
322
- "quiz_explanation": "string (정답/오답 이유, quiz_question일 때만)",
323
- "challenge_starter_code": "string (TODO 포함 뼈대, challenge일 때만)",
324
- "challenge_answer_code": "string (완성 정답, challenge일 때만)"
530
+ "title": "string (필수) — 섹션 제목",
531
+ "body": "string (필수) — 마크다운 본문. explanation은 최소 ${minBodyChars}자",
532
+
533
+ "code": "string (code_example일 필수) 프로젝트 실제 코드 + 라인별 주석",
534
+
535
+ "quiz_options": ["string", "string", "string", "string"] (quiz_question일 때 필수, 정확히 4개),
536
+ "quiz_answer": number (quiz_question일 필수, 0-3),
537
+ "quiz_explanation": "string (quiz_question일 필수) — 정답/오답 이유",
538
+
539
+ "challenge_starter_code": "string (challenge일 때 필수) — ___BLANK___ 포함",
540
+ "challenge_answer_code": "string (challenge일 때 필수) — 완성 코드"
325
541
  }
326
542
  ]
327
543
  }
@@ -329,14 +545,18 @@ ${eduInstruction}${kbInstruction}
329
545
  ]
330
546
  }
331
547
 
332
- ## 중요
548
+ **필수 규칙:**
549
+ - 모듈당 code_example 최소 1개 + quiz_question 최소 1개
550
+ - 최소 10개 모듈, 모듈당 최소 ${minSections}개 섹션
551
+ - explanation body는 ${minBodyChars}자 이상${difficulty === "beginner" ? `
333
552
 
334
- - JSON만 출력하세요. 코드 펜스(\`\`\`), 설명문, 앞뒤 텍스트 없이 순수 JSON만.
335
- - \`tech_name\`은 기술 스택 목록의 이름과 **정확히 일치**해야 합니다.
336
- - 모듈당 **5-8개 섹션** 필수.
553
+ **[초급 전용 추가 규칙 반드시 준수]:**
554
+ - 모든 개념에 "이게 없으면?" before/after 비교 필수
555
+ - code_example 모든 코드 라인에 "우리말 번역" 필수
556
+ - challenge 빈칸 1-2개만, 힌트 매우 구체적
557
+ - 기술 용어에 한국어 별명 필수` : ""}
337
558
 
338
- 생성 완료 아래 도구로 제출:
339
- vibeuniv_submit_curriculum({ project_id: "${project_id}", curriculum: <생성된 JSON> })`;
559
+ 생성 후: vibeuniv_submit_curriculum({ project_id: "${project_id}", curriculum: <JSON> })`;
340
560
  return {
341
561
  content: [
342
562
  {