{"openapi":"3.1.0","info":{"title":"DisasterGPT API","description":"\n재난 대응 AI 챗봇 + 기상상황보고서 자동 생성 시스템.\n\n---\n\n## 인증\n\n`DISASTER_API_KEY` 환경변수가 설정된 경우 모든 `/api/*` 엔드포인트에 헤더가 필요합니다.\n\n```\nX-API-KEY: <your_key>\n```\n\n미설정 시 인증 없이 접근 가능합니다 (개발 모드).\n\n---\n\n## 채팅 파이프라인 (LangGraph)\n\n```\n사용자 질문\n  │\n  ▼\n[guard]       패턴 매칭 — 인사/감탄사 등 단순 발화 fast-path\n  │\n  ▼\n[analyzer]    LLM #1 — 의도 분석, 실행 계획 수립\n              → need_tools / need_rag / need_pdf 결정\n  │\n  ├─ tools/rag/pdf 필요 ──▶ [executor] ──▶ [synthesizer]\n  │                                              │\n  └─ 일반 대화 ──────────▶ [direct_response]    │\n                                                 ▼\n                                        [LLM #2 스트리밍]\n                                        EXAONE-4.0-32B (vLLM)\n                                        thinking: tool/RAG 시에만\n```\n\n### 채팅 SSE 이벤트 순서\n\n```\ndata: {\"type\":\"status\",\"content\":\"질문을 분석하고 있습니다...\"}\n\ndata: {\"type\":\"status\",\"content\":\"데이터를 수집하고 있습니다...\"}\ndata: {\"type\":\"tool_status\",\"tools\":[{\"tool\":\"기상청API\",\"data_source\":\"real\"}]}\n\ndata: {\"type\":\"refs\",\"refs\":[{\"docId\":0,\"name\":\"매뉴얼\",\"page\":12}]}\n\ndata: {\"type\":\"status\",\"content\":\"답변을 생성하고 있습니다...\"}\ndata: {\"type\":\"chunk\",\"content\":\"호우\",\"node\":\"final\"}\ndata: {\"type\":\"chunk\",\"content\":\"호우 특보\",\"node\":\"final\"}\n...\ndata: [DONE]\n```\n\n> `chunk.content`는 **누적 텍스트**입니다 (delta가 아님).\n\n---\n\n## 보고서 파이프라인 (WeatherReportGenerator)\n\nLangGraph를 우회하고 기상청 API를 직접 호출해 DOCX/PDF 보고서를 생성합니다.\n\n```\nPOST /api/report/generate\n  │\n  ├─ 기상특보    ← getWthrWrnMsg  (WthrWrnInfoService)\n  ├─ 기온        ← getUltraSrtNcst (VilageFcstInfoService)\n  ├─ 조석정보   ← GetTideFcstHghLwApiService\n  ├─ 적설량      ← getUltraSrtNcst\n  ├─ 기상전망(공군) ← UI 직접 입력\n  └─ 기상전망(기상청) ← getWthrInfo (WthrFcstInfoService)\n  │\n  └─ DOCX 렌더링 → done 이벤트 (report_id + 다운로드 URL)\n```\n\n### 보고서 SSE 이벤트 순서\n\n```\ndata: {\"type\":\"step\",\"step\":\"기상특보 수집\",\"status\":\"running\"}\ndata: {\"type\":\"step\",\"step\":\"기상특보 수집\",\"status\":\"done\",\"summary\":\"군산 기상특보 없음\"}\n...\ndata: {\"type\":\"review_flag\",\"message\":\"일부 모의 데이터 포함\"}\ndata: {\"type\":\"done\",\"report_id\":\"abc12345\",\"docx_url\":\"/api/report/download/abc12345?format=docx\",\"pdf_url\":\"...\",\"report_json\":{...}}\ndata: [DONE]\n```\n","contact":{"name":"DisasterGPT","url":"https://sw-kim.com/"},"license":{"name":"Internal"},"version":"3.1.0"},"paths":{"/health":{"get":{"tags":["시스템"],"summary":"헬스체크","operationId":"health_health_get","responses":{"200":{"description":"서버 상태 및 LangGraph 초기화 여부","content":{"application/json":{"schema":{}}}}}}},"/api/documents":{"get":{"tags":["문서"],"summary":"PDF 문서 목록 조회","description":"RAG 인덱스에 등록된 PDF 문서 목록을 반환합니다. `/api/chat/stream` 요청 시 `selected_pdf` 파라미터에 사용할 파일명을 확인할 수 있습니다.","operationId":"list_documents_api_documents_get","responses":{"200":{"description":"문서 목록 (id, name, filename, total_pages)","content":{"application/json":{"schema":{}}}}},"security":[{"APIKeyHeader":[]}]}},"/api/documents/{doc_id}/page/{page}":{"get":{"tags":["문서"],"summary":"PDF 페이지 이미지 조회","description":"지정한 PDF 문서의 특정 페이지를 1.5배 해상도 PNG로 렌더링해 base64로 반환합니다.\n\n- `doc_id`: `/api/documents` 응답의 `id` 값 (0-indexed)\n- `page`: 0-indexed 페이지 번호\n- 범위 초과 시 HTTP 404 반환","operationId":"get_document_page_api_documents__doc_id__page__page__get","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"doc_id","in":"path","required":true,"schema":{"type":"integer","title":"Doc Id"}},{"name":"page","in":"path","required":true,"schema":{"type":"integer","title":"Page"}}],"responses":{"200":{"description":"base64 PNG 이미지 및 페이지 정보","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/chat":{"post":{"tags":["채팅"],"summary":"채팅 단일 응답 (테스트용)","description":"LangGraph 파이프라인을 실행하고 최종 답변을 한 번에 반환합니다.\n\n> **주의**: 스트리밍이 없어 응답까지 수십 초가 걸릴 수 있습니다. 실제 서비스에서는 `/api/chat/stream`을 사용하세요.","operationId":"chat_api_chat_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatRequest"}}},"required":true},"responses":{"200":{"description":"LLM 최종 답변 및 thinking 내용","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"APIKeyHeader":[]}]}},"/api/chat/stream":{"post":{"tags":["채팅"],"summary":"채팅 SSE 스트리밍","description":"LangGraph 파이프라인을 실행하고 각 단계 결과를 SSE(Server-Sent Events)로 스트리밍합니다.\n\n**헤더**\n| 헤더 | 필수 | 설명 |\n|------|------|------|\n| `X-API-KEY` | 조건부 | `DISASTER_API_KEY` 설정 시 필수 |\n| `X-Trace-ID` | 선택 | 요청 추적 ID. 미전송 시 자동 생성 (로그에서 추적 가능) |\n\n**SSE 이벤트 타입**\n\n| type | 언제 | 페이로드 예시 |\n|------|------|--------------|\n| `status` | 파이프라인 각 단계 진입 시 | `{\"type\":\"status\",\"content\":\"질문을 분석하고 있습니다...\"}` |\n| `tool_status` | executor 완료 후 | `{\"type\":\"tool_status\",\"tools\":[{\"tool\":\"기상청API\",\"data_source\":\"real\"}]}` |\n| `refs` | RAG 결과 확정 후 (LLM 시작 전) | `{\"type\":\"refs\",\"refs\":[{\"docId\":0,\"name\":\"매뉴얼\",\"page\":12}]}` |\n| `chunk` | LLM 토큰 생성마다 | `{\"type\":\"chunk\",\"content\":\"호우 특보 발령 시...\",\"node\":\"final\"}` |\n| `error` | 오류 발생 시 | `{\"type\":\"error\",\"content\":\"...\"}` |\n| `[DONE]` | 스트림 종료 | `data: [DONE]` |\n\n> `chunk.content`는 **누적 텍스트**입니다 (delta가 아님). UI에서 그대로 교체(replace)하면 됩니다.\n\n**JavaScript 예제**\n```javascript\nconst res = await fetch('/api/chat/stream', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json', 'X-API-KEY': 'your_key' },\n  body: JSON.stringify({ question: '호우 대피 절차는?' }),\n});\nconst reader = res.body.getReader();\nconst decoder = new TextDecoder();\nlet buf = '';\nwhile (true) {\n  const { value, done } = await reader.read();\n  if (done) break;\n  buf += decoder.decode(value, { stream: true });\n  const lines = buf.split('\\n');\n  buf = lines.pop();\n  for (const line of lines) {\n    if (!line.startsWith('data:')) continue;\n    const raw = line.slice(5).trim();\n    if (raw === '[DONE]') return;\n    const ev = JSON.parse(raw);\n    if (ev.type === 'chunk') document.getElementById('answer').textContent = ev.content;\n  }\n}\n```\n\n**Python (httpx) 예제**\n```python\nimport httpx, json\n\nwith httpx.stream('POST', 'http://localhost:8001/api/chat/stream',\n                  json={'question': '호우 대피 절차는?'},\n                  headers={'X-API-KEY': 'your_key'}) as r:\n    buf = ''\n    for text in r.iter_text():\n        buf += text\n        while '\\n\\n' in buf:\n            event, buf = buf.split('\\n\\n', 1)\n            for line in event.splitlines():\n                if line.startswith('data:'):\n                    raw = line[5:].strip()\n                    if raw == '[DONE]': break\n                    ev = json.loads(raw)\n                    if ev['type'] == 'chunk':\n                        print(ev['content'], end='\\r')\n```","operationId":"chat_stream_api_chat_stream_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatRequest"}}},"required":true},"responses":{"200":{"description":"text/event-stream — SSE 이벤트 스트림","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"APIKeyHeader":[]}]}},"/api/report/edit-stream":{"post":{"tags":["보고서"],"summary":"조치사항 AI 편집 SSE","description":"보고서 내 조치사항 텍스트를 LLM으로 직접 편집합니다.\n\nLangGraph 파이프라인을 **우회**하고 EXAONE을 직접 호출하므로 응답이 빠릅니다.\n\n**SSE 이벤트**\n\n| type | 페이로드 | 설명 |\n|------|----------|------|\n| `chunk` | `{\"type\":\"chunk\",\"content\":\"수정된 조치사항 전문...\"}` | 누적 텍스트 |\n| `error` | `{\"type\":\"error\",\"content\":\"...\"}` | 오류 |\n| `[DONE]` | `data: [DONE]` | 스트림 종료 |\n\n> LLM은 수정된 조치사항 텍스트만 출력합니다 (설명·제목 없음).\n\n**JavaScript 예제**\n```javascript\nconst res = await fetch('/api/report/edit-stream', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({\n    current_plan: '1. 현장 점검반 출동\\n2. 배수펌프 가동',\n    weather_context: '기상특보: 호우주의보',\n    instruction: '호우주의보 내용을 반영하고 더 구체적으로 작성해줘',\n  }),\n});\n```","operationId":"report_edit_stream_api_report_edit_stream_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditRequest"}}},"required":true},"responses":{"200":{"description":"text/event-stream — chunk 이벤트로 수정된 조치사항 누적 전송","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"APIKeyHeader":[]}]}},"/api/report/generate":{"post":{"tags":["보고서"],"summary":"기상상황보고서 생성 SSE","description":"기상청 API를 직접 호출해 기상상황보고서(DOCX/PDF)를 생성합니다.\n\n각 데이터 수집 단계의 진행 상황을 SSE로 실시간 스트리밍합니다.\n\n**SSE 이벤트 타입**\n\n| type | 언제 | 페이로드 예시 |\n|------|------|--------------|\n| `step` | 각 수집 단계마다 | `{\"type\":\"step\",\"step\":\"기상특보 수집\",\"status\":\"running\"\\|\"done\"\\|\"error\",\"summary\":\"군산 호우주의보\"}` |\n| `review_flag` | Mock 데이터 포함 시 | `{\"type\":\"review_flag\",\"message\":\"일부 데이터가 모의(Mock) 데이터입니다.\"}` |\n| `done` | 생성 완료 | `{\"type\":\"done\",\"report_id\":\"abc12345\",\"report_json\":{...},\"docx_url\":\"/api/report/download/abc12345?format=docx\",\"pdf_url\":\"...\"}` |\n| `error` | 오류 | `{\"type\":\"error\",\"content\":\"...\"}` |\n| `[DONE]` | 스트림 종료 | `data: [DONE]` |\n\n**`report_json` 주요 필드**\n\n| 필드 | 설명 |\n|------|------|\n| `special_weather_alert` | 기상특보 내용 |\n| `temperature_range` | 기온 (예: `\"현재 기온: 15.2°C\"`) |\n| `tide_data` | 조석 정보 (rows 배열 포함) |\n| `snowfall` | 적설량 |\n| `forecast_airforce` | 공군 기상대 전망 |\n| `forecast_kma` | 기상청 서술형 전망 |\n| `action_plan` | LLM이 생성한 조치사항 |\n\n**완료 후 파일 다운로드**\n\n```\nGET /api/report/download/{report_id}?format=docx\nGET /api/report/download/{report_id}?format=pdf\n```","operationId":"report_generate_api_report_generate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportRequest"}}},"required":true},"responses":{"200":{"description":"text/event-stream — step/done 이벤트 스트림","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"APIKeyHeader":[]}]}},"/api/report/download/{report_id}":{"get":{"tags":["보고서"],"summary":"보고서 파일 다운로드","description":"`/api/report/generate` 완료 후 `done` 이벤트의 `report_id`로 파일을 다운로드합니다.\n\n- `format=docx` (기본값): Word 문서\n- `format=pdf`: PDF 파일\n\n`Content-Disposition: attachment` 헤더로 반환되어 브라우저에서 즉시 다운로드됩니다.","operationId":"report_download_api_report_download__report_id__get","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","default":"docx","title":"Format"}}],"responses":{"200":{"description":"DOCX 또는 PDF 바이너리 파일","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"ChatRequest":{"properties":{"question":{"type":"string","title":"Question","description":"사용자 질문","examples":["호우 특보 발령 시 현장 대응 절차는?"]},"history":{"anyOf":[{"items":{"additionalProperties":{"type":"string"},"type":"object"},"type":"array"},{"type":"null"}],"title":"History","description":"이전 대화 내역. 각 항목은 `{\"role\": \"user\"|\"assistant\", \"content\": \"...\"}` 형식.","default":[],"examples":[[{"content":"풍수해 대비 매뉴얼이 있나요?","role":"user"},{"content":"네, 풍수해 위기관리 매뉴얼에 따르면...","role":"assistant"}]]},"selected_pdf":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Selected Pdf","description":"RAG 검색 대상 PDF 파일명. `\"all\"` 이면 전체 검색.","default":"all","examples":["all","풍수해대응매뉴얼.pdf"]}},"type":"object","required":["question"],"title":"ChatRequest","examples":[{"summary":"기상 관련 질문 (RAG + Tool)","value":{"history":[],"question":"호우 특보 발령 시 현장 대응 절차는?","selected_pdf":"all"}},{"summary":"대화 히스토리 포함","value":{"history":[{"content":"호우 특보 기준이 뭐야?","role":"user"},{"content":"호우주의보는 3시간 강우량 60mm 이상...","role":"assistant"}],"question":"그럼 지하 시설 대피는 언제 해야 해?","selected_pdf":"all"}}]},"ChatResponse":{"properties":{"answer":{"type":"string","title":"Answer","description":"LLM 최종 답변 텍스트"},"reference_docs":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reference Docs","description":"참조 문서 (현재 미사용)","default":""},"thinking_content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thinking Content","description":"모델 내부 추론 과정 (EXAONE thinking)","default":""}},"type":"object","required":["answer"],"title":"ChatResponse"},"EditRequest":{"properties":{"current_plan":{"type":"string","title":"Current Plan","description":"현재 조치사항 텍스트 (LLM이 수정할 대상)","examples":["1. 각 읍면동 현장 점검반 출동\n2. 배수펌프 가동 준비"]},"weather_context":{"type":"string","title":"Weather Context","description":"기상 현황 컨텍스트 (보고서 데이터 요약). 빈 문자열 가능.","default":"","examples":["기상특보: 호우주의보 (군산)\n기온: 최고 18도\n기상청 전망: 내일 오전까지 비"]},"instruction":{"type":"string","title":"Instruction","description":"수정 요청 내용","examples":["호우주의보 내용을 첫 번째 항목에 반영해줘"]}},"type":"object","required":["current_plan","instruction"],"title":"EditRequest","examples":[{"summary":"조치사항 수정 요청","value":{"current_plan":"1. 각 읍면동 현장 점검반 출동\n2. 배수펌프 가동 준비","instruction":"호우주의보 내용을 반영하고 더 간결하게 다듬어줘","weather_context":"기상특보: 호우주의보 (군산)\n기온: 최고 18도"}}]},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ReportRequest":{"properties":{"template_id":{"type":"string","title":"Template Id","description":"보고서 템플릿 ID","default":"기상상황보고","examples":["기상상황보고"]},"base_datetime":{"type":"string","title":"Base Datetime","description":"보고서 기준 일시. 형식: `\"YYYY-MM-DD HH:MM\"`","examples":["2026-03-29 22:00"]},"airforce_forecast":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Airforce Forecast","description":"공군 기상대 전망 텍스트. 공개 API가 없어 UI에서 직접 입력합니다.","default":"","examples":["전라북도는 내일 오전까지 구름이 많고 오후부터 점차 맑아지겠습니다."]},"formats":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Formats","description":"생성할 포맷 목록. 미입력 시 [\"docx\",\"pdf\"]. 예: [\"docx\"] (DOCX만)","examples":[["docx","pdf"]]}},"type":"object","required":["base_datetime"],"title":"ReportRequest","examples":[{"summary":"기본 예시 (DOCX+PDF)","value":{"airforce_forecast":"전라북도는 내일 오전까지 구름이 많겠습니다.","base_datetime":"2026-03-29 22:00","template_id":"기상상황보고"}},{"summary":"DOCX만 생성 (PDF 스킵)","value":{"airforce_forecast":"","base_datetime":"2026-03-29 22:00","formats":["docx"],"template_id":"기상상황보고"}}]},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"APIKeyHeader":{"type":"apiKey","in":"header","name":"X-API-KEY"}}},"tags":[{"name":"채팅","description":"LangGraph 기반 재난 대응 RAG 채팅. SSE 스트리밍과 단일 응답을 모두 지원합니다."},{"name":"문서","description":"RAG에 사용되는 PDF 문서 목록 조회 및 페이지 이미지 반환."},{"name":"보고서","description":"기상상황보고서 생성·편집·다운로드. 기상청 API 직접 호출, LangGraph 우회."},{"name":"시스템","description":"헬스체크 등 서버 상태 확인."}]}