alwaysgood commited on
Commit
d351ba7
·
verified ·
1 Parent(s): cee06b1

Create api_docs.py

Browse files
Files changed (1) hide show
  1. api_docs.py +514 -0
api_docs.py ADDED
@@ -0,0 +1,514 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API 문서 생성 모듈
3
+ API 엔드포인트 정보와 문서 생성 함수를 포함합니다.
4
+ """
5
+
6
+ import gradio as gr
7
+ import json
8
+
9
+ # API 엔드포인트별 상세 정보 정의
10
+ API_ENDPOINTS = {
11
+ "tide_level": {
12
+ "path": "/api/tide_level",
13
+ "title": "특정 시간 조위 조회",
14
+ "description": "지정한 관측소(`station_id`)의 특정 시간(`target_time`)에 대한 예측 조위 정보를 반환합니다. `target_time`을 지정하지 않으면 현재 시간에 가장 가까운 데이터를 반환합니다.",
15
+ "parameters": [
16
+ {"name": "station_id", "type": "string", "required": True, "description": "조회할 관측소의 고유 ID입니다. (예: 'DT_0001')"},
17
+ {"name": "target_time", "type": "string", "required": False, "description": "조회할 시간입니다. ISO 8601 형식(YYYY-MM-DDTHH:MM:SS)을 권장하며, 생략 시 현재 시간으로 자동 설정됩니다."}
18
+ ],
19
+ "example_params": {"station_id": "DT_0001", "target_time": "2025-08-10T09:00:00"},
20
+ "example_params_current": {"station_id": "DT_0001"},
21
+ "response_example": {
22
+ "success": True,
23
+ "timestamp": "2025-08-10T00:23:00.394870+09:00",
24
+ "meta": {
25
+ "obs_post_id": "DT_0001",
26
+ "obs_post_name": "인천",
27
+ "obs_lat": "37.452",
28
+ "obs_lon": "126.592",
29
+ "data_type": "prediction"
30
+ },
31
+ "data": {
32
+ "record_time": "2025-08-10T00:20:00+09:00",
33
+ "record_time_kst": "2025-08-10 00:20:00 KST",
34
+ "final_value": 138.8,
35
+ "residual_value": None,
36
+ "harmonic_value": 138.8,
37
+ "data_source": "harmonic_only",
38
+ "confidence": "medium",
39
+ "note": "잔차 예측이 없어 조화 예측만 제공됩니다",
40
+ "query_time": "2025-08-10 00:22:57 KST",
41
+ "matched_time_diff_seconds": 177.78439
42
+ }
43
+ }
44
+ },
45
+ "tide_series": {
46
+ "path": "/api/tide_series",
47
+ "title": "시계열 조위 데이터 조회",
48
+ "description": "지정된 기간 동안의 시계열 조위 데이터를 조회합니다. 공공 API와 유사한 형식으로 반환되며, 간격(interval)을 지정할 수 있습니다.",
49
+ "parameters": [
50
+ {"name": "station_id", "type": "string", "required": True, "description": "조회할 관측소의 고유 ID입니다."},
51
+ {"name": "start_time", "type": "string", "required": False, "description": "조회 시작 시간입니다. 생략 시 현재 시간부터 시작합니다."},
52
+ {"name": "end_time", "type": "string", "required": False, "description": "조회 종료 시간입니다. 생략 시 시작 시간으로부터 24시간 후까지 조회합니다."},
53
+ {"name": "interval", "type": "integer", "required": False, "description": "데이터 간격(분 단위). 기본값: 60분, 최소값: 5분"}
54
+ ],
55
+ "example_params": {"station_id": "DT_0001", "start_time": "2025-08-10T00:00:00", "end_time": "2025-08-11T00:00:00", "interval": 60},
56
+ "response_example": {
57
+ "success": True,
58
+ "timestamp": "2025-08-10T00:30:00.123456+09:00",
59
+ "meta": {
60
+ "obs_post_id": "DT_0001",
61
+ "obs_post_name": "인천",
62
+ "start_time": "2025-08-10T00:00:00+09:00",
63
+ "end_time": "2025-08-11T00:00:00+09:00",
64
+ "interval_minutes": 60,
65
+ "total_records": 25
66
+ },
67
+ "data": {
68
+ "tidal_obs": [
69
+ {
70
+ "record_time": "2025-08-10 00:00",
71
+ "pred_tide": 125.3,
72
+ "harmonic_tide": 125.3,
73
+ "residual_tide": None
74
+ },
75
+ {
76
+ "record_time": "2025-08-10 01:00",
77
+ "pred_tide": 142.7,
78
+ "harmonic_tide": 142.7,
79
+ "residual_tide": None
80
+ }
81
+ ]
82
+ }
83
+ }
84
+ },
85
+ "extremes": {
86
+ "path": "/api/extremes",
87
+ "title": "만조/간조 정보 조회",
88
+ "description": "특정 날짜의 만조(high tide)와 간조(low tide) 정보를 조회합니다. 주요 만조/간조와 부차 만조/간조를 구분하여 제공합니다.",
89
+ "parameters": [
90
+ {"name": "station_id", "type": "string", "required": True, "description": "조회할 관측소의 고유 ID입니다."},
91
+ {"name": "date", "type": "string", "required": False, "description": "조회할 날짜 (YYYY-MM-DD 형식). 생략 시 오늘 날짜로 설정됩니다."},
92
+ {"name": "include_secondary", "type": "boolean", "required": False, "description": "부차 만조/간조 포함 여부. 기본값: false"}
93
+ ],
94
+ "example_params": {"station_id": "DT_0001", "date": "2025-08-10", "include_secondary": True},
95
+ "response_example": {
96
+ "success": True,
97
+ "timestamp": "2025-08-10T00:35:00.123456+09:00",
98
+ "meta": {
99
+ "obs_post_id": "DT_0001",
100
+ "obs_post_name": "인천",
101
+ "date": "2025-08-10",
102
+ "include_secondary": True
103
+ },
104
+ "data": {
105
+ "high_tides": [
106
+ {
107
+ "time": "2025-08-10T06:15:00+09:00",
108
+ "time_kst": "2025-08-10 06:15:00 KST",
109
+ "level": 812.5,
110
+ "type": "primary"
111
+ },
112
+ {
113
+ "time": "2025-08-10T18:45:00+09:00",
114
+ "time_kst": "2025-08-10 18:45:00 KST",
115
+ "level": 798.3,
116
+ "type": "primary"
117
+ }
118
+ ],
119
+ "low_tides": [
120
+ {
121
+ "time": "2025-08-10T00:30:00+09:00",
122
+ "time_kst": "2025-08-10 00:30:00 KST",
123
+ "level": 98.2,
124
+ "type": "primary"
125
+ },
126
+ {
127
+ "time": "2025-08-10T12:45:00+09:00",
128
+ "time_kst": "2025-08-10 12:45:00 KST",
129
+ "level": 112.7,
130
+ "type": "primary"
131
+ }
132
+ ]
133
+ }
134
+ }
135
+ },
136
+ "alert": {
137
+ "path": "/api/alert",
138
+ "title": "위험 수위 체크",
139
+ "description": "향후 지정된 시간 동안 주의 수위 또는 경고 수위에 도달하는지 확인합니다. 위험 시점과 예상 수위를 반환합니다.",
140
+ "parameters": [
141
+ {"name": "station_id", "type": "string", "required": True, "description": "체크할 관측소의 고유 ID입니다."},
142
+ {"name": "hours_ahead", "type": "integer", "required": False, "description": "확인할 시간 범위(시간 단위). 기본값: 24시간, 최대: 72시간"},
143
+ {"name": "warning_level", "type": "number", "required": False, "description": "주의 수위(cm). 기본값: 700cm"},
144
+ {"name": "danger_level", "type": "number", "required": False, "description": "경고 수위(cm). 기본값: 750cm"}
145
+ ],
146
+ "example_params": {"station_id": "DT_0001", "hours_ahead": 24, "warning_level": 700, "danger_level": 750},
147
+ "response_example": {
148
+ "success": True,
149
+ "timestamp": "2025-08-10T00:40:00.123456+09:00",
150
+ "meta": {
151
+ "obs_post_id": "DT_0001",
152
+ "obs_post_name": "인천",
153
+ "check_period": {
154
+ "start": "2025-08-10T00:40:00+09:00",
155
+ "end": "2025-08-11T00:40:00+09:00"
156
+ },
157
+ "warning_level": 700,
158
+ "danger_level": 750
159
+ },
160
+ "data": {
161
+ "alert_status": "DANGER",
162
+ "max_level": 812.5,
163
+ "max_level_time": "2025-08-10T06:15:00+09:00",
164
+ "warning_events": [
165
+ {
166
+ "time": "2025-08-10T05:30:00+09:00",
167
+ "level": 702.3,
168
+ "type": "warning_exceeded"
169
+ }
170
+ ],
171
+ "danger_events": [
172
+ {
173
+ "time": "2025-08-10T06:00:00+09:00",
174
+ "level": 755.8,
175
+ "type": "danger_exceeded"
176
+ }
177
+ ],
178
+ "recommendation": "경고 수위 초과 예상. 해안가 접근 주의 필요"
179
+ }
180
+ }
181
+ },
182
+ "compare": {
183
+ "path": "/api/compare",
184
+ "title": "다중 관측소 비교",
185
+ "description": "여러 관측소의 조위를 동시에 비교합니다. 지정한 시간의 각 관측소별 조위 정보를 한 번에 조회할 수 있습니다.",
186
+ "parameters": [
187
+ {"name": "station_ids", "type": "array", "required": True, "description": "비교할 관측소 ID 목록 (배열 형태). 예: ['DT_0001', 'DT_0002']"},
188
+ {"name": "target_time", "type": "string", "required": False, "description": "비교할 시간. 생략 시 현재 시간으로 설정됩니다."}
189
+ ],
190
+ "example_params": {"station_ids": ["DT_0001", "DT_0002", "DT_0003"], "target_time": "2025-08-10T09:00:00"},
191
+ "response_example": {
192
+ "success": True,
193
+ "timestamp": "2025-08-10T00:45:00.123456+09:00",
194
+ "meta": {
195
+ "target_time": "2025-08-10T09:00:00+09:00",
196
+ "station_count": 3
197
+ },
198
+ "data": {
199
+ "comparisons": [
200
+ {
201
+ "station_id": "DT_0001",
202
+ "station_name": "인천",
203
+ "tide_level": 425.3,
204
+ "data_time": "2025-08-10T09:00:00+09:00"
205
+ },
206
+ {
207
+ "station_id": "DT_0002",
208
+ "station_name": "안흥",
209
+ "tide_level": 312.8,
210
+ "data_time": "2025-08-10T09:00:00+09:00"
211
+ },
212
+ {
213
+ "station_id": "DT_0003",
214
+ "station_name": "보령",
215
+ "tide_level": 298.5,
216
+ "data_time": "2025-08-10T09:00:00+09:00"
217
+ }
218
+ ],
219
+ "statistics": {
220
+ "max_level": 425.3,
221
+ "max_station": "인천",
222
+ "min_level": 298.5,
223
+ "min_station": "보령",
224
+ "avg_level": 345.5
225
+ }
226
+ }
227
+ }
228
+ },
229
+ "health": {
230
+ "path": "/api/health",
231
+ "title": "시스템 상태 확인",
232
+ "description": "API 서버 및 연결된 시스템의 상태를 확인합니다. 데이터베이스 연결, API 키 설정 등을 점검합니다.",
233
+ "parameters": [],
234
+ "example_params": {},
235
+ "response_example": {
236
+ "success": True,
237
+ "timestamp": "2025-08-10T00:50:00.123456+09:00",
238
+ "status": "healthy",
239
+ "services": {
240
+ "api_server": "running",
241
+ "supabase": "connected",
242
+ "gemini_api": "configured",
243
+ "predictions": "available"
244
+ },
245
+ "uptime": "2 hours 15 minutes",
246
+ "version": "1.0.0"
247
+ }
248
+ }
249
+ }
250
+
251
+
252
+ def copy_to_clipboard(text):
253
+ """클립보드에 텍스트 복사 (JavaScript 실행)"""
254
+ return f"""
255
+ <script>
256
+ navigator.clipboard.writeText('{text}');
257
+ alert('URL이 클립보드에 복사되었습니다!');
258
+ </script>
259
+ """
260
+
261
+
262
+ def generate_api_docs(endpoint_key: str):
263
+ """엔드포인트별 상세 API 문서를 생성합니다."""
264
+
265
+ base_url = "https://alwaysgood-my-tide-env.hf.space"
266
+ endpoint_info = API_ENDPOINTS[endpoint_key]
267
+
268
+ # 섹션 제목
269
+ gr.Markdown("---")
270
+ gr.Markdown(f"## 📚 API 사용 안내서")
271
+ gr.Markdown(f"### `{endpoint_info['path']}` : {endpoint_info['title']}")
272
+ gr.Markdown(endpoint_info['description'])
273
+
274
+ # 기본 정보
275
+ gr.Markdown("- **Method**: `GET`")
276
+ gr.Markdown(f"- **URL**: `{base_url}{endpoint_info['path']}`")
277
+ gr.Markdown("")
278
+
279
+ # 요청 파라미터 테이블
280
+ if endpoint_info['parameters']:
281
+ gr.Markdown("### 요청 파라미터 (Query Parameters)")
282
+
283
+ # 테이블 헤더
284
+ table_md = "|파라미터 (Parameter)|타입 (Type)|필수 (Required)|설명 (Description)|\n"
285
+ table_md += "|---|---|---|---|\n"
286
+
287
+ # 파라미터 정보
288
+ for param in endpoint_info['parameters']:
289
+ required_text = "**Yes**" if param['required'] else "No"
290
+ table_md += f"|`{param['name']}`|`{param['type']}`|{required_text}|{param['description']}|\n"
291
+
292
+ gr.Markdown(table_md)
293
+ gr.Markdown("")
294
+
295
+ # 사용 예시
296
+ gr.Markdown("### 사용 예시 (Usage Examples)")
297
+
298
+ # Python 예시
299
+ python_code = f'''import requests
300
+ import json
301
+
302
+ BASE_URL = "{base_url}"
303
+ '''
304
+
305
+ # 엔드포인트별 특별 처리
306
+ if endpoint_key == "tide_level":
307
+ python_code += f'''
308
+ # 1. 현재 조위 조회 (인천 관측소)
309
+ params_now = {{
310
+ "station_id": "DT_0001"
311
+ }}
312
+
313
+ response_now = requests.get(f"{{BASE_URL}}{endpoint_info['path']}", params=params_now)
314
+ print("--- 현재 조위 조회 결과 ---")
315
+ print(response_now.json())
316
+
317
+ # 2. 특정 시간 조위 조회 (2025년 8월 10일 오전 9시)
318
+ params_specific_time = {{
319
+ "station_id": "DT_0001",
320
+ "target_time": "2025-08-10T09:00:00"
321
+ }}
322
+
323
+ response_specific = requests.get(f"{{BASE_URL}}{endpoint_info['path']}", params=params_specific_time)
324
+ print("\\n--- 특정 시간 조위 조회 결과 ---")
325
+ print(response_specific.json())'''
326
+
327
+ elif endpoint_key == "compare":
328
+ python_code += f'''
329
+ # 여러 관측소 동시 비교
330
+ params = {{
331
+ "station_ids": ["DT_0001", "DT_0002", "DT_0003"],
332
+ "target_time": "2025-08-10T09:00:00"
333
+ }}
334
+
335
+ response = requests.get(f"{{BASE_URL}}{endpoint_info['path']}", params=params)
336
+
337
+ if response.status_code == 200:
338
+ data = response.json()
339
+ print("--- 관측소별 조위 비교 ---")
340
+ for station in data['data']['comparisons']:
341
+ print(f"{{station['station_name']}}: {{station['tide_level']}}cm")
342
+ else:
343
+ print(f"Error: {{response.status_code}}")'''
344
+
345
+ elif endpoint_key == "health":
346
+ python_code += f'''
347
+ # 시스템 상태 확인
348
+ response = requests.get(f"{{BASE_URL}}{endpoint_info['path']}")
349
+
350
+ if response.status_code == 200:
351
+ data = response.json()
352
+ print(f"시스템 상태: {{data['status']}}")
353
+ print(f"서비스 상태: {{data['services']}}")
354
+ else:
355
+ print(f"Error: {{response.status_code}}")'''
356
+
357
+ else:
358
+ # 일반적인 경우
359
+ python_code += f'''
360
+ # 요청 파라미터 설정
361
+ params = {json.dumps(endpoint_info['example_params'], indent=4, ensure_ascii=False)}
362
+
363
+ response = requests.get(f"{{BASE_URL}}{endpoint_info['path']}", params=params)
364
+
365
+ if response.status_code == 200:
366
+ data = response.json()
367
+ print(json.dumps(data, indent=2, ensure_ascii=False))
368
+ else:
369
+ print(f"Error: {{response.status_code}}")'''
370
+
371
+ gr.Markdown("#### **Python (`requests` 사용)**")
372
+ gr.Markdown(f"```python\n{python_code.strip()}\n```")
373
+
374
+ # curl 예시
375
+ # URL 파라미터 생성
376
+ if endpoint_key == "compare":
377
+ # 배열 파라미터는 특별 처리
378
+ curl_params = "&".join([f"station_ids={sid}" for sid in endpoint_info['example_params']['station_ids']])
379
+ if 'target_time' in endpoint_info['example_params']:
380
+ curl_params += f"&target_time={endpoint_info['example_params']['target_time']}"
381
+ elif endpoint_key == "health":
382
+ curl_params = ""
383
+ else:
384
+ curl_params = "&".join([f"{k}={v}" for k, v in endpoint_info['example_params'].items()])
385
+
386
+ curl_url = f"{base_url}{endpoint_info['path']}"
387
+ if curl_params:
388
+ curl_url += f"?{curl_params}"
389
+
390
+ curl_code = f'# 요청 예시\ncurl -X GET "{curl_url}"'
391
+
392
+ # 특별 케이스 추가
393
+ if endpoint_key == "tide_level":
394
+ curl_code = f'''# 현재 조위 조회
395
+ curl -X GET "{base_url}{endpoint_info['path']}?station_id=DT_0001"
396
+
397
+ # 특정 시간 조위 조회
398
+ curl -X GET "{base_url}{endpoint_info['path']}?station_id=DT_0001&target_time=2025-08-10T09:00:00"'''
399
+
400
+ gr.Markdown("#### **curl (Command Line)**")
401
+ gr.Markdown(f"```bash\n{curl_code}\n```")
402
+
403
+ # JavaScript 예시
404
+ if endpoint_key == "compare":
405
+ js_code = f'''const stationIds = ['DT_0001', 'DT_0002', 'DT_0003'];
406
+ const params = new URLSearchParams();
407
+ stationIds.forEach(id => params.append('station_ids', id));
408
+ params.append('target_time', '2025-08-10T09:00:00');
409
+
410
+ const url = `{base_url}{endpoint_info['path']}?${{params}}`;
411
+
412
+ fetch(url)
413
+ .then(response => response.json())
414
+ .then(data => {{
415
+ console.log('비교 결과:', data);
416
+ data.data.comparisons.forEach(station => {{
417
+ console.log(`${{station.station_name}}: ${{station.tide_level}}cm`);
418
+ }});
419
+ }})
420
+ .catch(error => {{
421
+ console.error('Error:', error);
422
+ }});'''
423
+
424
+ elif endpoint_key == "health":
425
+ js_code = f'''const url = '{base_url}{endpoint_info['path']}';
426
+
427
+ fetch(url)
428
+ .then(response => response.json())
429
+ .then(data => {{
430
+ console.log('시스템 상태:', data.status);
431
+ console.log('서비스:', data.services);
432
+ }})
433
+ .catch(error => {{
434
+ console.error('Error:', error);
435
+ }});'''
436
+
437
+ else:
438
+ # 일반 케이스
439
+ params_str = "&".join([f"{k}={v}" for k, v in endpoint_info.get('example_params_current', endpoint_info['example_params']).items()])
440
+ js_code = f'''const stationId = 'DT_0001';
441
+ const url = `{base_url}{endpoint_info['path']}?{params_str}`;
442
+
443
+ fetch(url)
444
+ .then(response => response.json())
445
+ .then(data => {{
446
+ console.log(data);
447
+ }})
448
+ .catch(error => {{
449
+ console.error('Error:', error);
450
+ }});'''
451
+
452
+ gr.Markdown("#### **JavaScript (`fetch` API)**")
453
+ gr.Markdown(f"```javascript\n{js_code}\n```")
454
+
455
+ # 브라우저 직접 접속 - 복사 버튼 추가
456
+ gr.Markdown("#### **웹 브라우저**")
457
+ gr.Markdown("아래 주소를 복사하여 웹 브라우저 주소창에 붙여넣기만 해도 결과를 확인할 수 있습니다.")
458
+
459
+ browser_params = ""
460
+ if endpoint_key != "health":
461
+ if endpoint_key == "compare":
462
+ browser_params = "?station_ids=DT_0001&station_ids=DT_0002"
463
+ else:
464
+ browser_params = "?" + "&".join([f"{k}={v}" for k, v in endpoint_info.get('example_params_current', {'station_id': 'DT_0001'}).items()])
465
+
466
+ browser_url = f"{base_url}{endpoint_info['path']}{browser_params}"
467
+
468
+ with gr.Row():
469
+ url_textbox = gr.Textbox(
470
+ value=browser_url,
471
+ interactive=False,
472
+ show_label=False,
473
+ scale=4
474
+ )
475
+ copy_btn = gr.Button("📋 복사", scale=1, size="sm")
476
+
477
+ # 복사 버튼 클릭 이벤트
478
+ copy_btn.click(
479
+ fn=lambda x: x, # 단순히 URL을 반환
480
+ inputs=[url_textbox],
481
+ outputs=[],
482
+ js=f"""(x) => {{
483
+ navigator.clipboard.writeText('{browser_url}');
484
+ alert('URL이 클립보드에 복사되었습니다!');
485
+ return x;
486
+ }}"""
487
+ )
488
+
489
+ # 응답 형식
490
+ gr.Markdown("")
491
+ gr.Markdown("### 응답 형식 (Response Format)")
492
+
493
+ # 성공 응답
494
+ gr.Markdown("#### **성공 (200 OK)**")
495
+ response_json = json.dumps(endpoint_info['response_example'], indent=2, ensure_ascii=False)
496
+ gr.Markdown(f"```json\n{response_json}\n```")
497
+
498
+ # 실패 응답 (파라미터가 필요한 경우에만)
499
+ if any(p['required'] for p in endpoint_info['parameters']):
500
+ gr.Markdown("#### **실패 (422 Unprocessable Entity - 파라미터 누락 시)**")
501
+ error_response = '''{
502
+ "detail": [
503
+ {
504
+ "type": "missing",
505
+ "loc": [
506
+ "query",
507
+ "station_id"
508
+ ],
509
+ "msg": "Field required",
510
+ "input": null
511
+ }
512
+ ]
513
+ }'''
514
+ gr.Markdown(f"```json\n{error_response}\n```")