feat: drug_dur_check — 성분(ingredient) 기반 DUR 조회 추가
DURIrdntInfoService03(성분 서비스)도 승인돼 있었다. 안 되던 건 함정 때문: 서비스명은 03인데 오퍼레이션 접미사는 02(getUsjntTabooInfoList02), 필터 파라미터는 병용금기만 ingrKorName·나머지는 ingrName, 응답은 items[].item로 한 겹 더 감쌈. - ingredient 파라미터 추가 → 성분 서비스로 조회. 성분 단위라 결과가 깔끔하고 행 수가 1/10. 노인주의(getOdsnAtentInfoList02)는 성분 서비스에만 있음 - drug_name(제품명)은 기존대로 품목 서비스(*List03, itemName 부분일치) 유지 - 둘 중 하나 필수. 모델엔 "성분 알면 ingredient 권장"으로 안내 - 기본 checks에 노인주의 추가 실측: 심바스타틴 → 케토코나졸/마크로라이드/프로테아제억제제 병용금기 + 노인주의, 이소트레티노인 → 임부금기 1·2등급. 468개 테스트 통과. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HdchNLTHsyYahp2PVQjoym
This commit is contained in:
@@ -747,17 +747,17 @@ export function createBuildTools(isOrchestrationSkillEnabled: () => boolean) {
|
||||
function: {
|
||||
name: 'drug_dur_check',
|
||||
description:
|
||||
"식약처(MFDS) DUR 데이터로 특정 약의 병용금기·임부금기·연령금기·용량주의·효능군중복 등을 조회한다. " +
|
||||
"복용 중인 약 이름(제품명/브랜드명)을 주면 해당 성분에 등록된 DUR 경고를 성분·사유별로 정리해 준다. " +
|
||||
"제품명은 부분일치이므로 '타이레놀'처럼 아는 만큼만 넣으면 된다. 한국 허가 의약품 전용. " +
|
||||
"약 상호작용·금기 질문에는 web_search 대신 이 도구를 쓸 것. " +
|
||||
"식약처(MFDS) DUR 데이터로 약의 병용금기·임부금기·노인주의·연령금기·용량주의·효능군중복을 조회한다. " +
|
||||
"성분명(한글)을 알면 ingredient로 조회(결과가 깔끔하고 노인주의까지 나옴), 제품명만 알면 drug_name으로 조회(부분일치, 예: '타이레놀'). 둘 중 하나 필수. " +
|
||||
"한국 허가 의약품 전용. 약 상호작용·금기 질문에는 web_search 대신 이 도구를 쓸 것. " +
|
||||
"답변에는 '식약처 DUR 참고용이며 최종 판단은 약사·의사'라는 취지를 반드시 덧붙일 것.",
|
||||
parameters: {
|
||||
type: 'object', required: ['drug_name'],
|
||||
type: 'object', required: [],
|
||||
properties: {
|
||||
drug_name: { type: 'string', description: '제품명 또는 브랜드명 (부분일치). 예: "타이레놀", "쿠마딘정2mg"' },
|
||||
checks: { type: 'array', items: { type: 'string', enum: ['병용금기', '임부금기', '특정연령대금기', '용량주의', '투여기간주의', '효능군중복', '분할주의'] },
|
||||
description: '조회 항목. 생략 시 [병용금기, 임부금기, 특정연령대금기, 효능군중복]' },
|
||||
ingredient: { type: 'string', description: '성분명(한글). 예: "심바스타틴", "아세트아미노펜". 성분을 알면 이걸 권장' },
|
||||
drug_name: { type: 'string', description: '제품명/브랜드명 (부분일치). 예: "타이레놀". 성분을 모를 때' },
|
||||
checks: { type: 'array', items: { type: 'string', enum: ['병용금기', '임부금기', '노인주의', '특정연령대금기', '용량주의', '투여기간주의', '효능군중복', '분할주의'] },
|
||||
description: '조회 항목. 생략 시 [병용금기, 임부금기, 노인주의, 특정연령대금기, 효능군중복]' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+119
-88
@@ -2,41 +2,47 @@ import { ToolResult } from '../types.js';
|
||||
import { getConfig } from '../config/config.js';
|
||||
|
||||
/**
|
||||
* mfds.ts — 식약처(MFDS) 의약품 DUR 품목정보 조회
|
||||
* mfds.ts — 식약처(MFDS) 의약품 DUR 조회 (data.go.kr)
|
||||
*
|
||||
* data.go.kr `DURPrdlstInfoService03` (건강보험심사평가원·식약처 DUR).
|
||||
* 제품명(itemName) 부분일치로만 필터가 걸린다 — ingrName/typeName 파라미터는
|
||||
* 서버가 무시한다(2026-09-02 실측). 그래서 사용자가 준 약 이름을 itemName에
|
||||
* 그대로 넣고 부분일치로 찾는다.
|
||||
* 두 서비스를 함께 쓴다. 같은 인증키(vault `mfds.dur_api_key`)로 둘 다 승인됨.
|
||||
*
|
||||
* getOldSderTabooInfoList03(노인주의)는 "해당 오픈API 서비스가 없거나 폐기됨"으로
|
||||
* 응답하므로 목록에서 뺐다. 노인 관련 경고는 특정연령대금기(SpcifyAgrde)에 일부 포함.
|
||||
* 1) 품목(제품) 기반 DURPrdlstInfoService03 / *List03
|
||||
* - `itemName` 부분일치로만 필터. 제품명만 아는 사용자용.
|
||||
* - 한 경고가 제품×제품 조합마다 행이 나와서 수백 행 → 상대 성분+사유로 묶어야 함.
|
||||
*
|
||||
* 2) 성분 기반 DURIrdntInfoService03 / *List02 ← 오퍼레이션 접미사가 02다(서비스는 03).
|
||||
* - 병용금기만 `ingrKorName`, 나머지는 `ingrName`(2026-09-02 실측).
|
||||
* - 항목이 items[].item 로 한 겹 더 감싸여 있음. DEL_YN != '정상'은 삭제된 행.
|
||||
* - 성분 단위라 행 수가 훨씬 적고 깔끔. 노인주의는 이쪽에만 있음.
|
||||
*
|
||||
* 개인 복약 정리 보조용 — 처방·진단을 대체하지 않는다. 최종 판단은 약사·의사.
|
||||
*/
|
||||
|
||||
const BASE = 'https://apis.data.go.kr/1471000/DURPrdlstInfoService03';
|
||||
const BASE_PRDLST = 'https://apis.data.go.kr/1471000/DURPrdlstInfoService03';
|
||||
const BASE_IRDNT = 'https://apis.data.go.kr/1471000/DURIrdntInfoService03';
|
||||
const ROWS = 100;
|
||||
|
||||
interface CheckSpec {
|
||||
key: string;
|
||||
label: string;
|
||||
op: string;
|
||||
/** 병용금기는 상대 약(MIXTURE_*)이 핵심이라 별도 포맷 */
|
||||
prdOp?: string; // 품목 서비스 오퍼레이션 (없으면 품목모드에서 스킵)
|
||||
irdOp?: string; // 성분 서비스 오퍼레이션 (없으면 성분모드에서 스킵)
|
||||
irdParam?: 'ingrName' | 'ingrKorName';
|
||||
kind: 'interaction' | 'attention';
|
||||
}
|
||||
|
||||
const CHECKS: CheckSpec[] = [
|
||||
{ key: '병용금기', label: '병용금기 (같이 먹으면 안 되는 약)', op: 'getUsjntTabooInfoList03', kind: 'interaction' },
|
||||
{ key: '임부금기', label: '임부금기 (임신 중 금기)', op: 'getPwnmTabooInfoList03', kind: 'attention' },
|
||||
{ key: '특정연령대금기', label: '특정연령대금기 (소아·고령자 등)', op: 'getSpcifyAgrdeTabooInfoList03', kind: 'attention' },
|
||||
{ key: '용량주의', label: '용량주의 (1일 최대용량 등)', op: 'getCpctyAtentInfoList03', kind: 'attention' },
|
||||
{ key: '투여기간주의', label: '투여기간주의', op: 'getMdctnPdAtentInfoList03', kind: 'attention' },
|
||||
{ key: '효능군중복', label: '효능군중복 (성분이 겹치는 약)', op: 'getEfcyDplctInfoList03', kind: 'attention' },
|
||||
{ key: '분할주의', label: '분할주의 (쪼개 먹으면 안 되는 제형)', op: 'getSeobangjeongPartitnAtentInfoList03', kind: 'attention' },
|
||||
{ key: '병용금기', label: '병용금기 (같이 먹으면 안 되는 약)', prdOp: 'getUsjntTabooInfoList03', irdOp: 'getUsjntTabooInfoList02', irdParam: 'ingrKorName', kind: 'interaction' },
|
||||
{ key: '임부금기', label: '임부금기 (임신 중 금기)', prdOp: 'getPwnmTabooInfoList03', irdOp: 'getPwnmTabooInfoList02', irdParam: 'ingrName', kind: 'attention' },
|
||||
{ key: '노인주의', label: '노인주의', irdOp: 'getOdsnAtentInfoList02', irdParam: 'ingrName', kind: 'attention' },
|
||||
{ key: '특정연령대금기', label: '특정연령대금기 (소아 등)', prdOp: 'getSpcifyAgrdeTabooInfoList03', irdOp: 'getSpcifyAgrdeTabooInfoList02', irdParam: 'ingrName', kind: 'attention' },
|
||||
{ key: '용량주의', label: '용량주의 (1일 최대용량 등)', prdOp: 'getCpctyAtentInfoList03', irdOp: 'getCpctyAtentInfoList02', irdParam: 'ingrName', kind: 'attention' },
|
||||
{ key: '투여기간주의', label: '투여기간주의', prdOp: 'getMdctnPdAtentInfoList03', irdOp: 'getMdctnPdAtentInfoList02', irdParam: 'ingrName', kind: 'attention' },
|
||||
{ key: '효능군중복', label: '효능군중복 (성분이 겹치는 약)', prdOp: 'getEfcyDplctInfoList03', irdOp: 'getEfcyDplctInfoList02', irdParam: 'ingrName', kind: 'attention' },
|
||||
{ key: '분할주의', label: '분할주의 (쪼개 먹으면 안 되는 제형)', prdOp: 'getSeobangjeongPartitnAtentInfoList03', kind: 'attention' },
|
||||
];
|
||||
|
||||
const DEFAULT_CHECKS = ['병용금기', '임부금기', '특정연령대금기', '효능군중복'];
|
||||
const ROWS = 100;
|
||||
const DEFAULT_CHECKS = ['병용금기', '임부금기', '노인주의', '특정연령대금기', '효능군중복'];
|
||||
|
||||
function getApiKey(): string | undefined {
|
||||
try {
|
||||
@@ -52,23 +58,34 @@ function asArray(items: any): any[] {
|
||||
return Array.isArray(items) ? items : [items];
|
||||
}
|
||||
|
||||
/** "이트라코나졸고체분산체", "클래리트로마이신제피과립" → "이트라코나졸", "클래리트로마이신" */
|
||||
function baseIngr(name: string): string {
|
||||
return String(name || '')
|
||||
.replace(/(고체분산체|제피과립|정제|나트륨|칼륨|염산염|황산염|인산염|말레산염|타르타르산염|스테아르산염|수화물|무수물|프로필렌글리콜|스티노프레이트)+$/g, '')
|
||||
.trim() || String(name || '').trim();
|
||||
/** 성분 서비스는 각 행을 { item: {...} } 로 감싼다. 품목 서비스는 안 감싼다. */
|
||||
function unwrap(row: any): any {
|
||||
return row && typeof row === 'object' && 'item' in row && Object.keys(row).length === 1 ? row.item : row;
|
||||
}
|
||||
|
||||
async function callDur(op: string, itemName: string, apiKey: string): Promise<{ rows: any[]; total: number; error?: string }> {
|
||||
/** "이트라코나졸고체분산체", "클래리트로마이신제피과립" → "이트라코나졸" 등 */
|
||||
function baseIngr(name: string): string {
|
||||
const s = String(name || '').trim();
|
||||
return s
|
||||
.replace(/(고체분산체|제피과립|제피세립|정제|나트륨|칼륨|염산염|황산염|인산염|말레산염|타르타르산염|스테아르산염|수화물|무수물|프로필렌글리콜|스티노프레이트|\s*\(미분화\)|\s*과립)+$/g, '')
|
||||
.trim() || s;
|
||||
}
|
||||
|
||||
function clean(v: any): string {
|
||||
return String(v ?? '').trim().replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
async function callDur(base: string, op: string, param: string, value: string, apiKey: string):
|
||||
Promise<{ rows: any[]; total: number; error?: string }> {
|
||||
const p = new URLSearchParams();
|
||||
p.set('serviceKey', apiKey);
|
||||
p.set('type', 'json');
|
||||
p.set('pageNo', '1');
|
||||
p.set('numOfRows', String(ROWS));
|
||||
p.set('itemName', itemName);
|
||||
p.set(param, value);
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`${BASE}/${op}?${p}`, {
|
||||
res = await fetch(`${base}/${op}?${p}`, {
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
headers: { Accept: 'application/json', 'User-Agent': 'SmallClaw/1.0 dur-check' },
|
||||
});
|
||||
@@ -79,113 +96,127 @@ async function callDur(op: string, itemName: string, apiKey: string): Promise<{
|
||||
if (!res.ok) return { rows: [], total: 0, error: `HTTP ${res.status}` };
|
||||
let data: any;
|
||||
try { data = JSON.parse(text); } catch { return { rows: [], total: 0, error: `응답 파싱 실패: ${text.slice(0, 120)}` }; }
|
||||
// data.go.kr 공통 오류 포맷
|
||||
const authErr = data?.OpenAPI_ServiceResponse?.cmmMsgHeader?.returnAuthMsg
|
||||
|| data?.OpenAPI_ServiceResponse?.cmmMsgHeader?.errMsg;
|
||||
if (authErr) return { rows: [], total: 0, error: String(authErr) };
|
||||
const rc = data?.header?.resultCode;
|
||||
if (rc && rc !== '00') return { rows: [], total: 0, error: `${data?.header?.resultMsg || rc}` };
|
||||
return { rows: asArray(data?.body?.items), total: Number(data?.body?.totalCount || 0) };
|
||||
const rows = asArray(data?.body?.items).map(unwrap).filter((r) => r && r.DEL_YN !== '삭제');
|
||||
return { rows, total: Number(data?.body?.totalCount || 0) };
|
||||
}
|
||||
|
||||
function renderInteraction(spec: CheckSpec, rows: any[], total: number, selfHint: string): { text: string[]; data: any[] } {
|
||||
const groups = new Map<string, { partner: string; reason: string; example: string }>();
|
||||
for (const r of rows) {
|
||||
const partner = baseIngr(r.MIXTURE_INGR_KOR_NAME || r.MIXTURE_INGR_NAME || r.MIXTURE_INGR_ENG_NAME || '(미상)');
|
||||
const reason = clean(r.PROHBT_CONTENT) || '(사유 미기재)';
|
||||
const gk = `${partner}|${reason}`;
|
||||
if (!groups.has(gk)) groups.set(gk, { partner, reason, example: clean(r.MIXTURE_ITEM_NAME) });
|
||||
}
|
||||
const self = baseIngr(rows[0]?.INGR_KOR_NAME || rows[0]?.INGR_NAME || selfHint);
|
||||
const arr = [...groups.values()];
|
||||
const text = [`⛔ ${spec.label} — 성분 "${self}" 기준${total > rows.length ? ` (상위 ${rows.length}/${total}행)` : ''}`];
|
||||
for (const g of arr) text.push(` • ${g.partner} 계열과 병용 금지 — ${g.reason}${g.example ? ` (예: ${g.example})` : ''}`);
|
||||
return { text, data: arr.map((g) => ({ partner_ingredient: g.partner, reason: g.reason, example_product: g.example })) };
|
||||
}
|
||||
|
||||
function renderAttention(spec: CheckSpec, rows: any[], total: number): { text: string[]; data: any[] } {
|
||||
const groups = new Map<string, { ingr: string; reason: string; extra: string; example: string }>();
|
||||
for (const r of rows) {
|
||||
const ingr = clean(r.INGR_NAME || r.INGR_KOR_NAME) || '(미상)';
|
||||
const reason = clean(r.PROHBT_CONTENT);
|
||||
const extra = [r.AGE_BASE && `연령: ${clean(r.AGE_BASE)}`, r.GRADE && `등급: ${clean(r.GRADE)}`,
|
||||
r.SERS_NAME && `계열: ${clean(r.SERS_NAME)}`, r.REMARK && `[${clean(r.REMARK)}]`].filter(Boolean).join(', ');
|
||||
const gk = `${ingr}|${reason}|${extra}`;
|
||||
if (!groups.has(gk)) groups.set(gk, { ingr, reason, extra, example: clean(r.ITEM_NAME) });
|
||||
}
|
||||
const arr = [...groups.values()];
|
||||
const text = [`⚠️ ${spec.label}${total > rows.length ? ` (상위 ${rows.length}/${total}행)` : ''}`];
|
||||
for (const g of arr) {
|
||||
const bits = [g.ingr, g.reason || null, g.extra || null].filter(Boolean);
|
||||
text.push(` • ${bits.join(' — ')}${g.example ? ` (예: ${g.example})` : ''}`);
|
||||
}
|
||||
return { text, data: arr.map((g) => ({ ingredient: g.ingr, content: g.reason, note: g.extra, example_product: g.example })) };
|
||||
}
|
||||
|
||||
export const drugDurCheckTool = {
|
||||
name: 'drug_dur_check',
|
||||
description:
|
||||
"식약처(MFDS) 의약품 DUR 데이터로 특정 약의 병용금기·임부금기·연령금기·용량주의·효능군중복 등을 조회한다. " +
|
||||
"복용 중인 약 이름(제품명/브랜드명)을 주면 해당 성분에 등록된 DUR 경고를 성분·사유별로 정리해 돌려준다. " +
|
||||
"제품명은 부분일치로 찾으므로 '타이레놀', '아스피린프로텍트정100mg'처럼 아는 만큼만 넣으면 된다. " +
|
||||
"식약처(MFDS) DUR 데이터로 약의 병용금기·임부금기·노인주의·연령금기·용량주의·효능군중복을 조회한다. " +
|
||||
"성분명(한글)을 알면 `ingredient`로 조회하는 게 결과가 깔끔하고 노인주의까지 나온다. " +
|
||||
"제품명/브랜드명만 알면 `drug_name`으로 조회한다(부분일치, 예: '타이레놀'). 둘 중 하나는 필수. " +
|
||||
"한국 허가 의약품 전용. 이 데이터는 처방·진단을 대체하지 않으며 최종 판단은 약사·의사 몫임을 답변에 반드시 명시할 것. " +
|
||||
"약 상호작용·금기 질문에는 web_search 대신 이 도구를 쓸 것.",
|
||||
schema: {
|
||||
drug_name: '조회할 약의 제품명 또는 브랜드명 (부분일치). 예: "타이레놀", "쿠마딘정2mg"',
|
||||
checks: `조회 항목 배열. 생략 시 [${DEFAULT_CHECKS.join(', ')}]. 가능: ${CHECKS.map(c => c.key).join(', ')}`,
|
||||
ingredient: '성분명(한글). 예: "심바스타틴", "아세트아미노펜". 이게 있으면 성분 기반으로 조회(권장)',
|
||||
drug_name: '제품명/브랜드명 (부분일치). 예: "타이레놀". 성분을 모를 때 사용',
|
||||
checks: `조회 항목 배열. 생략 시 [${DEFAULT_CHECKS.join(', ')}]. 가능: ${CHECKS.map((c) => c.key).join(', ')}`,
|
||||
},
|
||||
jsonSchema: {
|
||||
type: 'object',
|
||||
required: ['drug_name'],
|
||||
properties: {
|
||||
ingredient: { type: 'string' },
|
||||
drug_name: { type: 'string' },
|
||||
checks: { type: 'array', items: { type: 'string', enum: CHECKS.map(c => c.key) } },
|
||||
checks: { type: 'array', items: { type: 'string', enum: CHECKS.map((c) => c.key) } },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
execute: async (args: any): Promise<ToolResult> => {
|
||||
const ingredient = String(args?.ingredient || '').trim();
|
||||
const drugName = String(args?.drug_name || '').trim();
|
||||
if (!drugName) return { success: false, error: 'drug_name(제품명)이 필요합니다.' };
|
||||
if (!ingredient && !drugName) return { success: false, error: 'ingredient(성분명) 또는 drug_name(제품명) 중 하나가 필요합니다.' };
|
||||
|
||||
const apiKey = getApiKey();
|
||||
if (!apiKey) {
|
||||
return { success: false, error: '식약처 DUR API 키 미설정. config.json에 { "mfds": { "dur_api_key": "..." } } 추가 (저장 시 자동 vault 이관).' };
|
||||
}
|
||||
|
||||
const byIngredient = !!ingredient;
|
||||
const query = ingredient || drugName;
|
||||
const want = asArray(args?.checks).map(String);
|
||||
const specs = CHECKS.filter(c => (want.length ? want.includes(c.key) : DEFAULT_CHECKS.includes(c.key)));
|
||||
if (!specs.length) return { success: false, error: `checks 값이 올바르지 않습니다. 가능: ${CHECKS.map(c => c.key).join(', ')}` };
|
||||
let specs = CHECKS.filter((c) => (want.length ? want.includes(c.key) : DEFAULT_CHECKS.includes(c.key)));
|
||||
// 품목 모드는 노인주의를 지원 안 함
|
||||
if (!byIngredient) specs = specs.filter((c) => c.prdOp);
|
||||
if (!specs.length) return { success: false, error: `조회 가능한 항목이 없습니다. 가능: ${CHECKS.map((c) => c.key).join(', ')}` };
|
||||
|
||||
const results = await Promise.all(specs.map(async (spec) => ({ spec, ...(await callDur(spec.op, drugName, apiKey)) })));
|
||||
const results = await Promise.all(specs.map(async (spec) => {
|
||||
if (byIngredient) {
|
||||
if (!spec.irdOp) return { spec, rows: [], total: 0, error: '성분 서비스 미지원 항목' };
|
||||
return { spec, ...(await callDur(BASE_IRDNT, spec.irdOp, spec.irdParam || 'ingrName', query, apiKey)) };
|
||||
}
|
||||
return { spec, ...(await callDur(BASE_PRDLST, spec.prdOp!, 'itemName', query, apiKey)) };
|
||||
}));
|
||||
|
||||
const lines: string[] = [`💊 DUR 조회: "${drugName}" (제품명 부분일치)`, ''];
|
||||
const lines: string[] = [`💊 DUR 조회: ${byIngredient ? `성분 "${query}"` : `제품명 "${query}" (부분일치)`}`, ''];
|
||||
const data: Record<string, any> = {};
|
||||
let anyHit = false;
|
||||
const errs: string[] = [];
|
||||
|
||||
for (const { spec, rows, total, error } of results) {
|
||||
if (error) { errs.push(`${spec.key}: ${error}`); continue; }
|
||||
if (error) { if (error !== '성분 서비스 미지원 항목') errs.push(`${spec.key}: ${error}`); continue; }
|
||||
if (!rows.length) { lines.push(`✅ ${spec.label}: 해당 없음`); data[spec.key] = []; continue; }
|
||||
anyHit = true;
|
||||
|
||||
if (spec.kind === 'interaction') {
|
||||
// 상대 성분 + 사유별로 묶는다 (제품 단위로는 수백 행이라 무의미)
|
||||
const groups = new Map<string, { partner: string; reason: string; example: string; count: number }>();
|
||||
for (const r of rows) {
|
||||
const partner = baseIngr(r.MIXTURE_INGR_KOR_NAME || r.MIXTURE_INGR_ENG_NAME || '(미상)');
|
||||
const reason = String(r.PROHBT_CONTENT || '').trim().replace(/\s+/g, ' ') || '(사유 미기재)';
|
||||
const gk = `${partner}|${reason}`;
|
||||
const g = groups.get(gk) || { partner, reason, example: r.MIXTURE_ITEM_NAME || '', count: 0 };
|
||||
g.count++;
|
||||
groups.set(gk, g);
|
||||
}
|
||||
const self = baseIngr(rows[0].INGR_KOR_NAME || rows[0].INGR_NAME || drugName);
|
||||
lines.push(`⛔ ${spec.label} — 성분 "${self}" 기준${total > rows.length ? ` (상위 ${rows.length}/${total}행)` : ''}`);
|
||||
const arr = [...groups.values()].sort((a, b) => b.count - a.count);
|
||||
for (const g of arr) lines.push(` • ${g.partner} 계열과 병용 금지 — ${g.reason}${g.example ? ` (예: ${g.example.trim()})` : ''}`);
|
||||
data[spec.key] = arr.map(g => ({ partner_ingredient: g.partner, reason: g.reason, example_product: g.example }));
|
||||
} else {
|
||||
// 성분 + 사유(+비고)별로 묶는다
|
||||
const groups = new Map<string, { ingr: string; reason: string; remark: string; example: string; count: number }>();
|
||||
for (const r of rows) {
|
||||
const ingr = String(r.INGR_NAME || r.INGR_KOR_NAME || '(미상)').trim();
|
||||
const reason = String(r.PROHBT_CONTENT || '').trim().replace(/\s+/g, ' ');
|
||||
const remark = String(r.REMARK || '').trim().replace(/\s+/g, ' ');
|
||||
const gk = `${ingr}|${reason}|${remark}`;
|
||||
const g = groups.get(gk) || { ingr, reason, remark, example: r.ITEM_NAME || '', count: 0 };
|
||||
g.count++;
|
||||
groups.set(gk, g);
|
||||
}
|
||||
lines.push(`⚠️ ${spec.label}${total > rows.length ? ` (상위 ${rows.length}/${total}행)` : ''}`);
|
||||
const arr = [...groups.values()];
|
||||
for (const g of arr) {
|
||||
const bits = [g.ingr, g.reason || null, g.remark ? `[${g.remark}]` : null].filter(Boolean);
|
||||
lines.push(` • ${bits.join(' — ')}${g.example ? ` (예: ${g.example.trim()})` : ''}`);
|
||||
}
|
||||
data[spec.key] = arr.map(g => ({ ingredient: g.ingr, content: g.reason, remark: g.remark, example_product: g.example }));
|
||||
}
|
||||
lines.push('');
|
||||
const r = spec.kind === 'interaction'
|
||||
? renderInteraction(spec, rows, total, query)
|
||||
: renderAttention(spec, rows, total);
|
||||
lines.push(...r.text, '');
|
||||
data[spec.key] = r.data;
|
||||
}
|
||||
|
||||
if (errs.length) { lines.push(''); lines.push(`(조회 실패: ${errs.join(' / ')})`); }
|
||||
lines.push('');
|
||||
lines.push('출처: 식약처 DUR 품목정보(data.go.kr). 이 정보는 참고용이며 처방·복약 조정은 반드시 약사·의사와 상의하세요.');
|
||||
if (errs.length) { lines.push('', `(조회 실패: ${errs.join(' / ')})`); }
|
||||
lines.push('', '출처: 식약처 DUR(data.go.kr). 참고용이며 처방·복약 조정은 반드시 약사·의사와 상의하세요.');
|
||||
|
||||
if (!anyHit && !errs.length) {
|
||||
return {
|
||||
success: true,
|
||||
stdout: `"${drugName}"에 대해 조회한 DUR 항목(${specs.map(s => s.key).join(', ')})에서 등록된 경고가 없습니다.\n` +
|
||||
`제품명이 정확하지 않으면 결과가 안 나올 수 있습니다 — 처방전·약봉투의 정확한 제품명으로 다시 시도해 보세요.\n\n` +
|
||||
`출처: 식약처 DUR 품목정보(data.go.kr).`,
|
||||
data: { drug_name: drugName, checks: data },
|
||||
stdout: `"${query}"에 대해 조회한 DUR 항목(${specs.map((s) => s.key).join(', ')})에서 등록된 경고가 없습니다.\n` +
|
||||
(byIngredient
|
||||
? `성분명 표기가 다르면(예: 염 형태) 결과가 안 나올 수 있습니다.\n`
|
||||
: `제품명이 정확하지 않으면 결과가 안 나옵니다 — 처방전·약봉투의 정확한 제품명, 또는 성분명(ingredient)으로 다시 시도해 보세요.\n`) +
|
||||
`\n출처: 식약처 DUR(data.go.kr).`,
|
||||
data: { query, by: byIngredient ? 'ingredient' : 'drug_name', checks: data },
|
||||
};
|
||||
}
|
||||
return { success: true, stdout: lines.join('\n'), data: { drug_name: drugName, checks: data } };
|
||||
return { success: true, stdout: lines.join('\n'), data: { query, by: byIngredient ? 'ingredient' : 'drug_name', checks: data } };
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user