Appearance
AI
자연어로 물으면 무엇을 할지와 검색 조건을 돌려주는 API 와, MCP 엔드포인트를 제공합니다.
| AI 검색 | POST /healthcare/ai-search — 질문 → 실행할 툴 + 검색 조건 |
| 사용량 · 모델 | GET /ai/capabilities — 남은 몫과 고를 수 있는 모델 |
| MCP | POST /mcp/healthcare — 병원 검색 도구를 MCP 로 제공합니다 |
병원 목록이 아니라 검색 조건이 옵니다
ai-search 는 검색을 대신 해 주지 않습니다. 질문을 읽고 조건을 잡아 줄 뿐이고, 목록은 그 조건으로 여러분이 GET /healthcare/hospitals 를 한 번 더 불러 받습니다.
그래서 이렇게 쓸 수 있습니다.
- AI 가 무엇을 잡았는지 화면에 보여주고 사용자가 고치게 할 수 있습니다. 틀려도 다시 물을 필요 없이 조건 하나만 바꾸면 됩니다.
- 이미 만든 목록·지도·페이징을 그대로 씁니다. AI 응답 전용 화면을 따로 만들지 않습니다.
bash
curl -X POST "https://api.plzhans.com/healthcare/ai-search" \
-H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-H "Accept-Language: ko" \
-d '{ "q": "소아 천식 진료하는 병원" }'json
{
"tool": "search_hospitals",
"params": {
"filter": { "subjectCds": ["PD"], "asmItemCds": ["..."], "emergency": false, "baby": false },
"regionCd": "41450"
},
"conditions": [
{ "group": "subject", "names": ["소아청소년과"] },
{ "group": "assessment", "names": ["천식"] }
],
"explain": "소아청소년과 중 천식 진료 평가에서 1등급을 받은 병원입니다.",
"credits": 12000
}tool 로 갈라 쓰세요
응답의 tool 이 화면이 할 일입니다. 조건이 비어 있어도 할 일은 있을 수 있어서 (예: "근처 병원") 조건 개수로 판단하면 안 됩니다.
tool | 화면이 할 일 |
|---|---|
search_hospitals | params.filter + params.regionCd 로 병원 검색 |
search_nearby | 측위는 화면 몫입니다. 현재 위치를 얻어 거리순으로 검색 |
ask_location | 지역을 되묻습니다. params.placeText 에 사용자가 말한 표현이 있습니다 |
answer_medical | params.answer 를 그대로 보여줍니다(건강 질문에 답한 경우) |
reject | 검색하지 않습니다. params.reason 이 이유입니다 |
모르는 값은 넘기세요
tool 은 닫힌 목록이지만 늘어날 수 있습니다. switch 의 기본 가지에서 조용히 무시하면 새 값이 추가돼도 화면이 깨지지 않습니다.
conditions 는 잡힌 조건을 사람이 읽는 이름으로 푼 것입니다(요청 언어로 옵니다). 칩·태그로 그대로 그리면 되고, 코드표를 따로 부를 필요가 없습니다.
대화를 이으려면 돌려주세요
서버는 대화를 기억하지 않습니다. 이어서 물으려면 직전 응답을 다음 요청에 실어 보냅니다.
context— 직전 응답의params를 그대로 넣습니다. 지금까지 잡힌 조건이 이어집니다.history— 앞선 질문과 답. 최근 몇 마디만 넣으세요.
json
{
"q": "천안에서",
"context": { "filter": { "subjectCds": ["PD"] } }
}사용량
GET /ai/capabilities 로 남은 몫을 확인합니다. 아무것도 소모하지 않습니다.
- 화면을 열 때 한 번 부르는 용도입니다. 값이 바뀌는 때는 질문한 순간뿐이고, 그때는 검색 응답이 새 값을 싣고 옵니다 — 폴링하지 마세요.
- 사용량을 다 쓰면 검색은 503 으로 거절됩니다. 요청이 너무 잦으면 429 입니다 (둘은 안내가 달라야 합니다 — 429 는 잠시 뒤 다시, 503 은 기간이 바뀌어야 풀립니다).
응답이 늦을 수 있습니다
외부 모델을 부르므로 첫 질문은 수 초가 걸립니다(같은 질문은 캐시되어 즉시 옵니다). 타임아웃은 넉넉히 잡고, 실패하면 일반 검색으로 넘어갈 길을 남겨 두세요.
자연어 병원 검색
POST
/healthcare/ai-search
자연어 질문을 검색 조건으로 변환한다. 병원 목록은 반환하지 않는다 — 응답의 tool 로 동작을 고르고 params 를 실어 GET /healthcare/hospitals 를 호출한다.
좌표는 주고받지 않는다. 측위는 클라이언트가 하고, 서버는 위치 기준으로 검색할지 여부만 정한다.
진단·치료 조언은 하지 않는다. 병원을 고르기 위한 조건만 만든다.
Request
🔒 Bearer 인증 필요 · 인증 방법POST
/healthcare/ai-searchRequest Body
AiSearchRequest
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
q | string | required | maxLen 300 | 자연어 질문 |
model | string | optional | 쓸 모델. 안 보내면 서버 기본 모델. 허용 목록 밖이면 거절한다. | |
context | AiSearchParams | optional | 직전 응답의 `params` 를 그대로 돌려보낸다. 앞서 잡힌 조건을 이어받는 수단이다 — 서버는 대화를 기억하지 않는다. 새 주제를 시작하려면 빼고 보낸다. `answer`·`reason` 은 보내도 무시한다. | |
history | AiSearchHistoryTurn[] | optional | 앞서 오간 말. "아까 말한 증상" 처럼 앞을 가리키는 말을 푸는 데 쓴다 (`context` 는 조건만 담아 이것까지는 못 푼다). `answer` 를 보낼 때는 그 응답의 `answerSignature` 를 `signature` 로 같이 실어야 한다. 없거나 맞지 않으면 그 턴의 `answer` 만 버리고 `question` 은 그대로 쓴다. **최근 3마디만 쓴다**(더 보내면 앞쪽부터 버린다). 한 마디의 길이도 자른다 — 질문 300자, 답 800자. |
AiSearchParams
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
filter | AiSearchFilter | required | ||
↳subjectCds | string[] | required | 진료과목 코드(신고 기준) | |
↳specialistCds | string[] | required | 그 과목 전문의를 실제로 보유한 병원만 거는 진료과목 코드 | |
↳asmItemCds | string[] | required | 적정성평가 항목 코드. 그 항목 1등급(우수) 병원만 걸린다. | |
↳specialtyCds | string[] | required | 전문병원 지정분야 코드 | |
↳equipmentCds | string[] | required | 보유장비 코드 | |
↳classCds | string[] | required | 종별 코드 | |
↳tiers | string[] | required | 병원 등급. 비면 요양병원·정신병원이 제외된다. | |
↳emergency | boolean | required | 응급실 운영 병원만 | |
↳baby | boolean | required | 달빛어린이병원만 | |
↳name | string | optional | 병원 이름(부분 일치) | |
regionCd | string | optional | 시군구 코드(없으면 시도 코드). 검색의 `region` 파라미터다. **`search_hospitals` 에서만 채워진다.** | |
placeText | string | optional | 사용자가 쓴 지역 표현 원문(예: "강남역"). **코드가 아니다** — `ask_location` 이 되물을 때 화면에 그대로 보여준다. | |
reason | string | optional | off_topic, medical_question, emergency_suspected, medical_caution, unsupported_inverse, tertiary_referral, too_vague | `reject` 사유. `warnings` 와 같은 값 집합이다. |
answer | string | optional | `answer_medical` 의 본문. 다른 동작에서는 오지 않는다. |
AiSearchHistoryTurn
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
question | string | required | 사용자가 한 말 | |
answer | string | optional | 그 질문에 대한 응답. 답변이 있었던 턴에만 담는다. | |
signature | string | optional | `answer` 에 딸려 온 서명(응답의 `answerSignature`). **`answer` 를 보낼 거면 반드시 같이 보낸다.** |
Response
200
AiSearchResponse
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
tool | string | required | search_hospitals, search_nearby, ask_location, answer_medical, reject | 클라이언트가 실행할 동작. - `search_hospitals` `params.filter`(+`regionCd`)로 병원 검색 - `search_nearby` 현재 위치 기준 거리순 검색. 측위는 클라이언트가 한다 - `ask_location` 지역을 되묻는다. `params.placeText` 에 사용자가 쓴 표현이 있다 - `answer_medical` `params.answer` 를 그대로 보여준다 - `reject` 검색하지 않는다. `params.reason` 이 사유다 |
params | AiSearchParams | required | ||
warnings | string[] | required | off_topic, medical_question, emergency_suspected, medical_caution, unsupported_inverse, tertiary_referral, too_vague | 사용자에게 안내할 신호. - `off_topic` 병원 검색 범위 밖의 질문 - `medical_question` 건강 질문. 검색 조건으로 옮기지 않았다 - `emergency_suspected` 응급 징후. 119·응급실 안내가 필요하다 - `medical_caution` 요구가 일반적인 의학 권고와 어긋난다 - `unsupported_inverse` 검색 조건으로 표현할 수 없는 반대 조건이라 빠졌다 - `tertiary_referral` 상급종합병원은 진료의뢰서가 없으면 전액 본인 부담이다 - `too_vague` 조건을 잡기에 질문이 모호하다 |
explain | string | required | 조건이 맞게 잡혔는지 사용자가 확인할 한 문장 | |
dropped | string[] | required | 검증에서 제외된 값(`subject:XX` 꼴). 비어 있는 것이 정상이다. | |
conditions | AiSearchCondition[] | required | 잡힌 조건을 사람이 읽는 이름으로 푼 것. `params.filter` 의 코드와 같은 내용이라 코드표를 따로 조회할 필요가 없다. 등급·응급실처럼 코드표가 없는 값은 포함되지 않는다. | |
provider | string | required | anthropic, openai, local | |
model | string | required | 실제로 응답한 모델. 요청에 실은 이름이 아니라 확정된 버전이다. | |
credits | number | required | 이 요청이 사용한 양. `quota` 와 같은 단위라 그대로 견줄 수 있다. 같은 질문이면 언제 묻든 같은 값이다. | |
elapsedMs | number | required | 서버 처리 시간(ms). 네트워크 구간은 포함되지 않는다. | |
quota | Quota | optional | 사용량. 한도가 걸려 있지 않으면 오지 않는다. | |
answerSignature | string | optional | `params.answer` 의 서명. 답이 있을 때만 온다. 다음 요청의 `history[].signature` 로 그대로 돌려주면 그 답이 문맥으로 이어진다. |
AiSearchParams
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
filter | AiSearchFilter | required | ||
↳subjectCds | string[] | required | 진료과목 코드(신고 기준) | |
↳specialistCds | string[] | required | 그 과목 전문의를 실제로 보유한 병원만 거는 진료과목 코드 | |
↳asmItemCds | string[] | required | 적정성평가 항목 코드. 그 항목 1등급(우수) 병원만 걸린다. | |
↳specialtyCds | string[] | required | 전문병원 지정분야 코드 | |
↳equipmentCds | string[] | required | 보유장비 코드 | |
↳classCds | string[] | required | 종별 코드 | |
↳tiers | string[] | required | 병원 등급. 비면 요양병원·정신병원이 제외된다. | |
↳emergency | boolean | required | 응급실 운영 병원만 | |
↳baby | boolean | required | 달빛어린이병원만 | |
↳name | string | optional | 병원 이름(부분 일치) | |
regionCd | string | optional | 시군구 코드(없으면 시도 코드). 검색의 `region` 파라미터다. **`search_hospitals` 에서만 채워진다.** | |
placeText | string | optional | 사용자가 쓴 지역 표현 원문(예: "강남역"). **코드가 아니다** — `ask_location` 이 되물을 때 화면에 그대로 보여준다. | |
reason | string | optional | off_topic, medical_question, emergency_suspected, medical_caution, unsupported_inverse, tertiary_referral, too_vague | `reject` 사유. `warnings` 와 같은 값 집합이다. |
answer | string | optional | `answer_medical` 의 본문. 다른 동작에서는 오지 않는다. |
AiSearchCondition
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
group | string | required | subject, specialist, assessment, specialty, equipment, class | 묶음 이름. 화면이 이 값으로 "진료과"/"장비" 같은 앞말을 고른다. |
names | string[] | required | 사람이 읽는 이름들. **요청 언어**(Accept-Language)로 온다. |
Quota
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
app | AppQuota | optional | 앱 단위 사용량. | |
↳daily | QuotaWindow | optional | 일 단위 사용량. KST 자정에 리셋된다. | |
↳used | number | required | 사용한 양 | |
↳limit | number | required | 한도 | |
↳monthly | QuotaWindow | optional | 월 단위 사용량. 매월 1일에 리셋된다. `daily` 와 함께 걸리며, 먼저 소진되는 쪽이 적용된다. | |
↳used | number | required | 사용한 양 | |
↳limit | number | required | 한도 | |
user | UserQuota | optional | 사용자 단위 사용량. 로그인 상태에서만 온다. | |
↳balance | QuotaWindow | optional | 사용자 잔여량. 주기적으로 리셋되지 않는다. | |
↳used | number | required | 사용한 양 | |
↳limit | number | required | 한도 |
AI 사용량 · 모델 목록
GET
/ai/capabilities
남은 사용량과 선택할 수 있는 모델을 반환한다. 사용량을 소모하지 않는다.
AI 기능을 열 때 한 번 호출한다. 이후 값은 검색 응답(quota)에 실려 오므로 주기적으로 호출하지 않는다.
누구의 사용량인지는 자격증명이 정한다 — access token 이면 사용자, 클라이언트 키면 해당 앱의 몫이다.
quota 가 비어 오면 걸린 한도가 없다는 뜻이다. 사용량을 확인할 수 없는 상태면 503 이며, 이때는 검색도 거절되므로 AI 기능을 비활성화하는 것이 맞다.
Request
🔒 Bearer 인증 필요 · 인증 방법GET
/ai/capabilitiesResponse
200
CapabilitiesResponse
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
quota | Quota | optional | 사용량. 한도가 걸려 있지 않으면 오지 않는다. | |
models | ModelChoice[] | required | 선택할 수 있는 모델 목록. |
Quota
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
app | AppQuota | optional | 앱 단위 사용량. | |
↳daily | QuotaWindow | optional | 일 단위 사용량. KST 자정에 리셋된다. | |
↳used | number | required | 사용한 양 | |
↳limit | number | required | 한도 | |
↳monthly | QuotaWindow | optional | 월 단위 사용량. 매월 1일에 리셋된다. `daily` 와 함께 걸리며, 먼저 소진되는 쪽이 적용된다. | |
↳used | number | required | 사용한 양 | |
↳limit | number | required | 한도 | |
user | UserQuota | optional | 사용자 단위 사용량. 로그인 상태에서만 온다. | |
↳balance | QuotaWindow | optional | 사용자 잔여량. 주기적으로 리셋되지 않는다. | |
↳used | number | required | 사용한 양 | |
↳limit | number | required | 한도 |
ModelChoice
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
id | string | required | 모델 식별자. | |
locked | boolean | required | 선택할 수 없는 모델. 목록에는 표시되지만 요청에는 실리지 않는다. |
MCP
Model Context Protocol — Streamable HTTP 기반입니다. 인증만 우리 것이고(서비스 키), 나머지는 규약 그대로입니다.
도메인마다 엔드포인트가 하나입니다. 도구를 한 서버에 몰지 않는 이유는 도구 스키마가 매 턴 모델 컨텍스트에 실리기 때문입니다 — 쓰지 않는 도메인의 도구까지 짊어지지 않습니다.
| 엔드포인트 | 도메인 | 도구 |
|---|---|---|
https://api.plzhans.com/mcp/healthcare | 헬스케어 | search_hospitals · get_hospital · list_medical_codes |
공통
| 인증 | 서비스 키 (Authorization: Bearer sk_...) |
| 호출 한도 | 엔드포인트당 60초에 60회 |
| 언어 | 모든 도구가 lang 인자를 받습니다 (ko en ja zh, 기본 ko) |
코드 값은 도구 스키마에 enum 으로 실려 나갑니다 — 없는 코드는 클라이언트 단에서 걸립니다.
서비스 키로 붙습니다
MCP 클라이언트는 브라우저가 아니라 사용자 PC 에서 도는 프로그램이라 오리진 검사를 받지 않습니다. 그래서 X-Client-Id 가 아니라 서비스 키를 씁니다 — 키를 나눠 주는 것은 곧 호출 권한을 주는 것이니 배포용 키와 나눠 쓰세요.
/mcp/healthcare
search_hospitals
지역·진료과목·평가등급·응급실 여부로 검색합니다. 한 번에 최대 8건입니다.
| 인자 | 타입 | 설명 |
|---|---|---|
subjectCds | string[] (최대 5) | 진료과목 코드. 신고 기준이라 전문의 보유와는 다릅니다 |
specialistCds | string[] (최대 5) | 그 과목 전문의를 보유한 병원만 |
asmItemCds | string[] (최대 3) | 적정성평가 항목. 그 항목 1등급만 걸립니다 |
specialtyCds | string[] (최대 3) | 보건복지부 지정 전문병원 분야 |
tiers | string[] | 병원 등급. 비우면 요양병원·정신병원이 빠집니다 |
regionCd | string | 시도 또는 시군구 코드. 시도를 주면 하위 전체로 넓힙니다 |
name | string | 병원 이름(부분 일치) |
emergency · baby | boolean | 응급실 운영 / 달빛어린이병원만 |
get_hospital
병원 상세 — 진료시간·장비·병상·전문의 수·평가등급.
| 인자 | 타입 | 설명 |
|---|---|---|
id | number | search_hospitals 결과의 id |
없는 id 는 오류가 아니라 { "found": false } 로 옵니다.
list_medical_codes
검색에 쓰는 코드의 뜻.
| 인자 | 타입 | 설명 |
|---|---|---|
type | subject assessment specialty tier | 진료과목 · 적정성평가 · 전문병원 분야 · 병원 등급 |
적정성평가 1등급은 항목마다 뜻이 다릅니다
주사제 처방률 1등급은 "주사를 적게 놓는다" 는 뜻입니다. asmItemCds 를 쓰기 전에 항목의 의미를 확인하세요.