Skip to content

About

약궁합 — 같이 먹어도 되는 약인지 확인하는 앱. 식약처 DUR 병용금기 기준으로 조회하고, 약 이름은 서버로 보내지 않습니다 — Apps in Toss WebView 미니앱

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

💊 약궁합 (pill-check)

같이 먹어도 되는 약 — 지금 드시는 약을 담으면, 식약처가 함께 먹지 말라고 정해 둔 조합이 있는지 찾아드려요

React TypeScript Vite Apps in Toss

토스 앱 속 WebView 미니앱 · 비게임 · 광고 수익형 · 서버 없음


🎯 한눈에 보기

약 이름을 두 글자만 쳐서 담고 → 두 개 이상 담기면 확인 버튼 하나 → 식약처가 병용금기로 정해 둔 조합이면 왜 안 되는지 사유까지 → 자주 먹는 묶음은 조합으로 저장해 다음에 한 번에 불러오는 흐름.

무엇을 약을 여러 개 먹을 때 "이거 같이 먹어도 되나"를, 약 상자에 적힌 이름만으로 확인해요
어디서 토스 앱 내 미니앱 (화면 하나짜리 앱)
누구를 위해 혈압약·당뇨약에 감기약·진통제가 얹히는, 매일 여러 알을 챙겨 먹는 사람
누가 1인 기획·개발 (프론트엔드 + 공공 API 실측 검증 + 심사/정책 대응)

✨ 핵심 기능

약 검색 제품명 두 글자 이상이면 타이핑이 멈춘 뒤 검색 · 업체명과 전문/일반 구분까지 표시
병용금기 확인 담은 약(최대 10개)의 모든 짝을 확인하고, 금기면 식약처가 적어 둔 금기 사유를 그대로 보여줘요
내 약 · 최근 담은 약 별표로 고정하는 즐겨찾기, 최근 담은 약 12개까지 자동 기록 · 기록 지우기 버튼 상시 노출
조합 저장 "타이레놀정500밀리그람 외 2개" 처럼 이름을 자동으로 지어 저장 · 한 번 눌러 다시 담기

🩺 가장 중요한 판단 — 검색 카탈로그를 바꾼 이유

이 프로젝트에서 제일 크게 갈린 결정입니다. 처음 만든 앱은 사실상 항상 "문제 없어요" 라고 답하고 있었습니다.

처음에는 약 검색을 e약은요(getDrbEasyDrugList) 로 했습니다. 이름·효능·복용법이 사람이 읽기 좋게 정리돼 있어 자연스러운 선택이었어요. 그런데 이 카탈로그는 품목이 4,772개뿐이고 대부분 일반의약품입니다. 실제로 여기서 검색되는 약 2,100개를 훑어 병용금기를 찾아봤더니 결과는 0건이었습니다.

병용금기는 대부분 전문의약품 사이에서 걸립니다. 즉 사용자가 e약은요에서 고를 수 있는 약들끼리는 애초에 부딪힐 일이 거의 없고, 앱은 무엇을 담든 초록색 화면을 띄웁니다. 틀린 답을 주는 것보다, 항상 안심시켜 주는 것이 더 위험합니다. "앱이 괜찮다고 했는데" 가 성립해버리니까요.

그래서 검색 카탈로그를 DUR 품목정보(getDurPrdlstInfoList03) 로 바꿨습니다. 이 목록에 있는 약은 전부 DUR 판정 대상이라, 검색해서 담은 약이 그대로 금기 판정으로 이어집니다. 검색과 확인이 같은 데이터 위에서 도는 구조가 된 것이죠.

대신 잃은 것도 분명히 적어둡니다 — DUR 목록에는 효능·복용법 같은 설명이 없습니다. e약은요는 그 상세정보용 자리(getDrugDetail)로만 남겨뒀고, 카탈로그가 작아 없는 약이 많아서 없으면 그냥 안 보여주는 쪽으로 처리합니다.

허가취소 품목은 담지 않습니다. DUR 목록에는 허가가 취소된 품목도 함께 옵니다. CANCEL_NAME 이 "정상" 이 아닌 항목은 검색 결과에서 뺍니다 — 지금 살 수도, 먹을 수도 없는 약을 담게 하면 확인 자체가 헛돌아요.


⚠️ 공공 API 의 함정 두 가지

둘 다 문서만 읽어서는 안 나오고, 실제로 불러봐야 드러났습니다.

(a) numOfRows 최대는 1000 이 아니라 500 입니다. 1000을 넣으면 데이터가 잘리는 게 아니라 API 가 에러를 돌려줍니다. 병용금기 목록은 약 하나에도 수백 건이 걸리므로 한 번에 다 받을 수 없고, totalCount 로 페이지 수를 계산해 끝까지 이어받습니다. 금기를 한 건이라도 놓치면 사람이 다치는 데이터라, 여기서만큼은 "첫 페이지만" 이 허용되지 않습니다.

(b) 결과가 0건이면 응답에 items 키가 아예 없습니다. body.items 가 없는 것을 오류로 판정했더니, "금기 없음" 이라는 정상 케이스마다 오류 화면이 떴습니다. 가장 흔하게 나오는 정답이 통째로 에러가 되는 셈이죠. 그래서 body 자체가 없을 때만 오류로 보고, items 가 없으면 빈 목록으로 다룹니다.

if (json.body == null) throw new Error("약 정보를 불러오지 못했어요");
return { items: json.body.items ?? [], totalCount: Number(json.body.totalCount ?? 0) };

그 밖에 실측으로 정리한 것들:

  • 금기 관계는 데이터에 양방향으로 다 들어 있습니다. A→B 와 B→A 가 모두 있어서, 담은 약마다 한 번씩만 조회해도 짝을 놓치지 않습니다.
  • 여러 약을 동시에 조회하지 않고 하나씩 순서대로 부릅니다. 공공데이터포털이 동시 요청을 잘 버티지 못했습니다.
  • 같은 품목이 중복으로 내려옵니다. 품목기준코드(itemSeq)로 걸러냅니다.
  • 한 조합에 사유가 여러 개 걸려도 경고는 한 번만 띄웁니다. 같은 경고를 세 번 보여주면 사용자는 셋 다 무시하게 됩니다.

🔒 약 이름은 우리 서버로 보내지 않습니다

무슨 약을 먹는지는 그 사람의 병을 그대로 드러내는 정보입니다. 그래서 설계 원칙으로 못 박았습니다.

  • 자체 서버가 없습니다. 브라우저가 식약처 공공 API 를 직접 부릅니다. 검색어와 품목코드는 공공데이터포털로만 나가고, 중간에 우리 서버를 거치지 않습니다 — 애초에 보낼 곳이 없습니다.
  • 최근 담은 약 · 즐겨찾기 · 저장한 조합은 전부 기기 localStorage(pill-check: 네임스페이스)에만 둡니다. 계정도 로그인도 없습니다.
  • 권한을 하나도 요구하지 않습니다. permissions: [] — 위치·연락처·카메라 어느 것도 받지 않습니다.
  • 기록 지우기 버튼을 상시 노출합니다. 남의 폰을 잠깐 빌려 쓸 수도 있으니, 지우는 길이 항상 화면에 보여야 합니다.
  • 시크릿 모드처럼 저장이 막힌 환경에서는 저장만 조용히 실패하고 앱은 계속 동작합니다.

공공데이터포털 인증키는 브라우저에서 직접 호출하는 구조상 번들에 그대로 들어갑니다. 무료·공개 데이터라 감수한 절충이고, 결제·개인정보처럼 민감한 키에는 이 방식을 쓰면 안 됩니다.


🧺 조합 저장 — 순서와 무관하게 같은 조합

"아침에 먹는 세 알" 같은 묶음은 매번 다시 담기 번거롭습니다. 그래서 조합으로 저장하는데, 여기엔 함정이 하나 있습니다 — 사용자는 같은 약을 매번 다른 순서로 담습니다.

키를 담은 순서 그대로 만들면 A|B 와 B|A 가 다른 조합이 되어, 같은 묶음이 두 개 세 개 쌓입니다. 그래서 키는 품목코드를 정렬해서 만듭니다.

function comboKeyOf(drugs: Drug[]): string {
  return drugs.map((d) => d.seq).sort().join("|");
}

덕분에 순서가 달라도 같은 조합으로 찾아내고(findCombo), 이미 있으면 다시 저장하지 않으며(saveCombo), 지금 담은 약이 저장된 조합과 같으면 버튼이 "저장됨" 으로 바뀝니다.

이름은 autoComboLabel() 이 자동으로 짓습니다. 약 이름은 타이레놀정500밀리그람(아세트아미노펜) 처럼 길어서, 괄호 앞부분만 쓰고 나머지는 개수로 접습니다 — 타이레놀정500밀리그람 외 2개. 40~50대 사용자에게 조합 이름을 타이핑시키지 않기 위한 선택입니다.

npm run check:dur 이 이 로직을 네트워크 없이 검사합니다 — 순서 바꿔 저장해도 1개인지, 다른 조합은 따로 쌓이는지, 짝짓기 개수가 맞는지(약 3개 → 짝 3개), 같은 약을 두 번 담으면 하나로 줄어드는지.


🙅 한계와 면책 — 얼버무리지 않기

이 앱은 판단을 대신하지 않습니다. 식약처가 공개한 DUR 병용금기 정보를 그대로 보여줄 뿐이고, 실제로 먹을지 말지는 약사·의사가 정합니다.

특히 이 두 가지는 화면에 고정으로 띄웁니다.

  • "금기 없음" 은 "안전함" 이 아닙니다. DUR 병용금기는 식약처가 등록해 둔 조합의 목록이지, 모든 약물 상호작용의 목록이 아닙니다. 등록되지 않은 상호작용도, 개인의 상태에 따라 달라지는 것도 여기엔 없습니다.
  • 금기가 나와도 스스로 약을 끊지 마세요. 처방받은 약이라면 의사가 이미 알고 검토한 조합일 수 있습니다. 화면은 "끊으세요" 가 아니라 "이 화면을 약사·의사에게 보여주고 상의하세요" 라고 말합니다.

진단·복약 지도·용량 안내는 하지 않고, 앞으로도 넣지 않습니다.


📡 데이터 출처

전부 식품의약품안전처 공공데이터이고, 미리 표로 구워두지 않고 그때그때 직접 부릅니다.

쓰임 엔드포인트
약 검색 (카탈로그) DURPrdlstInfoService03/getDurPrdlstInfoList03
병용금기 조회 DURPrdlstInfoService03/getUsjntTabooInfoList03
상세정보 (효능·복용법·주의사항) DrbEasyDrugInfoService/getDrbEasyDrugList — 검색에는 쓰지 않아요

응답에서 실제로 쓰는 필드는 ITEM_SEQ(품목기준코드) · ITEM_NAME · ENTP_NAME(업체) · ETC_OTC_CODE(전문/일반) · CANCEL_NAME(허가상태), 금기 쪽은 MIXTURE_ITEM_SEQ(부딪히는 상대 약) · PROHBT_CONTENT(금기 사유)입니다.

📺 광고

자리 형태 갱신
화면 하단 고정 문구 강조형 배너 30초
본문 맨 아래 (면책 문구 아래) 이미지 강조형 배너 30초

30초인 이유: 토스 배너는 AdMob 기반이고, AdMob 은 30초보다 잦은 갱신을 무효 트래픽으로 봅니다. 더 짧게 잡으면 수익보다 제재 위험이 커져요. 화면이 가려져 있는 동안(document.hidden)에는 갱신을 멈춥니다 — 노출로 잡히지도 않으면서 호출만 쌓이니까요.

이미지형 자리는 소재가 잘리지 않도록 고정 높이가 아니라 최소 높이로 잡았습니다. 반대로 하단 고정 배너는 고정 높이여야 합니다 — 위로 자라면 본문을 덮습니다. 광고그룹 ID 가 비어 있으면 하단 자리는 아예 만들지 않습니다. 만들어두면 배너 없이 96px 흰 여백만 남아요.

리워드 광고·전면 광고는 넣지 않았습니다. 약을 확인하러 온 사람을 광고로 한 번 막는 흐름은 이 앱에서는 쓰지 않습니다.

🛠 기술 스택

영역 사용 기술
언어 TypeScript 5.7 (strict)
프론트엔드 React 18, Vite 6
플랫폼 SDK @apps-in-toss/web-framework 3.0.2 (WebView 브릿지 · 인앱광고 · 이벤트 로그)
데이터 식약처 공공 API 직접 호출 — 백엔드 없음
상태 화면 로컬 상태 + localStorage
자체 점검 npm run check:dur — 네트워크를 타지 않는 순수 로직 검사

📁 구조

src/
├─ features/home/HomeScreen.tsx   검색 · 담기 · 확인 · 결과 · 면책 (화면 하나)
├─ components/
│  ├─ BannerAd.tsx                배너 2지면 · 30초 갱신 · 화면 가려지면 정지
│  └─ ScreenLayout.tsx            흰 카드 컨테이너
├─ lib/
│  ├─ dur.ts                      검색 · 페이지네이션 · 금기 짝 찾기
│  ├─ pillbox.ts                  최근 · 즐겨찾기 · 조합(정렬 키)
│  ├─ analytics.ts                콘솔 전환 지표 로그
│  └─ env.ts                      광고그룹 ID · 공공데이터 인증키
└─ App.tsx                        셸 · 하단 고정 배너
scripts/
├─ check-dur.ts                   dur.ts · pillbox.ts 자체 점검
└─ shot*.mjs                      puppeteer-core 화면 캡처

🚀 개발

npm install
npm run dev          # 브라우저 (광고는 자리 없이 통과)
npm run check:dur    # 짝짓기 · 중복 제거 · 조합 키 자체 점검
npm run typecheck
npm run lint
npm run build        # vite build + ait build → pill-check.ait
npm run deploy

.env 에 아래 세 값을 넣습니다. 값은 공공데이터포털과 앱인토스 콘솔에서 각자 발급받으세요.

VITE_DATA_KEY=                 # 공공데이터포털 인증키 (없으면 검색이 안 돼요)
VITE_AD_GROUP_ID_BANNER=       # 문구 강조형 (하단 고정)
VITE_AD_GROUP_ID_BANNER_IMAGE= # 이미지 강조형 (본문 하단)

광고그룹 ID 는 비워둬도 앱은 그대로 동작합니다 — 배너 자리만 생기지 않아요. VITE_ 변수는 빌드 시점에 번들로 구워지니 값을 바꾸면 반드시 다시 빌드하세요. 실제 광고는 토스 앱/샌드박스에서만 렌더됩니다.


식품의약품안전처 공개 데이터를 그대로 보여주는 정보 앱입니다. 진단·복약 지도를 하지 않으며, 복용 여부는 반드시 약사·의사와 상의해 정하세요.

About

약궁합 — 같이 먹어도 되는 약인지 확인하는 앱. 식약처 DUR 병용금기 기준으로 조회하고, 약 이름은 서버로 보내지 않습니다 — Apps in Toss WebView 미니앱

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages