clouvel 0.3.1__py3-none-any.whl
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.
- clouvel/__init__.py +3 -0
- clouvel/server.py +1208 -0
- clouvel-0.3.1.dist-info/METADATA +80 -0
- clouvel-0.3.1.dist-info/RECORD +6 -0
- clouvel-0.3.1.dist-info/WHEEL +4 -0
- clouvel-0.3.1.dist-info/entry_points.txt +2 -0
clouvel/__init__.py
ADDED
clouvel/server.py
ADDED
|
@@ -0,0 +1,1208 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from mcp.server import Server
|
|
5
|
+
from mcp.server.stdio import stdio_server
|
|
6
|
+
from mcp.types import Tool, TextContent
|
|
7
|
+
|
|
8
|
+
server = Server("clouvel")
|
|
9
|
+
|
|
10
|
+
# 필수 문서 정의
|
|
11
|
+
REQUIRED_DOCS = [
|
|
12
|
+
{"type": "prd", "name": "PRD", "patterns": [r"prd", r"product.?requirement"], "priority": "critical"},
|
|
13
|
+
{"type": "architecture", "name": "아키텍처", "patterns": [r"architect", r"module"], "priority": "critical"},
|
|
14
|
+
{"type": "api_spec", "name": "API 스펙", "patterns": [r"api", r"swagger", r"openapi"], "priority": "critical"},
|
|
15
|
+
{"type": "db_schema", "name": "DB 스키마", "patterns": [r"schema", r"database", r"db"], "priority": "critical"},
|
|
16
|
+
{"type": "verification", "name": "검증 계획", "patterns": [r"verif", r"test.?plan"], "priority": "critical"},
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@server.list_tools()
|
|
21
|
+
async def list_tools() -> list[Tool]:
|
|
22
|
+
return [
|
|
23
|
+
Tool(
|
|
24
|
+
name="can_code",
|
|
25
|
+
description="""⚠️ 중요: 코드를 작성하기 전에 반드시 이 도구를 먼저 호출하세요.
|
|
26
|
+
|
|
27
|
+
이 도구는 프로젝트의 문서 상태를 확인하고 코딩 가능 여부를 판단합니다.
|
|
28
|
+
- 필수 문서가 없으면: 코딩 금지, PRD 작성 안내
|
|
29
|
+
- 필수 문서가 있으면: 코딩 허용
|
|
30
|
+
|
|
31
|
+
사용자가 코드 작성, 기능 구현, 버그 수정 등을 요청하면 무조건 이 도구를 먼저 호출하세요.""",
|
|
32
|
+
inputSchema={
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {
|
|
35
|
+
"path": {"type": "string", "description": "프로젝트 docs 폴더 경로"}
|
|
36
|
+
},
|
|
37
|
+
"required": ["path"]
|
|
38
|
+
}
|
|
39
|
+
),
|
|
40
|
+
Tool(
|
|
41
|
+
name="scan_docs",
|
|
42
|
+
description="프로젝트 docs 폴더 스캔. 파일 목록 반환.",
|
|
43
|
+
inputSchema={
|
|
44
|
+
"type": "object",
|
|
45
|
+
"properties": {
|
|
46
|
+
"path": {"type": "string", "description": "docs 폴더 경로"}
|
|
47
|
+
},
|
|
48
|
+
"required": ["path"]
|
|
49
|
+
}
|
|
50
|
+
),
|
|
51
|
+
Tool(
|
|
52
|
+
name="analyze_docs",
|
|
53
|
+
description="docs 폴더 분석. 필수 문서 있는지 체크하고 빠진 거 알려줌.",
|
|
54
|
+
inputSchema={
|
|
55
|
+
"type": "object",
|
|
56
|
+
"properties": {
|
|
57
|
+
"path": {"type": "string", "description": "docs 폴더 경로"}
|
|
58
|
+
},
|
|
59
|
+
"required": ["path"]
|
|
60
|
+
}
|
|
61
|
+
),
|
|
62
|
+
Tool(
|
|
63
|
+
name="get_prd_template",
|
|
64
|
+
description="PRD 템플릿 생성. 빈 PRD 파일을 만들어줌.",
|
|
65
|
+
inputSchema={
|
|
66
|
+
"type": "object",
|
|
67
|
+
"properties": {
|
|
68
|
+
"project_name": {"type": "string", "description": "프로젝트 이름"},
|
|
69
|
+
"output_path": {"type": "string", "description": "PRD 파일 저장 경로"}
|
|
70
|
+
},
|
|
71
|
+
"required": ["project_name", "output_path"]
|
|
72
|
+
}
|
|
73
|
+
),
|
|
74
|
+
Tool(
|
|
75
|
+
name="write_prd_section",
|
|
76
|
+
description="PRD 섹션별 작성 도우미. 단계별로 PRD를 작성할 수 있게 도와줌.",
|
|
77
|
+
inputSchema={
|
|
78
|
+
"type": "object",
|
|
79
|
+
"properties": {
|
|
80
|
+
"section": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"description": "작성할 섹션",
|
|
83
|
+
"enum": ["summary", "principles", "input_spec", "output_spec", "errors", "state_machine", "api_endpoints", "db_schema"]
|
|
84
|
+
},
|
|
85
|
+
"content": {"type": "string", "description": "섹션 내용"}
|
|
86
|
+
},
|
|
87
|
+
"required": ["section"]
|
|
88
|
+
}
|
|
89
|
+
),
|
|
90
|
+
Tool(
|
|
91
|
+
name="init_docs",
|
|
92
|
+
description="docs 폴더 초기화. 폴더 없으면 생성하고 필수 문서 템플릿 파일들 생성.",
|
|
93
|
+
inputSchema={
|
|
94
|
+
"type": "object",
|
|
95
|
+
"properties": {
|
|
96
|
+
"path": {"type": "string", "description": "프로젝트 루트 경로"},
|
|
97
|
+
"project_name": {"type": "string", "description": "프로젝트 이름"}
|
|
98
|
+
},
|
|
99
|
+
"required": ["path", "project_name"]
|
|
100
|
+
}
|
|
101
|
+
),
|
|
102
|
+
Tool(
|
|
103
|
+
name="get_prd_guide",
|
|
104
|
+
description="PRD 작성 가이드. step-by-step으로 뭘 써야 하는지.",
|
|
105
|
+
inputSchema={"type": "object", "properties": {}}
|
|
106
|
+
),
|
|
107
|
+
Tool(
|
|
108
|
+
name="get_verify_checklist",
|
|
109
|
+
description="PRD 검증 체크리스트. 빠뜨리기 쉬운 것들.",
|
|
110
|
+
inputSchema={"type": "object", "properties": {}}
|
|
111
|
+
),
|
|
112
|
+
Tool(
|
|
113
|
+
name="get_setup_guide",
|
|
114
|
+
description="Clouvel 설치/설정 가이드. Claude Desktop, Claude Code, VS Code 설정법.",
|
|
115
|
+
inputSchema={
|
|
116
|
+
"type": "object",
|
|
117
|
+
"properties": {
|
|
118
|
+
"platform": {
|
|
119
|
+
"type": "string",
|
|
120
|
+
"description": "플랫폼",
|
|
121
|
+
"enum": ["desktop", "code", "vscode", "cursor", "all"]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
),
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@server.call_tool()
|
|
130
|
+
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
131
|
+
if name == "can_code":
|
|
132
|
+
return await _can_code(arguments.get("path", ""))
|
|
133
|
+
elif name == "scan_docs":
|
|
134
|
+
return await _scan_docs(arguments.get("path", ""))
|
|
135
|
+
elif name == "analyze_docs":
|
|
136
|
+
return await _analyze_docs(arguments.get("path", ""))
|
|
137
|
+
elif name == "get_prd_template":
|
|
138
|
+
return await _get_prd_template(
|
|
139
|
+
arguments.get("project_name", ""),
|
|
140
|
+
arguments.get("output_path", "")
|
|
141
|
+
)
|
|
142
|
+
elif name == "write_prd_section":
|
|
143
|
+
return await _write_prd_section(
|
|
144
|
+
arguments.get("section", ""),
|
|
145
|
+
arguments.get("content", "")
|
|
146
|
+
)
|
|
147
|
+
elif name == "init_docs":
|
|
148
|
+
return await _init_docs(
|
|
149
|
+
arguments.get("path", ""),
|
|
150
|
+
arguments.get("project_name", "")
|
|
151
|
+
)
|
|
152
|
+
elif name == "get_prd_guide":
|
|
153
|
+
return await _get_prd_guide()
|
|
154
|
+
elif name == "get_verify_checklist":
|
|
155
|
+
return await _get_verify_checklist()
|
|
156
|
+
elif name == "get_setup_guide":
|
|
157
|
+
return await _get_setup_guide(arguments.get("platform", "all"))
|
|
158
|
+
else:
|
|
159
|
+
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
async def _can_code(path: str) -> list[TextContent]:
|
|
163
|
+
"""코딩 가능 여부 확인 - 핵심 기능"""
|
|
164
|
+
docs_path = Path(path)
|
|
165
|
+
|
|
166
|
+
# docs 폴더 없음
|
|
167
|
+
if not docs_path.exists():
|
|
168
|
+
return [TextContent(type="text", text=f"""
|
|
169
|
+
# ⛔ 코딩 금지
|
|
170
|
+
|
|
171
|
+
## 이유
|
|
172
|
+
docs 폴더가 없습니다: `{path}`
|
|
173
|
+
|
|
174
|
+
## 지금 해야 할 것
|
|
175
|
+
1. `docs` 폴더를 생성하세요
|
|
176
|
+
2. PRD(제품 요구사항 문서)를 먼저 작성하세요
|
|
177
|
+
3. `get_prd_template` 도구로 템플릿을 생성할 수 있습니다
|
|
178
|
+
|
|
179
|
+
## 왜?
|
|
180
|
+
PRD 없이 코딩하면:
|
|
181
|
+
- 요구사항 불명확 → 재작업
|
|
182
|
+
- 예외 케이스 누락 → 버그
|
|
183
|
+
- 팀원 간 인식 차이 → 충돌
|
|
184
|
+
|
|
185
|
+
**문서 먼저, 코딩은 나중에.**
|
|
186
|
+
|
|
187
|
+
사용자에게 PRD 작성을 도와주겠다고 말하세요.
|
|
188
|
+
""")]
|
|
189
|
+
|
|
190
|
+
files = [f.name.lower() for f in docs_path.iterdir() if f.is_file()]
|
|
191
|
+
|
|
192
|
+
detected = []
|
|
193
|
+
missing = []
|
|
194
|
+
|
|
195
|
+
for req in REQUIRED_DOCS:
|
|
196
|
+
found = False
|
|
197
|
+
for filename in files:
|
|
198
|
+
for pattern in req["patterns"]:
|
|
199
|
+
if re.search(pattern, filename, re.IGNORECASE):
|
|
200
|
+
detected.append(req["name"])
|
|
201
|
+
found = True
|
|
202
|
+
break
|
|
203
|
+
if found:
|
|
204
|
+
break
|
|
205
|
+
if not found:
|
|
206
|
+
missing.append(req["name"])
|
|
207
|
+
|
|
208
|
+
# 필수 문서 부족
|
|
209
|
+
if missing:
|
|
210
|
+
missing_list = "\n".join(f"- {m}" for m in missing)
|
|
211
|
+
detected_list = "\n".join(f"- {d}" for d in detected) if detected else "없음"
|
|
212
|
+
|
|
213
|
+
return [TextContent(type="text", text=f"""
|
|
214
|
+
# ⛔ 코딩 금지
|
|
215
|
+
|
|
216
|
+
## 현재 상태
|
|
217
|
+
✅ 있음:
|
|
218
|
+
{detected_list}
|
|
219
|
+
|
|
220
|
+
❌ 없음 (필수):
|
|
221
|
+
{missing_list}
|
|
222
|
+
|
|
223
|
+
## 지금 해야 할 것
|
|
224
|
+
코드를 작성하지 마세요. 대신:
|
|
225
|
+
|
|
226
|
+
1. 누락된 문서를 먼저 작성하세요
|
|
227
|
+
2. 특히 **PRD**가 가장 중요합니다
|
|
228
|
+
3. `get_prd_guide` 도구로 작성법을 확인하세요
|
|
229
|
+
4. `get_prd_template` 도구로 템플릿을 생성하세요
|
|
230
|
+
|
|
231
|
+
## 사용자에게 전달할 메시지
|
|
232
|
+
"코드를 작성하기 전에 먼저 문서를 준비해야 합니다.
|
|
233
|
+
{len(missing)}개의 필수 문서가 없습니다: {', '.join(missing)}
|
|
234
|
+
제가 PRD 작성을 도와드릴까요?"
|
|
235
|
+
|
|
236
|
+
**절대 코드를 작성하지 마세요. 문서 작성을 도와주세요.**
|
|
237
|
+
""")]
|
|
238
|
+
|
|
239
|
+
# 모든 필수 문서 있음 → 코딩 허용
|
|
240
|
+
return [TextContent(type="text", text=f"""
|
|
241
|
+
# ✅ 코딩 가능
|
|
242
|
+
|
|
243
|
+
## 문서 상태
|
|
244
|
+
모든 필수 문서가 준비되어 있습니다:
|
|
245
|
+
{chr(10).join(f'- {d}' for d in detected)}
|
|
246
|
+
|
|
247
|
+
## 코딩 시작 전 확인사항
|
|
248
|
+
1. PRD에 명시된 요구사항을 따르세요
|
|
249
|
+
2. API 스펙에 맞게 구현하세요
|
|
250
|
+
3. DB 스키마를 참고하세요
|
|
251
|
+
4. 검증 계획에 따라 테스트하세요
|
|
252
|
+
|
|
253
|
+
이제 사용자의 요청에 따라 코드를 작성해도 됩니다.
|
|
254
|
+
""")]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
async def _scan_docs(path: str) -> list[TextContent]:
|
|
258
|
+
docs_path = Path(path)
|
|
259
|
+
|
|
260
|
+
if not docs_path.exists():
|
|
261
|
+
return [TextContent(type="text", text=f"경로 없음: {path}")]
|
|
262
|
+
|
|
263
|
+
if not docs_path.is_dir():
|
|
264
|
+
return [TextContent(type="text", text=f"디렉토리 아님: {path}")]
|
|
265
|
+
|
|
266
|
+
files = []
|
|
267
|
+
for f in sorted(docs_path.iterdir()):
|
|
268
|
+
if f.is_file():
|
|
269
|
+
stat = f.stat()
|
|
270
|
+
files.append(f"{f.name} ({stat.st_size:,} bytes)")
|
|
271
|
+
|
|
272
|
+
result = f"📁 {path}\n총 {len(files)}개 파일\n\n"
|
|
273
|
+
result += "\n".join(files)
|
|
274
|
+
|
|
275
|
+
return [TextContent(type="text", text=result)]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
async def _analyze_docs(path: str) -> list[TextContent]:
|
|
279
|
+
docs_path = Path(path)
|
|
280
|
+
|
|
281
|
+
if not docs_path.exists():
|
|
282
|
+
return [TextContent(type="text", text=f"경로 없음: {path}")]
|
|
283
|
+
|
|
284
|
+
files = [f.name.lower() for f in docs_path.iterdir() if f.is_file()]
|
|
285
|
+
|
|
286
|
+
detected = []
|
|
287
|
+
missing = []
|
|
288
|
+
|
|
289
|
+
for req in REQUIRED_DOCS:
|
|
290
|
+
found = False
|
|
291
|
+
for filename in files:
|
|
292
|
+
for pattern in req["patterns"]:
|
|
293
|
+
if re.search(pattern, filename, re.IGNORECASE):
|
|
294
|
+
detected.append(req["name"])
|
|
295
|
+
found = True
|
|
296
|
+
break
|
|
297
|
+
if found:
|
|
298
|
+
break
|
|
299
|
+
if not found:
|
|
300
|
+
missing.append(req["name"])
|
|
301
|
+
|
|
302
|
+
critical_total = len([r for r in REQUIRED_DOCS if r["priority"] == "critical"])
|
|
303
|
+
critical_found = len([r for r in REQUIRED_DOCS if r["priority"] == "critical" and r["name"] in detected])
|
|
304
|
+
coverage = critical_found / critical_total if critical_total > 0 else 1.0
|
|
305
|
+
|
|
306
|
+
result = f"## 분석 결과: {path}\n\n"
|
|
307
|
+
result += f"커버리지: {coverage:.0%}\n\n"
|
|
308
|
+
|
|
309
|
+
if detected:
|
|
310
|
+
result += "### 있음\n" + "\n".join(f"- {d}" for d in detected) + "\n\n"
|
|
311
|
+
|
|
312
|
+
if missing:
|
|
313
|
+
result += "### 없음 (작성 필요)\n" + "\n".join(f"- {m}" for m in missing) + "\n\n"
|
|
314
|
+
|
|
315
|
+
if not missing:
|
|
316
|
+
result += "✅ 필수 문서 다 있음. 바이브코딩 시작해도 됨.\n"
|
|
317
|
+
else:
|
|
318
|
+
result += f"⛔ {len(missing)}개 문서 먼저 작성하고 코딩 시작할 것.\n"
|
|
319
|
+
|
|
320
|
+
return [TextContent(type="text", text=result)]
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
async def _get_prd_template(project_name: str, output_path: str) -> list[TextContent]:
|
|
324
|
+
"""PRD 템플릿 생성"""
|
|
325
|
+
template = f"""# {project_name} PRD
|
|
326
|
+
|
|
327
|
+
> 이 문서가 법. 여기 없으면 안 만듦.
|
|
328
|
+
> 작성일: {datetime.now().strftime('%Y-%m-%d')}
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## 1. 한 줄 요약
|
|
333
|
+
<!-- 프로젝트가 뭔지 한 문장으로. 못 쓰면 정리 안 된 거임. -->
|
|
334
|
+
|
|
335
|
+
[여기에 작성]
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## 2. 핵심 원칙
|
|
340
|
+
|
|
341
|
+
> 절대 안 변하는 것들. 이거 기준으로 기능 판단.
|
|
342
|
+
|
|
343
|
+
1. [원칙 1]
|
|
344
|
+
2. [원칙 2]
|
|
345
|
+
3. [원칙 3]
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## 3. 용어 정의
|
|
350
|
+
|
|
351
|
+
| 용어 | 설명 |
|
|
352
|
+
|------|------|
|
|
353
|
+
| [용어1] | [설명] |
|
|
354
|
+
| [용어2] | [설명] |
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## 4. 기능 목록
|
|
359
|
+
|
|
360
|
+
### 4.1 핵심 기능 (MVP)
|
|
361
|
+
- [ ] [기능 1]
|
|
362
|
+
- [ ] [기능 2]
|
|
363
|
+
|
|
364
|
+
### 4.2 추가 기능 (Phase 2)
|
|
365
|
+
- [ ] [기능 3]
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## 5. 입력 스펙
|
|
370
|
+
|
|
371
|
+
### 5.1 [API/기능명]
|
|
372
|
+
|
|
373
|
+
| 필드 | 타입 | 필수 | 제한 | 검증 | 예시 |
|
|
374
|
+
|------|------|------|------|------|------|
|
|
375
|
+
| [필드명] | string | O | 1~100자 | 빈문자열X | "예시값" |
|
|
376
|
+
| [필드명] | number | O | 1~9999 | 양수만 | 100 |
|
|
377
|
+
| [필드명] | enum | X | - | 목록 내 | "option1" |
|
|
378
|
+
|
|
379
|
+
#### enum 옵션
|
|
380
|
+
|
|
381
|
+
| 필드 | 옵션 | 설명 |
|
|
382
|
+
|------|------|------|
|
|
383
|
+
| [필드명] | option1 | [설명] |
|
|
384
|
+
| [필드명] | option2 | [설명] |
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## 6. 출력 스펙
|
|
389
|
+
|
|
390
|
+
### 6.1 성공 응답
|
|
391
|
+
|
|
392
|
+
```json
|
|
393
|
+
{{
|
|
394
|
+
"success": true,
|
|
395
|
+
"data": {{
|
|
396
|
+
"id": "abc123",
|
|
397
|
+
"createdAt": "2024-01-01T00:00:00Z",
|
|
398
|
+
"result": {{}}
|
|
399
|
+
}}
|
|
400
|
+
}}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### 6.2 필드 설명
|
|
404
|
+
|
|
405
|
+
| 필드 | 타입 | 설명 |
|
|
406
|
+
|------|------|------|
|
|
407
|
+
| id | string | 고유 식별자 |
|
|
408
|
+
| createdAt | datetime | 생성 시각 (ISO 8601) |
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## 7. 에러 코드
|
|
413
|
+
|
|
414
|
+
| 상황 | 코드 | HTTP | 메시지 |
|
|
415
|
+
|------|------|------|--------|
|
|
416
|
+
| 잔액 부족 | INSUFFICIENT_CREDITS | 402 | "크레딧 부족. 필요: {{required}}, 보유: {{available}}" |
|
|
417
|
+
| 권한 없음 | UNAUTHORIZED | 401 | "인증이 필요합니다" |
|
|
418
|
+
| 잘못된 요청 | INVALID_REQUEST | 400 | "{{field}} 필드가 잘못되었습니다" |
|
|
419
|
+
| 서버 오류 | INTERNAL_ERROR | 500 | "서버 오류가 발생했습니다" |
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## 8. 상태 머신
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
[상태1] --이벤트1--> [상태2] --이벤트2--> [상태3]
|
|
427
|
+
|
|
|
428
|
+
+--실패--> [에러상태]
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### 상태 설명
|
|
432
|
+
|
|
433
|
+
| 상태 | 설명 | 진입 조건 |
|
|
434
|
+
|------|------|----------|
|
|
435
|
+
| [상태1] | [설명] | 초기 상태 |
|
|
436
|
+
| [상태2] | [설명] | [이벤트] 발생 시 |
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## 9. API 엔드포인트
|
|
441
|
+
|
|
442
|
+
### 9.1 [API명]
|
|
443
|
+
|
|
444
|
+
```
|
|
445
|
+
POST /v1/[endpoint]
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**Request:**
|
|
449
|
+
```json
|
|
450
|
+
{{
|
|
451
|
+
"field": "value"
|
|
452
|
+
}}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
**Response:**
|
|
456
|
+
```json
|
|
457
|
+
{{
|
|
458
|
+
"success": true
|
|
459
|
+
}}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## 10. 데이터 정책
|
|
465
|
+
|
|
466
|
+
| 항목 | 무료 | 유료 |
|
|
467
|
+
|------|------|------|
|
|
468
|
+
| 보관 기간 | 24시간 | 7일 |
|
|
469
|
+
| 용량 제한 | 10MB | 100MB |
|
|
470
|
+
| API 호출 | 100/일 | 무제한 |
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## 11. 검증 계획
|
|
475
|
+
|
|
476
|
+
### 11.1 단위 테스트
|
|
477
|
+
- [ ] [테스트 케이스 1]
|
|
478
|
+
- [ ] [테스트 케이스 2]
|
|
479
|
+
|
|
480
|
+
### 11.2 통합 테스트
|
|
481
|
+
- [ ] [테스트 케이스]
|
|
482
|
+
|
|
483
|
+
### 11.3 엣지 케이스
|
|
484
|
+
- [ ] 빈 입력값
|
|
485
|
+
- [ ] 최대 길이 초과
|
|
486
|
+
- [ ] 특수문자 포함
|
|
487
|
+
- [ ] 동시 요청
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## 부록
|
|
492
|
+
|
|
493
|
+
### A. 참고 자료
|
|
494
|
+
- [링크1]
|
|
495
|
+
|
|
496
|
+
### B. 변경 이력
|
|
497
|
+
|
|
498
|
+
| 날짜 | 버전 | 변경 내용 | 작성자 |
|
|
499
|
+
|------|------|----------|--------|
|
|
500
|
+
| {datetime.now().strftime('%Y-%m-%d')} | 1.0 | 초안 작성 | |
|
|
501
|
+
"""
|
|
502
|
+
|
|
503
|
+
# 파일 저장
|
|
504
|
+
if output_path:
|
|
505
|
+
try:
|
|
506
|
+
output_file = Path(output_path)
|
|
507
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
508
|
+
output_file.write_text(template, encoding='utf-8')
|
|
509
|
+
return [TextContent(type="text", text=f"✅ PRD 템플릿 생성 완료: {output_path}\n\n이제 각 섹션을 채워주세요.")]
|
|
510
|
+
except Exception as e:
|
|
511
|
+
return [TextContent(type="text", text=f"❌ 파일 저장 실패: {e}\n\n아래 템플릿을 직접 복사해서 사용하세요:\n\n{template}")]
|
|
512
|
+
|
|
513
|
+
return [TextContent(type="text", text=template)]
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
async def _write_prd_section(section: str, content: str) -> list[TextContent]:
|
|
517
|
+
"""PRD 섹션별 작성 가이드"""
|
|
518
|
+
guides = {
|
|
519
|
+
"summary": """## 한 줄 요약 작성법
|
|
520
|
+
|
|
521
|
+
**목적**: 프로젝트를 한 문장으로 설명
|
|
522
|
+
|
|
523
|
+
**좋은 예시**:
|
|
524
|
+
- "한 번 라이브로 일주일치 콘텐츠 생성"
|
|
525
|
+
- "음성만으로 회의록 자동 작성"
|
|
526
|
+
- "코드 리뷰를 자동화하는 AI 봇"
|
|
527
|
+
|
|
528
|
+
**나쁜 예시**:
|
|
529
|
+
- "좋은 서비스" (너무 추상적)
|
|
530
|
+
- "AI를 활용한 혁신적인..." (마케팅 문구)
|
|
531
|
+
|
|
532
|
+
**체크리스트**:
|
|
533
|
+
- [ ] 10단어 이내인가?
|
|
534
|
+
- [ ] 누가 봐도 이해되는가?
|
|
535
|
+
- [ ] "그래서 뭘 하는 건데?"에 답이 되는가?
|
|
536
|
+
|
|
537
|
+
지금 한 줄 요약을 작성해주세요.""",
|
|
538
|
+
|
|
539
|
+
"principles": """## 핵심 원칙 작성법
|
|
540
|
+
|
|
541
|
+
**목적**: 의사결정의 기준이 되는 불변의 원칙
|
|
542
|
+
|
|
543
|
+
**좋은 예시**:
|
|
544
|
+
1. 원가 보호 - 절대 손해 보지 않음
|
|
545
|
+
2. 무료 체험 - 결제 전 가치 확인 가능
|
|
546
|
+
3. 현금 유입 - 모든 기능은 수익으로 연결
|
|
547
|
+
|
|
548
|
+
**나쁜 예시**:
|
|
549
|
+
- "좋은 UX" (측정 불가)
|
|
550
|
+
- "빠르게 개발" (원칙이 아닌 방법)
|
|
551
|
+
|
|
552
|
+
**체크리스트**:
|
|
553
|
+
- [ ] 3개 이하인가?
|
|
554
|
+
- [ ] 충돌 시 우선순위가 명확한가?
|
|
555
|
+
- [ ] 기능 추가 시 판단 기준이 되는가?
|
|
556
|
+
|
|
557
|
+
지금 핵심 원칙 3개를 작성해주세요.""",
|
|
558
|
+
|
|
559
|
+
"input_spec": """## 입력 스펙 작성법
|
|
560
|
+
|
|
561
|
+
**목적**: 모든 입력값의 정확한 정의
|
|
562
|
+
|
|
563
|
+
**필수 항목**:
|
|
564
|
+
| 필드 | 타입 | 필수 | 제한 | 검증 | 예시 |
|
|
565
|
+
|------|------|------|------|------|------|
|
|
566
|
+
| name | string | O | 1~100자 | 빈문자열X | "홍길동" |
|
|
567
|
+
| age | number | O | 1~150 | 정수만 | 25 |
|
|
568
|
+
| type | enum | X | - | 목록 내 | "premium" |
|
|
569
|
+
|
|
570
|
+
**체크리스트**:
|
|
571
|
+
- [ ] 모든 필드에 타입이 있는가?
|
|
572
|
+
- [ ] 문자열에 길이 제한이 있는가?
|
|
573
|
+
- [ ] 숫자에 범위가 있는가?
|
|
574
|
+
- [ ] enum에 가능한 값 목록이 있는가?
|
|
575
|
+
- [ ] 예시가 있는가?
|
|
576
|
+
|
|
577
|
+
지금 입력 스펙을 작성해주세요.""",
|
|
578
|
+
|
|
579
|
+
"output_spec": """## 출력 스펙 작성법
|
|
580
|
+
|
|
581
|
+
**목적**: API 응답의 정확한 구조
|
|
582
|
+
|
|
583
|
+
**필수 항목**:
|
|
584
|
+
```json
|
|
585
|
+
{
|
|
586
|
+
"success": true,
|
|
587
|
+
"data": {
|
|
588
|
+
"id": "abc123",
|
|
589
|
+
"createdAt": "2024-01-01T00:00:00Z"
|
|
590
|
+
},
|
|
591
|
+
"meta": {
|
|
592
|
+
"page": 1,
|
|
593
|
+
"total": 100
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
**체크리스트**:
|
|
599
|
+
- [ ] 실제 JSON 형태로 작성했는가?
|
|
600
|
+
- [ ] 모든 필드 타입이 명확한가?
|
|
601
|
+
- [ ] null이 올 수 있는 필드가 표시되어 있는가?
|
|
602
|
+
- [ ] 날짜 형식이 명시되어 있는가? (ISO 8601)
|
|
603
|
+
- [ ] 페이지네이션이 필요하면 포함했는가?
|
|
604
|
+
|
|
605
|
+
지금 출력 스펙을 작성해주세요.""",
|
|
606
|
+
|
|
607
|
+
"errors": """## 에러 테이블 작성법
|
|
608
|
+
|
|
609
|
+
**목적**: 모든 에러 상황의 정의
|
|
610
|
+
|
|
611
|
+
**형식**:
|
|
612
|
+
| 상황 | 코드 | HTTP | 메시지 |
|
|
613
|
+
|------|------|------|--------|
|
|
614
|
+
| 잔액 부족 | INSUFFICIENT_CREDITS | 402 | "크레딧 부족. 필요: {n}" |
|
|
615
|
+
|
|
616
|
+
**규칙**:
|
|
617
|
+
- 코드는 SNAKE_CASE
|
|
618
|
+
- 메시지에 동적 값은 {중괄호}로 표시
|
|
619
|
+
- HTTP 상태 코드 필수
|
|
620
|
+
|
|
621
|
+
**체크리스트**:
|
|
622
|
+
- [ ] 인증 에러가 있는가?
|
|
623
|
+
- [ ] 권한 에러가 있는가?
|
|
624
|
+
- [ ] 입력값 검증 에러가 있는가?
|
|
625
|
+
- [ ] 비즈니스 로직 에러가 있는가?
|
|
626
|
+
- [ ] 서버 에러가 있는가?
|
|
627
|
+
|
|
628
|
+
지금 에러 테이블을 작성해주세요.""",
|
|
629
|
+
|
|
630
|
+
"state_machine": """## 상태 머신 작성법
|
|
631
|
+
|
|
632
|
+
**목적**: 복잡한 플로우의 시각화
|
|
633
|
+
|
|
634
|
+
**형식**:
|
|
635
|
+
```
|
|
636
|
+
[available] --reserve--> [reserved] --capture--> [completed]
|
|
637
|
+
|
|
|
638
|
+
+--timeout--> [expired]
|
|
639
|
+
|
|
|
640
|
+
+--cancel--> [cancelled]
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
**체크리스트**:
|
|
644
|
+
- [ ] 시작 상태가 있는가?
|
|
645
|
+
- [ ] 종료 상태가 있는가?
|
|
646
|
+
- [ ] 모든 전이에 이벤트명이 있는가?
|
|
647
|
+
- [ ] 실패/에러 경로가 있는가?
|
|
648
|
+
- [ ] 타임아웃 처리가 있는가?
|
|
649
|
+
|
|
650
|
+
지금 상태 머신을 작성해주세요.""",
|
|
651
|
+
|
|
652
|
+
"api_endpoints": """## API 엔드포인트 작성법
|
|
653
|
+
|
|
654
|
+
**목적**: REST API 명세
|
|
655
|
+
|
|
656
|
+
**형식**:
|
|
657
|
+
```
|
|
658
|
+
POST /v1/orders
|
|
659
|
+
Authorization: Bearer {token}
|
|
660
|
+
Content-Type: application/json
|
|
661
|
+
|
|
662
|
+
Request:
|
|
663
|
+
{
|
|
664
|
+
"productId": "abc123",
|
|
665
|
+
"quantity": 1
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
Response (201):
|
|
669
|
+
{
|
|
670
|
+
"orderId": "ord_123",
|
|
671
|
+
"status": "created"
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
**체크리스트**:
|
|
676
|
+
- [ ] /v1/ 버전 prefix가 있는가?
|
|
677
|
+
- [ ] HTTP 메서드가 적절한가? (GET=조회, POST=생성, PUT=수정, DELETE=삭제)
|
|
678
|
+
- [ ] 인증 방식이 명시되어 있는가?
|
|
679
|
+
- [ ] 성공/실패 응답 코드가 있는가?
|
|
680
|
+
|
|
681
|
+
지금 API 엔드포인트를 작성해주세요.""",
|
|
682
|
+
|
|
683
|
+
"db_schema": """## DB 스키마 작성법
|
|
684
|
+
|
|
685
|
+
**목적**: 데이터 구조 정의
|
|
686
|
+
|
|
687
|
+
**형식**:
|
|
688
|
+
```sql
|
|
689
|
+
CREATE TABLE users (
|
|
690
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
691
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
692
|
+
name VARCHAR(100) NOT NULL,
|
|
693
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
694
|
+
updated_at TIMESTAMP DEFAULT NOW()
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
CREATE INDEX idx_users_email ON users(email);
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
**체크리스트**:
|
|
701
|
+
- [ ] 기본키가 있는가?
|
|
702
|
+
- [ ] 외래키 관계가 명확한가?
|
|
703
|
+
- [ ] 인덱스가 필요한 컬럼에 있는가?
|
|
704
|
+
- [ ] NOT NULL 제약이 적절한가?
|
|
705
|
+
- [ ] 기본값이 설정되어 있는가?
|
|
706
|
+
|
|
707
|
+
지금 DB 스키마를 작성해주세요."""
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
guide = guides.get(section, "알 수 없는 섹션입니다.")
|
|
711
|
+
return [TextContent(type="text", text=guide)]
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
async def _init_docs(path: str, project_name: str) -> list[TextContent]:
|
|
715
|
+
"""docs 폴더 초기화 - 필수 문서 템플릿 생성"""
|
|
716
|
+
from datetime import datetime
|
|
717
|
+
|
|
718
|
+
project_path = Path(path)
|
|
719
|
+
docs_path = project_path / "docs"
|
|
720
|
+
|
|
721
|
+
# docs 폴더 생성
|
|
722
|
+
docs_path.mkdir(parents=True, exist_ok=True)
|
|
723
|
+
|
|
724
|
+
created_files = []
|
|
725
|
+
|
|
726
|
+
# 1. PRD.md
|
|
727
|
+
prd_content = f"""# {project_name} PRD
|
|
728
|
+
|
|
729
|
+
> 이 문서가 법. 여기 없으면 안 만듦.
|
|
730
|
+
> 작성일: {datetime.now().strftime('%Y-%m-%d')}
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
## 1. 한 줄 요약
|
|
735
|
+
<!-- 프로젝트가 뭔지 한 문장으로 -->
|
|
736
|
+
|
|
737
|
+
[여기에 작성]
|
|
738
|
+
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
## 2. 핵심 원칙
|
|
742
|
+
|
|
743
|
+
1. [원칙 1]
|
|
744
|
+
2. [원칙 2]
|
|
745
|
+
3. [원칙 3]
|
|
746
|
+
|
|
747
|
+
---
|
|
748
|
+
|
|
749
|
+
## 3. 기능 목록
|
|
750
|
+
|
|
751
|
+
### MVP
|
|
752
|
+
- [ ] [기능 1]
|
|
753
|
+
- [ ] [기능 2]
|
|
754
|
+
|
|
755
|
+
---
|
|
756
|
+
|
|
757
|
+
## 4. 입력 스펙
|
|
758
|
+
|
|
759
|
+
| 필드 | 타입 | 필수 | 제한 | 예시 |
|
|
760
|
+
|------|------|------|------|------|
|
|
761
|
+
| | | | | |
|
|
762
|
+
|
|
763
|
+
---
|
|
764
|
+
|
|
765
|
+
## 5. 출력 스펙
|
|
766
|
+
|
|
767
|
+
```json
|
|
768
|
+
{{
|
|
769
|
+
"success": true,
|
|
770
|
+
"data": {{}}
|
|
771
|
+
}}
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
---
|
|
775
|
+
|
|
776
|
+
## 6. 에러 코드
|
|
777
|
+
|
|
778
|
+
| 상황 | 코드 | HTTP | 메시지 |
|
|
779
|
+
|------|------|------|--------|
|
|
780
|
+
| | | | |
|
|
781
|
+
|
|
782
|
+
---
|
|
783
|
+
|
|
784
|
+
## 변경 이력
|
|
785
|
+
|
|
786
|
+
| 날짜 | 버전 | 변경 내용 |
|
|
787
|
+
|------|------|----------|
|
|
788
|
+
| {datetime.now().strftime('%Y-%m-%d')} | 1.0 | 초안 |
|
|
789
|
+
"""
|
|
790
|
+
prd_file = docs_path / "PRD.md"
|
|
791
|
+
if not prd_file.exists():
|
|
792
|
+
prd_file.write_text(prd_content, encoding='utf-8')
|
|
793
|
+
created_files.append("PRD.md")
|
|
794
|
+
|
|
795
|
+
# 2. ARCHITECTURE.md
|
|
796
|
+
arch_content = f"""# {project_name} 아키텍처
|
|
797
|
+
|
|
798
|
+
## 시스템 구조
|
|
799
|
+
|
|
800
|
+
```
|
|
801
|
+
[클라이언트] --> [API 서버] --> [데이터베이스]
|
|
802
|
+
|
|
|
803
|
+
v
|
|
804
|
+
[외부 서비스]
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
## 기술 스택
|
|
808
|
+
|
|
809
|
+
| 구분 | 기술 | 이유 |
|
|
810
|
+
|------|------|------|
|
|
811
|
+
| 언어 | | |
|
|
812
|
+
| 프레임워크 | | |
|
|
813
|
+
| 데이터베이스 | | |
|
|
814
|
+
|
|
815
|
+
## 디렉토리 구조
|
|
816
|
+
|
|
817
|
+
```
|
|
818
|
+
src/
|
|
819
|
+
├── api/ # API 라우터
|
|
820
|
+
├── services/ # 비즈니스 로직
|
|
821
|
+
├── models/ # 데이터 모델
|
|
822
|
+
└── utils/ # 유틸리티
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
## 주요 모듈
|
|
826
|
+
|
|
827
|
+
### [모듈명]
|
|
828
|
+
- 역할:
|
|
829
|
+
- 의존성:
|
|
830
|
+
"""
|
|
831
|
+
arch_file = docs_path / "ARCHITECTURE.md"
|
|
832
|
+
if not arch_file.exists():
|
|
833
|
+
arch_file.write_text(arch_content, encoding='utf-8')
|
|
834
|
+
created_files.append("ARCHITECTURE.md")
|
|
835
|
+
|
|
836
|
+
# 3. API.md
|
|
837
|
+
api_content = f"""# {project_name} API 스펙
|
|
838
|
+
|
|
839
|
+
## Base URL
|
|
840
|
+
```
|
|
841
|
+
https://api.example.com/v1
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
## 인증
|
|
845
|
+
```
|
|
846
|
+
Authorization: Bearer {{token}}
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
851
|
+
## 엔드포인트
|
|
852
|
+
|
|
853
|
+
### [기능명]
|
|
854
|
+
|
|
855
|
+
```
|
|
856
|
+
POST /v1/endpoint
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
**Request:**
|
|
860
|
+
```json
|
|
861
|
+
{{
|
|
862
|
+
"field": "value"
|
|
863
|
+
}}
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
**Response (200):**
|
|
867
|
+
```json
|
|
868
|
+
{{
|
|
869
|
+
"success": true,
|
|
870
|
+
"data": {{}}
|
|
871
|
+
}}
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
**Errors:**
|
|
875
|
+
| 코드 | HTTP | 설명 |
|
|
876
|
+
|------|------|------|
|
|
877
|
+
| | | |
|
|
878
|
+
"""
|
|
879
|
+
api_file = docs_path / "API.md"
|
|
880
|
+
if not api_file.exists():
|
|
881
|
+
api_file.write_text(api_content, encoding='utf-8')
|
|
882
|
+
created_files.append("API.md")
|
|
883
|
+
|
|
884
|
+
# 4. DATABASE.md
|
|
885
|
+
db_content = f"""# {project_name} DB 스키마
|
|
886
|
+
|
|
887
|
+
## ERD
|
|
888
|
+
|
|
889
|
+
```
|
|
890
|
+
[users] 1--* [orders] *--1 [products]
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
## 테이블
|
|
894
|
+
|
|
895
|
+
### users
|
|
896
|
+
```sql
|
|
897
|
+
CREATE TABLE users (
|
|
898
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
899
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
900
|
+
name VARCHAR(100) NOT NULL,
|
|
901
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
902
|
+
);
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
### [테이블명]
|
|
906
|
+
```sql
|
|
907
|
+
-- 여기에 작성
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
## 인덱스
|
|
911
|
+
|
|
912
|
+
| 테이블 | 인덱스 | 컬럼 | 이유 |
|
|
913
|
+
|--------|--------|------|------|
|
|
914
|
+
| | | | |
|
|
915
|
+
"""
|
|
916
|
+
db_file = docs_path / "DATABASE.md"
|
|
917
|
+
if not db_file.exists():
|
|
918
|
+
db_file.write_text(db_content, encoding='utf-8')
|
|
919
|
+
created_files.append("DATABASE.md")
|
|
920
|
+
|
|
921
|
+
# 5. VERIFICATION.md
|
|
922
|
+
verify_content = f"""# {project_name} 검증 계획
|
|
923
|
+
|
|
924
|
+
## 테스트 전략
|
|
925
|
+
|
|
926
|
+
| 유형 | 범위 | 도구 |
|
|
927
|
+
|------|------|------|
|
|
928
|
+
| 단위 테스트 | 함수/메서드 | pytest |
|
|
929
|
+
| 통합 테스트 | API 엔드포인트 | pytest |
|
|
930
|
+
| E2E 테스트 | 사용자 시나리오 | |
|
|
931
|
+
|
|
932
|
+
## 테스트 케이스
|
|
933
|
+
|
|
934
|
+
### 핵심 기능
|
|
935
|
+
- [ ] [정상 케이스 1]
|
|
936
|
+
- [ ] [정상 케이스 2]
|
|
937
|
+
|
|
938
|
+
### 엣지 케이스
|
|
939
|
+
- [ ] 빈 입력값
|
|
940
|
+
- [ ] 최대 길이 초과
|
|
941
|
+
- [ ] 특수문자 포함
|
|
942
|
+
- [ ] 동시 요청
|
|
943
|
+
|
|
944
|
+
### 에러 케이스
|
|
945
|
+
- [ ] 인증 실패
|
|
946
|
+
- [ ] 권한 없음
|
|
947
|
+
- [ ] 리소스 없음
|
|
948
|
+
|
|
949
|
+
## 성능 기준
|
|
950
|
+
|
|
951
|
+
| 항목 | 목표 | 측정 방법 |
|
|
952
|
+
|------|------|----------|
|
|
953
|
+
| 응답 시간 | < 200ms | |
|
|
954
|
+
| 처리량 | > 100 req/s | |
|
|
955
|
+
"""
|
|
956
|
+
verify_file = docs_path / "VERIFICATION.md"
|
|
957
|
+
if not verify_file.exists():
|
|
958
|
+
verify_file.write_text(verify_content, encoding='utf-8')
|
|
959
|
+
created_files.append("VERIFICATION.md")
|
|
960
|
+
|
|
961
|
+
if created_files:
|
|
962
|
+
files_list = "\n".join(f"- {f}" for f in created_files)
|
|
963
|
+
return [TextContent(type="text", text=f"""# ✅ docs 폴더 초기화 완료
|
|
964
|
+
|
|
965
|
+
## 생성된 파일
|
|
966
|
+
{files_list}
|
|
967
|
+
|
|
968
|
+
## 위치
|
|
969
|
+
`{docs_path}`
|
|
970
|
+
|
|
971
|
+
## 다음 단계
|
|
972
|
+
1. **PRD.md**부터 작성하세요 - 가장 중요!
|
|
973
|
+
2. 한 줄 요약 → 핵심 원칙 → 기능 목록 순으로
|
|
974
|
+
3. `get_prd_guide` 도구로 작성법 확인 가능
|
|
975
|
+
|
|
976
|
+
⚠️ PRD가 완성되기 전까지 코딩은 금지입니다.
|
|
977
|
+
""")]
|
|
978
|
+
else:
|
|
979
|
+
return [TextContent(type="text", text=f"""# ℹ️ docs 폴더 이미 존재
|
|
980
|
+
|
|
981
|
+
## 위치
|
|
982
|
+
`{docs_path}`
|
|
983
|
+
|
|
984
|
+
## 기존 파일 유지됨
|
|
985
|
+
이미 있는 파일은 덮어쓰지 않았습니다.
|
|
986
|
+
|
|
987
|
+
`analyze_docs` 도구로 현재 문서 상태를 확인하세요.
|
|
988
|
+
""")]
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
async def _get_prd_guide() -> list[TextContent]:
|
|
992
|
+
guide = """## PRD 작성법
|
|
993
|
+
|
|
994
|
+
> 이 문서가 법. 여기 없으면 안 만듦.
|
|
995
|
+
|
|
996
|
+
### Step 1: 한 줄 요약
|
|
997
|
+
프로젝트가 뭔지 한 문장으로. 못 쓰면 정리 안 된 거임.
|
|
998
|
+
```
|
|
999
|
+
예: "한 번 라이브로 일주일치 콘텐츠"
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
### Step 2: 핵심 원칙 3개
|
|
1003
|
+
절대 안 변하는 것들. 이거 기준으로 기능 판단.
|
|
1004
|
+
```
|
|
1005
|
+
예: 원가 보호 / 무료 체험 / 현금 유입
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
### Step 3: 입력 스펙 테이블
|
|
1009
|
+
필드 | 타입 | 필수 | 제한 | 검증 | 예시
|
|
1010
|
+
```
|
|
1011
|
+
예: productName | string | O | 1~100자 | 빈문자열X | '코코넛오일'
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
### Step 4: 출력 JSON
|
|
1015
|
+
말로 설명 X. 실제 응답 그대로.
|
|
1016
|
+
```json
|
|
1017
|
+
{"id": "abc123", "status": "completed", "result": {...}}
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
### Step 5: 에러 테이블
|
|
1021
|
+
상황 | 코드 | 메시지. SNAKE_CASE 통일.
|
|
1022
|
+
```
|
|
1023
|
+
예: 잔액부족 | INSUFFICIENT_CREDITS | '크레딧 부족. 필요: {n}'
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
### Step 6: 상태 머신
|
|
1027
|
+
복잡한 플로우는 ASCII로.
|
|
1028
|
+
```
|
|
1029
|
+
[available] --reserve--> [reserved] --capture--> [done]
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
---
|
|
1033
|
+
|
|
1034
|
+
💡 팁: `get_prd_template` 도구로 빈 템플릿을 생성하세요.
|
|
1035
|
+
💡 팁: `write_prd_section` 도구로 섹션별 가이드를 받으세요.
|
|
1036
|
+
"""
|
|
1037
|
+
return [TextContent(type="text", text=guide)]
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
async def _get_verify_checklist() -> list[TextContent]:
|
|
1041
|
+
checklist = """## PRD 검증 체크리스트
|
|
1042
|
+
|
|
1043
|
+
> 빠뜨리면 나중에 다시 짬
|
|
1044
|
+
|
|
1045
|
+
### 스펙
|
|
1046
|
+
- [ ] 입력 제한값 다 있음? (1~100자, 최대 10개 같은 거)
|
|
1047
|
+
- [ ] enum 옵션표 있음? (tone: friendly|expert|urgent)
|
|
1048
|
+
- [ ] 출력 JSON 필드 다 나옴? (metadata, createdAt 빠뜨리기 쉬움)
|
|
1049
|
+
|
|
1050
|
+
### 에러
|
|
1051
|
+
- [ ] 에러코드 SNAKE_CASE? (INSUFFICIENT_CREDITS ⭕)
|
|
1052
|
+
- [ ] 동적 값 들어감? ('필요: {required}, 보유: {available}')
|
|
1053
|
+
|
|
1054
|
+
### 돈
|
|
1055
|
+
- [ ] 무료/유료 구분표? (Free: 미리보기 / Paid: 다운로드)
|
|
1056
|
+
- [ ] 크레딧 차감 시점? (reserve -> capture -> release)
|
|
1057
|
+
- [ ] 실패 시 환불? (작업 실패하면 release)
|
|
1058
|
+
|
|
1059
|
+
### API
|
|
1060
|
+
- [ ] /v1/ 붙어있음? (POST /v1/scripts ⭕)
|
|
1061
|
+
- [ ] 202 맞게 씀? (비동기는 202 + jobId)
|
|
1062
|
+
|
|
1063
|
+
### 데이터
|
|
1064
|
+
- [ ] 보관 기간? (무료 24시간, 유료 7일)
|
|
1065
|
+
- [ ] 만료 알림? (24시간 전 푸시)
|
|
1066
|
+
"""
|
|
1067
|
+
return [TextContent(type="text", text=checklist)]
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
async def _get_setup_guide(platform: str) -> list[TextContent]:
|
|
1071
|
+
"""Clouvel 설치/설정 가이드"""
|
|
1072
|
+
|
|
1073
|
+
desktop_guide = """## Claude Desktop 설정
|
|
1074
|
+
|
|
1075
|
+
### 1. 설정 파일 열기
|
|
1076
|
+
|
|
1077
|
+
**macOS:**
|
|
1078
|
+
```
|
|
1079
|
+
~/Library/Application Support/Claude/claude_desktop_config.json
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
**Windows:**
|
|
1083
|
+
```
|
|
1084
|
+
%APPDATA%\\Claude\\claude_desktop_config.json
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
### 2. MCP 서버 추가
|
|
1088
|
+
|
|
1089
|
+
```json
|
|
1090
|
+
{
|
|
1091
|
+
"mcpServers": {
|
|
1092
|
+
"clouvel": {
|
|
1093
|
+
"command": "uvx",
|
|
1094
|
+
"args": ["clouvel"]
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
### 3. Claude Desktop 재시작
|
|
1101
|
+
|
|
1102
|
+
설정 후 Claude Desktop을 완전히 종료했다가 다시 시작하세요.
|
|
1103
|
+
|
|
1104
|
+
### 4. 확인
|
|
1105
|
+
|
|
1106
|
+
Claude에게 "clouvel 도구 목록 보여줘"라고 말하면 도구들이 보입니다.
|
|
1107
|
+
"""
|
|
1108
|
+
|
|
1109
|
+
code_guide = """## Claude Code (CLI) 설정
|
|
1110
|
+
|
|
1111
|
+
### 1. 프로젝트 루트에 .mcp.json 생성
|
|
1112
|
+
|
|
1113
|
+
```json
|
|
1114
|
+
{
|
|
1115
|
+
"mcpServers": {
|
|
1116
|
+
"clouvel": {
|
|
1117
|
+
"command": "uvx",
|
|
1118
|
+
"args": ["clouvel"]
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
### 2. Claude Code 실행
|
|
1125
|
+
|
|
1126
|
+
```bash
|
|
1127
|
+
claude
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
### 3. 확인
|
|
1131
|
+
|
|
1132
|
+
```
|
|
1133
|
+
> clouvel 도구 목록 보여줘
|
|
1134
|
+
```
|
|
1135
|
+
"""
|
|
1136
|
+
|
|
1137
|
+
vscode_guide = """## VS Code 설정
|
|
1138
|
+
|
|
1139
|
+
### 1. 확장 설치
|
|
1140
|
+
|
|
1141
|
+
1. VS Code 열기
|
|
1142
|
+
2. 확장(Extensions) 탭 열기 (Ctrl+Shift+X)
|
|
1143
|
+
3. "Clouvel" 검색
|
|
1144
|
+
4. "Clouvel" 확장 설치 (whitening.clouvel)
|
|
1145
|
+
|
|
1146
|
+
### 2. MCP 서버 설정
|
|
1147
|
+
|
|
1148
|
+
1. 명령 팔레트 열기 (Ctrl+Shift+P)
|
|
1149
|
+
2. "Clouvel: MCP 서버 설정" 선택
|
|
1150
|
+
3. Claude Desktop 또는 Claude Code 선택
|
|
1151
|
+
|
|
1152
|
+
### 3. 사이드바에서 문서 상태 확인
|
|
1153
|
+
|
|
1154
|
+
왼쪽 사이드바에 Clouvel 아이콘이 생깁니다.
|
|
1155
|
+
문서 상태를 실시간으로 확인할 수 있습니다.
|
|
1156
|
+
"""
|
|
1157
|
+
|
|
1158
|
+
cursor_guide = """## Cursor 설정
|
|
1159
|
+
|
|
1160
|
+
### 1. 확장 설치
|
|
1161
|
+
|
|
1162
|
+
1. Cursor 열기
|
|
1163
|
+
2. 확장(Extensions) 탭 열기 (Ctrl+Shift+X)
|
|
1164
|
+
3. "Clouvel" 검색
|
|
1165
|
+
4. "Clouvel for Cursor" 확장 설치 (whitening.clouvel-cursor)
|
|
1166
|
+
|
|
1167
|
+
### 2. MCP 서버 설정
|
|
1168
|
+
|
|
1169
|
+
1. 명령 팔레트 열기 (Ctrl+Shift+P)
|
|
1170
|
+
2. "Clouvel: MCP 서버 설정" 선택
|
|
1171
|
+
3. Claude Desktop 또는 Claude Code 선택
|
|
1172
|
+
|
|
1173
|
+
### 3. 사이드바에서 문서 상태 확인
|
|
1174
|
+
|
|
1175
|
+
왼쪽 사이드바에 Clouvel 아이콘이 생깁니다.
|
|
1176
|
+
"""
|
|
1177
|
+
|
|
1178
|
+
guides = {
|
|
1179
|
+
"desktop": desktop_guide,
|
|
1180
|
+
"code": code_guide,
|
|
1181
|
+
"vscode": vscode_guide,
|
|
1182
|
+
"cursor": cursor_guide,
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
if platform == "all":
|
|
1186
|
+
result = "# Clouvel 설치/설정 가이드\n\n"
|
|
1187
|
+
result += desktop_guide + "\n---\n\n"
|
|
1188
|
+
result += code_guide + "\n---\n\n"
|
|
1189
|
+
result += vscode_guide + "\n---\n\n"
|
|
1190
|
+
result += cursor_guide
|
|
1191
|
+
else:
|
|
1192
|
+
result = guides.get(platform, "알 수 없는 플랫폼입니다.")
|
|
1193
|
+
|
|
1194
|
+
return [TextContent(type="text", text=result)]
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
async def run_server():
|
|
1198
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
1199
|
+
await server.run(read_stream, write_stream, server.create_initialization_options())
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
def main():
|
|
1203
|
+
import asyncio
|
|
1204
|
+
asyncio.run(run_server())
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
if __name__ == "__main__":
|
|
1208
|
+
main()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clouvel
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: 바이브코딩 프로세스를 강제하는 MCP 서버
|
|
5
|
+
Project-URL: Homepage, https://github.com/sinabro/clouvel
|
|
6
|
+
Project-URL: Repository, https://github.com/sinabro/clouvel
|
|
7
|
+
Author: SINABRO
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: documentation,mcp,prd,vibe-coding
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Requires-Dist: mcp>=1.0.0
|
|
17
|
+
Requires-Dist: pydantic>=2.0.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Clouvel
|
|
21
|
+
|
|
22
|
+
바이브코딩 프로세스를 강제하는 MCP 서버.
|
|
23
|
+
|
|
24
|
+
코딩 전에 PRD 검증부터.
|
|
25
|
+
|
|
26
|
+
## 설치
|
|
27
|
+
|
|
28
|
+
Claude Desktop 설정 (`claude_desktop_config.json`):
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"clouvel": {
|
|
34
|
+
"command": "uvx",
|
|
35
|
+
"args": ["clouvel"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
끝. pip install 필요 없음.
|
|
42
|
+
|
|
43
|
+
## 제공 도구
|
|
44
|
+
|
|
45
|
+
| 도구 | 설명 |
|
|
46
|
+
|------|------|
|
|
47
|
+
| `scan_docs` | docs 폴더 파일 목록 |
|
|
48
|
+
| `analyze_docs` | 필수 문서 체크. 빠진 거 알려줌 |
|
|
49
|
+
| `get_prd_guide` | PRD 작성 가이드 |
|
|
50
|
+
| `get_verify_checklist` | 검증 체크리스트 |
|
|
51
|
+
|
|
52
|
+
## 사용법
|
|
53
|
+
|
|
54
|
+
Claude한테:
|
|
55
|
+
- "이 프로젝트 docs 검증해줘"
|
|
56
|
+
- "PRD 어떻게 써야 해?"
|
|
57
|
+
- "검증 체크리스트 보여줘"
|
|
58
|
+
|
|
59
|
+
## 필수 문서
|
|
60
|
+
|
|
61
|
+
`analyze_docs`가 체크하는 것들:
|
|
62
|
+
|
|
63
|
+
- PRD (제품 요구사항)
|
|
64
|
+
- 아키텍처 문서
|
|
65
|
+
- API 스펙
|
|
66
|
+
- DB 스키마
|
|
67
|
+
- 검증 계획
|
|
68
|
+
|
|
69
|
+
다 있어야 "바이브코딩 시작해도 됨" 나옴.
|
|
70
|
+
|
|
71
|
+
## 왜?
|
|
72
|
+
|
|
73
|
+
바이브코딩 = AI가 코드 짬.
|
|
74
|
+
근데 PRD 없이 시작하면 = 나중에 다 뜯어고침.
|
|
75
|
+
|
|
76
|
+
Clouvel = 문서 없으면 코딩 못 하게 강제.
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
clouvel/__init__.py,sha256=kd5Da6RpqW39wTlqulTHiEdOPNmPaxxs4Ke14XeuyIg,45
|
|
2
|
+
clouvel/server.py,sha256=5ROWIMKoGmjknx1JdtWzDd2UnkfAWwylwf33dhVW-rk,30698
|
|
3
|
+
clouvel-0.3.1.dist-info/METADATA,sha256=dOjQLN-RIN-X1jabrNvCZ1-I6WtGe9ZTs4ntc0sbj2c,1775
|
|
4
|
+
clouvel-0.3.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
5
|
+
clouvel-0.3.1.dist-info/entry_points.txt,sha256=OvZ_fGmyJXE-9o54QYRRbGt4CCsm_u5r-9Bnc0BdquE,41
|
|
6
|
+
clouvel-0.3.1.dist-info/RECORD,,
|