muno-claude-plugin 1.9.0 → 1.10.0
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.
- package/bin/cli.js +1 -1
- package/package.json +1 -1
- package/templates/skills/swagger-docs-generator/SKILL.md +78 -10
- package/templates/skills/swagger-docs-generator/scripts/get-local-ip.py +329 -0
- package/templates/skills/swagger-docs-generator/scripts/swagger-to-markdown.py +416 -0
- /package/templates/skills/app-design/reference/{component-examples.md → components.md} +0 -0
- /package/templates/skills/app-design/reference/{design-system-template.md → design-system.md} +0 -0
package/bin/cli.js
CHANGED
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@ description: |
|
|
|
4
4
|
Swagger/OpenAPI 문서를 읽어서 개발자가 읽기 쉬운 Markdown API 명세로 변환합니다.
|
|
5
5
|
"API 문서 만들어줘", "Swagger 문서 변환", "API 명세 생성", "OpenAPI 문서" 등의 요청에 사용합니다.
|
|
6
6
|
서버가 제공하는 Swagger JSON을 자동으로 파싱하여 체계적인 문서를 생성합니다.
|
|
7
|
-
allowed-tools: Read, Write, WebFetch, Grep, Glob
|
|
7
|
+
allowed-tools: Read, Write, WebFetch, Grep, Glob, Bash
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Swagger Docs Generator
|
|
@@ -571,6 +571,76 @@ curl -X POST "http://localhost:8080/api/users" \
|
|
|
571
571
|
|
|
572
572
|
---
|
|
573
573
|
|
|
574
|
+
## 실행 방법
|
|
575
|
+
|
|
576
|
+
### 스크립트 활용
|
|
577
|
+
|
|
578
|
+
이 스킬은 파이썬 스크립트를 사용하여 Swagger 문서를 변환합니다.
|
|
579
|
+
|
|
580
|
+
#### 1. 로컬 IP 감지
|
|
581
|
+
|
|
582
|
+
```bash
|
|
583
|
+
# 스크립트 위치: scripts/get-local-ip.py
|
|
584
|
+
SKILL_DIR=$(pwd)/.claude/skills/swagger-docs-generator
|
|
585
|
+
|
|
586
|
+
# 로컬 IP 감지
|
|
587
|
+
python3 $SKILL_DIR/scripts/get-local-ip.py
|
|
588
|
+
|
|
589
|
+
# 출력 예시:
|
|
590
|
+
# [
|
|
591
|
+
# {"ip": "192.168.0.100", "interface": "en0", "type": "WiFi"},
|
|
592
|
+
# {"ip": "127.0.0.1", "interface": "lo0", "type": "Loopback"}
|
|
593
|
+
# ]
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
#### 2. Swagger → Markdown 변환
|
|
597
|
+
|
|
598
|
+
```bash
|
|
599
|
+
# 스크립트 위치: scripts/swagger-to-markdown.py
|
|
600
|
+
SKILL_DIR=$(pwd)/.claude/skills/swagger-docs-generator
|
|
601
|
+
|
|
602
|
+
# 기본 사용
|
|
603
|
+
python3 $SKILL_DIR/scripts/swagger-to-markdown.py http://localhost:8080/v3/api-docs
|
|
604
|
+
|
|
605
|
+
# 파일로 저장
|
|
606
|
+
python3 $SKILL_DIR/scripts/swagger-to-markdown.py \
|
|
607
|
+
http://localhost:8080/v3/api-docs \
|
|
608
|
+
documents/api/user-service-api-docs.md
|
|
609
|
+
|
|
610
|
+
# 로컬 IP 자동 감지 + 변환 (통합 실행)
|
|
611
|
+
LOCAL_IP=$(python3 $SKILL_DIR/scripts/get-local-ip.py | jq -r '.[0].ip')
|
|
612
|
+
python3 $SKILL_DIR/scripts/swagger-to-markdown.py \
|
|
613
|
+
http://$LOCAL_IP:8080/v3/api-docs \
|
|
614
|
+
documents/api/api-docs.md
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
#### 3. 스킬 워크플로우
|
|
618
|
+
|
|
619
|
+
사용자가 `/swagger-docs-generator`를 호출하면:
|
|
620
|
+
|
|
621
|
+
1. **로컬 IP 감지**
|
|
622
|
+
```bash
|
|
623
|
+
python3 scripts/get-local-ip.py
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
2. **사용자 확인** (여러 IP가 감지되면)
|
|
627
|
+
- 감지된 IP 목록 표시
|
|
628
|
+
- 사용자 선택 받기
|
|
629
|
+
|
|
630
|
+
3. **Swagger 문서 변환**
|
|
631
|
+
```bash
|
|
632
|
+
python3 scripts/swagger-to-markdown.py \
|
|
633
|
+
http://{selected_ip}:{port}/v3/api-docs \
|
|
634
|
+
documents/api/{service-name}-api-docs.md
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
4. **결과 전달**
|
|
638
|
+
- 생성된 파일 경로
|
|
639
|
+
- API 엔드포인트 요약
|
|
640
|
+
- Swagger UI 링크
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
574
644
|
## 실행 예시
|
|
575
645
|
|
|
576
646
|
### Input
|
|
@@ -584,12 +654,10 @@ http://localhost:8080/v3/api-docs 의 API 문서를 생성해주세요.
|
|
|
584
654
|
|
|
585
655
|
### Process
|
|
586
656
|
|
|
587
|
-
1.
|
|
588
|
-
2. JSON
|
|
589
|
-
3.
|
|
590
|
-
4.
|
|
591
|
-
5. Markdown 문서 생성
|
|
592
|
-
6. `documents/api/user-service-api-docs.md` 저장
|
|
657
|
+
1. `python3 scripts/swagger-to-markdown.py` 실행
|
|
658
|
+
2. Swagger JSON 가져오기
|
|
659
|
+
3. JSON 파싱 및 Markdown 생성
|
|
660
|
+
4. `documents/api/user-service-api-docs.md` 저장
|
|
593
661
|
|
|
594
662
|
### Output
|
|
595
663
|
|
|
@@ -634,10 +702,10 @@ http://localhost:8080/v3/api-docs 의 API 문서를 생성해주세요.
|
|
|
634
702
|
|
|
635
703
|
---
|
|
636
704
|
|
|
637
|
-
## 참고
|
|
705
|
+
## 참고 문서
|
|
638
706
|
|
|
639
|
-
상세한 Markdown
|
|
640
|
-
-
|
|
707
|
+
상세한 Markdown 템플릿 예시:
|
|
708
|
+
- [reference/api-docs-template.md](reference/api-docs-template.md) - API 문서 템플릿 및 예시
|
|
641
709
|
|
|
642
710
|
---
|
|
643
711
|
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
로컬 IP 주소를 자동으로 감지하는 스크립트
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python get-local-ip.py
|
|
7
|
+
|
|
8
|
+
Output:
|
|
9
|
+
JSON 형식으로 감지된 IP 주소 목록 출력
|
|
10
|
+
[
|
|
11
|
+
{"ip": "192.168.0.100", "interface": "en0", "type": "WiFi"},
|
|
12
|
+
{"ip": "172.16.0.50", "interface": "en1", "type": "Ethernet"},
|
|
13
|
+
{"ip": "127.0.0.1", "interface": "lo0", "type": "Loopback"}
|
|
14
|
+
]
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import socket
|
|
18
|
+
import json
|
|
19
|
+
import platform
|
|
20
|
+
import subprocess
|
|
21
|
+
from typing import List, Dict
|
|
22
|
+
|
|
23
|
+
def get_local_ips() -> List[Dict[str, str]]:
|
|
24
|
+
"""모든 네트워크 인터페이스의 IP 주소를 감지"""
|
|
25
|
+
ips = []
|
|
26
|
+
|
|
27
|
+
# Method 1: socket을 이용한 기본 IP 감지 (가장 신뢰할 수 있는 방법)
|
|
28
|
+
try:
|
|
29
|
+
# 외부 연결을 시뮬레이션하여 실제 사용되는 IP 얻기
|
|
30
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
31
|
+
s.settimeout(0.1)
|
|
32
|
+
try:
|
|
33
|
+
# Google DNS에 연결 (실제로 패킷을 보내지 않음)
|
|
34
|
+
s.connect(('8.8.8.8', 80))
|
|
35
|
+
primary_ip = s.getsockname()[0]
|
|
36
|
+
ips.append({
|
|
37
|
+
'ip': primary_ip,
|
|
38
|
+
'interface': 'primary',
|
|
39
|
+
'type': 'Primary',
|
|
40
|
+
'priority': 1
|
|
41
|
+
})
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
finally:
|
|
45
|
+
s.close()
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
# Method 2: 플랫폼별 네트워크 인터페이스 조회
|
|
50
|
+
system = platform.system()
|
|
51
|
+
|
|
52
|
+
if system == 'Darwin': # macOS
|
|
53
|
+
ips.extend(get_macos_ips())
|
|
54
|
+
elif system == 'Linux':
|
|
55
|
+
ips.extend(get_linux_ips())
|
|
56
|
+
elif system == 'Windows':
|
|
57
|
+
ips.extend(get_windows_ips())
|
|
58
|
+
|
|
59
|
+
# 중복 제거 (IP 기준)
|
|
60
|
+
seen_ips = set()
|
|
61
|
+
unique_ips = []
|
|
62
|
+
for ip_info in ips:
|
|
63
|
+
if ip_info['ip'] not in seen_ips:
|
|
64
|
+
seen_ips.add(ip_info['ip'])
|
|
65
|
+
unique_ips.append(ip_info)
|
|
66
|
+
|
|
67
|
+
# 우선순위 정렬
|
|
68
|
+
unique_ips.sort(key=lambda x: (
|
|
69
|
+
x.get('priority', 99), # 우선순위
|
|
70
|
+
0 if x['ip'].startswith('192.168.') else 1, # 사설 IP 우선
|
|
71
|
+
x['ip'] # IP 주소
|
|
72
|
+
))
|
|
73
|
+
|
|
74
|
+
return unique_ips
|
|
75
|
+
|
|
76
|
+
def get_macos_ips() -> List[Dict[str, str]]:
|
|
77
|
+
"""macOS에서 IP 주소 감지"""
|
|
78
|
+
ips = []
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
# ifconfig 명령어 실행
|
|
82
|
+
result = subprocess.run(
|
|
83
|
+
['ifconfig'],
|
|
84
|
+
capture_output=True,
|
|
85
|
+
text=True,
|
|
86
|
+
timeout=5
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if result.returncode == 0:
|
|
90
|
+
current_interface = None
|
|
91
|
+
interface_type = 'Unknown'
|
|
92
|
+
|
|
93
|
+
for line in result.stdout.split('\n'):
|
|
94
|
+
line = line.strip()
|
|
95
|
+
|
|
96
|
+
# 인터페이스 이름 감지
|
|
97
|
+
if line and not line.startswith(' ') and ':' in line:
|
|
98
|
+
current_interface = line.split(':')[0]
|
|
99
|
+
|
|
100
|
+
# 인터페이스 타입 판단
|
|
101
|
+
if current_interface.startswith('en0'):
|
|
102
|
+
interface_type = 'WiFi'
|
|
103
|
+
elif current_interface.startswith('en1'):
|
|
104
|
+
interface_type = 'Ethernet'
|
|
105
|
+
elif current_interface.startswith('lo'):
|
|
106
|
+
interface_type = 'Loopback'
|
|
107
|
+
else:
|
|
108
|
+
interface_type = 'Other'
|
|
109
|
+
|
|
110
|
+
# inet 주소 추출
|
|
111
|
+
if 'inet ' in line and current_interface:
|
|
112
|
+
parts = line.split()
|
|
113
|
+
if len(parts) >= 2:
|
|
114
|
+
ip = parts[1]
|
|
115
|
+
|
|
116
|
+
# 우선순위 결정
|
|
117
|
+
priority = 10
|
|
118
|
+
if interface_type == 'WiFi':
|
|
119
|
+
priority = 2
|
|
120
|
+
elif interface_type == 'Ethernet':
|
|
121
|
+
priority = 3
|
|
122
|
+
elif interface_type == 'Loopback':
|
|
123
|
+
priority = 99
|
|
124
|
+
|
|
125
|
+
ips.append({
|
|
126
|
+
'ip': ip,
|
|
127
|
+
'interface': current_interface,
|
|
128
|
+
'type': interface_type,
|
|
129
|
+
'priority': priority
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
# ipconfig 명령어로 추가 시도
|
|
133
|
+
for interface in ['en0', 'en1', 'en2']:
|
|
134
|
+
try:
|
|
135
|
+
result = subprocess.run(
|
|
136
|
+
['ipconfig', 'getifaddr', interface],
|
|
137
|
+
capture_output=True,
|
|
138
|
+
text=True,
|
|
139
|
+
timeout=2
|
|
140
|
+
)
|
|
141
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
142
|
+
ip = result.stdout.strip()
|
|
143
|
+
interface_type = 'WiFi' if interface == 'en0' else 'Ethernet'
|
|
144
|
+
priority = 2 if interface == 'en0' else 3
|
|
145
|
+
|
|
146
|
+
ips.append({
|
|
147
|
+
'ip': ip,
|
|
148
|
+
'interface': interface,
|
|
149
|
+
'type': interface_type,
|
|
150
|
+
'priority': priority
|
|
151
|
+
})
|
|
152
|
+
except Exception:
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
print(f"macOS IP detection error: {e}", file=sys.stderr)
|
|
157
|
+
|
|
158
|
+
return ips
|
|
159
|
+
|
|
160
|
+
def get_linux_ips() -> List[Dict[str, str]]:
|
|
161
|
+
"""Linux에서 IP 주소 감지"""
|
|
162
|
+
ips = []
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
# hostname -I 명령어
|
|
166
|
+
result = subprocess.run(
|
|
167
|
+
['hostname', '-I'],
|
|
168
|
+
capture_output=True,
|
|
169
|
+
text=True,
|
|
170
|
+
timeout=5
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if result.returncode == 0:
|
|
174
|
+
for idx, ip in enumerate(result.stdout.strip().split()):
|
|
175
|
+
ips.append({
|
|
176
|
+
'ip': ip,
|
|
177
|
+
'interface': f'auto-{idx}',
|
|
178
|
+
'type': 'Auto-detected',
|
|
179
|
+
'priority': 5 + idx
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
# ip addr 명령어
|
|
183
|
+
result = subprocess.run(
|
|
184
|
+
['ip', 'addr'],
|
|
185
|
+
capture_output=True,
|
|
186
|
+
text=True,
|
|
187
|
+
timeout=5
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if result.returncode == 0:
|
|
191
|
+
current_interface = None
|
|
192
|
+
interface_type = 'Unknown'
|
|
193
|
+
|
|
194
|
+
for line in result.stdout.split('\n'):
|
|
195
|
+
line = line.strip()
|
|
196
|
+
|
|
197
|
+
# 인터페이스 감지
|
|
198
|
+
if line and line[0].isdigit() and ':' in line:
|
|
199
|
+
parts = line.split(':')
|
|
200
|
+
if len(parts) >= 2:
|
|
201
|
+
current_interface = parts[1].strip()
|
|
202
|
+
|
|
203
|
+
if 'wlan' in current_interface or 'wlp' in current_interface:
|
|
204
|
+
interface_type = 'WiFi'
|
|
205
|
+
elif 'eth' in current_interface or 'enp' in current_interface:
|
|
206
|
+
interface_type = 'Ethernet'
|
|
207
|
+
elif 'lo' in current_interface:
|
|
208
|
+
interface_type = 'Loopback'
|
|
209
|
+
else:
|
|
210
|
+
interface_type = 'Other'
|
|
211
|
+
|
|
212
|
+
# IP 주소 추출
|
|
213
|
+
if 'inet ' in line and current_interface:
|
|
214
|
+
parts = line.split()
|
|
215
|
+
if len(parts) >= 2:
|
|
216
|
+
ip_with_mask = parts[1]
|
|
217
|
+
ip = ip_with_mask.split('/')[0]
|
|
218
|
+
|
|
219
|
+
priority = 10
|
|
220
|
+
if interface_type == 'WiFi':
|
|
221
|
+
priority = 2
|
|
222
|
+
elif interface_type == 'Ethernet':
|
|
223
|
+
priority = 3
|
|
224
|
+
elif interface_type == 'Loopback':
|
|
225
|
+
priority = 99
|
|
226
|
+
|
|
227
|
+
ips.append({
|
|
228
|
+
'ip': ip,
|
|
229
|
+
'interface': current_interface,
|
|
230
|
+
'type': interface_type,
|
|
231
|
+
'priority': priority
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
print(f"Linux IP detection error: {e}", file=sys.stderr)
|
|
236
|
+
|
|
237
|
+
return ips
|
|
238
|
+
|
|
239
|
+
def get_windows_ips() -> List[Dict[str, str]]:
|
|
240
|
+
"""Windows에서 IP 주소 감지"""
|
|
241
|
+
ips = []
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
# ipconfig 명령어
|
|
245
|
+
result = subprocess.run(
|
|
246
|
+
['ipconfig'],
|
|
247
|
+
capture_output=True,
|
|
248
|
+
text=True,
|
|
249
|
+
timeout=5,
|
|
250
|
+
encoding='cp949' # Windows 인코딩
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if result.returncode == 0:
|
|
254
|
+
current_adapter = None
|
|
255
|
+
adapter_type = 'Unknown'
|
|
256
|
+
|
|
257
|
+
for line in result.stdout.split('\n'):
|
|
258
|
+
line = line.strip()
|
|
259
|
+
|
|
260
|
+
# 어댑터 이름 감지
|
|
261
|
+
if 'adapter' in line.lower() or '어댑터' in line:
|
|
262
|
+
current_adapter = line.split(':')[0].strip()
|
|
263
|
+
|
|
264
|
+
if 'wireless' in line.lower() or '무선' in line:
|
|
265
|
+
adapter_type = 'WiFi'
|
|
266
|
+
elif 'ethernet' in line.lower() or '이더넷' in line:
|
|
267
|
+
adapter_type = 'Ethernet'
|
|
268
|
+
else:
|
|
269
|
+
adapter_type = 'Other'
|
|
270
|
+
|
|
271
|
+
# IPv4 주소 추출
|
|
272
|
+
if ('IPv4' in line or 'IPv4 주소' in line) and ':' in line:
|
|
273
|
+
ip = line.split(':')[-1].strip()
|
|
274
|
+
# (기본 설정) 같은 문구 제거
|
|
275
|
+
ip = ip.replace('(기본 설정)', '').strip()
|
|
276
|
+
|
|
277
|
+
priority = 10
|
|
278
|
+
if adapter_type == 'WiFi':
|
|
279
|
+
priority = 2
|
|
280
|
+
elif adapter_type == 'Ethernet':
|
|
281
|
+
priority = 3
|
|
282
|
+
|
|
283
|
+
ips.append({
|
|
284
|
+
'ip': ip,
|
|
285
|
+
'interface': current_adapter or 'unknown',
|
|
286
|
+
'type': adapter_type,
|
|
287
|
+
'priority': priority
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
except Exception as e:
|
|
291
|
+
print(f"Windows IP detection error: {e}", file=sys.stderr)
|
|
292
|
+
|
|
293
|
+
return ips
|
|
294
|
+
|
|
295
|
+
def main():
|
|
296
|
+
"""메인 함수"""
|
|
297
|
+
try:
|
|
298
|
+
ips = get_local_ips()
|
|
299
|
+
|
|
300
|
+
if not ips:
|
|
301
|
+
# 기본값으로 localhost 추가
|
|
302
|
+
ips.append({
|
|
303
|
+
'ip': '127.0.0.1',
|
|
304
|
+
'interface': 'lo0',
|
|
305
|
+
'type': 'Loopback',
|
|
306
|
+
'priority': 99
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
# JSON 출력 (priority 제외)
|
|
310
|
+
output = []
|
|
311
|
+
for ip_info in ips:
|
|
312
|
+
output.append({
|
|
313
|
+
'ip': ip_info['ip'],
|
|
314
|
+
'interface': ip_info['interface'],
|
|
315
|
+
'type': ip_info['type']
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
print(json.dumps(output, indent=2, ensure_ascii=False))
|
|
319
|
+
|
|
320
|
+
except Exception as e:
|
|
321
|
+
import sys
|
|
322
|
+
print(json.dumps({
|
|
323
|
+
'error': str(e),
|
|
324
|
+
'ips': [{'ip': '127.0.0.1', 'interface': 'lo0', 'type': 'Loopback'}]
|
|
325
|
+
}, indent=2), file=sys.stderr)
|
|
326
|
+
sys.exit(1)
|
|
327
|
+
|
|
328
|
+
if __name__ == '__main__':
|
|
329
|
+
main()
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Swagger/OpenAPI JSON을 Markdown 문서로 변환하는 스크립트
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python swagger-to-markdown.py <swagger_url> [output_file]
|
|
7
|
+
python swagger-to-markdown.py http://localhost:8080/v3/api-docs
|
|
8
|
+
python swagger-to-markdown.py http://localhost:8080/v3/api-docs api-docs.md
|
|
9
|
+
|
|
10
|
+
Requirements:
|
|
11
|
+
pip install requests
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
import json
|
|
16
|
+
import requests
|
|
17
|
+
from typing import Dict, List, Any, Optional
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from urllib.parse import urlparse
|
|
20
|
+
|
|
21
|
+
def fetch_swagger(url: str) -> Dict[str, Any]:
|
|
22
|
+
"""Swagger JSON 가져오기"""
|
|
23
|
+
try:
|
|
24
|
+
response = requests.get(url, timeout=10)
|
|
25
|
+
response.raise_for_status()
|
|
26
|
+
return response.json()
|
|
27
|
+
except requests.exceptions.RequestException as e:
|
|
28
|
+
print(f"❌ Failed to fetch Swagger docs from {url}")
|
|
29
|
+
print(f"Error: {e}")
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
except json.JSONDecodeError as e:
|
|
32
|
+
print(f"❌ Invalid JSON response from {url}")
|
|
33
|
+
print(f"Error: {e}")
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
def get_base_url(swagger: Dict[str, Any]) -> str:
|
|
37
|
+
"""Base URL 추출"""
|
|
38
|
+
# OpenAPI 3.0
|
|
39
|
+
if 'servers' in swagger and swagger['servers']:
|
|
40
|
+
return swagger['servers'][0].get('url', '')
|
|
41
|
+
|
|
42
|
+
# Swagger 2.0
|
|
43
|
+
if 'host' in swagger:
|
|
44
|
+
scheme = swagger.get('schemes', ['http'])[0]
|
|
45
|
+
host = swagger['host']
|
|
46
|
+
base_path = swagger.get('basePath', '')
|
|
47
|
+
return f"{scheme}://{host}{base_path}"
|
|
48
|
+
|
|
49
|
+
return ''
|
|
50
|
+
|
|
51
|
+
def resolve_ref(ref: str, swagger: Dict[str, Any]) -> Dict[str, Any]:
|
|
52
|
+
"""$ref 참조 해석"""
|
|
53
|
+
if not ref.startswith('#/'):
|
|
54
|
+
return {}
|
|
55
|
+
|
|
56
|
+
parts = ref.replace('#/', '').split('/')
|
|
57
|
+
result = swagger
|
|
58
|
+
|
|
59
|
+
for part in parts:
|
|
60
|
+
if part in result:
|
|
61
|
+
result = result[part]
|
|
62
|
+
else:
|
|
63
|
+
return {}
|
|
64
|
+
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
def get_schema_type(schema: Dict[str, Any], swagger: Dict[str, Any]) -> str:
|
|
68
|
+
"""스키마 타입 문자열 생성"""
|
|
69
|
+
if '$ref' in schema:
|
|
70
|
+
ref_name = schema['$ref'].split('/')[-1]
|
|
71
|
+
return ref_name
|
|
72
|
+
|
|
73
|
+
schema_type = schema.get('type', 'object')
|
|
74
|
+
|
|
75
|
+
if schema_type == 'array':
|
|
76
|
+
items = schema.get('items', {})
|
|
77
|
+
item_type = get_schema_type(items, swagger)
|
|
78
|
+
return f"array<{item_type}>"
|
|
79
|
+
|
|
80
|
+
if 'enum' in schema:
|
|
81
|
+
return f"enum: [{', '.join(map(str, schema['enum']))}]"
|
|
82
|
+
|
|
83
|
+
format_type = schema.get('format')
|
|
84
|
+
if format_type:
|
|
85
|
+
return f"{schema_type} ({format_type})"
|
|
86
|
+
|
|
87
|
+
return schema_type
|
|
88
|
+
|
|
89
|
+
def generate_example(schema: Dict[str, Any], swagger: Dict[str, Any]) -> Any:
|
|
90
|
+
"""스키마로부터 예제 데이터 생성"""
|
|
91
|
+
if 'example' in schema:
|
|
92
|
+
return schema['example']
|
|
93
|
+
|
|
94
|
+
if 'examples' in schema:
|
|
95
|
+
return list(schema['examples'].values())[0] if schema['examples'] else None
|
|
96
|
+
|
|
97
|
+
if '$ref' in schema:
|
|
98
|
+
resolved = resolve_ref(schema['$ref'], swagger)
|
|
99
|
+
return generate_example(resolved, swagger)
|
|
100
|
+
|
|
101
|
+
schema_type = schema.get('type', 'object')
|
|
102
|
+
|
|
103
|
+
if schema_type == 'object':
|
|
104
|
+
properties = schema.get('properties', {})
|
|
105
|
+
example = {}
|
|
106
|
+
for prop_name, prop_schema in properties.items():
|
|
107
|
+
example[prop_name] = generate_example(prop_schema, swagger)
|
|
108
|
+
return example
|
|
109
|
+
|
|
110
|
+
if schema_type == 'array':
|
|
111
|
+
items = schema.get('items', {})
|
|
112
|
+
item_example = generate_example(items, swagger)
|
|
113
|
+
return [item_example] if item_example is not None else []
|
|
114
|
+
|
|
115
|
+
if schema_type == 'string':
|
|
116
|
+
if schema.get('format') == 'date-time':
|
|
117
|
+
return "2025-01-01T00:00:00Z"
|
|
118
|
+
if schema.get('format') == 'date':
|
|
119
|
+
return "2025-01-01"
|
|
120
|
+
if 'enum' in schema:
|
|
121
|
+
return schema['enum'][0]
|
|
122
|
+
return "string"
|
|
123
|
+
|
|
124
|
+
if schema_type == 'integer':
|
|
125
|
+
return 1
|
|
126
|
+
|
|
127
|
+
if schema_type == 'number':
|
|
128
|
+
return 1.0
|
|
129
|
+
|
|
130
|
+
if schema_type == 'boolean':
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def convert_to_markdown(swagger: Dict[str, Any], base_url: str) -> str:
|
|
136
|
+
"""Swagger JSON을 Markdown으로 변환"""
|
|
137
|
+
info = swagger.get('info', {})
|
|
138
|
+
title = info.get('title', 'API Documentation')
|
|
139
|
+
version = info.get('version', '1.0.0')
|
|
140
|
+
description = info.get('description', '')
|
|
141
|
+
|
|
142
|
+
md = f"""# {title} API 문서
|
|
143
|
+
|
|
144
|
+
> Generated from Swagger/OpenAPI
|
|
145
|
+
> Version: {version}
|
|
146
|
+
> Generated Date: {datetime.now().strftime('%Y-%m-%d')}
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 📋 목차
|
|
151
|
+
|
|
152
|
+
- [개요](#개요)
|
|
153
|
+
- [서버 정보](#서버-정보)
|
|
154
|
+
- [인증](#인증)
|
|
155
|
+
- [API 엔드포인트](#api-엔드포인트)
|
|
156
|
+
- [데이터 모델](#데이터-모델)
|
|
157
|
+
- [에러 코드](#에러-코드)
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 개요
|
|
162
|
+
|
|
163
|
+
**서비스명**: {title}
|
|
164
|
+
**버전**: {version}
|
|
165
|
+
**설명**: {description if description else 'No description provided'}
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 서버 정보
|
|
170
|
+
|
|
171
|
+
**Base URL**: `{base_url}`
|
|
172
|
+
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
# 인증 정보
|
|
176
|
+
security_schemes = {}
|
|
177
|
+
if 'components' in swagger and 'securitySchemes' in swagger['components']:
|
|
178
|
+
security_schemes = swagger['components']['securitySchemes']
|
|
179
|
+
elif 'securityDefinitions' in swagger:
|
|
180
|
+
security_schemes = swagger['securityDefinitions']
|
|
181
|
+
|
|
182
|
+
if security_schemes:
|
|
183
|
+
md += "---\n\n## 인증\n\n"
|
|
184
|
+
for scheme_name, scheme_def in security_schemes.items():
|
|
185
|
+
scheme_type = scheme_def.get('type', '')
|
|
186
|
+
if scheme_type == 'http' and scheme_def.get('scheme') == 'bearer':
|
|
187
|
+
md += f"""### Bearer Token
|
|
188
|
+
|
|
189
|
+
이 API는 Bearer Token 인증을 사용합니다.
|
|
190
|
+
|
|
191
|
+
**Header**:
|
|
192
|
+
```
|
|
193
|
+
Authorization: Bearer {{token}}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Example**:
|
|
197
|
+
```bash
|
|
198
|
+
curl -X GET "{base_url}/api/endpoint" \\
|
|
199
|
+
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
"""
|
|
203
|
+
elif scheme_type == 'apiKey':
|
|
204
|
+
key_name = scheme_def.get('name', 'X-API-Key')
|
|
205
|
+
in_location = scheme_def.get('in', 'header')
|
|
206
|
+
md += f"""### API Key
|
|
207
|
+
|
|
208
|
+
**{key_name}** ({in_location})
|
|
209
|
+
|
|
210
|
+
**Example**:
|
|
211
|
+
```bash
|
|
212
|
+
curl -X GET "{base_url}/api/endpoint" \\
|
|
213
|
+
-H "{key_name}: your-api-key"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
# API 엔드포인트
|
|
219
|
+
md += "---\n\n## API 엔드포인트\n\n"
|
|
220
|
+
|
|
221
|
+
paths = swagger.get('paths', {})
|
|
222
|
+
|
|
223
|
+
# 태그별로 그룹핑
|
|
224
|
+
endpoints_by_tag = {}
|
|
225
|
+
for path, path_item in paths.items():
|
|
226
|
+
for method, operation in path_item.items():
|
|
227
|
+
if method.upper() not in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']:
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
tags = operation.get('tags', ['Default'])
|
|
231
|
+
tag = tags[0] if tags else 'Default'
|
|
232
|
+
|
|
233
|
+
if tag not in endpoints_by_tag:
|
|
234
|
+
endpoints_by_tag[tag] = []
|
|
235
|
+
|
|
236
|
+
endpoints_by_tag[tag].append({
|
|
237
|
+
'path': path,
|
|
238
|
+
'method': method.upper(),
|
|
239
|
+
'operation': operation
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
# 태그별로 문서 생성
|
|
243
|
+
for tag, endpoints in sorted(endpoints_by_tag.items()):
|
|
244
|
+
md += f"### {tag}\n\n"
|
|
245
|
+
|
|
246
|
+
for endpoint in endpoints:
|
|
247
|
+
path = endpoint['path']
|
|
248
|
+
method = endpoint['method']
|
|
249
|
+
operation = endpoint['operation']
|
|
250
|
+
|
|
251
|
+
summary = operation.get('summary', '')
|
|
252
|
+
description_text = operation.get('description', '')
|
|
253
|
+
|
|
254
|
+
md += f"#### `{method} {path}`\n\n"
|
|
255
|
+
|
|
256
|
+
if summary:
|
|
257
|
+
md += f"{summary}\n\n"
|
|
258
|
+
|
|
259
|
+
if description_text:
|
|
260
|
+
md += f"**Description**: {description_text}\n\n"
|
|
261
|
+
|
|
262
|
+
# Parameters
|
|
263
|
+
parameters = operation.get('parameters', [])
|
|
264
|
+
if parameters:
|
|
265
|
+
md += "**Parameters**:\n\n"
|
|
266
|
+
md += "| Name | In | Type | Required | Description |\n"
|
|
267
|
+
md += "|------|-----|------|----------|-------------|\n"
|
|
268
|
+
|
|
269
|
+
for param in parameters:
|
|
270
|
+
name = param.get('name', '')
|
|
271
|
+
in_location = param.get('in', '')
|
|
272
|
+
required = '✅' if param.get('required', False) else '❌'
|
|
273
|
+
param_description = param.get('description', '')
|
|
274
|
+
|
|
275
|
+
param_schema = param.get('schema', {})
|
|
276
|
+
param_type = get_schema_type(param_schema, swagger)
|
|
277
|
+
|
|
278
|
+
md += f"| {name} | {in_location} | {param_type} | {required} | {param_description} |\n"
|
|
279
|
+
|
|
280
|
+
md += "\n"
|
|
281
|
+
|
|
282
|
+
# Request Body
|
|
283
|
+
request_body = operation.get('requestBody', {})
|
|
284
|
+
if request_body:
|
|
285
|
+
content = request_body.get('content', {})
|
|
286
|
+
if 'application/json' in content:
|
|
287
|
+
schema = content['application/json'].get('schema', {})
|
|
288
|
+
example = generate_example(schema, swagger)
|
|
289
|
+
|
|
290
|
+
md += "**Request Body**:\n\n"
|
|
291
|
+
md += "```json\n"
|
|
292
|
+
md += json.dumps(example, indent=2, ensure_ascii=False)
|
|
293
|
+
md += "\n```\n\n"
|
|
294
|
+
|
|
295
|
+
# Responses
|
|
296
|
+
responses = operation.get('responses', {})
|
|
297
|
+
if responses:
|
|
298
|
+
md += "**Responses**:\n\n"
|
|
299
|
+
|
|
300
|
+
for status_code, response in sorted(responses.items()):
|
|
301
|
+
response_description = response.get('description', '')
|
|
302
|
+
md += f"- **{status_code}** {response_description}\n\n"
|
|
303
|
+
|
|
304
|
+
content = response.get('content', {})
|
|
305
|
+
if 'application/json' in content:
|
|
306
|
+
schema = content['application/json'].get('schema', {})
|
|
307
|
+
example = generate_example(schema, swagger)
|
|
308
|
+
|
|
309
|
+
md += " ```json\n"
|
|
310
|
+
md += " " + json.dumps(example, indent=2, ensure_ascii=False).replace("\n", "\n ")
|
|
311
|
+
md += "\n ```\n\n"
|
|
312
|
+
|
|
313
|
+
md += "---\n\n"
|
|
314
|
+
|
|
315
|
+
# 데이터 모델
|
|
316
|
+
md += "## 데이터 모델\n\n"
|
|
317
|
+
|
|
318
|
+
schemas = {}
|
|
319
|
+
if 'components' in swagger and 'schemas' in swagger['components']:
|
|
320
|
+
schemas = swagger['components']['schemas']
|
|
321
|
+
elif 'definitions' in swagger:
|
|
322
|
+
schemas = swagger['definitions']
|
|
323
|
+
|
|
324
|
+
for schema_name, schema_def in sorted(schemas.items()):
|
|
325
|
+
md += f"### {schema_name}\n\n"
|
|
326
|
+
|
|
327
|
+
description_text = schema_def.get('description', '')
|
|
328
|
+
if description_text:
|
|
329
|
+
md += f"{description_text}\n\n"
|
|
330
|
+
|
|
331
|
+
example = generate_example(schema_def, swagger)
|
|
332
|
+
md += "```json\n"
|
|
333
|
+
md += json.dumps(example, indent=2, ensure_ascii=False)
|
|
334
|
+
md += "\n```\n\n"
|
|
335
|
+
|
|
336
|
+
properties = schema_def.get('properties', {})
|
|
337
|
+
required_fields = schema_def.get('required', [])
|
|
338
|
+
|
|
339
|
+
if properties:
|
|
340
|
+
md += "**Properties**:\n\n"
|
|
341
|
+
md += "| Field | Type | Required | Description |\n"
|
|
342
|
+
md += "|-------|------|----------|-------------|\n"
|
|
343
|
+
|
|
344
|
+
for prop_name, prop_schema in properties.items():
|
|
345
|
+
prop_type = get_schema_type(prop_schema, swagger)
|
|
346
|
+
is_required = '✅' if prop_name in required_fields else '❌'
|
|
347
|
+
prop_description = prop_schema.get('description', '')
|
|
348
|
+
|
|
349
|
+
md += f"| {prop_name} | {prop_type} | {is_required} | {prop_description} |\n"
|
|
350
|
+
|
|
351
|
+
md += "\n"
|
|
352
|
+
|
|
353
|
+
md += "---\n\n"
|
|
354
|
+
|
|
355
|
+
# 에러 코드
|
|
356
|
+
md += """## 에러 코드
|
|
357
|
+
|
|
358
|
+
### HTTP Status Codes
|
|
359
|
+
|
|
360
|
+
| Code | Name | Description |
|
|
361
|
+
|------|------|-------------|
|
|
362
|
+
| 200 | OK | Request succeeded |
|
|
363
|
+
| 201 | Created | Resource created successfully |
|
|
364
|
+
| 204 | No Content | Request succeeded with no response body |
|
|
365
|
+
| 400 | Bad Request | Invalid request parameters |
|
|
366
|
+
| 401 | Unauthorized | Missing or invalid authentication |
|
|
367
|
+
| 403 | Forbidden | Insufficient permissions |
|
|
368
|
+
| 404 | Not Found | Resource not found |
|
|
369
|
+
| 500 | Internal Server Error | Server error |
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## 링크
|
|
374
|
+
|
|
375
|
+
- **Swagger UI**: {base_url.replace('/v3/api-docs', '/swagger-ui.html')}
|
|
376
|
+
- **API Docs JSON**: {base_url}
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
> 📝 이 문서는 Swagger/OpenAPI 명세로부터 자동 생성되었습니다.
|
|
381
|
+
> 마지막 업데이트: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
return md
|
|
385
|
+
|
|
386
|
+
def main():
|
|
387
|
+
"""메인 함수"""
|
|
388
|
+
if len(sys.argv) < 2:
|
|
389
|
+
print("Usage: python swagger-to-markdown.py <swagger_url> [output_file]")
|
|
390
|
+
print("Example: python swagger-to-markdown.py http://localhost:8080/v3/api-docs")
|
|
391
|
+
sys.exit(1)
|
|
392
|
+
|
|
393
|
+
swagger_url = sys.argv[1]
|
|
394
|
+
output_file = sys.argv[2] if len(sys.argv) > 2 else None
|
|
395
|
+
|
|
396
|
+
print(f"🔍 Fetching Swagger docs from: {swagger_url}")
|
|
397
|
+
swagger = fetch_swagger(swagger_url)
|
|
398
|
+
|
|
399
|
+
base_url = get_base_url(swagger)
|
|
400
|
+
if not base_url:
|
|
401
|
+
# URL에서 base path 추출
|
|
402
|
+
parsed = urlparse(swagger_url)
|
|
403
|
+
base_url = f"{parsed.scheme}://{parsed.netloc}"
|
|
404
|
+
|
|
405
|
+
print(f"📝 Generating Markdown documentation...")
|
|
406
|
+
markdown = convert_to_markdown(swagger, base_url)
|
|
407
|
+
|
|
408
|
+
if output_file:
|
|
409
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
410
|
+
f.write(markdown)
|
|
411
|
+
print(f"✅ Documentation saved to: {output_file}")
|
|
412
|
+
else:
|
|
413
|
+
print(markdown)
|
|
414
|
+
|
|
415
|
+
if __name__ == '__main__':
|
|
416
|
+
main()
|
|
File without changes
|
/package/templates/skills/app-design/reference/{design-system-template.md → design-system.md}
RENAMED
|
File without changes
|